jquery.better-autocomplete.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943
  1. /**
  2. * @fileOverview Better Autocomplete is a flexible jQuery plugin which offers
  3. * rich text autocompletion, both from local and remote sources.
  4. *
  5. * @author Didrik Nordström, http://betamos.se/
  6. *
  7. * @version v1.0-dev
  8. *
  9. * @requires
  10. * <ul><li>
  11. * jQuery 1.4+
  12. * </li><li>
  13. * IE7+ or any decent webkit/gecko-based web browser
  14. * </li></ul>
  15. *
  16. * @preserve Better Autocomplete v1.0-dev
  17. * https://github.com/betamos/Better-Autocomplete
  18. *
  19. * Copyright 2011, Didrik Nordström, http://betamos.se/
  20. * Dual licensed under the MIT or GPL Version 2 licenses.
  21. *
  22. * Requires jQuery 1.4+
  23. * http://jquery.com/
  24. */
  25. /**
  26. * Create or alter an autocomplete object instance that belongs to
  27. * the elements in the selection. Make sure there are only text field elements
  28. * in the selection.
  29. *
  30. * @constructor
  31. *
  32. * @name jQuery.betterAutocomplete
  33. *
  34. * @param {String} method
  35. * Should be one of the following:
  36. * <ul><li>
  37. * init: Initiate Better Autocomplete instances on the text input elements
  38. * in the current jQuery selection. They are enabled by default. The other
  39. * parameters are then required.
  40. * </li><li>
  41. * enable: In this jQuery selection, reenable the Better Autocomplete
  42. * instances.
  43. * </li><li>
  44. * disable: In this jQuery selection, disable the Better Autocomplete
  45. * instances.
  46. * </li><li>
  47. * destroy: In this jQuery selection, destroy the Better Autocomplete
  48. * instances. It will not be possible to reenable them after this.
  49. * </li></ul>
  50. *
  51. * @param {String|Object} [resource]
  52. * If String, it will become the path for a remote resource. If not, it will
  53. * be treated like a local resource. The path should provide JSON objects
  54. * upon HTTP requests.
  55. *
  56. * @param {Object} [options]
  57. * An object with configurable options:
  58. * <ul><li>
  59. * charLimit: (default=3 for remote or 1 for local resource) The minimum
  60. * number of chars to do an AJAX call. A typical use case for this limit is
  61. * to reduce server load.
  62. * </li><li>
  63. * delay: (default=350) The time in ms between last keypress and AJAX call.
  64. * Typically used to prevent looking up irrelevant strings while the user
  65. * is still typing. Only relevant for remote resources.
  66. * </li><li>
  67. * caseSensitive: (default=false) If the search should be case sensitive.
  68. * If false, query strings will be converted to lowercase.
  69. * </li><li>
  70. * cacheLimit: (default=256 for remote or 0 for local resource) The maximum
  71. * number of result objects to store in the cache. This option reduces
  72. * server load if the user deletes characters to check back on previous
  73. * results. To disable caching of previous results, set this option to 0.
  74. * </li><li>
  75. * remoteTimeout: (default=10000) The timeout for remote (AJAX) calls.
  76. * </li><li>
  77. * crossOrigin: (default=false) Set to true if cross origin requests will
  78. * be performed, i.e. that the remote URL has a different domain. This will
  79. * force Internet Explorer to use "jsonp" instead of "json" as datatype.
  80. * </li><li>
  81. * selectKeys: (default=[9, 13]) The key codes for keys which will select
  82. * the current highlighted element. The defaults are tab, enter.
  83. * </li><li>
  84. * autoHighlight: (default=true) Automatically highlight the first result.
  85. * </li></ul>
  86. *
  87. * @param {Object} [callbacks]
  88. * An object containing optional callback functions on certain events. See
  89. * {@link callbacks} for details. These callbacks should be used when
  90. * customization of the default behavior of Better Autocomplete is required.
  91. *
  92. * @returns {Object}
  93. * The jQuery object with the same element selection, for chaining.
  94. */
  95. (function($) {
  96. $.fn.betterAutocomplete = function(method) {
  97. /*
  98. * Each method expects the "this" object to be a valid DOM text input node.
  99. * The methods "enable", "disable" and "destroy" expects an instance of a
  100. * BetterAutocomplete object as their first argument.
  101. */
  102. var methods = {
  103. init: function(resource, options, callbacks) {
  104. var $input = $(this),
  105. bac = new BetterAutocomplete($input, resource, options, callbacks);
  106. $input.data('better-autocomplete', bac);
  107. bac.enable();
  108. },
  109. enable: function(bac) {
  110. bac.enable();
  111. },
  112. disable: function(bac) {
  113. bac.disable();
  114. },
  115. destroy: function(bac) {
  116. bac.destroy();
  117. }
  118. }, args = Array.prototype.slice.call(arguments, 1);
  119. // Method calling logic
  120. this.each(function() {
  121. switch (method) {
  122. case 'init':
  123. methods[method].apply(this, args);
  124. break;
  125. case 'enable':
  126. case 'disable':
  127. case 'destroy':
  128. var bac = $(this).data('better-autocomplete');
  129. if (bac instanceof BetterAutocomplete) {
  130. methods[method].call(this, bac);
  131. }
  132. break;
  133. default:
  134. $.error(['Method', method,
  135. 'does not exist in jQuery.betterAutocomplete.'].join(' '));
  136. }
  137. });
  138. // Maintain chainability
  139. return this;
  140. };
  141. /**
  142. * The BetterAutocomplete constructor function. Returns a BetterAutocomplete
  143. * instance object.
  144. *
  145. * @private @constructor
  146. * @name BetterAutocomplete
  147. *
  148. * @param {Object} $input
  149. * A single input element wrapped in jQuery.
  150. */
  151. var BetterAutocomplete = function($input, resource, options, callbacks) {
  152. var lastRenderedQuery = '',
  153. cache = {}, // Key-valued caching of search results
  154. cacheOrder = [], // Array of query strings, in the order they are added
  155. cacheSize = 0, // Keep count of the cache's size
  156. timer, // Used for options.delay
  157. activeRemoteCalls = [], // A flat array of query strings that are pending
  158. disableMouseHighlight = false, // Suppress the autotriggered mouseover event
  159. inputEvents = {},
  160. isLocal = ($.type(resource) != 'string'),
  161. $results = $('<ul />').addClass('better-autocomplete'),
  162. hiddenResults = true, // $results are hidden
  163. preventBlurTimer = null; // IE bug workaround, see below in code.
  164. options = $.extend({
  165. charLimit: isLocal ? 1 : 3,
  166. delay: 350, // milliseconds
  167. caseSensitive: false,
  168. cacheLimit: isLocal ? 0 : 256, // Number of result objects
  169. remoteTimeout: 10000, // milliseconds
  170. crossOrigin: false,
  171. selectKeys: [9, 13], // [tab, enter]
  172. autoHighlight: true // Automatically highlight the topmost result
  173. }, options);
  174. callbacks = $.extend({}, defaultCallbacks, callbacks);
  175. callbacks.insertSuggestionList($results, $input);
  176. inputEvents.focus = function() {
  177. // If the blur timer is active, a redraw is redundant.
  178. preventBlurTimer || redraw(true);
  179. };
  180. inputEvents.blur = function() {
  181. // If the blur prevention timer is active, refocus the input, since the
  182. // blur event can not be cancelled.
  183. if (preventBlurTimer) {
  184. $input.focus();
  185. }
  186. else {
  187. // The input has already lost focus, so redraw the suggestion list.
  188. redraw();
  189. }
  190. };
  191. inputEvents.keydown = function(event) {
  192. var index = getHighlightedIndex();
  193. // If an arrow key is pressed and a result is highlighted
  194. if ($.inArray(event.keyCode, [38, 40]) >= 0 && $results.children().length > 0) {
  195. var newIndex,
  196. size = $('.result', $results).length;
  197. switch (event.keyCode) {
  198. case 38: // Up arrow
  199. newIndex = Math.max(0, index - 1);
  200. break;
  201. case 40: // Down arrow
  202. newIndex = Math.min(size - 1, index + 1);
  203. break;
  204. }
  205. disableMouseHighlight = true;
  206. setHighlighted(newIndex, 'key', true);
  207. return false;
  208. }
  209. // A select key has been pressed
  210. else if ($.inArray(event.keyCode, options.selectKeys) >= 0 &&
  211. !event.shiftKey && !event.ctrlKey && !event.altKey &&
  212. !event.metaKey) {
  213. select();
  214. return event.keyCode == 9; // Never cancel tab
  215. }
  216. };
  217. inputEvents.keyup = inputEvents.click = reprocess;
  218. $results.delegate('.result', {
  219. // When the user hovers a result with the mouse, highlight it.
  220. mouseenter: function() {
  221. if (disableMouseHighlight) {
  222. return;
  223. }
  224. setHighlighted($('.result', $results).index($(this)), 'mouse');
  225. },
  226. mousemove: function() {
  227. // Enable mouseover again.
  228. disableMouseHighlight = false;
  229. },
  230. mousedown: function() {
  231. select();
  232. return false;
  233. }
  234. });
  235. // Prevent blur when clicking on group titles, scrollbars etc.,
  236. // This event is triggered after the others because of bubbling.
  237. $results.mousedown(function() {
  238. // Bug in IE where clicking on scrollbar would trigger a blur event for the
  239. // input field, despite using preventDefault() on the mousedown event.
  240. // This workaround locks the blur event on the input for a small time.
  241. clearTimeout(preventBlurTimer);
  242. preventBlurTimer = setTimeout(function() {
  243. preventBlurTimer = null;
  244. }, 50);
  245. return false;
  246. });
  247. // If auto highlight is off, remove highlighting
  248. $results.mouseleave(function() {
  249. if (!options.autoHighlight) {
  250. setHighlighted(-1);
  251. }
  252. });
  253. /*
  254. * PUBLIC METHODS
  255. */
  256. /**
  257. * Enable this instance.
  258. */
  259. this.enable = function() {
  260. // Turn off the browser's autocompletion
  261. $input
  262. .attr('autocomplete', 'OFF')
  263. .attr('aria-autocomplete', 'list');
  264. $input.bind(inputEvents);
  265. };
  266. /**
  267. * Disable this instance.
  268. */
  269. this.disable = function() {
  270. $input
  271. .removeAttr('autocomplete')
  272. .removeAttr('aria-autocomplete');
  273. $results.hide();
  274. $input.unbind(inputEvents);
  275. };
  276. /**
  277. * Disable and remove this instance. This instance should not be reused.
  278. */
  279. this.destroy = function() {
  280. $results.remove();
  281. $input.unbind(inputEvents);
  282. $input.removeData('better-autocomplete');
  283. };
  284. /*
  285. * PRIVATE METHODS
  286. */
  287. /**
  288. * Add an array of results to the cache. Internal methods always reads from
  289. * the cache, so this method must be invoked even when caching is not used,
  290. * e.g. when using local results. This method automatically clears as much of
  291. * the cache as required to fit within the cache limit.
  292. *
  293. * @param {String} query
  294. * The query to set the results to.
  295. *
  296. * @param {Array[Object]} results
  297. * The array of results for this query.
  298. */
  299. var cacheResults = function(query, results) {
  300. cacheSize += results.length;
  301. // Now reduce size until it fits
  302. while (cacheSize > options.cacheLimit && cacheOrder.length) {
  303. var key = cacheOrder.shift();
  304. cacheSize -= cache[key].length;
  305. delete cache[key];
  306. }
  307. cacheOrder.push(query);
  308. cache[query] = results;
  309. };
  310. /**
  311. * Set highlight to a specific result item
  312. *
  313. * @param {Number} index
  314. * The result item's index, or negative if highlight should be removed.
  315. *
  316. * @param {String} [trigger]
  317. * What triggered the highlight: "mouse", "key" or "auto". If index is
  318. * negative trigger may be omitted.
  319. *
  320. * @param {Boolean} [autoScroll]
  321. * (default=false) If scrolling of the results list should be automated.
  322. */
  323. var setHighlighted = function(index, trigger, autoScroll) {
  324. //console.log('Index: '+index)
  325. var prevIndex = getHighlightedIndex(),
  326. $resultList = $('.result', $results);
  327. //console.log('prevIndex: '+prevIndex)
  328. $resultList.removeClass('highlight');
  329. if (index < 0) {
  330. return
  331. }
  332. $resultList.eq(index).addClass('highlight')
  333. if (prevIndex != index) {
  334. var result = getResultByIndex(index);
  335. callbacks.highlight(result, $input, trigger);
  336. }
  337. // Scrolling
  338. var up = index == 0 || index < prevIndex,
  339. $scrollTo = $resultList.eq(index);
  340. if (!autoScroll) {
  341. return;
  342. }
  343. // Scrolling up, then make sure to show the group title
  344. if ($scrollTo.prev().is('.group') && up) {
  345. $scrollTo = $scrollTo.prev();
  346. }
  347. // Is $scrollTo partly above the visible region?
  348. if ($scrollTo.position().top < 0) {
  349. $results.scrollTop($scrollTo.position().top + $results.scrollTop());
  350. }
  351. // Or is it partly below the visible region?
  352. else if (($scrollTo.position().top + $scrollTo.outerHeight()) >
  353. $results.height()) {
  354. $results.scrollTop($scrollTo.position().top + $results.scrollTop() +
  355. $scrollTo.outerHeight() - $results.height());
  356. }
  357. };
  358. /**
  359. * Retrieve the index of the currently highlighted result item
  360. *
  361. * @returns {Number}
  362. * The result's index or -1 if no result is highlighted.
  363. */
  364. var getHighlightedIndex = function() {
  365. var res = $('.result.highlight', $results)
  366. ind= $('.result', $results).index(res);
  367. return ind
  368. };
  369. /**
  370. * Retrieve the result object with the specific position in the results list
  371. *
  372. * @param {Number} index
  373. * The index of the item in the current result list.
  374. *
  375. * @returns {Object}
  376. * The result object or null if index out of bounds.
  377. */
  378. var getResultByIndex = function(index) {
  379. var $result = $('.result', $results).eq(index);
  380. if (!$result.length) {
  381. return; // No selectable element
  382. }
  383. return $result.data('result');
  384. };
  385. /**
  386. * Select the current highlighted element, if any.
  387. */
  388. var select = function() {
  389. var highlighted = getHighlightedIndex(),
  390. result = getResultByIndex(highlighted);
  391. callbacks.select(result, $input);
  392. // Redraw again, if the callback changed focus or content
  393. reprocess();
  394. };
  395. /**
  396. * Fetch results asynchronously via AJAX.
  397. * Errors are ignored.
  398. *
  399. * @param {String} query
  400. * The query string.
  401. */
  402. var fetchResults = function(query) {
  403. // Synchronously fetch local data
  404. if (isLocal) {
  405. cacheResults(query, callbacks.queryLocalResults(query, resource,
  406. options.caseSensitive));
  407. redraw();
  408. }
  409. // Asynchronously fetch remote data
  410. else {
  411. activeRemoteCalls.push(query);
  412. var url = callbacks.constructURL(resource, query);
  413. callbacks.beginFetching($input);
  414. callbacks.fetchRemoteData(url, function(data) {
  415. var searchResults = callbacks.processRemoteData(data);
  416. if (!$.isArray(searchResults)) {
  417. searchResults = [];
  418. }
  419. cacheResults(query, searchResults);
  420. // Remove the query from active remote calls, since it's finished
  421. activeRemoteCalls = $.grep(activeRemoteCalls, function(value) {
  422. return value != query;
  423. });
  424. if (!activeRemoteCalls.length) {
  425. callbacks.finishFetching($input);
  426. }
  427. redraw();
  428. }, options.remoteTimeout, options.crossOrigin);
  429. }
  430. };
  431. /**
  432. * Reprocess the contents of the input field, fetch data and redraw if
  433. * necessary.
  434. *
  435. * @param {Object} [event]
  436. * The event that triggered the reprocessing. Not always present.
  437. */
  438. function reprocess(event) {
  439. // If this call was triggered by an arrow key, cancel the reprocessing.
  440. if ($.type(event) == 'object' && event.type == 'keyup' &&
  441. $.inArray(event.keyCode, [38, 40]) >= 0) {
  442. return;
  443. }
  444. var query = callbacks.canonicalQuery($input.val(), options.caseSensitive);
  445. clearTimeout(timer);
  446. // Indicate that timer is inactive
  447. timer = null;
  448. redraw();
  449. if (query.length >= options.charLimit && !$.isArray(cache[query]) &&
  450. $.inArray(query, activeRemoteCalls) == -1) {
  451. // Fetching is required
  452. $results.empty();
  453. if (isLocal) {
  454. fetchResults(query);
  455. }
  456. else {
  457. timer = setTimeout(function() {
  458. fetchResults(query);
  459. timer = null;
  460. }, options.delay);
  461. }
  462. }
  463. };
  464. /**
  465. * Redraws the autocomplete list based on current query and focus.
  466. *
  467. * @param {Boolean} [focus]
  468. * (default=false) Force to treat the input element like it's focused.
  469. */
  470. var redraw = function(focus) {
  471. var query = callbacks.canonicalQuery($input.val(), options.caseSensitive);
  472. // The query does not exist in db
  473. if (!$.isArray(cache[query])) {
  474. lastRenderedQuery = null;
  475. $results.empty();
  476. }
  477. // The query exists and is not already rendered
  478. else if (lastRenderedQuery !== query) {
  479. lastRenderedQuery = query;
  480. renderResults(cache[query]);
  481. if (options.autoHighlight && $('.result', $results).length > 0) {
  482. setHighlighted(0, 'auto');
  483. }
  484. }
  485. // Finally show/hide based on focus and emptiness
  486. if (($input.is(':focus') || focus) && !$results.is(':empty')) {
  487. $results.filter(':hidden').show() // Show if hidden
  488. .scrollTop($results.data('scroll-top')); // Reset the lost scrolling
  489. if (hiddenResults) {
  490. hiddenResults = false;
  491. callbacks.afterShow($results);
  492. }
  493. }
  494. else if ($results.is(':visible')) {
  495. // Store the scrolling position for later
  496. $results.data('scroll-top', $results.scrollTop())
  497. .hide(); // Hiding it resets it's scrollTop
  498. if (!hiddenResults) {
  499. hiddenResults = true;
  500. callbacks.afterHide($results);
  501. }
  502. }
  503. };
  504. /**
  505. * Regenerate the DOM content within the results list for a given set of
  506. * results. Heavy method, use only when necessary.
  507. *
  508. * @param {Array[Object]} results
  509. * An array of result objects to render.
  510. */
  511. var renderResults = function(results) {
  512. $results.empty();
  513. var groups = {}; // Key is the group name, value is the heading element.
  514. $.each(results, function(index, result) {
  515. if ($.type(result) != 'object') {
  516. return; // Continue
  517. }
  518. var output = callbacks.themeResult(result);
  519. if ($.type(output) != 'string') {
  520. return; // Continue
  521. }
  522. // Add the group if it doesn't exist
  523. var group = callbacks.getGroup(result);
  524. if ($.type(group) == 'string' && !groups[group]) {
  525. var $groupHeading = $('<li />').addClass('group')
  526. .append($('<h3 />').html(group))
  527. .appendTo($results);
  528. groups[group] = $groupHeading;
  529. }
  530. var $result = $('<li />').addClass('result')
  531. .append(output)
  532. .data('result', result) // Store the result object on this DOM element
  533. .addClass(result.addClass);
  534. // First groupless item
  535. if ($.type(group) != 'string' &&
  536. !$results.children().first().is('.result')) {
  537. $results.prepend($result);
  538. return; // Continue
  539. }
  540. var $traverseFrom = ($.type(group) == 'string') ?
  541. groups[group] : $results.children().first();
  542. var $target = $traverseFrom.nextUntil('.group').last();
  543. $result.insertAfter($target.length ? $target : $traverseFrom);
  544. });
  545. };
  546. };
  547. /*
  548. * CALLBACK METHODS
  549. */
  550. /**
  551. * These callbacks are supposed to be overridden by you when you need
  552. * customization of the default behavior. When you are overriding a callback
  553. * function, it is a good idea to copy the source code from the default
  554. * callback function, as a skeleton.
  555. *
  556. * @name callbacks
  557. * @namespace
  558. */
  559. var defaultCallbacks = {
  560. /**
  561. * @lends callbacks.prototype
  562. */
  563. /**
  564. * Gets fired when the user selects a result by clicking or using the
  565. * keyboard to select an element.
  566. *
  567. * <br /><br /><em>Default behavior: Inserts the result's title into the
  568. * input field.</em>
  569. *
  570. * @param {Object} result
  571. * The result object that was selected.
  572. *
  573. * @param {Object} $input
  574. * The input DOM element, wrapped in jQuery.
  575. */
  576. select: function(result, $input) {
  577. $input.val(result.title);
  578. },
  579. /**
  580. * Gets fired when the a result is highlighted. This may happen either
  581. * automatically or by user action.
  582. *
  583. * <br /><br /><em>Default behavior: Does nothing.</em>
  584. *
  585. * @param {Object} result
  586. * The result object that was selected.
  587. *
  588. * @param {Object} $input
  589. * The input DOM element, wrapped in jQuery.
  590. *
  591. * @param {String} trigger
  592. * The event which triggered the highlighting. Must be one of the
  593. * following:
  594. * <ul><li>
  595. * "mouse": A mouseover event triggered the highlighting.
  596. * </li><li>
  597. * "key": The user pressed an arrow key to navigate amongst the results.
  598. * </li><li>
  599. * "auto": If options.autoHighlight is set, an automatic highlight of the
  600. * first result will occur each time a new result set is rendered.
  601. * </li></ul>
  602. */
  603. highlight: function(result, $input, trigger) {
  604. // Does nothing
  605. },
  606. /**
  607. * Given a result object, theme it to HTML.
  608. *
  609. * <br /><br /><em>Default behavior: Wraps result.title in an h4 tag, and
  610. * result.description in a p tag. Note that no sanitization of malicious
  611. * scripts is done here. Whatever is within the title/description is just
  612. * printed out. May contain HTML.</em>
  613. *
  614. * @param {Object} result
  615. * The result object that should be rendered.
  616. *
  617. * @returns {String}
  618. * HTML output, will be wrapped in a list element.
  619. */
  620. themeResult: function(result) {
  621. var output = [];
  622. if ($.type(result.title) == 'string') {
  623. output.push('<h4>', result.title, '</h4>');
  624. }
  625. if ($.type(result.description) == 'string') {
  626. output.push('<p>', result.description, '</p>');
  627. }
  628. return output.join('');
  629. },
  630. /**
  631. * Retrieve local results from the local resource by providing a query
  632. * string.
  633. *
  634. * <br /><br /><em>Default behavior: Automatically handles arrays, if the
  635. * data inside each element is either a plain string or a result object.
  636. * If it is a result object, it will match the query string against the
  637. * title and description property. Search is not case sensitive.</em>
  638. *
  639. * @param {String} query
  640. * The query string, unescaped. May contain any UTF-8 character.
  641. * If case insensitive, it already is lowercased.
  642. *
  643. * @param {Object} resource
  644. * The resource provided in the {@link jQuery.betterAutocomplete} init
  645. * constructor.
  646. *
  647. * @param {Boolean} caseSensitive
  648. * From options.caseSensitive, the searching should be case sensitive.
  649. *
  650. * @returns {Array[Object]}
  651. * A flat array containing pure result objects. May be an empty array.
  652. */
  653. queryLocalResults: function(query, resource, caseSensitive) {
  654. if (!$.isArray(resource)) {
  655. // Per default Better Autocomplete only handles arrays
  656. return [];
  657. }
  658. var results = [];
  659. $.each(resource, function(i, value) {
  660. switch ($.type(value)) {
  661. case 'string': // Flat array of strings
  662. if ((caseSensitive ? value : value.toLowerCase())
  663. .indexOf(query) >= 0) {
  664. // Match found
  665. results.push({ title: value });
  666. }
  667. break;
  668. case 'object': // Array of result objects
  669. if ($.type(value.title) == 'string' &&
  670. (caseSensitive ? value.title : value.title.toLowerCase())
  671. .indexOf(query) >= 0) {
  672. // Match found in title field
  673. results.push(value);
  674. }
  675. else if ($.type(value.description) == 'string' &&
  676. (caseSensitive ? value.description :
  677. value.description.toLowerCase()).indexOf(query) >= 0) {
  678. // Match found in description field
  679. results.push(value);
  680. }
  681. break;
  682. }
  683. });
  684. return results;
  685. },
  686. /**
  687. * Fetch remote result data and return it using completeCallback when
  688. * fetching is finished. Must be asynchronous in order to not freeze the
  689. * Better Autocomplete instance.
  690. *
  691. * <br /><br /><em>Default behavior: Fetches JSON data from the url, using
  692. * the jQuery.ajax() method. Errors are ignored.</em>
  693. *
  694. * @param {String} url
  695. * The URL to fetch data from.
  696. *
  697. * @param {Function} completeCallback
  698. * This function must be called, even if an error occurs. It takes zero
  699. * or one parameter: the data that was fetched.
  700. *
  701. * @param {Number} timeout
  702. * The preferred timeout for the request. This callback should respect
  703. * the timeout.
  704. *
  705. * @param {Boolean} crossOrigin
  706. * True if a cross origin request should be performed.
  707. */
  708. fetchRemoteData: function(url, completeCallback, timeout, crossOrigin) {
  709. $.ajax({
  710. url: url,
  711. dataType: crossOrigin && !$.support.cors ? 'jsonp' : 'json',
  712. timeout: timeout,
  713. success: function(data, textStatus) {
  714. completeCallback(data);
  715. },
  716. error: function(jqXHR, textStatus, errorThrown) {
  717. completeCallback();
  718. }
  719. });
  720. },
  721. /**
  722. * Process remote fetched data by extracting an array of result objects
  723. * from it. This callback is useful if the fetched data is not the plain
  724. * results array, but a more complicated object which does contain results.
  725. *
  726. * <br /><br /><em>Default behavior: If the data is defined and is an
  727. * array, return it. Otherwise return an empty array.</em>
  728. *
  729. * @param {mixed} data
  730. * The raw data recieved from the server. Can be undefined.
  731. *
  732. * @returns {Array[Object]}
  733. * A flat array containing result objects. May be an empty array.
  734. */
  735. processRemoteData: function(data) {
  736. if ($.isArray(data)) {
  737. return data;
  738. }
  739. else {
  740. return [];
  741. }
  742. },
  743. /**
  744. * From a given result object, return it's group name (if any). Used for
  745. * grouping results together.
  746. *
  747. * <br /><br /><em>Default behavior: If the result has a "group" property
  748. * defined, return it.</em>
  749. *
  750. * @param {Object} result
  751. * The result object.
  752. *
  753. * @returns {String}
  754. * The group name, may contain HTML. If no group, don't return anything.
  755. */
  756. getGroup: function(result) {
  757. if ($.type(result.group) == 'string') {
  758. return result.group;
  759. }
  760. },
  761. /**
  762. * Called when remote fetching begins.
  763. *
  764. * <br /><br /><em>Default behavior: Adds the CSS class "fetching" to the
  765. * input field, for styling purposes.</em>
  766. *
  767. * @param {Object} $input
  768. * The input DOM element, wrapped in jQuery.
  769. */
  770. beginFetching: function($input) {
  771. $input.addClass('fetching');
  772. },
  773. /**
  774. * Called when fetching is finished. All active requests must finish before
  775. * this function is called.
  776. *
  777. * <br /><br /><em>Default behavior: Removes the "fetching" class.</em>
  778. *
  779. * @param {Object} $input
  780. * The input DOM element, wrapped in jQuery.
  781. */
  782. finishFetching: function($input) {
  783. $input.removeClass('fetching');
  784. },
  785. /**
  786. * Executed after the suggestion list has been shown.
  787. *
  788. * @param {Object} $results
  789. * The suggestion list UL element, wrapped in jQuery.
  790. *
  791. * <br /><br /><em>Default behavior: Does nothing.</em>
  792. */
  793. afterShow: function($results) {},
  794. /**
  795. * Executed after the suggestion list has been hidden.
  796. *
  797. * @param {Object} $results
  798. * The suggestion list UL element, wrapped in jQuery.
  799. *
  800. * <br /><br /><em>Default behavior: Does nothing.</em>
  801. */
  802. afterHide: function($results) {},
  803. /**
  804. * Construct the remote fetching URL.
  805. *
  806. * <br /><br /><em>Default behavior: Adds "?q=<query>" or "&q=<query>" to the
  807. * path. The query string is URL encoded.</em>
  808. *
  809. * @param {String} path
  810. * The path given in the {@link jQuery.betterAutocomplete} constructor.
  811. *
  812. * @param {String} query
  813. * The raw query string. Remember to URL encode this to prevent illegal
  814. * character errors.
  815. *
  816. * @returns {String}
  817. * The URL, ready for fetching.
  818. */
  819. constructURL: function(path, query) {
  820. return path + (path.indexOf('?') > -1 ? '&' : '?') + 'q=' + encodeURIComponent(query);
  821. },
  822. /**
  823. * To ease up on server load, treat similar strings the same.
  824. *
  825. * <br /><br /><em>Default behavior: Trims the query from leading and
  826. * trailing whitespace.</em>
  827. *
  828. * @param {String} rawQuery
  829. * The user's raw input.
  830. *
  831. * @param {Boolean} caseSensitive
  832. * Case sensitive. Will convert to lowercase if false.
  833. *
  834. * @returns {String}
  835. * The canonical query associated with this string.
  836. */
  837. canonicalQuery: function(rawQuery, caseSensitive) {
  838. var query = $.trim(rawQuery);
  839. if (!caseSensitive) {
  840. query = query.toLowerCase();
  841. }
  842. return query;
  843. },
  844. /**
  845. * Insert the results list into the DOM and position it properly.
  846. *
  847. * <br /><br /><em>Default behavior: Inserts suggestion list directly
  848. * after the input element and sets an absolute position using
  849. * jQuery.position() for determining left/top values. Also adds a nice
  850. * looking box-shadow to the list.</em>
  851. *
  852. * @param {Object} $results
  853. * The UL list element to insert, wrapped in jQuery.
  854. *
  855. * @param {Object} $input
  856. * The text input element, wrapped in jQuery.
  857. */
  858. insertSuggestionList: function($results, $input) {
  859. $results.width($input.outerWidth() - 2) // Subtract border width.
  860. .css({
  861. position: 'absolute',
  862. zIndex: 10,
  863. maxHeight: '330px',
  864. // Visually indicate that results are in the topmost layer
  865. boxShadow: '0 0 15px rgba(0, 0, 0, 0.5)'
  866. })
  867. .hide()
  868. .insertAfter($input);
  869. }
  870. };
  871. /*
  872. * jQuery focus selector, required by Better Autocomplete.
  873. *
  874. * @see http://stackoverflow.com/questions/967096/using-jquery-to-test-if-an-input-has-focus/2684561#2684561
  875. */
  876. var filters = $.expr[':'];
  877. if (!filters.focus) {
  878. filters.focus = function(elem) {
  879. return elem === document.activeElement && (elem.type || elem.href);
  880. };
  881. }
  882. })(jQuery);