jquery.better-autocomplete.js 31 KB

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