blob: 77c1db60a8ffa546dc8e718c7de53b98e6c81567 [file] [log] [blame]
/**
* @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);