has_mailbox 1.5.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. data/Gemfile +4 -0
  2. data/README.rdoc +95 -0
  3. data/Rakefile +2 -0
  4. data/app/controllers/mailboxes_controller.rb +3 -0
  5. data/has_mailbox.gemspec +24 -0
  6. data/lib/generators/has_mailbox/install/install_generator.rb +33 -0
  7. data/lib/generators/has_mailbox/install/templates/README +26 -0
  8. data/lib/generators/has_mailbox/install/templates/jquery.tokeninput.js +718 -0
  9. data/lib/generators/has_mailbox/install/templates/mailboxes.css +147 -0
  10. data/lib/generators/has_mailbox/install/templates/mailboxes.js +28 -0
  11. data/lib/generators/has_mailbox/install/templates/token-input-facebook.css +122 -0
  12. data/lib/generators/has_mailbox/install/templates/views/_head.html.erb +19 -0
  13. data/lib/generators/has_mailbox/install/templates/views/_messages.html.erb +39 -0
  14. data/lib/generators/has_mailbox/install/templates/views/_tabs_panel.html.erb +11 -0
  15. data/lib/generators/has_mailbox/install/templates/views/index.html.erb +10 -0
  16. data/lib/generators/has_mailbox/install/templates/views/index.js.erb +1 -0
  17. data/lib/generators/has_mailbox/install/templates/views/new.html.erb +28 -0
  18. data/lib/generators/has_mailbox/install/templates/views/show.html.erb +35 -0
  19. data/lib/generators/has_mailbox/migration/migration_generator.rb +22 -0
  20. data/lib/generators/has_mailbox/migration/templates/create_message_copies_table.rb +17 -0
  21. data/lib/generators/has_mailbox/migration/templates/create_messages_table.rb +19 -0
  22. data/lib/has_mailbox.rb +14 -0
  23. data/lib/has_mailbox/controllers/method_helpers.rb +112 -0
  24. data/lib/has_mailbox/has_mailbox.rb +95 -0
  25. data/lib/has_mailbox/mailboxes/engine.rb +6 -0
  26. data/lib/has_mailbox/mailboxes/routing.rb +22 -0
  27. data/lib/has_mailbox/models/message.rb +52 -0
  28. data/lib/has_mailbox/models/message_copies.rb +30 -0
  29. data/lib/has_mailbox/version.rb +3 -0
  30. metadata +96 -0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in has_mailbox.gemspec
4
+ gemspec
data/README.rdoc ADDED
@@ -0,0 +1,95 @@
1
+ = has_mailbox
2
+
3
+ If you like to add messaging functionality between the users in your rails app, use this gem!
4
+ this gem also provide standard mailbox/messagebox interface allowing each users have their own inbox, outbox and trash.
5
+
6
+ This rails engine compatible with Rails 3.x only.
7
+
8
+ == Setup
9
+
10
+ add the gem to your Gemfile.
11
+
12
+ gem "has_mailbox"
13
+
14
+ run bundle command and generate the migration files, this will generate messages table and messagecopies table,
15
+ after that don't forget to run the migration.
16
+
17
+ rails g has_mailbox:migration
18
+ rake db:migrate
19
+
20
+ Then add has_mailbox method in your user model, for example :
21
+
22
+ class User < ActiveRecord::Base
23
+ has_mailbox
24
+ ...
25
+ end
26
+
27
+ Until this step you are able to send messages between the users across your application.
28
+ Try to create two user object and send messages to one user with another,
29
+ and list all messages from the entire mailbox.
30
+
31
+ @user1 = User.first # => create first user
32
+ @user2 = User.last # => return the second user
33
+
34
+ @user1.send_message("Hi Subject","Hi Body !!!",@user2) # => send message to @user2
35
+ @user1.send_message?("Hi Subject","Hi Body !!!",@user2) # => send message with true/false return
36
+ @user1.send_message("Hi Subject","Hi Body !!!",@user2,@user3) # => send message with with multiple recipients
37
+
38
+ @user1.inbox # => return all incoming messages for @user1
39
+ @user1.outbox # => return outgoing messages for @user1
40
+ @user1.trash # => return all messages that has been deleted
41
+
42
+ If you like to delete the message from user object, and empty the box try use this method.
43
+
44
+ @message = @user1.inbox.find(1) # => get message from inbox with id 1
45
+ @message.delete # => delete message from inbox will be moved to trash box.
46
+
47
+ @user1.trash # => return deleted messages
48
+
49
+ @message = @user.trash.find(1) # => get the current deleted message
50
+
51
+ @message.undelete # => will return the message to inbox
52
+ or
53
+ @message.delete # => will delete message permanently
54
+
55
+ @user.empty_mailbox(:inbox => true) # => will delete all messages from inbox to trash.
56
+ @user.empty_mailbox(:outbox => true) # => will delete all outgoing messages permanently.
57
+ @user.empty_mailbox(:inbox => true) # => will delete all messages from trash permanently.
58
+
59
+ Each user are able to mark their message as read/unread. Example :
60
+
61
+ @user1.inbox.find(1).mark_as_read # update attribute message opened to true
62
+ @user1.inbox.find(1).mark_as_unread # to false
63
+
64
+ == Install Mailbox Views
65
+ This gem has been build using Devise authentication, but if you like using another authentication plugin,
66
+ such as authlogic / restful-authentication or any other authentication plugins, it still suits to your app.
67
+
68
+ rails g has_mailbox:install
69
+
70
+ this generator will install to your application the views and stylesheet file in your public directory,
71
+ notice the required argument "user_attribute_name", you have to fill this with desired attribute name in your user model
72
+ you wish to display in your generated views. e.g. : email, username, first_name, etc.
73
+ the default will be set to "email", cause Devise default user login using "email" attribute.
74
+
75
+ here's the tricky part. you have to define the route for mailboxes in order to get controller will work with your views.
76
+ Also you have to specifies the current user object from your authentication plugin and also the attribute name you wish to display.
77
+
78
+ with Devise authentication to get the default user object that is currently sign in is "current_user"
79
+ and the display attribute using "email".
80
+ So in your config/routes.rb, please add mailboxes_for method. Example :
81
+
82
+ mailboxes_for :users # => with this argument user_object_name
83
+ # => will set to "current_user" and
84
+ # => user_attribute_name will set to "email"
85
+
86
+ or if you using different plugins/gems, you can set with your own user_object_name and user_attribute_name
87
+
88
+ mailboxes_for :users, :user_object_name => "current_user_sign_in", :user_attribute_name => "username"
89
+
90
+
91
+ == Your issues are needed
92
+
93
+ This is gem are still under development mode, so if you are having some problems with has_mailbox, please submit an issues here.
94
+
95
+ http://github.com/fajrif/has_mailbox/issues
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,3 @@
1
+ class MailboxesController < ApplicationController
2
+ include HasMailbox::Controllers::MethodHelpers
3
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "has_mailbox/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "has_mailbox"
7
+ s.version = HasMailbox::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Fajri Fachriansyah"]
10
+ s.email = ["fajrif@hotmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Add ability for each user to have a Mailbox}
13
+ s.description = %q{This gem allowing each user to send and receive private messages}
14
+
15
+ s.rubyforge_project = "has_mailbox"
16
+
17
+ s.add_runtime_dependency "jquery-rails"
18
+ s.add_runtime_dependency "will_paginate"
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+ end
@@ -0,0 +1,33 @@
1
+ module HasMailbox
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.join(File.dirname(__FILE__), 'templates')
5
+
6
+ desc "Create views, javascripts and stylesheets files for mailboxes controller."
7
+
8
+ def copy_stylesheet
9
+ copy_file 'mailboxes.css', 'public/stylesheets/mailboxes.css'
10
+ copy_file 'token-input-facebook.css', 'public/stylesheets/token-input-facebook.css'
11
+ end
12
+
13
+ def copy_javascript
14
+ copy_file 'mailboxes.js', 'public/javascripts/mailboxes.js'
15
+ copy_file 'jquery.tokeninput.js', 'public/javascripts/jquery.tokeninput.js'
16
+ end
17
+
18
+ def copy_views
19
+ copy_file "views/_head.html.erb", "app/views/mailboxes/_head.html.erb"
20
+ copy_file "views/_messages.html.erb", "app/views/mailboxes/_messages.html.erb"
21
+ copy_file "views/_tabs_panel.html.erb", "app/views/mailboxes/_tabs_panel.html.erb"
22
+ copy_file "views/index.html.erb", "app/views/mailboxes/index.html.erb"
23
+ copy_file "views/index.js.erb", "app/views/mailboxes/index.js.erb"
24
+ copy_file "views/new.html.erb", "app/views/mailboxes/new.html.erb"
25
+ copy_file "views/show.html.erb", "app/views/mailboxes/show.html.erb"
26
+ end
27
+
28
+ def show_readme
29
+ readme "README"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+
2
+ *****************************************************************************************
3
+
4
+ Some setup you must do manually if you haven't yet:
5
+
6
+ 1. Ensure the has_mailbox method has been defined your user model.
7
+
8
+ class User < ActiveRecord::Base
9
+ has_mailbox
10
+ ...
11
+ end
12
+
13
+ 2. Ensure you have defined mailboxes route to your config/routes.rb.
14
+ For example:
15
+
16
+ mailboxes_for :users
17
+ or
18
+ mailboxes_for :users, :user_object_name => "current_user", :user_display_attribute => "email"
19
+
20
+ 3. Ensure you have flash messages in app/views/layouts/application.html.erb.
21
+ For example:
22
+
23
+ <p class="notice"><%= notice %></p>
24
+ <p class="alert"><%= alert %></p>
25
+
26
+ *****************************************************************************************
@@ -0,0 +1,718 @@
1
+ /*
2
+ * jQuery Plugin: Tokenizing Autocomplete Text Entry
3
+ * Version 1.4.2
4
+ *
5
+ * Copyright (c) 2009 James Smith (http://loopj.com)
6
+ * Licensed jointly under the GPL and MIT licenses,
7
+ * choose which one suits your project best!
8
+ *
9
+ */
10
+
11
+ (function ($) {
12
+ // Default settings
13
+ var DEFAULT_SETTINGS = {
14
+ hintText: "Type in a search term",
15
+ noResultsText: "No results",
16
+ searchingText: "Searching...",
17
+ deleteText: "&times;",
18
+ searchDelay: 300,
19
+ minChars: 1,
20
+ tokenLimit: null,
21
+ jsonContainer: null,
22
+ method: "GET",
23
+ contentType: "json",
24
+ queryParam: "q",
25
+ tokenDelimiter: ",",
26
+ preventDuplicates: false,
27
+ prePopulate: null,
28
+ animateDropdown: true,
29
+ onResult: null,
30
+ onAdd: null,
31
+ onDelete: null
32
+ };
33
+
34
+ // Default classes to use when theming
35
+ var DEFAULT_CLASSES = {
36
+ tokenList: "token-input-list",
37
+ token: "token-input-token",
38
+ tokenDelete: "token-input-delete-token",
39
+ selectedToken: "token-input-selected-token",
40
+ highlightedToken: "token-input-highlighted-token",
41
+ dropdown: "token-input-dropdown",
42
+ dropdownItem: "token-input-dropdown-item",
43
+ dropdownItem2: "token-input-dropdown-item2",
44
+ selectedDropdownItem: "token-input-selected-dropdown-item",
45
+ inputToken: "token-input-input-token"
46
+ };
47
+
48
+ // Input box position "enum"
49
+ var POSITION = {
50
+ BEFORE: 0,
51
+ AFTER: 1,
52
+ END: 2
53
+ };
54
+
55
+ // Keys "enum"
56
+ var KEY = {
57
+ BACKSPACE: 8,
58
+ TAB: 9,
59
+ ENTER: 13,
60
+ ESCAPE: 27,
61
+ SPACE: 32,
62
+ PAGE_UP: 33,
63
+ PAGE_DOWN: 34,
64
+ END: 35,
65
+ HOME: 36,
66
+ LEFT: 37,
67
+ UP: 38,
68
+ RIGHT: 39,
69
+ DOWN: 40,
70
+ NUMPAD_ENTER: 108,
71
+ COMMA: 188
72
+ };
73
+
74
+
75
+ // Expose the .tokenInput function to jQuery as a plugin
76
+ $.fn.tokenInput = function (url_or_data, options) {
77
+ var settings = $.extend({}, DEFAULT_SETTINGS, options || {});
78
+
79
+ return this.each(function () {
80
+ new $.TokenList(this, url_or_data, settings);
81
+ });
82
+ };
83
+
84
+
85
+ // TokenList class for each input
86
+ $.TokenList = function (input, url_or_data, settings) {
87
+ //
88
+ // Initialization
89
+ //
90
+
91
+ // Configure the data source
92
+ if($.type(url_or_data) === "string") {
93
+ // Set the url to query against
94
+ settings.url = url_or_data;
95
+
96
+ // Make a smart guess about cross-domain if it wasn't explicitly specified
97
+ if(settings.crossDomain === undefined) {
98
+ if(settings.url.indexOf("://") === -1) {
99
+ settings.crossDomain = false;
100
+ } else {
101
+ settings.crossDomain = (location.href.split(/\/+/g)[1] !== settings.url.split(/\/+/g)[1]);
102
+ }
103
+ }
104
+ } else if($.type(url_or_data) === "array") {
105
+ // Set the local data to search through
106
+ settings.local_data = url_or_data;
107
+ }
108
+
109
+ // Build class names
110
+ if(settings.classes) {
111
+ // Use custom class names
112
+ settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes);
113
+ } else if(settings.theme) {
114
+ // Use theme-suffixed default class names
115
+ settings.classes = {};
116
+ $.each(DEFAULT_CLASSES, function(key, value) {
117
+ settings.classes[key] = value + "-" + settings.theme;
118
+ });
119
+ } else {
120
+ settings.classes = DEFAULT_CLASSES;
121
+ }
122
+
123
+
124
+ // Save the tokens
125
+ var saved_tokens = [];
126
+
127
+ // Keep track of the number of tokens in the list
128
+ var token_count = 0;
129
+
130
+ // Basic cache to save on db hits
131
+ var cache = new $.TokenList.Cache();
132
+
133
+ // Keep track of the timeout, old vals
134
+ var timeout;
135
+ var input_val;
136
+
137
+ // Create a new text input an attach keyup events
138
+ var input_box = $("<input type=\"text\" autocomplete=\"off\">")
139
+ .css({
140
+ outline: "none"
141
+ })
142
+ .focus(function () {
143
+ if (settings.tokenLimit === null || settings.tokenLimit !== token_count) {
144
+ show_dropdown_hint();
145
+ }
146
+ })
147
+ .blur(function () {
148
+ hide_dropdown();
149
+ })
150
+ .bind("keyup keydown blur update", resize_input)
151
+ .keydown(function (event) {
152
+ var previous_token;
153
+ var next_token;
154
+
155
+ switch(event.keyCode) {
156
+ case KEY.LEFT:
157
+ case KEY.RIGHT:
158
+ case KEY.UP:
159
+ case KEY.DOWN:
160
+ if(!$(this).val()) {
161
+ previous_token = input_token.prev();
162
+ next_token = input_token.next();
163
+
164
+ if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
165
+ // Check if there is a previous/next token and it is selected
166
+ if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) {
167
+ deselect_token($(selected_token), POSITION.BEFORE);
168
+ } else {
169
+ deselect_token($(selected_token), POSITION.AFTER);
170
+ }
171
+ } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) {
172
+ // We are moving left, select the previous token if it exists
173
+ select_token($(previous_token.get(0)));
174
+ } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) {
175
+ // We are moving right, select the next token if it exists
176
+ select_token($(next_token.get(0)));
177
+ }
178
+ } else {
179
+ var dropdown_item = null;
180
+
181
+ if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) {
182
+ dropdown_item = $(selected_dropdown_item).next();
183
+ } else {
184
+ dropdown_item = $(selected_dropdown_item).prev();
185
+ }
186
+
187
+ if(dropdown_item.length) {
188
+ select_dropdown_item(dropdown_item);
189
+ }
190
+ return false;
191
+ }
192
+ break;
193
+
194
+ case KEY.BACKSPACE:
195
+ previous_token = input_token.prev();
196
+
197
+ if(!$(this).val().length) {
198
+ if(selected_token) {
199
+ delete_token($(selected_token));
200
+ } else if(previous_token.length) {
201
+ select_token($(previous_token.get(0)));
202
+ }
203
+
204
+ return false;
205
+ } else if($(this).val().length === 1) {
206
+ hide_dropdown();
207
+ } else {
208
+ // set a timeout just long enough to let this function finish.
209
+ setTimeout(function(){do_search();}, 5);
210
+ }
211
+ break;
212
+
213
+ case KEY.TAB:
214
+ case KEY.ENTER:
215
+ case KEY.NUMPAD_ENTER:
216
+ case KEY.COMMA:
217
+ if(selected_dropdown_item) {
218
+ add_token($(selected_dropdown_item));
219
+ return false;
220
+ }
221
+ break;
222
+
223
+ case KEY.ESCAPE:
224
+ hide_dropdown();
225
+ return true;
226
+
227
+ default:
228
+ if(String.fromCharCode(event.which)) {
229
+ // set a timeout just long enough to let this function finish.
230
+ setTimeout(function(){do_search();}, 5);
231
+ }
232
+ break;
233
+ }
234
+ });
235
+
236
+ // Keep a reference to the original input box
237
+ var hidden_input = $(input)
238
+ .hide()
239
+ .val("")
240
+ .focus(function () {
241
+ input_box.focus();
242
+ })
243
+ .blur(function () {
244
+ input_box.blur();
245
+ });
246
+
247
+ // Keep a reference to the selected token and dropdown item
248
+ var selected_token = null;
249
+ var selected_dropdown_item = null;
250
+
251
+ // The list to store the token items in
252
+ var token_list = $("<ul />")
253
+ .addClass(settings.classes.tokenList)
254
+ .click(function (event) {
255
+ var li = $(event.target).closest("li");
256
+ if(li && li.get(0) && $.data(li.get(0), "tokeninput")) {
257
+ toggle_select_token(li);
258
+ } else {
259
+ // Deselect selected token
260
+ if(selected_token) {
261
+ deselect_token($(selected_token), POSITION.END);
262
+ }
263
+
264
+ // Focus input box
265
+ input_box.focus();
266
+ }
267
+ })
268
+ .mouseover(function (event) {
269
+ var li = $(event.target).closest("li");
270
+ if(li && selected_token !== this) {
271
+ li.addClass(settings.classes.highlightedToken);
272
+ }
273
+ })
274
+ .mouseout(function (event) {
275
+ var li = $(event.target).closest("li");
276
+ if(li && selected_token !== this) {
277
+ li.removeClass(settings.classes.highlightedToken);
278
+ }
279
+ })
280
+ .insertBefore(hidden_input);
281
+
282
+ // The token holding the input box
283
+ var input_token = $("<li />")
284
+ .addClass(settings.classes.inputToken)
285
+ .appendTo(token_list)
286
+ .append(input_box);
287
+
288
+ // The list to store the dropdown items in
289
+ var dropdown = $("<div>")
290
+ .addClass(settings.classes.dropdown)
291
+ .appendTo("body")
292
+ .hide();
293
+
294
+ // Magic element to help us resize the text input
295
+ var input_resizer = $("<tester/>")
296
+ .insertAfter(input_box)
297
+ .css({
298
+ position: "absolute",
299
+ top: -9999,
300
+ left: -9999,
301
+ width: "auto",
302
+ fontSize: input_box.css("fontSize"),
303
+ fontFamily: input_box.css("fontFamily"),
304
+ fontWeight: input_box.css("fontWeight"),
305
+ letterSpacing: input_box.css("letterSpacing"),
306
+ whiteSpace: "nowrap"
307
+ });
308
+
309
+ // Pre-populate list if items exist
310
+ hidden_input.val("");
311
+ li_data = settings.prePopulate || hidden_input.data("pre");
312
+ if(li_data && li_data.length) {
313
+ $.each(li_data, function (index, value) {
314
+ insert_token(value.id, value.name);
315
+ });
316
+ }
317
+
318
+
319
+
320
+ //
321
+ // Private functions
322
+ //
323
+
324
+ function resize_input() {
325
+ if(input_val === (input_val = input_box.val())) {return;}
326
+
327
+ // Enter new content into resizer and resize input accordingly
328
+ var escaped = input_val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
329
+ input_resizer.html(escaped);
330
+ input_box.width(input_resizer.width() + 30);
331
+ }
332
+
333
+ function is_printable_character(keycode) {
334
+ return ((keycode >= 48 && keycode <= 90) || // 0-1a-z
335
+ (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * .
336
+ (keycode >= 186 && keycode <= 192) || // ; = , - . / ^
337
+ (keycode >= 219 && keycode <= 222)); // ( \ ) '
338
+ }
339
+
340
+ // Inner function to a token to the list
341
+ function insert_token(id, value) {
342
+ var this_token = $("<li><p>"+ value +"</p> </li>")
343
+ .addClass(settings.classes.token)
344
+ .insertBefore(input_token);
345
+
346
+ // The 'delete token' button
347
+ $("<span>" + settings.deleteText + "</span>")
348
+ .addClass(settings.classes.tokenDelete)
349
+ .appendTo(this_token)
350
+ .click(function () {
351
+ delete_token($(this).parent());
352
+ return false;
353
+ });
354
+
355
+ // Store data on the token
356
+ var token_data = {"id": id, "name": value};
357
+ $.data(this_token.get(0), "tokeninput", token_data);
358
+
359
+ // Save this token for duplicate checking
360
+ saved_tokens.push(token_data);
361
+
362
+ // Update the hidden input
363
+ var token_ids = $.map(saved_tokens, function (el) {
364
+ return el.id;
365
+ });
366
+ hidden_input.val(token_ids.join(settings.tokenDelimiter));
367
+
368
+ token_count += 1;
369
+
370
+ return this_token;
371
+ }
372
+
373
+ // Add a token to the token list based on user input
374
+ function add_token (item) {
375
+ var li_data = $.data(item.get(0), "tokeninput");
376
+ var callback = settings.onAdd;
377
+
378
+ // See if the token already exists and select it if we don't want duplicates
379
+ if(token_count > 0 && settings.preventDuplicates) {
380
+ var found_existing_token = null;
381
+ token_list.children().each(function () {
382
+ var existing_token = $(this);
383
+ var existing_data = $.data(existing_token.get(0), "tokeninput");
384
+ if(existing_data && existing_data.id === li_data.id) {
385
+ found_existing_token = existing_token;
386
+ return false;
387
+ }
388
+ });
389
+
390
+ if(found_existing_token) {
391
+ select_token(found_existing_token);
392
+ input_token.insertAfter(found_existing_token);
393
+ input_box.focus();
394
+ return;
395
+ }
396
+ }
397
+
398
+ // Insert the new tokens
399
+ insert_token(li_data.id, li_data.name);
400
+
401
+ // Check the token limit
402
+ if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
403
+ input_box.hide();
404
+ hide_dropdown();
405
+ return;
406
+ } else {
407
+ input_box.focus();
408
+ }
409
+
410
+ // Clear input box
411
+ input_box.val("");
412
+
413
+ // Don't show the help dropdown, they've got the idea
414
+ hide_dropdown();
415
+
416
+ // Execute the onAdd callback if defined
417
+ if($.isFunction(callback)) {
418
+ callback(li_data);
419
+ }
420
+ }
421
+
422
+ // Select a token in the token list
423
+ function select_token (token) {
424
+ token.addClass(settings.classes.selectedToken);
425
+ selected_token = token.get(0);
426
+
427
+ // Hide input box
428
+ input_box.val("");
429
+
430
+ // Hide dropdown if it is visible (eg if we clicked to select token)
431
+ hide_dropdown();
432
+ }
433
+
434
+ // Deselect a token in the token list
435
+ function deselect_token (token, position) {
436
+ token.removeClass(settings.classes.selectedToken);
437
+ selected_token = null;
438
+
439
+ if(position === POSITION.BEFORE) {
440
+ input_token.insertBefore(token);
441
+ } else if(position === POSITION.AFTER) {
442
+ input_token.insertAfter(token);
443
+ } else {
444
+ input_token.appendTo(token_list);
445
+ }
446
+
447
+ // Show the input box and give it focus again
448
+ input_box.focus();
449
+ }
450
+
451
+ // Toggle selection of a token in the token list
452
+ function toggle_select_token(token) {
453
+ var previous_selected_token = selected_token;
454
+
455
+ if(selected_token) {
456
+ deselect_token($(selected_token), POSITION.END);
457
+ }
458
+
459
+ if(previous_selected_token === token.get(0)) {
460
+ deselect_token(token, POSITION.END);
461
+ } else {
462
+ select_token(token);
463
+ }
464
+ }
465
+
466
+ // Delete a token from the token list
467
+ function delete_token (token) {
468
+ // Remove the id from the saved list
469
+ var token_data = $.data(token.get(0), "tokeninput");
470
+ var callback = settings.onDelete;
471
+
472
+ // Delete the token
473
+ token.remove();
474
+ selected_token = null;
475
+
476
+ // Show the input box and give it focus again
477
+ input_box.focus();
478
+
479
+ // Remove this token from the saved list
480
+ saved_tokens = $.grep(saved_tokens, function (val) {
481
+ return (val.id !== token_data.id);
482
+ });
483
+
484
+ // Update the hidden input
485
+ var token_ids = $.map(saved_tokens, function (el) {
486
+ return el.id;
487
+ });
488
+ hidden_input.val(token_ids.join(settings.tokenDelimiter));
489
+
490
+ token_count -= 1;
491
+
492
+ if(settings.tokenLimit !== null) {
493
+ input_box
494
+ .show()
495
+ .val("")
496
+ .focus();
497
+ }
498
+
499
+ // Execute the onDelete callback if defined
500
+ if($.isFunction(callback)) {
501
+ callback(token_data);
502
+ }
503
+ }
504
+
505
+ // Hide and clear the results dropdown
506
+ function hide_dropdown () {
507
+ dropdown.hide().empty();
508
+ selected_dropdown_item = null;
509
+ }
510
+
511
+ function show_dropdown() {
512
+ dropdown
513
+ .css({
514
+ position: "absolute",
515
+ top: $(token_list).offset().top + $(token_list).outerHeight(),
516
+ left: $(token_list).offset().left,
517
+ zindex: 999
518
+ })
519
+ .show();
520
+ }
521
+
522
+ function show_dropdown_searching () {
523
+ if(settings.searchingText) {
524
+ dropdown.html("<p>"+settings.searchingText+"</p>");
525
+ show_dropdown();
526
+ }
527
+ }
528
+
529
+ function show_dropdown_hint () {
530
+ if(settings.hintText) {
531
+ dropdown.html("<p>"+settings.hintText+"</p>");
532
+ show_dropdown();
533
+ }
534
+ }
535
+
536
+ // Highlight the query part of the search term
537
+ function highlight_term(value, term) {
538
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
539
+ }
540
+
541
+ // Populate the results dropdown with some results
542
+ function populate_dropdown (query, results) {
543
+ if(results && results.length) {
544
+ dropdown.empty();
545
+ var dropdown_ul = $("<ul>")
546
+ .appendTo(dropdown)
547
+ .mouseover(function (event) {
548
+ select_dropdown_item($(event.target).closest("li"));
549
+ })
550
+ .mousedown(function (event) {
551
+ add_token($(event.target).closest("li"));
552
+ return false;
553
+ })
554
+ .hide();
555
+
556
+ $.each(results, function(index, value) {
557
+ var this_li = $("<li>" + highlight_term(value.name, query) + "</li>")
558
+ .appendTo(dropdown_ul);
559
+
560
+ if(index % 2) {
561
+ this_li.addClass(settings.classes.dropdownItem);
562
+ } else {
563
+ this_li.addClass(settings.classes.dropdownItem2);
564
+ }
565
+
566
+ if(index === 0) {
567
+ select_dropdown_item(this_li);
568
+ }
569
+
570
+ $.data(this_li.get(0), "tokeninput", {"id": value.id, "name": value.name});
571
+ });
572
+
573
+ show_dropdown();
574
+
575
+ if(settings.animateDropdown) {
576
+ dropdown_ul.slideDown("fast");
577
+ } else {
578
+ dropdown_ul.show();
579
+ }
580
+ } else {
581
+ if(settings.noResultsText) {
582
+ dropdown.html("<p>"+settings.noResultsText+"</p>");
583
+ show_dropdown();
584
+ }
585
+ }
586
+ }
587
+
588
+ // Highlight an item in the results dropdown
589
+ function select_dropdown_item (item) {
590
+ if(item) {
591
+ if(selected_dropdown_item) {
592
+ deselect_dropdown_item($(selected_dropdown_item));
593
+ }
594
+
595
+ item.addClass(settings.classes.selectedDropdownItem);
596
+ selected_dropdown_item = item.get(0);
597
+ }
598
+ }
599
+
600
+ // Remove highlighting from an item in the results dropdown
601
+ function deselect_dropdown_item (item) {
602
+ item.removeClass(settings.classes.selectedDropdownItem);
603
+ selected_dropdown_item = null;
604
+ }
605
+
606
+ // Do a search and show the "searching" dropdown if the input is longer
607
+ // than settings.minChars
608
+ function do_search() {
609
+ var query = input_box.val().toLowerCase();
610
+
611
+ if(query && query.length) {
612
+ if(selected_token) {
613
+ deselect_token($(selected_token), POSITION.AFTER);
614
+ }
615
+
616
+ if(query.length >= settings.minChars) {
617
+ show_dropdown_searching();
618
+ clearTimeout(timeout);
619
+
620
+ timeout = setTimeout(function(){
621
+ run_search(query);
622
+ }, settings.searchDelay);
623
+ } else {
624
+ hide_dropdown();
625
+ }
626
+ }
627
+ }
628
+
629
+ // Do the actual search
630
+ function run_search(query) {
631
+ var cached_results = cache.get(query);
632
+ if(cached_results) {
633
+ populate_dropdown(query, cached_results);
634
+ } else {
635
+ // Are we doing an ajax search or local data search?
636
+ if(settings.url) {
637
+ // Extract exisiting get params
638
+ var ajax_params = {};
639
+ ajax_params.data = {};
640
+ if(settings.url.indexOf("?") > -1) {
641
+ var parts = settings.url.split("?");
642
+ ajax_params.url = parts[0];
643
+
644
+ var param_array = parts[1].split("&");
645
+ $.each(param_array, function (index, value) {
646
+ var kv = value.split("=");
647
+ ajax_params.data[kv[0]] = kv[1];
648
+ });
649
+ } else {
650
+ ajax_params.url = settings.url;
651
+ }
652
+
653
+ // Prepare the request
654
+ ajax_params.data[settings.queryParam] = query;
655
+ ajax_params.type = settings.method;
656
+ ajax_params.dataType = settings.contentType;
657
+ if(settings.crossDomain) {
658
+ ajax_params.dataType = "jsonp";
659
+ }
660
+
661
+ // Attach the success callback
662
+ ajax_params.success = function(results) {
663
+ if($.isFunction(settings.onResult)) {
664
+ results = settings.onResult.call(this, results);
665
+ }
666
+ cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
667
+
668
+ // only populate the dropdown if the results are associated with the active search query
669
+ if(input_box.val().toLowerCase() === query) {
670
+ populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
671
+ }
672
+ };
673
+
674
+ // Make the request
675
+ $.ajax(ajax_params);
676
+ } else if(settings.local_data) {
677
+ // Do the search through local data
678
+ var results = $.grep(settings.local_data, function (row) {
679
+ return row.name.toLowerCase().indexOf(query.toLowerCase()) > -1;
680
+ });
681
+
682
+ populate_dropdown(query, results);
683
+ }
684
+ }
685
+ }
686
+ };
687
+
688
+ // Really basic cache for the results
689
+ $.TokenList.Cache = function (options) {
690
+ var settings = $.extend({
691
+ max_size: 500
692
+ }, options);
693
+
694
+ var data = {};
695
+ var size = 0;
696
+
697
+ var flush = function () {
698
+ data = {};
699
+ size = 0;
700
+ };
701
+
702
+ this.add = function (query, results) {
703
+ if(size > settings.max_size) {
704
+ flush();
705
+ }
706
+
707
+ if(!data[query]) {
708
+ size += 1;
709
+ }
710
+
711
+ data[query] = results;
712
+ };
713
+
714
+ this.get = function (query) {
715
+ return data[query];
716
+ };
717
+ };
718
+ }(jQuery));