rails-jquery-tokeninput 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NTY5YmI4ZjRmMDUzZGVmNzQ5ZjIyOTAwY2IxOWIxNDQ1MzY5ZjQzZg==
5
+ data.tar.gz: !binary |-
6
+ NmI3MTk1YzA1N2MwYmMzZDdiMWU1Yzg2YmRiZThmMGMxMjZjZTVkMw==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZjdlNjBlYjA3YzZlMzg3MGZiYTYzYWVlNGEyODQ2NDQ5ODJjNGYwZjNjNTE3
10
+ OGViY2Y5OTAyMzBmNWEyOWM1MDQzYmEyMTM5MjBjZDkyOWUxMjkzMWQ4Mjdm
11
+ OGI2MzRhNDQ3ZTcwODgxZjBiYmQwZjc2MjIyYWYwMDE0YTYwZjA=
12
+ data.tar.gz: !binary |-
13
+ MjUzZjIzZGI1NTY3YWQ3ODBmNjc1ZDRmYzNjOTVjNzZmMWYwN2ZiMmM1ZWIx
14
+ OGVhOTdlYTljYmU4NmZmYTllZmY0Y2RiZTk2NGIxY2I2ZGM4ZGRhNzdhNGEy
15
+ NTk5NTgxMzlmY2Y2ODYxZjMxMjdjMzQ5OGM5M2FhMGYxMDY1ZDc=
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rails-jquery-tokeninput.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Evgeny Li
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.
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # rails-jquery-tokeninput
2
+
3
+ Integrating [Tokeninput](http://loopj.com/jquery-tokeninput/) jQuery plugin version 1.6.1.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'rails-jquery-tokeninput'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Require jquery.tokeninput.js in your application.js
18
+
19
+ ```js
20
+ //= require jquery
21
+ //= require rails-jquery-tokeninput
22
+ ```
23
+
24
+ Add the stylesheets you want to use in your application.css
25
+
26
+ ```css
27
+ *= require_self
28
+ *= require token-input-facebook
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ To start using **rails-jquery-tokeninput** you just have to add attributes to your text input:
34
+
35
+ ```ruby
36
+ names = [{ id: 1, name: 'John' }, { id: 2, name: 'Mike' }]
37
+ options = { options: { theme: 'facebook' } }
38
+
39
+ f.text_field :names, class: 'tokeninput', data: { tokeninput: {
40
+ collection: names,
41
+ options: options
42
+ }}
43
+ ```
44
+
45
+ If you are using Simple Form or Formtastic it may looks like:
46
+
47
+ ```ruby
48
+ countries = [{ name: 'Hong Kong', iso_code: 'HK' }, { name: 'Jamaica', iso_code: 'JM' }]
49
+ options = { theme: 'facebook', tokenValue: 'iso_code' }
50
+
51
+ f.input :countries, as: :string, input_html: { class: 'tokeninput', data: {
52
+ tokeninput: {
53
+ collection: countries,
54
+ options: options
55
+ }
56
+ }}
57
+ ```
58
+
59
+ ## Contributing
60
+
61
+ 1. Fork it
62
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
63
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
64
+ 4. Push to the branch (`git push origin my-new-feature`)
65
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,1061 @@
1
+ /*
2
+ * jQuery Plugin: Tokenizing Autocomplete Text Entry
3
+ * Version 1.6.1
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
+ placeholder: null,
34
+ theme: null,
35
+ zindex: 999,
36
+ resultsLimit: null,
37
+
38
+ enableHTML: false,
39
+
40
+ resultsFormatter: function(item) {
41
+ var string = item[this.propertyToSearch];
42
+ return "<li>" + (this.enableHTML ? string : _escapeHTML(string)) + "</li>";
43
+ },
44
+
45
+ tokenFormatter: function(item) {
46
+ var string = item[this.propertyToSearch];
47
+ return "<li><p>" + (this.enableHTML ? string : _escapeHTML(string)) + "</p></li>";
48
+ },
49
+
50
+ // Tokenization settings
51
+ tokenLimit: null,
52
+ tokenDelimiter: ",",
53
+ preventDuplicates: false,
54
+ tokenValue: "id",
55
+
56
+ // Behavioral settings
57
+ allowFreeTagging: false,
58
+ allowTabOut: false,
59
+
60
+ // Callbacks
61
+ onResult: null,
62
+ onCachedResult: null,
63
+ onAdd: null,
64
+ onFreeTaggingAdd: null,
65
+ onDelete: null,
66
+ onReady: null,
67
+
68
+ // Other settings
69
+ idPrefix: "token-input-",
70
+
71
+ // Keep track if the input is currently in disabled mode
72
+ disabled: false
73
+ };
74
+
75
+ // Default classes to use when theming
76
+ var DEFAULT_CLASSES = {
77
+ tokenList: "token-input-list",
78
+ token: "token-input-token",
79
+ tokenReadOnly: "token-input-token-readonly",
80
+ tokenDelete: "token-input-delete-token",
81
+ selectedToken: "token-input-selected-token",
82
+ highlightedToken: "token-input-highlighted-token",
83
+ dropdown: "token-input-dropdown",
84
+ dropdownItem: "token-input-dropdown-item",
85
+ dropdownItem2: "token-input-dropdown-item2",
86
+ selectedDropdownItem: "token-input-selected-dropdown-item",
87
+ inputToken: "token-input-input-token",
88
+ focused: "token-input-focused",
89
+ disabled: "token-input-disabled"
90
+ };
91
+
92
+ // Input box position "enum"
93
+ var POSITION = {
94
+ BEFORE: 0,
95
+ AFTER: 1,
96
+ END: 2
97
+ };
98
+
99
+ // Keys "enum"
100
+ var KEY = {
101
+ BACKSPACE: 8,
102
+ TAB: 9,
103
+ ENTER: 13,
104
+ ESCAPE: 27,
105
+ SPACE: 32,
106
+ PAGE_UP: 33,
107
+ PAGE_DOWN: 34,
108
+ END: 35,
109
+ HOME: 36,
110
+ LEFT: 37,
111
+ UP: 38,
112
+ RIGHT: 39,
113
+ DOWN: 40,
114
+ NUMPAD_ENTER: 108,
115
+ COMMA: 188
116
+ };
117
+
118
+ var HTML_ESCAPES = {
119
+ '&': '&amp;',
120
+ '<': '&lt;',
121
+ '>': '&gt;',
122
+ '"': '&quot;',
123
+ "'": '&#x27;',
124
+ '/': '&#x2F;'
125
+ };
126
+
127
+ var HTML_ESCAPE_CHARS = /[&<>"'\/]/g;
128
+
129
+ function coerceToString(val) {
130
+ return String((val === null || val === undefined) ? '' : val);
131
+ }
132
+
133
+ function _escapeHTML(text) {
134
+ return coerceToString(text).replace(HTML_ESCAPE_CHARS, function(match) {
135
+ return HTML_ESCAPES[match];
136
+ });
137
+ }
138
+
139
+ // Additional public (exposed) methods
140
+ var methods = {
141
+ init: function(url_or_data_or_function, options) {
142
+ var settings = $.extend({}, DEFAULT_SETTINGS, options || {});
143
+
144
+ return this.each(function () {
145
+ $(this).data("settings", settings);
146
+ $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings));
147
+ });
148
+ },
149
+ clear: function() {
150
+ this.data("tokenInputObject").clear();
151
+ return this;
152
+ },
153
+ add: function(item) {
154
+ this.data("tokenInputObject").add(item);
155
+ return this;
156
+ },
157
+ remove: function(item) {
158
+ this.data("tokenInputObject").remove(item);
159
+ return this;
160
+ },
161
+ get: function() {
162
+ return this.data("tokenInputObject").getTokens();
163
+ },
164
+ toggleDisabled: function(disable) {
165
+ this.data("tokenInputObject").toggleDisabled(disable);
166
+ return this;
167
+ },
168
+ setOptions: function(options){
169
+ $(this).data("settings", $.extend({}, $(this).data("settings"), options || {}));
170
+ return this;
171
+ },
172
+ destroy: function () {
173
+ if(this.data("tokenInputObject")){
174
+ this.data("tokenInputObject").clear();
175
+ var tmpInput = this;
176
+ var closest = this.parent();
177
+ closest.empty();
178
+ tmpInput.show();
179
+ closest.append(tmpInput);
180
+ return tmpInput;
181
+ }
182
+ }
183
+ };
184
+
185
+ // Expose the .tokenInput function to jQuery as a plugin
186
+ $.fn.tokenInput = function (method) {
187
+ // Method calling and initialization logic
188
+ if(methods[method]) {
189
+ return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
190
+ } else {
191
+ return methods.init.apply(this, arguments);
192
+ }
193
+ };
194
+
195
+ // TokenList class for each input
196
+ $.TokenList = function (input, url_or_data, settings) {
197
+ //
198
+ // Initialization
199
+ //
200
+
201
+ // Configure the data source
202
+ if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") {
203
+ // Set the url to query against
204
+ $(input).data("settings").url = url_or_data;
205
+
206
+ // If the URL is a function, evaluate it here to do our initalization work
207
+ var url = computeURL();
208
+
209
+ // Make a smart guess about cross-domain if it wasn't explicitly specified
210
+ if($(input).data("settings").crossDomain === undefined && typeof url === "string") {
211
+ if(url.indexOf("://") === -1) {
212
+ $(input).data("settings").crossDomain = false;
213
+ } else {
214
+ $(input).data("settings").crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]);
215
+ }
216
+ }
217
+ } else if(typeof(url_or_data) === "object") {
218
+ // Set the local data to search through
219
+ $(input).data("settings").local_data = url_or_data;
220
+ }
221
+
222
+ // Build class names
223
+ if($(input).data("settings").classes) {
224
+ // Use custom class names
225
+ $(input).data("settings").classes = $.extend({}, DEFAULT_CLASSES, $(input).data("settings").classes);
226
+ } else if($(input).data("settings").theme) {
227
+ // Use theme-suffixed default class names
228
+ $(input).data("settings").classes = {};
229
+ $.each(DEFAULT_CLASSES, function(key, value) {
230
+ $(input).data("settings").classes[key] = value + "-" + $(input).data("settings").theme;
231
+ });
232
+ } else {
233
+ $(input).data("settings").classes = DEFAULT_CLASSES;
234
+ }
235
+
236
+
237
+ // Save the tokens
238
+ var saved_tokens = [];
239
+
240
+ // Keep track of the number of tokens in the list
241
+ var token_count = 0;
242
+
243
+ // Basic cache to save on db hits
244
+ var cache = new $.TokenList.Cache();
245
+
246
+ // Keep track of the timeout, old vals
247
+ var timeout;
248
+ var input_val;
249
+
250
+ // Create a new text input an attach keyup events
251
+ var input_box = $("<input type=\"text\" autocomplete=\"off\" autocapitalize=\"off\">")
252
+ .css({
253
+ outline: "none"
254
+ })
255
+ .attr("id", $(input).data("settings").idPrefix + input.id)
256
+ .focus(function () {
257
+ if ($(input).data("settings").disabled) {
258
+ return false;
259
+ } else
260
+ if ($(input).data("settings").tokenLimit === null || $(input).data("settings").tokenLimit !== token_count) {
261
+ show_dropdown_hint();
262
+ }
263
+ token_list.addClass($(input).data("settings").classes.focused);
264
+ })
265
+ .blur(function () {
266
+ hide_dropdown();
267
+
268
+ if ($(input).data("settings").allowFreeTagging) {
269
+ add_freetagging_tokens();
270
+ }
271
+
272
+ $(this).val("");
273
+ token_list.removeClass($(input).data("settings").classes.focused);
274
+ })
275
+ .bind("keyup keydown blur update", resize_input)
276
+ .keydown(function (event) {
277
+ var previous_token;
278
+ var next_token;
279
+
280
+ switch(event.keyCode) {
281
+ case KEY.LEFT:
282
+ case KEY.RIGHT:
283
+ case KEY.UP:
284
+ case KEY.DOWN:
285
+ if(!$(this).val()) {
286
+ previous_token = input_token.prev();
287
+ next_token = input_token.next();
288
+
289
+ if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
290
+ // Check if there is a previous/next token and it is selected
291
+ if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) {
292
+ deselect_token($(selected_token), POSITION.BEFORE);
293
+ } else {
294
+ deselect_token($(selected_token), POSITION.AFTER);
295
+ }
296
+ } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) {
297
+ // We are moving left, select the previous token if it exists
298
+ select_token($(previous_token.get(0)));
299
+ } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) {
300
+ // We are moving right, select the next token if it exists
301
+ select_token($(next_token.get(0)));
302
+ }
303
+ } else {
304
+ var dropdown_item = null;
305
+
306
+ if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) {
307
+ dropdown_item = $(selected_dropdown_item).next();
308
+ } else {
309
+ dropdown_item = $(selected_dropdown_item).prev();
310
+ }
311
+
312
+ if(dropdown_item.length) {
313
+ select_dropdown_item(dropdown_item);
314
+ }
315
+ }
316
+ return false;
317
+ break;
318
+
319
+ case KEY.BACKSPACE:
320
+ previous_token = input_token.prev();
321
+
322
+ if(!$(this).val().length) {
323
+ if(selected_token) {
324
+ delete_token($(selected_token));
325
+ hidden_input.change();
326
+ } else if(previous_token.length) {
327
+ select_token($(previous_token.get(0)));
328
+ }
329
+
330
+ return false;
331
+ } else if($(this).val().length === 1) {
332
+ hide_dropdown();
333
+ } else {
334
+ // set a timeout just long enough to let this function finish.
335
+ setTimeout(function(){do_search();}, 5);
336
+ }
337
+ break;
338
+
339
+ case KEY.TAB:
340
+ case KEY.ENTER:
341
+ case KEY.NUMPAD_ENTER:
342
+ case KEY.COMMA:
343
+ if(selected_dropdown_item) {
344
+ add_token($(selected_dropdown_item).data("tokeninput"));
345
+ hidden_input.change();
346
+ } else {
347
+ if ($(input).data("settings").allowFreeTagging) {
348
+ if($(input).data("settings").allowTabOut && $(this).val() === "") {
349
+ return true;
350
+ } else {
351
+ add_freetagging_tokens();
352
+ }
353
+ } else {
354
+ $(this).val("");
355
+ if($(input).data("settings").allowTabOut) {
356
+ return true;
357
+ }
358
+ }
359
+ event.stopPropagation();
360
+ event.preventDefault();
361
+ }
362
+ return false;
363
+
364
+ case KEY.ESCAPE:
365
+ hide_dropdown();
366
+ return true;
367
+
368
+ default:
369
+ if(String.fromCharCode(event.which)) {
370
+ // set a timeout just long enough to let this function finish.
371
+ setTimeout(function(){do_search();}, 5);
372
+ }
373
+ break;
374
+ }
375
+ });
376
+
377
+ // Keep reference for placeholder
378
+ if (settings.placeholder)
379
+ input_box.attr("placeholder", settings.placeholder)
380
+
381
+ // Keep a reference to the original input box
382
+ var hidden_input = $(input)
383
+ .hide()
384
+ .val("")
385
+ .focus(function () {
386
+ focus_with_timeout(input_box);
387
+ })
388
+ .blur(function () {
389
+ input_box.blur();
390
+ //return the object to this can be referenced in the callback functions.
391
+ return hidden_input;
392
+ });
393
+
394
+ // Keep a reference to the selected token and dropdown item
395
+ var selected_token = null;
396
+ var selected_token_index = 0;
397
+ var selected_dropdown_item = null;
398
+
399
+ // The list to store the token items in
400
+ var token_list = $("<ul />")
401
+ .addClass($(input).data("settings").classes.tokenList)
402
+ .click(function (event) {
403
+ var li = $(event.target).closest("li");
404
+ if(li && li.get(0) && $.data(li.get(0), "tokeninput")) {
405
+ toggle_select_token(li);
406
+ } else {
407
+ // Deselect selected token
408
+ if(selected_token) {
409
+ deselect_token($(selected_token), POSITION.END);
410
+ }
411
+
412
+ // Focus input box
413
+ focus_with_timeout(input_box);
414
+ }
415
+ })
416
+ .mouseover(function (event) {
417
+ var li = $(event.target).closest("li");
418
+ if(li && selected_token !== this) {
419
+ li.addClass($(input).data("settings").classes.highlightedToken);
420
+ }
421
+ })
422
+ .mouseout(function (event) {
423
+ var li = $(event.target).closest("li");
424
+ if(li && selected_token !== this) {
425
+ li.removeClass($(input).data("settings").classes.highlightedToken);
426
+ }
427
+ })
428
+ .insertBefore(hidden_input);
429
+
430
+ // The token holding the input box
431
+ var input_token = $("<li />")
432
+ .addClass($(input).data("settings").classes.inputToken)
433
+ .appendTo(token_list)
434
+ .append(input_box);
435
+
436
+ // The list to store the dropdown items in
437
+ var dropdown = $("<div>")
438
+ .addClass($(input).data("settings").classes.dropdown)
439
+ .appendTo("body")
440
+ .hide();
441
+
442
+ // Magic element to help us resize the text input
443
+ var input_resizer = $("<tester/>")
444
+ .insertAfter(input_box)
445
+ .css({
446
+ position: "absolute",
447
+ top: -9999,
448
+ left: -9999,
449
+ width: "auto",
450
+ fontSize: input_box.css("fontSize"),
451
+ fontFamily: input_box.css("fontFamily"),
452
+ fontWeight: input_box.css("fontWeight"),
453
+ letterSpacing: input_box.css("letterSpacing"),
454
+ whiteSpace: "nowrap"
455
+ });
456
+
457
+ // Pre-populate list if items exist
458
+ hidden_input.val("");
459
+ var li_data = $(input).data("settings").prePopulate || hidden_input.data("pre");
460
+ if($(input).data("settings").processPrePopulate && $.isFunction($(input).data("settings").onResult)) {
461
+ li_data = $(input).data("settings").onResult.call(hidden_input, li_data);
462
+ }
463
+ if(li_data && li_data.length) {
464
+ $.each(li_data, function (index, value) {
465
+ insert_token(value);
466
+ checkTokenLimit();
467
+ input_box.attr("placeholder", null)
468
+ });
469
+ }
470
+
471
+ // Check if widget should initialize as disabled
472
+ if ($(input).data("settings").disabled) {
473
+ toggleDisabled(true);
474
+ }
475
+
476
+ // Initialization is done
477
+ if($.isFunction($(input).data("settings").onReady)) {
478
+ $(input).data("settings").onReady.call();
479
+ }
480
+
481
+ //
482
+ // Public functions
483
+ //
484
+
485
+ this.clear = function() {
486
+ token_list.children("li").each(function() {
487
+ if ($(this).children("input").length === 0) {
488
+ delete_token($(this));
489
+ }
490
+ });
491
+ };
492
+
493
+ this.add = function(item) {
494
+ add_token(item);
495
+ };
496
+
497
+ this.remove = function(item) {
498
+ token_list.children("li").each(function() {
499
+ if ($(this).children("input").length === 0) {
500
+ var currToken = $(this).data("tokeninput");
501
+ var match = true;
502
+ for (var prop in item) {
503
+ if (item[prop] !== currToken[prop]) {
504
+ match = false;
505
+ break;
506
+ }
507
+ }
508
+ if (match) {
509
+ delete_token($(this));
510
+ }
511
+ }
512
+ });
513
+ };
514
+
515
+ this.getTokens = function() {
516
+ return saved_tokens;
517
+ };
518
+
519
+ this.toggleDisabled = function(disable) {
520
+ toggleDisabled(disable);
521
+ };
522
+
523
+ // Resize input to maximum width so the placeholder can be seen
524
+ resize_input();
525
+
526
+ //
527
+ // Private functions
528
+ //
529
+
530
+ function escapeHTML(text) {
531
+ return $(input).data("settings").enableHTML ? text : _escapeHTML(text);
532
+ }
533
+
534
+ // Toggles the widget between enabled and disabled state, or according
535
+ // to the [disable] parameter.
536
+ function toggleDisabled(disable) {
537
+ if (typeof disable === 'boolean') {
538
+ $(input).data("settings").disabled = disable
539
+ } else {
540
+ $(input).data("settings").disabled = !$(input).data("settings").disabled;
541
+ }
542
+ input_box.attr('disabled', $(input).data("settings").disabled);
543
+ token_list.toggleClass($(input).data("settings").classes.disabled, $(input).data("settings").disabled);
544
+ // if there is any token selected we deselect it
545
+ if(selected_token) {
546
+ deselect_token($(selected_token), POSITION.END);
547
+ }
548
+ hidden_input.attr('disabled', $(input).data("settings").disabled);
549
+ }
550
+
551
+ function checkTokenLimit() {
552
+ if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) {
553
+ input_box.hide();
554
+ hide_dropdown();
555
+ return;
556
+ }
557
+ }
558
+
559
+ function resize_input() {
560
+ if(input_val === (input_val = input_box.val())) {return;}
561
+
562
+ // Get width left on the current line
563
+ var width_left = token_list.width() - input_box.offset().left - token_list.offset().left;
564
+ // Enter new content into resizer and resize input accordingly
565
+ input_resizer.html(_escapeHTML(input_val) || _escapeHTML(settings.placeholder));
566
+ // Get maximum width, minimum the size of input and maximum the widget's width
567
+ input_box.width(Math.min(token_list.width(),
568
+ Math.max(width_left, input_resizer.width() + 30)));
569
+ }
570
+
571
+ function is_printable_character(keycode) {
572
+ return ((keycode >= 48 && keycode <= 90) || // 0-1a-z
573
+ (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * .
574
+ (keycode >= 186 && keycode <= 192) || // ; = , - . / ^
575
+ (keycode >= 219 && keycode <= 222)); // ( \ ) '
576
+ }
577
+
578
+ function add_freetagging_tokens() {
579
+ var value = $.trim(input_box.val());
580
+ var tokens = value.split($(input).data("settings").tokenDelimiter);
581
+ $.each(tokens, function(i, token) {
582
+ if (!token) {
583
+ return;
584
+ }
585
+
586
+ if ($.isFunction($(input).data("settings").onFreeTaggingAdd)) {
587
+ token = $(input).data("settings").onFreeTaggingAdd.call(hidden_input, token);
588
+ }
589
+ var object = {};
590
+ object[$(input).data("settings").tokenValue] = object[$(input).data("settings").propertyToSearch] = token;
591
+ add_token(object);
592
+ });
593
+ }
594
+
595
+ // Inner function to a token to the list
596
+ function insert_token(item) {
597
+ var $this_token = $($(input).data("settings").tokenFormatter(item));
598
+ var readonly = item.readonly === true ? true : false;
599
+
600
+ if(readonly) $this_token.addClass($(input).data("settings").classes.tokenReadOnly);
601
+
602
+ $this_token.addClass($(input).data("settings").classes.token).insertBefore(input_token);
603
+
604
+ // The 'delete token' button
605
+ if(!readonly) {
606
+ $("<span>" + $(input).data("settings").deleteText + "</span>")
607
+ .addClass($(input).data("settings").classes.tokenDelete)
608
+ .appendTo($this_token)
609
+ .click(function () {
610
+ if (!$(input).data("settings").disabled) {
611
+ delete_token($(this).parent());
612
+ hidden_input.change();
613
+ return false;
614
+ }
615
+ });
616
+ }
617
+
618
+ // Store data on the token
619
+ var token_data = item;
620
+ $.data($this_token.get(0), "tokeninput", item);
621
+
622
+ // Save this token for duplicate checking
623
+ saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index));
624
+ selected_token_index++;
625
+
626
+ // Update the hidden input
627
+ update_hidden_input(saved_tokens, hidden_input);
628
+
629
+ token_count += 1;
630
+
631
+ // Check the token limit
632
+ if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) {
633
+ input_box.hide();
634
+ hide_dropdown();
635
+ }
636
+
637
+ return $this_token;
638
+ }
639
+
640
+ // Add a token to the token list based on user input
641
+ function add_token (item) {
642
+ var callback = $(input).data("settings").onAdd;
643
+
644
+ // See if the token already exists and select it if we don't want duplicates
645
+ if(token_count > 0 && $(input).data("settings").preventDuplicates) {
646
+ var found_existing_token = null;
647
+ token_list.children().each(function () {
648
+ var existing_token = $(this);
649
+ var existing_data = $.data(existing_token.get(0), "tokeninput");
650
+ if(existing_data && existing_data[settings.tokenValue] === item[settings.tokenValue]) {
651
+ found_existing_token = existing_token;
652
+ return false;
653
+ }
654
+ });
655
+
656
+ if(found_existing_token) {
657
+ select_token(found_existing_token);
658
+ input_token.insertAfter(found_existing_token);
659
+ focus_with_timeout(input_box);
660
+ return;
661
+ }
662
+ }
663
+
664
+ // Squeeze input_box so we force no unnecessary line break
665
+ input_box.width(0);
666
+
667
+ // Insert the new tokens
668
+ if($(input).data("settings").tokenLimit == null || token_count < $(input).data("settings").tokenLimit) {
669
+ insert_token(item);
670
+ // Remove the placeholder so it's not seen after you've added a token
671
+ input_box.attr("placeholder", null)
672
+ checkTokenLimit();
673
+ }
674
+
675
+ // Clear input box
676
+ input_box.val("");
677
+
678
+ // Don't show the help dropdown, they've got the idea
679
+ hide_dropdown();
680
+
681
+ // Execute the onAdd callback if defined
682
+ if($.isFunction(callback)) {
683
+ callback.call(hidden_input,item);
684
+ }
685
+ }
686
+
687
+ // Select a token in the token list
688
+ function select_token (token) {
689
+ if (!$(input).data("settings").disabled) {
690
+ token.addClass($(input).data("settings").classes.selectedToken);
691
+ selected_token = token.get(0);
692
+
693
+ // Hide input box
694
+ input_box.val("");
695
+
696
+ // Hide dropdown if it is visible (eg if we clicked to select token)
697
+ hide_dropdown();
698
+ }
699
+ }
700
+
701
+ // Deselect a token in the token list
702
+ function deselect_token (token, position) {
703
+ token.removeClass($(input).data("settings").classes.selectedToken);
704
+ selected_token = null;
705
+
706
+ if(position === POSITION.BEFORE) {
707
+ input_token.insertBefore(token);
708
+ selected_token_index--;
709
+ } else if(position === POSITION.AFTER) {
710
+ input_token.insertAfter(token);
711
+ selected_token_index++;
712
+ } else {
713
+ input_token.appendTo(token_list);
714
+ selected_token_index = token_count;
715
+ }
716
+
717
+ // Show the input box and give it focus again
718
+ focus_with_timeout(input_box);
719
+ }
720
+
721
+ // Toggle selection of a token in the token list
722
+ function toggle_select_token(token) {
723
+ var previous_selected_token = selected_token;
724
+
725
+ if(selected_token) {
726
+ deselect_token($(selected_token), POSITION.END);
727
+ }
728
+
729
+ if(previous_selected_token === token.get(0)) {
730
+ deselect_token(token, POSITION.END);
731
+ } else {
732
+ select_token(token);
733
+ }
734
+ }
735
+
736
+ // Delete a token from the token list
737
+ function delete_token (token) {
738
+ // Remove the id from the saved list
739
+ var token_data = $.data(token.get(0), "tokeninput");
740
+ var callback = $(input).data("settings").onDelete;
741
+
742
+ var index = token.prevAll().length;
743
+ if(index > selected_token_index) index--;
744
+
745
+ // Delete the token
746
+ token.remove();
747
+ selected_token = null;
748
+
749
+ // Show the input box and give it focus again
750
+ focus_with_timeout(input_box);
751
+
752
+ // Remove this token from the saved list
753
+ saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1));
754
+ if (saved_tokens.length == 0) {
755
+ input_box.attr("placeholder", settings.placeholder)
756
+ }
757
+ if(index < selected_token_index) selected_token_index--;
758
+
759
+ // Update the hidden input
760
+ update_hidden_input(saved_tokens, hidden_input);
761
+
762
+ token_count -= 1;
763
+
764
+ if($(input).data("settings").tokenLimit !== null) {
765
+ input_box
766
+ .show()
767
+ .val("");
768
+ focus_with_timeout(input_box);
769
+ }
770
+
771
+ // Execute the onDelete callback if defined
772
+ if($.isFunction(callback)) {
773
+ callback.call(hidden_input,token_data);
774
+ }
775
+ }
776
+
777
+ // Update the hidden input box value
778
+ function update_hidden_input(saved_tokens, hidden_input) {
779
+ var token_values = $.map(saved_tokens, function (el) {
780
+ if(typeof $(input).data("settings").tokenValue == 'function')
781
+ return $(input).data("settings").tokenValue.call(this, el);
782
+
783
+ return el[$(input).data("settings").tokenValue];
784
+ });
785
+ hidden_input.val(token_values.join($(input).data("settings").tokenDelimiter));
786
+
787
+ }
788
+
789
+ // Hide and clear the results dropdown
790
+ function hide_dropdown () {
791
+ dropdown.hide().empty();
792
+ selected_dropdown_item = null;
793
+ }
794
+
795
+ function show_dropdown() {
796
+ dropdown
797
+ .css({
798
+ position: "absolute",
799
+ top: token_list.offset().top + token_list.outerHeight(),
800
+ left: token_list.offset().left,
801
+ width: token_list.width(),
802
+ 'z-index': $(input).data("settings").zindex
803
+ })
804
+ .show();
805
+ }
806
+
807
+ function show_dropdown_searching () {
808
+ if($(input).data("settings").searchingText) {
809
+ dropdown.html("<p>" + escapeHTML($(input).data("settings").searchingText) + "</p>");
810
+ show_dropdown();
811
+ }
812
+ }
813
+
814
+ function show_dropdown_hint () {
815
+ if($(input).data("settings").hintText) {
816
+ dropdown.html("<p>" + escapeHTML($(input).data("settings").hintText) + "</p>");
817
+ show_dropdown();
818
+ }
819
+ }
820
+
821
+ var regexp_special_chars = new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\-]', 'g');
822
+ function regexp_escape(term) {
823
+ return term.replace(regexp_special_chars, '\\$&');
824
+ }
825
+
826
+ // Highlight the query part of the search term
827
+ function highlight_term(value, term) {
828
+ return value.replace(
829
+ new RegExp(
830
+ "(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(term) + ")(?![^<>]*>)(?![^&;]+;)",
831
+ "gi"
832
+ ), function(match, p1) {
833
+ return "<b>" + escapeHTML(p1) + "</b>";
834
+ }
835
+ );
836
+ }
837
+
838
+ function find_value_and_highlight_term(template, value, term) {
839
+ return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(value) + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term));
840
+ }
841
+
842
+ // Populate the results dropdown with some results
843
+ function populate_dropdown (query, results) {
844
+ if(results && results.length) {
845
+ dropdown.empty();
846
+ var dropdown_ul = $("<ul>")
847
+ .appendTo(dropdown)
848
+ .mouseover(function (event) {
849
+ select_dropdown_item($(event.target).closest("li"));
850
+ })
851
+ .mousedown(function (event) {
852
+ add_token($(event.target).closest("li").data("tokeninput"));
853
+ hidden_input.change();
854
+ return false;
855
+ })
856
+ .hide();
857
+
858
+ if ($(input).data("settings").resultsLimit && results.length > $(input).data("settings").resultsLimit) {
859
+ results = results.slice(0, $(input).data("settings").resultsLimit);
860
+ }
861
+
862
+ $.each(results, function(index, value) {
863
+ var this_li = $(input).data("settings").resultsFormatter(value);
864
+
865
+ this_li = find_value_and_highlight_term(this_li ,value[$(input).data("settings").propertyToSearch], query);
866
+
867
+ this_li = $(this_li).appendTo(dropdown_ul);
868
+
869
+ if(index % 2) {
870
+ this_li.addClass($(input).data("settings").classes.dropdownItem);
871
+ } else {
872
+ this_li.addClass($(input).data("settings").classes.dropdownItem2);
873
+ }
874
+
875
+ if(index === 0) {
876
+ select_dropdown_item(this_li);
877
+ }
878
+
879
+ $.data(this_li.get(0), "tokeninput", value);
880
+ });
881
+
882
+ show_dropdown();
883
+
884
+ if($(input).data("settings").animateDropdown) {
885
+ dropdown_ul.slideDown("fast");
886
+ } else {
887
+ dropdown_ul.show();
888
+ }
889
+ } else {
890
+ if($(input).data("settings").noResultsText) {
891
+ dropdown.html("<p>" + escapeHTML($(input).data("settings").noResultsText) + "</p>");
892
+ show_dropdown();
893
+ }
894
+ }
895
+ }
896
+
897
+ // Highlight an item in the results dropdown
898
+ function select_dropdown_item (item) {
899
+ if(item) {
900
+ if(selected_dropdown_item) {
901
+ deselect_dropdown_item($(selected_dropdown_item));
902
+ }
903
+
904
+ item.addClass($(input).data("settings").classes.selectedDropdownItem);
905
+ selected_dropdown_item = item.get(0);
906
+ }
907
+ }
908
+
909
+ // Remove highlighting from an item in the results dropdown
910
+ function deselect_dropdown_item (item) {
911
+ item.removeClass($(input).data("settings").classes.selectedDropdownItem);
912
+ selected_dropdown_item = null;
913
+ }
914
+
915
+ // Do a search and show the "searching" dropdown if the input is longer
916
+ // than $(input).data("settings").minChars
917
+ function do_search() {
918
+ var query = input_box.val();
919
+
920
+ if(query && query.length) {
921
+ if(selected_token) {
922
+ deselect_token($(selected_token), POSITION.AFTER);
923
+ }
924
+
925
+ if(query.length >= $(input).data("settings").minChars) {
926
+ show_dropdown_searching();
927
+ clearTimeout(timeout);
928
+
929
+ timeout = setTimeout(function(){
930
+ run_search(query);
931
+ }, $(input).data("settings").searchDelay);
932
+ } else {
933
+ hide_dropdown();
934
+ }
935
+ }
936
+ }
937
+
938
+ // Do the actual search
939
+ function run_search(query) {
940
+ var cache_key = query + computeURL();
941
+ var cached_results = cache.get(cache_key);
942
+ if(cached_results) {
943
+ if ($.isFunction($(input).data("settings").onCachedResult)) {
944
+ cached_results = $(input).data("settings").onCachedResult.call(hidden_input, cached_results);
945
+ }
946
+ populate_dropdown(query, cached_results);
947
+ } else {
948
+ // Are we doing an ajax search or local data search?
949
+ if($(input).data("settings").url) {
950
+ var url = computeURL();
951
+ // Extract exisiting get params
952
+ var ajax_params = {};
953
+ ajax_params.data = {};
954
+ if(url.indexOf("?") > -1) {
955
+ var parts = url.split("?");
956
+ ajax_params.url = parts[0];
957
+
958
+ var param_array = parts[1].split("&");
959
+ $.each(param_array, function (index, value) {
960
+ var kv = value.split("=");
961
+ ajax_params.data[kv[0]] = kv[1];
962
+ });
963
+ } else {
964
+ ajax_params.url = url;
965
+ }
966
+
967
+ // Prepare the request
968
+ ajax_params.data[$(input).data("settings").queryParam] = query;
969
+ ajax_params.type = $(input).data("settings").method;
970
+ ajax_params.dataType = $(input).data("settings").contentType;
971
+ if($(input).data("settings").crossDomain) {
972
+ ajax_params.dataType = "jsonp";
973
+ }
974
+
975
+ // Attach the success callback
976
+ ajax_params.success = function(results) {
977
+ cache.add(cache_key, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results);
978
+ if($.isFunction($(input).data("settings").onResult)) {
979
+ results = $(input).data("settings").onResult.call(hidden_input, results);
980
+ }
981
+
982
+ // only populate the dropdown if the results are associated with the active search query
983
+ if(input_box.val() === query) {
984
+ populate_dropdown(query, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results);
985
+ }
986
+ };
987
+
988
+ // Provide a beforeSend callback
989
+ if (settings.onSend) {
990
+ settings.onSend(ajax_params);
991
+ }
992
+
993
+ // Make the request
994
+ $.ajax(ajax_params);
995
+ } else if($(input).data("settings").local_data) {
996
+ // Do the search through local data
997
+ var results = $.grep($(input).data("settings").local_data, function (row) {
998
+ return row[$(input).data("settings").propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1;
999
+ });
1000
+
1001
+ cache.add(cache_key, results);
1002
+ if($.isFunction($(input).data("settings").onResult)) {
1003
+ results = $(input).data("settings").onResult.call(hidden_input, results);
1004
+ }
1005
+ populate_dropdown(query, results);
1006
+ }
1007
+ }
1008
+ }
1009
+
1010
+ // compute the dynamic URL
1011
+ function computeURL() {
1012
+ var url = $(input).data("settings").url;
1013
+ if(typeof $(input).data("settings").url == 'function') {
1014
+ url = $(input).data("settings").url.call($(input).data("settings"));
1015
+ }
1016
+ return url;
1017
+ }
1018
+
1019
+ // Bring browser focus to the specified object.
1020
+ // Use of setTimeout is to get around an IE bug.
1021
+ // (See, e.g., http://stackoverflow.com/questions/2600186/focus-doesnt-work-in-ie)
1022
+ //
1023
+ // obj: a jQuery object to focus()
1024
+ function focus_with_timeout(obj) {
1025
+ setTimeout(function() { obj.focus(); }, 50);
1026
+ }
1027
+
1028
+ };
1029
+
1030
+ // Really basic cache for the results
1031
+ $.TokenList.Cache = function (options) {
1032
+ var settings = $.extend({
1033
+ max_size: 500
1034
+ }, options);
1035
+
1036
+ var data = {};
1037
+ var size = 0;
1038
+
1039
+ var flush = function () {
1040
+ data = {};
1041
+ size = 0;
1042
+ };
1043
+
1044
+ this.add = function (query, results) {
1045
+ if(size > settings.max_size) {
1046
+ flush();
1047
+ }
1048
+
1049
+ if(!data[query]) {
1050
+ size += 1;
1051
+ }
1052
+
1053
+ data[query] = results;
1054
+ };
1055
+
1056
+ this.get = function (query) {
1057
+ return data[query];
1058
+ };
1059
+ };
1060
+ }(jQuery));
1061
+