activeadmin_associations 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. data/.coveralls.yml +1 -0
  2. data/.gitignore +57 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +9 -0
  6. data/MIT_LICENSE.txt +20 -0
  7. data/README.md +220 -0
  8. data/Rakefile +20 -0
  9. data/activeadmin_associations.gemspec +32 -0
  10. data/app/controllers/autocomplete_controller.rb +37 -0
  11. data/app/helpers/active_admin_associations_helper.rb +77 -0
  12. data/app/views/admin/shared/_add_to_association.html.erb +21 -0
  13. data/app/views/admin/shared/_association_collection_table_actions.html.erb +4 -0
  14. data/app/views/admin/shared/_blank_slate.html.erb +3 -0
  15. data/app/views/admin/shared/_collection_table.html.erb +58 -0
  16. data/app/views/admin/shared/_form.html.erb +7 -0
  17. data/config/routes.rb +7 -0
  18. data/lib/active_admin_associations/active_admin_extensions.rb +17 -0
  19. data/lib/active_admin_associations/association_actions.rb +77 -0
  20. data/lib/active_admin_associations/association_config.rb +50 -0
  21. data/lib/active_admin_associations/autocompleter.rb +61 -0
  22. data/lib/active_admin_associations/engine.rb +23 -0
  23. data/lib/active_admin_associations/form_config_dsl.rb +15 -0
  24. data/lib/active_admin_associations/redirect_destroy_actions.rb +7 -0
  25. data/lib/active_admin_associations/version.rb +3 -0
  26. data/lib/activeadmin_associations.rb +20 -0
  27. data/lib/formtastic/inputs/token_input.rb +43 -0
  28. data/lib/formtastic/token_input_default_for_association.rb +19 -0
  29. data/spec/association_config_spec.rb +41 -0
  30. data/spec/autocompleter_spec.rb +58 -0
  31. data/spec/controllers/admin_posts_controller_spec.rb +87 -0
  32. data/spec/controllers/autocomplete_controller_spec.rb +36 -0
  33. data/spec/dummy/README.rdoc +261 -0
  34. data/spec/dummy/Rakefile +7 -0
  35. data/spec/dummy/app/admin/dashboards.rb +33 -0
  36. data/spec/dummy/app/admin/posts.rb +20 -0
  37. data/spec/dummy/app/admin/tags.rb +11 -0
  38. data/spec/dummy/app/assets/javascripts/active_admin.js +8 -0
  39. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  40. data/spec/dummy/app/assets/stylesheets/active_admin.css.scss +6 -0
  41. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  42. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  43. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  44. data/spec/dummy/app/mailers/.gitkeep +0 -0
  45. data/spec/dummy/app/models/.gitkeep +0 -0
  46. data/spec/dummy/app/models/admin_user.rb +10 -0
  47. data/spec/dummy/app/models/post.rb +11 -0
  48. data/spec/dummy/app/models/tag.rb +15 -0
  49. data/spec/dummy/app/models/tagging.rb +7 -0
  50. data/spec/dummy/app/models/user.rb +8 -0
  51. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  52. data/spec/dummy/config.ru +4 -0
  53. data/spec/dummy/config/application.rb +63 -0
  54. data/spec/dummy/config/boot.rb +10 -0
  55. data/spec/dummy/config/database.yml +19 -0
  56. data/spec/dummy/config/environment.rb +5 -0
  57. data/spec/dummy/config/environments/development.rb +37 -0
  58. data/spec/dummy/config/environments/production.rb +67 -0
  59. data/spec/dummy/config/environments/test.rb +37 -0
  60. data/spec/dummy/config/initializers/active_admin.rb +129 -0
  61. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  62. data/spec/dummy/config/initializers/devise.rb +218 -0
  63. data/spec/dummy/config/initializers/inflections.rb +15 -0
  64. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  65. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  66. data/spec/dummy/config/initializers/session_store.rb +8 -0
  67. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  68. data/spec/dummy/config/locales/devise.en.yml +57 -0
  69. data/spec/dummy/config/locales/en.yml +5 -0
  70. data/spec/dummy/config/routes.rb +62 -0
  71. data/spec/dummy/db/.gitignore +1 -0
  72. data/spec/dummy/db/migrate/20120504220404_devise_create_admin_users.rb +52 -0
  73. data/spec/dummy/db/migrate/20120504221534_create_posts.rb +12 -0
  74. data/spec/dummy/db/migrate/20120504221936_create_users.rb +9 -0
  75. data/spec/dummy/db/migrate/20120504222040_create_tags.rb +8 -0
  76. data/spec/dummy/db/migrate/20120504222247_create_taggings.rb +10 -0
  77. data/spec/dummy/db/schema.rb +65 -0
  78. data/spec/dummy/lib/assets/.gitkeep +0 -0
  79. data/spec/dummy/public/404.html +26 -0
  80. data/spec/dummy/public/422.html +26 -0
  81. data/spec/dummy/public/500.html +25 -0
  82. data/spec/dummy/public/favicon.ico +0 -0
  83. data/spec/dummy/script/rails +6 -0
  84. data/spec/dummy/test/unit/admin_user_test.rb +7 -0
  85. data/spec/dummy/test/unit/post_test.rb +7 -0
  86. data/spec/dummy/test/unit/tag_test.rb +7 -0
  87. data/spec/dummy/test/unit/tagging_test.rb +7 -0
  88. data/spec/dummy/test/unit/user_test.rb +7 -0
  89. data/spec/factories/admin_users.rb +9 -0
  90. data/spec/factories/posts.rb +11 -0
  91. data/spec/factories/taggings.rb +6 -0
  92. data/spec/factories/tags.rb +7 -0
  93. data/spec/factories/users.rb +8 -0
  94. data/spec/features/active_admin_associations_spec.rb +94 -0
  95. data/spec/spec_helper.rb +41 -0
  96. data/spec/support/admin_login_controller_helper.rb +7 -0
  97. data/spec/support/admin_login_integration_helper.rb +8 -0
  98. data/vendor/assets/javascripts/active_admin_associations.js +14 -0
  99. data/vendor/assets/javascripts/jquery.tokeninput.js +915 -0
  100. data/vendor/assets/stylesheets/active_admin_associations.css.scss +18 -0
  101. data/vendor/assets/stylesheets/token-input-facebook.css +121 -0
  102. metadata +383 -0
@@ -0,0 +1,41 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ ENV['RAILS_ENV'] ||= 'test'
3
+ require 'simplecov'
4
+ require 'coveralls'
5
+ Coveralls.wear! 'rails'
6
+
7
+ require File.expand_path("../dummy/config/environment.rb", __FILE__)
8
+ require 'rspec/rails'
9
+ require 'rspec/autorun'
10
+ require 'shoulda-matchers'
11
+ require 'capybara/rails'
12
+ require 'capybara/rspec'
13
+ require 'database_cleaner'
14
+ require 'factory_girl_rails'
15
+
16
+ Rails.backtrace_cleaner.remove_silencers!
17
+
18
+ DatabaseCleaner.strategy = :truncation
19
+
20
+ Warden.test_mode!
21
+
22
+ # Load support files
23
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
24
+
25
+ RSpec.configure do |config|
26
+ config.mock_with :rspec
27
+ config.use_transactional_fixtures = true
28
+ config.infer_base_class_for_anonymous_controllers = false
29
+ config.order = "random"
30
+
31
+ config.include Devise::TestHelpers, :type => :controller
32
+ config.include Warden::Test::Helpers, :type => :feature
33
+ config.include AdminLoginIntegrationHelper, :type => :feature
34
+ config.include AdminLoginControllerHelper, :type => :controller
35
+
36
+ config.after(:each, :type => :request) do
37
+ DatabaseCleaner.clean # Truncate the database
38
+ Capybara.reset_sessions! # Forget the (simulated) browser state
39
+ Capybara.use_default_driver # Revert Capybara.current_driver to Capybara.default_driver
40
+ end
41
+ end
@@ -0,0 +1,7 @@
1
+ module AdminLoginControllerHelper
2
+ def admin_login_as(admin_user = Factory(:admin_user))
3
+ request.env["devise.mapping"] = Devise.mappings[:admin_user]
4
+ sign_in admin_user
5
+ admin_user
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ module AdminLoginIntegrationHelper
2
+ def admin_login_as(admin_user = Factory(:admin_user))
3
+ visit '/admin/login'
4
+ fill_in 'admin_user_email', :with => admin_user.email
5
+ fill_in 'admin_user_password', :with => 'BaudP0wer!'
6
+ click_button 'Login'
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ //= require jquery.tokeninput
2
+
3
+ $(document).ready(function(){
4
+ $('input.token-input').tokenInput(function($input){
5
+ var modelName = $input.data("model-name");
6
+ return "/autocomplete/"+modelName;
7
+ }, {
8
+ minChars: 3,
9
+ propertyToSearch: "value",
10
+ theme: "facebook",
11
+ tokenLimit: 1,
12
+ preventDuplicates: true
13
+ });
14
+ });
@@ -0,0 +1,915 @@
1
+ /*
2
+ * jQuery Plugin: Tokenizing Autocomplete Text Entry
3
+ * Version 1.6.0
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
+ // Search settings
15
+ method: "GET",
16
+ queryParam: "q",
17
+ searchDelay: 300,
18
+ minChars: 1,
19
+ propertyToSearch: "name",
20
+ jsonContainer: null,
21
+ contentType: "json",
22
+
23
+ // Prepopulation settings
24
+ prePopulate: null,
25
+ processPrePopulate: false,
26
+
27
+ // Display settings
28
+ hintText: "Type in a search term",
29
+ noResultsText: "No results",
30
+ searchingText: "Searching...",
31
+ deleteText: "×",
32
+ animateDropdown: true,
33
+ theme: null,
34
+ zindex: 999,
35
+ resultsFormatter: function(item){ return "<li>" + item[this.propertyToSearch]+ "</li>" },
36
+ tokenFormatter: function(item) { return "<li><p>" + item[this.propertyToSearch] + "</p></li>" },
37
+
38
+ // Tokenization settings
39
+ tokenLimit: null,
40
+ tokenDelimiter: ",",
41
+ preventDuplicates: false,
42
+ tokenValue: "id",
43
+
44
+ // Callbacks
45
+ onResult: null,
46
+ onAdd: null,
47
+ onDelete: null,
48
+ onReady: null,
49
+
50
+ // Other settings
51
+ idPrefix: "token-input-",
52
+
53
+ // Keep track if the input is currently in disabled mode
54
+ disabled: false
55
+ };
56
+
57
+ // Default classes to use when theming
58
+ var DEFAULT_CLASSES = {
59
+ tokenList: "token-input-list",
60
+ token: "token-input-token",
61
+ tokenDelete: "token-input-delete-token",
62
+ selectedToken: "token-input-selected-token",
63
+ highlightedToken: "token-input-highlighted-token",
64
+ dropdown: "token-input-dropdown",
65
+ dropdownItem: "token-input-dropdown-item",
66
+ dropdownItem2: "token-input-dropdown-item2",
67
+ selectedDropdownItem: "token-input-selected-dropdown-item",
68
+ inputToken: "token-input-input-token",
69
+ focused: "token-input-focused",
70
+ disabled: "token-input-disabled"
71
+ };
72
+
73
+ // Input box position "enum"
74
+ var POSITION = {
75
+ BEFORE: 0,
76
+ AFTER: 1,
77
+ END: 2
78
+ };
79
+
80
+ // Keys "enum"
81
+ var KEY = {
82
+ BACKSPACE: 8,
83
+ TAB: 9,
84
+ ENTER: 13,
85
+ ESCAPE: 27,
86
+ SPACE: 32,
87
+ PAGE_UP: 33,
88
+ PAGE_DOWN: 34,
89
+ END: 35,
90
+ HOME: 36,
91
+ LEFT: 37,
92
+ UP: 38,
93
+ RIGHT: 39,
94
+ DOWN: 40,
95
+ NUMPAD_ENTER: 108,
96
+ COMMA: 188
97
+ };
98
+
99
+ // Additional public (exposed) methods
100
+ var methods = {
101
+ init: function(url_or_data_or_function, options) {
102
+ var settings = $.extend({}, DEFAULT_SETTINGS, options || {});
103
+
104
+ return this.each(function () {
105
+ $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings));
106
+ });
107
+ },
108
+ clear: function() {
109
+ this.data("tokenInputObject").clear();
110
+ return this;
111
+ },
112
+ add: function(item) {
113
+ this.data("tokenInputObject").add(item);
114
+ return this;
115
+ },
116
+ remove: function(item) {
117
+ this.data("tokenInputObject").remove(item);
118
+ return this;
119
+ },
120
+ get: function() {
121
+ return this.data("tokenInputObject").getTokens();
122
+ },
123
+ toggleDisabled: function(disable) {
124
+ this.data("tokenInputObject").toggleDisabled(disable);
125
+ return this;
126
+ }
127
+ }
128
+
129
+ // Expose the .tokenInput function to jQuery as a plugin
130
+ $.fn.tokenInput = function (method) {
131
+ // Method calling and initialization logic
132
+ if(methods[method]) {
133
+ return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
134
+ } else {
135
+ return methods.init.apply(this, arguments);
136
+ }
137
+ };
138
+
139
+ // TokenList class for each input
140
+ $.TokenList = function (input, url_or_data, settings) {
141
+ //
142
+ // Initialization
143
+ //
144
+
145
+ // Configure the data source
146
+ if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") {
147
+ // Set the url to query against
148
+ settings.url = url_or_data;
149
+
150
+ // If the URL is a function, evaluate it here to do our initalization work
151
+ var url = computeURL();
152
+
153
+ // Make a smart guess about cross-domain if it wasn't explicitly specified
154
+ if(settings.crossDomain === undefined && typeof url === "string") {
155
+ if(url.indexOf("://") === -1) {
156
+ settings.crossDomain = false;
157
+ } else {
158
+ settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]);
159
+ }
160
+ }
161
+ } else if(typeof(url_or_data) === "object") {
162
+ // Set the local data to search through
163
+ settings.local_data = url_or_data;
164
+ }
165
+
166
+ // Build class names
167
+ if(settings.classes) {
168
+ // Use custom class names
169
+ settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes);
170
+ } else if(settings.theme) {
171
+ // Use theme-suffixed default class names
172
+ settings.classes = {};
173
+ $.each(DEFAULT_CLASSES, function(key, value) {
174
+ settings.classes[key] = value + "-" + settings.theme;
175
+ });
176
+ } else {
177
+ settings.classes = DEFAULT_CLASSES;
178
+ }
179
+
180
+
181
+ // Save the tokens
182
+ var saved_tokens = [];
183
+
184
+ // Keep track of the number of tokens in the list
185
+ var token_count = 0;
186
+
187
+ // Basic cache to save on db hits
188
+ var cache = new $.TokenList.Cache();
189
+
190
+ // Keep track of the timeout, old vals
191
+ var timeout;
192
+ var input_val;
193
+
194
+ // Create a new text input an attach keyup events
195
+ var input_box = $("<input type=\"text\" autocomplete=\"off\">")
196
+ .css({
197
+ outline: "none"
198
+ })
199
+ .attr("id", settings.idPrefix + input.id)
200
+ .focus(function () {
201
+ if (settings.disabled) {
202
+ return false;
203
+ } else
204
+ if (settings.tokenLimit === null || settings.tokenLimit !== token_count) {
205
+ show_dropdown_hint();
206
+ }
207
+ token_list.addClass(settings.classes.focused);
208
+ })
209
+ .blur(function () {
210
+ hide_dropdown();
211
+ $(this).val("");
212
+ token_list.removeClass(settings.classes.focused);
213
+ })
214
+ .bind("keyup keydown blur update", resize_input)
215
+ .keydown(function (event) {
216
+ var previous_token;
217
+ var next_token;
218
+
219
+ switch(event.keyCode) {
220
+ case KEY.LEFT:
221
+ case KEY.RIGHT:
222
+ case KEY.UP:
223
+ case KEY.DOWN:
224
+ if(!$(this).val()) {
225
+ previous_token = input_token.prev();
226
+ next_token = input_token.next();
227
+
228
+ if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
229
+ // Check if there is a previous/next token and it is selected
230
+ if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) {
231
+ deselect_token($(selected_token), POSITION.BEFORE);
232
+ } else {
233
+ deselect_token($(selected_token), POSITION.AFTER);
234
+ }
235
+ } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) {
236
+ // We are moving left, select the previous token if it exists
237
+ select_token($(previous_token.get(0)));
238
+ } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) {
239
+ // We are moving right, select the next token if it exists
240
+ select_token($(next_token.get(0)));
241
+ }
242
+ } else {
243
+ var dropdown_item = null;
244
+
245
+ if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) {
246
+ dropdown_item = $(selected_dropdown_item).next();
247
+ } else {
248
+ dropdown_item = $(selected_dropdown_item).prev();
249
+ }
250
+
251
+ if(dropdown_item.length) {
252
+ select_dropdown_item(dropdown_item);
253
+ }
254
+ }
255
+ return false;
256
+ break;
257
+
258
+ case KEY.BACKSPACE:
259
+ previous_token = input_token.prev();
260
+
261
+ if(!$(this).val().length) {
262
+ if(selected_token) {
263
+ delete_token($(selected_token));
264
+ hidden_input.change();
265
+ } else if(previous_token.length) {
266
+ select_token($(previous_token.get(0)));
267
+ }
268
+
269
+ return false;
270
+ } else if($(this).val().length === 1) {
271
+ hide_dropdown();
272
+ } else {
273
+ // set a timeout just long enough to let this function finish.
274
+ setTimeout(function(){do_search();}, 5);
275
+ }
276
+ break;
277
+
278
+ case KEY.TAB:
279
+ case KEY.ENTER:
280
+ case KEY.NUMPAD_ENTER:
281
+ case KEY.COMMA:
282
+ if(selected_dropdown_item) {
283
+ add_token($(selected_dropdown_item).data("tokeninput"));
284
+ hidden_input.change();
285
+ return false;
286
+ }
287
+ break;
288
+
289
+ case KEY.ESCAPE:
290
+ hide_dropdown();
291
+ return true;
292
+
293
+ default:
294
+ if(String.fromCharCode(event.which)) {
295
+ // set a timeout just long enough to let this function finish.
296
+ setTimeout(function(){do_search();}, 5);
297
+ }
298
+ break;
299
+ }
300
+ });
301
+
302
+ // Keep a reference to the original input box
303
+ var hidden_input = $(input)
304
+ .hide()
305
+ .val("")
306
+ .focus(function () {
307
+ focus_with_timeout(input_box);
308
+ })
309
+ .blur(function () {
310
+ input_box.blur();
311
+ });
312
+
313
+ // Keep a reference to the selected token and dropdown item
314
+ var selected_token = null;
315
+ var selected_token_index = 0;
316
+ var selected_dropdown_item = null;
317
+
318
+ // The list to store the token items in
319
+ var token_list = $("<ul />")
320
+ .addClass(settings.classes.tokenList)
321
+ .click(function (event) {
322
+ var li = $(event.target).closest("li");
323
+ if(li && li.get(0) && $.data(li.get(0), "tokeninput")) {
324
+ toggle_select_token(li);
325
+ } else {
326
+ // Deselect selected token
327
+ if(selected_token) {
328
+ deselect_token($(selected_token), POSITION.END);
329
+ }
330
+
331
+ // Focus input box
332
+ focus_with_timeout(input_box);
333
+ }
334
+ })
335
+ .mouseover(function (event) {
336
+ var li = $(event.target).closest("li");
337
+ if(li && selected_token !== this && li.get(0) != $(this).closest("li").get(0)) {
338
+ li.addClass(settings.classes.highlightedToken);
339
+ }
340
+ })
341
+ .mouseout(function (event) {
342
+ var li = $(event.target).closest("li");
343
+ if(li && selected_token !== this) {
344
+ li.removeClass(settings.classes.highlightedToken);
345
+ }
346
+ })
347
+ .insertBefore(hidden_input);
348
+
349
+ // The token holding the input box
350
+ var input_token = $("<li />")
351
+ .addClass(settings.classes.inputToken)
352
+ .appendTo(token_list)
353
+ .append(input_box);
354
+
355
+ // The list to store the dropdown items in
356
+ var dropdown = $("<div>")
357
+ .addClass(settings.classes.dropdown)
358
+ .appendTo("body")
359
+ .hide();
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();
386
+ });
387
+ }
388
+
389
+ // Check if widget should initialize as disabled
390
+ if (settings.disabled) {
391
+ toggleDisabled(true);
392
+ }
393
+
394
+ // Initialization is done
395
+ if($.isFunction(settings.onReady)) {
396
+ settings.onReady.call();
397
+ }
398
+
399
+ //
400
+ // Public functions
401
+ //
402
+
403
+ this.clear = function() {
404
+ token_list.children("li").each(function() {
405
+ if ($(this).children("input").length === 0) {
406
+ delete_token($(this));
407
+ }
408
+ });
409
+ }
410
+
411
+ this.add = function(item) {
412
+ add_token(item);
413
+ }
414
+
415
+ this.remove = function(item) {
416
+ token_list.children("li").each(function() {
417
+ if ($(this).children("input").length === 0) {
418
+ var currToken = $(this).data("tokeninput");
419
+ var match = true;
420
+ for (var prop in item) {
421
+ if (item[prop] !== currToken[prop]) {
422
+ match = false;
423
+ break;
424
+ }
425
+ }
426
+ if (match) {
427
+ delete_token($(this));
428
+ }
429
+ }
430
+ });
431
+ }
432
+
433
+ this.getTokens = function() {
434
+ return saved_tokens;
435
+ }
436
+
437
+ this.toggleDisabled = function(disable) {
438
+ toggleDisabled(disable);
439
+ }
440
+
441
+ //
442
+ // Private functions
443
+ //
444
+
445
+ // Toggles the widget between enabled and disabled state, or according
446
+ // to the [disable] parameter.
447
+ function toggleDisabled(disable) {
448
+ if (typeof disable === 'boolean') {
449
+ settings.disabled = disable
450
+ } else {
451
+ settings.disabled = !settings.disabled;
452
+ }
453
+ input_box.prop('disabled', settings.disabled);
454
+ token_list.toggleClass(settings.classes.disabled, settings.disabled);
455
+ // if there is any token selected we deselect it
456
+ if(selected_token) {
457
+ deselect_token($(selected_token), POSITION.END);
458
+ }
459
+ hidden_input.prop('disabled', settings.disabled);
460
+ }
461
+
462
+ function checkTokenLimit() {
463
+ if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
464
+ input_box.hide();
465
+ hide_dropdown();
466
+ return;
467
+ }
468
+ }
469
+
470
+ function resize_input() {
471
+ if(input_val === (input_val = input_box.val())) {return;}
472
+
473
+ // Enter new content into resizer and resize input accordingly
474
+ var escaped = input_val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
475
+ input_resizer.html(escaped);
476
+ input_box.width(input_resizer.width() + 30);
477
+ }
478
+
479
+ function is_printable_character(keycode) {
480
+ return ((keycode >= 48 && keycode <= 90) || // 0-1a-z
481
+ (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * .
482
+ (keycode >= 186 && keycode <= 192) || // ; = , - . / ^
483
+ (keycode >= 219 && keycode <= 222)); // ( \ ) '
484
+ }
485
+
486
+ // Inner function to a token to the list
487
+ function insert_token(item) {
488
+ var this_token = settings.tokenFormatter(item);
489
+ this_token = $(this_token)
490
+ .addClass(settings.classes.token)
491
+ .insertBefore(input_token);
492
+
493
+ // The 'delete token' button
494
+ $("<span>" + settings.deleteText + "</span>")
495
+ .addClass(settings.classes.tokenDelete)
496
+ .appendTo(this_token)
497
+ .click(function () {
498
+ if (!settings.disabled) {
499
+ delete_token($(this).parent());
500
+ hidden_input.change();
501
+ return false;
502
+ }
503
+ });
504
+
505
+ // Store data on the token
506
+ var token_data = item;
507
+ $.data(this_token.get(0), "tokeninput", item);
508
+
509
+ // Save this token for duplicate checking
510
+ saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index));
511
+ selected_token_index++;
512
+
513
+ // Update the hidden input
514
+ update_hidden_input(saved_tokens, hidden_input);
515
+
516
+ token_count += 1;
517
+
518
+ // Check the token limit
519
+ if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
520
+ input_box.hide();
521
+ hide_dropdown();
522
+ }
523
+
524
+ return this_token;
525
+ }
526
+
527
+ // Add a token to the token list based on user input
528
+ function add_token (item) {
529
+ var callback = settings.onAdd;
530
+
531
+ // See if the token already exists and select it if we don't want duplicates
532
+ if(token_count > 0 && settings.preventDuplicates) {
533
+ var found_existing_token = null;
534
+ token_list.children().each(function () {
535
+ var existing_token = $(this);
536
+ var existing_data = $.data(existing_token.get(0), "tokeninput");
537
+ if(existing_data && existing_data.id === item.id) {
538
+ found_existing_token = existing_token;
539
+ return false;
540
+ }
541
+ });
542
+
543
+ if(found_existing_token) {
544
+ select_token(found_existing_token);
545
+ input_token.insertAfter(found_existing_token);
546
+ focus_with_timeout(input_box);
547
+ return;
548
+ }
549
+ }
550
+
551
+ // Insert the new tokens
552
+ if(settings.tokenLimit == null || token_count < settings.tokenLimit) {
553
+ insert_token(item);
554
+ checkTokenLimit();
555
+ }
556
+
557
+ // Clear input box
558
+ input_box.val("");
559
+
560
+ // Don't show the help dropdown, they've got the idea
561
+ hide_dropdown();
562
+
563
+ // Execute the onAdd callback if defined
564
+ if($.isFunction(callback)) {
565
+ callback.call(hidden_input,item);
566
+ }
567
+ }
568
+
569
+ // Select a token in the token list
570
+ function select_token (token) {
571
+ if (!settings.disabled) {
572
+ token.addClass(settings.classes.selectedToken);
573
+ selected_token = token.get(0);
574
+
575
+ // Hide input box
576
+ input_box.val("");
577
+
578
+ // Hide dropdown if it is visible (eg if we clicked to select token)
579
+ hide_dropdown();
580
+ }
581
+ }
582
+
583
+ // Deselect a token in the token list
584
+ function deselect_token (token, position) {
585
+ token.removeClass(settings.classes.selectedToken);
586
+ selected_token = null;
587
+
588
+ if(position === POSITION.BEFORE) {
589
+ input_token.insertBefore(token);
590
+ selected_token_index--;
591
+ } else if(position === POSITION.AFTER) {
592
+ input_token.insertAfter(token);
593
+ selected_token_index++;
594
+ } else {
595
+ input_token.appendTo(token_list);
596
+ selected_token_index = token_count;
597
+ }
598
+
599
+ // Show the input box and give it focus again
600
+ focus_with_timeout(input_box);
601
+ }
602
+
603
+ // Toggle selection of a token in the token list
604
+ function toggle_select_token(token) {
605
+ var previous_selected_token = selected_token;
606
+
607
+ if(selected_token) {
608
+ deselect_token($(selected_token), POSITION.END);
609
+ }
610
+
611
+ if(previous_selected_token === token.get(0)) {
612
+ deselect_token(token, POSITION.END);
613
+ } else {
614
+ select_token(token);
615
+ }
616
+ }
617
+
618
+ // Delete a token from the token list
619
+ function delete_token (token) {
620
+ // Remove the id from the saved list
621
+ var token_data = $.data(token.get(0), "tokeninput");
622
+ var callback = settings.onDelete;
623
+
624
+ var index = token.prevAll().length;
625
+ if(index > selected_token_index) index--;
626
+
627
+ // Delete the token
628
+ token.remove();
629
+ selected_token = null;
630
+
631
+ // Show the input box and give it focus again
632
+ focus_with_timeout(input_box);
633
+
634
+ // Remove this token from the saved list
635
+ saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1));
636
+ if(index < selected_token_index) selected_token_index--;
637
+
638
+ // Update the hidden input
639
+ update_hidden_input(saved_tokens, hidden_input);
640
+
641
+ token_count -= 1;
642
+
643
+ if(settings.tokenLimit !== null) {
644
+ input_box
645
+ .show()
646
+ .val("");
647
+ focus_with_timeout(input_box);
648
+ }
649
+
650
+ // Execute the onDelete callback if defined
651
+ if($.isFunction(callback)) {
652
+ callback.call(hidden_input,token_data);
653
+ }
654
+ }
655
+
656
+ // Update the hidden input box value
657
+ function update_hidden_input(saved_tokens, hidden_input) {
658
+ var token_values = $.map(saved_tokens, function (el) {
659
+ if(typeof settings.tokenValue == 'function')
660
+ return settings.tokenValue.call(this, el);
661
+
662
+ return el[settings.tokenValue];
663
+ });
664
+ hidden_input.val(token_values.join(settings.tokenDelimiter));
665
+
666
+ }
667
+
668
+ // Hide and clear the results dropdown
669
+ function hide_dropdown () {
670
+ dropdown.hide().empty();
671
+ selected_dropdown_item = null;
672
+ }
673
+
674
+ function show_dropdown() {
675
+ dropdown
676
+ .css({
677
+ position: "absolute",
678
+ top: $(token_list).offset().top + $(token_list).outerHeight(),
679
+ left: $(token_list).offset().left,
680
+ width: $(token_list).outerWidth(),
681
+ 'z-index': settings.zindex
682
+ })
683
+ .show();
684
+ }
685
+
686
+ function show_dropdown_searching () {
687
+ if(settings.searchingText) {
688
+ dropdown.html("<p>"+settings.searchingText+"</p>");
689
+ show_dropdown();
690
+ }
691
+ }
692
+
693
+ function show_dropdown_hint () {
694
+ if(settings.hintText) {
695
+ dropdown.html("<p>"+settings.hintText+"</p>");
696
+ show_dropdown();
697
+ }
698
+ }
699
+
700
+ // Highlight the query part of the search term
701
+ function highlight_term(value, term) {
702
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
703
+ }
704
+
705
+ function find_value_and_highlight_term(template, value, term) {
706
+ return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + value + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term));
707
+ }
708
+
709
+ // Populate the results dropdown with some results
710
+ function populate_dropdown (query, results) {
711
+ if(results && results.length) {
712
+ dropdown.empty();
713
+ var dropdown_ul = $("<ul>")
714
+ .appendTo(dropdown)
715
+ .mouseover(function (event) {
716
+ select_dropdown_item($(event.target).closest("li"));
717
+ })
718
+ .mousedown(function (event) {
719
+ add_token($(event.target).closest("li").data("tokeninput"));
720
+ hidden_input.change();
721
+ return false;
722
+ })
723
+ .hide();
724
+
725
+ $.each(results, function(index, value) {
726
+ var this_li = settings.resultsFormatter(value);
727
+
728
+ this_li = find_value_and_highlight_term(this_li ,value[settings.propertyToSearch], query);
729
+
730
+ this_li = $(this_li).appendTo(dropdown_ul);
731
+
732
+ if(index % 2) {
733
+ this_li.addClass(settings.classes.dropdownItem);
734
+ } else {
735
+ this_li.addClass(settings.classes.dropdownItem2);
736
+ }
737
+
738
+ if(index === 0) {
739
+ select_dropdown_item(this_li);
740
+ }
741
+
742
+ $.data(this_li.get(0), "tokeninput", value);
743
+ });
744
+
745
+ show_dropdown();
746
+
747
+ if(settings.animateDropdown) {
748
+ dropdown_ul.slideDown("fast");
749
+ } else {
750
+ dropdown_ul.show();
751
+ }
752
+ } else {
753
+ if(settings.noResultsText) {
754
+ dropdown.html("<p>"+settings.noResultsText+"</p>");
755
+ show_dropdown();
756
+ }
757
+ }
758
+ }
759
+
760
+ // Highlight an item in the results dropdown
761
+ function select_dropdown_item (item) {
762
+ if(item) {
763
+ if(selected_dropdown_item) {
764
+ deselect_dropdown_item($(selected_dropdown_item));
765
+ }
766
+
767
+ item.addClass(settings.classes.selectedDropdownItem);
768
+ selected_dropdown_item = item.get(0);
769
+ }
770
+ }
771
+
772
+ // Remove highlighting from an item in the results dropdown
773
+ function deselect_dropdown_item (item) {
774
+ item.removeClass(settings.classes.selectedDropdownItem);
775
+ selected_dropdown_item = null;
776
+ }
777
+
778
+ // Do a search and show the "searching" dropdown if the input is longer
779
+ // than settings.minChars
780
+ function do_search() {
781
+ var query = input_box.val();
782
+
783
+ if(query && query.length) {
784
+ if(selected_token) {
785
+ deselect_token($(selected_token), POSITION.AFTER);
786
+ }
787
+
788
+ if(query.length >= settings.minChars) {
789
+ show_dropdown_searching();
790
+ clearTimeout(timeout);
791
+
792
+ timeout = setTimeout(function(){
793
+ run_search(query);
794
+ }, settings.searchDelay);
795
+ } else {
796
+ hide_dropdown();
797
+ }
798
+ }
799
+ }
800
+
801
+ // Do the actual search
802
+ function run_search(query) {
803
+ var cache_key = query + computeURL();
804
+ var cached_results = cache.get(cache_key);
805
+ if(cached_results) {
806
+ populate_dropdown(query, cached_results);
807
+ } else {
808
+ // Are we doing an ajax search or local data search?
809
+ if(settings.url) {
810
+ var url = computeURL();
811
+ // Extract exisiting get params
812
+ var ajax_params = {};
813
+ ajax_params.data = {};
814
+ if(url.indexOf("?") > -1) {
815
+ var parts = url.split("?");
816
+ ajax_params.url = parts[0];
817
+
818
+ var param_array = parts[1].split("&");
819
+ $.each(param_array, function (index, value) {
820
+ var kv = value.split("=");
821
+ ajax_params.data[kv[0]] = kv[1];
822
+ });
823
+ } else {
824
+ ajax_params.url = url;
825
+ }
826
+
827
+ // Prepare the request
828
+ ajax_params.data[settings.queryParam] = query;
829
+ ajax_params.type = settings.method;
830
+ ajax_params.dataType = settings.contentType;
831
+ if(settings.crossDomain) {
832
+ ajax_params.dataType = "jsonp";
833
+ }
834
+
835
+ // Attach the success callback
836
+ ajax_params.success = function(results) {
837
+ if($.isFunction(settings.onResult)) {
838
+ results = settings.onResult.call(hidden_input, results);
839
+ }
840
+ cache.add(cache_key, settings.jsonContainer ? results[settings.jsonContainer] : results);
841
+
842
+ // only populate the dropdown if the results are associated with the active search query
843
+ if(input_box.val() === query) {
844
+ populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
845
+ }
846
+ };
847
+
848
+ // Make the request
849
+ $.ajax(ajax_params);
850
+ } else if(settings.local_data) {
851
+ // Do the search through local data
852
+ var results = $.grep(settings.local_data, function (row) {
853
+ return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1;
854
+ });
855
+
856
+ if($.isFunction(settings.onResult)) {
857
+ results = settings.onResult.call(hidden_input, results);
858
+ }
859
+ cache.add(cache_key, results);
860
+ populate_dropdown(query, results);
861
+ }
862
+ }
863
+ }
864
+
865
+ // compute the dynamic URL
866
+ function computeURL() {
867
+ var url = settings.url;
868
+ if(typeof settings.url == 'function') {
869
+ url = settings.url.call(settings, $(input));
870
+ }
871
+ return url;
872
+ }
873
+
874
+ // Bring browser focus to the specified object.
875
+ // Use of setTimeout is to get around an IE bug.
876
+ // (See, e.g., http://stackoverflow.com/questions/2600186/focus-doesnt-work-in-ie)
877
+ //
878
+ // obj: a jQuery object to focus()
879
+ function focus_with_timeout(obj) {
880
+ setTimeout(function() { obj.focus(); }, 50);
881
+ }
882
+
883
+ };
884
+
885
+ // Really basic cache for the results
886
+ $.TokenList.Cache = function (options) {
887
+ var settings = $.extend({
888
+ max_size: 500
889
+ }, options);
890
+
891
+ var data = {};
892
+ var size = 0;
893
+
894
+ var flush = function () {
895
+ data = {};
896
+ size = 0;
897
+ };
898
+
899
+ this.add = function (query, results) {
900
+ if(size > settings.max_size) {
901
+ flush();
902
+ }
903
+
904
+ if(!data[query]) {
905
+ size += 1;
906
+ }
907
+
908
+ data[query] = results;
909
+ };
910
+
911
+ this.get = function (query) {
912
+ return data[query];
913
+ };
914
+ };
915
+ }(jQuery));