Commit e68ef809aca10100a8b846104daf74dcb779489b
Exists in
staging
and in
4 other branches
Merge branch 'private-scraps' into 'master'
Private scraps Write scraps that are visible for only the people or group marked. This feature was implemented by Rodrigo Souto, I'm just creating the MR. See merge request !997
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) | |
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,39 @@ 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 | + owner != user && | |
525 | + target.is_a?(Scrap) && | |
526 | + target.marked_people.present? && | |
527 | + !target.marked_people.include?(user) | |
528 | + end | |
529 | + activities | |
530 | + end | |
483 | 531 | 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 | + delete_token($(this)); | |
501 | + } | |
502 | + }); | |
503 | + }; | |
504 | + | |
505 | + this.add = function(item) { | |
506 | + add_token(item); | |
507 | + }; | |
508 | + | |
509 | + this.remove = function(item) { | |
510 | + token_list.children("li").each(function() { | |
511 | + if ($(this).children("input").length === 0) { | |
512 | + var currToken = $(this).data("tokeninput"); | |
513 | + var match = true; | |
514 | + for (var prop in item) { | |
515 | + if (item[prop] !== currToken[prop]) { | |
516 | + match = false; | |
517 | + break; | |
518 | + } | |
519 | + } | |
520 | + if (match) { | |
521 | + delete_token($(this)); | |
522 | + } | |
523 | + } | |
524 | + }); | |
525 | + }; | |
526 | + | |
527 | + this.getTokens = function() { | |
528 | + return saved_tokens; | |
529 | + }; | |
530 | + | |
531 | + this.toggleDisabled = function(disable) { | |
532 | + toggleDisabled(disable); | |
533 | + }; | |
534 | + | |
535 | + // Resize input to maximum width so the placeholder can be seen | |
536 | + resize_input(); | |
537 | + | |
538 | + // | |
539 | + // Private functions | |
540 | + // | |
541 | + | |
542 | + function escapeHTML(text) { | |
543 | + return $(input).data("settings").enableHTML ? text : _escapeHTML(text); | |
544 | + } | |
545 | + | |
546 | + // Toggles the widget between enabled and disabled state, or according | |
547 | + // to the [disable] parameter. | |
548 | + function toggleDisabled(disable) { | |
549 | + if (typeof disable === 'boolean') { | |
550 | + $(input).data("settings").disabled = disable | |
551 | + } else { | |
552 | + $(input).data("settings").disabled = !$(input).data("settings").disabled; | |
553 | + } | |
554 | + input_box.attr('disabled', $(input).data("settings").disabled); | |
555 | + token_list.toggleClass($(input).data("settings").classes.disabled, $(input).data("settings").disabled); | |
556 | + // if there is any token selected we deselect it | |
557 | + if(selected_token) { | |
558 | + deselect_token($(selected_token), POSITION.END); | |
559 | + } | |
560 | + hiddenInput.attr('disabled', $(input).data("settings").disabled); | |
561 | + } | |
562 | + | |
563 | + function checkTokenLimit() { | |
564 | + if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) { | |
565 | + input_box.hide(); | |
566 | + hide_dropdown(); | |
567 | + return; | |
568 | + } | |
569 | + } | |
570 | + | |
571 | + function resize_input() { | |
572 | + if(input_val === (input_val = input_box.val())) {return;} | |
573 | + | |
574 | + // Get width left on the current line | |
575 | + var width_left = token_list.width() - input_box.offset().left - token_list.offset().left; | |
576 | + // Enter new content into resizer and resize input accordingly | |
577 | + input_resizer.html(_escapeHTML(input_val) || _escapeHTML(settings.placeholder)); | |
578 | + // Get maximum width, minimum the size of input and maximum the widget's width | |
579 | + input_box.width(Math.min(token_list.width(), | |
580 | + Math.max(width_left, input_resizer.width() + 30))); | |
581 | + } | |
582 | + | |
583 | + function add_freetagging_tokens() { | |
584 | + var value = $.trim(input_box.val()); | |
585 | + var tokens = value.split($(input).data("settings").tokenDelimiter); | |
586 | + $.each(tokens, function(i, token) { | |
587 | + if (!token) { | |
588 | + return; | |
331 | 589 | } |
332 | - }) | |
333 | - .insertBefore(hidden_input); | |
334 | - | |
335 | - // The token holding the input box | |
336 | - var input_token = $("<li />") | |
337 | - .addClass(settings.classes.inputToken) | |
338 | - .appendTo(token_list) | |
339 | - .append(input_box); | |
340 | - | |
341 | - // The list to store the dropdown items in | |
342 | - var dropdown = $("<div>") | |
343 | - .addClass(settings.classes.dropdown) | |
344 | - .hide(); | |
345 | - dropdown.appendTo("body"); | |
346 | - if (!settings.permanentDropdown) | |
347 | - dropdown.appendTo("body"); | |
348 | - else | |
349 | - $(input).after(dropdown.show()); | |
350 | - | |
351 | - if (settings.permanentDropdown || settings.showAllResults) { | |
352 | - do_search(); | |
353 | - if (!settings.permanentDropdown && settings.showAllResults) | |
354 | - hide_dropdown(); | |
355 | - } | |
356 | - | |
357 | - // Hint for permanentDropdown | |
358 | - if (settings.permanentDropdown || settings.showAllResults) | |
359 | - show_dropdown_hint(); | |
360 | - | |
361 | - // Magic element to help us resize the text input | |
362 | - var input_resizer = $("<tester/>") | |
363 | - .insertAfter(input_box) | |
364 | - .css({ | |
365 | - position: "absolute", | |
366 | - top: -9999, | |
367 | - left: -9999, | |
368 | - width: "auto", | |
369 | - fontSize: input_box.css("fontSize"), | |
370 | - fontFamily: input_box.css("fontFamily"), | |
371 | - fontWeight: input_box.css("fontWeight"), | |
372 | - letterSpacing: input_box.css("letterSpacing"), | |
373 | - whiteSpace: "nowrap" | |
374 | - }); | |
375 | - | |
376 | - // Pre-populate list if items exist | |
377 | - hidden_input.val(""); | |
378 | - var li_data = settings.prePopulate || hidden_input.data("pre"); | |
379 | - if(settings.processPrePopulate && $.isFunction(settings.onResult)) { | |
380 | - li_data = settings.onResult.call(hidden_input, li_data); | |
381 | - } | |
382 | - if(li_data && li_data.length) { | |
383 | - $.each(li_data, function (index, value) { | |
384 | - insert_token(value); | |
385 | - checkTokenLimit({init: true}); | |
386 | - }); | |
387 | - } | |
388 | - | |
389 | - | |
390 | - // | |
391 | - // Public functions | |
392 | - // | |
393 | - | |
394 | - this.clear = function() { | |
395 | - token_list.children("li").each(function() { | |
396 | - if ($(this).children("input").length === 0) { | |
397 | - delete_token($(this)); | |
590 | + | |
591 | + if ($.isFunction($(input).data("settings").onFreeTaggingAdd)) { | |
592 | + token = $(input).data("settings").onFreeTaggingAdd.call(hiddenInput, token); | |
398 | 593 | } |
399 | - }); | |
400 | - } | |
401 | - | |
402 | - this.add = function(item) { | |
403 | - add_token(item); | |
404 | - } | |
405 | - | |
406 | - this.remove = function(item) { | |
407 | - token_list.children("li").each(function() { | |
408 | - if ($(this).children("input").length === 0) { | |
409 | - var currToken = $(this).data("tokeninput"); | |
410 | - var match = true; | |
411 | - for (var prop in item) { | |
412 | - if (item[prop] !== currToken[prop]) { | |
413 | - match = false; | |
414 | - break; | |
594 | + var object = {}; | |
595 | + object[$(input).data("settings").tokenValue] = object[$(input).data("settings").propertyToSearch] = token; | |
596 | + add_token(object); | |
597 | + }); | |
598 | + } | |
599 | + | |
600 | + // Inner function to a token to the list | |
601 | + function insert_token(item) { | |
602 | + var $this_token = $($(input).data("settings").tokenFormatter(item)); | |
603 | + var readonly = item.readonly === true; | |
604 | + | |
605 | + if(readonly) $this_token.addClass($(input).data("settings").classes.tokenReadOnly); | |
606 | + | |
607 | + $this_token.addClass($(input).data("settings").classes.token).insertBefore(input_token); | |
608 | + | |
609 | + // The 'delete token' button | |
610 | + if(!readonly) { | |
611 | + $("<span>" + $(input).data("settings").deleteText + "</span>") | |
612 | + .addClass($(input).data("settings").classes.tokenDelete) | |
613 | + .appendTo($this_token) | |
614 | + .click(function () { | |
615 | + if (!$(input).data("settings").disabled) { | |
616 | + delete_token($(this).parent()); | |
617 | + hiddenInput.change(); | |
618 | + return false; | |
415 | 619 | } |
416 | - } | |
417 | - if (match) { | |
418 | - delete_token($(this)); | |
419 | - } | |
420 | - } | |
421 | - }); | |
422 | - } | |
423 | - | |
424 | - // | |
425 | - // Private functions | |
426 | - // | |
427 | - | |
428 | - function checkTokenLimit(options) { | |
429 | - if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) { | |
430 | - input_box.hide(); | |
431 | - hide_dropdown(); | |
432 | - return; | |
433 | - } else if (options && !options.init) { | |
434 | - input_box.focus(); | |
435 | - } | |
436 | - } | |
437 | - | |
438 | - function resize_input() { | |
439 | - if(input_val === (input_val = input_box.val())) {return;} | |
440 | - | |
441 | - // Enter new content into resizer and resize input accordingly | |
442 | - var escaped = input_val.replace(/&/g, '&').replace(/\s/g,' ').replace(/</g, '<').replace(/>/g, '>'); | |
443 | - input_resizer.html(escaped); | |
444 | - input_box.width(input_resizer.width() + 30); | |
445 | - | |
446 | - if((settings.permanentDropdown || settings.showAllResults) && input_box.hasClass(settings.classes.blurText)) | |
447 | - input_val = ''; | |
448 | - } | |
449 | - | |
450 | - function is_printable_character(keycode) { | |
451 | - return ((keycode >= 48 && keycode <= 90) || // 0-1a-z | |
452 | - (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * . | |
453 | - (keycode >= 186 && keycode <= 192) || // ; = , - . / ^ | |
454 | - (keycode >= 219 && keycode <= 222)); // ( \ ) ' | |
455 | - } | |
456 | - | |
457 | - // Inner function to a token to the list | |
458 | - function insert_token(item) { | |
459 | - var this_token = $("<li><p>"+ item.name +"</p></li>") | |
460 | - .addClass(settings.classes.token) | |
461 | - .insertBefore(input_token); | |
462 | - | |
463 | - // The 'delete token' button | |
464 | - $("<span>" + settings.deleteText + "</span>") | |
465 | - .addClass(settings.classes.tokenDelete) | |
466 | - .appendTo(this_token) | |
467 | - .click(function () { | |
468 | - delete_token($(this).parent()); | |
469 | - return false; | |
470 | - }); | |
471 | - | |
472 | - // Store data on the token | |
473 | - var token_data = {"id": item.id, "name": item.name}; | |
474 | - $.data(this_token.get(0), "tokeninput", item); | |
475 | - | |
476 | - // Save this token for duplicate checking | |
477 | - saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index)); | |
478 | - selected_token_index++; | |
479 | - | |
480 | - // Update the hidden input | |
481 | - var token_ids = $.map(saved_tokens, function (el) { | |
482 | - return el.id; | |
483 | - }); | |
484 | - hidden_input.val(token_ids.join(settings.tokenDelimiter)); | |
485 | - | |
486 | - token_count += 1; | |
487 | - | |
488 | - return this_token; | |
489 | - } | |
490 | - | |
491 | - // Add a token to the token list based on user input | |
492 | - function add_token (item) { | |
493 | - if (settings.dontAdd) | |
494 | - return; | |
495 | - | |
496 | - var callback = settings.onAdd; | |
497 | - | |
498 | - // See if the token already exists and select it if we don't want duplicates | |
499 | - if(token_count > 0 && settings.preventDuplicates) { | |
500 | - var found_existing_token = null; | |
501 | - token_list.children().each(function () { | |
502 | - var existing_token = $(this); | |
503 | - var existing_data = $.data(existing_token.get(0), "tokeninput"); | |
504 | - if(existing_data && existing_data.id === item.id) { | |
505 | - found_existing_token = existing_token; | |
506 | - return false; | |
507 | - } | |
508 | - }); | |
620 | + }); | |
621 | + } | |
509 | 622 | |
510 | - if(found_existing_token) { | |
511 | - select_token(found_existing_token); | |
512 | - input_token.insertAfter(found_existing_token); | |
513 | - return; | |
514 | - } | |
515 | - } | |
623 | + // Store data on the token | |
624 | + var token_data = item; | |
625 | + $.data($this_token.get(0), "tokeninput", item); | |
516 | 626 | |
517 | - // Insert the new tokens | |
518 | - insert_token(item); | |
519 | - checkTokenLimit(); | |
627 | + // Save this token for duplicate checking | |
628 | + saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index)); | |
629 | + selected_token_index++; | |
520 | 630 | |
521 | - // Clear input box | |
522 | - input_box.val(""); | |
631 | + // Update the hidden input | |
632 | + update_hiddenInput(saved_tokens, hiddenInput); | |
523 | 633 | |
524 | - // Don't show the help dropdown, they've got the idea | |
525 | - hide_dropdown(); | |
634 | + token_count += 1; | |
526 | 635 | |
527 | - // Execute the onAdd callback if defined | |
528 | - if($.isFunction(callback)) { | |
529 | - callback.call(hidden_input,item); | |
530 | - } | |
531 | - } | |
532 | - | |
533 | - // Select a token in the token list | |
534 | - function select_token (token) { | |
535 | - token.addClass(settings.classes.selectedToken); | |
536 | - selected_token = token.get(0); | |
537 | - | |
538 | - // Hide input box | |
539 | - input_box.val(""); | |
540 | - | |
541 | - // Hide dropdown if it is visible (eg if we clicked to select token) | |
542 | - hide_dropdown(); | |
543 | - } | |
544 | - | |
545 | - // Deselect a token in the token list | |
546 | - function deselect_token (token, position) { | |
547 | - token.removeClass(settings.classes.selectedToken); | |
548 | - selected_token = null; | |
549 | - | |
550 | - if(position === POSITION.BEFORE) { | |
551 | - input_token.insertBefore(token); | |
552 | - selected_token_index--; | |
553 | - } else if(position === POSITION.AFTER) { | |
554 | - input_token.insertAfter(token); | |
555 | - selected_token_index++; | |
556 | - } else { | |
557 | - input_token.appendTo(token_list); | |
558 | - selected_token_index = token_count; | |
559 | - } | |
636 | + // Check the token limit | |
637 | + if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) { | |
638 | + input_box.hide(); | |
639 | + hide_dropdown(); | |
640 | + } | |
560 | 641 | |
561 | - // Show the input box and give it focus again | |
562 | - input_box.focus(); | |
563 | - } | |
642 | + return $this_token; | |
643 | + } | |
564 | 644 | |
565 | - // Toggle selection of a token in the token list | |
566 | - function toggle_select_token(token) { | |
567 | - var previous_selected_token = selected_token; | |
645 | + // Add a token to the token list based on user input | |
646 | + function add_token (item) { | |
647 | + var callback = $(input).data("settings").onAdd; | |
648 | + | |
649 | + // See if the token already exists and select it if we don't want duplicates | |
650 | + if(token_count > 0 && $(input).data("settings").preventDuplicates) { | |
651 | + var found_existing_token = null; | |
652 | + token_list.children().each(function () { | |
653 | + var existing_token = $(this); | |
654 | + var existing_data = $.data(existing_token.get(0), "tokeninput"); | |
655 | + if(existing_data && existing_data[settings.tokenValue] === item[settings.tokenValue]) { | |
656 | + found_existing_token = existing_token; | |
657 | + return false; | |
658 | + } | |
659 | + }); | |
660 | + | |
661 | + if(found_existing_token) { | |
662 | + select_token(found_existing_token); | |
663 | + input_token.insertAfter(found_existing_token); | |
664 | + focusWithTimeout(input_box); | |
665 | + return; | |
666 | + } | |
667 | + } | |
568 | 668 | |
569 | - if(selected_token) { | |
570 | - deselect_token($(selected_token), POSITION.END); | |
571 | - } | |
669 | + // Squeeze input_box so we force no unnecessary line break | |
670 | + input_box.width(1); | |
572 | 671 | |
573 | - if(previous_selected_token === token.get(0)) { | |
574 | - deselect_token(token, POSITION.END); | |
575 | - } else { | |
576 | - select_token(token); | |
577 | - } | |
578 | - } | |
672 | + // Insert the new tokens | |
673 | + if($(input).data("settings").tokenLimit == null || token_count < $(input).data("settings").tokenLimit) { | |
674 | + insert_token(item); | |
675 | + // Remove the placeholder so it's not seen after you've added a token | |
676 | + input_box.attr("placeholder", null); | |
677 | + checkTokenLimit(); | |
678 | + } | |
679 | + | |
680 | + // Clear input box | |
681 | + input_box.val(""); | |
579 | 682 | |
580 | - // Delete a token from the token list | |
581 | - function delete_token (token) { | |
582 | - // Remove the id from the saved list | |
583 | - var token_data = $.data(token.get(0), "tokeninput"); | |
584 | - var callback = settings.onDelete; | |
683 | + // Don't show the help dropdown, they've got the idea | |
684 | + hide_dropdown(); | |
585 | 685 | |
586 | - var index = token.prevAll().length; | |
587 | - if(index > selected_token_index) index--; | |
686 | + // Execute the onAdd callback if defined | |
687 | + if($.isFunction(callback)) { | |
688 | + callback.call(hiddenInput,item); | |
689 | + } | |
690 | + } | |
588 | 691 | |
589 | - // Delete the token | |
590 | - token.remove(); | |
591 | - selected_token = null; | |
692 | + // Select a token in the token list | |
693 | + function select_token (token) { | |
694 | + if (!$(input).data("settings").disabled) { | |
695 | + token.addClass($(input).data("settings").classes.selectedToken); | |
696 | + selected_token = token.get(0); | |
592 | 697 | |
593 | - // Remove this token from the saved list | |
594 | - saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1)); | |
595 | - if(index < selected_token_index) selected_token_index--; | |
698 | + // Hide input box | |
699 | + input_box.val(""); | |
596 | 700 | |
597 | - // Update the hidden input | |
598 | - var token_ids = $.map(saved_tokens, function (el) { | |
599 | - return el.id; | |
600 | - }); | |
601 | - hidden_input.val(token_ids.join(settings.tokenDelimiter)); | |
701 | + // Hide dropdown if it is visible (eg if we clicked to select token) | |
702 | + hide_dropdown(); | |
703 | + } | |
704 | + } | |
602 | 705 | |
603 | - token_count -= 1; | |
706 | + // Deselect a token in the token list | |
707 | + function deselect_token (token, position) { | |
708 | + token.removeClass($(input).data("settings").classes.selectedToken); | |
709 | + selected_token = null; | |
710 | + | |
711 | + if(position === POSITION.BEFORE) { | |
712 | + input_token.insertBefore(token); | |
713 | + selected_token_index--; | |
714 | + } else if(position === POSITION.AFTER) { | |
715 | + input_token.insertAfter(token); | |
716 | + selected_token_index++; | |
717 | + } else { | |
718 | + input_token.appendTo(token_list); | |
719 | + selected_token_index = token_count; | |
720 | + } | |
604 | 721 | |
605 | - if(settings.tokenLimit !== null) | |
606 | - input_box.show().val(""); | |
722 | + // Show the input box and give it focus again | |
723 | + focusWithTimeout(input_box); | |
724 | + } | |
607 | 725 | |
608 | - // Execute the onDelete callback if defined | |
609 | - if($.isFunction(callback)) { | |
610 | - callback.call(hidden_input,token_data); | |
611 | - } | |
612 | - } | |
726 | + // Toggle selection of a token in the token list | |
727 | + function toggle_select_token(token) { | |
728 | + var previous_selected_token = selected_token; | |
613 | 729 | |
614 | - // Hide and clear the results dropdown | |
615 | - function hide_dropdown () { | |
616 | - if (!settings.permanentDropdown) { | |
617 | - dropdown.hide(); | |
618 | - if (!settings.showAllResults) | |
619 | - dropdown.empty(); | |
620 | - selected_dropdown_item = null; | |
621 | - } | |
622 | - if (settings.showAllResults) | |
623 | - show_dropdown_hint(); | |
624 | - } | |
625 | - | |
626 | - function show_dropdown() { | |
627 | - if (!settings.permanentDropdown) | |
628 | - dropdown.css({ | |
629 | - position: "absolute", | |
630 | - top: $(token_list).offset().top + $(token_list).outerHeight(), | |
631 | - left: $(token_list).offset().left, | |
632 | - 'z-index': settings.zindex | |
633 | - }).show(); | |
634 | - else | |
635 | - dropdown.css({ | |
636 | - position: "relative", | |
637 | - }).show(); | |
638 | - } | |
639 | - | |
640 | - function show_dropdown_searching () { | |
641 | - if(settings.searchingText) { | |
642 | - dropdown.html("<p>"+settings.searchingText+"</p>"); | |
643 | - show_dropdown(); | |
644 | - } | |
645 | - } | |
646 | - | |
647 | - function show_dropdown_hint () { | |
648 | - if(settings.hintText) { | |
649 | - if(settings.permanentDropdown || settings.showAllResults) { | |
650 | - if (input_val == '') { | |
651 | - input_box.val(settings.hintText); | |
652 | - input_box.addClass(settings.classes.blurText); | |
653 | - } | |
730 | + if(selected_token) { | |
731 | + deselect_token($(selected_token), POSITION.END); | |
732 | + } | |
733 | + | |
734 | + if(previous_selected_token === token.get(0)) { | |
735 | + deselect_token(token, POSITION.END); | |
654 | 736 | } else { |
655 | - dropdown.html("<p>"+settings.hintText+"</p>"); | |
656 | - show_dropdown(); | |
737 | + select_token(token); | |
657 | 738 | } |
658 | - } | |
659 | - } | |
660 | - | |
661 | - function hide_dropdown_hint () { | |
662 | - if (input_box.hasClass(settings.classes.blurText)) { | |
663 | - input_box.val(''); | |
664 | - input_box.removeClass(settings.classes.blurText); | |
665 | - } | |
666 | - } | |
667 | - | |
668 | - // Highlight the query part of the search term | |
669 | - function highlight_term(value, term) { | |
670 | - return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>"); | |
671 | - } | |
672 | - | |
673 | - // Populate the results dropdown with some results | |
674 | - function populate_dropdown (query, results) { | |
675 | - if(results && results.length) { | |
676 | - dropdown.empty(); | |
677 | - var dropdown_ul = $("<ul>") | |
678 | - .appendTo(dropdown) | |
679 | - .mouseover(function (event) { | |
680 | - select_dropdown_item($(event.target).closest("li")); | |
681 | - }) | |
682 | - .mousedown(function (event) { | |
683 | - add_token($(event.target).closest("li").data("tokeninput")); | |
684 | - input_box.blur(); | |
685 | - return false; | |
686 | - }) | |
687 | - .hide(); | |
739 | + } | |
688 | 740 | |
689 | - $.each(results, function(index, value) { | |
690 | - var this_li = $("<li>" + highlight_term(value.name, query) + "</li>") | |
691 | - .appendTo(dropdown_ul); | |
741 | + // Delete a token from the token list | |
742 | + function delete_token (token) { | |
743 | + // Remove the id from the saved list | |
744 | + var token_data = $.data(token.get(0), "tokeninput"); | |
745 | + var callback = $(input).data("settings").onDelete; | |
692 | 746 | |
693 | - if(index % 2) { | |
694 | - this_li.addClass(settings.classes.dropdownItem); | |
695 | - } else { | |
696 | - this_li.addClass(settings.classes.dropdownItem2); | |
697 | - } | |
747 | + var index = token.prevAll().length; | |
748 | + if(index > selected_token_index) index--; | |
698 | 749 | |
699 | - if(index === 0) { | |
700 | - select_dropdown_item(this_li); | |
701 | - } | |
750 | + // Delete the token | |
751 | + token.remove(); | |
752 | + selected_token = null; | |
702 | 753 | |
703 | - $.data(this_li.get(0), "tokeninput", value); | |
704 | - }); | |
754 | + // Show the input box and give it focus again | |
755 | + focusWithTimeout(input_box); | |
705 | 756 | |
706 | - show_dropdown(); | |
757 | + // Remove this token from the saved list | |
758 | + saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1)); | |
759 | + if (saved_tokens.length == 0) { | |
760 | + input_box.attr("placeholder", settings.placeholder) | |
761 | + } | |
762 | + if(index < selected_token_index) selected_token_index--; | |
707 | 763 | |
708 | - if(settings.animateDropdown) { | |
709 | - dropdown_ul.slideDown("fast"); | |
710 | - } else { | |
711 | - dropdown_ul.show(); | |
712 | - } | |
713 | - } else { | |
714 | - if(settings.noResultsText) { | |
715 | - dropdown.html("<p>"+settings.noResultsText+"</p>"); | |
716 | - show_dropdown(); | |
717 | - } | |
718 | - } | |
719 | - } | |
764 | + // Update the hidden input | |
765 | + update_hiddenInput(saved_tokens, hiddenInput); | |
720 | 766 | |
721 | - // Highlight an item in the results dropdown | |
722 | - function select_dropdown_item (item) { | |
723 | - if(item) { | |
724 | - if(selected_dropdown_item) { | |
725 | - deselect_dropdown_item($(selected_dropdown_item)); | |
726 | - } | |
767 | + token_count -= 1; | |
727 | 768 | |
728 | - item.addClass(settings.classes.selectedDropdownItem); | |
729 | - selected_dropdown_item = item.get(0); | |
730 | - | |
731 | - isBefore = item[0].offsetTop <= (dropdown[0].scrollTop + dropdown[0].scrollWidth); | |
732 | - isAfter = item[0].offsetTop >= dropdown[0].scrollTop; | |
733 | - visible = isBefore && isAfter; | |
734 | - if (!visible) { | |
735 | - if (isBefore) | |
736 | - dropdown[0].scrollTop = item[0].offsetTop; | |
737 | - else //isAfter | |
738 | - dropdown[0].scrollTop = item[0].offsetTop - dropdown[0].scrollWidth; | |
739 | - } | |
740 | - } | |
741 | - } | |
742 | - | |
743 | - // Remove highlighting from an item in the results dropdown | |
744 | - function deselect_dropdown_item (item) { | |
745 | - item.removeClass(settings.classes.selectedDropdownItem); | |
746 | - selected_dropdown_item = null; | |
747 | - } | |
748 | - | |
749 | - // Do a search and show the "searching" dropdown if the input is longer | |
750 | - // than settings.minChars | |
751 | - function do_search() { | |
752 | - var query = input_box.val().toLowerCase(); | |
753 | - | |
754 | - if(query && query.length) { | |
755 | - if(selected_token) { | |
756 | - deselect_token($(selected_token), POSITION.AFTER); | |
757 | - } | |
769 | + if($(input).data("settings").tokenLimit !== null) { | |
770 | + input_box | |
771 | + .show() | |
772 | + .val(""); | |
773 | + focusWithTimeout(input_box); | |
774 | + } | |
775 | + | |
776 | + // Execute the onDelete callback if defined | |
777 | + if($.isFunction(callback)) { | |
778 | + callback.call(hiddenInput,token_data); | |
779 | + } | |
780 | + } | |
758 | 781 | |
759 | - if(query.length >= settings.minChars) { | |
760 | - show_dropdown_searching(); | |
761 | - clearTimeout(timeout); | |
782 | + // Update the hidden input box value | |
783 | + function update_hiddenInput(saved_tokens, hiddenInput) { | |
784 | + var token_values = $.map(saved_tokens, function (el) { | |
785 | + if(typeof $(input).data("settings").tokenValue == 'function') | |
786 | + return $(input).data("settings").tokenValue.call(this, el); | |
787 | + | |
788 | + return el[$(input).data("settings").tokenValue]; | |
789 | + }); | |
790 | + hiddenInput.val(token_values.join($(input).data("settings").tokenDelimiter)); | |
791 | + | |
792 | + } | |
793 | + | |
794 | + // Hide and clear the results dropdown | |
795 | + function hide_dropdown () { | |
796 | + dropdown.hide().empty(); | |
797 | + selected_dropdown_item = null; | |
798 | + } | |
799 | + | |
800 | + function show_dropdown() { | |
801 | + dropdown | |
802 | + .css({ | |
803 | + position: "absolute", | |
804 | + top: token_list.offset().top + token_list.outerHeight(true), | |
805 | + left: token_list.offset().left, | |
806 | + width: token_list.width(), | |
807 | + 'z-index': $(input).data("settings").zindex | |
808 | + }) | |
809 | + .show(); | |
810 | + } | |
811 | + | |
812 | + function show_dropdown_searching () { | |
813 | + if($(input).data("settings").searchingText) { | |
814 | + dropdown.html("<p>" + escapeHTML($(input).data("settings").searchingText) + "</p>"); | |
815 | + show_dropdown(); | |
816 | + } | |
817 | + } | |
818 | + | |
819 | + function show_dropdown_hint () { | |
820 | + if($(input).data("settings").hintText) { | |
821 | + dropdown.html("<p>" + escapeHTML($(input).data("settings").hintText) + "</p>"); | |
822 | + show_dropdown(); | |
823 | + } | |
824 | + } | |
825 | + | |
826 | + var regexp_special_chars = new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\-]', 'g'); | |
827 | + function regexp_escape(term) { | |
828 | + return term.replace(regexp_special_chars, '\\$&'); | |
829 | + } | |
762 | 830 | |
763 | - timeout = setTimeout(function(){ | |
764 | - run_search(query); | |
765 | - }, settings.searchDelay); | |
766 | - } else { | |
767 | - hide_dropdown(); | |
831 | + // Highlight the query part of the search term | |
832 | + function highlight_term(value, term) { | |
833 | + return value.replace( | |
834 | + new RegExp( | |
835 | + "(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(term) + ")(?![^<>]*>)(?![^&;]+;)", | |
836 | + "gi" | |
837 | + ), function(match, p1) { | |
838 | + return "<b>" + escapeHTML(p1) + "</b>"; | |
768 | 839 | } |
769 | - } else if (settings.permanentDropdown || settings.showAllResults) | |
770 | - run_search(''); | |
771 | - } | |
772 | - | |
773 | - // Do the actual search | |
774 | - function run_search(query) { | |
775 | - var cached_results = cache.get(query); | |
776 | - if(cached_results) { | |
777 | - populate_dropdown(query, cached_results); | |
778 | - } else { | |
779 | - // Are we doing an ajax search or local data search? | |
780 | - if(settings.url) { | |
781 | - // Extract exisiting get params | |
782 | - var ajax_params = {}; | |
783 | - ajax_params.data = {}; | |
784 | - if(settings.url.indexOf("?") > -1) { | |
785 | - var parts = settings.url.split("?"); | |
786 | - ajax_params.url = parts[0]; | |
787 | - | |
788 | - var param_array = parts[1].split("&"); | |
789 | - $.each(param_array, function (index, value) { | |
790 | - var kv = value.split("="); | |
791 | - ajax_params.data[kv[0]] = kv[1]; | |
792 | - }); | |
793 | - } else { | |
794 | - ajax_params.url = settings.url; | |
795 | - } | |
796 | - | |
797 | - // Prepare the request | |
798 | - ajax_params.data[settings.queryParam] = query; | |
799 | - ajax_params.type = settings.method; | |
800 | - ajax_params.dataType = settings.contentType; | |
801 | - if(settings.crossDomain) { | |
802 | - ajax_params.dataType = "jsonp"; | |
803 | - } | |
804 | - | |
805 | - // Attach the success callback | |
806 | - ajax_params.success = function(results) { | |
807 | - if($.isFunction(settings.onResult)) { | |
808 | - results = settings.onResult.call(hidden_input, results); | |
840 | + ); | |
841 | + } | |
842 | + | |
843 | + function find_value_and_highlight_term(template, value, term) { | |
844 | + return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(value) + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term)); | |
845 | + } | |
846 | + | |
847 | + // exclude existing tokens from dropdown, so the list is clearer | |
848 | + function excludeCurrent(results) { | |
849 | + if ($(input).data("settings").excludeCurrent) { | |
850 | + var currentTokens = $(input).data("tokenInputObject").getTokens(), | |
851 | + trimmedList = []; | |
852 | + if (currentTokens.length) { | |
853 | + $.each(results, function(index, value) { | |
854 | + var notFound = true; | |
855 | + $.each(currentTokens, function(cIndex, cValue) { | |
856 | + if (value[$(input).data("settings").propertyToSearch] == cValue[$(input).data("settings").propertyToSearch]) { | |
857 | + notFound = false; | |
858 | + return false; | |
859 | + } | |
860 | + }); | |
861 | + | |
862 | + if (notFound) { | |
863 | + trimmedList.push(value); | |
864 | + } | |
865 | + }); | |
866 | + results = trimmedList; | |
867 | + } | |
868 | + } | |
869 | + | |
870 | + return results; | |
871 | + } | |
872 | + | |
873 | + // Populate the results dropdown with some results | |
874 | + function populateDropdown (query, results) { | |
875 | + // exclude current tokens if configured | |
876 | + results = excludeCurrent(results); | |
877 | + | |
878 | + if(results && results.length) { | |
879 | + dropdown.empty(); | |
880 | + var dropdown_ul = $("<ul/>") | |
881 | + .appendTo(dropdown) | |
882 | + .mouseover(function (event) { | |
883 | + select_dropdown_item($(event.target).closest("li")); | |
884 | + }) | |
885 | + .mousedown(function (event) { | |
886 | + add_token($(event.target).closest("li").data("tokeninput")); | |
887 | + hiddenInput.change(); | |
888 | + return false; | |
889 | + }) | |
890 | + .hide(); | |
891 | + | |
892 | + if ($(input).data("settings").resultsLimit && results.length > $(input).data("settings").resultsLimit) { | |
893 | + results = results.slice(0, $(input).data("settings").resultsLimit); | |
894 | + } | |
895 | + | |
896 | + $.each(results, function(index, value) { | |
897 | + var this_li = $(input).data("settings").resultsFormatter(value); | |
898 | + | |
899 | + this_li = find_value_and_highlight_term(this_li ,value[$(input).data("settings").propertyToSearch], query); | |
900 | + this_li = $(this_li).appendTo(dropdown_ul); | |
901 | + | |
902 | + if(index % 2) { | |
903 | + this_li.addClass($(input).data("settings").classes.dropdownItem); | |
904 | + } else { | |
905 | + this_li.addClass($(input).data("settings").classes.dropdownItem2); | |
809 | 906 | } |
810 | - cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results); | |
811 | 907 | |
812 | - // only populate the dropdown if the results are associated with the active search query | |
813 | - if(input_box.val().toLowerCase() === query) { | |
814 | - populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results); | |
908 | + if(index === 0 && $(input).data("settings").autoSelectFirstResult) { | |
909 | + select_dropdown_item(this_li); | |
815 | 910 | } |
816 | - }; | |
817 | - | |
818 | - // Make the request | |
819 | - $.ajax(ajax_params); | |
820 | - } else if(settings.local_data) { | |
821 | - // Do the search through local data | |
822 | - var results = $.grep(settings.local_data, function (row) { | |
823 | - return row.name.toLowerCase().indexOf(query.toLowerCase()) > -1; | |
824 | - }); | |
825 | 911 | |
826 | - if($.isFunction(settings.onResult)) { | |
827 | - results = settings.onResult.call(hidden_input, results); | |
828 | - } | |
829 | - cache.add(query, results); | |
830 | - populate_dropdown(query, results); | |
831 | - } | |
832 | - } | |
833 | - } | |
834 | -}; | |
912 | + $.data(this_li.get(0), "tokeninput", value); | |
913 | + }); | |
914 | + | |
915 | + show_dropdown(); | |
916 | + | |
917 | + if($(input).data("settings").animateDropdown) { | |
918 | + dropdown_ul.slideDown("fast"); | |
919 | + } else { | |
920 | + dropdown_ul.show(); | |
921 | + } | |
922 | + } else { | |
923 | + if($(input).data("settings").noResultsText) { | |
924 | + dropdown.html("<p>" + escapeHTML($(input).data("settings").noResultsText) + "</p>"); | |
925 | + show_dropdown(); | |
926 | + } | |
927 | + } | |
928 | + } | |
929 | + | |
930 | + // Highlight an item in the results dropdown | |
931 | + function select_dropdown_item (item) { | |
932 | + if(item) { | |
933 | + if(selected_dropdown_item) { | |
934 | + deselect_dropdown_item($(selected_dropdown_item)); | |
935 | + } | |
835 | 936 | |
836 | -// Really basic cache for the results | |
837 | -$.TokenList.Cache = function (options) { | |
838 | - var settings = $.extend({ | |
839 | - max_size: 500 | |
840 | - }, options); | |
937 | + item.addClass($(input).data("settings").classes.selectedDropdownItem); | |
938 | + selected_dropdown_item = item.get(0); | |
939 | + } | |
940 | + } | |
941 | + | |
942 | + // Remove highlighting from an item in the results dropdown | |
943 | + function deselect_dropdown_item (item) { | |
944 | + item.removeClass($(input).data("settings").classes.selectedDropdownItem); | |
945 | + selected_dropdown_item = null; | |
946 | + } | |
841 | 947 | |
842 | - var data = {}; | |
843 | - var size = 0; | |
948 | + // Do a search and show the "searching" dropdown if the input is longer | |
949 | + // than $(input).data("settings").minChars | |
950 | + function do_search() { | |
951 | + var query = input_box.val(); | |
844 | 952 | |
845 | - var flush = function () { | |
846 | - data = {}; | |
847 | - size = 0; | |
953 | + if(query && query.length) { | |
954 | + if(selected_token) { | |
955 | + deselect_token($(selected_token), POSITION.AFTER); | |
956 | + } | |
957 | + | |
958 | + if(query.length >= $(input).data("settings").minChars) { | |
959 | + show_dropdown_searching(); | |
960 | + clearTimeout(timeout); | |
961 | + | |
962 | + timeout = setTimeout(function(){ | |
963 | + run_search(query); | |
964 | + }, $(input).data("settings").searchDelay); | |
965 | + } else { | |
966 | + hide_dropdown(); | |
967 | + } | |
968 | + } | |
969 | + } | |
970 | + | |
971 | + // Do the actual search | |
972 | + function run_search(query) { | |
973 | + var cache_key = query + computeURL(); | |
974 | + var cached_results = cache.get(cache_key); | |
975 | + if (cached_results) { | |
976 | + if ($.isFunction($(input).data("settings").onCachedResult)) { | |
977 | + cached_results = $(input).data("settings").onCachedResult.call(hiddenInput, cached_results); | |
978 | + } | |
979 | + populateDropdown(query, cached_results); | |
980 | + } else { | |
981 | + // Are we doing an ajax search or local data search? | |
982 | + if($(input).data("settings").url) { | |
983 | + var url = computeURL(); | |
984 | + // Extract existing get params | |
985 | + var ajax_params = {}; | |
986 | + ajax_params.data = {}; | |
987 | + if(url.indexOf("?") > -1) { | |
988 | + var parts = url.split("?"); | |
989 | + ajax_params.url = parts[0]; | |
990 | + | |
991 | + var param_array = parts[1].split("&"); | |
992 | + $.each(param_array, function (index, value) { | |
993 | + var kv = value.split("="); | |
994 | + ajax_params.data[kv[0]] = kv[1]; | |
995 | + }); | |
996 | + } else { | |
997 | + ajax_params.url = url; | |
998 | + } | |
999 | + | |
1000 | + // Prepare the request | |
1001 | + ajax_params.data[$(input).data("settings").queryParam] = query; | |
1002 | + ajax_params.type = $(input).data("settings").method; | |
1003 | + ajax_params.dataType = $(input).data("settings").contentType; | |
1004 | + if ($(input).data("settings").crossDomain) { | |
1005 | + ajax_params.dataType = "jsonp"; | |
1006 | + } | |
1007 | + | |
1008 | + // exclude current tokens? | |
1009 | + // send exclude list to the server, so it can also exclude existing tokens | |
1010 | + if ($(input).data("settings").excludeCurrent) { | |
1011 | + var currentTokens = $(input).data("tokenInputObject").getTokens(); | |
1012 | + var tokenList = $.map(currentTokens, function (el) { | |
1013 | + if(typeof $(input).data("settings").tokenValue == 'function') | |
1014 | + return $(input).data("settings").tokenValue.call(this, el); | |
1015 | + | |
1016 | + return el[$(input).data("settings").tokenValue]; | |
1017 | + }); | |
1018 | + | |
1019 | + ajax_params.data[$(input).data("settings").excludeCurrentParameter] = tokenList.join($(input).data("settings").tokenDelimiter); | |
1020 | + } | |
1021 | + | |
1022 | + // Attach the success callback | |
1023 | + ajax_params.success = function(results) { | |
1024 | + cache.add(cache_key, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results); | |
1025 | + if($.isFunction($(input).data("settings").onResult)) { | |
1026 | + results = $(input).data("settings").onResult.call(hiddenInput, results); | |
1027 | + } | |
1028 | + | |
1029 | + // only populate the dropdown if the results are associated with the active search query | |
1030 | + if(input_box.val() === query) { | |
1031 | + populateDropdown(query, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results); | |
1032 | + } | |
1033 | + }; | |
1034 | + | |
1035 | + // Provide a beforeSend callback | |
1036 | + if (settings.onSend) { | |
1037 | + settings.onSend(ajax_params); | |
1038 | + } | |
1039 | + | |
1040 | + // Make the request | |
1041 | + $.ajax(ajax_params); | |
1042 | + } else if($(input).data("settings").local_data) { | |
1043 | + // Do the search through local data | |
1044 | + var results = $.grep($(input).data("settings").local_data, function (row) { | |
1045 | + return row[$(input).data("settings").propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1; | |
1046 | + }); | |
1047 | + | |
1048 | + cache.add(cache_key, results); | |
1049 | + if($.isFunction($(input).data("settings").onResult)) { | |
1050 | + results = $(input).data("settings").onResult.call(hiddenInput, results); | |
1051 | + } | |
1052 | + populateDropdown(query, results); | |
1053 | + } | |
1054 | + } | |
1055 | + } | |
1056 | + | |
1057 | + // compute the dynamic URL | |
1058 | + function computeURL() { | |
1059 | + var settings = $(input).data("settings"); | |
1060 | + return typeof settings.url == 'function' ? settings.url.call(settings) : settings.url; | |
1061 | + } | |
1062 | + | |
1063 | + // Bring browser focus to the specified object. | |
1064 | + // Use of setTimeout is to get around an IE bug. | |
1065 | + // (See, e.g., http://stackoverflow.com/questions/2600186/focus-doesnt-work-in-ie) | |
1066 | + // | |
1067 | + // obj: a jQuery object to focus() | |
1068 | + function focusWithTimeout(object) { | |
1069 | + setTimeout( | |
1070 | + function() { | |
1071 | + object.focus(); | |
1072 | + }, | |
1073 | + 50 | |
1074 | + ); | |
1075 | + } | |
1076 | + }; | |
1077 | + | |
1078 | + // Really basic cache for the results | |
1079 | + $.TokenList.Cache = function (options) { | |
1080 | + var settings, data = {}, size = 0, flush; | |
1081 | + | |
1082 | + settings = $.extend({ max_size: 500 }, options); | |
1083 | + | |
1084 | + flush = function () { | |
1085 | + data = {}; | |
1086 | + size = 0; | |
848 | 1087 | }; |
849 | 1088 | |
850 | 1089 | this.add = function (query, results) { |
851 | - if(size > settings.max_size) { | |
852 | - flush(); | |
853 | - } | |
1090 | + if (size > settings.max_size) { | |
1091 | + flush(); | |
1092 | + } | |
854 | 1093 | |
855 | - if(!data[query]) { | |
856 | - size += 1; | |
857 | - } | |
1094 | + if (!data[query]) { | |
1095 | + size += 1; | |
1096 | + } | |
858 | 1097 | |
859 | - data[query] = results; | |
1098 | + data[query] = results; | |
860 | 1099 | }; |
861 | 1100 | |
862 | 1101 | this.get = function (query) { |
863 | - return data[query]; | |
1102 | + return data[query]; | |
864 | 1103 | }; |
865 | -}; | |
1104 | + }; | |
1105 | + | |
866 | 1106 | }(jQuery)); | ... | ... |
public/stylesheets/profile-activity.scss
... | ... | @@ -3,7 +3,8 @@ |
3 | 3 | padding-left: 0; |
4 | 4 | clear: both; |
5 | 5 | } |
6 | -#profile-activity li, #profile-network li, #profile-wall li { | |
6 | + | |
7 | +.profile-activities li { | |
7 | 8 | display: block; |
8 | 9 | padding: 3px 0px; |
9 | 10 | margin-bottom: 3px; |
... | ... | @@ -367,6 +368,9 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img { |
367 | 368 | .profile-wall-message { |
368 | 369 | margin: 0; |
369 | 370 | } |
371 | +.limited-text-area { | |
372 | + margin-bottom: 15px; | |
373 | +} | |
370 | 374 | .limited-text-area p { |
371 | 375 | margin: 0; |
372 | 376 | font-size: 11px; |
... | ... | @@ -378,7 +382,8 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img { |
378 | 382 | margin-bottom: 10px; |
379 | 383 | } |
380 | 384 | #leave_scrap_content_limit, #leave_scrap_content_left { |
381 | - float: left; | |
385 | + float: right; | |
386 | + margin-right: 2px; | |
382 | 387 | } |
383 | 388 | #leave_scrap { |
384 | 389 | float: left; |
... | ... | @@ -392,6 +397,9 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img { |
392 | 397 | #leave_scrap .loading textarea { |
393 | 398 | background: url('/images/loading-small.gif') 50% 50% no-repeat; |
394 | 399 | } |
400 | +#leave_scrap .submit { | |
401 | + margin-top: 5px; | |
402 | +} | |
395 | 403 | .profile-send-reply { |
396 | 404 | color: #aaa; |
397 | 405 | } |
... | ... | @@ -598,7 +606,6 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img { |
598 | 606 | } |
599 | 607 | |
600 | 608 | #profile-wall #leave_scrap textarea { |
601 | - width: 442px; | |
602 | 609 | height: 100px |
603 | 610 | } |
604 | 611 | .profile-wall-scrap-replies { | ... | ... |
public/stylesheets/vendor/token-input-facebook.css
... | ... | @@ -7,7 +7,7 @@ ul.token-input-list-facebook { |
7 | 7 | border: 1px solid #8496ba; |
8 | 8 | cursor: text; |
9 | 9 | font-size: 12px; |
10 | - font-family: Verdana; | |
10 | + font-family: Verdana, sans-serif; | |
11 | 11 | min-height: 1px; |
12 | 12 | z-index: 999; |
13 | 13 | margin: 0; |
... | ... | @@ -80,7 +80,7 @@ div.token-input-dropdown-facebook { |
80 | 80 | border-bottom: 1px solid #ccc; |
81 | 81 | cursor: default; |
82 | 82 | font-size: 11px; |
83 | - font-family: Verdana; | |
83 | + font-family: Verdana, sans-serif; | |
84 | 84 | z-index: 1; |
85 | 85 | } |
86 | 86 | |
... | ... | @@ -119,8 +119,4 @@ div.token-input-dropdown-facebook ul li em { |
119 | 119 | div.token-input-dropdown-facebook ul li.token-input-selected-dropdown-item-facebook { |
120 | 120 | background-color: #3b5998; |
121 | 121 | color: #fff; |
122 | -} | |
123 | -.token-input-blur-text-facebook { | |
124 | - font-style: italic; | |
125 | - color: #AAA; | |
126 | -} | |
122 | +} | |
127 | 123 | \ No newline at end of file | ... | ... |
test/functional/profile_controller_test.rb
... | ... | @@ -7,6 +7,7 @@ class ProfileControllerTest < ActionController::TestCase |
7 | 7 | |
8 | 8 | self.default_params = {profile: 'testuser'} |
9 | 9 | def setup |
10 | + @controller = ProfileController.new | |
10 | 11 | @profile = create_user('testuser').person |
11 | 12 | end |
12 | 13 | attr_reader :profile |
... | ... | @@ -759,7 +760,7 @@ class ProfileControllerTest < ActionController::TestCase |
759 | 760 | |
760 | 761 | login_as(profile.identifier) |
761 | 762 | get :index, :profile => p1.identifier |
762 | - assert_nil assigns(:activities) | |
763 | + assert assigns(:activities).blank? | |
763 | 764 | end |
764 | 765 | |
765 | 766 | should 'see the activities_items paginated' do |
... | ... | @@ -954,14 +955,14 @@ class ProfileControllerTest < ActionController::TestCase |
954 | 955 | should 'not have activities defined if not logged in' do |
955 | 956 | p1= fast_create(Person) |
956 | 957 | get :index, :profile => p1.identifier |
957 | - assert_nil assigns(:actvities) | |
958 | + assert assigns(:actvities).blank? | |
958 | 959 | end |
959 | 960 | |
960 | 961 | should 'not have activities defined if logged in but is not following profile' do |
961 | 962 | login_as(profile.identifier) |
962 | 963 | p1= fast_create(Person) |
963 | 964 | get :index, :profile => p1.identifier |
964 | - assert_nil assigns(:activities) | |
965 | + assert assigns(:activities).blank? | |
965 | 966 | end |
966 | 967 | |
967 | 968 | should 'have activities defined if logged in and is following profile' do |
... | ... | @@ -2045,4 +2046,218 @@ class ProfileControllerTest < ActionController::TestCase |
2045 | 2046 | assert_redirected_to "/some/url" |
2046 | 2047 | end |
2047 | 2048 | |
2049 | + should "search followed people or circles" do | |
2050 | + login_as(@profile.identifier) | |
2051 | + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person) | |
2052 | + c2 = Circle.create!(:name => 'Work', :person => @profile, :profile_type => Person) | |
2053 | + p1 = create_user('emily').person | |
2054 | + p2 = create_user('wollie').person | |
2055 | + p3 = create_user('mary').person | |
2056 | + ProfileFollower.create!(:profile => p1, :circle => c2) | |
2057 | + ProfileFollower.create!(:profile => p2, :circle => c1) | |
2058 | + ProfileFollower.create!(:profile => p3, :circle => c1) | |
2059 | + | |
2060 | + get :search_followed, :q => 'mily' | |
2061 | + assert_equal 'Family (Circle)', json_response[0]['name'] | |
2062 | + assert_equal 'Circle', json_response[0]['class'] | |
2063 | + assert_equal "Circle_#{c1.id}", json_response[0]['id'] | |
2064 | + assert_equal 'emily (Person)', json_response[1]['name'] | |
2065 | + assert_equal 'Person', json_response[1]['class'] | |
2066 | + assert_equal "Person_#{p1.id}", json_response[1]['id'] | |
2067 | + | |
2068 | + get :search_followed, :q => 'wo' | |
2069 | + assert_equal 'Work (Circle)', json_response[0]['name'] | |
2070 | + assert_equal 'Circle', json_response[0]['class'] | |
2071 | + assert_equal "Circle_#{c2.id}", json_response[0]['id'] | |
2072 | + assert_equal 'wollie (Person)', json_response[1]['name'] | |
2073 | + assert_equal 'Person', json_response[1]['class'] | |
2074 | + assert_equal "Person_#{p2.id}", json_response[1]['id'] | |
2075 | + | |
2076 | + get :search_followed, :q => 'mar' | |
2077 | + assert_equal 'mary (Person)', json_response[0]['name'] | |
2078 | + assert_equal 'Person', json_response[0]['class'] | |
2079 | + assert_equal "Person_#{p3.id}", json_response[0]['id'] | |
2080 | + end | |
2081 | + | |
2082 | + should 'treat followed entries' do | |
2083 | + login_as(@profile.identifier) | |
2084 | + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person) | |
2085 | + p1 = create_user('emily').person | |
2086 | + p2 = create_user('wollie').person | |
2087 | + p3 = create_user('mary').person | |
2088 | + ProfileFollower.create!(:profile => p1, :circle => c1) | |
2089 | + ProfileFollower.create!(:profile => p3, :circle => c1) | |
2090 | + | |
2091 | + entries = "Circle_#{c1.id},Person_#{p1.id},Person_#{p2.id}" | |
2092 | + @controller.stubs(:profile).returns(@profile) | |
2093 | + @controller.stubs(:user).returns(@profile) | |
2094 | + marked_people = @controller.send(:treat_followed_entries, entries) | |
2095 | + | |
2096 | + assert_equivalent [p1,p2,p3], marked_people | |
2097 | + end | |
2098 | + | |
2099 | + should 'return empty followed entries if the user is not on his wall' do | |
2100 | + login_as(@profile.identifier) | |
2101 | + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person) | |
2102 | + p1 = create_user('emily').person | |
2103 | + p2 = create_user('wollie').person | |
2104 | + p3 = create_user('mary').person | |
2105 | + ProfileFollower.create!(:profile => p1, :circle => c1) | |
2106 | + ProfileFollower.create!(:profile => p3, :circle => c1) | |
2107 | + | |
2108 | + entries = "Circle_#{c1.id},Person_#{p1.id},Person_#{p2.id}" | |
2109 | + @controller.stubs(:profile).returns(@profile) | |
2110 | + @controller.stubs(:user).returns(p1) | |
2111 | + marked_people = @controller.send(:treat_followed_entries, entries) | |
2112 | + | |
2113 | + assert_empty marked_people | |
2114 | + end | |
2115 | + | |
2116 | + should 'leave private scrap' do | |
2117 | + login_as(@profile.identifier) | |
2118 | + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person) | |
2119 | + p1 = create_user('emily').person | |
2120 | + p2 = create_user('wollie').person | |
2121 | + ProfileFollower.create!(:profile => p1, :circle => c1) | |
2122 | + ProfileFollower.create!(:profile => p2, :circle => c1) | |
2123 | + | |
2124 | + content = 'Remember my birthday!' | |
2125 | + | |
2126 | + post :leave_scrap, :profile => @profile.identifier, :scrap => {:content => content}, :filter_followed => "Person_#{p1.id},Person_#{p2.id}" | |
2127 | + | |
2128 | + scrap = Scrap.last | |
2129 | + assert_equal content, scrap.content | |
2130 | + assert_equivalent [p1,p2], scrap.marked_people | |
2131 | + end | |
2132 | + | |
2133 | + should 'list private scraps on wall for marked people' do | |
2134 | + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person) | |
2135 | + p1 = create_user('emily').person | |
2136 | + ProfileFollower.create!(:profile => p1, :circle => c1) | |
2137 | + p1.add_friend(@profile) | |
2138 | + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1]) | |
2139 | + scrap_activity = ProfileActivity.where(:activity => scrap).first | |
2140 | + login_as(p1.identifier) | |
2141 | + | |
2142 | + get :index, :profile => @profile.identifier | |
2143 | + | |
2144 | + assert assigns(:activities).include?(scrap_activity) | |
2145 | + end | |
2146 | + | |
2147 | + should 'not list private scraps on wall for not marked people' do | |
2148 | + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person) | |
2149 | + p1 = create_user('emily').person | |
2150 | + p2 = create_user('wollie').person | |
2151 | + not_marked = create_user('jack').person | |
2152 | + not_marked.add_friend(@profile) | |
2153 | + ProfileFollower.create!(:profile => p1, :circle => c1) | |
2154 | + ProfileFollower.create!(:profile => p2, :circle => c1) | |
2155 | + ProfileFollower.create!(:profile => not_marked, :circle => c1) | |
2156 | + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1,p2]) | |
2157 | + scrap_activity = ProfileActivity.where(:activity => scrap).first | |
2158 | + login_as(not_marked.identifier) | |
2159 | + | |
2160 | + get :index, :profile => @profile.identifier | |
2161 | + | |
2162 | + assert !assigns(:activities).include?(scrap_activity) | |
2163 | + end | |
2164 | + | |
2165 | + should 'list private scraps on wall for creator' do | |
2166 | + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person) | |
2167 | + p1 = create_user('emily').person | |
2168 | + ProfileFollower.create!(:profile => p1, :circle => c1) | |
2169 | + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1]) | |
2170 | + scrap_activity = ProfileActivity.where(:activity => scrap).first | |
2171 | + login_as(@profile.identifier) | |
2172 | + | |
2173 | + get :index, :profile => @profile.identifier | |
2174 | + | |
2175 | + assert assigns(:activities).include?(scrap_activity) | |
2176 | + end | |
2177 | + | |
2178 | + should 'list private scraps on wall for environment administrator' do | |
2179 | + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person) | |
2180 | + p1 = create_user('emily').person | |
2181 | + admin = create_user('env-admin').person | |
2182 | + env = @profile.environment | |
2183 | + env.add_admin(admin) | |
2184 | + admin.add_friend(@profile) | |
2185 | + ProfileFollower.create!(:profile => p1, :circle => c1) | |
2186 | + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1]) | |
2187 | + scrap_activity = ProfileActivity.where(:activity => scrap).first | |
2188 | + login_as(admin.identifier) | |
2189 | + | |
2190 | + get :index, :profile => @profile.identifier | |
2191 | + | |
2192 | + assert assigns(:activities).include?(scrap_activity) | |
2193 | + end | |
2194 | + | |
2195 | + should 'list private scraps on network for marked people' do | |
2196 | + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person) | |
2197 | + p1 = create_user('emily').person | |
2198 | + p2 = create_user('wollie').person | |
2199 | + p2.add_friend(p1) | |
2200 | + p1.add_friend(p2) | |
2201 | + ProfileFollower.create!(:profile => p1, :circle => c1) | |
2202 | + ProfileFollower.create!(:profile => p2, :circle => c1) | |
2203 | + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1,p2]) | |
2204 | + process_delayed_job_queue | |
2205 | + scrap_activity = p1.tracked_notifications.where(:target => scrap).first | |
2206 | + login_as(p2.identifier) | |
2207 | + | |
2208 | + get :index, :profile => p1.identifier | |
2209 | + | |
2210 | + assert assigns(:network_activities).include?(scrap_activity) | |
2211 | + end | |
2212 | + | |
2213 | + should 'not list private scraps on network for not marked people' do | |
2214 | + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person) | |
2215 | + p1 = create_user('emily').person | |
2216 | + not_marked = create_user('jack').person | |
2217 | + not_marked.add_friend(p1) | |
2218 | + ProfileFollower.create!(:profile => p1, :circle => c1) | |
2219 | + ProfileFollower.create!(:profile => not_marked, :circle => c1) | |
2220 | + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1]) | |
2221 | + process_delayed_job_queue | |
2222 | + scrap_activity = p1.tracked_notifications.where(:target => scrap).first | |
2223 | + login_as(not_marked.identifier) | |
2224 | + | |
2225 | + get :index, :profile => p1.identifier | |
2226 | + | |
2227 | + assert !assigns(:network_activities).include?(scrap_activity) | |
2228 | + end | |
2229 | + | |
2230 | + should 'list private scraps on network for creator' do | |
2231 | + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person) | |
2232 | + p1 = create_user('emily').person | |
2233 | + p1.add_friend(@profile) | |
2234 | + ProfileFollower.create!(:profile => p1, :circle => c1) | |
2235 | + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1]) | |
2236 | + process_delayed_job_queue | |
2237 | + scrap_activity = p1.tracked_notifications.where(:target => scrap).first | |
2238 | + login_as(@profile.identifier) | |
2239 | + | |
2240 | + get :index, :profile => p1.identifier | |
2241 | + | |
2242 | + assert assigns(:network_activities).include?(scrap_activity) | |
2243 | + end | |
2244 | + | |
2245 | + should 'list private scraps on network for environment admin' do | |
2246 | + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person) | |
2247 | + p1 = create_user('emily').person | |
2248 | + admin = create_user('env-admin').person | |
2249 | + env = @profile.environment | |
2250 | + env.add_admin(admin) | |
2251 | + admin.add_friend(p1) | |
2252 | + ProfileFollower.create!(:profile => p1, :circle => c1) | |
2253 | + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1]) | |
2254 | + process_delayed_job_queue | |
2255 | + scrap_activity = p1.tracked_notifications.where(:target => scrap).first | |
2256 | + login_as(admin.identifier) | |
2257 | + | |
2258 | + get :index, :profile => p1.identifier | |
2259 | + | |
2260 | + assert assigns(:network_activities).include?(scrap_activity) | |
2261 | + end | |
2262 | + | |
2048 | 2263 | end | ... | ... |
test/test_helper.rb
test/unit/notify_activity_to_profiles_job_test.rb
... | ... | @@ -52,6 +52,27 @@ class NotifyActivityToProfilesJobTest < ActiveSupport::TestCase |
52 | 52 | end |
53 | 53 | end |
54 | 54 | |
55 | + should 'notify only marked people on marked scraps' do | |
56 | + profile = create_user('scrap-creator').person | |
57 | + c1 = Circle.create!(:name => 'Family', :person => profile, :profile_type => Person) | |
58 | + p1 = create_user('emily').person | |
59 | + p2 = create_user('wollie').person | |
60 | + not_marked = create_user('jack').person | |
61 | + not_marked.add_friend(p1) | |
62 | + not_marked.add_friend(p2) | |
63 | + not_marked.add_friend(profile) | |
64 | + ProfileFollower.create!(:profile => p1, :circle => c1) | |
65 | + ProfileFollower.create!(:profile => p2, :circle => c1) | |
66 | + ProfileFollower.create!(:profile => not_marked, :circle => c1) | |
67 | + | |
68 | + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => profile.id, :receiver_id => profile.id, :marked_people => [p1,p2]) | |
69 | + process_delayed_job_queue | |
70 | + | |
71 | + assert p1.tracked_notifications.where(:target => scrap).present? | |
72 | + assert p2.tracked_notifications.where(:target => scrap).present? | |
73 | + assert not_marked.tracked_notifications.where(:target => scrap).blank? | |
74 | + end | |
75 | + | |
55 | 76 | should 'not notify the communities members' do |
56 | 77 | person = fast_create(Person) |
57 | 78 | community = fast_create(Community) | ... | ... |