/*jslint browser: true, white: true, plusplus: true */ /*global define, window, document, jquery */ // 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 { // browser globals factory(jquery); } }(function ($) { 'use strict'; var utils = (function () { return { escaperegexchars: function (value) { return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); }, createnode: function (html) { var div = document.createelement('div'); div.innerhtml = html; return div.firstchild; } }; }()), keys = { esc: 27, tab: 9, return: 13, left: 37, up: 38, right: 39, down: 40 }; function autocomplete(el, options) { var noop = function () { }, that = this, defaults = { autoselectfirst: false, appendto: 'body', serviceurl: null, lookup: null, onselect: null, width: 'auto', minchars: 1, maxheight: 300, deferrequestby: 0, params: {}, formatresult: autocomplete.formatresult, delimiter: null, zindex: 9999, type: 'get', nocache: false, onsearchstart: noop, onsearchcomplete: noop, containerclass: 'autocomplete-suggestions', tabdisabled: false, datatype: 'text', currentrequest: null, lookupfilter: function (suggestion, originalquery, querylowercase) { return suggestion.value.tolowercase().indexof(querylowercase) !== -1; }, paramname: 'query', transformresult: function (response) { return typeof response === 'string' ? $.parsejson(response) : response; } }; // 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.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) { var pattern = '(' + utils.escaperegexchars(currentvalue) + ')'; return suggestion.value.replace(new regexp(pattern, 'gi'), '$1<\/strong>'); }; 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 === 0) { that.killsuggestions(); that.disablekillerfn(); } }; that.suggestionscontainer = autocomplete.utils.createnode(''); container = $(that.suggestionscontainer); container.appendto(options.appendto); // only set width if it was provided: if (options.width !== 'auto') { container.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')); }); that.fixposition(); that.fixpositioncapture = function () { if (that.visible) { that.fixposition(); } }; $(window).on('resize', 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.fixposition(); }); that.el.on('change.autocomplete', function (e) { that.onkeyup(e); }); }, onblur: function () { this.enablekillerfn(); }, 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); } // 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 () { this.disabled = true; }, enable: function () { this.disabled = false; }, fixposition: function () { var that = this, offset; // don't adjsut position if custom container has been specified: if (that.options.appendto !== 'body') { return; } offset = that.el.offset(); $(that.suggestionscontainer).css({ top: (offset.top + that.el.outerheight()) + 'px', left: offset.left + 'px' }); }, 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 () { that.hide(); that.stopkillsuggestions(); }, 300); }, 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; } // fall through to return case keys.return: if (that.selectedindex === -1) { that.hide(); return; } that.select(that.selectedindex); if (e.which === keys.tab && that.options.tabdisabled === false) { return; } 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, q; if (that.selection) { that.selection = null; (that.options.oninvalidateselection || $.noop)(); } clearinterval(that.onchangeinterval); that.currentvalue = that.el.val(); q = that.getquery(that.currentvalue); that.selectedindex = -1; if (q.length < that.options.minchars) { that.hide(); } else { that.getsuggestions(q); } }, getquery: function (value) { var delimiter = this.options.delimiter, parts; if (!delimiter) { return $.trim(value); } parts = value.split(delimiter); return $.trim(parts[parts.length - 1]); }, getsuggestionslocal: function (query) { var that = this, querylowercase = query.tolowercase(), filter = that.options.lookupfilter; return { suggestions: $.grep(that.options.lookup, function (suggestion) { return filter(suggestion, query, querylowercase); }) }; }, getsuggestions: function (q) { var response, that = this, options = that.options, serviceurl = options.serviceurl; response = that.islocal ? that.getsuggestionslocal(q) : that.cachedresponse[q]; if (response && $.isarray(response.suggestions)) { that.suggestions = response.suggestions; that.suggest(); } else if (!that.isbadquery(q)) { options.params[options.paramname] = q; if (options.onsearchstart.call(that.element, options.params) === false) { return; } if ($.isfunction(options.serviceurl)) { serviceurl = options.serviceurl.call(that.element, q); } if(this.currentrequest != null) { this.currentrequest.abort(); } this.currentrequest = $.ajax({ url: serviceurl, data: options.ignoreparams ? null : options.params, type: options.type, datatype: options.datatype }).done(function (data) { that.processresponse(data, q); options.onsearchcomplete.call(that.element, q); }); } }, isbadquery: function (q) { var badqueries = this.badqueries, i = badqueries.length; while (i--) { if (q.indexof(badqueries[i]) === 0) { return true; } } return false; }, hide: function () { var that = this; that.visible = false; that.selectedindex = -1; $(that.suggestionscontainer).hide(); that.signalhint(null); }, suggest: function () { if (this.suggestions.length === 0) { this.hide(); return; } var that = this, formatresult = that.options.formatresult, value = that.getquery(that.currentvalue), classname = that.classes.suggestion, classselected = that.classes.selected, container = $(that.suggestionscontainer), html = '', width; // build suggestions inner html: $.each(that.suggestions, function (i, suggestion) { html += '
' + formatresult(suggestion, value) + '
'; }); // 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. // -2px to account for suggestions border. if (that.options.width === 'auto') { width = that.el.outerwidth() - 2; container.width(width > 0 ? width : 300); } container.html(html).show(); that.visible = true; // select first value by default: if (that.options.autoselectfirst) { that.selectedindex = 0; container.children().first().addclass(classselected); } that.findbesthint(); }, 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; }, processresponse: function (response, originalquery) { var that = this, options = that.options, result = options.transformresult(response, originalquery); result.suggestions = that.verifysuggestionsformat(result.suggestions); // cache results if cache is not disabled: if (!options.nocache) { that.cachedresponse[result[options.paramname]] = result; if (result.suggestions.length === 0) { that.badqueries.push(result[options.paramname]); } } // display suggestions only if returned query matches current value: if (originalquery === that.getquery(that.currentvalue)) { that.suggestions = result.suggestions; that.suggest(); } }, activate: function (index) { var that = this, activeitem, selected = that.classes.selected, container = $(that.suggestionscontainer), children = container.children(); container.children('.' + 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().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), offsettop, upperbound, lowerbound, heightdelta = 25; if (!activeitem) { return; } 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); } 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); 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', that.fixpositioncapture); $(that.suggestionscontainer).remove(); } }; // create chainable jquery plugin: $.fn.autocomplete = function (options, args) { var datakey = 'autocomplete'; // if function invoked without argument return // instance of the first matched element: if (arguments.length === 0) { 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); } }); }; }));