/** * ajax autocomplete for jquery, version %version% * (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 }; function autocomplete(el, options) { var noop = $.noop, that = this, defaults = { ajaxsettings: {}, autoselectfirst: false, appendto: document.body, serviceurl: null, lookup: null, onselect: null, width: 'auto', minchars: 1, maxheight: 300, deferrequestby: 0, params: {}, formatresult: autocomplete.formatresult, formatgroup: autocomplete.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: function (suggestion, originalquery, querylowercase) { return suggestion.value.tolowercase().indexof(querylowercase) !== -1; }, paramname: 'query', transformresult: function (response) { return typeof response === 'string' ? $.parsejson(response) : response; }, shownosuggestionnotice: false, nosuggestionnotice: 'no results', orientation: 'bottom', forcefixposition: false }; // shared variables: that.element = el; that.el = $(el); that.suggestions = []; that.badqueries = []; that.selectedindex = -1; that.currentvalue = that.element.value; that.intervalid = 0; that.cachedresponse = {}; that.onchangeinterval = null; that.onchange = null; that.islocal = false; that.suggestionscontainer = null; that.nosuggestionscontainer = null; that.options = $.extend({}, 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.formatresult = function (suggestion, currentvalue) { // do not replace anything if there current value is empty if (!currentvalue) { return suggestion.value; } var pattern = '(' + utils.escaperegexchars(currentvalue) + ')'; return suggestion.value .replace(new regexp(pattern, 'gi'), '$1<\/strong>') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/<(\/?strong)>/g, '<$1>'); }; autocomplete.formatgroup = function (suggestion, category) { return '
' + category + '
'; }; autocomplete.prototype = { killerfn: null, initialize: function () { var that = this, suggestionselector = '.' + that.classes.suggestion, selected = that.classes.selected, options = that.options, container; // remove autocomplete attribute to prevent native suggestions: that.element.setattribute('autocomplete', 'off'); that.killerfn = function (e) { if (!$(e.target).closest('.' + that.options.containerclass).length) { that.killsuggestions(); that.disablekillerfn(); } }; // html() deals with many types: htmlstring or element or array or jquery that.nosuggestionscontainer = $('
') .html(this.options.nosuggestionnotice).get(0); that.suggestionscontainer = autocomplete.utils.createnode(options.containerclass); container = $(that.suggestionscontainer); container.appendto(options.appendto); // 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')); return false; }); 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; that.fixposition(); if (that.el.val().length >= that.options.minchars) { that.onvaluechange(); } }, onblur: function () { this.enablekillerfn(); }, abortajax: function () { var that = this; if (that.currentrequest) { that.currentrequest.abort(); that.currentrequest = null; } }, setoptions: function (suppliedoptions) { var that = this, options = that.options; $.extend(options, suppliedoptions); that.islocal = $.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 }); }, clearcache: function () { this.cachedresponse = {}; this.badqueries = []; }, clear: function () { this.clearcache(); this.currentvalue = ''; this.suggestions = []; }, disable: function () { var that = this; that.disabled = true; clearinterval(that.onchangeinterval); 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.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); }, enablekillerfn: function () { var that = this; $(document).on('click.autocomplete', that.killerfn); }, disablekillerfn: function () { var that = this; $(document).off('click.autocomplete', that.killerfn); }, killsuggestions: function () { var that = this; that.stopkillsuggestions(); that.intervalid = window.setinterval(function () { if (that.visible) { // no need to restore value when // preserveinput === true, // because we did not change it if (!that.options.preserveinput) { that.el.val(that.currentvalue); } that.hide(); } that.stopkillsuggestions(); }, 50); }, stopkillsuggestions: function () { window.clearinterval(this.intervalid); }, 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; } clearinterval(that.onchangeinterval); if (that.currentvalue !== that.el.val()) { that.findbesthint(); if (that.options.deferrequestby > 0) { // defer lookup in case when value changes very quickly: that.onchangeinterval = setinterval(function () { that.onvaluechange(); }, that.options.deferrequestby); } else { that.onvaluechange(); } } }, onvaluechange: function () { 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); } clearinterval(that.onchangeinterval); 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; params = options.ignoreparams ? null : options.params; if (options.onsearchstart.call(that.element, options.params) === false) { return; } 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 && $.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; clearinterval(that.onchangeinterval); $(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 += '
' + formatresult(suggestion, value, i) + '
'; }); 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, 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(); container.empty(); // clean suggestions if any container.append(nosuggestionscontainer); 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); that.disablekillerfn(); }, moveup: function () { var that = this; if (that.selectedindex === -1) { return; } if (that.selectedindex === 0) { $(that.suggestionscontainer).children().first().removeclass(that.classes.selected); that.selectedindex = -1; 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) { 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'); that.disablekillerfn(); $(window).off('resize.autocomplete', that.fixpositioncapture); $(that.suggestionscontainer).remove(); } }; // create chainable jquery plugin: $.fn.autocomplete = $.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); } }); }; }));