/** * @fileOverview jquery-autocomplete, the jQuery Autocompleter * @author Dylan Verheul * @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 = $('
').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.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 = $(''); 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 $('

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