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