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); | |