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