jquery.autocomplete.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. /*
  2. * Autocomplete - jQuery plugin 1.0.2
  3. *
  4. * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
  5. *
  6. * Dual licensed under the MIT and GPL licenses:
  7. * http://www.opensource.org/licenses/mit-license.php
  8. * http://www.gnu.org/licenses/gpl.html
  9. *
  10. * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
  11. *
  12. */
  13. ;(function($) {
  14. $.fn.extend({
  15. autocomplete: function(urlOrData, options) {
  16. var isUrl = typeof urlOrData == "string";
  17. options = $.extend({}, $.Autocompleter.defaults, {
  18. url: isUrl ? urlOrData : null,
  19. data: isUrl ? null : urlOrData,
  20. delay: isUrl ? $.Autocompleter.defaults.delay : 10,
  21. max: options && !options.scroll ? 10 : 150
  22. }, options);
  23. // if highlight is set to false, replace it with a do-nothing function
  24. options.highlight = options.highlight || function(value) { return value; };
  25. // if the formatMatch option is not specified, then use formatItem for backwards compatibility
  26. options.formatMatch = options.formatMatch || options.formatItem;
  27. return this.each(function() {
  28. new $.Autocompleter(this, options);
  29. });
  30. },
  31. result: function(handler) {
  32. return this.bind("result", handler);
  33. },
  34. search: function(handler) {
  35. return this.trigger("search", [handler]);
  36. },
  37. flushCache: function() {
  38. return this.trigger("flushCache");
  39. },
  40. setOptions: function(options){
  41. return this.trigger("setOptions", [options]);
  42. },
  43. unautocomplete: function() {
  44. return this.trigger("unautocomplete");
  45. }
  46. });
  47. $.Autocompleter = function(input, options) {
  48. var KEY = {
  49. UP: 38,
  50. DOWN: 40,
  51. DEL: 46,
  52. TAB: 9,
  53. RETURN: 13,
  54. ESC: 27,
  55. COMMA: 188,
  56. PAGEUP: 33,
  57. PAGEDOWN: 34,
  58. BACKSPACE: 8
  59. };
  60. // Create $ object for input element
  61. var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
  62. var timeout;
  63. var previousValue = "";
  64. var cache = $.Autocompleter.Cache(options);
  65. var hasFocus = 0;
  66. var lastKeyPressCode;
  67. var config = {
  68. mouseDownOnSelect: false
  69. };
  70. var select = $.Autocompleter.Select(options, input, selectCurrent, config);
  71. var blockSubmit;
  72. // prevent form submit in opera when selecting with return key
  73. $.browser.opera && $(input.form).bind("submit.autocomplete", function() {
  74. if (blockSubmit) {
  75. blockSubmit = false;
  76. return false;
  77. }
  78. });
  79. // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
  80. $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
  81. // track last key pressed
  82. lastKeyPressCode = event.keyCode;
  83. switch(event.keyCode) {
  84. case KEY.UP:
  85. event.preventDefault();
  86. if ( select.visible() ) {
  87. select.prev();
  88. } else {
  89. onChange(0, true);
  90. }
  91. break;
  92. case KEY.DOWN:
  93. event.preventDefault();
  94. if ( select.visible() ) {
  95. select.next();
  96. } else {
  97. onChange(0, true);
  98. }
  99. break;
  100. case KEY.PAGEUP:
  101. event.preventDefault();
  102. if ( select.visible() ) {
  103. select.pageUp();
  104. } else {
  105. onChange(0, true);
  106. }
  107. break;
  108. case KEY.PAGEDOWN:
  109. event.preventDefault();
  110. if ( select.visible() ) {
  111. select.pageDown();
  112. } else {
  113. onChange(0, true);
  114. }
  115. break;
  116. // matches also semicolon
  117. case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
  118. case KEY.TAB:
  119. case KEY.RETURN:
  120. if( selectCurrent() ) {
  121. // stop default to prevent a form submit, Opera needs special handling
  122. event.preventDefault();
  123. blockSubmit = true;
  124. return false;
  125. }
  126. break;
  127. case KEY.ESC:
  128. select.hide();
  129. break;
  130. default:
  131. clearTimeout(timeout);
  132. timeout = setTimeout(onChange, options.delay);
  133. break;
  134. }
  135. }).focus(function(){
  136. // track whether the field has focus, we shouldn't process any
  137. // results if the field no longer has focus
  138. hasFocus++;
  139. }).blur(function() {
  140. hasFocus = 0;
  141. if (!config.mouseDownOnSelect) {
  142. hideResults();
  143. }
  144. }).click(function() {
  145. // show select when clicking in a focused field
  146. if ( hasFocus++ > 1 && !select.visible() ) {
  147. onChange(0, true);
  148. }
  149. }).bind("search", function() {
  150. // TODO why not just specifying both arguments?
  151. var fn = (arguments.length > 1) ? arguments[1] : null;
  152. function findValueCallback(q, data) {
  153. var result;
  154. if( data && data.length ) {
  155. for (var i=0; i < data.length; i++) {
  156. if( data[i].result.toLowerCase() == q.toLowerCase() ) {
  157. result = data[i];
  158. break;
  159. }
  160. }
  161. }
  162. if( typeof fn == "function" ) fn(result);
  163. else $input.trigger("result", result && [result.data, result.value]);
  164. }
  165. $.each(trimWords($input.val()), function(i, value) {
  166. request(value, findValueCallback, findValueCallback);
  167. });
  168. }).bind("flushCache", function() {
  169. cache.flush();
  170. }).bind("setOptions", function() {
  171. $.extend(options, arguments[1]);
  172. // if we've updated the data, repopulate
  173. if ( "data" in arguments[1] )
  174. cache.populate();
  175. }).bind("unautocomplete", function() {
  176. select.unbind();
  177. $input.unbind();
  178. $(input.form).unbind(".autocomplete");
  179. });
  180. function selectCurrent() {
  181. var selected = select.selected();
  182. if( !selected )
  183. return false;
  184. var v = selected.result;
  185. previousValue = v;
  186. if ( options.multiple ) {
  187. var words = trimWords($input.val());
  188. if ( words.length > 1 ) {
  189. v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
  190. }
  191. v += options.multipleSeparator;
  192. }
  193. $input.val(v);
  194. hideResultsNow();
  195. $input.trigger("result", [selected.data, selected.value]);
  196. return true;
  197. }
  198. function onChange(crap, skipPrevCheck) {
  199. if( lastKeyPressCode == KEY.DEL ) {
  200. select.hide();
  201. return;
  202. }
  203. var currentValue = $input.val();
  204. if ( !skipPrevCheck && currentValue == previousValue )
  205. return;
  206. previousValue = currentValue;
  207. currentValue = lastWord(currentValue);
  208. if ( currentValue.length >= options.minChars) {
  209. $input.addClass(options.loadingClass);
  210. if (!options.matchCase)
  211. currentValue = currentValue.toLowerCase();
  212. request(currentValue, receiveData, hideResultsNow);
  213. } else {
  214. stopLoading();
  215. select.hide();
  216. }
  217. };
  218. function trimWords(value) {
  219. if ( !value ) {
  220. return [""];
  221. }
  222. var words = value.split( options.multipleSeparator );
  223. var result = [];
  224. $.each(words, function(i, value) {
  225. if ( $.trim(value) )
  226. result[i] = $.trim(value);
  227. });
  228. return result;
  229. }
  230. function lastWord(value) {
  231. if ( !options.multiple )
  232. return value;
  233. var words = trimWords(value);
  234. return words[words.length - 1];
  235. }
  236. // fills in the input box w/the first match (assumed to be the best match)
  237. // q: the term entered
  238. // sValue: the first matching result
  239. function autoFill(q, sValue){
  240. // autofill in the complete box w/the first match as long as the user hasn't entered in more data
  241. // if the last user key pressed was backspace, don't autofill
  242. if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
  243. // fill in the value (keep the case the user has typed)
  244. $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
  245. // select the portion of the value not typed by the user (so the next character will erase)
  246. $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
  247. }
  248. };
  249. function hideResults() {
  250. clearTimeout(timeout);
  251. timeout = setTimeout(hideResultsNow, 200);
  252. };
  253. function hideResultsNow() {
  254. var wasVisible = select.visible();
  255. select.hide();
  256. clearTimeout(timeout);
  257. stopLoading();
  258. if (options.mustMatch) {
  259. // call search and run callback
  260. $input.search(
  261. function (result){
  262. // if no value found, clear the input box
  263. if( !result ) {
  264. if (options.multiple) {
  265. var words = trimWords($input.val()).slice(0, -1);
  266. $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
  267. }
  268. else
  269. $input.val( "" );
  270. }
  271. }
  272. );
  273. }
  274. if (wasVisible)
  275. // position cursor at end of input field
  276. $.Autocompleter.Selection(input, input.value.length, input.value.length);
  277. };
  278. function receiveData(q, data) {
  279. if ( data && data.length && hasFocus ) {
  280. stopLoading();
  281. select.display(data, q);
  282. autoFill(q, data[0].value);
  283. select.show();
  284. } else {
  285. hideResultsNow();
  286. }
  287. };
  288. function request(term, success, failure) {
  289. if (!options.matchCase)
  290. term = term.toLowerCase();
  291. var data = cache.load(term);
  292. // recieve the cached data
  293. if (data && data.length) {
  294. success(term, data);
  295. // if an AJAX url has been supplied, try loading the data now
  296. } else if( (typeof options.url == "string") && (options.url.length > 0) ){
  297. var extraParams = {
  298. timestamp: +new Date()
  299. };
  300. $.each(options.extraParams, function(key, param) {
  301. extraParams[key] = typeof param == "function" ? param() : param;
  302. });
  303. $.ajax({
  304. // try to leverage ajaxQueue plugin to abort previous requests
  305. mode: "abort",
  306. // limit abortion to this input
  307. port: "autocomplete" + input.name,
  308. dataType: options.dataType,
  309. url: options.url,
  310. data: $.extend({
  311. q: lastWord(term),
  312. limit: options.max
  313. }, extraParams),
  314. success: function(data) {
  315. var parsed = options.parse && options.parse(data) || parse(data);
  316. cache.add(term, parsed);
  317. success(term, parsed);
  318. }
  319. });
  320. } else {
  321. // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
  322. select.emptyList();
  323. failure(term);
  324. }
  325. };
  326. function parse(data) {
  327. var parsed = [];
  328. var rows = data.split("\n");
  329. for (var i=0; i < rows.length; i++) {
  330. var row = $.trim(rows[i]);
  331. if (row) {
  332. row = row.split("|");
  333. parsed[parsed.length] = {
  334. data: row,
  335. value: row[0],
  336. result: options.formatResult && options.formatResult(row, row[0]) || row[0]
  337. };
  338. }
  339. }
  340. return parsed;
  341. };
  342. function stopLoading() {
  343. $input.removeClass(options.loadingClass);
  344. };
  345. };
  346. $.Autocompleter.defaults = {
  347. inputClass: "ac_input",
  348. resultsClass: "ac_results",
  349. loadingClass: "ac_loading",
  350. minChars: 1,
  351. delay: 400,
  352. matchCase: false,
  353. matchSubset: true,
  354. matchContains: false,
  355. cacheLength: 10,
  356. max: 100,
  357. mustMatch: false,
  358. extraParams: {},
  359. selectFirst: true,
  360. formatItem: function(row) { return row[0]; },
  361. formatMatch: null,
  362. autoFill: false,
  363. width: 0,
  364. multiple: false,
  365. multipleSeparator: ", ",
  366. highlight: function(value, term) {
  367. return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
  368. },
  369. scroll: true,
  370. scrollHeight: 180
  371. };
  372. $.Autocompleter.Cache = function(options) {
  373. var data = {};
  374. var length = 0;
  375. function matchSubset(s, sub) {
  376. if (!options.matchCase)
  377. s = s.toLowerCase();
  378. var i = s.indexOf(sub);
  379. if (i == -1) return false;
  380. return i == 0 || options.matchContains;
  381. };
  382. function add(q, value) {
  383. if (length > options.cacheLength){
  384. flush();
  385. }
  386. if (!data[q]){
  387. length++;
  388. }
  389. data[q] = value;
  390. }
  391. function populate(){
  392. if( !options.data ) return false;
  393. // track the matches
  394. var stMatchSets = {},
  395. nullData = 0;
  396. // no url was specified, we need to adjust the cache length to make sure it fits the local data store
  397. if( !options.url ) options.cacheLength = 1;
  398. // track all options for minChars = 0
  399. stMatchSets[""] = [];
  400. // loop through the array and create a lookup structure
  401. for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
  402. var rawValue = options.data[i];
  403. // if rawValue is a string, make an array otherwise just reference the array
  404. rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
  405. var value = options.formatMatch(rawValue, i+1, options.data.length);
  406. if ( value === false )
  407. continue;
  408. var firstChar = value.charAt(0).toLowerCase();
  409. // if no lookup array for this character exists, look it up now
  410. if( !stMatchSets[firstChar] )
  411. stMatchSets[firstChar] = [];
  412. // if the match is a string
  413. var row = {
  414. value: value,
  415. data: rawValue,
  416. result: options.formatResult && options.formatResult(rawValue) || value
  417. };
  418. // push the current match into the set list
  419. stMatchSets[firstChar].push(row);
  420. // keep track of minChars zero items
  421. if ( nullData++ < options.max ) {
  422. stMatchSets[""].push(row);
  423. }
  424. };
  425. // add the data items to the cache
  426. $.each(stMatchSets, function(i, value) {
  427. // increase the cache size
  428. options.cacheLength++;
  429. // add to the cache
  430. add(i, value);
  431. });
  432. }
  433. // populate any existing data
  434. setTimeout(populate, 25);
  435. function flush(){
  436. data = {};
  437. length = 0;
  438. }
  439. return {
  440. flush: flush,
  441. add: add,
  442. populate: populate,
  443. load: function(q) {
  444. if (!options.cacheLength || !length)
  445. return null;
  446. /*
  447. * if dealing w/local data and matchContains than we must make sure
  448. * to loop through all the data collections looking for matches
  449. */
  450. if( !options.url && options.matchContains ){
  451. // track all matches
  452. var csub = [];
  453. // loop through all the data grids for matches
  454. for( var k in data ){
  455. // don't search through the stMatchSets[""] (minChars: 0) cache
  456. // this prevents duplicates
  457. if( k.length > 0 ){
  458. var c = data[k];
  459. $.each(c, function(i, x) {
  460. // if we've got a match, add it to the array
  461. if (matchSubset(x.value, q)) {
  462. csub.push(x);
  463. }
  464. });
  465. }
  466. }
  467. return csub;
  468. } else
  469. // if the exact item exists, use it
  470. if (data[q]){
  471. return data[q];
  472. } else
  473. if (options.matchSubset) {
  474. for (var i = q.length - 1; i >= options.minChars; i--) {
  475. var c = data[q.substr(0, i)];
  476. if (c) {
  477. var csub = [];
  478. $.each(c, function(i, x) {
  479. if (matchSubset(x.value, q)) {
  480. csub[csub.length] = x;
  481. }
  482. });
  483. return csub;
  484. }
  485. }
  486. }
  487. return null;
  488. }
  489. };
  490. };
  491. $.Autocompleter.Select = function (options, input, select, config) {
  492. var CLASSES = {
  493. ACTIVE: "ac_over"
  494. };
  495. var listItems,
  496. active = -1,
  497. data,
  498. term = "",
  499. needsInit = true,
  500. element,
  501. list;
  502. // Create results
  503. function init() {
  504. if (!needsInit)
  505. return;
  506. element = $("<div/>")
  507. .hide()
  508. .addClass(options.resultsClass)
  509. .css("position", "absolute")
  510. .appendTo(document.body);
  511. list = $("<ul/>").appendTo(element).mouseover( function(event) {
  512. if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
  513. active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
  514. $(target(event)).addClass(CLASSES.ACTIVE);
  515. }
  516. }).click(function(event) {
  517. $(target(event)).addClass(CLASSES.ACTIVE);
  518. select();
  519. // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
  520. input.focus();
  521. return false;
  522. }).mousedown(function() {
  523. config.mouseDownOnSelect = true;
  524. }).mouseup(function() {
  525. config.mouseDownOnSelect = false;
  526. });
  527. if( options.width > 0 )
  528. element.css("width", options.width);
  529. needsInit = false;
  530. }
  531. function target(event) {
  532. var element = event.target;
  533. while(element && element.tagName != "LI")
  534. element = element.parentNode;
  535. // more fun with IE, sometimes event.target is empty, just ignore it then
  536. if(!element)
  537. return [];
  538. return element;
  539. }
  540. function moveSelect(step) {
  541. listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
  542. movePosition(step);
  543. var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
  544. if(options.scroll) {
  545. var offset = 0;
  546. listItems.slice(0, active).each(function() {
  547. offset += this.offsetHeight;
  548. });
  549. if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
  550. list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
  551. } else if(offset < list.scrollTop()) {
  552. list.scrollTop(offset);
  553. }
  554. }
  555. };
  556. function movePosition(step) {
  557. active += step;
  558. if (active < 0) {
  559. active = listItems.size() - 1;
  560. } else if (active >= listItems.size()) {
  561. active = 0;
  562. }
  563. }
  564. function limitNumberOfItems(available) {
  565. return options.max && options.max < available
  566. ? options.max
  567. : available;
  568. }
  569. function fillList() {
  570. list.empty();
  571. var max = limitNumberOfItems(data.length);
  572. for (var i=0; i < max; i++) {
  573. if (!data[i])
  574. continue;
  575. var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
  576. if ( formatted === false )
  577. continue;
  578. var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
  579. $.data(li, "ac_data", data[i]);
  580. }
  581. listItems = list.find("li");
  582. if ( options.selectFirst ) {
  583. listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
  584. active = 0;
  585. }
  586. // apply bgiframe if available
  587. if ( $.fn.bgiframe )
  588. list.bgiframe();
  589. }
  590. return {
  591. display: function(d, q) {
  592. init();
  593. data = d;
  594. term = q;
  595. fillList();
  596. },
  597. next: function() {
  598. moveSelect(1);
  599. },
  600. prev: function() {
  601. moveSelect(-1);
  602. },
  603. pageUp: function() {
  604. if (active != 0 && active - 8 < 0) {
  605. moveSelect( -active );
  606. } else {
  607. moveSelect(-8);
  608. }
  609. },
  610. pageDown: function() {
  611. if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
  612. moveSelect( listItems.size() - 1 - active );
  613. } else {
  614. moveSelect(8);
  615. }
  616. },
  617. hide: function() {
  618. element && element.hide();
  619. listItems && listItems.removeClass(CLASSES.ACTIVE);
  620. active = -1;
  621. },
  622. visible : function() {
  623. return element && element.is(":visible");
  624. },
  625. current: function() {
  626. return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
  627. },
  628. show: function() {
  629. var offset = $(input).offset();
  630. element.css({
  631. width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
  632. top: offset.top + input.offsetHeight,
  633. left: offset.left
  634. }).show();
  635. if(options.scroll) {
  636. list.scrollTop(0);
  637. list.css({
  638. maxHeight: options.scrollHeight,
  639. overflow: 'auto'
  640. });
  641. if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
  642. var listHeight = 0;
  643. listItems.each(function() {
  644. listHeight += this.offsetHeight;
  645. });
  646. var scrollbarsVisible = listHeight > options.scrollHeight;
  647. list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
  648. if (!scrollbarsVisible) {
  649. // IE doesn't recalculate width when scrollbar disappears
  650. listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
  651. }
  652. }
  653. }
  654. },
  655. selected: function() {
  656. var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
  657. return selected && selected.length && $.data(selected[0], "ac_data");
  658. },
  659. emptyList: function (){
  660. list && list.empty();
  661. },
  662. unbind: function() {
  663. element && element.remove();
  664. }
  665. };
  666. };
  667. $.Autocompleter.Selection = function(field, start, end) {
  668. if( field.createTextRange ){
  669. var selRange = field.createTextRange();
  670. selRange.collapse(true);
  671. selRange.moveStart("character", start);
  672. selRange.moveEnd("character", end);
  673. selRange.select();
  674. } else if( field.setSelectionRange ){
  675. field.setSelectionRange(start, end);
  676. } else {
  677. if( field.selectionStart ){
  678. field.selectionStart = start;
  679. field.selectionEnd = end;
  680. }
  681. }
  682. field.focus();
  683. };
  684. })(jQuery);