|
|
/*** Ajax Autocomplete for jQuery, version 1.4.11* (c) 2017 Tomas Kirda** Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.* For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete
*/
/*jslint browser: true, white: true, single: true, this: true, multivar: true *//*global define, window, document, jQuery, exports, require */
// Expose plugin as an AMD module if AMD loader is present:
(function (factory) { "use strict"; if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module.
define(['jquery'], factory); } else if (typeof exports === 'object' && typeof require === 'function') { // Browserify
factory(require('jquery')); } else { // Browser globals
factory(jQuery); }}(function ($) { 'use strict';
var utils = (function () { return { escapeRegExChars: function (value) { return value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&"); }, createNode: function (containerClass) { var div = document.createElement('div'); div.className = containerClass; div.style.position = 'absolute'; div.style.display = 'none'; return div; } }; }()),
keys = { ESC: 27, TAB: 9, RETURN: 13, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40 },
noop = $.noop;
function Autocomplete(el, options) { var that = this;
// Shared variables:
that.element = el; that.el = $(el); that.suggestions = []; that.badQueries = []; that.selectedIndex = -1; that.currentValue = that.element.value; that.timeoutId = null; that.cachedResponse = {}; that.onChangeTimeout = null; that.onChange = null; that.isLocal = false; that.suggestionsContainer = null; that.noSuggestionsContainer = null; that.options = $.extend(true, {}, Autocomplete.defaults, options); that.classes = { selected: 'autocomplete-selected', suggestion: 'autocomplete-suggestion' }; that.hint = null; that.hintValue = ''; that.selection = null;
// Initialize and set options:
that.initialize(); that.setOptions(options); }
Autocomplete.utils = utils;
$.Autocomplete = Autocomplete;
Autocomplete.defaults = { ajaxSettings: {}, autoSelectFirst: false, appendTo: 'body', serviceUrl: null, lookup: null, onSelect: null, width: 'auto', minChars: 1, maxHeight: 300, deferRequestBy: 0, params: {}, formatResult: _formatResult, formatGroup: _formatGroup, delimiter: null, zIndex: 9999, type: 'GET', noCache: false, onSearchStart: noop, onSearchComplete: noop, onSearchError: noop, preserveInput: false, containerClass: 'autocomplete-suggestions', tabDisabled: false, dataType: 'text', currentRequest: null, triggerSelectOnValidInput: true, preventBadQueries: true, lookupFilter: _lookupFilter, paramName: 'query', transformResult: _transformResult, showNoSuggestionNotice: false, noSuggestionNotice: 'No results', orientation: 'bottom', forceFixPosition: false };
function _lookupFilter(suggestion, originalQuery, queryLowerCase) { return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1; };
function _transformResult(response) { return typeof response === 'string' ? $.parseJSON(response) : response; };
function _formatResult(suggestion, currentValue) { // Do not replace anything if the current value is empty
if (!currentValue) { return suggestion.value; }
var pattern = '(' + utils.escapeRegExChars(currentValue) + ')';
return suggestion.value .replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/<(\/?strong)>/g, '<$1>'); };
function _formatGroup(suggestion, category) { return '<div class="autocomplete-group">' + category + '</div>'; };
Autocomplete.prototype = {
initialize: function () { var that = this, suggestionSelector = '.' + that.classes.suggestion, selected = that.classes.selected, options = that.options, container;
that.element.setAttribute('autocomplete', 'off');
// html() deals with many types: htmlString or Element or Array or jQuery
that.noSuggestionsContainer = $('<div class="autocomplete-no-suggestion"></div>') .html(this.options.noSuggestionNotice).get(0);
that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass);
container = $(that.suggestionsContainer);
container.appendTo(options.appendTo || 'body');
// Only set width if it was provided:
if (options.width !== 'auto') { container.css('width', options.width); }
// Listen for mouse over event on suggestions list:
container.on('mouseover.autocomplete', suggestionSelector, function () { that.activate($(this).data('index')); });
// Deselect active element when mouse leaves suggestions container:
container.on('mouseout.autocomplete', function () { that.selectedIndex = -1; container.children('.' + selected).removeClass(selected); });
// Listen for click event on suggestions list:
container.on('click.autocomplete', suggestionSelector, function () { that.select($(this).data('index')); });
container.on('click.autocomplete', function () { clearTimeout(that.blurTimeoutId); })
that.fixPositionCapture = function () { if (that.visible) { that.fixPosition(); } };
$(window).on('resize.autocomplete', that.fixPositionCapture);
that.el.on('keydown.autocomplete', function (e) { that.onKeyPress(e); }); that.el.on('keyup.autocomplete', function (e) { that.onKeyUp(e); }); that.el.on('blur.autocomplete', function () { that.onBlur(); }); that.el.on('focus.autocomplete', function () { that.onFocus(); }); that.el.on('change.autocomplete', function (e) { that.onKeyUp(e); }); that.el.on('input.autocomplete', function (e) { that.onKeyUp(e); }); },
onFocus: function () { var that = this;
if (that.disabled) { return; }
that.fixPosition();
if (that.el.val().length >= that.options.minChars) { that.onValueChange(); } },
onBlur: function () { var that = this, options = that.options, value = that.el.val(), query = that.getQuery(value);
// If user clicked on a suggestion, hide() will
// be canceled, otherwise close suggestions
that.blurTimeoutId = setTimeout(function () { that.hide();
if (that.selection && that.currentValue !== query) { (options.onInvalidateSelection || $.noop).call(that.element); } }, 200); },
abortAjax: function () { var that = this; if (that.currentRequest) { that.currentRequest.abort(); that.currentRequest = null; } },
setOptions: function (suppliedOptions) { var that = this, options = $.extend({}, that.options, suppliedOptions);
that.isLocal = Array.isArray(options.lookup);
if (that.isLocal) { options.lookup = that.verifySuggestionsFormat(options.lookup); }
options.orientation = that.validateOrientation(options.orientation, 'bottom');
// Adjust height, width and z-index:
$(that.suggestionsContainer).css({ 'max-height': options.maxHeight + 'px', 'width': options.width + 'px', 'z-index': options.zIndex });
this.options = options; },
clearCache: function () { this.cachedResponse = {}; this.badQueries = []; },
clear: function () { this.clearCache(); this.currentValue = ''; this.suggestions = []; },
disable: function () { var that = this; that.disabled = true; clearTimeout(that.onChangeTimeout); that.abortAjax(); },
enable: function () { this.disabled = false; },
fixPosition: function () { // Use only when container has already its content
var that = this, $container = $(that.suggestionsContainer), containerParent = $container.parent().get(0); // Fix position automatically when appended to body.
// In other cases force parameter must be given.
if (containerParent !== document.body && !that.options.forceFixPosition) { return; }
// Choose orientation
var orientation = that.options.orientation, containerHeight = $container.outerHeight(), height = that.el.outerHeight(), offset = that.el.offset(), styles = { 'top': offset.top, 'left': offset.left };
if (orientation === 'auto') { var viewPortHeight = $(window).height(), scrollTop = $(window).scrollTop(), topOverflow = -scrollTop + offset.top - containerHeight, bottomOverflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight);
orientation = (Math.max(topOverflow, bottomOverflow) === topOverflow) ? 'top' : 'bottom'; }
if (orientation === 'top') { styles.top += -containerHeight; } else { styles.top += height; }
// If container is not positioned to body,
// correct its position using offset parent offset
if(containerParent !== document.body) { var opacity = $container.css('opacity'), parentOffsetDiff;
if (!that.visible){ $container.css('opacity', 0).show(); }
parentOffsetDiff = $container.offsetParent().offset(); styles.top -= parentOffsetDiff.top; styles.top += containerParent.scrollTop; styles.left -= parentOffsetDiff.left;
if (!that.visible){ $container.css('opacity', opacity).hide(); } }
if (that.options.width === 'auto') { styles.width = that.el.outerWidth() + 'px'; }
$container.css(styles); },
isCursorAtEnd: function () { var that = this, valLength = that.el.val().length, selectionStart = that.element.selectionStart, range;
if (typeof selectionStart === 'number') { return selectionStart === valLength; } if (document.selection) { range = document.selection.createRange(); range.moveStart('character', -valLength); return valLength === range.text.length; } return true; },
onKeyPress: function (e) { var that = this;
// If suggestions are hidden and user presses arrow down, display suggestions:
if (!that.disabled && !that.visible && e.which === keys.DOWN && that.currentValue) { that.suggest(); return; }
if (that.disabled || !that.visible) { return; }
switch (e.which) { case keys.ESC: that.el.val(that.currentValue); that.hide(); break; case keys.RIGHT: if (that.hint && that.options.onHint && that.isCursorAtEnd()) { that.selectHint(); break; } return; case keys.TAB: if (that.hint && that.options.onHint) { that.selectHint(); return; } if (that.selectedIndex === -1) { that.hide(); return; } that.select(that.selectedIndex); if (that.options.tabDisabled === false) { return; } break; case keys.RETURN: if (that.selectedIndex === -1) { that.hide(); return; } that.select(that.selectedIndex); break; case keys.UP: that.moveUp(); break; case keys.DOWN: that.moveDown(); break; default: return; }
// Cancel event if function did not return:
e.stopImmediatePropagation(); e.preventDefault(); },
onKeyUp: function (e) { var that = this;
if (that.disabled) { return; }
switch (e.which) { case keys.UP: case keys.DOWN: return; }
clearTimeout(that.onChangeTimeout);
if (that.currentValue !== that.el.val()) { that.findBestHint(); if (that.options.deferRequestBy > 0) { // Defer lookup in case when value changes very quickly:
that.onChangeTimeout = setTimeout(function () { that.onValueChange(); }, that.options.deferRequestBy); } else { that.onValueChange(); } } },
onValueChange: function () { if (this.ignoreValueChange) { this.ignoreValueChange = false; return; }
var that = this, options = that.options, value = that.el.val(), query = that.getQuery(value);
if (that.selection && that.currentValue !== query) { that.selection = null; (options.onInvalidateSelection || $.noop).call(that.element); }
clearTimeout(that.onChangeTimeout); that.currentValue = value; that.selectedIndex = -1;
// Check existing suggestion for the match before proceeding:
if (options.triggerSelectOnValidInput && that.isExactMatch(query)) { that.select(0); return; }
if (query.length < options.minChars) { that.hide(); } else { that.getSuggestions(query); } },
isExactMatch: function (query) { var suggestions = this.suggestions;
return (suggestions.length === 1 && suggestions[0].value.toLowerCase() === query.toLowerCase()); },
getQuery: function (value) { var delimiter = this.options.delimiter, parts;
if (!delimiter) { return value; } parts = value.split(delimiter); return $.trim(parts[parts.length - 1]); },
getSuggestionsLocal: function (query) { var that = this, options = that.options, queryLowerCase = query.toLowerCase(), filter = options.lookupFilter, limit = parseInt(options.lookupLimit, 10), data;
data = { suggestions: $.grep(options.lookup, function (suggestion) { return filter(suggestion, query, queryLowerCase); }) };
if (limit && data.suggestions.length > limit) { data.suggestions = data.suggestions.slice(0, limit); }
return data; },
getSuggestions: function (q) { var response, that = this, options = that.options, serviceUrl = options.serviceUrl, params, cacheKey, ajaxSettings;
options.params[options.paramName] = q;
if (options.onSearchStart.call(that.element, options.params) === false) { return; }
params = options.ignoreParams ? null : options.params;
if ($.isFunction(options.lookup)){ options.lookup(q, function (data) { that.suggestions = data.suggestions; that.suggest(); options.onSearchComplete.call(that.element, q, data.suggestions); }); return; }
if (that.isLocal) { response = that.getSuggestionsLocal(q); } else { if ($.isFunction(serviceUrl)) { serviceUrl = serviceUrl.call(that.element, q); } cacheKey = serviceUrl + '?' + $.param(params || {}); response = that.cachedResponse[cacheKey]; }
if (response && Array.isArray(response.suggestions)) { that.suggestions = response.suggestions; that.suggest(); options.onSearchComplete.call(that.element, q, response.suggestions); } else if (!that.isBadQuery(q)) { that.abortAjax();
ajaxSettings = { url: serviceUrl, data: params, type: options.type, dataType: options.dataType };
$.extend(ajaxSettings, options.ajaxSettings);
that.currentRequest = $.ajax(ajaxSettings).done(function (data) { var result; that.currentRequest = null; result = options.transformResult(data, q); that.processResponse(result, q, cacheKey); options.onSearchComplete.call(that.element, q, result.suggestions); }).fail(function (jqXHR, textStatus, errorThrown) { options.onSearchError.call(that.element, q, jqXHR, textStatus, errorThrown); }); } else { options.onSearchComplete.call(that.element, q, []); } },
isBadQuery: function (q) { if (!this.options.preventBadQueries){ return false; }
var badQueries = this.badQueries, i = badQueries.length;
while (i--) { if (q.indexOf(badQueries[i]) === 0) { return true; } }
return false; },
hide: function () { var that = this, container = $(that.suggestionsContainer);
if ($.isFunction(that.options.onHide) && that.visible) { that.options.onHide.call(that.element, container); }
that.visible = false; that.selectedIndex = -1; clearTimeout(that.onChangeTimeout); $(that.suggestionsContainer).hide(); that.signalHint(null); },
suggest: function () { if (!this.suggestions.length) { if (this.options.showNoSuggestionNotice) { this.noSuggestions(); } else { this.hide(); } return; }
var that = this, options = that.options, groupBy = options.groupBy, formatResult = options.formatResult, value = that.getQuery(that.currentValue), className = that.classes.suggestion, classSelected = that.classes.selected, container = $(that.suggestionsContainer), noSuggestionsContainer = $(that.noSuggestionsContainer), beforeRender = options.beforeRender, html = '', category, formatGroup = function (suggestion, index) { var currentCategory = suggestion.data[groupBy];
if (category === currentCategory){ return ''; }
category = currentCategory;
return options.formatGroup(suggestion, category); };
if (options.triggerSelectOnValidInput && that.isExactMatch(value)) { that.select(0); return; }
// Build suggestions inner HTML:
$.each(that.suggestions, function (i, suggestion) { if (groupBy){ html += formatGroup(suggestion, value, i); }
html += '<div class="' + className + '" data-index="' + i + '">' + formatResult(suggestion, value, i) + '</div>'; });
this.adjustContainerWidth();
noSuggestionsContainer.detach(); container.html(html);
if ($.isFunction(beforeRender)) { beforeRender.call(that.element, container, that.suggestions); }
that.fixPosition(); container.show();
// Select first value by default:
if (options.autoSelectFirst) { that.selectedIndex = 0; container.scrollTop(0); container.children('.' + className).first().addClass(classSelected); }
that.visible = true; that.findBestHint(); },
noSuggestions: function() { var that = this, beforeRender = that.options.beforeRender, container = $(that.suggestionsContainer), noSuggestionsContainer = $(that.noSuggestionsContainer);
this.adjustContainerWidth();
// Some explicit steps. Be careful here as it easy to get
// noSuggestionsContainer removed from DOM if not detached properly.
noSuggestionsContainer.detach();
// clean suggestions if any
container.empty(); container.append(noSuggestionsContainer);
if ($.isFunction(beforeRender)) { beforeRender.call(that.element, container, that.suggestions); }
that.fixPosition();
container.show(); that.visible = true; },
adjustContainerWidth: function() { var that = this, options = that.options, width, container = $(that.suggestionsContainer);
// If width is auto, adjust width before displaying suggestions,
// because if instance was created before input had width, it will be zero.
// Also it adjusts if input width has changed.
if (options.width === 'auto') { width = that.el.outerWidth(); container.css('width', width > 0 ? width : 300); } else if(options.width === 'flex') { // Trust the source! Unset the width property so it will be the max length
// the containing elements.
container.css('width', ''); } },
findBestHint: function () { var that = this, value = that.el.val().toLowerCase(), bestMatch = null;
if (!value) { return; }
$.each(that.suggestions, function (i, suggestion) { var foundMatch = suggestion.value.toLowerCase().indexOf(value) === 0; if (foundMatch) { bestMatch = suggestion; } return !foundMatch; });
that.signalHint(bestMatch); },
signalHint: function (suggestion) { var hintValue = '', that = this; if (suggestion) { hintValue = that.currentValue + suggestion.value.substr(that.currentValue.length); } if (that.hintValue !== hintValue) { that.hintValue = hintValue; that.hint = suggestion; (this.options.onHint || $.noop)(hintValue); } },
verifySuggestionsFormat: function (suggestions) { // If suggestions is string array, convert them to supported format:
if (suggestions.length && typeof suggestions[0] === 'string') { return $.map(suggestions, function (value) { return { value: value, data: null }; }); }
return suggestions; },
validateOrientation: function(orientation, fallback) { orientation = $.trim(orientation || '').toLowerCase();
if($.inArray(orientation, ['auto', 'bottom', 'top']) === -1){ orientation = fallback; }
return orientation; },
processResponse: function (result, originalQuery, cacheKey) { var that = this, options = that.options;
result.suggestions = that.verifySuggestionsFormat(result.suggestions);
// Cache results if cache is not disabled:
if (!options.noCache) { that.cachedResponse[cacheKey] = result; if (options.preventBadQueries && !result.suggestions.length) { that.badQueries.push(originalQuery); } }
// Return if originalQuery is not matching current query:
if (originalQuery !== that.getQuery(that.currentValue)) { return; }
that.suggestions = result.suggestions; that.suggest(); },
activate: function (index) { var that = this, activeItem, selected = that.classes.selected, container = $(that.suggestionsContainer), children = container.find('.' + that.classes.suggestion);
container.find('.' + selected).removeClass(selected);
that.selectedIndex = index;
if (that.selectedIndex !== -1 && children.length > that.selectedIndex) { activeItem = children.get(that.selectedIndex); $(activeItem).addClass(selected); return activeItem; }
return null; },
selectHint: function () { var that = this, i = $.inArray(that.hint, that.suggestions);
that.select(i); },
select: function (i) { var that = this; that.hide(); that.onSelect(i); },
moveUp: function () { var that = this;
if (that.selectedIndex === -1) { return; }
if (that.selectedIndex === 0) { $(that.suggestionsContainer).children('.' + that.classes.suggestion).first().removeClass(that.classes.selected); that.selectedIndex = -1; that.ignoreValueChange = false; that.el.val(that.currentValue); that.findBestHint(); return; }
that.adjustScroll(that.selectedIndex - 1); },
moveDown: function () { var that = this;
if (that.selectedIndex === (that.suggestions.length - 1)) { return; }
that.adjustScroll(that.selectedIndex + 1); },
adjustScroll: function (index) { var that = this, activeItem = that.activate(index);
if (!activeItem) { return; }
var offsetTop, upperBound, lowerBound, heightDelta = $(activeItem).outerHeight();
offsetTop = activeItem.offsetTop; upperBound = $(that.suggestionsContainer).scrollTop(); lowerBound = upperBound + that.options.maxHeight - heightDelta;
if (offsetTop < upperBound) { $(that.suggestionsContainer).scrollTop(offsetTop); } else if (offsetTop > lowerBound) { $(that.suggestionsContainer).scrollTop(offsetTop - that.options.maxHeight + heightDelta); }
if (!that.options.preserveInput) { // During onBlur event, browser will trigger "change" event,
// because value has changed, to avoid side effect ignore,
// that event, so that correct suggestion can be selected
// when clicking on suggestion with a mouse
that.ignoreValueChange = true; that.el.val(that.getValue(that.suggestions[index].value)); }
that.signalHint(null); },
onSelect: function (index) { var that = this, onSelectCallback = that.options.onSelect, suggestion = that.suggestions[index];
that.currentValue = that.getValue(suggestion.value);
if (that.currentValue !== that.el.val() && !that.options.preserveInput) { that.el.val(that.currentValue); }
that.signalHint(null); that.suggestions = []; that.selection = suggestion;
if ($.isFunction(onSelectCallback)) { onSelectCallback.call(that.element, suggestion); } },
getValue: function (value) { var that = this, delimiter = that.options.delimiter, currentValue, parts;
if (!delimiter) { return value; }
currentValue = that.currentValue; parts = currentValue.split(delimiter);
if (parts.length === 1) { return value; }
return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value; },
dispose: function () { var that = this; that.el.off('.autocomplete').removeData('autocomplete'); $(window).off('resize.autocomplete', that.fixPositionCapture); $(that.suggestionsContainer).remove(); } };
// Create chainable jQuery plugin:
$.fn.devbridgeAutocomplete = function (options, args) { var dataKey = 'autocomplete'; // If function invoked without argument return
// instance of the first matched element:
if (!arguments.length) { return this.first().data(dataKey); }
return this.each(function () { var inputElement = $(this), instance = inputElement.data(dataKey);
if (typeof options === 'string') { if (instance && typeof instance[options] === 'function') { instance[options](args); } } else { // If instance already exists, destroy it:
if (instance && instance.dispose) { instance.dispose(); } instance = new Autocomplete(this, options); inputElement.data(dataKey, instance); } }); };
// Don't overwrite if it already exists
if (!$.fn.autocomplete) { $.fn.autocomplete = $.fn.devbridgeAutocomplete; }}));
|