jquery.tokeninput.js 29 KB

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