jquery-tokeninput-rails 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jquery-tokeninput-rails.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 foohey
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,42 @@
1
+ # jquery-tokeninput-rails
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'jquery-tokeninput-rails'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install jquery-tokeninput-rails
18
+
19
+ ## Usage
20
+
21
+ ### Javascript include
22
+ ```javascript
23
+ //= require jquery.tokeninput
24
+ ```
25
+
26
+ ### Stylesheet include
27
+
28
+ ```css
29
+ //= require token-input
30
+ ```
31
+
32
+ Or
33
+
34
+ ```css
35
+ //= require token-input-facebook
36
+ ```
37
+
38
+ Or
39
+
40
+ ```css
41
+ //= require token-input-mac
42
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/jquery-tokeninput-rails/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["foohey"]
6
+ gem.email = ["offim.r@gmail.com"]
7
+ gem.description = %q{jquery-tokeninput for rails}
8
+ gem.summary = %q{Integration with the asset pipeline}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "jquery-tokeninput-rails"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Jquery::Tokeninput::Rails::VERSION
17
+ end
@@ -0,0 +1,10 @@
1
+ require "jquery-tokeninput-rails/version"
2
+
3
+ module Jquery
4
+ module Tokeninput
5
+ module Rails
6
+ class Engine < ::Rails::Engine
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Jquery
2
+ module Tokeninput
3
+ module Rails
4
+ VERSION = "1.6.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,930 @@
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: "&times;",
32
+ animateDropdown: true,
33
+ theme: null,
34
+ zindex: 999,
35
+ resultsLimit: null,
36
+ resultsFormatter: function(item){ return "<li>" + item[this.propertyToSearch]+ "</li>" },
37
+ tokenFormatter: function(item) { return "<li><p>" + item[this.propertyToSearch] + "</p></li>" },
38
+
39
+ // Tokenization settings
40
+ tokenLimit: null,
41
+ tokenDelimiter: ",",
42
+ preventDuplicates: false,
43
+ tokenValue: "id",
44
+
45
+ // Callbacks
46
+ onResult: null,
47
+ onAdd: null,
48
+ onDelete: null,
49
+ onReady: null,
50
+
51
+ // Other settings
52
+ idPrefix: "token-input-",
53
+
54
+ // Keep track if the input is currently in disabled mode
55
+ disabled: false
56
+ };
57
+
58
+ // Default classes to use when theming
59
+ var DEFAULT_CLASSES = {
60
+ tokenList: "token-input-list",
61
+ token: "token-input-token",
62
+ tokenReadOnly: "token-input-token-readonly",
63
+ tokenDelete: "token-input-delete-token",
64
+ selectedToken: "token-input-selected-token",
65
+ highlightedToken: "token-input-highlighted-token",
66
+ dropdown: "token-input-dropdown",
67
+ dropdownItem: "token-input-dropdown-item",
68
+ dropdownItem2: "token-input-dropdown-item2",
69
+ selectedDropdownItem: "token-input-selected-dropdown-item",
70
+ inputToken: "token-input-input-token",
71
+ focused: "token-input-focused",
72
+ disabled: "token-input-disabled"
73
+ };
74
+
75
+ // Input box position "enum"
76
+ var POSITION = {
77
+ BEFORE: 0,
78
+ AFTER: 1,
79
+ END: 2
80
+ };
81
+
82
+ // Keys "enum"
83
+ var KEY = {
84
+ BACKSPACE: 8,
85
+ TAB: 9,
86
+ ENTER: 13,
87
+ ESCAPE: 27,
88
+ SPACE: 32,
89
+ PAGE_UP: 33,
90
+ PAGE_DOWN: 34,
91
+ END: 35,
92
+ HOME: 36,
93
+ LEFT: 37,
94
+ UP: 38,
95
+ RIGHT: 39,
96
+ DOWN: 40,
97
+ NUMPAD_ENTER: 108,
98
+ COMMA: 188
99
+ };
100
+
101
+ // Additional public (exposed) methods
102
+ var methods = {
103
+ init: function(url_or_data_or_function, options) {
104
+ var settings = $.extend({}, DEFAULT_SETTINGS, options || {});
105
+
106
+ return this.each(function () {
107
+ $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings));
108
+ });
109
+ },
110
+ clear: function() {
111
+ this.data("tokenInputObject").clear();
112
+ return this;
113
+ },
114
+ add: function(item) {
115
+ this.data("tokenInputObject").add(item);
116
+ return this;
117
+ },
118
+ remove: function(item) {
119
+ this.data("tokenInputObject").remove(item);
120
+ return this;
121
+ },
122
+ get: function() {
123
+ return this.data("tokenInputObject").getTokens();
124
+ },
125
+ toggleDisabled: function(disable) {
126
+ this.data("tokenInputObject").toggleDisabled(disable);
127
+ return this;
128
+ }
129
+ }
130
+
131
+ // Expose the .tokenInput function to jQuery as a plugin
132
+ $.fn.tokenInput = function (method) {
133
+ // Method calling and initialization logic
134
+ if(methods[method]) {
135
+ return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
136
+ } else {
137
+ return methods.init.apply(this, arguments);
138
+ }
139
+ };
140
+
141
+ // TokenList class for each input
142
+ $.TokenList = function (input, url_or_data, settings) {
143
+ //
144
+ // Initialization
145
+ //
146
+
147
+ // Configure the data source
148
+ if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") {
149
+ // Set the url to query against
150
+ settings.url = url_or_data;
151
+
152
+ // If the URL is a function, evaluate it here to do our initalization work
153
+ var url = computeURL();
154
+
155
+ // Make a smart guess about cross-domain if it wasn't explicitly specified
156
+ if(settings.crossDomain === undefined && typeof url === "string") {
157
+ if(url.indexOf("://") === -1) {
158
+ settings.crossDomain = false;
159
+ } else {
160
+ settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]);
161
+ }
162
+ }
163
+ } else if(typeof(url_or_data) === "object") {
164
+ // Set the local data to search through
165
+ settings.local_data = url_or_data;
166
+ }
167
+
168
+ // Build class names
169
+ if(settings.classes) {
170
+ // Use custom class names
171
+ settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes);
172
+ } else if(settings.theme) {
173
+ // Use theme-suffixed default class names
174
+ settings.classes = {};
175
+ $.each(DEFAULT_CLASSES, function(key, value) {
176
+ settings.classes[key] = value + "-" + settings.theme;
177
+ });
178
+ } else {
179
+ settings.classes = DEFAULT_CLASSES;
180
+ }
181
+
182
+
183
+ // Save the tokens
184
+ var saved_tokens = [];
185
+
186
+ // Keep track of the number of tokens in the list
187
+ var token_count = 0;
188
+
189
+ // Basic cache to save on db hits
190
+ var cache = new $.TokenList.Cache();
191
+
192
+ // Keep track of the timeout, old vals
193
+ var timeout;
194
+ var input_val;
195
+
196
+ // Create a new text input an attach keyup events
197
+ var input_box = $("<input type=\"text\" autocomplete=\"off\">")
198
+ .css({
199
+ outline: "none"
200
+ })
201
+ .attr("id", settings.idPrefix + input.id)
202
+ .focus(function () {
203
+ if (settings.disabled) {
204
+ return false;
205
+ } else
206
+ if (settings.tokenLimit === null || settings.tokenLimit !== token_count) {
207
+ show_dropdown_hint();
208
+ }
209
+ token_list.addClass(settings.classes.focused);
210
+ })
211
+ .blur(function () {
212
+ hide_dropdown();
213
+ $(this).val("");
214
+ token_list.removeClass(settings.classes.focused);
215
+ })
216
+ .bind("keyup keydown blur update", resize_input)
217
+ .keydown(function (event) {
218
+ var previous_token;
219
+ var next_token;
220
+
221
+ switch(event.keyCode) {
222
+ case KEY.LEFT:
223
+ case KEY.RIGHT:
224
+ case KEY.UP:
225
+ case KEY.DOWN:
226
+ if(!$(this).val()) {
227
+ previous_token = input_token.prev();
228
+ next_token = input_token.next();
229
+
230
+ if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
231
+ // Check if there is a previous/next token and it is selected
232
+ if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) {
233
+ deselect_token($(selected_token), POSITION.BEFORE);
234
+ } else {
235
+ deselect_token($(selected_token), POSITION.AFTER);
236
+ }
237
+ } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) {
238
+ // We are moving left, select the previous token if it exists
239
+ select_token($(previous_token.get(0)));
240
+ } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) {
241
+ // We are moving right, select the next token if it exists
242
+ select_token($(next_token.get(0)));
243
+ }
244
+ } else {
245
+ var dropdown_item = null;
246
+
247
+ if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) {
248
+ dropdown_item = $(selected_dropdown_item).next();
249
+ } else {
250
+ dropdown_item = $(selected_dropdown_item).prev();
251
+ }
252
+
253
+ if(dropdown_item.length) {
254
+ select_dropdown_item(dropdown_item);
255
+ }
256
+ }
257
+ return false;
258
+ break;
259
+
260
+ case KEY.BACKSPACE:
261
+ previous_token = input_token.prev();
262
+
263
+ if(!$(this).val().length) {
264
+ if(selected_token) {
265
+ delete_token($(selected_token));
266
+ hidden_input.change();
267
+ } else if(previous_token.length) {
268
+ select_token($(previous_token.get(0)));
269
+ }
270
+
271
+ return false;
272
+ } else if($(this).val().length === 1) {
273
+ hide_dropdown();
274
+ } else {
275
+ // set a timeout just long enough to let this function finish.
276
+ setTimeout(function(){do_search();}, 5);
277
+ }
278
+ break;
279
+
280
+ case KEY.TAB:
281
+ case KEY.ENTER:
282
+ case KEY.NUMPAD_ENTER:
283
+ case KEY.COMMA:
284
+ if(selected_dropdown_item) {
285
+ add_token($(selected_dropdown_item).data("tokeninput"));
286
+ hidden_input.change();
287
+ return false;
288
+ }
289
+ break;
290
+
291
+ case KEY.ESCAPE:
292
+ hide_dropdown();
293
+ return true;
294
+
295
+ default:
296
+ if(String.fromCharCode(event.which)) {
297
+ // set a timeout just long enough to let this function finish.
298
+ setTimeout(function(){do_search();}, 5);
299
+ }
300
+ break;
301
+ }
302
+ });
303
+
304
+ // Keep a reference to the original input box
305
+ var hidden_input = $(input)
306
+ .hide()
307
+ .val("")
308
+ .focus(function () {
309
+ focus_with_timeout(input_box);
310
+ })
311
+ .blur(function () {
312
+ input_box.blur();
313
+ });
314
+
315
+ // Keep a reference to the selected token and dropdown item
316
+ var selected_token = null;
317
+ var selected_token_index = 0;
318
+ var selected_dropdown_item = null;
319
+
320
+ // The list to store the token items in
321
+ var token_list = $("<ul />")
322
+ .addClass(settings.classes.tokenList)
323
+ .click(function (event) {
324
+ var li = $(event.target).closest("li");
325
+ if(li && li.get(0) && $.data(li.get(0), "tokeninput")) {
326
+ toggle_select_token(li);
327
+ } else {
328
+ // Deselect selected token
329
+ if(selected_token) {
330
+ deselect_token($(selected_token), POSITION.END);
331
+ }
332
+
333
+ // Focus input box
334
+ focus_with_timeout(input_box);
335
+ }
336
+ })
337
+ .mouseover(function (event) {
338
+ var li = $(event.target).closest("li");
339
+ if(li && selected_token !== this) {
340
+ li.addClass(settings.classes.highlightedToken);
341
+ }
342
+ })
343
+ .mouseout(function (event) {
344
+ var li = $(event.target).closest("li");
345
+ if(li && selected_token !== this) {
346
+ li.removeClass(settings.classes.highlightedToken);
347
+ }
348
+ })
349
+ .insertBefore(hidden_input);
350
+
351
+ // The token holding the input box
352
+ var input_token = $("<li />")
353
+ .addClass(settings.classes.inputToken)
354
+ .appendTo(token_list)
355
+ .append(input_box);
356
+
357
+ // The list to store the dropdown items in
358
+ var dropdown = $("<div>")
359
+ .addClass(settings.classes.dropdown)
360
+ .appendTo("body")
361
+ .hide();
362
+
363
+ // Magic element to help us resize the text input
364
+ var input_resizer = $("<tester/>")
365
+ .insertAfter(input_box)
366
+ .css({
367
+ position: "absolute",
368
+ top: -9999,
369
+ left: -9999,
370
+ width: "auto",
371
+ fontSize: input_box.css("fontSize"),
372
+ fontFamily: input_box.css("fontFamily"),
373
+ fontWeight: input_box.css("fontWeight"),
374
+ letterSpacing: input_box.css("letterSpacing"),
375
+ whiteSpace: "nowrap"
376
+ });
377
+
378
+ // Pre-populate list if items exist
379
+ hidden_input.val("");
380
+ var li_data = settings.prePopulate || hidden_input.data("pre");
381
+ if(settings.processPrePopulate && $.isFunction(settings.onResult)) {
382
+ li_data = settings.onResult.call(hidden_input, li_data);
383
+ }
384
+ if(li_data && li_data.length) {
385
+ $.each(li_data, function (index, value) {
386
+ insert_token(value);
387
+ checkTokenLimit();
388
+ });
389
+ }
390
+
391
+ // Check if widget should initialize as disabled
392
+ if (settings.disabled) {
393
+ toggleDisabled(true);
394
+ }
395
+
396
+ // Initialization is done
397
+ if($.isFunction(settings.onReady)) {
398
+ settings.onReady.call();
399
+ }
400
+
401
+ //
402
+ // Public functions
403
+ //
404
+
405
+ this.clear = function() {
406
+ token_list.children("li").each(function() {
407
+ if ($(this).children("input").length === 0) {
408
+ delete_token($(this));
409
+ }
410
+ });
411
+ }
412
+
413
+ this.add = function(item) {
414
+ add_token(item);
415
+ }
416
+
417
+ this.remove = function(item) {
418
+ token_list.children("li").each(function() {
419
+ if ($(this).children("input").length === 0) {
420
+ var currToken = $(this).data("tokeninput");
421
+ var match = true;
422
+ for (var prop in item) {
423
+ if (item[prop] !== currToken[prop]) {
424
+ match = false;
425
+ break;
426
+ }
427
+ }
428
+ if (match) {
429
+ delete_token($(this));
430
+ }
431
+ }
432
+ });
433
+ }
434
+
435
+ this.getTokens = function() {
436
+ return saved_tokens;
437
+ }
438
+
439
+ this.toggleDisabled = function(disable) {
440
+ toggleDisabled(disable);
441
+ }
442
+
443
+ //
444
+ // Private functions
445
+ //
446
+
447
+ // Toggles the widget between enabled and disabled state, or according
448
+ // to the [disable] parameter.
449
+ function toggleDisabled(disable) {
450
+ if (typeof disable === 'boolean') {
451
+ settings.disabled = disable
452
+ } else {
453
+ settings.disabled = !settings.disabled;
454
+ }
455
+ input_box.prop('disabled', settings.disabled);
456
+ token_list.toggleClass(settings.classes.disabled, settings.disabled);
457
+ // if there is any token selected we deselect it
458
+ if(selected_token) {
459
+ deselect_token($(selected_token), POSITION.END);
460
+ }
461
+ hidden_input.prop('disabled', settings.disabled);
462
+ }
463
+
464
+ function checkTokenLimit() {
465
+ if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
466
+ input_box.hide();
467
+ hide_dropdown();
468
+ return;
469
+ }
470
+ }
471
+
472
+ function resize_input() {
473
+ if(input_val === (input_val = input_box.val())) {return;}
474
+
475
+ // Enter new content into resizer and resize input accordingly
476
+ var escaped = input_val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
477
+ input_resizer.html(escaped);
478
+ input_box.width(input_resizer.width() + 30);
479
+ }
480
+
481
+ function is_printable_character(keycode) {
482
+ return ((keycode >= 48 && keycode <= 90) || // 0-1a-z
483
+ (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * .
484
+ (keycode >= 186 && keycode <= 192) || // ; = , - . / ^
485
+ (keycode >= 219 && keycode <= 222)); // ( \ ) '
486
+ }
487
+
488
+ // Inner function to a token to the list
489
+ function insert_token(item) {
490
+ var $this_token = $(settings.tokenFormatter(item));
491
+ var readonly = item.readonly === true ? true : false;
492
+
493
+ if(readonly) $this_token.addClass(settings.classes.tokenReadOnly);
494
+
495
+ $this_token.addClass(settings.classes.token).insertBefore(input_token);
496
+
497
+ // The 'delete token' button
498
+ if(!readonly) {
499
+ $("<span>" + settings.deleteText + "</span>")
500
+ .addClass(settings.classes.tokenDelete)
501
+ .appendTo($this_token)
502
+ .click(function () {
503
+ if (!settings.disabled) {
504
+ delete_token($(this).parent());
505
+ hidden_input.change();
506
+ return false;
507
+ }
508
+ });
509
+ }
510
+
511
+ // Store data on the token
512
+ var token_data = item;
513
+ $.data($this_token.get(0), "tokeninput", item);
514
+
515
+ // Save this token for duplicate checking
516
+ saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index));
517
+ selected_token_index++;
518
+
519
+ // Update the hidden input
520
+ update_hidden_input(saved_tokens, hidden_input);
521
+
522
+ token_count += 1;
523
+
524
+ // Check the token limit
525
+ if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
526
+ input_box.hide();
527
+ hide_dropdown();
528
+ }
529
+
530
+ return $this_token;
531
+ }
532
+
533
+ // Add a token to the token list based on user input
534
+ function add_token (item) {
535
+ var callback = settings.onAdd;
536
+
537
+ // See if the token already exists and select it if we don't want duplicates
538
+ if(token_count > 0 && settings.preventDuplicates) {
539
+ var found_existing_token = null;
540
+ token_list.children().each(function () {
541
+ var existing_token = $(this);
542
+ var existing_data = $.data(existing_token.get(0), "tokeninput");
543
+ if(existing_data && existing_data.id === item.id) {
544
+ found_existing_token = existing_token;
545
+ return false;
546
+ }
547
+ });
548
+
549
+ if(found_existing_token) {
550
+ select_token(found_existing_token);
551
+ input_token.insertAfter(found_existing_token);
552
+ focus_with_timeout(input_box);
553
+ return;
554
+ }
555
+ }
556
+
557
+ // Insert the new tokens
558
+ if(settings.tokenLimit == null || token_count < settings.tokenLimit) {
559
+ insert_token(item);
560
+ checkTokenLimit();
561
+ }
562
+
563
+ // Clear input box
564
+ input_box.val("");
565
+
566
+ // Don't show the help dropdown, they've got the idea
567
+ hide_dropdown();
568
+
569
+ // Execute the onAdd callback if defined
570
+ if($.isFunction(callback)) {
571
+ callback.call(hidden_input,item);
572
+ }
573
+ }
574
+
575
+ // Select a token in the token list
576
+ function select_token (token) {
577
+ if (!settings.disabled) {
578
+ token.addClass(settings.classes.selectedToken);
579
+ selected_token = token.get(0);
580
+
581
+ // Hide input box
582
+ input_box.val("");
583
+
584
+ // Hide dropdown if it is visible (eg if we clicked to select token)
585
+ hide_dropdown();
586
+ }
587
+ }
588
+
589
+ // Deselect a token in the token list
590
+ function deselect_token (token, position) {
591
+ token.removeClass(settings.classes.selectedToken);
592
+ selected_token = null;
593
+
594
+ if(position === POSITION.BEFORE) {
595
+ input_token.insertBefore(token);
596
+ selected_token_index--;
597
+ } else if(position === POSITION.AFTER) {
598
+ input_token.insertAfter(token);
599
+ selected_token_index++;
600
+ } else {
601
+ input_token.appendTo(token_list);
602
+ selected_token_index = token_count;
603
+ }
604
+
605
+ // Show the input box and give it focus again
606
+ focus_with_timeout(input_box);
607
+ }
608
+
609
+ // Toggle selection of a token in the token list
610
+ function toggle_select_token(token) {
611
+ var previous_selected_token = selected_token;
612
+
613
+ if(selected_token) {
614
+ deselect_token($(selected_token), POSITION.END);
615
+ }
616
+
617
+ if(previous_selected_token === token.get(0)) {
618
+ deselect_token(token, POSITION.END);
619
+ } else {
620
+ select_token(token);
621
+ }
622
+ }
623
+
624
+ // Delete a token from the token list
625
+ function delete_token (token) {
626
+ // Remove the id from the saved list
627
+ var token_data = $.data(token.get(0), "tokeninput");
628
+ var callback = settings.onDelete;
629
+
630
+ var index = token.prevAll().length;
631
+ if(index > selected_token_index) index--;
632
+
633
+ // Delete the token
634
+ token.remove();
635
+ selected_token = null;
636
+
637
+ // Show the input box and give it focus again
638
+ focus_with_timeout(input_box);
639
+
640
+ // Remove this token from the saved list
641
+ saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1));
642
+ if(index < selected_token_index) selected_token_index--;
643
+
644
+ // Update the hidden input
645
+ update_hidden_input(saved_tokens, hidden_input);
646
+
647
+ token_count -= 1;
648
+
649
+ if(settings.tokenLimit !== null) {
650
+ input_box
651
+ .show()
652
+ .val("");
653
+ focus_with_timeout(input_box);
654
+ }
655
+
656
+ // Execute the onDelete callback if defined
657
+ if($.isFunction(callback)) {
658
+ callback.call(hidden_input,token_data);
659
+ }
660
+ }
661
+
662
+ // Update the hidden input box value
663
+ function update_hidden_input(saved_tokens, hidden_input) {
664
+ var token_values = $.map(saved_tokens, function (el) {
665
+ if(typeof settings.tokenValue == 'function')
666
+ return settings.tokenValue.call(this, el);
667
+
668
+ return el[settings.tokenValue];
669
+ });
670
+ hidden_input.val(token_values.join(settings.tokenDelimiter));
671
+
672
+ }
673
+
674
+ // Hide and clear the results dropdown
675
+ function hide_dropdown () {
676
+ dropdown.hide().empty();
677
+ selected_dropdown_item = null;
678
+ }
679
+
680
+ function show_dropdown() {
681
+ dropdown
682
+ .css({
683
+ position: "absolute",
684
+ top: $(token_list).offset().top + $(token_list).outerHeight(),
685
+ left: $(token_list).offset().left,
686
+ width: $(token_list).outerWidth(),
687
+ 'z-index': settings.zindex
688
+ })
689
+ .show();
690
+ }
691
+
692
+ function show_dropdown_searching () {
693
+ if(settings.searchingText) {
694
+ dropdown.html("<p>"+settings.searchingText+"</p>");
695
+ show_dropdown();
696
+ }
697
+ }
698
+
699
+ function show_dropdown_hint () {
700
+ if(settings.hintText) {
701
+ dropdown.html("<p>"+settings.hintText+"</p>");
702
+ show_dropdown();
703
+ }
704
+ }
705
+
706
+ var regexp_special_chars = new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\-]', 'g');
707
+ function regexp_escape(term) {
708
+ return term.replace(regexp_special_chars, '\\$&');
709
+ }
710
+
711
+ // Highlight the query part of the search term
712
+ function highlight_term(value, term) {
713
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(term) + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
714
+ }
715
+
716
+ function find_value_and_highlight_term(template, value, term) {
717
+ return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(value) + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term));
718
+ }
719
+
720
+ // Populate the results dropdown with some results
721
+ function populate_dropdown (query, results) {
722
+ if(results && results.length) {
723
+ dropdown.empty();
724
+ var dropdown_ul = $("<ul>")
725
+ .appendTo(dropdown)
726
+ .mouseover(function (event) {
727
+ select_dropdown_item($(event.target).closest("li"));
728
+ })
729
+ .mousedown(function (event) {
730
+ add_token($(event.target).closest("li").data("tokeninput"));
731
+ hidden_input.change();
732
+ return false;
733
+ })
734
+ .hide();
735
+
736
+ if (settings.resultsLimit && results.length > settings.resultsLimit) {
737
+ results = results.slice(0, settings.resultsLimit);
738
+ }
739
+
740
+ $.each(results, function(index, value) {
741
+ var this_li = settings.resultsFormatter(value);
742
+
743
+ this_li = find_value_and_highlight_term(this_li ,value[settings.propertyToSearch], query);
744
+
745
+ this_li = $(this_li).appendTo(dropdown_ul);
746
+
747
+ if(index % 2) {
748
+ this_li.addClass(settings.classes.dropdownItem);
749
+ } else {
750
+ this_li.addClass(settings.classes.dropdownItem2);
751
+ }
752
+
753
+ if(index === 0) {
754
+ select_dropdown_item(this_li);
755
+ }
756
+
757
+ $.data(this_li.get(0), "tokeninput", value);
758
+ });
759
+
760
+ show_dropdown();
761
+
762
+ if(settings.animateDropdown) {
763
+ dropdown_ul.slideDown("fast");
764
+ } else {
765
+ dropdown_ul.show();
766
+ }
767
+ } else {
768
+ if(settings.noResultsText) {
769
+ dropdown.html("<p>"+settings.noResultsText+"</p>");
770
+ show_dropdown();
771
+ }
772
+ }
773
+ }
774
+
775
+ // Highlight an item in the results dropdown
776
+ function select_dropdown_item (item) {
777
+ if(item) {
778
+ if(selected_dropdown_item) {
779
+ deselect_dropdown_item($(selected_dropdown_item));
780
+ }
781
+
782
+ item.addClass(settings.classes.selectedDropdownItem);
783
+ selected_dropdown_item = item.get(0);
784
+ }
785
+ }
786
+
787
+ // Remove highlighting from an item in the results dropdown
788
+ function deselect_dropdown_item (item) {
789
+ item.removeClass(settings.classes.selectedDropdownItem);
790
+ selected_dropdown_item = null;
791
+ }
792
+
793
+ // Do a search and show the "searching" dropdown if the input is longer
794
+ // than settings.minChars
795
+ function do_search() {
796
+ var query = input_box.val();
797
+
798
+ if(query && query.length) {
799
+ if(selected_token) {
800
+ deselect_token($(selected_token), POSITION.AFTER);
801
+ }
802
+
803
+ if(query.length >= settings.minChars) {
804
+ show_dropdown_searching();
805
+ clearTimeout(timeout);
806
+
807
+ timeout = setTimeout(function(){
808
+ run_search(query);
809
+ }, settings.searchDelay);
810
+ } else {
811
+ hide_dropdown();
812
+ }
813
+ }
814
+ }
815
+
816
+ // Do the actual search
817
+ function run_search(query) {
818
+ var cache_key = query + computeURL();
819
+ var cached_results = cache.get(cache_key);
820
+ if(cached_results) {
821
+ populate_dropdown(query, cached_results);
822
+ } else {
823
+ // Are we doing an ajax search or local data search?
824
+ if(settings.url) {
825
+ var url = computeURL();
826
+ // Extract exisiting get params
827
+ var ajax_params = {};
828
+ ajax_params.data = {};
829
+ if(url.indexOf("?") > -1) {
830
+ var parts = url.split("?");
831
+ ajax_params.url = parts[0];
832
+
833
+ var param_array = parts[1].split("&");
834
+ $.each(param_array, function (index, value) {
835
+ var kv = value.split("=");
836
+ ajax_params.data[kv[0]] = kv[1];
837
+ });
838
+ } else {
839
+ ajax_params.url = url;
840
+ }
841
+
842
+ // Prepare the request
843
+ ajax_params.data[settings.queryParam] = query;
844
+ ajax_params.type = settings.method;
845
+ ajax_params.dataType = settings.contentType;
846
+ if(settings.crossDomain) {
847
+ ajax_params.dataType = "jsonp";
848
+ }
849
+
850
+ // Attach the success callback
851
+ ajax_params.success = function(results) {
852
+ if($.isFunction(settings.onResult)) {
853
+ results = settings.onResult.call(hidden_input, results);
854
+ }
855
+ cache.add(cache_key, settings.jsonContainer ? results[settings.jsonContainer] : results);
856
+
857
+ // only populate the dropdown if the results are associated with the active search query
858
+ if(input_box.val() === query) {
859
+ populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
860
+ }
861
+ };
862
+
863
+ // Make the request
864
+ $.ajax(ajax_params);
865
+ } else if(settings.local_data) {
866
+ // Do the search through local data
867
+ var results = $.grep(settings.local_data, function (row) {
868
+ return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1;
869
+ });
870
+
871
+ if($.isFunction(settings.onResult)) {
872
+ results = settings.onResult.call(hidden_input, results);
873
+ }
874
+ cache.add(cache_key, results);
875
+ populate_dropdown(query, results);
876
+ }
877
+ }
878
+ }
879
+
880
+ // compute the dynamic URL
881
+ function computeURL() {
882
+ var url = settings.url;
883
+ if(typeof settings.url == 'function') {
884
+ url = settings.url.call(settings);
885
+ }
886
+ return url;
887
+ }
888
+
889
+ // Bring browser focus to the specified object.
890
+ // Use of setTimeout is to get around an IE bug.
891
+ // (See, e.g., http://stackoverflow.com/questions/2600186/focus-doesnt-work-in-ie)
892
+ //
893
+ // obj: a jQuery object to focus()
894
+ function focus_with_timeout(obj) {
895
+ setTimeout(function() { obj.focus(); }, 50);
896
+ }
897
+
898
+ };
899
+
900
+ // Really basic cache for the results
901
+ $.TokenList.Cache = function (options) {
902
+ var settings = $.extend({
903
+ max_size: 500
904
+ }, options);
905
+
906
+ var data = {};
907
+ var size = 0;
908
+
909
+ var flush = function () {
910
+ data = {};
911
+ size = 0;
912
+ };
913
+
914
+ this.add = function (query, results) {
915
+ if(size > settings.max_size) {
916
+ flush();
917
+ }
918
+
919
+ if(!data[query]) {
920
+ size += 1;
921
+ }
922
+
923
+ data[query] = results;
924
+ };
925
+
926
+ this.get = function (query) {
927
+ return data[query];
928
+ };
929
+ };
930
+ }(jQuery));