| /** |
| * @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); |