; (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; // Get the input element options.inputElement = this; return this.each(function() { new $.Autocompleter(this, options); }); }, result : function(handler) { return this.bind("result", handler); }, itemSelected : function(handler) { return this.bind("itemSelected", 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" : "keyup") + ".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; 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]); // TODO $input.trigger("itemSelected", [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 // sId: the first matching id function autoFill(q, sValue, sId) { // 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); if (options.hiddenId) { $("#" + options.hiddenId).val(sId); } } }; 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); autoFill(q, data[0].value, data[0].id); 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; }); $.ajax({ // try to leverage ajaxQueue plugin to abort // previous requests mode : "abort", type : 'POST', // limit abortion to this input port : "autocomplete" + input.name, dataType : options.dataType ? options.dataType : "json", url : options.url, data : $.extend({ key : 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); }, error : function(req, mes, error) { // alert("涓庢湇鍔″櫒閫氫俊澶辫触"); alert("涓庢湇鍔″櫒閫氫俊澶辫触锛? + mes); } }); } 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 : "ac_input", resultsClass : "fuzzy_search", loadingClass : "ac_loading", minChars : 1, delay : 200, matchCase : false, matchSubset : true, matchContains : true, cacheLength : 200, max : 200, mustMatch : false, extraParams : { "type" : "general" }, selectFirst : true, formatItem: function (row, i, max) {return row.macType;}, formatMatch: function (row, i, max) {return row.id + " " + row.macType;}, formatResult:function (row) {return row.macType;}, parse:function (data) { return $.map(data, function (row) { return {data:row, value:row.macType,result:row.macType,id:row.id}; }); }, autoFill : false, // 鑷姩濉厖 width : 206, multiple : false, multipleSeparator : ", ", highlight : function(value, term) { return value .replace( new RegExp( "(?![^&;]+;)(?!<[^<>]*)(" + term .replace( /([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); }, scroll : true, scrollHeight : 180, hiddenId : null }; $.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 = $("