Spaces:
Runtime error
Runtime error
| /** | |
| * @fileOverview jquery-autocomplete, the jQuery Autocompleter | |
| * @author <a href="mailto:dylan@dyve.net">Dylan Verheul</a> | |
| * @version 2.4.4 | |
| * @requires jQuery 1.6+ | |
| * @license MIT | GPL | Apache 2.0, see LICENSE.txt | |
| * @see https://github.com/dyve/jquery-autocomplete | |
| */ | |
| (function($) { | |
| "use strict"; | |
| /** | |
| * jQuery autocomplete plugin | |
| * @param {object|string} options | |
| * @returns (object} jQuery object | |
| */ | |
| $.fn.autocomplete = function(options) { | |
| var url; | |
| if (arguments.length > 1) { | |
| url = options; | |
| options = arguments[1]; | |
| options.url = url; | |
| } else if (typeof options === 'string') { | |
| url = options; | |
| options = { url: url }; | |
| } | |
| var opts = $.extend({}, $.fn.autocomplete.defaults, options); | |
| return this.each(function() { | |
| var $this = $(this); | |
| $this.data('autocompleter', new $.Autocompleter( | |
| $this, | |
| $.meta ? $.extend({}, opts, $this.data()) : opts | |
| )); | |
| }); | |
| }; | |
| /** | |
| * Store default options | |
| * @type {object} | |
| */ | |
| $.fn.autocomplete.defaults = { | |
| inputClass: 'acInput', | |
| loadingClass: 'acLoading', | |
| resultsClass: 'acResults', | |
| selectClass: 'acSelect', | |
| queryParamName: 'q', | |
| extraParams: {}, | |
| remoteDataType: false, | |
| lineSeparator: '\n', | |
| cellSeparator: '|', | |
| minChars: 2, | |
| maxItemsToShow: 10, | |
| delay: 400, | |
| useCache: true, | |
| maxCacheLength: 10, | |
| matchSubset: true, | |
| matchCase: false, | |
| matchInside: true, | |
| mustMatch: false, | |
| selectFirst: false, | |
| selectOnly: false, | |
| showResult: null, | |
| preventDefaultReturn: 1, | |
| preventDefaultTab: 0, | |
| autoFill: false, | |
| filterResults: true, | |
| filter: true, | |
| sortResults: true, | |
| sortFunction: null, | |
| onItemSelect: null, | |
| onNoMatch: null, | |
| onFinish: null, | |
| matchStringConverter: null, | |
| beforeUseConverter: null, | |
| autoWidth: 'min-width', | |
| useDelimiter: false, | |
| delimiterChar: ',', | |
| delimiterKeyCode: 188, | |
| processData: null, | |
| onError: null, | |
| enabled: true | |
| }; | |
| /** | |
| * Sanitize result | |
| * @param {Object} result | |
| * @returns {Object} object with members value (String) and data (Object) | |
| * @private | |
| */ | |
| var sanitizeResult = function(result) { | |
| var value, data; | |
| var type = typeof result; | |
| if (type === 'string') { | |
| value = result; | |
| data = {}; | |
| } else if ($.isArray(result)) { | |
| value = result[0]; | |
| data = result.slice(1); | |
| } else if (type === 'object') { | |
| value = result.value; | |
| data = result.data; | |
| } | |
| value = String(value); | |
| if (typeof data !== 'object') { | |
| data = {}; | |
| } | |
| return { | |
| value: value, | |
| data: data | |
| }; | |
| }; | |
| /** | |
| * Sanitize integer | |
| * @param {mixed} value | |
| * @param {Object} options | |
| * @returns {Number} integer | |
| * @private | |
| */ | |
| var sanitizeInteger = function(value, stdValue, options) { | |
| var num = parseInt(value, 10); | |
| options = options || {}; | |
| if (isNaN(num) || (options.min && num < options.min)) { | |
| num = stdValue; | |
| } | |
| return num; | |
| }; | |
| /** | |
| * Create partial url for a name/value pair | |
| */ | |
| var makeUrlParam = function(name, value) { | |
| return [name, encodeURIComponent(value)].join('='); | |
| }; | |
| /** | |
| * Build an url | |
| * @param {string} url Base url | |
| * @param {object} [params] Dictionary of parameters | |
| */ | |
| var makeUrl = function(url, params) { | |
| var urlAppend = []; | |
| $.each(params, function(index, value) { | |
| urlAppend.push(makeUrlParam(index, value)); | |
| }); | |
| if (urlAppend.length) { | |
| url += url.indexOf('?') === -1 ? '?' : '&'; | |
| url += urlAppend.join('&'); | |
| } | |
| return url; | |
| }; | |
| /** | |
| * Default sort filter | |
| * @param {object} a | |
| * @param {object} b | |
| * @param {boolean} matchCase | |
| * @returns {number} | |
| */ | |
| var sortValueAlpha = function(a, b, matchCase) { | |
| a = String(a.value); | |
| b = String(b.value); | |
| if (!matchCase) { | |
| a = a.toLowerCase(); | |
| b = b.toLowerCase(); | |
| } | |
| if (a > b) { | |
| return 1; | |
| } | |
| if (a < b) { | |
| return -1; | |
| } | |
| return 0; | |
| }; | |
| /** | |
| * Parse data received in text format | |
| * @param {string} text Plain text input | |
| * @param {string} lineSeparator String that separates lines | |
| * @param {string} cellSeparator String that separates cells | |
| * @returns {array} Array of autocomplete data objects | |
| */ | |
| var plainTextParser = function(text, lineSeparator, cellSeparator) { | |
| var results = []; | |
| var i, j, data, line, value, lines; | |
| // Be nice, fix linebreaks before splitting on lineSeparator | |
| lines = String(text).replace('\r\n', '\n').split(lineSeparator); | |
| for (i = 0; i < lines.length; i++) { | |
| line = lines[i].split(cellSeparator); | |
| data = []; | |
| for (j = 0; j < line.length; j++) { | |
| data.push(decodeURIComponent(line[j])); | |
| } | |
| value = data.shift(); | |
| results.push({ value: value, data: data }); | |
| } | |
| return results; | |
| }; | |
| /** | |
| * Autocompleter class | |
| * @param {object} $elem jQuery object with one input tag | |
| * @param {object} options Settings | |
| * @constructor | |
| */ | |
| $.Autocompleter = function($elem, options) { | |
| /** | |
| * Assert parameters | |
| */ | |
| if (!$elem || !($elem instanceof $) || $elem.length !== 1 || $elem.get(0).tagName.toUpperCase() !== 'INPUT') { | |
| throw new Error('Invalid parameter for jquery.Autocompleter, jQuery object with one element with INPUT tag expected.'); | |
| } | |
| /** | |
| * @constant Link to this instance | |
| * @type object | |
| * @private | |
| */ | |
| var self = this; | |
| /** | |
| * @property {object} Options for this instance | |
| * @public | |
| */ | |
| this.options = options; | |
| /** | |
| * @property object Cached data for this instance | |
| * @private | |
| */ | |
| this.cacheData_ = {}; | |
| /** | |
| * @property {number} Number of cached data items | |
| * @private | |
| */ | |
| this.cacheLength_ = 0; | |
| /** | |
| * @property {string} Class name to mark selected item | |
| * @private | |
| */ | |
| this.selectClass_ = 'jquery-autocomplete-selected-item'; | |
| /** | |
| * @property {number} Handler to activation timeout | |
| * @private | |
| */ | |
| this.keyTimeout_ = null; | |
| /** | |
| * @property {number} Handler to finish timeout | |
| * @private | |
| */ | |
| this.finishTimeout_ = null; | |
| /** | |
| * @property {number} Last key pressed in the input field (store for behavior) | |
| * @private | |
| */ | |
| this.lastKeyPressed_ = null; | |
| /** | |
| * @property {string} Last value processed by the autocompleter | |
| * @private | |
| */ | |
| this.lastProcessedValue_ = null; | |
| /** | |
| * @property {string} Last value selected by the user | |
| * @private | |
| */ | |
| this.lastSelectedValue_ = null; | |
| /** | |
| * @property {boolean} Is this autocompleter active (showing results)? | |
| * @see showResults | |
| * @private | |
| */ | |
| this.active_ = false; | |
| /** | |
| * @property {boolean} Is this autocompleter allowed to finish on blur? | |
| * @private | |
| */ | |
| this.finishOnBlur_ = true; | |
| /** | |
| * Sanitize options | |
| */ | |
| this.options.minChars = sanitizeInteger(this.options.minChars, $.fn.autocomplete.defaults.minChars, { min: 0 }); | |
| this.options.maxItemsToShow = sanitizeInteger(this.options.maxItemsToShow, $.fn.autocomplete.defaults.maxItemsToShow, { min: 0 }); | |
| this.options.maxCacheLength = sanitizeInteger(this.options.maxCacheLength, $.fn.autocomplete.defaults.maxCacheLength, { min: 1 }); | |
| this.options.delay = sanitizeInteger(this.options.delay, $.fn.autocomplete.defaults.delay, { min: 0 }); | |
| if (this.options.preventDefaultReturn != 2) { | |
| this.options.preventDefaultReturn = this.options.preventDefaultReturn ? 1 : 0; | |
| } | |
| if (this.options.preventDefaultTab != 2) { | |
| this.options.preventDefaultTab = this.options.preventDefaultTab ? 1 : 0; | |
| } | |
| /** | |
| * Init DOM elements repository | |
| */ | |
| this.dom = {}; | |
| /** | |
| * Store the input element we're attached to in the repository | |
| */ | |
| this.dom.$elem = $elem; | |
| /** | |
| * Switch off the native autocomplete and add the input class | |
| */ | |
| this.dom.$elem.attr('autocomplete', 'off').addClass(this.options.inputClass); | |
| /** | |
| * Create DOM element to hold results, and force absolute position | |
| */ | |
| this.dom.$results = $('<div></div>').hide().addClass(this.options.resultsClass).css({ | |
| position: 'absolute' | |
| }); | |
| $('body').append(this.dom.$results); | |
| /** | |
| * Attach keyboard monitoring to $elem | |
| */ | |
| $elem.keydown(function(e) { | |
| self.lastKeyPressed_ = e.keyCode; | |
| switch(self.lastKeyPressed_) { | |
| case self.options.delimiterKeyCode: // comma = 188 | |
| if (self.options.useDelimiter && self.active_) { | |
| self.selectCurrent(); | |
| } | |
| break; | |
| // ignore navigational & special keys | |
| case 35: // end | |
| case 36: // home | |
| case 16: // shift | |
| case 17: // ctrl | |
| case 18: // alt | |
| case 37: // left | |
| case 39: // right | |
| break; | |
| case 38: // up | |
| e.preventDefault(); | |
| if (self.active_) { | |
| self.focusPrev(); | |
| } else { | |
| self.activate(); | |
| } | |
| return false; | |
| case 40: // down | |
| e.preventDefault(); | |
| if (self.active_) { | |
| self.focusNext(); | |
| } else { | |
| self.activate(); | |
| } | |
| return false; | |
| case 9: // tab | |
| if (self.active_) { | |
| self.selectCurrent(); | |
| if (self.options.preventDefaultTab) { | |
| e.preventDefault(); | |
| return false; | |
| } | |
| } | |
| if (self.options.preventDefaultTab === 2) { | |
| e.preventDefault(); | |
| return false; | |
| } | |
| break; | |
| case 13: // return | |
| if (self.active_) { | |
| self.selectCurrent(); | |
| if (self.options.preventDefaultReturn) { | |
| e.preventDefault(); | |
| return false; | |
| } | |
| } | |
| if (self.options.preventDefaultReturn === 2) { | |
| e.preventDefault(); | |
| return false; | |
| } | |
| break; | |
| case 27: // escape | |
| if (self.active_) { | |
| e.preventDefault(); | |
| self.deactivate(true); | |
| return false; | |
| } | |
| break; | |
| default: | |
| self.activate(); | |
| } | |
| }); | |
| /** | |
| * Attach paste event listener because paste may occur much later then keydown or even without a keydown at all | |
| */ | |
| $elem.on('paste', function() { | |
| self.activate(); | |
| }); | |
| /** | |
| * Finish on blur event | |
| * Use a timeout because instant blur gives race conditions | |
| */ | |
| var onBlurFunction = function() { | |
| self.deactivate(true); | |
| } | |
| $elem.blur(function() { | |
| if (self.finishOnBlur_) { | |
| self.finishTimeout_ = setTimeout(onBlurFunction, 200); | |
| } | |
| }); | |
| /** | |
| * Catch a race condition on form submit | |
| */ | |
| $elem.parents('form').on('submit', onBlurFunction); | |
| }; | |
| /** | |
| * Position output DOM elements | |
| * @private | |
| */ | |
| $.Autocompleter.prototype.position = function() { | |
| var offset = this.dom.$elem.offset(); | |
| var height = this.dom.$results.outerHeight(); | |
| var totalHeight = $(window).outerHeight(); | |
| var inputBottom = offset.top + this.dom.$elem.outerHeight(); | |
| var bottomIfDown = inputBottom + height; | |
| // Set autocomplete results at the bottom of input | |
| var position = {top: inputBottom, left: offset.left}; | |
| if (bottomIfDown > totalHeight) { | |
| // Try to set autocomplete results at the top of input | |
| var topIfUp = offset.top - height; | |
| if (topIfUp >= 0) { | |
| position.top = topIfUp; | |
| } | |
| } | |
| this.dom.$results.css(position); | |
| }; | |
| /** | |
| * Read from cache | |
| * @private | |
| */ | |
| $.Autocompleter.prototype.cacheRead = function(filter) { | |
| var filterLength, searchLength, search, maxPos, pos; | |
| if (this.options.useCache) { | |
| filter = String(filter); | |
| filterLength = filter.length; | |
| if (this.options.matchSubset) { | |
| searchLength = 1; | |
| } else { | |
| searchLength = filterLength; | |
| } | |
| while (searchLength <= filterLength) { | |
| if (this.options.matchInside) { | |
| maxPos = filterLength - searchLength; | |
| } else { | |
| maxPos = 0; | |
| } | |
| pos = 0; | |
| while (pos <= maxPos) { | |
| search = filter.substr(0, searchLength); | |
| if (this.cacheData_[search] !== undefined) { | |
| return this.cacheData_[search]; | |
| } | |
| pos++; | |
| } | |
| searchLength++; | |
| } | |
| } | |
| return false; | |
| }; | |
| /** | |
| * Write to cache | |
| * @private | |
| */ | |
| $.Autocompleter.prototype.cacheWrite = function(filter, data) { | |
| if (this.options.useCache) { | |
| if (this.cacheLength_ >= this.options.maxCacheLength) { | |
| this.cacheFlush(); | |
| } | |
| filter = String(filter); | |
| if (this.cacheData_[filter] !== undefined) { | |
| this.cacheLength_++; | |
| } | |
| this.cacheData_[filter] = data; | |
| return this.cacheData_[filter]; | |
| } | |
| return false; | |
| }; | |
| /** | |
| * Flush cache | |
| * @public | |
| */ | |
| $.Autocompleter.prototype.cacheFlush = function() { | |
| this.cacheData_ = {}; | |
| this.cacheLength_ = 0; | |
| }; | |
| /** | |
| * Call hook | |
| * Note that all called hooks are passed the autocompleter object | |
| * @param {string} hook | |
| * @param data | |
| * @returns Result of called hook, false if hook is undefined | |
| */ | |
| $.Autocompleter.prototype.callHook = function(hook, data) { | |
| var f = this.options[hook]; | |
| if (f && $.isFunction(f)) { | |
| return f(data, this); | |
| } | |
| return false; | |
| }; | |
| /** | |
| * Set timeout to activate autocompleter | |
| */ | |
| $.Autocompleter.prototype.activate = function() { | |
| if (!this.options.enabled) return; | |
| var self = this; | |
| if (this.keyTimeout_) { | |
| clearTimeout(this.keyTimeout_); | |
| } | |
| this.keyTimeout_ = setTimeout(function() { | |
| self.activateNow(); | |
| }, this.options.delay); | |
| }; | |
| /** | |
| * Activate autocompleter immediately | |
| */ | |
| $.Autocompleter.prototype.activateNow = function() { | |
| var value = this.beforeUseConverter(this.dom.$elem.val()); | |
| if (value !== this.lastProcessedValue_ && value !== this.lastSelectedValue_) { | |
| this.fetchData(value); | |
| } | |
| }; | |
| /** | |
| * Get autocomplete data for a given value | |
| * @param {string} value Value to base autocompletion on | |
| * @private | |
| */ | |
| $.Autocompleter.prototype.fetchData = function(value) { | |
| var self = this; | |
| var processResults = function(results, filter) { | |
| if (self.options.processData) { | |
| results = self.options.processData(results); | |
| } | |
| self.showResults(self.filterResults(results, filter), filter); | |
| }; | |
| this.lastProcessedValue_ = value; | |
| if (value.length < this.options.minChars) { | |
| processResults([], value); | |
| } else if (this.options.data) { | |
| processResults(this.options.data, value); | |
| } else { | |
| this.fetchRemoteData(value, function(remoteData) { | |
| processResults(remoteData, value); | |
| }); | |
| } | |
| }; | |
| /** | |
| * Get remote autocomplete data for a given value | |
| * @param {string} filter The filter to base remote data on | |
| * @param {function} callback The function to call after data retrieval | |
| * @private | |
| */ | |
| $.Autocompleter.prototype.fetchRemoteData = function(filter, callback) { | |
| var data = this.cacheRead(filter); | |
| if (data) { | |
| callback(data); | |
| } else { | |
| var self = this; | |
| var dataType = self.options.remoteDataType === 'json' ? 'json' : 'text'; | |
| var ajaxCallback = function(data) { | |
| var parsed = false; | |
| if (data !== false) { | |
| parsed = self.parseRemoteData(data); | |
| self.cacheWrite(filter, parsed); | |
| } | |
| self.dom.$elem.removeClass(self.options.loadingClass); | |
| callback(parsed); | |
| }; | |
| this.dom.$elem.addClass(this.options.loadingClass); | |
| $.ajax({ | |
| url: this.makeUrl(filter), | |
| success: ajaxCallback, | |
| error: function(jqXHR, textStatus, errorThrown) { | |
| if($.isFunction(self.options.onError)) { | |
| self.options.onError(jqXHR, textStatus, errorThrown); | |
| } else { | |
| ajaxCallback(false); | |
| } | |
| }, | |
| dataType: dataType | |
| }); | |
| } | |
| }; | |
| /** | |
| * Create or update an extra parameter for the remote request | |
| * @param {string} name Parameter name | |
| * @param {string} value Parameter value | |
| * @public | |
| */ | |
| $.Autocompleter.prototype.setExtraParam = function(name, value) { | |
| var index = $.trim(String(name)); | |
| if (index) { | |
| if (!this.options.extraParams) { | |
| this.options.extraParams = {}; | |
| } | |
| if (this.options.extraParams[index] !== value) { | |
| this.options.extraParams[index] = value; | |
| this.cacheFlush(); | |
| } | |
| } | |
| return this; | |
| }; | |
| /** | |
| * Build the url for a remote request | |
| * If options.queryParamName === false, append query to url instead of using a GET parameter | |
| * @param {string} param The value parameter to pass to the backend | |
| * @returns {string} The finished url with parameters | |
| */ | |
| $.Autocompleter.prototype.makeUrl = function(param) { | |
| var self = this; | |
| var url = this.options.url; | |
| var params = $.extend({}, this.options.extraParams); | |
| if (this.options.queryParamName === false) { | |
| url += encodeURIComponent(param); | |
| } else { | |
| params[this.options.queryParamName] = param; | |
| } | |
| return makeUrl(url, params); | |
| }; | |
| /** | |
| * Parse data received from server | |
| * @param remoteData Data received from remote server | |
| * @returns {array} Parsed data | |
| */ | |
| $.Autocompleter.prototype.parseRemoteData = function(remoteData) { | |
| var remoteDataType; | |
| var data = remoteData; | |
| if (this.options.remoteDataType === 'json') { | |
| remoteDataType = typeof(remoteData); | |
| switch (remoteDataType) { | |
| case 'object': | |
| data = remoteData; | |
| break; | |
| case 'string': | |
| data = $.parseJSON(remoteData); | |
| break; | |
| default: | |
| throw new Error("Unexpected remote data type: " + remoteDataType); | |
| } | |
| return data; | |
| } | |
| return plainTextParser(data, this.options.lineSeparator, this.options.cellSeparator); | |
| }; | |
| /** | |
| * Default filter for results | |
| * @param {Object} result | |
| * @param {String} filter | |
| * @returns {boolean} Include this result | |
| * @private | |
| */ | |
| $.Autocompleter.prototype.defaultFilter = function(result, filter) { | |
| if (!result.value) { | |
| return false; | |
| } | |
| if (this.options.filterResults) { | |
| var pattern = this.matchStringConverter(filter); | |
| var testValue = this.matchStringConverter(result.value); | |
| if (!this.options.matchCase) { | |
| pattern = pattern.toLowerCase(); | |
| testValue = testValue.toLowerCase(); | |
| } | |
| var patternIndex = testValue.indexOf(pattern); | |
| if (this.options.matchInside) { | |
| return patternIndex > -1; | |
| } else { | |
| return patternIndex === 0; | |
| } | |
| } | |
| return true; | |
| }; | |
| /** | |
| * Filter result | |
| * @param {Object} result | |
| * @param {String} filter | |
| * @returns {boolean} Include this result | |
| * @private | |
| */ | |
| $.Autocompleter.prototype.filterResult = function(result, filter) { | |
| // No filter | |
| if (this.options.filter === false) { | |
| return true; | |
| } | |
| // Custom filter | |
| if ($.isFunction(this.options.filter)) { | |
| return this.options.filter(result, filter); | |
| } | |
| // Default filter | |
| return this.defaultFilter(result, filter); | |
| }; | |
| /** | |
| * Filter results | |
| * @param results | |
| * @param filter | |
| */ | |
| $.Autocompleter.prototype.filterResults = function(results, filter) { | |
| var filtered = []; | |
| var i, result; | |
| for (i = 0; i < results.length; i++) { | |
| result = sanitizeResult(results[i]); | |
| if (this.filterResult(result, filter)) { | |
| filtered.push(result); | |
| } | |
| } | |
| if (this.options.sortResults) { | |
| filtered = this.sortResults(filtered, filter); | |
| } | |
| if (this.options.maxItemsToShow > 0 && this.options.maxItemsToShow < filtered.length) { | |
| filtered.length = this.options.maxItemsToShow; | |
| } | |
| return filtered; | |
| }; | |
| /** | |
| * Sort results | |
| * @param results | |
| * @param filter | |
| */ | |
| $.Autocompleter.prototype.sortResults = function(results, filter) { | |
| var self = this; | |
| var sortFunction = this.options.sortFunction; | |
| if (!$.isFunction(sortFunction)) { | |
| sortFunction = function(a, b, f) { | |
| return sortValueAlpha(a, b, self.options.matchCase); | |
| }; | |
| } | |
| results.sort(function(a, b) { | |
| return sortFunction(a, b, filter, self.options); | |
| }); | |
| return results; | |
| }; | |
| /** | |
| * Convert string before matching | |
| * @param s | |
| * @param a | |
| * @param b | |
| */ | |
| $.Autocompleter.prototype.matchStringConverter = function(s, a, b) { | |
| var converter = this.options.matchStringConverter; | |
| if ($.isFunction(converter)) { | |
| s = converter(s, a, b); | |
| } | |
| return s; | |
| }; | |
| /** | |
| * Convert string before use | |
| * @param {String} s | |
| */ | |
| $.Autocompleter.prototype.beforeUseConverter = function(s) { | |
| s = this.getValue(s); | |
| var converter = this.options.beforeUseConverter; | |
| if ($.isFunction(converter)) { | |
| s = converter(s); | |
| } | |
| return s; | |
| }; | |
| /** | |
| * Enable finish on blur event | |
| */ | |
| $.Autocompleter.prototype.enableFinishOnBlur = function() { | |
| this.finishOnBlur_ = true; | |
| }; | |
| /** | |
| * Disable finish on blur event | |
| */ | |
| $.Autocompleter.prototype.disableFinishOnBlur = function() { | |
| this.finishOnBlur_ = false; | |
| }; | |
| /** | |
| * Create a results item (LI element) from a result | |
| * @param result | |
| */ | |
| $.Autocompleter.prototype.createItemFromResult = function(result) { | |
| var self = this; | |
| var $li = $('<li/>'); | |
| $li.html(this.showResult(result.value, result.data)); | |
| $li.data({value: result.value, data: result.data}) | |
| .click(function() { | |
| self.selectItem($li); | |
| }) | |
| .mousedown(self.disableFinishOnBlur) | |
| .mouseup(self.enableFinishOnBlur) | |
| ; | |
| return $li; | |
| }; | |
| /** | |
| * Get all items from the results list | |
| * @param result | |
| */ | |
| $.Autocompleter.prototype.getItems = function() { | |
| return $('>ul>li', this.dom.$results); | |
| }; | |
| /** | |
| * Show all results | |
| * @param results | |
| * @param filter | |
| */ | |
| $.Autocompleter.prototype.showResults = function(results, filter) { | |
| var numResults = results.length; | |
| var self = this; | |
| var $ul = $('<ul></ul>'); | |
| var i, result, $li, autoWidth, first = false, $first = false; | |
| if (numResults) { | |
| for (i = 0; i < numResults; i++) { | |
| result = results[i]; | |
| $li = this.createItemFromResult(result); | |
| $ul.append($li); | |
| if (first === false) { | |
| first = String(result.value); | |
| $first = $li; | |
| $li.addClass(this.options.firstItemClass); | |
| } | |
| if (i === numResults - 1) { | |
| $li.addClass(this.options.lastItemClass); | |
| } | |
| } | |
| this.dom.$results.html($ul).show(); | |
| // Always recalculate position since window size or | |
| // input element location may have changed. | |
| this.position(); | |
| if (this.options.autoWidth) { | |
| autoWidth = this.dom.$elem.outerWidth() - this.dom.$results.outerWidth() + this.dom.$results.width(); | |
| this.dom.$results.css(this.options.autoWidth, autoWidth); | |
| } | |
| this.getItems().hover( | |
| function() { self.focusItem(this); }, | |
| function() { /* void */ } | |
| ); | |
| if (this.autoFill(first, filter) || this.options.selectFirst || (this.options.selectOnly && numResults === 1)) { | |
| this.focusItem($first); | |
| } | |
| this.active_ = true; | |
| } else { | |
| this.hideResults(); | |
| this.active_ = false; | |
| } | |
| }; | |
| $.Autocompleter.prototype.showResult = function(value, data) { | |
| if ($.isFunction(this.options.showResult)) { | |
| return this.options.showResult(value, data); | |
| } else { | |
| return $('<p></p>').text(value).html(); | |
| } | |
| }; | |
| $.Autocompleter.prototype.autoFill = function(value, filter) { | |
| var lcValue, lcFilter, valueLength, filterLength; | |
| if (this.options.autoFill && this.lastKeyPressed_ !== 8) { | |
| lcValue = String(value).toLowerCase(); | |
| lcFilter = String(filter).toLowerCase(); | |
| valueLength = value.length; | |
| filterLength = filter.length; | |
| if (lcValue.substr(0, filterLength) === lcFilter) { | |
| var d = this.getDelimiterOffsets(); | |
| var pad = d.start ? ' ' : ''; // if there is a preceding delimiter | |
| this.setValue( pad + value ); | |
| var start = filterLength + d.start + pad.length; | |
| var end = valueLength + d.start + pad.length; | |
| this.selectRange(start, end); | |
| return true; | |
| } | |
| } | |
| return false; | |
| }; | |
| $.Autocompleter.prototype.focusNext = function() { | |
| this.focusMove(+1); | |
| }; | |
| $.Autocompleter.prototype.focusPrev = function() { | |
| this.focusMove(-1); | |
| }; | |
| $.Autocompleter.prototype.focusMove = function(modifier) { | |
| var $items = this.getItems(); | |
| modifier = sanitizeInteger(modifier, 0); | |
| if (modifier) { | |
| for (var i = 0; i < $items.length; i++) { | |
| if ($($items[i]).hasClass(this.selectClass_)) { | |
| this.focusItem(i + modifier); | |
| return; | |
| } | |
| } | |
| } | |
| this.focusItem(0); | |
| }; | |
| $.Autocompleter.prototype.focusItem = function(item) { | |
| var $item, $items = this.getItems(); | |
| if ($items.length) { | |
| $items.removeClass(this.selectClass_).removeClass(this.options.selectClass); | |
| if (typeof item === 'number') { | |
| if (item < 0) { | |
| item = 0; | |
| } else if (item >= $items.length) { | |
| item = $items.length - 1; | |
| } | |
| $item = $($items[item]); | |
| } else { | |
| $item = $(item); | |
| } | |
| if ($item) { | |
| $item.addClass(this.selectClass_).addClass(this.options.selectClass); | |
| } | |
| } | |
| }; | |
| $.Autocompleter.prototype.selectCurrent = function() { | |
| var $item = $('li.' + this.selectClass_, this.dom.$results); | |
| if ($item.length === 1) { | |
| this.selectItem($item); | |
| } else { | |
| this.deactivate(false); | |
| } | |
| }; | |
| $.Autocompleter.prototype.selectItem = function($li) { | |
| var value = $li.data('value'); | |
| var data = $li.data('data'); | |
| var displayValue = this.displayValue(value, data); | |
| var processedDisplayValue = this.beforeUseConverter(displayValue); | |
| this.lastProcessedValue_ = processedDisplayValue; | |
| this.lastSelectedValue_ = processedDisplayValue; | |
| var d = this.getDelimiterOffsets(); | |
| var delimiter = this.options.delimiterChar; | |
| var elem = this.dom.$elem; | |
| var extraCaretPos = 0; | |
| if ( this.options.useDelimiter ) { | |
| // if there is a preceding delimiter, add a space after the delimiter | |
| if ( elem.val().substring(d.start-1, d.start) == delimiter && delimiter != ' ' ) { | |
| displayValue = ' ' + displayValue; | |
| } | |
| // if there is not already a delimiter trailing this value, add it | |
| if ( elem.val().substring(d.end, d.end+1) != delimiter && this.lastKeyPressed_ != this.options.delimiterKeyCode ) { | |
| displayValue = displayValue + delimiter; | |
| } else { | |
| // move the cursor after the existing trailing delimiter | |
| extraCaretPos = 1; | |
| } | |
| } | |
| this.setValue(displayValue); | |
| this.setCaret(d.start + displayValue.length + extraCaretPos); | |
| this.callHook('onItemSelect', { value: value, data: data }); | |
| this.deactivate(true); | |
| elem.focus(); | |
| }; | |
| $.Autocompleter.prototype.displayValue = function(value, data) { | |
| if ($.isFunction(this.options.displayValue)) { | |
| return this.options.displayValue(value, data); | |
| } | |
| return value; | |
| }; | |
| $.Autocompleter.prototype.hideResults = function() { | |
| this.dom.$results.hide(); | |
| }; | |
| $.Autocompleter.prototype.deactivate = function(finish) { | |
| if (this.finishTimeout_) { | |
| clearTimeout(this.finishTimeout_); | |
| } | |
| if (this.keyTimeout_) { | |
| clearTimeout(this.keyTimeout_); | |
| } | |
| if (finish) { | |
| if (this.lastProcessedValue_ !== this.lastSelectedValue_) { | |
| if (this.options.mustMatch) { | |
| this.setValue(''); | |
| } | |
| this.callHook('onNoMatch'); | |
| } | |
| if (this.active_) { | |
| this.callHook('onFinish'); | |
| } | |
| this.lastKeyPressed_ = null; | |
| this.lastProcessedValue_ = null; | |
| this.lastSelectedValue_ = null; | |
| this.active_ = false; | |
| } | |
| this.hideResults(); | |
| }; | |
| $.Autocompleter.prototype.selectRange = function(start, end) { | |
| var input = this.dom.$elem.get(0); | |
| if (input.setSelectionRange) { | |
| input.focus(); | |
| input.setSelectionRange(start, end); | |
| } else if (input.createTextRange) { | |
| var range = input.createTextRange(); | |
| range.collapse(true); | |
| range.moveEnd('character', end); | |
| range.moveStart('character', start); | |
| range.select(); | |
| } | |
| }; | |
| /** | |
| * Move caret to position | |
| * @param {Number} pos | |
| */ | |
| $.Autocompleter.prototype.setCaret = function(pos) { | |
| this.selectRange(pos, pos); | |
| }; | |
| /** | |
| * Get caret position | |
| */ | |
| $.Autocompleter.prototype.getCaret = function() { | |
| var $elem = this.dom.$elem; | |
| var elem = $elem[0]; | |
| var val, selection, range, start, end, stored_range; | |
| if (elem.createTextRange) { // IE | |
| selection = document.selection; | |
| if (elem.tagName.toLowerCase() != 'textarea') { | |
| val = $elem.val(); | |
| range = selection.createRange().duplicate(); | |
| range.moveEnd('character', val.length); | |
| if (range.text === '') { | |
| start = val.length; | |
| } else { | |
| start = val.lastIndexOf(range.text); | |
| } | |
| range = selection.createRange().duplicate(); | |
| range.moveStart('character', -val.length); | |
| end = range.text.length; | |
| } else { | |
| range = selection.createRange(); | |
| stored_range = range.duplicate(); | |
| stored_range.moveToElementText(elem); | |
| stored_range.setEndPoint('EndToEnd', range); | |
| start = stored_range.text.length - range.text.length; | |
| end = start + range.text.length; | |
| } | |
| } else { | |
| start = $elem[0].selectionStart; | |
| end = $elem[0].selectionEnd; | |
| } | |
| return { | |
| start: start, | |
| end: end | |
| }; | |
| }; | |
| /** | |
| * Set the value that is currently being autocompleted | |
| * @param {String} value | |
| */ | |
| $.Autocompleter.prototype.setValue = function(value) { | |
| if ( this.options.useDelimiter ) { | |
| // set the substring between the current delimiters | |
| var val = this.dom.$elem.val(); | |
| var d = this.getDelimiterOffsets(); | |
| var preVal = val.substring(0, d.start); | |
| var postVal = val.substring(d.end); | |
| value = preVal + value + postVal; | |
| } | |
| this.dom.$elem.val(value); | |
| }; | |
| /** | |
| * Get the value currently being autocompleted | |
| * @param {String} value | |
| */ | |
| $.Autocompleter.prototype.getValue = function(value) { | |
| if ( this.options.useDelimiter ) { | |
| var d = this.getDelimiterOffsets(); | |
| return value.substring(d.start, d.end).trim(); | |
| } else { | |
| return value; | |
| } | |
| }; | |
| /** | |
| * Get the offsets of the value currently being autocompleted | |
| */ | |
| $.Autocompleter.prototype.getDelimiterOffsets = function() { | |
| var val = this.dom.$elem.val(); | |
| if ( this.options.useDelimiter ) { | |
| var preCaretVal = val.substring(0, this.getCaret().start); | |
| var start = preCaretVal.lastIndexOf(this.options.delimiterChar) + 1; | |
| var postCaretVal = val.substring(this.getCaret().start); | |
| var end = postCaretVal.indexOf(this.options.delimiterChar); | |
| if ( end == -1 ) end = val.length; | |
| end += this.getCaret().start; | |
| } else { | |
| start = 0; | |
| end = val.length; | |
| } | |
| return { | |
| start: start, | |
| end: end | |
| }; | |
| }; | |
| })((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined')? django.jQuery : jQuery); | |