/* * jQuery Autocomplete plugin 1.1 * * Copyright (c) 2009 Jörn Zaefferer * * Dual licensed under the MIT and GPL licenses: * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html * * Revision: $Id: jquery.autocomplete.js 15 2009-08-22 10:30:27Z joern.zaefferer $ */ ; (function ($) { $.fn.extend({ autocomplete: function (urlOrData, options) { var isUrl = typeof urlOrData == "string"; options = $.extend({}, $.Autocompleter.defaults, { url: isUrl ? urlOrData : null, data: isUrl ? null : urlOrData, delay: isUrl ? $.Autocompleter.defaults.delay : 10, max: options && !options.scroll ? 10 : 150 }, options); // if highlight is set to false, replace it with a do-nothing function options.highlight = options.highlight || function (value) { return value; }; // if the formatMatch option is not specified, then use formatItem for backwards compatibility options.formatMatch = options.formatMatch || options.formatItem; var sourceControl = this[0]; sourceControl.setDefaultValue = function (value, textField, valueField) { $.ajax({ // try to leverage ajaxQueue plugin to abort previous requests mode: "abort", // limit abortion to this input dataType: options.dataType, url: options.url, data: $.extend({ q: "", limit: options.max }, {}), success: function (data) { if (data && data.length) { for (var i = 0; i < data.length; i++) { if (data[i][valueField].toString().toLowerCase() == value) { $(sourceControl).val(data[i][textField]); break; } } } } }); }; return this.each(function () { new $.Autocompleter(this, options); }); }, result: function (handler) { return this.bind("result", handler); }, search: function (handler) { return this.trigger("search", [handler]); }, flushCache: function () { return this.trigger("flushCache"); }, setOptions: function (options) { return this.trigger("setOptions", [options]); }, unautocomplete: function () { return this.trigger("unautocomplete"); } }); $.Autocompleter = function (input, options) { var KEY = { UP: 38, DOWN: 40, DEL: 46, TAB: 9, RETURN: 13, ESC: 27, COMMA: 188, PAGEUP: 33, PAGEDOWN: 34, BACKSPACE: 8 }; // Create $ object for input element var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass); var timeout; var previousValue = ""; var cache = $.Autocompleter.Cache(options); var hasFocus = 0; var lastKeyPressCode; var config = { mouseDownOnSelect: false }; var select = $.Autocompleter.Select(options, input, selectCurrent, config); var blockSubmit; // prevent form submit in opera when selecting with return key $.browser.opera && $(input.form).bind("submit.autocomplete", function () { if (blockSubmit) { blockSubmit = false; return false; } }); // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function (event) { // a keypress means the input has focus // avoids issue where input had focus before the autocomplete was applied hasFocus = 1; // track last key pressed lastKeyPressCode = event.keyCode; switch (event.keyCode) { case KEY.UP: event.preventDefault(); if (select.visible()) { select.prev(); } else { onChange(0, true); } break; case KEY.DOWN: event.preventDefault(); if (select.visible()) { select.next(); } else { onChange(0, true); } break; case KEY.PAGEUP: event.preventDefault(); if (select.visible()) { select.pageUp(); } else { onChange(0, true); } break; case KEY.PAGEDOWN: event.preventDefault(); if (select.visible()) { select.pageDown(); } else { onChange(0, true); } break; // matches also semicolon case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA: case KEY.TAB: case KEY.RETURN: if (selectCurrent()) { // stop default to prevent a form submit, Opera needs special handling event.preventDefault(); blockSubmit = true; return false; } break; case KEY.ESC: select.hide(); break; case KEY.BACKSPACE: if (options.Backspace) { options.Backspace(); } clearTimeout(timeout); timeout = setTimeout(onChange, options.delay); break; default: // clearTimeout(timeout); // timeout = setTimeout(onChange, options.delay); break; } }).focus(function () { // track whether the field has focus, we shouldn't process any // results if the field no longer has focus hasFocus++; }).blur(function () { hasFocus = 0; if (!config.mouseDownOnSelect) { hideResults(); } }).click(function () { // show select when clicking in a focused field if (hasFocus++ > 1 && !select.visible()) { onChange(0, true); } }).bind("search", function () { // TODO why not just specifying both arguments? var fn = (arguments.length > 1) ? arguments[1] : null; function findValueCallback(q, data) { var result; if (data && data.length) { for (var i = 0; i < data.length; i++) { if (data[i].result.toLowerCase() == q.toLowerCase()) { result = data[i]; break; } } } if (typeof fn == "function") fn(result); else $input.trigger("result", result && [result.data, result.value]); } $.each(trimWords($input.val()), function (i, value) { request(value, findValueCallback, findValueCallback); }); }).bind("flushCache", function () { cache.flush(); }).bind("setOptions", function () { $.extend(options, arguments[1]); // if we've updated the data, repopulate if ("data" in arguments[1]) cache.populate(); }).bind("unautocomplete", function () { select.unbind(); $input.unbind(); $(input.form).unbind(".autocomplete"); }); function selectCurrent() { var selected = select.selected(); if (!selected) return false; var v = selected.result; previousValue = v; if (options.multiple) { var words = trimWords($input.val()); if (words.length > 1) { var seperator = options.multipleSeparator.length; var cursorAt = $(input).selection().start; var wordAt, progress = 0; $.each(words, function (i, word) { progress += word.length; if (cursorAt <= progress) { wordAt = i; return false; } progress += seperator; }); words[wordAt] = v; // TODO this should set the cursor to the right position, but it gets overriden somewhere //$.Autocompleter.Selection(input, progress + seperator, progress + seperator); v = words.join(options.multipleSeparator); } v += options.multipleSeparator; } $input.val(v); hideResultsNow(); $input.trigger("result", [selected.data, selected.value]); return true; } function onChange(crap, skipPrevCheck) { if (lastKeyPressCode == KEY.DEL) { select.hide(); return; } var currentValue = $input.val(); if (!skipPrevCheck && currentValue == previousValue) return; previousValue = currentValue; currentValue = lastWord(currentValue); if (currentValue.length >= options.minChars) { $input.addClass(options.loadingClass); if (!options.matchCase) currentValue = currentValue.toLowerCase(); request(currentValue, receiveData, hideResultsNow); } else { stopLoading(); select.hide(); } }; function trimWords(value) { if (!value) return [""]; if (!options.multiple) return [$.trim(value)]; return $.map(value.split(options.multipleSeparator), function (word) { return $.trim(value).length ? $.trim(word) : null; }); } function lastWord(value) { if (!options.multiple) return value; var words = trimWords(value); if (words.length == 1) return words[0]; var cursorAt = $(input).selection().start; if (cursorAt == value.length) { words = trimWords(value) } else { words = trimWords(value.replace(value.substring(cursorAt), "")); } return words[words.length - 1]; } // fills in the input box w/the first match (assumed to be the best match) // q: the term entered // sValue: the first matching result function autoFill(q, sValue) { // autofill in the complete box w/the first match as long as the user hasn't entered in more data // if the last user key pressed was backspace, don't autofill if (options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE) { // fill in the value (keep the case the user has typed) $input.val($input.val() + sValue.substring(lastWord(previousValue).length)); // select the portion of the value not typed by the user (so the next character will erase) $(input).selection(previousValue.length, previousValue.length + sValue.length); } }; function hideResults() { clearTimeout(timeout); timeout = setTimeout(hideResultsNow, 200); }; function hideResultsNow() { var wasVisible = select.visible(); select.hide(); clearTimeout(timeout); stopLoading(); if (options.mustMatch) { // call search and run callback $input.search( function (result) { // if no value found, clear the input box if (!result) { if (options.multiple) { var words = trimWords($input.val()).slice(0, -1); $input.val(words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "")); } else { $input.val(""); $input.trigger("result", null); } } } ); } }; function receiveData(q, data) { if (data && data.length && hasFocus) { stopLoading(); select.display(data, q); autoFill(q, data[0].value); select.show(); } else { hideResultsNow(); } }; function request(term, success, failure) { if (!options.matchCase) term = term.toLowerCase(); var data = cache.load(term); // recieve the cached data if (data && data.length) { success(term, data); // if an AJAX url has been supplied, try loading the data now } else if ((typeof options.url == "string") && (options.url.length > 0)) { var extraParams = { timestamp: +new Date() }; $.each(options.extraParams, function (key, param) { extraParams[key] = typeof param == "function" ? param() : param; }); //options.queryParamter; $.ajax({ // try to leverage ajaxQueue plugin to abort previous requests mode: "abort", // limit abortion to this input port: "autocomplete" + input.name, dataType: options.dataType, url: options.url, data: $.extend({ q: lastWord(term), limit: options.max }, extraParams), success: function (data) { var parsed = options.parse && options.parse(data) || parse(data); cache.add(term, parsed); success(term, parsed); } }); } else { // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match select.emptyList(); failure(term); } }; function parse(data) { var parsed = []; var rows = data.split("\n"); for (var i = 0; i < rows.length; i++) { var row = $.trim(rows[i]); if (row) { row = row.split("|"); parsed[parsed.length] = { data: row, value: row[0], result: options.formatResult && options.formatResult(row, row[0]) || row[0] }; } } return parsed; }; function stopLoading() { $input.removeClass(options.loadingClass); }; }; $.Autocompleter.defaults = { inputClass: "x-form-field", resultsClass: "ac_results", loadingClass: "ac_loading", minChars: 1, delay: 400, matchCase: false, matchSubset: true, matchContains: false, cacheLength: 10, valueField: "value", textField: "text", max: 100, mustMatch: false, extraParams: {}, selectFirst: true, formatItem: function (row) { return row[this.textField]; }, formatMatch: null, autoFill: false, width: 0, multiple: false, parse: function (data) { /*默认以Json格式*/ var parsed = []; if (data && data != "") { var obj = JSON.parse(data); for (var i = 0; i < obj.length; i++) { if (obj[i]) { parsed[parsed.length] = { data: obj[i], value: obj[i][this.valueField], result: this.formatResult && this.formatResult(obj[i], obj[i]) || obj[i][this.textField] }; } } } return parsed; }, multipleSeparator: ", ", highlight: function (value, term) { return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); }, scroll: true, scrollHeight: 180 }; $.Autocompleter.Cache = function (options) { var data = {}; var length = 0; function matchSubset(s, sub) { if (!options.matchCase) s = s.toLowerCase(); var i = s.indexOf(sub); if (options.matchContains == "word") { i = s.toLowerCase().search("\\b" + sub.toLowerCase()); } if (i == -1) return false; return i == 0 || options.matchContains; }; function add(q, value) { if (length > options.cacheLength) { flush(); } if (!data[q]) { length++; } data[q] = value; } function populate() { if (!options.data) return false; // track the matches var stMatchSets = {}, nullData = 0; // no url was specified, we need to adjust the cache length to make sure it fits the local data store if (!options.url) options.cacheLength = 1; // track all options for minChars = 0 stMatchSets[""] = []; // loop through the array and create a lookup structure for (var i = 0, ol = options.data.length; i < ol; i++) { var rawValue = options.data[i]; // if rawValue is a string, make an array otherwise just reference the array rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue; var value = options.formatMatch(rawValue, i + 1, options.data.length); if (value === false) continue; var firstChar = value.charAt(0).toLowerCase(); // if no lookup array for this character exists, look it up now if (!stMatchSets[firstChar]) stMatchSets[firstChar] = []; // if the match is a string var row = { value: value, data: rawValue, result: options.formatResult && options.formatResult(rawValue) || value }; // push the current match into the set list stMatchSets[firstChar].push(row); // keep track of minChars zero items if (nullData++ < options.max) { stMatchSets[""].push(row); } }; // add the data items to the cache $.each(stMatchSets, function (i, value) { // increase the cache size options.cacheLength++; // add to the cache add(i, value); }); } // populate any existing data setTimeout(populate, 25); function flush() { data = {}; length = 0; } return { flush: flush, add: add, populate: populate, load: function (q) { if (!options.cacheLength || !length) return null; /* * if dealing w/local data and matchContains than we must make sure * to loop through all the data collections looking for matches */ if (!options.url && options.matchContains) { // track all matches var csub = []; // loop through all the data grids for matches for (var k in data) { // don't search through the stMatchSets[""] (minChars: 0) cache // this prevents duplicates if (k.length > 0) { var c = data[k]; $.each(c, function (i, x) { // if we've got a match, add it to the array if (matchSubset(x.value, q)) { csub.push(x); } }); } } return csub; } else // if the exact item exists, use it if (data[q]) { return data[q]; } else if (options.matchSubset) { for (var i = q.length - 1; i >= options.minChars; i--) { var c = data[q.substr(0, i)]; if (c) { var csub = []; $.each(c, function (i, x) { if (matchSubset(x.value, q)) { csub[csub.length] = x; } }); return csub; } } } return null; } }; }; $.Autocompleter.Select = function (options, input, select, config) { var CLASSES = { ACTIVE: "ac_over" }; var listItems, active = -1, data, term = "", needsInit = true, element, list; // Create results function init() { if (!needsInit) return; element = $("
") .hide() .addClass(options.resultsClass) .css("position", "absolute") .appendTo(document.body); list = $("