jquery.autocomplete.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152
  1. /**
  2. * @fileOverview jquery-autocomplete, the jQuery Autocompleter
  3. * @author <a href="mailto:dylan@dyve.net">Dylan Verheul</a>
  4. * @version 2.4.4
  5. * @requires jQuery 1.6+
  6. * @license MIT | GPL | Apache 2.0, see LICENSE.txt
  7. * @see https://github.com/dyve/jquery-autocomplete
  8. */
  9. (function($) {
  10. "use strict";
  11. /**
  12. * jQuery autocomplete plugin
  13. * @param {object|string} options
  14. * @returns (object} jQuery object
  15. */
  16. $.fn.autocomplete = function(options) {
  17. var url;
  18. if (arguments.length > 1) {
  19. url = options;
  20. options = arguments[1];
  21. options.url = url;
  22. } else if (typeof options === 'string') {
  23. url = options;
  24. options = { url: url };
  25. }
  26. var opts = $.extend({}, $.fn.autocomplete.defaults, options);
  27. return this.each(function() {
  28. var $this = $(this);
  29. $this.data('autocompleter', new $.Autocompleter(
  30. $this,
  31. $.meta ? $.extend({}, opts, $this.data()) : opts
  32. ));
  33. });
  34. };
  35. /**
  36. * Store default options
  37. * @type {object}
  38. */
  39. $.fn.autocomplete.defaults = {
  40. inputClass: 'acInput',
  41. loadingClass: 'acLoading',
  42. resultsClass: 'acResults',
  43. selectClass: 'acSelect',
  44. queryParamName: 'q',
  45. extraParams: {},
  46. remoteDataType: false,
  47. lineSeparator: '\n',
  48. cellSeparator: '|',
  49. minChars: 2,
  50. maxItemsToShow: 10,
  51. delay: 400,
  52. useCache: true,
  53. maxCacheLength: 10,
  54. matchSubset: true,
  55. matchCase: false,
  56. matchInside: true,
  57. mustMatch: false,
  58. selectFirst: false,
  59. selectOnly: false,
  60. showResult: null,
  61. preventDefaultReturn: 1,
  62. preventDefaultTab: 0,
  63. autoFill: false,
  64. filterResults: true,
  65. filter: true,
  66. sortResults: true,
  67. sortFunction: null,
  68. onItemSelect: null,
  69. onNoMatch: null,
  70. onFinish: null,
  71. matchStringConverter: null,
  72. beforeUseConverter: null,
  73. autoWidth: 'min-width',
  74. useDelimiter: false,
  75. delimiterChar: ',',
  76. delimiterKeyCode: 188,
  77. processData: null,
  78. onError: null,
  79. enabled: true
  80. };
  81. /**
  82. * Sanitize result
  83. * @param {Object} result
  84. * @returns {Object} object with members value (String) and data (Object)
  85. * @private
  86. */
  87. var sanitizeResult = function(result) {
  88. var value, data;
  89. var type = typeof result;
  90. if (type === 'string') {
  91. value = result;
  92. data = {};
  93. } else if ($.isArray(result)) {
  94. value = result[0];
  95. data = result.slice(1);
  96. } else if (type === 'object') {
  97. value = result.value;
  98. data = result.data;
  99. }
  100. value = String(value);
  101. if (typeof data !== 'object') {
  102. data = {};
  103. }
  104. return {
  105. value: value,
  106. data: data
  107. };
  108. };
  109. /**
  110. * Sanitize integer
  111. * @param {mixed} value
  112. * @param {Object} options
  113. * @returns {Number} integer
  114. * @private
  115. */
  116. var sanitizeInteger = function(value, stdValue, options) {
  117. var num = parseInt(value, 10);
  118. options = options || {};
  119. if (isNaN(num) || (options.min && num < options.min)) {
  120. num = stdValue;
  121. }
  122. return num;
  123. };
  124. /**
  125. * Create partial url for a name/value pair
  126. */
  127. var makeUrlParam = function(name, value) {
  128. return [name, encodeURIComponent(value)].join('=');
  129. };
  130. /**
  131. * Build an url
  132. * @param {string} url Base url
  133. * @param {object} [params] Dictionary of parameters
  134. */
  135. var makeUrl = function(url, params) {
  136. var urlAppend = [];
  137. $.each(params, function(index, value) {
  138. urlAppend.push(makeUrlParam(index, value));
  139. });
  140. if (urlAppend.length) {
  141. url += url.indexOf('?') === -1 ? '?' : '&';
  142. url += urlAppend.join('&');
  143. }
  144. return url;
  145. };
  146. /**
  147. * Default sort filter
  148. * @param {object} a
  149. * @param {object} b
  150. * @param {boolean} matchCase
  151. * @returns {number}
  152. */
  153. var sortValueAlpha = function(a, b, matchCase) {
  154. a = String(a.value);
  155. b = String(b.value);
  156. if (!matchCase) {
  157. a = a.toLowerCase();
  158. b = b.toLowerCase();
  159. }
  160. if (a > b) {
  161. return 1;
  162. }
  163. if (a < b) {
  164. return -1;
  165. }
  166. return 0;
  167. };
  168. /**
  169. * Parse data received in text format
  170. * @param {string} text Plain text input
  171. * @param {string} lineSeparator String that separates lines
  172. * @param {string} cellSeparator String that separates cells
  173. * @returns {array} Array of autocomplete data objects
  174. */
  175. var plainTextParser = function(text, lineSeparator, cellSeparator) {
  176. var results = [];
  177. var i, j, data, line, value, lines;
  178. // Be nice, fix linebreaks before splitting on lineSeparator
  179. lines = String(text).replace('\r\n', '\n').split(lineSeparator);
  180. for (i = 0; i < lines.length; i++) {
  181. line = lines[i].split(cellSeparator);
  182. data = [];
  183. for (j = 0; j < line.length; j++) {
  184. data.push(decodeURIComponent(line[j]));
  185. }
  186. value = data.shift();
  187. results.push({ value: value, data: data });
  188. }
  189. return results;
  190. };
  191. /**
  192. * Autocompleter class
  193. * @param {object} $elem jQuery object with one input tag
  194. * @param {object} options Settings
  195. * @constructor
  196. */
  197. $.Autocompleter = function($elem, options) {
  198. /**
  199. * Assert parameters
  200. */
  201. if (!$elem || !($elem instanceof $) || $elem.length !== 1 || $elem.get(0).tagName.toUpperCase() !== 'INPUT') {
  202. throw new Error('Invalid parameter for jquery.Autocompleter, jQuery object with one element with INPUT tag expected.');
  203. }
  204. /**
  205. * @constant Link to this instance
  206. * @type object
  207. * @private
  208. */
  209. var self = this;
  210. /**
  211. * @property {object} Options for this instance
  212. * @public
  213. */
  214. this.options = options;
  215. /**
  216. * @property object Cached data for this instance
  217. * @private
  218. */
  219. this.cacheData_ = {};
  220. /**
  221. * @property {number} Number of cached data items
  222. * @private
  223. */
  224. this.cacheLength_ = 0;
  225. /**
  226. * @property {string} Class name to mark selected item
  227. * @private
  228. */
  229. this.selectClass_ = 'jquery-autocomplete-selected-item';
  230. /**
  231. * @property {number} Handler to activation timeout
  232. * @private
  233. */
  234. this.keyTimeout_ = null;
  235. /**
  236. * @property {number} Handler to finish timeout
  237. * @private
  238. */
  239. this.finishTimeout_ = null;
  240. /**
  241. * @property {number} Last key pressed in the input field (store for behavior)
  242. * @private
  243. */
  244. this.lastKeyPressed_ = null;
  245. /**
  246. * @property {string} Last value processed by the autocompleter
  247. * @private
  248. */
  249. this.lastProcessedValue_ = null;
  250. /**
  251. * @property {string} Last value selected by the user
  252. * @private
  253. */
  254. this.lastSelectedValue_ = null;
  255. /**
  256. * @property {boolean} Is this autocompleter active (showing results)?
  257. * @see showResults
  258. * @private
  259. */
  260. this.active_ = false;
  261. /**
  262. * @property {boolean} Is this autocompleter allowed to finish on blur?
  263. * @private
  264. */
  265. this.finishOnBlur_ = true;
  266. /**
  267. * Sanitize options
  268. */
  269. this.options.minChars = sanitizeInteger(this.options.minChars, $.fn.autocomplete.defaults.minChars, { min: 0 });
  270. this.options.maxItemsToShow = sanitizeInteger(this.options.maxItemsToShow, $.fn.autocomplete.defaults.maxItemsToShow, { min: 0 });
  271. this.options.maxCacheLength = sanitizeInteger(this.options.maxCacheLength, $.fn.autocomplete.defaults.maxCacheLength, { min: 1 });
  272. this.options.delay = sanitizeInteger(this.options.delay, $.fn.autocomplete.defaults.delay, { min: 0 });
  273. if (this.options.preventDefaultReturn != 2) {
  274. this.options.preventDefaultReturn = this.options.preventDefaultReturn ? 1 : 0;
  275. }
  276. if (this.options.preventDefaultTab != 2) {
  277. this.options.preventDefaultTab = this.options.preventDefaultTab ? 1 : 0;
  278. }
  279. /**
  280. * Init DOM elements repository
  281. */
  282. this.dom = {};
  283. /**
  284. * Store the input element we're attached to in the repository
  285. */
  286. this.dom.$elem = $elem;
  287. /**
  288. * Switch off the native autocomplete and add the input class
  289. */
  290. this.dom.$elem.attr('autocomplete', 'off').addClass(this.options.inputClass);
  291. /**
  292. * Create DOM element to hold results, and force absolute position
  293. */
  294. this.dom.$results = $('<div></div>').hide().addClass(this.options.resultsClass).css({
  295. position: 'absolute'
  296. });
  297. $('body').append(this.dom.$results);
  298. /**
  299. * Attach keyboard monitoring to $elem
  300. */
  301. $elem.keydown(function(e) {
  302. self.lastKeyPressed_ = e.keyCode;
  303. switch(self.lastKeyPressed_) {
  304. case self.options.delimiterKeyCode: // comma = 188
  305. if (self.options.useDelimiter && self.active_) {
  306. self.selectCurrent();
  307. }
  308. break;
  309. // ignore navigational & special keys
  310. case 35: // end
  311. case 36: // home
  312. case 16: // shift
  313. case 17: // ctrl
  314. case 18: // alt
  315. case 37: // left
  316. case 39: // right
  317. break;
  318. case 38: // up
  319. e.preventDefault();
  320. if (self.active_) {
  321. self.focusPrev();
  322. } else {
  323. self.activate();
  324. }
  325. return false;
  326. case 40: // down
  327. e.preventDefault();
  328. if (self.active_) {
  329. self.focusNext();
  330. } else {
  331. self.activate();
  332. }
  333. return false;
  334. case 9: // tab
  335. if (self.active_) {
  336. self.selectCurrent();
  337. if (self.options.preventDefaultTab) {
  338. e.preventDefault();
  339. return false;
  340. }
  341. }
  342. if (self.options.preventDefaultTab === 2) {
  343. e.preventDefault();
  344. return false;
  345. }
  346. break;
  347. case 13: // return
  348. if (self.active_) {
  349. self.selectCurrent();
  350. if (self.options.preventDefaultReturn) {
  351. e.preventDefault();
  352. return false;
  353. }
  354. }
  355. if (self.options.preventDefaultReturn === 2) {
  356. e.preventDefault();
  357. return false;
  358. }
  359. break;
  360. case 27: // escape
  361. if (self.active_) {
  362. e.preventDefault();
  363. self.deactivate(true);
  364. return false;
  365. }
  366. break;
  367. default:
  368. self.activate();
  369. }
  370. });
  371. /**
  372. * Attach paste event listener because paste may occur much later then keydown or even without a keydown at all
  373. */
  374. $elem.on('paste', function() {
  375. self.activate();
  376. });
  377. /**
  378. * Finish on blur event
  379. * Use a timeout because instant blur gives race conditions
  380. */
  381. var onBlurFunction = function() {
  382. self.deactivate(true);
  383. }
  384. $elem.blur(function() {
  385. if (self.finishOnBlur_) {
  386. self.finishTimeout_ = setTimeout(onBlurFunction, 200);
  387. }
  388. });
  389. /**
  390. * Catch a race condition on form submit
  391. */
  392. $elem.parents('form').on('submit', onBlurFunction);
  393. };
  394. /**
  395. * Position output DOM elements
  396. * @private
  397. */
  398. $.Autocompleter.prototype.position = function() {
  399. var offset = this.dom.$elem.offset();
  400. var height = this.dom.$results.outerHeight();
  401. var totalHeight = $(window).outerHeight();
  402. var inputBottom = offset.top + this.dom.$elem.outerHeight();
  403. var bottomIfDown = inputBottom + height;
  404. // Set autocomplete results at the bottom of input
  405. var position = {top: inputBottom, left: offset.left};
  406. if (bottomIfDown > totalHeight) {
  407. // Try to set autocomplete results at the top of input
  408. var topIfUp = offset.top - height;
  409. if (topIfUp >= 0) {
  410. position.top = topIfUp;
  411. }
  412. }
  413. this.dom.$results.css(position);
  414. };
  415. /**
  416. * Read from cache
  417. * @private
  418. */
  419. $.Autocompleter.prototype.cacheRead = function(filter) {
  420. var filterLength, searchLength, search, maxPos, pos;
  421. if (this.options.useCache) {
  422. filter = String(filter);
  423. filterLength = filter.length;
  424. if (this.options.matchSubset) {
  425. searchLength = 1;
  426. } else {
  427. searchLength = filterLength;
  428. }
  429. while (searchLength <= filterLength) {
  430. if (this.options.matchInside) {
  431. maxPos = filterLength - searchLength;
  432. } else {
  433. maxPos = 0;
  434. }
  435. pos = 0;
  436. while (pos <= maxPos) {
  437. search = filter.substr(0, searchLength);
  438. if (this.cacheData_[search] !== undefined) {
  439. return this.cacheData_[search];
  440. }
  441. pos++;
  442. }
  443. searchLength++;
  444. }
  445. }
  446. return false;
  447. };
  448. /**
  449. * Write to cache
  450. * @private
  451. */
  452. $.Autocompleter.prototype.cacheWrite = function(filter, data) {
  453. if (this.options.useCache) {
  454. if (this.cacheLength_ >= this.options.maxCacheLength) {
  455. this.cacheFlush();
  456. }
  457. filter = String(filter);
  458. if (this.cacheData_[filter] !== undefined) {
  459. this.cacheLength_++;
  460. }
  461. this.cacheData_[filter] = data;
  462. return this.cacheData_[filter];
  463. }
  464. return false;
  465. };
  466. /**
  467. * Flush cache
  468. * @public
  469. */
  470. $.Autocompleter.prototype.cacheFlush = function() {
  471. this.cacheData_ = {};
  472. this.cacheLength_ = 0;
  473. };
  474. /**
  475. * Call hook
  476. * Note that all called hooks are passed the autocompleter object
  477. * @param {string} hook
  478. * @param data
  479. * @returns Result of called hook, false if hook is undefined
  480. */
  481. $.Autocompleter.prototype.callHook = function(hook, data) {
  482. var f = this.options[hook];
  483. if (f && $.isFunction(f)) {
  484. return f(data, this);
  485. }
  486. return false;
  487. };
  488. /**
  489. * Set timeout to activate autocompleter
  490. */
  491. $.Autocompleter.prototype.activate = function() {
  492. if (!this.options.enabled) return;
  493. var self = this;
  494. if (this.keyTimeout_) {
  495. clearTimeout(this.keyTimeout_);
  496. }
  497. this.keyTimeout_ = setTimeout(function() {
  498. self.activateNow();
  499. }, this.options.delay);
  500. };
  501. /**
  502. * Activate autocompleter immediately
  503. */
  504. $.Autocompleter.prototype.activateNow = function() {
  505. var value = this.beforeUseConverter(this.dom.$elem.val());
  506. if (value !== this.lastProcessedValue_ && value !== this.lastSelectedValue_) {
  507. this.fetchData(value);
  508. }
  509. };
  510. /**
  511. * Get autocomplete data for a given value
  512. * @param {string} value Value to base autocompletion on
  513. * @private
  514. */
  515. $.Autocompleter.prototype.fetchData = function(value) {
  516. var self = this;
  517. var processResults = function(results, filter) {
  518. if (self.options.processData) {
  519. results = self.options.processData(results);
  520. }
  521. self.showResults(self.filterResults(results, filter), filter);
  522. };
  523. this.lastProcessedValue_ = value;
  524. if (value.length < this.options.minChars) {
  525. processResults([], value);
  526. } else if (this.options.data) {
  527. processResults(this.options.data, value);
  528. } else {
  529. this.fetchRemoteData(value, function(remoteData) {
  530. processResults(remoteData, value);
  531. });
  532. }
  533. };
  534. /**
  535. * Get remote autocomplete data for a given value
  536. * @param {string} filter The filter to base remote data on
  537. * @param {function} callback The function to call after data retrieval
  538. * @private
  539. */
  540. $.Autocompleter.prototype.fetchRemoteData = function(filter, callback) {
  541. var data = this.cacheRead(filter);
  542. if (data) {
  543. callback(data);
  544. } else {
  545. var self = this;
  546. var dataType = self.options.remoteDataType === 'json' ? 'json' : 'text';
  547. var ajaxCallback = function(data) {
  548. var parsed = false;
  549. if (data !== false) {
  550. parsed = self.parseRemoteData(data);
  551. self.cacheWrite(filter, parsed);
  552. }
  553. self.dom.$elem.removeClass(self.options.loadingClass);
  554. callback(parsed);
  555. };
  556. this.dom.$elem.addClass(this.options.loadingClass);
  557. $.ajax({
  558. url: this.makeUrl(filter),
  559. success: ajaxCallback,
  560. error: function(jqXHR, textStatus, errorThrown) {
  561. if($.isFunction(self.options.onError)) {
  562. self.options.onError(jqXHR, textStatus, errorThrown);
  563. } else {
  564. ajaxCallback(false);
  565. }
  566. },
  567. dataType: dataType
  568. });
  569. }
  570. };
  571. /**
  572. * Create or update an extra parameter for the remote request
  573. * @param {string} name Parameter name
  574. * @param {string} value Parameter value
  575. * @public
  576. */
  577. $.Autocompleter.prototype.setExtraParam = function(name, value) {
  578. var index = $.trim(String(name));
  579. if (index) {
  580. if (!this.options.extraParams) {
  581. this.options.extraParams = {};
  582. }
  583. if (this.options.extraParams[index] !== value) {
  584. this.options.extraParams[index] = value;
  585. this.cacheFlush();
  586. }
  587. }
  588. return this;
  589. };
  590. /**
  591. * Build the url for a remote request
  592. * If options.queryParamName === false, append query to url instead of using a GET parameter
  593. * @param {string} param The value parameter to pass to the backend
  594. * @returns {string} The finished url with parameters
  595. */
  596. $.Autocompleter.prototype.makeUrl = function(param) {
  597. var self = this;
  598. var url = this.options.url;
  599. var params = $.extend({}, this.options.extraParams);
  600. if (this.options.queryParamName === false) {
  601. url += encodeURIComponent(param);
  602. } else {
  603. params[this.options.queryParamName] = param;
  604. }
  605. return makeUrl(url, params);
  606. };
  607. /**
  608. * Parse data received from server
  609. * @param remoteData Data received from remote server
  610. * @returns {array} Parsed data
  611. */
  612. $.Autocompleter.prototype.parseRemoteData = function(remoteData) {
  613. var remoteDataType;
  614. var data = remoteData;
  615. if (this.options.remoteDataType === 'json') {
  616. remoteDataType = typeof(remoteData);
  617. switch (remoteDataType) {
  618. case 'object':
  619. data = remoteData;
  620. break;
  621. case 'string':
  622. data = $.parseJSON(remoteData);
  623. break;
  624. default:
  625. throw new Error("Unexpected remote data type: " + remoteDataType);
  626. }
  627. return data;
  628. }
  629. return plainTextParser(data, this.options.lineSeparator, this.options.cellSeparator);
  630. };
  631. /**
  632. * Default filter for results
  633. * @param {Object} result
  634. * @param {String} filter
  635. * @returns {boolean} Include this result
  636. * @private
  637. */
  638. $.Autocompleter.prototype.defaultFilter = function(result, filter) {
  639. if (!result.value) {
  640. return false;
  641. }
  642. if (this.options.filterResults) {
  643. var pattern = this.matchStringConverter(filter);
  644. var testValue = this.matchStringConverter(result.value);
  645. if (!this.options.matchCase) {
  646. pattern = pattern.toLowerCase();
  647. testValue = testValue.toLowerCase();
  648. }
  649. var patternIndex = testValue.indexOf(pattern);
  650. if (this.options.matchInside) {
  651. return patternIndex > -1;
  652. } else {
  653. return patternIndex === 0;
  654. }
  655. }
  656. return true;
  657. };
  658. /**
  659. * Filter result
  660. * @param {Object} result
  661. * @param {String} filter
  662. * @returns {boolean} Include this result
  663. * @private
  664. */
  665. $.Autocompleter.prototype.filterResult = function(result, filter) {
  666. // No filter
  667. if (this.options.filter === false) {
  668. return true;
  669. }
  670. // Custom filter
  671. if ($.isFunction(this.options.filter)) {
  672. return this.options.filter(result, filter);
  673. }
  674. // Default filter
  675. return this.defaultFilter(result, filter);
  676. };
  677. /**
  678. * Filter results
  679. * @param results
  680. * @param filter
  681. */
  682. $.Autocompleter.prototype.filterResults = function(results, filter) {
  683. var filtered = [];
  684. var i, result;
  685. for (i = 0; i < results.length; i++) {
  686. result = sanitizeResult(results[i]);
  687. if (this.filterResult(result, filter)) {
  688. filtered.push(result);
  689. }
  690. }
  691. if (this.options.sortResults) {
  692. filtered = this.sortResults(filtered, filter);
  693. }
  694. if (this.options.maxItemsToShow > 0 && this.options.maxItemsToShow < filtered.length) {
  695. filtered.length = this.options.maxItemsToShow;
  696. }
  697. return filtered;
  698. };
  699. /**
  700. * Sort results
  701. * @param results
  702. * @param filter
  703. */
  704. $.Autocompleter.prototype.sortResults = function(results, filter) {
  705. var self = this;
  706. var sortFunction = this.options.sortFunction;
  707. if (!$.isFunction(sortFunction)) {
  708. sortFunction = function(a, b, f) {
  709. return sortValueAlpha(a, b, self.options.matchCase);
  710. };
  711. }
  712. results.sort(function(a, b) {
  713. return sortFunction(a, b, filter, self.options);
  714. });
  715. return results;
  716. };
  717. /**
  718. * Convert string before matching
  719. * @param s
  720. * @param a
  721. * @param b
  722. */
  723. $.Autocompleter.prototype.matchStringConverter = function(s, a, b) {
  724. var converter = this.options.matchStringConverter;
  725. if ($.isFunction(converter)) {
  726. s = converter(s, a, b);
  727. }
  728. return s;
  729. };
  730. /**
  731. * Convert string before use
  732. * @param {String} s
  733. */
  734. $.Autocompleter.prototype.beforeUseConverter = function(s) {
  735. s = this.getValue(s);
  736. var converter = this.options.beforeUseConverter;
  737. if ($.isFunction(converter)) {
  738. s = converter(s);
  739. }
  740. return s;
  741. };
  742. /**
  743. * Enable finish on blur event
  744. */
  745. $.Autocompleter.prototype.enableFinishOnBlur = function() {
  746. this.finishOnBlur_ = true;
  747. };
  748. /**
  749. * Disable finish on blur event
  750. */
  751. $.Autocompleter.prototype.disableFinishOnBlur = function() {
  752. this.finishOnBlur_ = false;
  753. };
  754. /**
  755. * Create a results item (LI element) from a result
  756. * @param result
  757. */
  758. $.Autocompleter.prototype.createItemFromResult = function(result) {
  759. var self = this;
  760. var $li = $('<li/>');
  761. $li.html(this.showResult(result.value, result.data));
  762. $li.data({value: result.value, data: result.data})
  763. .click(function() {
  764. self.selectItem($li);
  765. })
  766. .mousedown(self.disableFinishOnBlur)
  767. .mouseup(self.enableFinishOnBlur)
  768. ;
  769. return $li;
  770. };
  771. /**
  772. * Get all items from the results list
  773. * @param result
  774. */
  775. $.Autocompleter.prototype.getItems = function() {
  776. return $('>ul>li', this.dom.$results);
  777. };
  778. /**
  779. * Show all results
  780. * @param results
  781. * @param filter
  782. */
  783. $.Autocompleter.prototype.showResults = function(results, filter) {
  784. var numResults = results.length;
  785. var self = this;
  786. var $ul = $('<ul></ul>');
  787. var i, result, $li, autoWidth, first = false, $first = false;
  788. if (numResults) {
  789. for (i = 0; i < numResults; i++) {
  790. result = results[i];
  791. $li = this.createItemFromResult(result);
  792. $ul.append($li);
  793. if (first === false) {
  794. first = String(result.value);
  795. $first = $li;
  796. $li.addClass(this.options.firstItemClass);
  797. }
  798. if (i === numResults - 1) {
  799. $li.addClass(this.options.lastItemClass);
  800. }
  801. }
  802. this.dom.$results.html($ul).show();
  803. // Always recalculate position since window size or
  804. // input element location may have changed.
  805. this.position();
  806. if (this.options.autoWidth) {
  807. autoWidth = this.dom.$elem.outerWidth() - this.dom.$results.outerWidth() + this.dom.$results.width();
  808. this.dom.$results.css(this.options.autoWidth, autoWidth);
  809. }
  810. this.getItems().hover(
  811. function() { self.focusItem(this); },
  812. function() { /* void */ }
  813. );
  814. if (this.autoFill(first, filter) || this.options.selectFirst || (this.options.selectOnly && numResults === 1)) {
  815. this.focusItem($first);
  816. }
  817. this.active_ = true;
  818. } else {
  819. this.hideResults();
  820. this.active_ = false;
  821. }
  822. };
  823. $.Autocompleter.prototype.showResult = function(value, data) {
  824. if ($.isFunction(this.options.showResult)) {
  825. return this.options.showResult(value, data);
  826. } else {
  827. return $('<p></p>').text(value).html();
  828. }
  829. };
  830. $.Autocompleter.prototype.autoFill = function(value, filter) {
  831. var lcValue, lcFilter, valueLength, filterLength;
  832. if (this.options.autoFill && this.lastKeyPressed_ !== 8) {
  833. lcValue = String(value).toLowerCase();
  834. lcFilter = String(filter).toLowerCase();
  835. valueLength = value.length;
  836. filterLength = filter.length;
  837. if (lcValue.substr(0, filterLength) === lcFilter) {
  838. var d = this.getDelimiterOffsets();
  839. var pad = d.start ? ' ' : ''; // if there is a preceding delimiter
  840. this.setValue( pad + value );
  841. var start = filterLength + d.start + pad.length;
  842. var end = valueLength + d.start + pad.length;
  843. this.selectRange(start, end);
  844. return true;
  845. }
  846. }
  847. return false;
  848. };
  849. $.Autocompleter.prototype.focusNext = function() {
  850. this.focusMove(+1);
  851. };
  852. $.Autocompleter.prototype.focusPrev = function() {
  853. this.focusMove(-1);
  854. };
  855. $.Autocompleter.prototype.focusMove = function(modifier) {
  856. var $items = this.getItems();
  857. modifier = sanitizeInteger(modifier, 0);
  858. if (modifier) {
  859. for (var i = 0; i < $items.length; i++) {
  860. if ($($items[i]).hasClass(this.selectClass_)) {
  861. this.focusItem(i + modifier);
  862. return;
  863. }
  864. }
  865. }
  866. this.focusItem(0);
  867. };
  868. $.Autocompleter.prototype.focusItem = function(item) {
  869. var $item, $items = this.getItems();
  870. if ($items.length) {
  871. $items.removeClass(this.selectClass_).removeClass(this.options.selectClass);
  872. if (typeof item === 'number') {
  873. if (item < 0) {
  874. item = 0;
  875. } else if (item >= $items.length) {
  876. item = $items.length - 1;
  877. }
  878. $item = $($items[item]);
  879. } else {
  880. $item = $(item);
  881. }
  882. if ($item) {
  883. $item.addClass(this.selectClass_).addClass(this.options.selectClass);
  884. }
  885. }
  886. };
  887. $.Autocompleter.prototype.selectCurrent = function() {
  888. var $item = $('li.' + this.selectClass_, this.dom.$results);
  889. if ($item.length === 1) {
  890. this.selectItem($item);
  891. } else {
  892. this.deactivate(false);
  893. }
  894. };
  895. $.Autocompleter.prototype.selectItem = function($li) {
  896. var value = $li.data('value');
  897. var data = $li.data('data');
  898. var displayValue = this.displayValue(value, data);
  899. var processedDisplayValue = this.beforeUseConverter(displayValue);
  900. this.lastProcessedValue_ = processedDisplayValue;
  901. this.lastSelectedValue_ = processedDisplayValue;
  902. var d = this.getDelimiterOffsets();
  903. var delimiter = this.options.delimiterChar;
  904. var elem = this.dom.$elem;
  905. var extraCaretPos = 0;
  906. if ( this.options.useDelimiter ) {
  907. // if there is a preceding delimiter, add a space after the delimiter
  908. if ( elem.val().substring(d.start-1, d.start) == delimiter && delimiter != ' ' ) {
  909. displayValue = ' ' + displayValue;
  910. }
  911. // if there is not already a delimiter trailing this value, add it
  912. if ( elem.val().substring(d.end, d.end+1) != delimiter && this.lastKeyPressed_ != this.options.delimiterKeyCode ) {
  913. displayValue = displayValue + delimiter;
  914. } else {
  915. // move the cursor after the existing trailing delimiter
  916. extraCaretPos = 1;
  917. }
  918. }
  919. this.setValue(displayValue);
  920. this.setCaret(d.start + displayValue.length + extraCaretPos);
  921. this.callHook('onItemSelect', { value: value, data: data });
  922. this.deactivate(true);
  923. elem.focus();
  924. };
  925. $.Autocompleter.prototype.displayValue = function(value, data) {
  926. if ($.isFunction(this.options.displayValue)) {
  927. return this.options.displayValue(value, data);
  928. }
  929. return value;
  930. };
  931. $.Autocompleter.prototype.hideResults = function() {
  932. this.dom.$results.hide();
  933. };
  934. $.Autocompleter.prototype.deactivate = function(finish) {
  935. if (this.finishTimeout_) {
  936. clearTimeout(this.finishTimeout_);
  937. }
  938. if (this.keyTimeout_) {
  939. clearTimeout(this.keyTimeout_);
  940. }
  941. if (finish) {
  942. if (this.lastProcessedValue_ !== this.lastSelectedValue_) {
  943. if (this.options.mustMatch) {
  944. this.setValue('');
  945. }
  946. this.callHook('onNoMatch');
  947. }
  948. if (this.active_) {
  949. this.callHook('onFinish');
  950. }
  951. this.lastKeyPressed_ = null;
  952. this.lastProcessedValue_ = null;
  953. this.lastSelectedValue_ = null;
  954. this.active_ = false;
  955. }
  956. this.hideResults();
  957. };
  958. $.Autocompleter.prototype.selectRange = function(start, end) {
  959. var input = this.dom.$elem.get(0);
  960. if (input.setSelectionRange) {
  961. input.focus();
  962. input.setSelectionRange(start, end);
  963. } else if (input.createTextRange) {
  964. var range = input.createTextRange();
  965. range.collapse(true);
  966. range.moveEnd('character', end);
  967. range.moveStart('character', start);
  968. range.select();
  969. }
  970. };
  971. /**
  972. * Move caret to position
  973. * @param {Number} pos
  974. */
  975. $.Autocompleter.prototype.setCaret = function(pos) {
  976. this.selectRange(pos, pos);
  977. };
  978. /**
  979. * Get caret position
  980. */
  981. $.Autocompleter.prototype.getCaret = function() {
  982. var $elem = this.dom.$elem;
  983. var elem = $elem[0];
  984. var val, selection, range, start, end, stored_range;
  985. if (elem.createTextRange) { // IE
  986. selection = document.selection;
  987. if (elem.tagName.toLowerCase() != 'textarea') {
  988. val = $elem.val();
  989. range = selection.createRange().duplicate();
  990. range.moveEnd('character', val.length);
  991. if (range.text === '') {
  992. start = val.length;
  993. } else {
  994. start = val.lastIndexOf(range.text);
  995. }
  996. range = selection.createRange().duplicate();
  997. range.moveStart('character', -val.length);
  998. end = range.text.length;
  999. } else {
  1000. range = selection.createRange();
  1001. stored_range = range.duplicate();
  1002. stored_range.moveToElementText(elem);
  1003. stored_range.setEndPoint('EndToEnd', range);
  1004. start = stored_range.text.length - range.text.length;
  1005. end = start + range.text.length;
  1006. }
  1007. } else {
  1008. start = $elem[0].selectionStart;
  1009. end = $elem[0].selectionEnd;
  1010. }
  1011. return {
  1012. start: start,
  1013. end: end
  1014. };
  1015. };
  1016. /**
  1017. * Set the value that is currently being autocompleted
  1018. * @param {String} value
  1019. */
  1020. $.Autocompleter.prototype.setValue = function(value) {
  1021. if ( this.options.useDelimiter ) {
  1022. // set the substring between the current delimiters
  1023. var val = this.dom.$elem.val();
  1024. var d = this.getDelimiterOffsets();
  1025. var preVal = val.substring(0, d.start);
  1026. var postVal = val.substring(d.end);
  1027. value = preVal + value + postVal;
  1028. }
  1029. this.dom.$elem.val(value);
  1030. };
  1031. /**
  1032. * Get the value currently being autocompleted
  1033. * @param {String} value
  1034. */
  1035. $.Autocompleter.prototype.getValue = function(value) {
  1036. if ( this.options.useDelimiter ) {
  1037. var d = this.getDelimiterOffsets();
  1038. return value.substring(d.start, d.end).trim();
  1039. } else {
  1040. return value;
  1041. }
  1042. };
  1043. /**
  1044. * Get the offsets of the value currently being autocompleted
  1045. */
  1046. $.Autocompleter.prototype.getDelimiterOffsets = function() {
  1047. var val = this.dom.$elem.val();
  1048. if ( this.options.useDelimiter ) {
  1049. var preCaretVal = val.substring(0, this.getCaret().start);
  1050. var start = preCaretVal.lastIndexOf(this.options.delimiterChar) + 1;
  1051. var postCaretVal = val.substring(this.getCaret().start);
  1052. var end = postCaretVal.indexOf(this.options.delimiterChar);
  1053. if ( end == -1 ) end = val.length;
  1054. end += this.getCaret().start;
  1055. } else {
  1056. start = 0;
  1057. end = val.length;
  1058. }
  1059. return {
  1060. start: start,
  1061. end: end
  1062. };
  1063. };
  1064. })((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined')? django.jQuery : jQuery);