Commit 988d1e617dbd7f95de6bb822f95a2a7a8663fb5e
Committed by
Marcos Pereira
1 parent
020591c2
Exists in
staging
and in
5 other branches
private-scraps: send scrap only to marked people
(cherry picked from commit fca616032cf3c305806a8a121639ea6dcb90fd93)
Showing
16 changed files
with
1352 additions
and
778 deletions
Show diff stats
app/controllers/public/profile_controller.rb
... | ... | @@ -19,6 +19,11 @@ class ProfileController < PublicController |
19 | 19 | @network_activities = @profile.tracked_notifications.visible.paginate(:per_page => 15, :page => params[:page]) if @network_activities.empty? |
20 | 20 | @activities = @profile.activities.paginate(:per_page => 15, :page => params[:page]) |
21 | 21 | end |
22 | + | |
23 | + # TODO Find a way to filter these through sql | |
24 | + @network_activities = filter_private_scraps(@network_activities) | |
1 |
|
|
25 | + @activities = filter_private_scraps(@activities) | |
26 | + | |
22 | 27 | @tags = profile.article_tags |
23 | 28 | allow_access_to_page |
24 | 29 | end |
... | ... | @@ -231,6 +236,7 @@ class ProfileController < PublicController |
231 | 236 | @scrap = Scrap.new(params[:scrap]) |
232 | 237 | @scrap.sender= sender |
233 | 238 | @scrap.receiver= receiver |
239 | + @scrap.marked_people = treat_followed_entries(params[:filter_followed]) | |
234 | 240 | @tab_action = params[:tab_action] |
235 | 241 | @message = @scrap.save ? _("Message successfully sent.") : _("You can't leave an empty message.") |
236 | 242 | activities = @profile.activities.paginate(:per_page => 15, :page => params[:page]) if params[:not_load_scraps].nil? |
... | ... | @@ -253,6 +259,14 @@ class ProfileController < PublicController |
253 | 259 | end |
254 | 260 | end |
255 | 261 | |
262 | + def search_followed | |
263 | + result = [] | |
264 | + circles = find_by_contents(:circles, user, user.circles.where(:profile_type => 'Person'), params[:q])[:results] | |
265 | + followed = find_by_contents(:followed, user, Profile.followed_by(user), params[:q])[:results] | |
266 | + result = circles + followed | |
267 | + render :text => prepare_to_token_input_by_class(result).to_json | |
268 | + end | |
269 | + | |
256 | 270 | def view_more_activities |
257 | 271 | @activities = @profile.activities.paginate(:per_page => 10, :page => params[:page]) |
258 | 272 | render :partial => 'profile_activities_list', :locals => {:activities => @activities} |
... | ... | @@ -434,7 +448,6 @@ class ProfileController < PublicController |
434 | 448 | end |
435 | 449 | end |
436 | 450 | |
437 | - | |
438 | 451 | protected |
439 | 452 | |
440 | 453 | def check_access_to_profile |
... | ... | @@ -480,4 +493,41 @@ class ProfileController < PublicController |
480 | 493 | render_not_found unless profile.allow_followers? |
481 | 494 | end |
482 | 495 | |
496 | + def treat_followed_entries(entries) | |
497 | + return [] if entries.blank? || profile != user | |
498 | + | |
499 | + followed = [] | |
500 | + entries.split(',').map do |entry| | |
501 | + klass, identifier = entry.split('_') | |
502 | + case klass | |
503 | + when 'Person' | |
504 | + followed << Person.find(identifier) | |
505 | + when 'Circle' | |
506 | + circle = Circle.find(identifier) | |
507 | + followed += Profile.in_circle(circle) | |
508 | + end | |
509 | + end | |
510 | + followed.uniq | |
511 | + end | |
512 | + | |
513 | + def filter_private_scraps(activities) | |
514 | + activities = Array(activities) | |
515 | + activities.delete_if do |item| | |
516 | + if item.kind_of?(ProfileActivity) | |
517 | + target = item.activity | |
518 | + owner = profile | |
519 | + else | |
520 | + target = item.target | |
521 | + owner = item.user | |
522 | + end | |
523 | + !environment.admins.include?(user) && | |
524 | + #TODO Consider this if allowing to mark people on organization's wall | |
525 | + #!profile.admins.include?(user) && | |
526 | + owner != user && | |
527 | + target.is_a?(Scrap) && | |
528 | + target.marked_people.present? && | |
529 | + !target.marked_people.include?(user) | |
530 | + end | |
531 | + activities | |
532 | + end | |
483 | 533 | end | ... | ... |
app/helpers/article_helper.rb
... | ... | @@ -160,6 +160,10 @@ module ArticleHelper |
160 | 160 | array.map { |object| {:label => object.name, :value => object.name} } |
161 | 161 | end |
162 | 162 | |
163 | + def prepare_to_token_input_by_class(array) | |
164 | + array.map { |object| {:id => "#{object.class.name}_#{object.id || object.name}", :name => "#{object.name} (#{_(object.class.name)})", :class => object.class.name}} | |
165 | + end | |
166 | + | |
163 | 167 | def cms_label_for_new_children |
164 | 168 | _('New article') |
165 | 169 | end | ... | ... |
app/helpers/token_helper.rb
... | ... | @@ -5,10 +5,11 @@ module TokenHelper |
5 | 5 | end |
6 | 6 | |
7 | 7 | def token_input_field_tag(name, element_id, search_action, options = {}, text_field_options = {}, html_options = {}) |
8 | - options[:min_chars] ||= 3 | |
8 | + options[:min_chars] ||= 2 | |
9 | 9 | options[:hint_text] ||= _("Type in a search term") |
10 | 10 | options[:no_results_text] ||= _("No results") |
11 | 11 | options[:searching_text] ||= _("Searching...") |
12 | + options[:placeholder] ||= 'null' | |
12 | 13 | options[:search_delay] ||= 1000 |
13 | 14 | options[:prevent_duplicates] ||= true |
14 | 15 | options[:backspace_delete_item] ||= false |
... | ... | @@ -20,6 +21,9 @@ module TokenHelper |
20 | 21 | options[:on_delete] ||= 'null' |
21 | 22 | options[:on_ready] ||= 'null' |
22 | 23 | options[:query_param] ||= 'q' |
24 | + options[:theme] ||= 'null' | |
25 | + options[:results_formatter] ||= 'null' | |
26 | + options[:token_formatter] ||= 'null' | |
23 | 27 | |
24 | 28 | result = text_field_tag(name, nil, text_field_options.merge(html_options.merge({:id => element_id}))) |
25 | 29 | result += javascript_tag("jQuery('##{element_id}') |
... | ... | @@ -29,6 +33,7 @@ module TokenHelper |
29 | 33 | hintText: #{options[:hint_text].to_json}, |
30 | 34 | noResultsText: #{options[:no_results_text].to_json}, |
31 | 35 | searchingText: #{options[:searching_text].to_json}, |
36 | + placeholder: #{options[:placeholder].to_json}, | |
32 | 37 | searchDelay: #{options[:search_delay].to_json}, |
33 | 38 | preventDuplicates: #{options[:prevent_duplicates].to_json}, |
34 | 39 | backspaceDeleteItem: #{options[:backspace_delete_item].to_json}, |
... | ... | @@ -39,6 +44,9 @@ module TokenHelper |
39 | 44 | onAdd: #{options[:on_add]}, |
40 | 45 | onDelete: #{options[:on_delete]}, |
41 | 46 | onReady: #{options[:on_ready]}, |
47 | + theme: #{options[:theme] == 'null' ? options[:theme] : options[:theme].to_json}, | |
48 | + resultsFormater: #{options[:results_formatter]}, | |
49 | + tokenFormater: #{options[:token_formatter]}, | |
42 | 50 | }); |
43 | 51 | ") |
44 | 52 | result += javascript_tag("jQuery('##{element_id}').focus();") if options[:focus] | ... | ... |
app/jobs/notify_activity_to_profiles_job.rb
... | ... | @@ -19,8 +19,13 @@ class NotifyActivityToProfilesJob < Struct.new(:tracked_action_id) |
19 | 19 | # Notify the user |
20 | 20 | ActionTrackerNotification.create(:profile_id => tracked_action.user.id, :action_tracker_id => tracked_action.id) |
21 | 21 | |
22 | - # Notify all followers | |
23 | - 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}))") | |
22 | + if target.is_a?(Scrap) && target.marked_people.present? | |
23 | + # Notify only marked people | |
24 | + 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(',')})") | |
25 | + else | |
26 | + # Notify all followers | |
27 | + 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}))") | |
28 | + end | |
24 | 29 | |
25 | 30 | if tracked_action.user.is_a? Organization |
26 | 31 | ActionTrackerNotification.connection.execute "insert into action_tracker_notifications(profile_id, action_tracker_id) " + | ... | ... |
app/models/circle.rb
app/models/person.rb
... | ... | @@ -121,6 +121,8 @@ class Person < Profile |
121 | 121 | where 'profile_suggestions.suggestion_type = ? AND profile_suggestions.enabled = ?', 'Community', true |
122 | 122 | }, through: :suggested_profiles, source: :suggestion |
123 | 123 | |
124 | + has_and_belongs_to_many :marked_scraps, :join_table => :private_scraps, :class_name => 'Scrap' | |
125 | + | |
124 | 126 | scope :more_popular, -> { order 'friends_count DESC' } |
125 | 127 | |
126 | 128 | scope :abusers, -> { | ... | ... |
app/models/scrap.rb
... | ... | @@ -2,7 +2,7 @@ class Scrap < ApplicationRecord |
2 | 2 | |
3 | 3 | include SanitizeHelper |
4 | 4 | |
5 | - attr_accessible :content, :sender_id, :receiver_id, :scrap_id | |
5 | + attr_accessible :content, :sender_id, :receiver_id, :scrap_id, :marked_people | |
6 | 6 | |
7 | 7 | SEARCHABLE_FIELDS = { |
8 | 8 | :content => {:label => _('Content'), :weight => 1}, |
... | ... | @@ -19,6 +19,8 @@ class Scrap < ApplicationRecord |
19 | 19 | where profile_activities: {activity_type: 'Scrap'} |
20 | 20 | }, foreign_key: :activity_id, dependent: :destroy |
21 | 21 | |
22 | + has_and_belongs_to_many :marked_people, :join_table => :private_scraps, :class_name => 'Person' | |
23 | + | |
22 | 24 | after_create :create_activity |
23 | 25 | after_update :update_activity |
24 | 26 | ... | ... |
app/views/profile/_profile_wall.html.erb
1 | 1 | <h3><%= _("%s's wall") % @profile.name %></h3> |
2 | 2 | <div id='leave_scrap'> |
3 | 3 | <%= flash[:error] %> |
4 | - <%= 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 %> | |
5 | - <%= limited_text_area :scrap, :content, 420, 'leave_scrap_content', :cols => 50, :rows => 2, :class => 'autogrow' %> | |
4 | + <%= 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 %> | |
5 | + <%= limited_text_area :scrap, :content, 420, 'leave_scrap_content', :rows => 2, :class => 'autogrow' %> | |
6 | + <% if profile == user %> | |
7 | + <%= 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...')}) %> | |
8 | + <% end %> | |
6 | 9 | <%= submit_button :new, _('Share') %> |
7 | 10 | <% end %> |
8 | 11 | </div> | ... | ... |
db/schema.rb
... | ... | @@ -11,7 +11,7 @@ |
11 | 11 | # |
12 | 12 | # It's strongly recommended that you check this file into your version control system. |
13 | 13 | |
14 | -ActiveRecord::Schema.define(version: 20160608123748) do | |
14 | +ActiveRecord::Schema.define(version: 20160705162914) do | |
15 | 15 | |
16 | 16 | # These are extensions that must be enabled in order to support this database |
17 | 17 | enable_extension "plpgsql" |
... | ... | @@ -524,6 +524,11 @@ ActiveRecord::Schema.define(version: 20160608123748) do |
524 | 524 | t.datetime "updated_at" |
525 | 525 | end |
526 | 526 | |
527 | + create_table "private_scraps", force: :cascade do |t| | |
528 | + t.integer "person_id" | |
529 | + t.integer "scrap_id" | |
530 | + end | |
531 | + | |
527 | 532 | create_table "product_qualifiers", force: :cascade do |t| |
528 | 533 | t.integer "product_id" |
529 | 534 | t.integer "qualifier_id" | ... | ... |
public/javascripts/jquery.tokeninput.js
1 | 1 | /* |
2 | 2 | * jQuery Plugin: Tokenizing Autocomplete Text Entry |
3 | - * Version 1.5.0 | |
4 | - * Requires jQuery 1.6+ | |
3 | + * Version 1.6.2 | |
5 | 4 | * |
6 | 5 | * Copyright (c) 2009 James Smith (http://loopj.com) |
7 | 6 | * Licensed jointly under the GPL and MIT licenses, |
8 | 7 | * choose which one suits your project best! |
9 | 8 | * |
10 | 9 | */ |
11 | - | |
12 | -(function ($) { | |
13 | -// Default settings | |
14 | -var DEFAULT_SETTINGS = { | |
15 | - hintText: "Type in a search term", | |
16 | - noResultsText: "No results", | |
17 | - searchingText: "Searching...", | |
18 | - deleteText: "×", | |
10 | +;(function ($) { | |
11 | + var DEFAULT_SETTINGS = { | |
12 | + // Search settings | |
13 | + method: "GET", | |
14 | + queryParam: "q", | |
19 | 15 | searchDelay: 300, |
20 | 16 | minChars: 1, |
21 | - permanentDropdown: false, | |
22 | - showAllResults: false, | |
23 | - tokenLimit: null, | |
17 | + propertyToSearch: "name", | |
24 | 18 | jsonContainer: null, |
25 | - method: "GET", | |
26 | 19 | contentType: "json", |
27 | - queryParam: "q", | |
28 | - tokenDelimiter: ",", | |
29 | - preventDuplicates: false, | |
20 | + excludeCurrent: false, | |
21 | + excludeCurrentParameter: "x", | |
22 | + | |
23 | + // Prepopulation settings | |
30 | 24 | prePopulate: null, |
31 | 25 | processPrePopulate: false, |
26 | + | |
27 | + // Display settings | |
28 | + hintText: "Type in a search term", | |
29 | + noResultsText: "No results", | |
30 | + searchingText: "Searching...", | |
31 | + deleteText: "×", | |
32 | 32 | animateDropdown: true, |
33 | - dontAdd: false, | |
33 | + placeholder: null, | |
34 | + theme: null, | |
35 | + zindex: 999, | |
36 | + resultsLimit: null, | |
37 | + | |
38 | + enableHTML: false, | |
39 | + | |
40 | + resultsFormatter: function(item) { | |
41 | + var string = item[this.propertyToSearch]; | |
42 | + return "<li>" + (this.enableHTML ? string : _escapeHTML(string)) + "</li>"; | |
43 | + }, | |
44 | + | |
45 | + tokenFormatter: function(item) { | |
46 | + var string = item[this.propertyToSearch]; | |
47 | + return "<li><p>" + (this.enableHTML ? string : _escapeHTML(string)) + "</p></li>"; | |
48 | + }, | |
49 | + | |
50 | + // Tokenization settings | |
51 | + tokenLimit: null, | |
52 | + tokenDelimiter: ",", | |
53 | + preventDuplicates: false, | |
54 | + tokenValue: "id", | |
55 | + | |
56 | + // Behavioral settings | |
57 | + allowFreeTagging: false, | |
58 | + allowTabOut: false, | |
59 | + autoSelectFirstResult: false, | |
60 | + | |
61 | + // Callbacks | |
34 | 62 | onResult: null, |
63 | + onCachedResult: null, | |
35 | 64 | onAdd: null, |
65 | + onFreeTaggingAdd: null, | |
36 | 66 | onDelete: null, |
67 | + onReady: null, | |
68 | + | |
69 | + // Other settings | |
37 | 70 | idPrefix: "token-input-", |
38 | - zindex: 999, | |
39 | - backspaceDeleteItem: true | |
40 | -}; | |
41 | - | |
42 | -// Default classes to use when theming | |
43 | -var DEFAULT_CLASSES = { | |
44 | - tokenList: "token-input-list", | |
45 | - token: "token-input-token", | |
46 | - tokenDelete: "token-input-delete-token", | |
47 | - selectedToken: "token-input-selected-token", | |
48 | - highlightedToken: "token-input-highlighted-token", | |
49 | - dropdown: "token-input-dropdown", | |
50 | - dropdownItem: "token-input-dropdown-item", | |
51 | - dropdownItem2: "token-input-dropdown-item2", | |
52 | - selectedDropdownItem: "token-input-selected-dropdown-item", | |
53 | - inputToken: "token-input-input-token", | |
54 | - blurText: "token-input-blur-text", | |
55 | -}; | |
56 | - | |
57 | -// Input box position "enum" | |
58 | -var POSITION = { | |
59 | - BEFORE: 0, | |
60 | - AFTER: 1, | |
61 | - END: 2 | |
62 | -}; | |
63 | - | |
64 | -// Keys "enum" | |
65 | -var KEY = { | |
66 | - BACKSPACE: 8, | |
67 | - DELETE: 46, | |
68 | - TAB: 9, | |
69 | - ENTER: 13, | |
70 | - ESCAPE: 27, | |
71 | - SPACE: 32, | |
72 | - PAGE_UP: 33, | |
73 | - PAGE_DOWN: 34, | |
74 | - END: 35, | |
75 | - HOME: 36, | |
76 | - LEFT: 37, | |
77 | - UP: 38, | |
78 | - RIGHT: 39, | |
79 | - DOWN: 40, | |
80 | - NUMPAD_ENTER: 108, | |
81 | - COMMA: 188 | |
82 | -}; | |
83 | - | |
84 | -// Additional public (exposed) methods | |
85 | -var methods = { | |
86 | - init: function(url_or_data_or_function, options) { | |
87 | - return this.each(function () { | |
88 | - $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, options)); | |
89 | - }); | |
90 | - }, | |
91 | - clear: function() { | |
92 | - this.data("tokenInputObject").clear(); | |
93 | - return this; | |
94 | - }, | |
95 | - add: function(item) { | |
96 | - this.data("tokenInputObject").add(item); | |
97 | - return this; | |
98 | - }, | |
99 | - remove: function(item) { | |
100 | - this.data("tokenInputObject").remove(item); | |
101 | - return this; | |
102 | - } | |
103 | -} | |
104 | - | |
105 | -// Expose the .tokenInput function to jQuery as a plugin | |
106 | -$.fn.tokenInput = function (method) { | |
107 | - // Method calling and initialization logic | |
108 | - if(methods[method]) { | |
109 | - return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); | |
110 | - } else { | |
111 | - return methods.init.apply(this, arguments); | |
112 | - } | |
113 | -}; | |
114 | - | |
115 | -// TokenList class for each input | |
116 | -$.TokenList = function (input, url_or_data, options) { | |
117 | - // | |
118 | - // Initialization | |
119 | - // | |
120 | - var settings = $.extend({}, DEFAULT_SETTINGS, options || {}); | |
121 | - | |
122 | - // Configure the data source | |
123 | - if(typeof(url_or_data) === "string") { | |
124 | - // Set the url to query against | |
125 | - settings.url = url_or_data; | |
126 | - | |
127 | - // Make a smart guess about cross-domain if it wasn't explicitly specified | |
128 | - if(settings.crossDomain === undefined) { | |
129 | - if(settings.url.indexOf("://") === -1) { | |
130 | - settings.crossDomain = false; | |
131 | - } else { | |
132 | - settings.crossDomain = (location.href.split(/\/+/g)[1] !== settings.url.split(/\/+/g)[1]); | |
133 | - } | |
71 | + | |
72 | + // Keep track if the input is currently in disabled mode | |
73 | + disabled: false | |
74 | + }; | |
75 | + | |
76 | + // Default classes to use when theming | |
77 | + var DEFAULT_CLASSES = { | |
78 | + tokenList : "token-input-list", | |
79 | + token : "token-input-token", | |
80 | + tokenReadOnly : "token-input-token-readonly", | |
81 | + tokenDelete : "token-input-delete-token", | |
82 | + selectedToken : "token-input-selected-token", | |
83 | + highlightedToken : "token-input-highlighted-token", | |
84 | + dropdown : "token-input-dropdown", | |
85 | + dropdownItem : "token-input-dropdown-item", | |
86 | + dropdownItem2 : "token-input-dropdown-item2", | |
87 | + selectedDropdownItem : "token-input-selected-dropdown-item", | |
88 | + inputToken : "token-input-input-token", | |
89 | + focused : "token-input-focused", | |
90 | + disabled : "token-input-disabled" | |
91 | + }; | |
92 | + | |
93 | + // Input box position "enum" | |
94 | + var POSITION = { | |
95 | + BEFORE : 0, | |
96 | + AFTER : 1, | |
97 | + END : 2 | |
98 | + }; | |
99 | + | |
100 | + // Keys "enum" | |
101 | + var KEY = { | |
102 | + BACKSPACE : 8, | |
103 | + TAB : 9, | |
104 | + ENTER : 13, | |
105 | + ESCAPE : 27, | |
106 | + SPACE : 32, | |
107 | + PAGE_UP : 33, | |
108 | + PAGE_DOWN : 34, | |
109 | + END : 35, | |
110 | + HOME : 36, | |
111 | + LEFT : 37, | |
112 | + UP : 38, | |
113 | + RIGHT : 39, | |
114 | + DOWN : 40, | |
115 | + NUMPAD_ENTER : 108, | |
116 | + COMMA : 188 | |
117 | + }; | |
118 | + | |
119 | + var HTML_ESCAPES = { | |
120 | + '&' : '&', | |
121 | + '<' : '<', | |
122 | + '>' : '>', | |
123 | + '"' : '"', | |
124 | + "'" : ''', | |
125 | + '/' : '/' | |
126 | + }; | |
127 | + | |
128 | + var HTML_ESCAPE_CHARS = /[&<>"'\/]/g; | |
129 | + | |
130 | + function coerceToString(val) { | |
131 | + return String((val === null || val === undefined) ? '' : val); | |
132 | + } | |
133 | + | |
134 | + function _escapeHTML(text) { | |
135 | + return coerceToString(text).replace(HTML_ESCAPE_CHARS, function(match) { | |
136 | + return HTML_ESCAPES[match]; | |
137 | + }); | |
138 | + } | |
139 | + | |
140 | + // Additional public (exposed) methods | |
141 | + var methods = { | |
142 | + init: function(url_or_data_or_function, options) { | |
143 | + var settings = $.extend({}, DEFAULT_SETTINGS, options || {}); | |
144 | + | |
145 | + return this.each(function () { | |
146 | + $(this).data("settings", settings); | |
147 | + $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings)); | |
148 | + }); | |
149 | + }, | |
150 | + clear: function() { | |
151 | + this.data("tokenInputObject").clear(); | |
152 | + return this; | |
153 | + }, | |
154 | + add: function(item) { | |
155 | + this.data("tokenInputObject").add(item); | |
156 | + return this; | |
157 | + }, | |
158 | + remove: function(item) { | |
159 | + this.data("tokenInputObject").remove(item); | |
160 | + return this; | |
161 | + }, | |
162 | + get: function() { | |
163 | + return this.data("tokenInputObject").getTokens(); | |
164 | + }, | |
165 | + toggleDisabled: function(disable) { | |
166 | + this.data("tokenInputObject").toggleDisabled(disable); | |
167 | + return this; | |
168 | + }, | |
169 | + setOptions: function(options){ | |
170 | + $(this).data("settings", $.extend({}, $(this).data("settings"), options || {})); | |
171 | + return this; | |
172 | + }, | |
173 | + destroy: function () { | |
174 | + if (this.data("tokenInputObject")) { | |
175 | + this.data("tokenInputObject").clear(); | |
176 | + var tmpInput = this; | |
177 | + var closest = this.parent(); | |
178 | + closest.empty(); | |
179 | + tmpInput.show(); | |
180 | + closest.append(tmpInput); | |
181 | + return tmpInput; | |
134 | 182 | } |
135 | - } else if(typeof(url_or_data) === "object") { | |
136 | - // Set the local data to search through | |
137 | - settings.local_data = url_or_data; | |
138 | - } | |
139 | - | |
140 | - // Build class names | |
141 | - if(settings.classes) { | |
142 | - // Use custom class names | |
143 | - settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes); | |
144 | - } else if(settings.theme) { | |
145 | - // Use theme-suffixed default class names | |
146 | - settings.classes = {}; | |
147 | - $.each(DEFAULT_CLASSES, function(key, value) { | |
148 | - settings.classes[key] = value + "-" + settings.theme; | |
149 | - }); | |
150 | - } else { | |
151 | - settings.classes = DEFAULT_CLASSES; | |
152 | - } | |
153 | - | |
154 | - | |
155 | - // Save the tokens | |
156 | - var saved_tokens = []; | |
157 | - | |
158 | - // Keep track of the number of tokens in the list | |
159 | - var token_count = 0; | |
160 | - | |
161 | - // Basic cache to save on db hits | |
162 | - var cache = new $.TokenList.Cache(); | |
163 | - | |
164 | - // Keep track of the timeout, old vals | |
165 | - var timeout; | |
166 | - var input_val = ''; | |
167 | - | |
168 | - // Create a new text input an attach keyup events | |
169 | - var input_box = $("<input type=\"text\" autocomplete=\"off\">") | |
170 | - .css({ | |
171 | - outline: "none" | |
172 | - }) | |
173 | - .attr("id", settings.idPrefix + input.id) | |
174 | - .focus(function () { | |
175 | - if (settings.tokenLimit === null || settings.tokenLimit !== token_count) { | |
176 | - if(settings.permanentDropdown || settings.showAllResults) { | |
177 | - hide_dropdown_hint(); | |
183 | + } | |
184 | + }; | |
185 | + | |
186 | + // Expose the .tokenInput function to jQuery as a plugin | |
187 | + $.fn.tokenInput = function (method) { | |
188 | + // Method calling and initialization logic | |
189 | + if (methods[method]) { | |
190 | + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); | |
191 | + } else { | |
192 | + return methods.init.apply(this, arguments); | |
193 | + } | |
194 | + }; | |
195 | + | |
196 | + // TokenList class for each input | |
197 | + $.TokenList = function (input, url_or_data, settings) { | |
198 | + // | |
199 | + // Initialization | |
200 | + // | |
201 | + | |
202 | + // Configure the data source | |
203 | + if (typeof(url_or_data) === "string" || typeof(url_or_data) === "function") { | |
204 | + // Set the url to query against | |
205 | + $(input).data("settings").url = url_or_data; | |
206 | + | |
207 | + // If the URL is a function, evaluate it here to do our initalization work | |
208 | + var url = computeURL(); | |
209 | + | |
210 | + // Make a smart guess about cross-domain if it wasn't explicitly specified | |
211 | + if ($(input).data("settings").crossDomain === undefined && typeof url === "string") { | |
212 | + if(url.indexOf("://") === -1) { | |
213 | + $(input).data("settings").crossDomain = false; | |
214 | + } else { | |
215 | + $(input).data("settings").crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]); | |
216 | + } | |
217 | + } | |
218 | + } else if (typeof(url_or_data) === "object") { | |
219 | + // Set the local data to search through | |
220 | + $(input).data("settings").local_data = url_or_data; | |
221 | + } | |
222 | + | |
223 | + // Build class names | |
224 | + if($(input).data("settings").classes) { | |
225 | + // Use custom class names | |
226 | + $(input).data("settings").classes = $.extend({}, DEFAULT_CLASSES, $(input).data("settings").classes); | |
227 | + } else if($(input).data("settings").theme) { | |
228 | + // Use theme-suffixed default class names | |
229 | + $(input).data("settings").classes = {}; | |
230 | + $.each(DEFAULT_CLASSES, function(key, value) { | |
231 | + $(input).data("settings").classes[key] = value + "-" + $(input).data("settings").theme; | |
232 | + }); | |
233 | + } else { | |
234 | + $(input).data("settings").classes = DEFAULT_CLASSES; | |
235 | + } | |
236 | + | |
237 | + // Save the tokens | |
238 | + var saved_tokens = []; | |
239 | + | |
240 | + // Keep track of the number of tokens in the list | |
241 | + var token_count = 0; | |
242 | + | |
243 | + // Basic cache to save on db hits | |
244 | + var cache = new $.TokenList.Cache(); | |
245 | + | |
246 | + // Keep track of the timeout, old vals | |
247 | + var timeout; | |
248 | + var input_val; | |
249 | + | |
250 | + // Create a new text input an attach keyup events | |
251 | + var input_box = $("<input type=\"text\" autocomplete=\"off\" autocapitalize=\"off\"/>") | |
252 | + .css({ | |
253 | + outline: "none" | |
254 | + }) | |
255 | + .attr("id", $(input).data("settings").idPrefix + input.id) | |
256 | + .focus(function () { | |
257 | + if ($(input).data("settings").disabled) { | |
258 | + return false; | |
178 | 259 | } else |
179 | - show_dropdown_hint(); | |
180 | - if (settings.showAllResults) | |
181 | - do_search(); | |
182 | - } | |
183 | - }) | |
184 | - .blur(function () { | |
185 | - if(settings.permanentDropdown) | |
186 | - show_dropdown_hint(); | |
187 | - else { | |
260 | + if ($(input).data("settings").tokenLimit === null || $(input).data("settings").tokenLimit !== token_count) { | |
261 | + show_dropdown_hint(); | |
262 | + } | |
263 | + token_list.addClass($(input).data("settings").classes.focused); | |
264 | + }) | |
265 | + .blur(function () { | |
188 | 266 | hide_dropdown(); |
189 | - } | |
190 | - }) | |
191 | - .bind("keyup keydown blur update", resize_input) | |
192 | - .keydown(function (event) { | |
193 | - var previous_token; | |
194 | - var next_token; | |
195 | - | |
196 | - switch(event.keyCode) { | |
197 | - case KEY.LEFT: | |
198 | - case KEY.RIGHT: | |
199 | - case KEY.UP: | |
200 | - case KEY.DOWN: | |
201 | - if(!$(this).val()) { | |
267 | + | |
268 | + if ($(input).data("settings").allowFreeTagging) { | |
269 | + add_freetagging_tokens(); | |
270 | + } | |
271 | + | |
272 | + $(this).val(""); | |
273 | + token_list.removeClass($(input).data("settings").classes.focused); | |
274 | + }) | |
275 | + .bind("keyup keydown blur update", resize_input) | |
276 | + .keydown(function (event) { | |
277 | + var previous_token; | |
278 | + var next_token; | |
279 | + | |
280 | + switch(event.keyCode) { | |
281 | + case KEY.LEFT: | |
282 | + case KEY.RIGHT: | |
283 | + case KEY.UP: | |
284 | + case KEY.DOWN: | |
285 | + if(this.value.length === 0) { | |
202 | 286 | previous_token = input_token.prev(); |
203 | 287 | next_token = input_token.next(); |
204 | 288 | |
205 | - if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) { | |
289 | + if((previous_token.length && previous_token.get(0) === selected_token) || | |
290 | + (next_token.length && next_token.get(0) === selected_token)) { | |
206 | 291 | // Check if there is a previous/next token and it is selected |
207 | 292 | if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) { |
208 | 293 | deselect_token($(selected_token), POSITION.BEFORE); |
... | ... | @@ -217,650 +302,805 @@ $.TokenList = function (input, url_or_data, options) { |
217 | 302 | select_token($(next_token.get(0))); |
218 | 303 | } |
219 | 304 | } else { |
220 | - var dropdown_item = null; | |
221 | - | |
222 | - if (event.keyCode == KEY.LEFT && (this.selectionStart > 0 || this.selectionStart != this.selectionEnd)) | |
223 | - return true; | |
224 | - else if (event.keyCode == KEY.RIGHT && (this.selectionEnd < $(this).val().length || this.selectionStart != this.selectionEnd)) | |
225 | - return true; | |
226 | - else if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) { | |
227 | - dropdown_item = $(selected_dropdown_item).next(); | |
228 | - } else { | |
229 | - dropdown_item = $(selected_dropdown_item).prev(); | |
305 | + var dropdown_item = null; | |
306 | + | |
307 | + if (event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) { | |
308 | + dropdown_item = $(dropdown).find('li').first(); | |
309 | + | |
310 | + if (selected_dropdown_item) { | |
311 | + dropdown_item = $(selected_dropdown_item).next(); | |
230 | 312 | } |
313 | + } else { | |
314 | + dropdown_item = $(dropdown).find('li').last(); | |
231 | 315 | |
232 | - if(dropdown_item.length) { | |
233 | - select_dropdown_item(dropdown_item); | |
316 | + if (selected_dropdown_item) { | |
317 | + dropdown_item = $(selected_dropdown_item).prev(); | |
234 | 318 | } |
235 | - return false; | |
319 | + } | |
320 | + | |
321 | + select_dropdown_item(dropdown_item); | |
236 | 322 | } |
323 | + | |
237 | 324 | break; |
238 | 325 | |
239 | - case KEY.BACKSPACE: | |
240 | - case KEY.DELETE: | |
241 | - previous_token = input_token.prev(); | |
242 | - next_token = input_token.next(); | |
326 | + case KEY.BACKSPACE: | |
327 | + previous_token = input_token.prev(); | |
243 | 328 | |
244 | - if(!$(this).val().length && settings.backspaceDeleteItem) { | |
245 | - if(selected_token) { | |
246 | - delete_token($(selected_token)); | |
247 | - input_box.focus(); | |
248 | - } else if(KEY.DELETE && next_token.length) { | |
249 | - select_token($(next_token.get(0))); | |
250 | - } else if(KEY.BACKSPACE && previous_token.length) { | |
251 | - select_token($(previous_token.get(0))); | |
329 | + if (this.value.length === 0) { | |
330 | + if (selected_token) { | |
331 | + delete_token($(selected_token)); | |
332 | + hiddenInput.change(); | |
333 | + } else if(previous_token.length) { | |
334 | + select_token($(previous_token.get(0))); | |
252 | 335 | } |
253 | 336 | |
254 | 337 | return false; |
255 | - } else if(!settings.permanentDropdown && $(this).val().length === 1) { | |
256 | - hide_dropdown(); | |
338 | + } else if($(this).val().length === 1) { | |
339 | + hide_dropdown(); | |
340 | + } else { | |
341 | + // set a timeout just long enough to let this function finish. | |
342 | + setTimeout(function(){ do_search(); }, 5); | |
343 | + } | |
344 | + break; | |
345 | + | |
346 | + case KEY.TAB: | |
347 | + case KEY.ENTER: | |
348 | + case KEY.NUMPAD_ENTER: | |
349 | + case KEY.COMMA: | |
350 | + if(selected_dropdown_item) { | |
351 | + add_token($(selected_dropdown_item).data("tokeninput")); | |
352 | + hiddenInput.change(); | |
257 | 353 | } else { |
258 | - // set a timeout just long enough to let this function finish. | |
259 | - setTimeout(function(){do_search();}, 5); | |
354 | + if ($(input).data("settings").allowFreeTagging) { | |
355 | + if($(input).data("settings").allowTabOut && $(this).val() === "") { | |
356 | + return true; | |
357 | + } else { | |
358 | + add_freetagging_tokens(); | |
359 | + } | |
360 | + } else { | |
361 | + $(this).val(""); | |
362 | + if($(input).data("settings").allowTabOut) { | |
363 | + return true; | |
364 | + } | |
365 | + } | |
366 | + event.stopPropagation(); | |
367 | + event.preventDefault(); | |
260 | 368 | } |
261 | - break; | |
262 | - | |
263 | - case KEY.TAB: | |
264 | - case KEY.ENTER: | |
265 | - case KEY.NUMPAD_ENTER: | |
266 | - case KEY.COMMA: | |
267 | - if(selected_dropdown_item) { | |
268 | - add_token($(selected_dropdown_item).data("tokeninput")); | |
269 | - input_box.focus(); | |
270 | 369 | return false; |
271 | - } | |
272 | - break; | |
273 | 370 | |
274 | - case KEY.ESCAPE: | |
275 | - hide_dropdown(); | |
276 | - return true; | |
371 | + case KEY.ESCAPE: | |
372 | + hide_dropdown(); | |
373 | + return true; | |
277 | 374 | |
278 | - default: | |
279 | - if(String.fromCharCode(event.which)) { | |
280 | - // set a timeout just long enough to let this function finish. | |
281 | - setTimeout(function(){do_search();}, 5); | |
375 | + default: | |
376 | + if (String.fromCharCode(event.which)) { | |
377 | + // set a timeout just long enough to let this function finish. | |
378 | + setTimeout(function(){ do_search(); }, 5); | |
282 | 379 | } |
283 | 380 | break; |
284 | - } | |
285 | - }); | |
286 | - | |
287 | - // Keep a reference to the original input box | |
288 | - var hidden_input = $(input) | |
289 | - .hide() | |
290 | - .val("") | |
291 | - .focus(function () { | |
292 | - input_box.focus(); | |
293 | - }) | |
294 | - .blur(function () { | |
295 | - input_box.blur(); | |
296 | - }); | |
297 | - | |
298 | - // Keep a reference to the selected token and dropdown item | |
299 | - var selected_token = null; | |
300 | - var selected_token_index = 0; | |
301 | - var selected_dropdown_item = null; | |
302 | - | |
303 | - // The list to store the token items in | |
304 | - var token_list = $("<ul />") | |
305 | - .addClass(settings.classes.tokenList) | |
306 | - .click(function (event) { | |
307 | - var li = $(event.target).closest("li"); | |
308 | - if(li && li.get(0) && $.data(li.get(0), "tokeninput")) { | |
309 | - toggle_select_token(li); | |
310 | - } else { | |
311 | - // Deselect selected token | |
312 | - if(selected_token) { | |
313 | - deselect_token($(selected_token), POSITION.END); | |
314 | - } | |
315 | - | |
316 | - // Transfer focus | |
317 | - if (!input_box.is(':focus')) | |
318 | - input_box.focus(); | |
319 | - } | |
381 | + } | |
382 | + }); | |
383 | + | |
384 | + // Keep reference for placeholder | |
385 | + if (settings.placeholder) { | |
386 | + input_box.attr("placeholder", settings.placeholder); | |
387 | + } | |
388 | + | |
389 | + // Keep a reference to the original input box | |
390 | + var hiddenInput = $(input) | |
391 | + .hide() | |
392 | + .val("") | |
393 | + .focus(function () { | |
394 | + focusWithTimeout(input_box); | |
320 | 395 | }) |
321 | - .mouseover(function (event) { | |
322 | - var li = $(event.target).closest("li"); | |
323 | - if(li && selected_token !== this) { | |
324 | - li.addClass(settings.classes.highlightedToken); | |
325 | - } | |
396 | + .blur(function () { | |
397 | + input_box.blur(); | |
398 | + | |
399 | + //return the object to this can be referenced in the callback functions. | |
400 | + return hiddenInput; | |
326 | 401 | }) |
327 | - .mouseout(function (event) { | |
328 | - var li = $(event.target).closest("li"); | |
329 | - if(li && selected_token !== this) { | |
330 | - li.removeClass(settings.classes.highlightedToken); | |
402 | + ; | |
403 | + | |
404 | + // Keep a reference to the selected token and dropdown item | |
405 | + var selected_token = null; | |
406 | + var selected_token_index = 0; | |
407 | + var selected_dropdown_item = null; | |
408 | + | |
409 | + // The list to store the token items in | |
410 | + var token_list = $("<ul />") | |
411 | + .addClass($(input).data("settings").classes.tokenList) | |
412 | + .click(function (event) { | |
413 | + var li = $(event.target).closest("li"); | |
414 | + if(li && li.get(0) && $.data(li.get(0), "tokeninput")) { | |
415 | + toggle_select_token(li); | |
416 | + } else { | |
417 | + // Deselect selected token | |
418 | + if(selected_token) { | |
419 | + deselect_token($(selected_token), POSITION.END); | |
420 | + } | |
421 | + | |
422 | + // Focus input box | |
423 | + focusWithTimeout(input_box); | |
424 | + } | |
425 | + }) | |
426 | + .mouseover(function (event) { | |
427 | + var li = $(event.target).closest("li"); | |
428 | + if(li && selected_token !== this) { | |
429 | + li.addClass($(input).data("settings").classes.highlightedToken); | |
430 | + } | |
431 | + }) | |
432 | + .mouseout(function (event) { | |
433 | + var li = $(event.target).closest("li"); | |
434 | + if(li && selected_token !== this) { | |
435 | + li.removeClass($(input).data("settings").classes.highlightedToken); | |
436 | + } | |
437 | + }) | |
438 | + .insertBefore(hiddenInput); | |
439 | + | |
440 | + // The token holding the input box | |
441 | + var input_token = $("<li />") | |
442 | + .addClass($(input).data("settings").classes.inputToken) | |
443 | + .appendTo(token_list) | |
444 | + .append(input_box); | |
445 | + | |
446 | + // The list to store the dropdown items in | |
447 | + var dropdown = $("<div/>") | |
448 | + .addClass($(input).data("settings").classes.dropdown) | |
449 | + .appendTo("body") | |
450 | + .hide(); | |
451 | + | |
452 | + // Magic element to help us resize the text input | |
453 | + var input_resizer = $("<tester/>") | |
454 | + .insertAfter(input_box) | |
455 | + .css({ | |
456 | + position: "absolute", | |
457 | + top: -9999, | |
458 | + left: -9999, | |
459 | + width: "auto", | |
460 | + fontSize: input_box.css("fontSize"), | |
461 | + fontFamily: input_box.css("fontFamily"), | |
462 | + fontWeight: input_box.css("fontWeight"), | |
463 | + letterSpacing: input_box.css("letterSpacing"), | |
464 | + whiteSpace: "nowrap" | |
465 | + }); | |
466 | + | |
467 | + // Pre-populate list if items exist | |
468 | + hiddenInput.val(""); | |
469 | + var li_data = $(input).data("settings").prePopulate || hiddenInput.data("pre"); | |
470 | + | |
471 | + if ($(input).data("settings").processPrePopulate && $.isFunction($(input).data("settings").onResult)) { | |
472 | + li_data = $(input).data("settings").onResult.call(hiddenInput, li_data); | |
473 | + } | |
474 | + | |
475 | + if (li_data && li_data.length) { | |
476 | + $.each(li_data, function (index, value) { | |
477 | + insert_token(value); | |
478 | + checkTokenLimit(); | |
479 | + input_box.attr("placeholder", null) | |
480 | + }); | |
481 | + } | |
482 | + | |
483 | + // Check if widget should initialize as disabled | |
484 | + if ($(input).data("settings").disabled) { | |
485 | + toggleDisabled(true); | |
486 | + } | |
487 | + | |
488 | + // Initialization is done | |
489 | + if (typeof($(input).data("settings").onReady) === "function") { | |
490 | + $(input).data("settings").onReady.call(); | |
491 | + } | |
492 | + | |
493 | + // | |
494 | + // Public functions | |
495 | + // | |
496 | + | |
497 | + this.clear = function() { | |
498 | + token_list.children("li").each(function() { | |
499 | + if ($(this).children("input").length === 0) { | |
500 |