jquery.autocomplete.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. jQuery.autocomplete = function(input, options) {
  2. // Create a link to self
  3. var me = this;
  4. // Create jQuery object for input element
  5. var $input = $(input).attr("autocomplete", "off");
  6. // Apply inputClass if necessary
  7. if (options.inputClass) $input.addClass(options.inputClass);
  8. // Create results
  9. var results = document.createElement("div");
  10. // Create jQuery object for results
  11. var $results = $(results);
  12. $results.hide().addClass(options.resultsClass).css("position", "absolute");
  13. if( options.width > 0 ) $results.css("width", options.width);
  14. // Add to body element
  15. $("body").append(results);
  16. input.autocompleter = me;
  17. var timeout = null;
  18. var prev = "";
  19. var active = -1;
  20. var cache = {};
  21. var keyb = false;
  22. var hasFocus = false;
  23. var lastKeyPressCode = null;
  24. // flush cache
  25. function flushCache(){
  26. cache = {};
  27. cache.data = {};
  28. cache.length = 0;
  29. };
  30. // flush cache
  31. flushCache();
  32. // if there is a data array supplied
  33. if( options.data != null ){
  34. var sFirstChar = "", stMatchSets = {}, row = [];
  35. // no url was specified, we need to adjust the cache length to make sure it fits the local data store
  36. if( typeof options.url != "string" ) options.cacheLength = 1;
  37. // loop through the array and create a lookup structure
  38. for( var i=0; i < options.data.length; i++ ){
  39. // if row is a string, make an array otherwise just reference the array
  40. row = ((typeof options.data[i] == "string") ? [options.data[i]] : options.data[i]);
  41. // if the length is zero, don't add to list
  42. if( row[0].length > 0 ){
  43. // get the first character
  44. sFirstChar = row[0].substring(0, 1).toLowerCase();
  45. // if no lookup array for this character exists, look it up now
  46. if( !stMatchSets[sFirstChar] ) stMatchSets[sFirstChar] = [];
  47. // if the match is a string
  48. stMatchSets[sFirstChar].push(row);
  49. }
  50. }
  51. // add the data items to the cache
  52. for( var k in stMatchSets ){
  53. // increase the cache size
  54. options.cacheLength++;
  55. // add to the cache
  56. addToCache(k, stMatchSets[k]);
  57. }
  58. }
  59. $input
  60. .keydown(function(e) {
  61. // track last key pressed
  62. lastKeyPressCode = e.keyCode;
  63. switch(e.keyCode) {
  64. case 38: // up
  65. e.preventDefault();
  66. moveSelect(-1);
  67. break;
  68. case 40: // down
  69. e.preventDefault();
  70. moveSelect(1);
  71. break;
  72. case 9: // tab
  73. case 13: // return
  74. if( selectCurrent() ){
  75. // make sure to blur off the current field
  76. $input.get(0).blur();
  77. e.preventDefault();
  78. }
  79. break;
  80. default:
  81. active = -1;
  82. if (timeout) clearTimeout(timeout);
  83. timeout = setTimeout(function(){onChange();}, options.delay);
  84. break;
  85. }
  86. })
  87. .focus(function(){
  88. // track whether the field has focus, we shouldn't process any results if the field no longer has focus
  89. hasFocus = true;
  90. })
  91. .blur(function() {
  92. // track whether the field has focus
  93. hasFocus = false;
  94. hideResults();
  95. });
  96. hideResultsNow();
  97. function onChange() {
  98. // ignore if the following keys are pressed: [del] [shift] [capslock]
  99. if( lastKeyPressCode == 46 || (lastKeyPressCode > 8 && lastKeyPressCode < 32) ) return $results.hide();
  100. var v = $input.val();
  101. if (v == prev) return;
  102. prev = v;
  103. if (v.length >= options.minChars) {
  104. $input.addClass(options.loadingClass);
  105. requestData(v);
  106. } else {
  107. $input.removeClass(options.loadingClass);
  108. $results.hide();
  109. }
  110. };
  111. function moveSelect(step) {
  112. var lis = $("li", results);
  113. if (!lis) return;
  114. active += step;
  115. if (active < 0) {
  116. active = 0;
  117. } else if (active >= lis.size()) {
  118. active = lis.size() - 1;
  119. }
  120. lis.removeClass("ac_over");
  121. $(lis[active]).addClass("ac_over");
  122. // Weird behaviour in IE
  123. // if (lis[active] && lis[active].scrollIntoView) {
  124. // lis[active].scrollIntoView(false);
  125. // }
  126. };
  127. function selectCurrent() {
  128. var li = $("li.ac_over", results)[0];
  129. if (!li) {
  130. var $li = $("li", results);
  131. if (options.selectOnly) {
  132. if ($li.length == 1) li = $li[0];
  133. } else if (options.selectFirst) {
  134. li = $li[0];
  135. }
  136. }
  137. if (li) {
  138. selectItem(li);
  139. return true;
  140. } else {
  141. return false;
  142. }
  143. };
  144. function selectItem(li) {
  145. if (!li) {
  146. li = document.createElement("li");
  147. li.extra = [];
  148. li.selectValue = "";
  149. }
  150. var v = $.trim(li.selectValue ? li.selectValue : li.innerHTML);
  151. input.lastSelected = v;
  152. prev = v;
  153. $results.html("");
  154. $input.val(v);
  155. hideResultsNow();
  156. if (options.onItemSelect) setTimeout(function() { options.onItemSelect(li) }, 1);
  157. };
  158. // selects a portion of the input string
  159. function createSelection(start, end){
  160. // get a reference to the input element
  161. var field = $input.get(0);
  162. if( field.createTextRange ){
  163. var selRange = field.createTextRange();
  164. selRange.collapse(true);
  165. selRange.moveStart("character", start);
  166. selRange.moveEnd("character", end);
  167. selRange.select();
  168. } else if( field.setSelectionRange ){
  169. field.setSelectionRange(start, end);
  170. } else {
  171. if( field.selectionStart ){
  172. field.selectionStart = start;
  173. field.selectionEnd = end;
  174. }
  175. }
  176. field.focus();
  177. };
  178. // fills in the input box w/the first match (assumed to be the best match)
  179. function autoFill(sValue){
  180. // if the last user key pressed was backspace, don't autofill
  181. if( lastKeyPressCode != 8 ){
  182. // fill in the value (keep the case the user has typed)
  183. $input.val($input.val() + sValue.substring(prev.length));
  184. // select the portion of the value not typed by the user (so the next character will erase)
  185. createSelection(prev.length, sValue.length);
  186. }
  187. };
  188. function showResults() {
  189. // get the position of the input field right now (in case the DOM is shifted)
  190. var pos = findPos(input);
  191. // either use the specified width, or autocalculate based on form element
  192. var iWidth = (options.width > 0) ? options.width : $input.width();
  193. // reposition
  194. $results.css({
  195. width: parseInt(iWidth) + "px",
  196. top: (pos.y + input.offsetHeight) + "px",
  197. left: pos.x + "px"
  198. }).show();
  199. };
  200. function hideResults() {
  201. if (timeout) clearTimeout(timeout);
  202. timeout = setTimeout(hideResultsNow, 200);
  203. };
  204. function hideResultsNow() {
  205. if (timeout) clearTimeout(timeout);
  206. $input.removeClass(options.loadingClass);
  207. if ($results.is(":visible")) {
  208. $results.hide();
  209. }
  210. if (options.mustMatch) {
  211. var v = $input.val();
  212. if (v != input.lastSelected) {
  213. selectItem(null);
  214. }
  215. }
  216. };
  217. function receiveData(q, data) {
  218. if (data) {
  219. $input.removeClass(options.loadingClass);
  220. results.innerHTML = "";
  221. // if the field no longer has focus or if there are no matches, do not display the drop down
  222. if( !hasFocus || data.length == 0 ) return hideResultsNow();
  223. if ($.browser.msie) {
  224. // we put a styled iframe behind the calendar so HTML SELECT elements don't show through
  225. $results.append(document.createElement('iframe'));
  226. }
  227. results.appendChild(dataToDom(data));
  228. // autofill in the complete box w/the first match as long as the user hasn't entered in more data
  229. if( options.autoFill && ($input.val().toLowerCase() == q.toLowerCase()) ) autoFill(data[0][0]);
  230. showResults();
  231. } else {
  232. hideResultsNow();
  233. }
  234. };
  235. function parseData(data) {
  236. if (!data) return null;
  237. var parsed = [];
  238. var rows = data.split(options.lineSeparator);
  239. for (var i=0; i < rows.length; i++) {
  240. var row = $.trim(rows[i]);
  241. if (row) {
  242. parsed[parsed.length] = row.split(options.cellSeparator);
  243. }
  244. }
  245. return parsed;
  246. };
  247. function dataToDom(data) {
  248. var ul = document.createElement("ul");
  249. var num = data.length;
  250. // limited results to a max number
  251. if( (options.maxItemsToShow > 0) && (options.maxItemsToShow < num) ) num = options.maxItemsToShow;
  252. for (var i=0; i < num; i++) {
  253. var row = data[i];
  254. if (!row) continue;
  255. var li = document.createElement("li");
  256. if (options.formatItem) {
  257. li.innerHTML = options.formatItem(row, i, num);
  258. li.selectValue = row[0];
  259. } else {
  260. li.innerHTML = row[0];
  261. li.selectValue = row[0];
  262. }
  263. var extra = null;
  264. if (row.length > 1) {
  265. extra = [];
  266. for (var j=1; j < row.length; j++) {
  267. extra[extra.length] = row[j];
  268. }
  269. }
  270. li.extra = extra;
  271. ul.appendChild(li);
  272. $(li).hover(
  273. function() { $("li", ul).removeClass("ac_over"); $(this).addClass("ac_over"); active = $("li", ul).indexOf($(this).get(0)); },
  274. function() { $(this).removeClass("ac_over"); }
  275. ).click(function(e) { e.preventDefault(); e.stopPropagation(); selectItem(this) });
  276. }
  277. return ul;
  278. };
  279. function requestData(q) {
  280. if (!options.matchCase) q = q.toLowerCase();
  281. var data = options.cacheLength ? loadFromCache(q) : null;
  282. // recieve the cached data
  283. if (data) {
  284. receiveData(q, data);
  285. // if an AJAX url has been supplied, try loading the data now
  286. } else if( (typeof options.url == "string") && (options.url.length > 0) ){
  287. $.get(makeUrl(q), function(data) {
  288. data = parseData(data);
  289. addToCache(q, data);
  290. receiveData(q, data);
  291. });
  292. // if there's been no data found, remove the loading class
  293. } else {
  294. $input.removeClass(options.loadingClass);
  295. }
  296. };
  297. function makeUrl(q) {
  298. var url = options.url + "?q=" + encodeURI(q);
  299. for (var i in options.extraParams) {
  300. url += "&" + i + "=" + encodeURI(options.extraParams[i]);
  301. }
  302. return url;
  303. };
  304. function loadFromCache(q) {
  305. if (!q) return null;
  306. if (cache.data[q]) return cache.data[q];
  307. if (options.matchSubset) {
  308. for (var i = q.length - 1; i >= options.minChars; i--) {
  309. var qs = q.substr(0, i);
  310. var c = cache.data[qs];
  311. if (c) {
  312. var csub = [];
  313. for (var j = 0; j < c.length; j++) {
  314. var x = c[j];
  315. var x0 = x[0];
  316. if (matchSubset(x0, q)) {
  317. csub[csub.length] = x;
  318. }
  319. }
  320. return csub;
  321. }
  322. }
  323. }
  324. return null;
  325. };
  326. function matchSubset(s, sub) {
  327. if (!options.matchCase) s = s.toLowerCase();
  328. var i = s.indexOf(sub);
  329. if (i == -1) return false;
  330. return i == 0 || options.matchContains;
  331. };
  332. this.flushCache = function() {
  333. flushCache();
  334. };
  335. this.setExtraParams = function(p) {
  336. options.extraParams = p;
  337. };
  338. this.findValue = function(){
  339. var q = $input.val();
  340. if (!options.matchCase) q = q.toLowerCase();
  341. var data = options.cacheLength ? loadFromCache(q) : null;
  342. if (data) {
  343. findValueCallback(q, data);
  344. } else if( (typeof options.url == "string") && (options.url.length > 0) ){
  345. $.get(makeUrl(q), function(data) {
  346. data = parseData(data)
  347. addToCache(q, data);
  348. findValueCallback(q, data);
  349. });
  350. } else {
  351. // no matches
  352. findValueCallback(q, null);
  353. }
  354. }
  355. function findValueCallback(q, data){
  356. if (data) $input.removeClass(options.loadingClass);
  357. var num = (data) ? data.length : 0;
  358. var li = null;
  359. for (var i=0; i < num; i++) {
  360. var row = data[i];
  361. if( row[0].toLowerCase() == q.toLowerCase() ){
  362. li = document.createElement("li");
  363. if (options.formatItem) {
  364. li.innerHTML = options.formatItem(row, i, num);
  365. li.selectValue = row[0];
  366. } else {
  367. li.innerHTML = row[0];
  368. li.selectValue = row[0];
  369. }
  370. var extra = null;
  371. if( row.length > 1 ){
  372. extra = [];
  373. for (var j=1; j < row.length; j++) {
  374. extra[extra.length] = row[j];
  375. }
  376. }
  377. li.extra = extra;
  378. }
  379. }
  380. if( options.onFindValue ) setTimeout(function() { options.onFindValue(li) }, 1);
  381. }
  382. function addToCache(q, data) {
  383. if (!data || !q || !options.cacheLength) return;
  384. if (!cache.length || cache.length > options.cacheLength) {
  385. flushCache();
  386. cache.length++;
  387. } else if (!cache[q]) {
  388. cache.length++;
  389. }
  390. cache.data[q] = data;
  391. };
  392. function findPos(obj) {
  393. var curleft = obj.offsetLeft || 0;
  394. var curtop = obj.offsetTop || 0;
  395. while (obj = obj.offsetParent) {
  396. curleft += obj.offsetLeft
  397. curtop += obj.offsetTop
  398. }
  399. return {x:curleft,y:curtop};
  400. }
  401. }
  402. jQuery.fn.autocomplete = function(url, options, data) {
  403. // Make sure options exists
  404. options = options || {};
  405. // Set url as option
  406. options.url = url;
  407. // set some bulk local data
  408. options.data = ((typeof data == "object") && (data.constructor == Array)) ? data : null;
  409. // Set default values for required options
  410. options.inputClass = options.inputClass || "ac_input";
  411. options.resultsClass = options.resultsClass || "ac_results";
  412. options.lineSeparator = options.lineSeparator || "\n";
  413. options.cellSeparator = options.cellSeparator || "|";
  414. options.minChars = options.minChars || 1;
  415. options.delay = options.delay || 400;
  416. options.matchCase = options.matchCase || 0;
  417. options.matchSubset = options.matchSubset || 1;
  418. options.matchContains = options.matchContains || 0;
  419. options.cacheLength = options.cacheLength || 1;
  420. options.mustMatch = options.mustMatch || 0;
  421. options.extraParams = options.extraParams || {};
  422. options.loadingClass = options.loadingClass || "ac_loading";
  423. options.selectFirst = options.selectFirst || false;
  424. options.selectOnly = options.selectOnly || false;
  425. options.maxItemsToShow = options.maxItemsToShow || -1;
  426. options.autoFill = options.autoFill || false;
  427. options.width = parseInt(options.width, 10) || 0;
  428. this.each(function() {
  429. var input = this;
  430. new jQuery.autocomplete(input, options);
  431. });
  432. // Don't break the chain
  433. return this;
  434. }
  435. jQuery.fn.autocompleteArray = function(data, options) {
  436. return this.autocomplete(null, options, data);
  437. }
  438. jQuery.fn.indexOf = function(e){
  439. for( var i=0; i<this.length; i++ ){
  440. if( this[i] == e ) return i;
  441. }
  442. return -1;
  443. };