diff --git a/app/controllers/public/profile_controller.rb b/app/controllers/public/profile_controller.rb index fca23a7..0e5c83d 100644 --- a/app/controllers/public/profile_controller.rb +++ b/app/controllers/public/profile_controller.rb @@ -19,6 +19,11 @@ class ProfileController < PublicController @network_activities = @profile.tracked_notifications.visible.paginate(:per_page => 15, :page => params[:page]) if @network_activities.empty? @activities = @profile.activities.paginate(:per_page => 15, :page => params[:page]) end + + # TODO Find a way to filter these through sql + @network_activities = filter_private_scraps(@network_activities) + @activities = filter_private_scraps(@activities) + @tags = profile.article_tags allow_access_to_page end @@ -231,6 +236,7 @@ class ProfileController < PublicController @scrap = Scrap.new(params[:scrap]) @scrap.sender= sender @scrap.receiver= receiver + @scrap.marked_people = treat_followed_entries(params[:filter_followed]) @tab_action = params[:tab_action] @message = @scrap.save ? _("Message successfully sent.") : _("You can't leave an empty message.") activities = @profile.activities.paginate(:per_page => 15, :page => params[:page]) if params[:not_load_scraps].nil? @@ -253,6 +259,14 @@ class ProfileController < PublicController end end + def search_followed + result = [] + circles = find_by_contents(:circles, user, user.circles.where(:profile_type => 'Person'), params[:q])[:results] + followed = find_by_contents(:followed, user, Profile.followed_by(user), params[:q])[:results] + result = circles + followed + render :text => prepare_to_token_input_by_class(result).to_json + end + def view_more_activities @activities = @profile.activities.paginate(:per_page => 10, :page => params[:page]) render :partial => 'profile_activities_list', :locals => {:activities => @activities} @@ -434,7 +448,6 @@ class ProfileController < PublicController end end - protected def check_access_to_profile @@ -480,4 +493,41 @@ class ProfileController < PublicController render_not_found unless profile.allow_followers? end + def treat_followed_entries(entries) + return [] if entries.blank? || profile != user + + followed = [] + entries.split(',').map do |entry| + klass, identifier = entry.split('_') + case klass + when 'Person' + followed << Person.find(identifier) + when 'Circle' + circle = Circle.find(identifier) + followed += Profile.in_circle(circle) + end + end + followed.uniq + end + + def filter_private_scraps(activities) + activities = Array(activities) + activities.delete_if do |item| + if item.kind_of?(ProfileActivity) + target = item.activity + owner = profile + else + target = item.target + owner = item.user + end + !environment.admins.include?(user) && + #TODO Consider this if allowing to mark people on organization's wall + #!profile.admins.include?(user) && + owner != user && + target.is_a?(Scrap) && + target.marked_people.present? && + !target.marked_people.include?(user) + end + activities + end end diff --git a/app/helpers/article_helper.rb b/app/helpers/article_helper.rb index f15158a..ff82b2a 100644 --- a/app/helpers/article_helper.rb +++ b/app/helpers/article_helper.rb @@ -160,6 +160,10 @@ module ArticleHelper array.map { |object| {:label => object.name, :value => object.name} } end + def prepare_to_token_input_by_class(array) + array.map { |object| {:id => "#{object.class.name}_#{object.id || object.name}", :name => "#{object.name} (#{_(object.class.name)})", :class => object.class.name}} + end + def cms_label_for_new_children _('New article') end diff --git a/app/helpers/token_helper.rb b/app/helpers/token_helper.rb index 43540b0..008da6b 100644 --- a/app/helpers/token_helper.rb +++ b/app/helpers/token_helper.rb @@ -5,10 +5,11 @@ module TokenHelper end def token_input_field_tag(name, element_id, search_action, options = {}, text_field_options = {}, html_options = {}) - options[:min_chars] ||= 3 + options[:min_chars] ||= 2 options[:hint_text] ||= _("Type in a search term") options[:no_results_text] ||= _("No results") options[:searching_text] ||= _("Searching...") + options[:placeholder] ||= 'null' options[:search_delay] ||= 1000 options[:prevent_duplicates] ||= true options[:backspace_delete_item] ||= false @@ -20,6 +21,9 @@ module TokenHelper options[:on_delete] ||= 'null' options[:on_ready] ||= 'null' options[:query_param] ||= 'q' + options[:theme] ||= 'null' + options[:results_formatter] ||= 'null' + options[:token_formatter] ||= 'null' result = text_field_tag(name, nil, text_field_options.merge(html_options.merge({:id => element_id}))) result += javascript_tag("jQuery('##{element_id}') @@ -29,6 +33,7 @@ module TokenHelper hintText: #{options[:hint_text].to_json}, noResultsText: #{options[:no_results_text].to_json}, searchingText: #{options[:searching_text].to_json}, + placeholder: #{options[:placeholder].to_json}, searchDelay: #{options[:search_delay].to_json}, preventDuplicates: #{options[:prevent_duplicates].to_json}, backspaceDeleteItem: #{options[:backspace_delete_item].to_json}, @@ -39,6 +44,9 @@ module TokenHelper onAdd: #{options[:on_add]}, onDelete: #{options[:on_delete]}, onReady: #{options[:on_ready]}, + theme: #{options[:theme] == 'null' ? options[:theme] : options[:theme].to_json}, + resultsFormater: #{options[:results_formatter]}, + tokenFormater: #{options[:token_formatter]}, }); ") result += javascript_tag("jQuery('##{element_id}').focus();") if options[:focus] diff --git a/app/jobs/notify_activity_to_profiles_job.rb b/app/jobs/notify_activity_to_profiles_job.rb index 42d6f88..98a94ee 100644 --- a/app/jobs/notify_activity_to_profiles_job.rb +++ b/app/jobs/notify_activity_to_profiles_job.rb @@ -19,8 +19,13 @@ class NotifyActivityToProfilesJob < Struct.new(:tracked_action_id) # Notify the user ActionTrackerNotification.create(:profile_id => tracked_action.user.id, :action_tracker_id => tracked_action.id) - # Notify all followers - ActionTrackerNotification.connection.execute("INSERT INTO action_tracker_notifications(profile_id, action_tracker_id) SELECT DISTINCT c.person_id, #{tracked_action.id} FROM profiles_circles AS p JOIN circles as c ON c.id = p.circle_id WHERE p.profile_id = #{tracked_action.user.id} AND (c.person_id NOT IN (SELECT atn.profile_id FROM action_tracker_notifications AS atn WHERE atn.action_tracker_id = #{tracked_action.id}))") + if target.is_a?(Scrap) && target.marked_people.present? + # Notify only marked people + ActionTrackerNotification.connection.execute("INSERT INTO action_tracker_notifications(profile_id, action_tracker_id) SELECT DISTINCT profiles.id, #{tracked_action.id} FROM profiles WHERE profiles.id IN (#{target.marked_people.map(&:id).join(',')})") + else + # Notify all followers + ActionTrackerNotification.connection.execute("INSERT INTO action_tracker_notifications(profile_id, action_tracker_id) SELECT DISTINCT c.person_id, #{tracked_action.id} FROM profiles_circles AS p JOIN circles as c ON c.id = p.circle_id WHERE p.profile_id = #{tracked_action.user.id} AND (c.person_id NOT IN (SELECT atn.profile_id FROM action_tracker_notifications AS atn WHERE atn.action_tracker_id = #{tracked_action.id}))") + end if tracked_action.user.is_a? Organization ActionTrackerNotification.connection.execute "insert into action_tracker_notifications(profile_id, action_tracker_id) " + diff --git a/app/models/circle.rb b/app/models/circle.rb index d5e5478..ee4c897 100644 --- a/app/models/circle.rb +++ b/app/models/circle.rb @@ -1,4 +1,10 @@ class Circle < ApplicationRecord + SEARCHABLE_FIELDS = { + :name => {:label => _('Name'), :weight => 1} + } + + _('Circle') + has_many :profile_followers belongs_to :person diff --git a/app/models/person.rb b/app/models/person.rb index 828b536..a68fa4e 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -121,6 +121,8 @@ class Person < Profile where 'profile_suggestions.suggestion_type = ? AND profile_suggestions.enabled = ?', 'Community', true }, through: :suggested_profiles, source: :suggestion + has_and_belongs_to_many :marked_scraps, :join_table => :private_scraps, :class_name => 'Scrap' + scope :more_popular, -> { order 'friends_count DESC' } scope :abusers, -> { diff --git a/app/models/scrap.rb b/app/models/scrap.rb index 3cc869a..a5a4a62 100644 --- a/app/models/scrap.rb +++ b/app/models/scrap.rb @@ -2,7 +2,7 @@ class Scrap < ApplicationRecord include SanitizeHelper - attr_accessible :content, :sender_id, :receiver_id, :scrap_id + attr_accessible :content, :sender_id, :receiver_id, :scrap_id, :marked_people SEARCHABLE_FIELDS = { :content => {:label => _('Content'), :weight => 1}, @@ -19,6 +19,8 @@ class Scrap < ApplicationRecord where profile_activities: {activity_type: 'Scrap'} }, foreign_key: :activity_id, dependent: :destroy + has_and_belongs_to_many :marked_people, :join_table => :private_scraps, :class_name => 'Person' + after_create :create_activity after_update :update_activity diff --git a/app/views/profile/_profile_wall.html.erb b/app/views/profile/_profile_wall.html.erb index 831347a..a5c9501 100644 --- a/app/views/profile/_profile_wall.html.erb +++ b/app/views/profile/_profile_wall.html.erb @@ -1,8 +1,11 @@

<%= _("%s's wall") % @profile.name %>

<%= flash[:error] %> - <%= form_remote_tag :url => {:controller => 'profile', :action => 'leave_scrap', :tab_action => 'wall' }, :update => 'profile_activities', :success => "jQuery('#leave_scrap_content').val('')", :complete => "jQuery('#leave_scrap_form').removeClass('loading').find('*').attr('disabled', false)", :loading => "jQuery('#leave_scrap_form').addClass('loading').find('*').attr('disabled', true)", :html => {:id => 'leave_scrap_form' } do %> - <%= limited_text_area :scrap, :content, 420, 'leave_scrap_content', :cols => 50, :rows => 2, :class => 'autogrow' %> + <%= form_remote_tag :url => {:controller => 'profile', :action => 'leave_scrap', :tab_action => 'wall' }, :update => 'profile_activities', :success => "jQuery('#leave_scrap_content').val(''); jQuery('#filter-followed').tokenInput('clear')", :complete => "jQuery('#leave_scrap_form').removeClass('loading').find('*').attr('disabled', false)", :loading => "jQuery('#leave_scrap_form').addClass('loading').find('*').attr('disabled', true)", :html => {:id => 'leave_scrap_form' } do %> + <%= limited_text_area :scrap, :content, 420, 'leave_scrap_content', :rows => 2, :class => 'autogrow' %> + <% if profile == user %> + <%= token_input_field_tag(:filter_followed, 'filter-followed', {:action => 'search_followed'}, {:theme => 'facebook', :placeholder => _('Filter followed, friends or group of friends to send them a private scrap...')}) %> + <% end %> <%= submit_button :new, _('Share') %> <% end %>
diff --git a/db/migrate/20160705162914_create_private_scraps.rb b/db/migrate/20160705162914_create_private_scraps.rb new file mode 100644 index 0000000..cfa2154 --- /dev/null +++ b/db/migrate/20160705162914_create_private_scraps.rb @@ -0,0 +1,8 @@ +class CreatePrivateScraps < ActiveRecord::Migration + def change + create_table :private_scraps do |t| + t.references :person + t.references :scrap + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 5019b82..a97d4ac 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160608123748) do +ActiveRecord::Schema.define(version: 20160705162914) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -524,6 +524,11 @@ ActiveRecord::Schema.define(version: 20160608123748) do t.datetime "updated_at" end + create_table "private_scraps", force: :cascade do |t| + t.integer "person_id" + t.integer "scrap_id" + end + create_table "product_qualifiers", force: :cascade do |t| t.integer "product_id" t.integer "qualifier_id" diff --git a/public/javascripts/jquery.tokeninput.js b/public/javascripts/jquery.tokeninput.js index 548ec70..4b69d82 100644 --- a/public/javascripts/jquery.tokeninput.js +++ b/public/javascripts/jquery.tokeninput.js @@ -1,208 +1,293 @@ /* * jQuery Plugin: Tokenizing Autocomplete Text Entry - * Version 1.5.0 - * Requires jQuery 1.6+ + * Version 1.6.2 * * Copyright (c) 2009 James Smith (http://loopj.com) * Licensed jointly under the GPL and MIT licenses, * choose which one suits your project best! * */ - -(function ($) { -// Default settings -var DEFAULT_SETTINGS = { - hintText: "Type in a search term", - noResultsText: "No results", - searchingText: "Searching...", - deleteText: "×", +;(function ($) { + var DEFAULT_SETTINGS = { + // Search settings + method: "GET", + queryParam: "q", searchDelay: 300, minChars: 1, - permanentDropdown: false, - showAllResults: false, - tokenLimit: null, + propertyToSearch: "name", jsonContainer: null, - method: "GET", contentType: "json", - queryParam: "q", - tokenDelimiter: ",", - preventDuplicates: false, + excludeCurrent: false, + excludeCurrentParameter: "x", + + // Prepopulation settings prePopulate: null, processPrePopulate: false, + + // Display settings + hintText: "Type in a search term", + noResultsText: "No results", + searchingText: "Searching...", + deleteText: "×", animateDropdown: true, - dontAdd: false, + placeholder: null, + theme: null, + zindex: 999, + resultsLimit: null, + + enableHTML: false, + + resultsFormatter: function(item) { + var string = item[this.propertyToSearch]; + return "
  • " + (this.enableHTML ? string : _escapeHTML(string)) + "
  • "; + }, + + tokenFormatter: function(item) { + var string = item[this.propertyToSearch]; + return "
  • " + (this.enableHTML ? string : _escapeHTML(string)) + "

  • "; + }, + + // Tokenization settings + tokenLimit: null, + tokenDelimiter: ",", + preventDuplicates: false, + tokenValue: "id", + + // Behavioral settings + allowFreeTagging: false, + allowTabOut: false, + autoSelectFirstResult: false, + + // Callbacks onResult: null, + onCachedResult: null, onAdd: null, + onFreeTaggingAdd: null, onDelete: null, + onReady: null, + + // Other settings idPrefix: "token-input-", - zindex: 999, - backspaceDeleteItem: true -}; - -// Default classes to use when theming -var DEFAULT_CLASSES = { - tokenList: "token-input-list", - token: "token-input-token", - tokenDelete: "token-input-delete-token", - selectedToken: "token-input-selected-token", - highlightedToken: "token-input-highlighted-token", - dropdown: "token-input-dropdown", - dropdownItem: "token-input-dropdown-item", - dropdownItem2: "token-input-dropdown-item2", - selectedDropdownItem: "token-input-selected-dropdown-item", - inputToken: "token-input-input-token", - blurText: "token-input-blur-text", -}; - -// Input box position "enum" -var POSITION = { - BEFORE: 0, - AFTER: 1, - END: 2 -}; - -// Keys "enum" -var KEY = { - BACKSPACE: 8, - DELETE: 46, - TAB: 9, - ENTER: 13, - ESCAPE: 27, - SPACE: 32, - PAGE_UP: 33, - PAGE_DOWN: 34, - END: 35, - HOME: 36, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - NUMPAD_ENTER: 108, - COMMA: 188 -}; - -// Additional public (exposed) methods -var methods = { - init: function(url_or_data_or_function, options) { - return this.each(function () { - $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, options)); - }); - }, - clear: function() { - this.data("tokenInputObject").clear(); - return this; - }, - add: function(item) { - this.data("tokenInputObject").add(item); - return this; - }, - remove: function(item) { - this.data("tokenInputObject").remove(item); - return this; - } -} - -// Expose the .tokenInput function to jQuery as a plugin -$.fn.tokenInput = function (method) { - // Method calling and initialization logic - if(methods[method]) { - return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); - } else { - return methods.init.apply(this, arguments); - } -}; - -// TokenList class for each input -$.TokenList = function (input, url_or_data, options) { - // - // Initialization - // - var settings = $.extend({}, DEFAULT_SETTINGS, options || {}); - - // Configure the data source - if(typeof(url_or_data) === "string") { - // Set the url to query against - settings.url = url_or_data; - - // Make a smart guess about cross-domain if it wasn't explicitly specified - if(settings.crossDomain === undefined) { - if(settings.url.indexOf("://") === -1) { - settings.crossDomain = false; - } else { - settings.crossDomain = (location.href.split(/\/+/g)[1] !== settings.url.split(/\/+/g)[1]); - } + + // Keep track if the input is currently in disabled mode + disabled: false + }; + + // Default classes to use when theming + var DEFAULT_CLASSES = { + tokenList : "token-input-list", + token : "token-input-token", + tokenReadOnly : "token-input-token-readonly", + tokenDelete : "token-input-delete-token", + selectedToken : "token-input-selected-token", + highlightedToken : "token-input-highlighted-token", + dropdown : "token-input-dropdown", + dropdownItem : "token-input-dropdown-item", + dropdownItem2 : "token-input-dropdown-item2", + selectedDropdownItem : "token-input-selected-dropdown-item", + inputToken : "token-input-input-token", + focused : "token-input-focused", + disabled : "token-input-disabled" + }; + + // Input box position "enum" + var POSITION = { + BEFORE : 0, + AFTER : 1, + END : 2 + }; + + // Keys "enum" + var KEY = { + BACKSPACE : 8, + TAB : 9, + ENTER : 13, + ESCAPE : 27, + SPACE : 32, + PAGE_UP : 33, + PAGE_DOWN : 34, + END : 35, + HOME : 36, + LEFT : 37, + UP : 38, + RIGHT : 39, + DOWN : 40, + NUMPAD_ENTER : 108, + COMMA : 188 + }; + + var HTML_ESCAPES = { + '&' : '&', + '<' : '<', + '>' : '>', + '"' : '"', + "'" : ''', + '/' : '/' + }; + + var HTML_ESCAPE_CHARS = /[&<>"'\/]/g; + + function coerceToString(val) { + return String((val === null || val === undefined) ? '' : val); + } + + function _escapeHTML(text) { + return coerceToString(text).replace(HTML_ESCAPE_CHARS, function(match) { + return HTML_ESCAPES[match]; + }); + } + + // Additional public (exposed) methods + var methods = { + init: function(url_or_data_or_function, options) { + var settings = $.extend({}, DEFAULT_SETTINGS, options || {}); + + return this.each(function () { + $(this).data("settings", settings); + $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings)); + }); + }, + clear: function() { + this.data("tokenInputObject").clear(); + return this; + }, + add: function(item) { + this.data("tokenInputObject").add(item); + return this; + }, + remove: function(item) { + this.data("tokenInputObject").remove(item); + return this; + }, + get: function() { + return this.data("tokenInputObject").getTokens(); + }, + toggleDisabled: function(disable) { + this.data("tokenInputObject").toggleDisabled(disable); + return this; + }, + setOptions: function(options){ + $(this).data("settings", $.extend({}, $(this).data("settings"), options || {})); + return this; + }, + destroy: function () { + if (this.data("tokenInputObject")) { + this.data("tokenInputObject").clear(); + var tmpInput = this; + var closest = this.parent(); + closest.empty(); + tmpInput.show(); + closest.append(tmpInput); + return tmpInput; } - } else if(typeof(url_or_data) === "object") { - // Set the local data to search through - settings.local_data = url_or_data; - } - - // Build class names - if(settings.classes) { - // Use custom class names - settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes); - } else if(settings.theme) { - // Use theme-suffixed default class names - settings.classes = {}; - $.each(DEFAULT_CLASSES, function(key, value) { - settings.classes[key] = value + "-" + settings.theme; - }); - } else { - settings.classes = DEFAULT_CLASSES; - } - - - // Save the tokens - var saved_tokens = []; - - // Keep track of the number of tokens in the list - var token_count = 0; - - // Basic cache to save on db hits - var cache = new $.TokenList.Cache(); - - // Keep track of the timeout, old vals - var timeout; - var input_val = ''; - - // Create a new text input an attach keyup events - var input_box = $("") - .css({ - outline: "none" - }) - .attr("id", settings.idPrefix + input.id) - .focus(function () { - if (settings.tokenLimit === null || settings.tokenLimit !== token_count) { - if(settings.permanentDropdown || settings.showAllResults) { - hide_dropdown_hint(); + } + }; + + // Expose the .tokenInput function to jQuery as a plugin + $.fn.tokenInput = function (method) { + // Method calling and initialization logic + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else { + return methods.init.apply(this, arguments); + } + }; + + // TokenList class for each input + $.TokenList = function (input, url_or_data, settings) { + // + // Initialization + // + + // Configure the data source + if (typeof(url_or_data) === "string" || typeof(url_or_data) === "function") { + // Set the url to query against + $(input).data("settings").url = url_or_data; + + // If the URL is a function, evaluate it here to do our initalization work + var url = computeURL(); + + // Make a smart guess about cross-domain if it wasn't explicitly specified + if ($(input).data("settings").crossDomain === undefined && typeof url === "string") { + if(url.indexOf("://") === -1) { + $(input).data("settings").crossDomain = false; + } else { + $(input).data("settings").crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]); + } + } + } else if (typeof(url_or_data) === "object") { + // Set the local data to search through + $(input).data("settings").local_data = url_or_data; + } + + // Build class names + if($(input).data("settings").classes) { + // Use custom class names + $(input).data("settings").classes = $.extend({}, DEFAULT_CLASSES, $(input).data("settings").classes); + } else if($(input).data("settings").theme) { + // Use theme-suffixed default class names + $(input).data("settings").classes = {}; + $.each(DEFAULT_CLASSES, function(key, value) { + $(input).data("settings").classes[key] = value + "-" + $(input).data("settings").theme; + }); + } else { + $(input).data("settings").classes = DEFAULT_CLASSES; + } + + // Save the tokens + var saved_tokens = []; + + // Keep track of the number of tokens in the list + var token_count = 0; + + // Basic cache to save on db hits + var cache = new $.TokenList.Cache(); + + // Keep track of the timeout, old vals + var timeout; + var input_val; + + // Create a new text input an attach keyup events + var input_box = $("") + .css({ + outline: "none" + }) + .attr("id", $(input).data("settings").idPrefix + input.id) + .focus(function () { + if ($(input).data("settings").disabled) { + return false; } else - show_dropdown_hint(); - if (settings.showAllResults) - do_search(); - } - }) - .blur(function () { - if(settings.permanentDropdown) - show_dropdown_hint(); - else { + if ($(input).data("settings").tokenLimit === null || $(input).data("settings").tokenLimit !== token_count) { + show_dropdown_hint(); + } + token_list.addClass($(input).data("settings").classes.focused); + }) + .blur(function () { hide_dropdown(); - } - }) - .bind("keyup keydown blur update", resize_input) - .keydown(function (event) { - var previous_token; - var next_token; - - switch(event.keyCode) { - case KEY.LEFT: - case KEY.RIGHT: - case KEY.UP: - case KEY.DOWN: - if(!$(this).val()) { + + if ($(input).data("settings").allowFreeTagging) { + add_freetagging_tokens(); + } + + $(this).val(""); + token_list.removeClass($(input).data("settings").classes.focused); + }) + .bind("keyup keydown blur update", resize_input) + .keydown(function (event) { + var previous_token; + var next_token; + + switch(event.keyCode) { + case KEY.LEFT: + case KEY.RIGHT: + case KEY.UP: + case KEY.DOWN: + if(this.value.length === 0) { previous_token = input_token.prev(); next_token = input_token.next(); - if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) { + if((previous_token.length && previous_token.get(0) === selected_token) || + (next_token.length && next_token.get(0) === selected_token)) { // Check if there is a previous/next token and it is selected if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) { deselect_token($(selected_token), POSITION.BEFORE); @@ -217,650 +302,805 @@ $.TokenList = function (input, url_or_data, options) { select_token($(next_token.get(0))); } } else { - var dropdown_item = null; - - if (event.keyCode == KEY.LEFT && (this.selectionStart > 0 || this.selectionStart != this.selectionEnd)) - return true; - else if (event.keyCode == KEY.RIGHT && (this.selectionEnd < $(this).val().length || this.selectionStart != this.selectionEnd)) - return true; - else if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) { - dropdown_item = $(selected_dropdown_item).next(); - } else { - dropdown_item = $(selected_dropdown_item).prev(); + var dropdown_item = null; + + if (event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) { + dropdown_item = $(dropdown).find('li').first(); + + if (selected_dropdown_item) { + dropdown_item = $(selected_dropdown_item).next(); } + } else { + dropdown_item = $(dropdown).find('li').last(); - if(dropdown_item.length) { - select_dropdown_item(dropdown_item); + if (selected_dropdown_item) { + dropdown_item = $(selected_dropdown_item).prev(); } - return false; + } + + select_dropdown_item(dropdown_item); } + break; - case KEY.BACKSPACE: - case KEY.DELETE: - previous_token = input_token.prev(); - next_token = input_token.next(); + case KEY.BACKSPACE: + previous_token = input_token.prev(); - if(!$(this).val().length && settings.backspaceDeleteItem) { - if(selected_token) { - delete_token($(selected_token)); - input_box.focus(); - } else if(KEY.DELETE && next_token.length) { - select_token($(next_token.get(0))); - } else if(KEY.BACKSPACE && previous_token.length) { - select_token($(previous_token.get(0))); + if (this.value.length === 0) { + if (selected_token) { + delete_token($(selected_token)); + hiddenInput.change(); + } else if(previous_token.length) { + select_token($(previous_token.get(0))); } return false; - } else if(!settings.permanentDropdown && $(this).val().length === 1) { - hide_dropdown(); + } else if($(this).val().length === 1) { + hide_dropdown(); + } else { + // set a timeout just long enough to let this function finish. + setTimeout(function(){ do_search(); }, 5); + } + break; + + case KEY.TAB: + case KEY.ENTER: + case KEY.NUMPAD_ENTER: + case KEY.COMMA: + if(selected_dropdown_item) { + add_token($(selected_dropdown_item).data("tokeninput")); + hiddenInput.change(); } else { - // set a timeout just long enough to let this function finish. - setTimeout(function(){do_search();}, 5); + if ($(input).data("settings").allowFreeTagging) { + if($(input).data("settings").allowTabOut && $(this).val() === "") { + return true; + } else { + add_freetagging_tokens(); + } + } else { + $(this).val(""); + if($(input).data("settings").allowTabOut) { + return true; + } + } + event.stopPropagation(); + event.preventDefault(); } - break; - - case KEY.TAB: - case KEY.ENTER: - case KEY.NUMPAD_ENTER: - case KEY.COMMA: - if(selected_dropdown_item) { - add_token($(selected_dropdown_item).data("tokeninput")); - input_box.focus(); return false; - } - break; - case KEY.ESCAPE: - hide_dropdown(); - return true; + case KEY.ESCAPE: + hide_dropdown(); + return true; - default: - if(String.fromCharCode(event.which)) { - // set a timeout just long enough to let this function finish. - setTimeout(function(){do_search();}, 5); + default: + if (String.fromCharCode(event.which)) { + // set a timeout just long enough to let this function finish. + setTimeout(function(){ do_search(); }, 5); } break; - } - }); - - // Keep a reference to the original input box - var hidden_input = $(input) - .hide() - .val("") - .focus(function () { - input_box.focus(); - }) - .blur(function () { - input_box.blur(); - }); - - // Keep a reference to the selected token and dropdown item - var selected_token = null; - var selected_token_index = 0; - var selected_dropdown_item = null; - - // The list to store the token items in - var token_list = $("