Commit 988d1e617dbd7f95de6bb822f95a2a7a8663fb5e

Authored by Rodrigo Souto
Committed by Marcos Pereira
1 parent 020591c2
Exists in fix_sign_up_form

private-scraps: send scrap only to marked people

(cherry picked from commit fca616032cf3c305806a8a121639ea6dcb90fd93)
app/controllers/public/profile_controller.rb
@@ -19,6 +19,11 @@ class ProfileController < PublicController @@ -19,6 +19,11 @@ class ProfileController < PublicController
19 @network_activities = @profile.tracked_notifications.visible.paginate(:per_page => 15, :page => params[:page]) if @network_activities.empty? 19 @network_activities = @profile.tracked_notifications.visible.paginate(:per_page => 15, :page => params[:page]) if @network_activities.empty?
20 @activities = @profile.activities.paginate(:per_page => 15, :page => params[:page]) 20 @activities = @profile.activities.paginate(:per_page => 15, :page => params[:page])
21 end 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 @tags = profile.article_tags 27 @tags = profile.article_tags
23 allow_access_to_page 28 allow_access_to_page
24 end 29 end
@@ -231,6 +236,7 @@ class ProfileController < PublicController @@ -231,6 +236,7 @@ class ProfileController < PublicController
231 @scrap = Scrap.new(params[:scrap]) 236 @scrap = Scrap.new(params[:scrap])
232 @scrap.sender= sender 237 @scrap.sender= sender
233 @scrap.receiver= receiver 238 @scrap.receiver= receiver
  239 + @scrap.marked_people = treat_followed_entries(params[:filter_followed])
234 @tab_action = params[:tab_action] 240 @tab_action = params[:tab_action]
235 @message = @scrap.save ? _("Message successfully sent.") : _("You can't leave an empty message.") 241 @message = @scrap.save ? _("Message successfully sent.") : _("You can't leave an empty message.")
236 activities = @profile.activities.paginate(:per_page => 15, :page => params[:page]) if params[:not_load_scraps].nil? 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,6 +259,14 @@ class ProfileController < PublicController
253 end 259 end
254 end 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 def view_more_activities 270 def view_more_activities
257 @activities = @profile.activities.paginate(:per_page => 10, :page => params[:page]) 271 @activities = @profile.activities.paginate(:per_page => 10, :page => params[:page])
258 render :partial => 'profile_activities_list', :locals => {:activities => @activities} 272 render :partial => 'profile_activities_list', :locals => {:activities => @activities}
@@ -434,7 +448,6 @@ class ProfileController < PublicController @@ -434,7 +448,6 @@ class ProfileController < PublicController
434 end 448 end
435 end 449 end
436 450
437 -  
438 protected 451 protected
439 452
440 def check_access_to_profile 453 def check_access_to_profile
@@ -480,4 +493,41 @@ class ProfileController < PublicController @@ -480,4 +493,41 @@ class ProfileController < PublicController
480 render_not_found unless profile.allow_followers? 493 render_not_found unless profile.allow_followers?
481 end 494 end
482 495
  496 + def treat_followed_entries(entries)
  497 + return [] if entries.blank? || profile != user
  498 +
  499 + followed = []
  500 + entries.split(',').map do |entry|
  501 + klass, identifier = entry.split('_')
  502 + case klass
  503 + when 'Person'
  504 + followed << Person.find(identifier)
  505 + when 'Circle'
  506 + circle = Circle.find(identifier)
  507 + followed += Profile.in_circle(circle)
  508 + end
  509 + end
  510 + followed.uniq
  511 + end
  512 +
  513 + def filter_private_scraps(activities)
  514 + activities = Array(activities)
  515 + activities.delete_if do |item|
  516 + if item.kind_of?(ProfileActivity)
  517 + target = item.activity
  518 + owner = profile
  519 + else
  520 + target = item.target
  521 + owner = item.user
  522 + end
  523 + !environment.admins.include?(user) &&
  524 + #TODO Consider this if allowing to mark people on organization's wall
  525 + #!profile.admins.include?(user) &&
  526 + owner != user &&
  527 + target.is_a?(Scrap) &&
  528 + target.marked_people.present? &&
  529 + !target.marked_people.include?(user)
  530 + end
  531 + activities
  532 + end
483 end 533 end
app/helpers/article_helper.rb
@@ -160,6 +160,10 @@ module ArticleHelper @@ -160,6 +160,10 @@ module ArticleHelper
160 array.map { |object| {:label => object.name, :value => object.name} } 160 array.map { |object| {:label => object.name, :value => object.name} }
161 end 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 def cms_label_for_new_children 167 def cms_label_for_new_children
164 _('New article') 168 _('New article')
165 end 169 end
app/helpers/token_helper.rb
@@ -5,10 +5,11 @@ module TokenHelper @@ -5,10 +5,11 @@ module TokenHelper
5 end 5 end
6 6
7 def token_input_field_tag(name, element_id, search_action, options = {}, text_field_options = {}, html_options = {}) 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 options[:hint_text] ||= _("Type in a search term") 9 options[:hint_text] ||= _("Type in a search term")
10 options[:no_results_text] ||= _("No results") 10 options[:no_results_text] ||= _("No results")
11 options[:searching_text] ||= _("Searching...") 11 options[:searching_text] ||= _("Searching...")
  12 + options[:placeholder] ||= 'null'
12 options[:search_delay] ||= 1000 13 options[:search_delay] ||= 1000
13 options[:prevent_duplicates] ||= true 14 options[:prevent_duplicates] ||= true
14 options[:backspace_delete_item] ||= false 15 options[:backspace_delete_item] ||= false
@@ -20,6 +21,9 @@ module TokenHelper @@ -20,6 +21,9 @@ module TokenHelper
20 options[:on_delete] ||= 'null' 21 options[:on_delete] ||= 'null'
21 options[:on_ready] ||= 'null' 22 options[:on_ready] ||= 'null'
22 options[:query_param] ||= 'q' 23 options[:query_param] ||= 'q'
  24 + options[:theme] ||= 'null'
  25 + options[:results_formatter] ||= 'null'
  26 + options[:token_formatter] ||= 'null'
23 27
24 result = text_field_tag(name, nil, text_field_options.merge(html_options.merge({:id => element_id}))) 28 result = text_field_tag(name, nil, text_field_options.merge(html_options.merge({:id => element_id})))
25 result += javascript_tag("jQuery('##{element_id}') 29 result += javascript_tag("jQuery('##{element_id}')
@@ -29,6 +33,7 @@ module TokenHelper @@ -29,6 +33,7 @@ module TokenHelper
29 hintText: #{options[:hint_text].to_json}, 33 hintText: #{options[:hint_text].to_json},
30 noResultsText: #{options[:no_results_text].to_json}, 34 noResultsText: #{options[:no_results_text].to_json},
31 searchingText: #{options[:searching_text].to_json}, 35 searchingText: #{options[:searching_text].to_json},
  36 + placeholder: #{options[:placeholder].to_json},
32 searchDelay: #{options[:search_delay].to_json}, 37 searchDelay: #{options[:search_delay].to_json},
33 preventDuplicates: #{options[:prevent_duplicates].to_json}, 38 preventDuplicates: #{options[:prevent_duplicates].to_json},
34 backspaceDeleteItem: #{options[:backspace_delete_item].to_json}, 39 backspaceDeleteItem: #{options[:backspace_delete_item].to_json},
@@ -39,6 +44,9 @@ module TokenHelper @@ -39,6 +44,9 @@ module TokenHelper
39 onAdd: #{options[:on_add]}, 44 onAdd: #{options[:on_add]},
40 onDelete: #{options[:on_delete]}, 45 onDelete: #{options[:on_delete]},
41 onReady: #{options[:on_ready]}, 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 result += javascript_tag("jQuery('##{element_id}').focus();") if options[:focus] 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 &lt; Struct.new(:tracked_action_id) @@ -19,8 +19,13 @@ class NotifyActivityToProfilesJob &lt; Struct.new(:tracked_action_id)
19 # Notify the user 19 # Notify the user
20 ActionTrackerNotification.create(:profile_id => tracked_action.user.id, :action_tracker_id => tracked_action.id) 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 if tracked_action.user.is_a? Organization 30 if tracked_action.user.is_a? Organization
26 ActionTrackerNotification.connection.execute "insert into action_tracker_notifications(profile_id, action_tracker_id) " + 31 ActionTrackerNotification.connection.execute "insert into action_tracker_notifications(profile_id, action_tracker_id) " +
app/models/circle.rb
1 class Circle < ApplicationRecord 1 class Circle < ApplicationRecord
  2 + SEARCHABLE_FIELDS = {
  3 + :name => {:label => _('Name'), :weight => 1}
  4 + }
  5 +
  6 + _('Circle')
  7 +
2 has_many :profile_followers 8 has_many :profile_followers
3 belongs_to :person 9 belongs_to :person
4 10
app/models/person.rb
@@ -121,6 +121,8 @@ class Person &lt; Profile @@ -121,6 +121,8 @@ class Person &lt; Profile
121 where 'profile_suggestions.suggestion_type = ? AND profile_suggestions.enabled = ?', 'Community', true 121 where 'profile_suggestions.suggestion_type = ? AND profile_suggestions.enabled = ?', 'Community', true
122 }, through: :suggested_profiles, source: :suggestion 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 scope :more_popular, -> { order 'friends_count DESC' } 126 scope :more_popular, -> { order 'friends_count DESC' }
125 127
126 scope :abusers, -> { 128 scope :abusers, -> {
app/models/scrap.rb
@@ -2,7 +2,7 @@ class Scrap &lt; ApplicationRecord @@ -2,7 +2,7 @@ class Scrap &lt; ApplicationRecord
2 2
3 include SanitizeHelper 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 SEARCHABLE_FIELDS = { 7 SEARCHABLE_FIELDS = {
8 :content => {:label => _('Content'), :weight => 1}, 8 :content => {:label => _('Content'), :weight => 1},
@@ -19,6 +19,8 @@ class Scrap &lt; ApplicationRecord @@ -19,6 +19,8 @@ class Scrap &lt; ApplicationRecord
19 where profile_activities: {activity_type: 'Scrap'} 19 where profile_activities: {activity_type: 'Scrap'}
20 }, foreign_key: :activity_id, dependent: :destroy 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 after_create :create_activity 24 after_create :create_activity
23 after_update :update_activity 25 after_update :update_activity
24 26
app/views/profile/_profile_wall.html.erb
1 <h3><%= _("%s's wall") % @profile.name %></h3> 1 <h3><%= _("%s's wall") % @profile.name %></h3>
2 <div id='leave_scrap'> 2 <div id='leave_scrap'>
3 <%= flash[:error] %> 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 <%= submit_button :new, _('Share') %> 9 <%= submit_button :new, _('Share') %>
7 <% end %> 10 <% end %>
8 </div> 11 </div>
db/migrate/20160705162914_create_private_scraps.rb 0 → 100644
@@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
  1 +class CreatePrivateScraps < ActiveRecord::Migration
  2 + def change
  3 + create_table :private_scraps do |t|
  4 + t.references :person
  5 + t.references :scrap
  6 + end
  7 + end
  8 +end
@@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
11 # 11 #
12 # It's strongly recommended that you check this file into your version control system. 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 # These are extensions that must be enabled in order to support this database 16 # These are extensions that must be enabled in order to support this database
17 enable_extension "plpgsql" 17 enable_extension "plpgsql"
@@ -524,6 +524,11 @@ ActiveRecord::Schema.define(version: 20160608123748) do @@ -524,6 +524,11 @@ ActiveRecord::Schema.define(version: 20160608123748) do
524 t.datetime "updated_at" 524 t.datetime "updated_at"
525 end 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 create_table "product_qualifiers", force: :cascade do |t| 532 create_table "product_qualifiers", force: :cascade do |t|
528 t.integer "product_id" 533 t.integer "product_id"
529 t.integer "qualifier_id" 534 t.integer "qualifier_id"
public/javascripts/jquery.tokeninput.js
1 /* 1 /*
2 * jQuery Plugin: Tokenizing Autocomplete Text Entry 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 * Copyright (c) 2009 James Smith (http://loopj.com) 5 * Copyright (c) 2009 James Smith (http://loopj.com)
7 * Licensed jointly under the GPL and MIT licenses, 6 * Licensed jointly under the GPL and MIT licenses,
8 * choose which one suits your project best! 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: "&times;", 10 +;(function ($) {
  11 + var DEFAULT_SETTINGS = {
  12 + // Search settings
  13 + method: "GET",
  14 + queryParam: "q",
19 searchDelay: 300, 15 searchDelay: 300,
20 minChars: 1, 16 minChars: 1,
21 - permanentDropdown: false,  
22 - showAllResults: false,  
23 - tokenLimit: null, 17 + propertyToSearch: "name",
24 jsonContainer: null, 18 jsonContainer: null,
25 - method: "GET",  
26 contentType: "json", 19 contentType: "json",
27 - queryParam: "q",  
28 - tokenDelimiter: ",",  
29 - preventDuplicates: false, 20 + excludeCurrent: false,
  21 + excludeCurrentParameter: "x",
  22 +
  23 + // Prepopulation settings
30 prePopulate: null, 24 prePopulate: null,
31 processPrePopulate: false, 25 processPrePopulate: false,
  26 +
  27 + // Display settings
  28 + hintText: "Type in a search term",
  29 + noResultsText: "No results",
  30 + searchingText: "Searching...",
  31 + deleteText: "&#215;",
32 animateDropdown: true, 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 onResult: null, 62 onResult: null,
  63 + onCachedResult: null,
35 onAdd: null, 64 onAdd: null,
  65 + onFreeTaggingAdd: null,
36 onDelete: null, 66 onDelete: null,
  67 + onReady: null,
  68 +
  69 + // Other settings
37 idPrefix: "token-input-", 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 + '&' : '&amp;',
  121 + '<' : '&lt;',
  122 + '>' : '&gt;',
  123 + '"' : '&quot;',
  124 + "'" : '&#x27;',
  125 + '/' : '&#x2F;'
  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 } else 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 hide_dropdown(); 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 previous_token = input_token.prev(); 286 previous_token = input_token.prev();
203 next_token = input_token.next(); 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 // Check if there is a previous/next token and it is selected 291 // Check if there is a previous/next token and it is selected
207 if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) { 292 if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) {
208 deselect_token($(selected_token), POSITION.BEFORE); 293 deselect_token($(selected_token), POSITION.BEFORE);
@@ -217,650 +302,805 @@ $.TokenList = function (input, url_or_data, options) { @@ -217,650 +302,805 @@ $.TokenList = function (input, url_or_data, options) {
217 select_token($(next_token.get(0))); 302 select_token($(next_token.get(0)));
218 } 303 }
219 } else { 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 break; 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 return false; 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 } else { 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 return false; 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 break; 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, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');  
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 } else { 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 this.add = function (query, results) { 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 this.get = function (query) { 1101 this.get = function (query) {
863 - return data[query]; 1102 + return data[query];
864 }; 1103 };
865 -}; 1104 + };
  1105 +
866 }(jQuery)); 1106 }(jQuery));
public/stylesheets/profile-activity.scss
@@ -3,7 +3,8 @@ @@ -3,7 +3,8 @@
3 padding-left: 0; 3 padding-left: 0;
4 clear: both; 4 clear: both;
5 } 5 }
6 -#profile-activity li, #profile-network li, #profile-wall li { 6 +
  7 +.profile-activities li {
7 display: block; 8 display: block;
8 padding: 3px 0px; 9 padding: 3px 0px;
9 margin-bottom: 3px; 10 margin-bottom: 3px;
@@ -367,6 +368,9 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img { @@ -367,6 +368,9 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img {
367 .profile-wall-message { 368 .profile-wall-message {
368 margin: 0; 369 margin: 0;
369 } 370 }
  371 +.limited-text-area {
  372 + margin-bottom: 15px;
  373 +}
370 .limited-text-area p { 374 .limited-text-area p {
371 margin: 0; 375 margin: 0;
372 font-size: 11px; 376 font-size: 11px;
@@ -378,7 +382,8 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img { @@ -378,7 +382,8 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img {
378 margin-bottom: 10px; 382 margin-bottom: 10px;
379 } 383 }
380 #leave_scrap_content_limit, #leave_scrap_content_left { 384 #leave_scrap_content_limit, #leave_scrap_content_left {
381 - float: left; 385 + float: right;
  386 + margin-right: 2px;
382 } 387 }
383 #leave_scrap { 388 #leave_scrap {
384 float: left; 389 float: left;
@@ -392,6 +397,9 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img { @@ -392,6 +397,9 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img {
392 #leave_scrap .loading textarea { 397 #leave_scrap .loading textarea {
393 background: url('/images/loading-small.gif') 50% 50% no-repeat; 398 background: url('/images/loading-small.gif') 50% 50% no-repeat;
394 } 399 }
  400 +#leave_scrap .submit {
  401 + margin-top: 5px;
  402 +}
395 .profile-send-reply { 403 .profile-send-reply {
396 color: #aaa; 404 color: #aaa;
397 } 405 }
@@ -598,7 +606,6 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img { @@ -598,7 +606,6 @@ li.profile-activity-item.upload_image .activity-gallery-images-count-1 img {
598 } 606 }
599 607
600 #profile-wall #leave_scrap textarea { 608 #profile-wall #leave_scrap textarea {
601 - width: 442px;  
602 height: 100px 609 height: 100px
603 } 610 }
604 .profile-wall-scrap-replies { 611 .profile-wall-scrap-replies {
public/stylesheets/vendor/token-input-facebook.css
@@ -7,7 +7,7 @@ ul.token-input-list-facebook { @@ -7,7 +7,7 @@ ul.token-input-list-facebook {
7 border: 1px solid #8496ba; 7 border: 1px solid #8496ba;
8 cursor: text; 8 cursor: text;
9 font-size: 12px; 9 font-size: 12px;
10 - font-family: Verdana; 10 + font-family: Verdana, sans-serif;
11 min-height: 1px; 11 min-height: 1px;
12 z-index: 999; 12 z-index: 999;
13 margin: 0; 13 margin: 0;
@@ -80,7 +80,7 @@ div.token-input-dropdown-facebook { @@ -80,7 +80,7 @@ div.token-input-dropdown-facebook {
80 border-bottom: 1px solid #ccc; 80 border-bottom: 1px solid #ccc;
81 cursor: default; 81 cursor: default;
82 font-size: 11px; 82 font-size: 11px;
83 - font-family: Verdana; 83 + font-family: Verdana, sans-serif;
84 z-index: 1; 84 z-index: 1;
85 } 85 }
86 86
@@ -119,8 +119,4 @@ div.token-input-dropdown-facebook ul li em { @@ -119,8 +119,4 @@ div.token-input-dropdown-facebook ul li em {
119 div.token-input-dropdown-facebook ul li.token-input-selected-dropdown-item-facebook { 119 div.token-input-dropdown-facebook ul li.token-input-selected-dropdown-item-facebook {
120 background-color: #3b5998; 120 background-color: #3b5998;
121 color: #fff; 121 color: #fff;
122 -}  
123 -.token-input-blur-text-facebook {  
124 - font-style: italic;  
125 - color: #AAA;  
126 -} 122 +}
127 \ No newline at end of file 123 \ No newline at end of file
test/functional/profile_controller_test.rb
@@ -7,6 +7,7 @@ class ProfileControllerTest &lt; ActionController::TestCase @@ -7,6 +7,7 @@ class ProfileControllerTest &lt; ActionController::TestCase
7 7
8 self.default_params = {profile: 'testuser'} 8 self.default_params = {profile: 'testuser'}
9 def setup 9 def setup
  10 + @controller = ProfileController.new
10 @profile = create_user('testuser').person 11 @profile = create_user('testuser').person
11 end 12 end
12 attr_reader :profile 13 attr_reader :profile
@@ -759,7 +760,7 @@ class ProfileControllerTest &lt; ActionController::TestCase @@ -759,7 +760,7 @@ class ProfileControllerTest &lt; ActionController::TestCase
759 760
760 login_as(profile.identifier) 761 login_as(profile.identifier)
761 get :index, :profile => p1.identifier 762 get :index, :profile => p1.identifier
762 - assert_nil assigns(:activities) 763 + assert assigns(:activities).blank?
763 end 764 end
764 765
765 should 'see the activities_items paginated' do 766 should 'see the activities_items paginated' do
@@ -954,14 +955,14 @@ class ProfileControllerTest &lt; ActionController::TestCase @@ -954,14 +955,14 @@ class ProfileControllerTest &lt; ActionController::TestCase
954 should 'not have activities defined if not logged in' do 955 should 'not have activities defined if not logged in' do
955 p1= fast_create(Person) 956 p1= fast_create(Person)
956 get :index, :profile => p1.identifier 957 get :index, :profile => p1.identifier
957 - assert_nil assigns(:actvities) 958 + assert assigns(:actvities).blank?
958 end 959 end
959 960
960 should 'not have activities defined if logged in but is not following profile' do 961 should 'not have activities defined if logged in but is not following profile' do
961 login_as(profile.identifier) 962 login_as(profile.identifier)
962 p1= fast_create(Person) 963 p1= fast_create(Person)
963 get :index, :profile => p1.identifier 964 get :index, :profile => p1.identifier
964 - assert_nil assigns(:activities) 965 + assert assigns(:activities).blank?
965 end 966 end
966 967
967 should 'have activities defined if logged in and is following profile' do 968 should 'have activities defined if logged in and is following profile' do
@@ -2045,4 +2046,216 @@ class ProfileControllerTest &lt; ActionController::TestCase @@ -2045,4 +2046,216 @@ class ProfileControllerTest &lt; ActionController::TestCase
2045 assert_redirected_to "/some/url" 2046 assert_redirected_to "/some/url"
2046 end 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 + marked_people = @controller.send(:treat_followed_entries, entries)
  2093 +
  2094 + assert_equivalent [p1,p2,p3], marked_people
  2095 + end
  2096 +
  2097 + should 'return empty followed entries if the user is not on his wall' do
  2098 + login_as(@profile.identifier)
  2099 + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person)
  2100 + p1 = create_user('emily').person
  2101 + p2 = create_user('wollie').person
  2102 + p3 = create_user('mary').person
  2103 + ProfileFollower.create!(:profile => p1, :circle => c1)
  2104 + ProfileFollower.create!(:profile => p3, :circle => c1)
  2105 +
  2106 + entries = "Circle_#{c1.id},Person_#{p1.id},Person_#{p2.id}"
  2107 + @controller.stubs(:profile).returns(@profile)
  2108 + @controller.stubs(:user).returns(p1)
  2109 + marked_people = @controller.send(:treat_followed_entries, entries)
  2110 +
  2111 + assert_empty marked_people
  2112 + end
  2113 +
  2114 + should 'leave private scrap' do
  2115 + login_as(@profile.identifier)
  2116 + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person)
  2117 + p1 = create_user('emily').person
  2118 + p2 = create_user('wollie').person
  2119 + ProfileFollower.create!(:profile => p1, :circle => c1)
  2120 + ProfileFollower.create!(:profile => p2, :circle => c1)
  2121 +
  2122 + content = 'Remember my birthday!'
  2123 +
  2124 + post :leave_scrap, :profile => @profile.identifier, :scrap => {:content => content}, :filter_followed => "Person_#{p1.id},Person_#{p2.id}"
  2125 +
  2126 + scrap = Scrap.last
  2127 + assert_equal content, scrap.content
  2128 + assert_equivalent [p1,p2], scrap.marked_people
  2129 + end
  2130 +
  2131 + should 'list private scraps on wall for marked people' do
  2132 + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person)
  2133 + p1 = create_user('emily').person
  2134 + ProfileFollower.create!(:profile => p1, :circle => c1)
  2135 + p1.add_friend(@profile)
  2136 + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1])
  2137 + scrap_activity = ProfileActivity.where(:activity => scrap).first
  2138 + login_as(p1.identifier)
  2139 +
  2140 + get :index, :profile => @profile.identifier
  2141 +
  2142 + assert assigns(:activities).include?(scrap_activity)
  2143 + end
  2144 +
  2145 + should 'not list private scraps on wall for not marked people' do
  2146 + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person)
  2147 + p1 = create_user('emily').person
  2148 + p2 = create_user('wollie').person
  2149 + not_marked = create_user('jack').person
  2150 + not_marked.add_friend(@profile)
  2151 + ProfileFollower.create!(:profile => p1, :circle => c1)
  2152 + ProfileFollower.create!(:profile => p2, :circle => c1)
  2153 + ProfileFollower.create!(:profile => not_marked, :circle => c1)
  2154 + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1,p2])
  2155 + scrap_activity = ProfileActivity.where(:activity => scrap).first
  2156 + login_as(not_marked.identifier)
  2157 +
  2158 + get :index, :profile => @profile.identifier
  2159 +
  2160 + assert !assigns(:activities).include?(scrap_activity)
  2161 + end
  2162 +
  2163 + should 'list private scraps on wall for creator' do
  2164 + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person)
  2165 + p1 = create_user('emily').person
  2166 + ProfileFollower.create!(:profile => p1, :circle => c1)
  2167 + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1])
  2168 + scrap_activity = ProfileActivity.where(:activity => scrap).first
  2169 + login_as(@profile.identifier)
  2170 +
  2171 + get :index, :profile => @profile.identifier
  2172 +
  2173 + assert assigns(:activities).include?(scrap_activity)
  2174 + end
  2175 +
  2176 + should 'list private scraps on wall for environment administrator' do
  2177 + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person)
  2178 + p1 = create_user('emily').person
  2179 + admin = create_user('env-admin').person
  2180 + env = @profile.environment
  2181 + env.add_admin(admin)
  2182 + admin.add_friend(@profile)
  2183 + ProfileFollower.create!(:profile => p1, :circle => c1)
  2184 + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1])
  2185 + scrap_activity = ProfileActivity.where(:activity => scrap).first
  2186 + login_as(admin.identifier)
  2187 +
  2188 + get :index, :profile => @profile.identifier
  2189 +
  2190 + assert assigns(:activities).include?(scrap_activity)
  2191 + end
  2192 +
  2193 + should 'list private scraps on network for marked people' do
  2194 + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person)
  2195 + p1 = create_user('emily').person
  2196 + p2 = create_user('wollie').person
  2197 + p2.add_friend(p1)
  2198 + p1.add_friend(p2)
  2199 + ProfileFollower.create!(:profile => p1, :circle => c1)
  2200 + ProfileFollower.create!(:profile => p2, :circle => c1)
  2201 + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1,p2])
  2202 + process_delayed_job_queue
  2203 + scrap_activity = p1.tracked_notifications.where(:target => scrap).first
  2204 + login_as(p2.identifier)
  2205 +
  2206 + get :index, :profile => p1.identifier
  2207 +
  2208 + assert assigns(:network_activities).include?(scrap_activity)
  2209 + end
  2210 +
  2211 + should 'not list private scraps on network for not marked people' do
  2212 + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person)
  2213 + p1 = create_user('emily').person
  2214 + not_marked = create_user('jack').person
  2215 + not_marked.add_friend(p1)
  2216 + ProfileFollower.create!(:profile => p1, :circle => c1)
  2217 + ProfileFollower.create!(:profile => not_marked, :circle => c1)
  2218 + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1])
  2219 + process_delayed_job_queue
  2220 + scrap_activity = p1.tracked_notifications.where(:target => scrap).first
  2221 + login_as(not_marked.identifier)
  2222 +
  2223 + get :index, :profile => p1.identifier
  2224 +
  2225 + assert !assigns(:network_activities).include?(scrap_activity)
  2226 + end
  2227 +
  2228 + should 'list private scraps on network for creator' do
  2229 + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person)
  2230 + p1 = create_user('emily').person
  2231 + p1.add_friend(@profile)
  2232 + ProfileFollower.create!(:profile => p1, :circle => c1)
  2233 + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1])
  2234 + process_delayed_job_queue
  2235 + scrap_activity = p1.tracked_notifications.where(:target => scrap).first
  2236 + login_as(@profile.identifier)
  2237 +
  2238 + get :index, :profile => p1.identifier
  2239 +
  2240 + assert assigns(:network_activities).include?(scrap_activity)
  2241 + end
  2242 +
  2243 + should 'list private scraps on network for environment admin' do
  2244 + c1 = Circle.create!(:name => 'Family', :person => @profile, :profile_type => Person)
  2245 + p1 = create_user('emily').person
  2246 + admin = create_user('env-admin').person
  2247 + env = @profile.environment
  2248 + env.add_admin(admin)
  2249 + admin.add_friend(p1)
  2250 + ProfileFollower.create!(:profile => p1, :circle => c1)
  2251 + scrap = Scrap.create!(:content => 'Secret message.', :sender_id => @profile.id, :receiver_id => @profile.id, :marked_people => [p1])
  2252 + process_delayed_job_queue
  2253 + scrap_activity = p1.tracked_notifications.where(:target => scrap).first
  2254 + login_as(admin.identifier)
  2255 +
  2256 + get :index, :profile => p1.identifier
  2257 +
  2258 + assert assigns(:network_activities).include?(scrap_activity)
  2259 + end
  2260 +
2048 end 2261 end
test/test_helper.rb
@@ -199,5 +199,9 @@ class ActiveSupport::TestCase @@ -199,5 +199,9 @@ class ActiveSupport::TestCase
199 ret 199 ret
200 end 200 end
201 201
  202 + def json_response
  203 + ActiveSupport::JSON.decode(@response.body)
  204 + end
  205 +
202 end 206 end
203 207
test/unit/notify_activity_to_profiles_job_test.rb
@@ -52,6 +52,27 @@ class NotifyActivityToProfilesJobTest &lt; ActiveSupport::TestCase @@ -52,6 +52,27 @@ class NotifyActivityToProfilesJobTest &lt; ActiveSupport::TestCase
52 end 52 end
53 end 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 should 'not notify the communities members' do 76 should 'not notify the communities members' do
56 person = fast_create(Person) 77 person = fast_create(Person)
57 community = fast_create(Community) 78 community = fast_create(Community)