PageSourceSearch

https://katadyngroup.com/cache-buster-1652339379/static/js/jquery.typeahead.js

js katadyngroup.com collected 2026-10-03 23:14:52 UTC 160,624 bytes, 3,931 lines download raw bytes

1/*!
2 * jQuery Typeahead
3 * Copyright (C) 2019 RunningCoder.org
4 * Licensed under the MIT license
5 *
6 * @author Tom Bertrand
7 * @version 2.11.0 (2019-10-31)
8 * @link http://www.runningcoder.org/jquerytypeahead/
9 */
10(function (factory) {
11    if (typeof define === "function" && define.amd) {
12        define("jquery-typeahead", ["jquery"], function (jQuery) {
13            return factory(jQuery);
14        });
15    } else if (typeof module === "object" && module.exports) {
16        module.exports = (function (jQuery, root) {
17            if (jQuery === undefined) {
18                if (typeof window !== "undefined") {
19                    jQuery = require("jquery");
20                } else {
21                    jQuery = require("jquery")(root);
22                }
23            }
24            return factory(jQuery);
25        })();
26    } else {
27        factory(jQuery);
28    }
29})(function ($) {
30    "use strict";
31
32    window.Typeahead = {
33        version: '2.11.0'
34    };
35
36    /**
37     * @private
38     * Default options
39     * @link http://www.runningcoder.org/jquerytypeahead/documentation/
40     */
41    var _options = {
42        input: null,                // *RECOMMENDED*, jQuery selector to reach Typeahead's input for initialization
43        minLength: 2,               // Accepts 0 to search on focus, minimum character length to perform a search
44        maxLength: false,           // False as "Infinity" will not put character length restriction for searching results
45        maxItem: 8,                 // Accepts 0 / false as "Infinity" meaning all the results will be displayed
46        dynamic: false,             // When true, Typeahead will get a new dataset from the source option on every key press
47        delay: 300,                 // delay in ms when dynamic option is set to true
48        order: null,                // "asc" or "desc" to sort results
49        offset: false,              // Set to true to match items starting from their first character
50        hint: false,                // Added support for excessive "space" characters
51        accent: false,              // Will allow to type accent and give letter equivalent results, also can define a custom replacement object
52        highlight: true,            // Added "any" to highlight any word in the template, by default true will only highlight display keys
53        multiselect: null,          // Multiselect configuration object, see documentation for all options
54        group: false,               // Improved feature, Boolean,string,object(key, template (string, function))
55        groupOrder: null,           // New feature, order groups "asc", "desc", Array, Function
56        maxItemPerGroup: null,      // Maximum number of result per Group
57        dropdownFilter: false,      // Take group options string and create a dropdown filter
58        dynamicFilter: null,        // Filter the typeahead results based on dynamic value, Ex: Players based on TeamID
59        backdrop: false,            // Add a backdrop behind Typeahead results
60        backdropOnFocus: false,     // Display the backdrop option as the Typeahead input is :focused
61        cache: false,               // Improved option, true OR 'localStorage' OR 'sessionStorage'
62        ttl: 3600000,               // Cache time to live in ms
63        compression: false,         // Requires LZString library
64        searchOnFocus: false,       // Display search results on input focus
65        blurOnTab: true,            // Blur Typeahead when Tab key is pressed, if false Tab will go though search results
66        resultContainer: null,      // List the results inside any container string or jQuery object
67        generateOnLoad: null,       // Forces the source to be generated on page load even if the input is not focused!
68        mustSelectItem: false,      // The submit function only gets called if an item is selected
69        href: null,                 // String or Function to format the url for right-click & open in new tab on link results
70        display: ["display"],       // Allows search in multiple item keys ["display1", "display2"]
71        template: null,             // Display template of each of the result list
72        templateValue: null,        // Set the input value template when an item is clicked
73        groupTemplate: null,        // Set a custom template for the groups
74        correlativeTemplate: false, // Compile display keys, enables multiple key search from the template string
75        emptyTemplate: false,       // Display an empty template if no result
76        cancelButton: true,         // If text is detected in the input, a cancel button will be available to reset the input (pressing ESC also cancels)
77        loadingAnimation: true,     // Display a loading animation when typeahead is doing request / searching for results
78        asyncResult: false,         // If set to true, the search results will be displayed as they are beging received from the requests / async data function
79        filter: true,               // Set to false or function to bypass Typeahead filtering. WARNING: accent, correlativeTemplate, offset & matcher will not be interpreted
80        matcher: null,              // Add an extra filtering function after the typeahead functions
81        source: null,               // Source of data for Typeahead to filter
82        callback: {
83            onInit: null,               // When Typeahead is first initialized (happens only once)
84            onReady: null,              // When the Typeahead initial preparation is completed
85            onShowLayout: null,         // Called when the layout is shown
86            onHideLayout: null,         // Called when the layout is hidden
87            onSearch: null,             // When data is being fetched & analyzed to give search results
88            onResult: null,             // When the result container is displayed
89            onLayoutBuiltBefore: null,  // When the result HTML is build, modify it before it get showed
90            onLayoutBuiltAfter: null,   // Modify the dom right after the results gets inserted in the result container
91            onNavigateBefore: null,     // When a key is pressed to navigate the results, before the navigation happens
92            onNavigateAfter: null,      // When a key is pressed to navigate the results
93            onEnter: null,              // When an item in the result list is focused
94            onLeave: null,              // When an item in the result list is blurred
95            onClickBefore: null,        // Possibility to e.preventDefault() to prevent the Typeahead behaviors
96            onClickAfter: null,         // Happens after the default clicked behaviors has been executed
97            onDropdownFilter: null,     // When the dropdownFilter is changed, trigger this callback
98            onSendRequest: null,        // Gets called when the Ajax request(s) are sent
99            onReceiveRequest: null,     // Gets called when the Ajax request(s) are all received
100            onPopulateSource: null,     // Perform operation on the source data before it gets in Typeahead data
101            onCacheSave: null,          // Perform operation on the source data before it gets in Typeahead cache
102            onSubmit: null,             // When Typeahead form is submitted
103            onCancel: null              // Triggered if the typeahead had text inside and is cleared
104        },
105        selector: {
106            container: "typeahead__container",
107            result: "typeahead__result",
108            list: "typeahead__list",
109            group: "typeahead__group",
110            item: "typeahead__item",
111            empty: "typeahead__empty",
112            display: "typeahead__display",
113            query: "typeahead__query",
114            filter: "typeahead__filter",
115            filterButton: "typeahead__filter-button",
116            dropdown: "typeahead__dropdown",
117            dropdownItem: "typeahead__dropdown-item",
118            labelContainer: "typeahead__label-container",
119            label: "typeahead__label",
120            button: "typeahead__button",
121            backdrop: "typeahead__backdrop",
122            hint: "typeahead__hint",
123            cancelButton: "typeahead__cancel-button"
124        },
125        debug: false // Display debug information (RECOMMENDED for dev environment)
126    };
127
128    /**
129     * @private
130     * Event namespace
131     */
132    var _namespace = ".typeahead";
133
134    /**
135     * @private
136     * Accent equivalents
137     */
138    var _accent = {
139        from: "ãàáäâẽèéëêìíïîõòóöôùúüûñç",
140        to: "aaaaaeeeeeiiiiooooouuuunc"
141    };
142
143    /**
144     * #62 IE9 doesn't trigger "input" event when text gets removed (backspace, ctrl+x, etc)
145     * @private
146     */
147    var _isIE9 = ~window.navigator.appVersion.indexOf("MSIE 9.");
148
149    /**
150     * #193 Clicking on a suggested option does not select it on IE10/11
151     * @private
152     */
153    var _isIE10 = ~window.navigator.appVersion.indexOf("MSIE 10");
154    var _isIE11 = ~window.navigator.userAgent.indexOf("Trident")
155        ? ~window.navigator.userAgent.indexOf("rv:11")
156        : false;
157
158    // SOURCE GROUP RESERVED WORDS: ajax, data, url
159    // SOURCE ITEMS RESERVED KEYS: group, display, data, matchedKey, compiled, href
160
161    /**
162     * @constructor
163     * Typeahead Class
164     *
165     * @param {object} node jQuery input object
166     * @param {object} options User defined options
167     */
168    var Typeahead = function (node, options) {
169        this.rawQuery = node.val() || "";   // Unmodified input query
170        this.query = node.val() || "";      // Input query
171        this.selector = node[0].selector;   // Typeahead instance selector (to reach from window.Typeahead[SELECTOR])
172        this.deferred = null;               // Promise when "input" event in triggered, this.node.triggerHandler('input').then(() => {})
173        this.tmpSource = {};                // Temp var to preserve the source order for the searchResult function
174        this.source = {};                   // The generated source kept in memory
175        this.dynamicGroups = [];            // Store the source groups that are defined as dynamic
176        this.hasDynamicGroups = false;      // Boolean if at least one of the groups has a dynamic source
177        this.generatedGroupCount = 0;       // Number of groups generated, if limit reached the search can be done
178        this.groupBy = "group";             // This option will change according to filtering or custom grouping
179        this.groups = [];                   // Array of all the available groups, used to build the groupTemplate
180        this.searchGroups = [];             // Array of groups to generate when Typeahead searches data
181        this.generateGroups = [];           // Array of groups to generate when Typeahead requests data
182        this.requestGroups = [];            // Array of groups to request via Ajax
183        this.result = [];                   // Results based on Source-query match (only contains the displayed elements)
184        this.tmpResult = {};                // Temporary object of results, before they get passed to the buildLayout function
185        this.groupTemplate = "";            // Result template at the {{group}} level
186        this.resultHtml = null;             // HTML Results (displayed elements)
187        this.resultCount = 0;               // Total results based on Source-query match
188        this.resultCountPerGroup = {};      // Total results based on Source-query match per group
189        this.options = options;             // Typeahead options (Merged default & user defined)
190        this.node = node;                   // jQuery object of the Typeahead <input>
191        this.namespace =
192            "." +
193            this.helper.slugify.call(this, this.selector) +
194            _namespace;                     // Every Typeahead instance gets its own namespace for events
195        this.isContentEditable = typeof this.node.attr('contenteditable') !== "undefined"
196            && this.node.attr('contenteditable') !== "false";
197        this.container = null;              // Typeahead container, usually right after <form>
198        this.resultContainer = null;        // Typeahead result container (html)
199        this.item = null;                   // Selected item
200        this.items = null;                  // Multiselect selected items
201        this.comparedItems = null;          // Multiselect items stored for comparison
202        this.xhr = {};                      // Ajax request(s) stack
203        this.hintIndex = null;              // Numeric value of the hint index in the result list
204        this.filters = {                    // Filter list for searching, dropdown and dynamic(s)
205            dropdown: {},                   // Dropdown menu if options.dropdownFilter is set
206            dynamic: {}                     // Checkbox / Radio / Select to filter the source data
207        };
208        this.dropdownFilter = {
209            static: [],                     // Objects that has a value
210            dynamic: []
211        };
212        this.dropdownFilterAll = null;      // The last "all" definition
213        this.isDropdownEvent = false;       // If a dropdownFilter is clicked, this will be true to trigger the callback
214
215        this.requests = {};                 // Store the group:request instead of generating them every time
216
217        this.backdrop = {};                 // The backdrop object
218        this.hint = {};                     // The hint object
219        this.label = {};                    // The label object
220        this.hasDragged = false;
220            // Will cancel mouseend events if true
221        this.focusOnly = false;             // Focus the input preventing any operations
222        this.displayEmptyTemplate;          // Display the empty template in the result list
223
224        this.__construct();
225    };
226
227    Typeahead.prototype = {
228        _validateCacheMethod: function (cache) {
229            var supportedCache = ["localStorage", "sessionStorage"],
230                supported;
231
232            if (cache === true) {
233                cache = "localStorage";
234            } else if (typeof cache === "string" && !~supportedCache.indexOf(cache)) {
235                // {debug}
236                if (this.options.debug) {
237                    _debug.log({
238                        node: this.selector,
239                        function: "extendOptions()",
240                        message: 'Invalid options.cache, possible options are "localStorage" or "sessionStorage"'
241                    });
242
243                    _debug.print();
244                }
245                // {/debug}
246                return false;
247            }
248
249            supported = typeof window[cache] !== "undefined";
250
251            try {
252                window[cache].setItem("typeahead", "typeahead");
253                window[cache].removeItem("typeahead");
254            } catch (e) {
255                supported = false;
256            }
257
258            return (supported && cache) || false;
259        },
260
261        extendOptions: function () {
262            this.options.cache = this._validateCacheMethod(this.options.cache);
263
264            if (this.options.compression) {
265                if (typeof LZString !== "object" || !this.options.cache) {
266                    // {debug}
267                    if (this.options.debug) {
268                        _debug.log({
269                            node: this.selector,
270                            function: "extendOptions()",
271                            message: "Missing LZString Library or options.cache, no compression will occur."
272                        });
273
274                        _debug.print();
275                    }
276                    // {/debug}
277                    this.options.compression = false;
278                }
279            }
280
281            if (!this.options.maxLength || isNaN(this.options.maxLength)) {
282                this.options.maxLength = Infinity;
283            }
284
285            if (
286                typeof this.options.maxItem !== "undefined" && ~[0, false].indexOf(this.options.maxItem)
287            ) {
288                this.options.maxItem = Infinity;
289            }
290
291            if (
292                this.options.maxItemPerGroup && !/^\d+$/.test(this.options.maxItemPerGroup)
293            ) {
294                this.options.maxItemPerGroup = null;
295            }
296
297            if (this.options.display && !Array.isArray(this.options.display)) {
298                this.options.display = [this.options.display];
299            }
300
301            if (this.options.multiselect) {
302                this.items = [];
303                this.comparedItems = [];
304                if (typeof this.options.multiselect.matchOn === "string") {
305                    this.options.multiselect.matchOn = [this.options.multiselect.matchOn];
306                }
307            }
308
309            if (this.options.group) {
310                if (!Array.isArray(this.options.group)) {
311                    if (typeof this.options.group === "string") {
312                        this.options.group = {
313                            key: this.options.group
314                        };
315                    } else if (typeof this.options.group === "boolean") {
316                        this.options.group = {
317                            key: "group"
318                        };
319                    }
320
321                    this.options.group.key = this.options.group.key || "group";
322                } else {
323                    // {debug}
324                    if (this.options.debug) {
325                        _debug.log({
326                            node: this.selector,
327                            function: "extendOptions()",
328                            message: "options.group must be a boolean|string|object as of 2.5.0"
329                        });
330
331                        _debug.print();
332                    }
333                    // {/debug}
334                }
335            }
336
337            if (this.options.highlight && !~["any", true].indexOf(this.options.highlight)) {
338                this.options.highlight = false;
339            }
340
341            if (
342                this.options.dropdownFilter &&
343                this.options.dropdownFilter instanceof Object
344            ) {
345                if (!Array.isArray(this.options.dropdownFilter)) {
346                    this.options.dropdownFilter = [this.options.dropdownFilter];
347                }
348                for (var i = 0, ii = this.options.dropdownFilter.length; i < ii; ++i) {
349                    this.dropdownFilter[
350                        this.options.dropdownFilter[i].value ? "static" : "dynamic"
351                        ].push(this.options.dropdownFilter[i]);
352                }
353            }
354
355            if (this.options.dynamicFilter && !Array.isArray(this.options.dynamicFilter)) {
356                this.options.dynamicFilter = [this.options.dynamicFilter];
357            }
358
359            if (this.options.accent) {
360                if (typeof this.options.accent === "object") {
361                    if (
362                        this.options.accent.from &&
363                        this.options.accent.to &&
364                        this.options.accent.from.length !== this.options.accent.to.length
365                    ) {
366                        // {debug}
367                        if (this.options.debug) {
368                            _debug.log({
369                                node: this.selector,
370                                function: "extendOptions()",
371                                message: 'Invalid "options.accent", from and to must be defined and same length.'
372                            });
373
374                            _debug.print();
375                        }
376                        // {/debug}
377                    }
378
379                } else {
380                    this.options.accent = _accent;
381                }
382            }
383
384            if (this.options.groupTemplate) {
385                this.groupTemplate = this.options.groupTemplate;
386            }
387
388            if (this.options.resultContainer) {
389                if (typeof this.options.resultContainer === "string") {
390                    this.options.resultContainer = $(this.options.resultContainer);
391                }
392
393                if (
394                    !(this.options.resultContainer instanceof $) || !this.options.resultContainer[0]
395                ) {
396                    // {debug}
397                    if (this.options.debug) {
398                        _debug.log({
399                            node: this.selector,
400                            function: "extendOptions()",
401                            message: 'Invalid jQuery selector or jQuery Object for "options.resultContainer".'
402                        });
403
404                        _debug.print();
405                    }
406                    // {/debug}
407                } else {
408                    this.resultContainer = this.options.resultContainer;
409                }
410            }
411
412            if (
413                this.options.group &&
414                this.options.group.key
415            ) {
416                this.groupBy = this.options.group.key;
417            }
418
419            // Compatibility onClick callback
420            if (this.options.callback && this.options.callback.onClick) {
421                this.options.callback.onClickBefore = this.options.callback.onClick;
422                delete this.options.callback.onClick;
423            }
424
425            // Compatibility onNavigate callback
426            if (this.options.callback && this.options.callback.onNavigate) {
427                this.options.callback.onNavigateBefore = this.options.callback.onNavigate;
428                delete this.options.callback.onNavigate;
429            }
430
431            this.options = $.extend(true, {}, _options, this.options);
432        },
433
434        unifySourceFormat: function () {
435            this.dynamicGroups = [];
436
437            // source: ['item1', 'item2', 'item3']
438            if (Array.isArray(this.options.source)) {
439                this.options.source = {
440                    group: {
441                        data: this.options.source
442                    }
443                };
444            }
445
446            // source: "http://www.test.com/url.json"
447            if (typeof this.options.source === "string") {
448                this.options.source = {
449                    group: {
450                        ajax: {
451                            url: this.options.source
452                        }
453                    }
454                };
455            }
456
457            if (this.options.source.ajax) {
458                this.options.source = {
459                    group: {
460                        ajax: this.options.source.ajax
461                    }
462                };
463            }
464
465            // source: {data: ['item1', 'item2'], url: "http://www.test.com/url.json"}
466            if (this.options.source.url || this.options.source.data) {
467                this.options.source = {
468                    group: this.options.source
469                };
470            }
471
472            var group, groupSource, tmpAjax;
473
474            for (group in this.options.source) {
475                if (!this.options.source.hasOwnProperty(group)) continue;
476
477                groupSource = this.options.source[group];
478
479                // source: {group: "http://www.test.com/url.json"}
480                if (typeof groupSource === "string") {
481                    groupSource = {
482                        ajax: {
483                            url: groupSource
484                        }
485                    };
486                }
487
488                // source: {group: {url: ["http://www.test.com/url.json", "json.path"]}}
489                tmpAjax = groupSource.url || groupSource.ajax;
490                if (Array.isArray(tmpAjax)) {
491                    groupSource.ajax =
492                        typeof tmpAjax[0] === "string"
493                            ? {
494                                url: tmpAjax[0]
495                            }
496                            : tmpAjax[0];
497                    groupSource.ajax.path = groupSource.ajax.path || tmpAjax[1] || null;
498                    delete groupSource.url;
499                } else {
500                    // source: {group: {url: {url: "http://www.test.com/url.json", method: "GET"}}}
501                    // source: {group: {url: "http://www.test.com/url.json", dataType: "jsonp"}}
502                    if (typeof groupSource.url === "object") {
503                        groupSource.ajax = groupSource.url;
504                    } else if (typeof groupSource.url === "string") {
505                        groupSource.ajax = {
506                            url: groupSource.url
507                        };
508                    }
509                    delete groupSource.url;
510                }
511
512                if (!groupSource.data && !groupSource.ajax) {
513                    // {debug}
514                    if (this.options.debug) {
515                        _debug.log({
516                            node: this.selector,
517                            function: "unifySourceFormat()",
518                            arguments: JSON.stringify(this.options.source),
519                            message: 'Undefined "options.source.' +
520                            group +
521                            '.[data|ajax]" is Missing - Typeahead dropped'
522                        });
523
524                        _debug.print();
525                    }
526                    // {/debug}
527
528                    return false;
529                }
530
531                if (groupSource.display && !Array.isArray(groupSource.display)) {
532                    groupSource.display = [groupSource.display];
533                }
534
535                groupSource.minLength =
536                    typeof groupSource.minLength === "number"
537                        ? groupSource.minLength
538                        : this.options.minLength;
539                groupSource.maxLength =
540                    typeof groupSource.maxLength === "number"
541                        ? groupSource.maxLength
542                        : this.options.maxLength;
543                groupSource.dynamic =
544                    typeof groupSource.dynamic === "boolean" || this.options.dynamic;
545
546                if (groupSource.minLength > groupSource.maxLength) {
547                    groupSource.minLength = groupSource.maxLength;
548                }
549                this.options.source[group] = groupSource;
550
551                if (this.options.source[group].dynamic) {
552                    this.dynamicGroups.push(group);
553                }
554
555                groupSource.cache =
556                    typeof groupSource.cache !== "undefined"
557                        ? this._validateCacheMethod(groupSource.cache)
558                        : this.options.cache;
559
560                if (groupSource.compression) {
561                    if (typeof LZString !== "object" || !groupSource.cache) {
562                        // {debug}
563                        if (this.options.debug) {
564                            _debug.log({
565                                node: this.selector,
566                                function: "unifySourceFormat()",
567                                message: "Missing LZString Library or group.cache, no compression will occur on group: " +
568                                group
569                            });
570
571                            _debug.print();
572                        }
573                        // {/debug}
574                        groupSource.compression = false;
575                    }
576                }
577            }
578
579            this.hasDynamicGroups =
580                this.options.dynamic || !!this.dynamicGroups.length;
581
582            return true;
583        },
584
585        init: function () {
586            this.helper.executeCallback.call(this, this.options.callback.onInit, [
587                this.node
588            ]);
589
590            this.container = this.node.closest("." + this.options.selector.container);
591
592            // {debug}
593            if (this.options.debug) {
594                _debug.log({
595                    node: this.selector,
596                    function: "init()",
597                    //'arguments': JSON.stringify(this.options),
598                    message: "OK - Typeahead activated on " + this.selector
599                });
600
601                _debug.print();
602            }
603            // {/debug}
604        },
605
606        delegateEvents: function () {
607            var scope = this,
608                events = [
609                    "focus" + this.namespace,
610                    "input" + this.namespace,
611                    "propertychange" + this.namespace, // IE8 Fix
612                    "keydown" + this.namespace,
613                    "keyup" + this.namespace, // IE9 Fix
614                    "search" + this.namespace,
615                    "generate" + this.namespace
616                ];
617
618            // #149 - Adding support for Mobiles
619            $("html")
620                .on("touchmove", function () {
621                    scope.hasDragged = true;
622                })
623                .on("touchstart", function () {
624                    scope.hasDragged = false;
625                });
626
627            this.node
628                .closest("form")
629                .on("submit", function (e) {
630                    if (
631                        scope.options.mustSelectItem &&
632                        scope.helper.isEmpty(scope.item)
633                    ) {
634                        e.preventDefault();
635                        return;
636                    }
637
638                    if (!scope.options.backdropOnFocus) {
639                        scope.hideLayout();
640                    }
641
642                    if (scope.options.callback.onSubmit) {
643                        return scope.helper.executeCallback.call(
644                            scope,
645                            scope.options.callback.onSubmit,
646                            [scope.node, this, scope.item || scope.items, e]
647                        );
648                    }
649                })
650                .on("reset", function () {
651                    // #221 - Reset Typeahead on form reset.
652                    // setTimeout to re-queue the `input.typeahead` event at the end
653                    setTimeout(function () {
654                        scope.node.trigger("input" + scope.namespace);
655                        // #243 - minLength: 0 opens the Typeahead results
656                        scope.hideLayout();
657                    });
658                });
659
660            // IE8 fix
661            var preventNextEvent = false;
662
663            // IE10/11 fix
664            if (this.node.attr("placeholder") && (_isIE10 || _isIE11)) {
665                var preventInputEvent = true;
666
667                this.node.on("focusin focusout", function () {
668                    preventInputEvent = !!(!this.value && this.placeholder);
669                });
670
671                this.node.on("input", function (e) {
672                    if (preventInputEvent) {
673                        e.stopImmediatePropagation();
674                        preventInputEvent = false;
675                    }
676                });
677            }
678
679            this.node
680                .off(this.namespace)
681                .on(events.join(" "), function (e, data) {
682                    switch (e.type) {
683                        case "generate":
684                            scope.generateSource(Object.keys(scope.options.source));
685                            break;
686                        case "focus":
687                            if (scope.focusOnly) {
688                                scope.focusOnly = false;
689                                break;
690                            }
691                            if (scope.options.backdropOnFocus) {
692                                scope.buildBackdropLayout();
693                                scope.showLayout();
694                            }
695                            if (scope.options.searchOnFocus && !scope.item) {
696                                scope.deferred = $.Deferred();
697                                scope.assignQuery();
698                                scope.generateSource();
699                            }
700                            break;
701                        case "keydown":
702                            if (e.keyCode === 8
703                                && scope.options.multiselect
704                                && scope.options.multiselect.cancelOnBackspace
705                                && scope.query === ''
706                                && scope.items.length
707                            ) {
708                                scope.cancelMultiselectItem(scope.items.length - 1, null, e);
709                            } else if (e.keyCode && ~[9, 13, 27, 38, 39, 40].indexOf(e.keyCode)) {
710                                preventNextEvent = true;
711                                scope.navigate(e);
712                            }
713                            break;
714                        case "keyup":
715                            if (
716                                _isIE9 &&
717                                scope.node[0].value.replace(/^\s+/, "").toString().length <
718                                scope.query.length
719                            ) {
720                                scope.node.trigger("input" + scope.namespace);
721                            }
722                            break;
723                        case "propertychange":
724                            if (preventNextEvent) {
725                                preventNextEvent = false;
726                                break;
727                            }
728                        case "input":
729                            scope.deferred = $.Deferred();
730                            scope.assignQuery();
731
732                            // #195 Trigger an onCancel event if the Typeahead is cleared
733                            if (scope.rawQuery === "" && scope.query === "") {
734                                e.originalEvent = data || {};
735                                scope.helper.executeCallback.call(
736                                    scope,
737                                    scope.options.callback.onCancel,
738                                    [scope.node, scope.item, e]
739                                );
740                                scope.item = null;
741                            }
742
743                            scope.options.cancelButton &&
744                            scope.toggleCancelButtonVisibility();
745
746                            if (
747                                scope.options.hint &&
748                                scope.hint.container &&
749                                scope.hint.container.val() !== ""
750                            ) {
751                                if (scope.hint.container.val().indexOf(scope.rawQuery) !== 0) {
752                                    scope.hint.container.val("");
753                                    if (scope.isContentEditable) {
754                                        scope.hint.container.text("");
755                                    }
756                                }
757                            }
758
759                            if (scope.hasDynamicGroups) {
760                                scope.helper.typeWatch(function () {
761                                    scope.generateSource();
762                                }, scope.options.delay);
763                            } else {
764                                scope.generateSource();
765                            }
766                            break;
767                        case "search":
768                            scope.searchResult();
769                            scope.buildLayout();
770
771                            if (scope.result.length ||
772                                (scope.searchGroups.length &&
773                                scope.displayEmptyTemplate)
774                            ) {
775                                scope.showLayout();
776                            } else {
777                                scope.hideLayout();
778                            }
779
780                            scope.deferred && scope.deferred.resolve();
781                            break;
782                    }
783
784                    return scope.deferred && scope.deferred.promise();
785                });
786
787            if (this.options.generateOnLoad) {
788                this.node.trigger("generate" + this.namespace);
789            }
790        },
791
792        assignQuery: function () {
793            if (this.isContentEditable) {
794                this.rawQuery = this.node.text();
795            } else {
796                this.rawQuery = this.node.val().toString();
797            }
798            this.rawQuery = this.rawQuery.replace(/^\s+/, "");
799
800            if (this.rawQuery !== this.query) {
801                this.query = this.rawQuery;
802            }
803        },
804
805        filterGenerateSource: function () {
806            this.searchGroups = [];
807            this.generateGroups = [];
808
809            if (this.focusOnly && !this.options.multiselect) return;
810
811            for (var group in this.options.source) {
812                if (!this.options.source.hasOwnProperty(group)) continue;
813                if (
814                    this.query.length >= this.options.source[group].minLength &&
815                    this.query.length <= this.options.source[group].maxLength
816                ) {
817                    if (
818                        this.filters.dropdown &&
819                        this.filters.dropdown.key === 'group' &&
820                        this.filters.dropdown.value !== group
821                    ) {
822                        continue;
823                    }
824
825                    this.searchGroups.push(group);
826                    if (!this.options.source[group].dynamic && this.source[group]) {
827                        continue;
828                    }
829                    this.generateGroups.push(group);
830                }
831            }
832        },
833
834        generateSource: function (generateGroups) {
835            this.filterGenerateSource();
836
837            this.generatedGroupCount = 0;
838
839            if (Array.isArray(generateGroups) && generateGroups.length) {
840                this.generateGroups = generateGroups;
841            } else if (!this.generateGroups.length) {
842                this.node.trigger("search" + this.namespace);
843                return;
844            }
845
846            this.requestGroups = [];
847            this.options.loadingAnimation && this.container.addClass("loading");
848
849            if (!this.helper.isEmpty(this.xhr)) {
850                for (var i in this.xhr) {
851                    if (!this.xhr.hasOwnProperty(i)) continue;
852                    this.xhr[i].abort();
853                }
854                this.xhr = {};
855            }
856
857            var scope = this,
858                group,
859                groupData,
860                groupSource,
861                cache,
862                compression,
863                dataInStorage,
864                isValidStorage;
865
866            for (var i = 0, ii = this.generateGroups.length; i < ii; ++i) {
867                group = this.generateGroups[i];
868                groupSource = this.options.source[group];
869                cache = groupSource.cache;
870                compression = groupSource.compression;
871
872                if (this.options.asyncResult) {
873                    delete this.source[group];
874                }
875
876                if (cache) {
877                    dataInStorage = window[cache].getItem(
878                        "TYPEAHEAD_" + this.selector + ":" + group
879                    );
880                    if (dataInStorage) {
881                        if (compression) {
882                            dataInStorage = LZString.decompressFromUTF16(dataInStorage);
883                        }
884
885                        isValidStorage = false;
886                        try {
887                            dataInStorage = JSON.parse(dataInStorage + "");
888
889                            if (
890                                dataInStorage.data &&
891                                dataInStorage.ttl > new Date().getTime()
892                            ) {
893                                this.populateSource(dataInStorage.data, group);
894                                isValidStorage = true;
895
896                                // {debug}
897                                if (this.options.debug) {
898                                    _debug.log({
899                                        node: this.selector,
900                                        function: "generateSource()",
901                                        message: 'Source for group "' + group + '" found in ' + cache
902                                    });
903                                    _debug.print();
904                                }
905                                // {/debug}
906                            } else {
907                                window[cache].removeItem(
908                                    "TYPEAHEAD_" + this.selector + ":" + group
909                                );
910                            }
911                        } catch (error) {
912                        }
913
914                        if (isValidStorage) continue;
915                    }
916                }
917
918                if (groupSource.data && !groupSource.ajax) {
919                    // #198 Add support for async data source
920                    if (typeof groupSource.data === "function") {
921                        groupData = groupSource.data.call(this);
922                        if (Array.isArray(groupData)) {
923                            scope.populateSource(groupData, group);
924                        } else if (typeof groupData.promise === "function") {
925                            (function (group) {
926                                $.when(groupData).then(function (deferredData) {
927                                    if (deferredData && Array.isArray(deferredData)) {
928                                        scope.populateSource(deferredData, group);
929                                    }
930                                });
931                            })(group);
932                        }
933                    } else {
934                        this.populateSource($.extend(true, [], groupSource.data), group);
935                    }
936                    continue;
937                }
938
939                if (groupSource.ajax) {
940                    if (!this.requests[group]) {
941                        this.requests[group] = this.generateRequestObject(group);
942                    }
943                    this.requestGroups.push(group);
944                }
945            }
946
947            if (this.requestGroups.length) {
948                this.handleRequests();
949            }
950
951            if (this.options.asyncResult && this.searchGroups.length !== this.generateGroups) {
952                this.node.trigger("search" + this.namespace);
953            }
954
955            return !!this.generateGroups.length;
956        },
957
958        generateRequestObject: function (group) {
959            var scope = this,
960                groupSource = this.options.source[group];
961
962            var xhrObject = {
963                request: {
964                    url: groupSource.ajax.url || null,
965                    dataType: "json",
966                    beforeSend: function (jqXHR, options) {
967                        // Important to call .abort() in case of dynamic requests
968                        scope.xhr[group] = jqXHR;
969
970                        var beforeSend =
971                            scope.requests[group].callback.beforeSend ||
972                            groupSource.ajax.beforeSend;
973                        typeof beforeSend === "function" &&
974                        beforeSend.apply(null, arguments);
975                    }
976                },
977                callback: {
978                    beforeSend: null,
979                    done: null,
980                    fail: null,
981                    then: null,
982                    always: null
983                },
984                extra: {
985                    path: groupSource.ajax.path || null,
986                    group: group
987                },
988                validForGroup: [group]
989            };
990
991            if (typeof groupSource.ajax !== "function") {
992                if (groupSource.ajax instanceof Object) {
993                    xhrObject = this.extendXhrObject(xhrObject, groupSource.ajax);
994                }
995
996                if (Object.keys(this.options.source).length > 1) {
997                    for (var _group in this.requests) {
998                        if (!this.requests.hasOwnProperty(_group)) continue;
999                        if (this.requests[_group].isDuplicated) continue;
1000
1001                        if (
1002                            xhrObject.request.url &&
1003                            xhrObject.request.url === this.requests[_group].request.url
1004                        ) {
1005                            this.requests[_group].validForGroup.push(group);
1006                            xhrObject.isDuplicated = true;
1007                            delete xhrObject.validForGroup;
1008                        }
1009                    }
1010                }
1011            }
1012
1013            return xhrObject;
1014        },
1015
1016        extendXhrObject: function (xhrObject, groupRequest) {
1017            if (typeof groupRequest.callback === "object") {
1018                xhrObject.callback = groupRequest.callback;
1019                delete groupRequest.callback;
1020            }
1021
1022            // #132 Fixed beforeSend when using a function as the request object
1023            if (typeof groupRequest.beforeSend === "function") {
1024                xhrObject.callback.beforeSend = groupRequest.beforeSend;
1025                delete groupRequest.beforeSend;
1026            }
1027
1028            // Fixes #105 Allow user to define their beforeSend function.
1029            // Fixes #181 IE8 incompatibility
1030            xhrObject.request = $.extend(true, xhrObject.request, groupRequest);
1031
1032            // JSONP needs a unique jsonpCallback to run concurrently
1033            if (
1034                xhrObject.request.dataType.toLowerCase() === "jsonp" && !xhrObject.request.jsonpCallback
1035            ) {
1036                xhrObject.request.jsonpCallback = "callback_" + xhrObject.extra.group;
1037            }
1038
1039            return xhrObject;
1040        },
1041
1042        handleRequests: function () {
1043            var scope = this,
1044                group,
1045                requestsCount = this.requestGroups.length;
1046
1047            if (
1048                this.helper.executeCallback.call(
1049                    this,
1050                    this.options.callback.onSendRequest,
1051                    [this.node, this.query]
1052                ) === false
1053            ) {
1054                return;
1055            }
1056
1057            for (var i = 0, ii = this.requestGroups.length; i < ii; ++i) {
1058                group = this.requestGroups[i];
1059                if (this.requests[group].isDuplicated) continue;
1060
1061                (function (group, xhrObject) {
1062                    if (typeof scope.options.source[group].ajax === "function") {
1063                        var _groupRequest = scope.options.source[group].ajax.call(
1064                            scope,
1065                            scope.query
1066                        );
1067
1068                        // Fixes #271 Data is cached inside the xhrObject
1069                        xhrObject = scope.extendXhrObject(
1070                            scope.generateRequestObject(group),
1071                            typeof _groupRequest === "object" ? _groupRequest : {}
1072                        );
1073
1074                        if (
1075                            typeof xhrObject.request !== "object" || !xhrObject.request.url
1076                        ) {
1077                            // {debug}
1078                            if (scope.options.debug) {
1079                                _debug.log({
1080                                    node: scope.selector,
1081                                    function: "handleRequests",
1082                                    message: 'Source function must return an object c
1082ontaining ".url" key for group "' +
1083                                    group +
1084                                    '"'
1085                                });
1086                                _debug.print();
1087                            }
1088                            // {/debug}
1089                            scope.populateSource([], group);
1090                            return;
1091                        }
1092                        scope.requests[group] = xhrObject;
1093                    }
1094
1095                    var _request,
1096                        _isExtended = false, // Prevent the main request from being changed
1097                        _groupData = {};
1098
1099                    if (~xhrObject.request.url.indexOf("{{query}}")) {
1100                        if (!_isExtended) {
1101                            xhrObject = $.extend(true, {}, xhrObject);
1102                            _isExtended = true;
1103                        }
1104                        // #184 Invalid encoded characters on dynamic requests for `{{query}}`
1105                        xhrObject.request.url = xhrObject.request.url.replace(
1106                            "{{query}}",
1107                            encodeURIComponent(scope.query)
1108                        );
1109                    }
1110
1111                    if (xhrObject.request.data) {
1112                        for (var i in xhrObject.request.data) {
1113                            if (!xhrObject.request.data.hasOwnProperty(i)) continue;
1114                            if (~String(xhrObject.request.data[i]).indexOf("{{query}}")) {
1115                                if (!_isExtended) {
1116                                    xhrObject = $.extend(true, {}, xhrObject);
1117                                    _isExtended = true;
1118                                }
1119                                // jQuery handles encodeURIComponent when the query is inside the data object
1120                                xhrObject.request.data[i] = xhrObject.request.data[i].replace(
1121                                    "{{query}}",
1122                                    scope.query
1123                                );
1124                                break;
1125                            }
1126                        }
1127                    }
1128
1129                    $.ajax(xhrObject.request)
1130                        .done(function (data, textStatus, jqXHR) {
1131                            var _group;
1132
1133                            for (
1134                                var i = 0, ii = xhrObject.validForGroup.length;
1135                                i < ii;
1136                                i++
1137                            ) {
1138                                _group = xhrObject.validForGroup[i];
1139                                _request = scope.requests[_group];
1140
1141                                if (typeof _request.callback.done === 'function') {
1142                                    _groupData[_group] = _request.callback.done.call(
1143                                        scope,
1144                                        data,
1145                                        textStatus,
1146                                        jqXHR
1147                                    );
1148
1149                                    // {debug}
1150                                    if (
1151                                        !Array.isArray(_groupData[_group]) ||
1152                                        typeof _groupData[_group] !== "object"
1153                                    ) {
1154                                        if (scope.options.debug) {
1155                                            _debug.log({
1156                                                node: scope.selector,
1157                                                function: "Ajax.callback.done()",
1158                                                message: "Invalid returned data has to be an Array"
1159                                            });
1160                                            _debug.print();
1161                                        }
1162                                    }
1163                                    // {/debug}
1164                                }
1165                            }
1166                        })
1167                        .fail(function (jqXHR, textStatus, errorThrown) {
1168                            for (
1169                                var i = 0, ii = xhrObject.validForGroup.length;
1170                                i < ii;
1171                                i++
1172                            ) {
1173                                _request = scope.requests[xhrObject.validForGroup[i]];
1174                                _request.callback.fail instanceof Function &&
1175                                _request.callback.fail.call(
1176                                    scope,
1177                                    jqXHR,
1178                                    textStatus,
1179                                    errorThrown
1180                                );
1181                            }
1182
1183                            // {debug}
1184                            if (scope.options.debug) {
1185                                _debug.log({
1186                                    node: scope.selector,
1187                                    function: "Ajax.callback.fail()",
1188                                    arguments: JSON.stringify(xhrObject.request),
1189                                    message: textStatus
1190                                });
1191
1192                                console.log(errorThrown);
1193
1194                                _debug.print();
1195                            }
1196                            // {/debug}
1197                        })
1198                        .always(function (data, textStatus, jqXHR) {
1199                            var _group;
1200                            for (
1201                                var i = 0, ii = xhrObject.validForGroup.length;
1202                                i < ii;
1203                                i++
1204                            ) {
1205                                _group = xhrObject.validForGroup[i];
1206                                _request = scope.requests[_group];
1207                                _request.callback.always instanceof Function &&
1208                                _request.callback.always.call(scope, data, textStatus, jqXHR);
1209
1210                                // #248, #303 Aborted requests would call populate with invali
1210d data
1211                                if (textStatus === 'abort') return;
1212
1213                                // #265 Modified data from ajax.callback.done is not being registered (use of _groupData[_group])
1214                                scope.populateSource(
1215                                    (data !== null && typeof data.promise === "function" && []) ||
1216                                    _groupData[_group] ||
1217                                    data,
1218                                    _request.extra.group,
1219                                    _request.extra.path || _request.request.path
1220                                );
1221
1222                                requestsCount -= 1;
1223                                if (requestsCount === 0) {
1224                                    scope.helper.executeCallback.call(
1225                                        scope,
1226                                        scope.options.callback.onReceiveRequest,
1227                                        [scope.node, scope.query]
1228                                    );
1229                                }
1230                            }
1231                        })
1232                        .then(function (jqXHR, textStatus) {
1233                            for (
1234                                var i = 0, ii = xhrObject.validForGroup.length;
1235                                i < ii;
1236                                i++
1237                            ) {
1238                                _request = scope.requests[xhrObject.validForGroup[i]];
1239                                _request.callback.then instanceof Function &&
1240                                _request.callback.then.call(scope, jqXHR, textStatus);
1241                            }
1242                        });
1243                })(group, this.requests[group]);
1244            }
1245        },
1246
1247        /**
1248         * Build the source groups to be cycled for matched results
1249         *
1250         * @param {Array} data Array of Strings or Array of Objects
1251         * @param {String} group
1252         * @param {String} [path]
1253         * @return {*}
1254         */
1255        populateSource: function (data, group, path) {
1256            var scope = this,
1257                groupSource = this.options.source[group],
1258                extraData = groupSource.ajax && groupSource.data;
1259
1260            if (path && typeof path === "string") {
1261                data = this.helper.namespace.call(this, path, data);
1262            }
1263
1264            if (typeof data === "undefined") {
1265                // {debug}
1266                if (this.options.debug) {
1267                    _debug.log({
1268                        node: this.selector,
1269                        function: "populateSource()",
1270                        arguments: path,
1271                        message: "Invalid data path."
1272                    });
1273
1274                    _debug.print();
1275                }
1276                // {/debug}
1277            }
1278
1279            if (!Array.isArray(data)) {
1280                // {debug}
1281                if (this.options.debug) {
1282                    _debug.log({
1283                        node: this.selector,
1284                        function: "populateSource()",
1285                        arguments: JSON.stringify({group: group}),
1286                        message: "Invalid data type, must be Array type."
1287                    });
1288                    _debug.print();
1289                }
1290                // {/debug}
1291                data = [];
1292            }
1293
1294            if (extraData) {
1295                if (typeof extraData === "function") {
1296                    extraData = extraData();
1297                }
1298
1299                if (Array.isArray(extraData)) {
1300                    data = data.concat(extraData);
1301                } else {
1302                    // {debug}
1303                    if (this.options.debug) {
1304                        _debug.log({
1305                            node: this.selector,
1306                            function: "populateSource()",
1307                            arguments: JSON.stringify(extraData),
1308                            message: "WARNING - this.options.source." +
1309                            group +
1310                            ".data Must be an Array or a function that returns an Array."
1311                        });
1312
1313                        _debug.print();
1314                    }
1315                    // {/debug}
1316                }
1317            }
1318
1319            var tmpObj,
1320                display = groupSource.display
1321                    ? groupSource.display[0] === "compiled"
1322                        ? groupSource.display[1]
1323                        : groupSource.display[0]
1324                    : this.options.display[0] === "compiled"
1325                        ? this.options.display[1]
1326                        : this.options.display[0];
1327
1328            for (var i = 0, ii = data.length; i < ii; i++) {
1329                if (data[i] === null || typeof data[i] === "boolean") {
1330                    // {debug}
1331                    if (this.options.debug) {
1332                        _debug.log({
1333                            node: this.selector,
1334                            function: "populateSource()",
1335                            message: "WARNING - NULL/BOOLEAN value inside " +
1336                            group +
1337                            "! The data was skipped."
1338                        });
1339
1340                        _debug.print();
1341                    }
1342                    // {/debug}
1343                    continue;
1344                }
1345                if (typeof data[i] === "string") {
1346                    tmpObj = {};
1347                    tmpObj[display] = data[i];
1348                    data[i] = tmpObj;
1349                }
1350                data[i].group = group;
1351            }
1352
1353            if (!this.hasDynamicGroups && this.dropdownFilter.dynamic.length) {
1354                var key,
1355                    value,
1356                    tmpValues = {};
1357
1358                for (var i = 0, ii = data.length; i < ii; i++) {
1359                    for (
1360                        var k = 0, kk = this.dropdownFilter.dynamic.length;
1361                        k < kk;
1362                        k++
1363                    ) {
1364                        key = this.dropdownFilter.dynamic[k].key;
1365
1366                        value = data[i][key];
1367                        if (!value) continue;
1368                        if (!this.dropdownFilter.dynamic[k].value) {
1369                            this.dropdownFilter.dynamic[k].value = [];
1370                        }
1371                        if (!tmpValues[key]) {
1372                            tmpValues[key] = [];
1373                        }
1374                        if (!~tmpValues[key].indexOf(value.toLowerCase())) {
1375                            tmpValues[key].push(value.toLowerCase());
1376                            this.dropdownFilter.dynamic[k].value.push(value);
1377                        }
1378                    }
1379                }
1380            }
1381
1382            if (this.options.correlativeTemplate) {
1383                var template = groupSource.template || this.options.template,
1384                    compiledTemplate = "";
1385
1386                if (typeof template === "function") {
1387                    template = template.call(this, "", {});
1388                }
1389
1390                if (!template) {
1391                    // {debug}
1392                    if (this.options.debug) {
1393                        _debug.log({
1394                            node: this.selector,
1395                            function: "populateSource()",
1396                            arguments: String(group),
1397                            message: "WARNING - this.options.correlativeTemplate is enabled but no template was found."
1398                        });
1399
1400                        _debug.print();
1401                    }
1402                    // {/debug}
1403                } else {
1404                    // #109 correlativeTemplate can be an array of display keys instead of the complete template
1405                    if (Array.isArray(this.options.correlativeTemplate)) {
1406                        for (
1407                            var i = 0, ii = this.options.correlativeTemplate.length;
1408                            i < ii;
1409                            i++
1410                        ) {
1411                            compiledTemplate +=
1412                                "{{" + this.options.correlativeTemplate[i] + "}} ";
1413                        }
1414                    } else {
1415                        // Strip down the html tags, #351 if the template needs "<>" use html entities instead &#60;{{email}}&#62;
1416                        compiledTemplate = template
1417                            .replace(/<.+?>/g, " ")
1418                            .replace(/\s{2,}/, " ")
1419                            .trim();
1420                    }
1421
1422                    for (var i = 0, ii = data.length; i < ii; i++) {
1423                        // Fix #351, convert htmlEntities from the template string
1424                        data[i].compiled = $("<textarea />")
1425                            .html(
1426                                compiledTemplate
1427                                    .replace(/\{\{([\w\-\.]+)(?:\|(\w+))?}}/g, function (match,
1428                                                                                         index) {
1429                                        return scope.helper.namespace.call(
1430                                            scope,
1431                                            index,
1432                                            data[i],
1433                                            "get",
1434                                            ""
1435                                        );
1436                                    })
1437                                    .trim()
1438                            )
1439                            .text();
1440                    }
1441
1442                    if (groupSource.display) {
1443                        if (!~groupSource.display.indexOf("compiled")) {
1444                            groupSource.display.unshift("compiled");
1445                        }
1446                    } else if (!~this.options.display.indexOf("compiled")) {
1447                        this.options.display.unshift("compiled");
1448                    }
1449                }
1450            }
1451
1452            if (this.options.callback.onPopulateSource) {
1453                data = this.helper.executeCallback.call(
1454                    this,
1455                    this.options.callback.onPopulateSource,
1456                    [this.node, data, group, path]
1457                );
1458
1459                // {debug}
1460                if (this.options.debug) {
1461                    if (!data || !Array.isArray(data)) {
1462                        _debug.log({
1463                            node: this.selector,
1464                            function: "callback.populateSource()",
1465                            message: 'callback.onPopulateSource must return the "data" parameter'
1466                        });
1467
1468                        _debug.print();
1469                    }
1470                }
1471                // {/debug}
1472            }
1473
1474            // Save the data inside tmpSource to re-order once every requests are completed
1475            this.tmpSource[group] = (Array.isArray(data) && data) || [];
1476
1477            var cache = this.options.source[group].cache,
1478                compression = this.options.source[group].compression,
1479                ttl = this.options.source[group].ttl || this.options.ttl;
1480
1481            if (
1482                cache && !window[cache].getItem("TYPEAHEAD_" + this.selector + ":" + group)
1483            ) {
1484                if (this.options.callback.onCacheSave) {
1485                    data = this.helper.executeCallback.call(
1486                        this,
1487                        this.options.callback.onCacheSave,
1488                        [this.node, data, group, path]
1489                    );
1490
1491                    // {debug}
1492                    if (this.options.debug) {
1493                        if (!data || !Array.isArray(data)) {
1494                            _debug.log({
1495                                node: this.selector,
1496                                function: "callback.populateSource()",
1497                                message: 'callback.onCacheSave must return the "data" parameter'
1498                            });
1499
1500                            _debug.print();
1501                        }
1502                    }
1503                    // {/debug}
1504                }
1505
1506                var storage = JSON.stringify({
1507                    data: data,
1508                    ttl: new Date().getTime() + ttl
1509                });
1510
1511                if (compression) {
1512                    storage = LZString.compressToUTF16(storage);
1513                }
1514
1515                window[cache].setItem(
1516                    "TYPEAHEAD_" + this.selector + ":" + group,
1517                    storage
1518                );
1519            }
1520
1521            this.incrementGeneratedGroup(group);
1522        },
1523
1524        incrementGeneratedGroup: function (group) {
1525            this.generatedGroupCount++;
1526            if (this.generatedGroupCount !== this.generateGroups.length && !this.options.asyncResult) {
1527                return;
1528            }
1529
1530            if (this.xhr && this.xhr[group]) {
1531                delete this.xhr[group];
1532            }
1533
1534            for (var i = 0, ii = this.generateGroups.length; i < ii; i++) {
1535                this.source[this.generateGroups[i]] = this.tmpSource[
1536                    this.generateGroups[i]
1537                ];
1538            }
1539
1540            if (!this.hasDynamicGroups) {
1541                this.buildDropdownItemLayout("dynamic");
1542            }
1543
1544            if (this.generatedGroupCount === this.generateGroups.length) {
1545                this.xhr = {};
1546                this.options.loadingAnimation && this.container.removeClass("loading");
1547            }
1548            this.node.trigger("search" + this.namespace);
1549        },
1550
1551        /**
1552         * Key Navigation
1553         * tab 9: if option is enabled, blur Typeahead
1554         * Up 38: select previous item, skip "group" item
1555         * Down 40: select next item, skip "group" item
1556         * Right 39: change charAt, if last char fill hint (if options is true)
1557         * Esc 27: clears input (is not empty) / blur (if empty)
1558         * Enter 13: Select item + submit search
1559         *
1560         * @param {Object} e Event object
1561         * @returns {*}
1562         */
1563        navigate: function (e) {
1564            this.helper.executeCallback.call(
1565                this,
1566                this.options.callback.onNavigateBefore,
1567                [this.node, this.query, e]
1568            );
1569
1570            if (e.keyCode === 27) {
1571                // #166 Different browsers do not have the same behaviors by default, lets enforce what we want instead
1572                e.preventDefault();
1573                if (this.query.length) {
1574                    this.resetInput();
1575                    this.node.trigger("input" + this.namespace, [e]);
1576                } else {
1577                    this.node.blur();
1578                    this.hideLayout();
1579                }
1580                return;
1581            }
1582
1583            if (!this.result.length) return;
1584
1585            var itemList = this.resultContainer
1586                    .find("." + this.options.selector.item)
1587                    .not("[disabled]"),
1588                activeItem = itemList.filter(".active"),
1589                activeItemIndex = activeItem[0] ? itemList.index(activeItem) : null,
1590                activeDataIndex = activeItem[0] ? activeItem.attr("data-index") : null,
1591                newActiveItemIndex = null,
1592                newActiveDataIndex = null;
1593
1594            this.clearActiveItem();
1595
1596            this.helper.executeCallback.call(this, this.options.callback.onLeave, [
1597                this.node,
1598                (activeItemIndex !== null && itemList.eq(activeItemIndex)) || undefined,
1599                (activeDataIndex !== null && this.result[activeDataIndex]) || undefined,
1600                e
1601            ]);
1602
1603            if (e.keyCode === 13) {
1604                // Chrome needs preventDefault else the input search event is triggered
1605                e.preventDefault();
1606                if (activeItem.length > 0) {
1607                    // #311 When href is defined and "enter" is pressed, it needs to act as a "clicked" link
1608                    if (activeItem.find("a:first")[0].href === "javascript:;") {
1609                        activeItem.find("a:first").trigger("click", e);
1610                    } else {
1611                        activeItem.find("a:first")[0].click();
1612                    }
1613                } else {
1614                    this.node
1615                        .closest("form")
1616                        .trigger("submit");
1617                }
1618                return;
1619            }
1620
1621            if (e.keyCode === 39) {
1622                if (activeItemIndex !== null) {
1623                    itemList
1624                        .eq(activeItemIndex)
1625                        .find("a:first")[0]
1626                        .click();
1627                } else if (
1628                    this.options.hint &&
1629                    this.hint.container.val() !== "" &&
1630                    this.helper.getCaret(this.node[0]) >= this.query.length
1631                ) {
1632                    itemList
1633                        .filter('[data-index="' + this.hintIndex + '"]')
1634                        .find("a:first")[0]
1635                        .click();
1636                }
1637                return;
1638            }
1639
1640            // #284 Blur Typeahead when "Tab" key is pressed
1641            // #326 Improve Up / Down / Tab navigation to have only 1 "selected" item
1642            if (e.keyCode === 9) {
1643                if (this.options.blurOnTab) {
1644                    this.hideLayout();
1645                } else {
1646                    if (activeItem.length > 0) {
1647                        if (activeItemIndex + 1 < itemList.length) {
1648                            e.preventDefault();
1649                            newActiveItemIndex = activeItemIndex + 1;
1650                            this.addActiveItem(itemList.eq(newActiveItemIndex));
1651                        } else {
1652                            this.hideLayout();
1653                        }
1654                    } else {
1655                        if (itemList.length) {
1656                            e.preventDefault();
1657                            newActiveItemIndex = 0;
1658                            this.addActiveItem(itemList.first());
1659                        } else {
1660                            this.hideLayout();
1661                        }
1662                    }
1663                }
1664            } else if (e.keyCode === 38) {
1665                e.preventDefault();
1666
1667                if (activeItem.length > 0) {
1668                    if (activeItemIndex - 1 >= 0) {
1669                        newActiveItemIndex = activeItemIndex - 1;
1670                        this.addActiveItem(itemList.eq(newActiveItemIndex));
1671                    }
1672                } else if (itemList.length) {
1673                    newActiveItemIndex = itemList.length - 1;
1674                    this.addActiveItem(itemList.last());
1675                }
1676            } else if (e.keyCode === 40) {
1677                e.preventDefault();
1678
1679                if (activeItem.length > 0) {
1680                    if (activeItemIndex + 1 < itemList.length) {
1681                        newActiveItemIndex = activeItemIndex + 1;
1682                        this.addActiveItem(itemList.eq(newActiveItemIndex));
1683                    }
1684                } else if (itemList.length) {
1685                    newActiveItemIndex = 0;
1686                    this.addActiveItem(itemList.first());
1687                }
1688            }
1689
1690            newActiveDataIndex =
1691                newActiveItemIndex !== null
1692                    ? itemList.eq(newActiveItemIndex).attr("data-index")
1693                    : null;
1694
1695            this.helper.executeCallback.call(this, this.options.callback.onEnter, [
1696                this.node,
1697                (newActiveItemIndex !== null && itemList.eq(newActiveItemIndex)) ||
1698                undefined,
1699                (newActiveDataIndex !== null && this.result[newActiveDataIndex]) ||
1700                undefined,
1701                e
1702            ]);
1703
1704            // #115 Prevent the input from changing when navigating (arrow up / down) the results
1705            if (e.preventInputChange && ~[38, 40].indexOf(e.keyCode)) {
1706                this.buildHintLayout(
1707                    newActiveDataIndex !== null && newActiveDataIndex < this.result.length
1708                        ? [this.result[newActiveDataIndex]]
1709                        : null
1710                );
1711            }
1712
1713            if (this.options.hint && this.hint.container) {
1714                this.hint.container.css(
1715                    "color",
1716                    e.preventInputChange
1717                        ? this.hint.css.color
1718                        : (newActiveDataIndex === null && this.hint.css.color) ||
1719                        this.hint.container.css("background-color") ||
1720                        "fff"
1721                );
1722            }
1723
1724            var nodeValue =
1725                newActiveDataIndex === null || e.preventInputChange
1726                    ? this.rawQuery
1727                    : this.getTemplateValue.call(this, this.result[newActiveDataIndex]);
1728
1729            this.node.val(nodeValue);
1730            if (this.isContentEditable) {
1731                this.node.text(nodeValue);
1732            }
1733
1734            this.helper.executeCallback.call(
1735                this,
1736                this.options.callback.onNavigateAfter,
1737                [
1738                    this.node,
1739                    itemList,
1740                    (newActiveItemIndex !== null &&
1741                    itemList.eq(newActiveItemIndex).find("a:first")) ||
1742                    undefined,
1743                    (newActiveDataIndex !== null && this.result[newActiveDataIndex]) ||
1744                    undefined,
1745                    this.query,
1746                    e
1747                ]
1748            );
1749        },
1750
1751        getTemplateValue: function (item) {
1752            if (!item) return;
1753            var templateValue =
1754                (item.group && this.options.source[item.group].templateValue) ||
1755                this.options.templateValue;
1756            if (typeof templateValue === "function") {
1757                templateValue = templateValue.call(this);
1758            }
1759            if (!templateValue) {
1760                return this.helper.namespace
1761                    .call(this, item.matchedKey, item)
1762                    .toString();
1763            }
1764            var scope = this;
1765
1766            return templateValue.replace(/\{\{([\w\-.]+)}}/gi, function (match, index) {
1767                return scope.helper.namespace.call(scope, index, item, "get", "");
1768            });
1769        },
1770
1771        clearActiveItem: function () {
1772            this.resultContainer
1773                .find("." + this.options.selector.item)
1774                .removeClass("active");
1775        },
1776
1777        addActiveItem: function (item) {
1778            item.addClass("active");
1779        },
1780
1781        searchResult: function () {
1782            this.resetLayout();
1783
1784            if (
1785                this.helper.executeCallback.call(this, this.options.callback.onSearch, [
1786                    this.node,
1787                    this.query
1788                ]) === false
1789            ) return;
1790
1791            if (
1792                this.searchGroups.length && !(
1793                    this.options.multiselect &&
1794                    this.options.multiselect.limit &&
1795                    this.items.length >= this.options.multiselect.limit
1796                )
1797            ) {
1798                this.searchResultData();
1799            }
1800
1801            this.helper.executeCallback.call(this, this.options.callback.onResult, [
1802                this.node,
1803                this.query,
1804                this.result,
1805                this.resultCount,
1806                this.resultCountPerGroup
1807            ]);
1808
1809            if (this.isDropdownEvent) {
1810                this.helper.executeCallback.call(
1811                    this,
1812                    this.options.callback.onDropdownFilter,
1813                    [this.node, this.query, this.filters.dropdown, this.result]
1814                );
1815                this.isDropdownEvent = false;
1816            }
1817        },
1818
1819        searchResultData: function () {
1820            var scope = this,
1821                group,
1822                groupBy = this.groupBy,
1823                groupReference = null,
1824                item,
1825                match,
1826                comparedDisplay,
1827                comparedQuery = this.query.toLowerCase(),
1828                maxItem = this.options.maxItem,
1829                maxItemPerGroup = this.options.maxItemPerGroup,
1830                hasDynamicFilters =
1831                    this.filters.dynamic && !this.helper.isEmpty(this.filters.dynamic),
1832                displayKeys,
1833                displayValue,
1834                missingDisplayKey = {},
1835                groupFilter,
1836                groupFilterResult,
1837                groupMatcher,
1838                groupMatcherResult,
1839                matcher =
1840                    typeof this.options.matcher === "function" && this.options.matcher,
1841                correlativeMatch,
1842                correlativeQuery,
1843                correlativeDisplay;
1844
1845            if (this.options.accent) {
1846                comparedQuery = this.helper.removeAccent.call(this, comparedQuery);
1847            }
1848
1849            for (var i = 0, ii = this.searchGroups.length; i < ii; ++i) {
1850                group = this.searchGroups[i];
1851
1852                if (
1853                    this.filters.dropdown &&
1854                    this.filters.dropdown.key === "group" &&
1855                    this.filters.dropdown.value !== group
1856                )
1857                    continue;
1858
1859                groupFilter =
1860                    typeof this.options.source[group].filter !== "undefined"
1861                        ? this.options.source[group].filter
1862                        : this.options.filter;
1863                groupMatcher =
1864                    (typeof this.options.source[group].matcher === "function" &&
1865                    this.options.source[group].matcher) ||
1866                    matcher;
1867
1868                if (!this.source[group]) continue;
1869
1870                for (var k = 0, kk = this.source[group].length; k < kk; k++) {
1871                    if (this.resultItemCount >= maxItem && !this.options.callback.onResult) break;
1872                    if (hasDynamicFilters && !this.dynamicFilter.validate.apply(this, [this.source[group][k]])) continue;
1873
1874                    item = this.source[group][k];
1875                    // Validation over null item
1876                    if (item === null || typeof item === "boolean") continue;
1877                    if (this.options.multiselect && !this.isMultiselectUniqueData(item)) continue;
1878
1879                    // dropdownFilter by custom groups
1880                    if (
1881                        this.filters.dropdown &&
1882                        (item[this.filters.dropdown.key] || "").toLowerCase() !==
1883                        (this.filters.dropdown.value || "").toLowerCase()
1884                    ) {
1885                        continue;
1886                    }
1887
1888                    groupReference =
1889                        groupBy === "group"
1890                            ? group
1891                            : item[groupBy] ? item[groupBy] : item.group;
1892
1893                    if (groupReference && !this.tmpResult[groupReference]) {
1894                        this.tmpResult[groupReference] = [];
1895                        this.resultCountPerGroup[groupReference] = 0;
1896                    }
1897
1898                    if (maxItemPerGroup) {
1899                        if (
1900                            groupBy === "group" &&
1901                            this.tmpResult[groupReference].length >= maxItemPerGroup && !this.options.callback.onResult
1902                        ) {
1903                            break;
1904                        }
1905                    }
1906
1907                    displayKeys = this.options.source[group].display || this.options.display;
1908                    for (var v = 0, vv = displayKeys.length; v < vv; ++v) {
1909                        // #286 option.filter: false shouldn't bother about the option.display keys
1910                        if (groupFilter !== false) {
1911                            // #183 Allow searching for deep source object keys
1912                            displayValue = /\./.test(displayKeys[v])
1913                                ? this.helper.namespace.call(this, displayKeys[v], item)
1914                                : item[displayKeys[v]];
1915
1916                            // #182 Continue looping if empty or undefined key
1917                            if (typeof displayValue === "undefined" || displayValue === "") {
1918                                // {debug}
1919                                if (this.options.debug) {
1920                                    missingDisplayKey[v] = {
1921                                        display: displayKeys[v],
1922                                        data: item
1923                                    };
1924                                }
1925                                // {/debug}
1926                                continue;
1927                            }
1928
1929                            displayValue = this.helper.cleanStringFromScript(displayValue);
1930                        }
1931
1932                        if (typeof groupFilter === "function") {
1933                            groupFilterResult = groupFilter.call(this, item, displayValue);
1934
1935                            // return undefined to skip to next item
1936                            // return false to attempt the matching function on the next displayKey
1937                            // return true to add the item to the result list
1938                            // return item object to modify the item and add it to the result list
1939
1940                            if (groupFilterResult === undefined) break;
1941                            if (!groupFilterResult) continue;
1942                            if (typeof groupFilterResult === "object") {
1943                                item = groupFilterResult;
1944                            }
1945                        }
1946
1947                        if (~[undefined, true].indexOf(groupFilter)) {
1948                            if (displayValue === null) continue;
1949
1950                            comparedDisplay = displayValue;
1951                            comparedDisplay = comparedDisplay.toString().toLowerCase();
1952
1953                            if (this.options.accent) {
1954                                comparedDisplay = this.helper.removeAccent.call(
1955                                    this,
1956                                    comparedDisplay
1957                                );
1958                            }
1959
1960                            match = comparedDisplay.indexOf(comparedQuery);
1961
1962                            if (
1963                                this.options.correlativeTemplate &&
1964                                displayKeys[v] === "compiled" &&
1965                                match < 0 &&
1966                                /\s/.test(comparedQuery)
1967                            ) {
1968                                correlativeMatch = true;
1969                                correlativeQuery = comparedQuery.split(" ");
1970                                correlativeDisplay = comparedDisplay;
1971                                for (var x = 0, xx = correlativeQuery.length; x < xx; x++) {
1972                                    if (correlativeQuery[x] === "") continue;
1973                                    if (!~correlativeDisplay.indexOf(correlativeQuery[x])) {
1974                                        correlativeMatch = false;
1975                                        break;
1976                                    }
1977                                    correlativeDisplay = correlativeDisplay.replace(
1978                                        correlativeQuery[x],
1979                                        ""
1980                                    );
1981                                }
1982                            }
1983
1984                            if (match < 0 && !correlativeMatch) continue;
1985                            if (this.options.offset && match !== 0) continue;
1986
1987                            if (groupMatcher) {
1988                                groupMatcherResult = groupMatcher.call(
1989                                    this,
1990                                    item,
1991                                    displayValue
1992                                );
1993
1994                                // return undefined to skip to next item
1995                                // return false to attempt the matching function on the next displayKey
1996                                // return true to add the item to the result list
1997                                // return item object to modify the item and add it to the result list
1998
1999                                if (groupMatcherResult === undefined) break;
2000                                if (!groupMatcherResult) continue;
2001                                if (typeof groupMatcherResult === "object") {
2002                                    item = groupMatcherResult;
2003                                }
2004                            }
2005                        }
2006
2007                        this.resultCount++;
2008                        this.resultCountPerGroup[groupReference]++;
2009
2010                        if (this.resultItemCount < maxItem) {
2011                            if (
2012                                maxItemPerGroup &&
2013                                this.tmpResult[groupReference].length >= maxItemPerGroup
2014                            ) {
2015                                break;
2016                            }
2017
2018                            this.tmpResult[groupReference].push(
2019                                $.extend(true, {matchedKey: displayKeys[v]}, item)
2020                            );
2021                            this.resultItemCount++;
2022                        }
2023                        break;
2024                    }
2025
2026                    if (!this.options.callback.onResult) {
2027                        if (this.resultItemCount >= maxItem) {
2028                            break;
2029                        }
2030                        if (
2031                            maxItemPerGroup &&
2032                            this.tmpResult[groupReference].length >= maxItemPerGroup
2033                        ) {
2034                            if (groupBy === "group") {
2035                                break;
2036                            }
2037                        }
2038                    }
2039                }
2040            }
2041
2042            // {debug}
2043            if (this.options.debug) {
2044                if (!this.helper.isEmpty(missingDisplayKey)) {
2045                    _debug.log({
2046                        node: this.selector,
2047                        function: "searchResult()",
2048                        arguments: JSON.stringify(missingDisplayKey),
2049                        message: "Missing keys for display, make sure options.display is set properly."
2050                    });
2051
2052                    _debug.print();
2053                }
2054            }
2055            // {/debug}
2056
2057            if (this.options.order) {
2058                var displayKeys = [],
2059                    displayKey;
2060
2061                for (var group in this.tmpResult) {
2062                    if (!this.tmpResult.hasOwnProperty(group)) continue;
2063                    for (var i = 0, ii = this.tmpResult[group].length; i < ii; i++) {
2064                        displayKey =
2065                            this.options.source[this.tmpResult[group][i].group].display ||
2066                            this.options.display;
2067                        if (!~displayKeys.indexOf(displayKey[0])) {
2068                            displayKeys.push(displayKey[0]);
2069                        }
2070                    }
2071                    this.tmpResult[group].sort(
2072                        scope.helper.sort(
2073                            displayKeys,
2074                            scope.options.order === "asc",
2075                            function (a) {
2076                                if (a) {
2077                                    return a.toString().toUpperCase();
2078                                } else {
2079                                    return "";
2080                                }
2081                            }
2082                        )
2083                    );
2084                }
2085            }
2086
2087            var concatResults = [],
2088                groupOrder = [];
2089
2090            if (typeof this.options.groupOrder === "function") {
2091                groupOrder = this.options.groupOrder.apply(this, [
2092                    this.node,
2093                    this.query,
2094                    this.tmpResult,
2095                    this.resultCount,
2096                    this.resultCountPerGroup
2097                ]);
2098            } else if (Array.isArray(this.options.groupOrder)) {
2099                groupOrder = this.options.groupOrder;
2100            } else if (
2101                typeof this.options.groupOrder === "string" && ~["asc", "desc"].indexOf(this.options.groupOrder)
2102            ) {
2103                groupOrder = Object.keys(this.tmpResult).sort(
2104                    scope.helper.sort([], scope.options.groupOrder === "asc", function (a) {
2105                        return a.toString().toUpperCase();
2106                    })
2107                );
2108            } else {
2109                groupOrder = Object.keys(this.tmpResult);
2110            }
2111
2112            for (var i = 0, ii = groupOrder.length; i < ii; i++) {
2113                concatResults = concatResults.concat(this.tmpResult[groupOrder[i]] || []);
2114            }
2115
2116            // #286 groupTemplate option was deleting group reference Array
2117            this.groups = JSON.parse(JSON.stringify(groupOrder));
2118
2119            this.result = concatResults;
2120        },
2121
2122        buildLayout: function () {
2123            this.buildHtmlLayout();
2124
2125            this.buildBackdropLayout();
2126
2127            this.buildHintLayout();
2128
2129            if (this.options.callback.onLayoutBuiltBefore) {
2130                this.tmpResultHtml = this.helper.executeCallback.call(
2131                    this,
2132                    this.options.callback.onLayoutBuiltBefore,
2133                    [this.node, this.query, this.result, this.resultHtml]
2134                );
2135            }
2136
2137            if (this.tmpResultHtml instanceof $) {
2138                this.resultContainer.html(this.tmpResultHtml);
2139            } else if (this.resultHtml instanceof $) {
2140                this.resultContainer.html(this.resultHtml);
2141            }
2142
2143            if (this.options.callback.onLayoutBuiltAfter) {
2144                this.helper.executeCallback.call(
2145                    this,
2146                    this.options.callback.onLayoutBuiltAfter,
2147                    [this.node, this.query, this.result]
2148                );
2149            }
2150        },
2151
2152        buildHtmlLayout: function () {
2153            // #150 Add the option to have no resultList but still perform the search and trigger the callbacks
2154            if (this.options.resultContainer === false) return;
2155
2156            if (!this.resultContainer) {
2157                this.resultContainer = $("<div/>", {
2158                    class: this.options.selector.result
2159                });
2160
2161                this.container.append(this.resultContainer);
2162            }
2163
2164            var emptyTemplate;
2165            if (!this.result.length && this.generatedGroupCount === this.generateGroups.length) {
2166                if (
2167                    this.options.multiselect &&
2168                    this.options.multiselect.limit &&
2169                    this.items.length >= this.options.multiselect.limit
2170                ) {
2171                    if (this.options.multiselect.limitTemplate) {
2172                        emptyTemplate =
2173                            typeof this.options.multiselect.limitTemplate === "function"
2174                                ? this.options.multiselect.limitTemplate.call(this, this.query)
2175                                : this.options.multiselect.limitTemplate.replace(
2176                                    /\{\{query}}/gi,
2177                                    $("<div>")
2178                                        .text(this.helper.cleanStringFromScript(this.query))
2179                                        .html()
2180                                );
2181                    } else {
2182                        emptyTemplate =
2183                            "Can't select more than " + this.items.length + " items.";
2184                    }
2185                } else if (this.options.emptyTemplate && this.query !== "") {
2186                    emptyTemplate =
2187                        typeof this.options.emptyTemplate === "function"
2188                            ? this.options.emptyTemplate.call(this, this.query)
2189                            : this.options.emptyTemplate.replace(
2190                                /\{\{query}}/gi,
2191                                $("<div>")
2192                                    .text(this.helper.cleanStringFromScript(this.query))
2193                                    .html()
2194                            );
2195                } else {
2196                    return;
2197                }
2198            }
2199            this.displayEmptyTemplate = !!emptyTemplate;
2200
2201            var _query = this.query.toLowerCase();
2202            if (this.options.accent) {
2203                _query = this.helper.removeAccent.call(this, _query
2203);
2204            }
2205
2206            var scope = this,
2207                groupTemplate = this.groupTemplate || "<ul></ul>",
2208                hasEmptyTemplate = false;
2209
2210            if (this.groupTemplate) {
2211                groupTemplate = $(
2212                    groupTemplate.replace(
2213                        /<([^>]+)>\{\{(.+?)}}<\/[^>]+>/g,
2214                        function (match, tag, group, offset, string) {
2215                            var template = "",
2216                                groups = group === "group" ? scope.groups : [group];
2217
2218                            if (!scope.result.length) {
2219                                if (hasEmptyTemplate === true) return "";
2220                                hasEmptyTemplate = true;
2221
2222                                return (
2223                                    "<" + tag + ' class="' + scope.options.selector.empty + '">' + emptyTemplate + "</" + tag + ">"
2224                                );
2225                            }
2226
2227                            for (var i = 0, ii = groups.length; i < ii; ++i) {
2228                                template += "<" + tag + ' data-group-template="' + groups[i] + '"><ul></ul></' + tag + ">";
2229                            }
2230
2231                            return template;
2232                        }
2233                    )
2234                );
2235            } else {
2236                groupTemplate = $(groupTemplate);
2237                if (!this.result.length) {
2238                    groupTemplate.append(
2239                        emptyTemplate instanceof $
2240                            ? emptyTemplate
2241                            : '<li class="' +
2242                            scope.options.selector.empty +
2243                            '">' +
2244                            emptyTemplate +
2245                            "</li>"
2246                    );
2247                }
2248            }
2249
2250            groupTemplate.addClass(
2251                this.options.selector.list +
2252                (this.helper.isEmpty(this.result) ? " empty" : "")
2253            );
2254
2255            var _group,
2256                _groupTemplate,
2257                _item,
2258                _href,
2259                _liHtml,
2260                _template,
2261                _aHtml,
2262                _display,
2263                _displayKeys,
2264                _displayValue,
2265                _unusedGroups =
2266                    (this.groupTemplate && this.result.length && scope.groups) || [],
2267                _tmpIndexOf;
2268
2269            for (var i = 0, ii = this.result.length; i < ii; ++i) {
2270                _item = this.result[i];
2271                _group = _item.group;
2272                _href =
2273                    (!this.options.multiselect &&
2274                    this.options.source[_item.group].href) ||
2275                    this.options.href;
2276                _display = [];
2277                _displayKeys =
2278                    this.options.source[_item.group].display || this.options.display;
2279
2280                if (this.options.group) {
2281                    _group = _item[this.options.group.key];
2282                    if (this.options.group.template) {
2283                        if (typeof this.options.group.template === "function") {
2284                            _groupTemplate = this.options.group.template.call(this, _item);
2285                        } else if (typeof this.options.group.template === "string") {
2286                            _groupTemplate = this.options.group.template.replace(
2287                                /\{\{([\w\-\.]+)}}/gi,
2288                                function (match, index) {
2289                                    return scope.helper.namespace.call(
2290                                        scope,
2291                                        index,
2292                                        _item,
2293                                        "get",
2294                                        ""
2295                                    );
2296                                }
2297                            );
2298                        }
2299                    }
2300
2301                    if (!groupTemplate.find('[data-search-group="' + _group + '"]')[0]) {
2302                        (this.groupTemplate
2303                            ? groupTemplate.find('[data-group-template="' + _group + '"] ul')
2304                            : groupTemplate).append(
2305                            $("<li/>", {
2306                                class: scope.options.selector.group,
2307                                html: $("<a/>", {
2308                                    href: "javascript:;",
2309                                    html: _groupTemplate || _group,
2310                                    tabindex: -1
2311                                }),
2312                                "data-search-group": _group
2313                            })
2314                        );
2315                    }
2316                }
2317
2318                if (this.groupTemplate && _unusedGroups.length) {
2319                    _tmpIndexOf = _unusedGroups.indexOf(_group || _item.group);
2320                    if (~_tmpIndexOf) {
2321                        _unusedGroups.splice(_tmpIndexOf, 1);
2322                    }
2323                }
2324
2325                _liHtml = $("<li/>", {
2326                    class: scope.options.selector.item + " " + scope.options.selector.group + "-" + this.helper.slugify.call(this, _group),
2327                    disabled: _item.disabled ? true : false,
2328                    "data-group": _group,
2329                    "data-index": i,
2330                    html: $("<a/>", {
2331                        href: _href && !_item.disabled
2332                            ? (function (href, item) {
2333                                return item.href = scope.generateHref.call(
2334                                    scope,
2335                                    href,
2336                                    item
2337                                );
2338                            })(_href, _item)
2339                            : "javascript:;",
2340                        html: function () {
2341                            _template =
2342                                (_item.group && scope.options.source[_item.group].template) ||
2343                                scope.options.template;
2344
2345                            if (_template) {
2346                                if (typeof _template === "function") {
2347                                    _template = _template.call(scope, scope.query, _item);
2348                                }
2349
2350                                _aHtml = _template.replace(
2351                                    /\{\{([^\|}]+)(?:\|([^}]+))*}}/gi,
2352                                    function (match, index, options) {
2353                                        var value = scope.helper.cleanStringFromScript(
2354                                            String(
2355                                                scope.helper.namespace.call(
2356                                                    scope,
2357                                                    index,
2358                                                    _item,
2359                                                    "get",
2360                                                    ""
2361                                                )
2362                                            )
2363                                        );
2364
2365                                        // #151 Slugify should be an option, not enforced
2366                                        options = (options && options.split("|")) || [];
2367                                        if (~options.indexOf("slugify")) {
2368                                            value = scope.helper.slugify.call(scope, value);
2369                                        }
2370
2371                                        if (!~options.indexOf("raw")) {
2372                                            if (
2373                                                scope.options.highlight === true &&
2374                                                _query && ~_displayKeys.indexOf(index)
2375                                            ) {
2376                                                value = scope.helper.highlight.call(
2377                                                    scope,
2378                                                    value,
2379                                                    _query.split(" "),
2380                                                    scope.options.accent
2381                                                );
2382                                            }
2383                                        }
2384                                        return value;
2385                                    }
2386                                );
2387                            } else {
2388                                for (var i = 0, ii = _displayKeys.length; i < ii; i++) {
2389                                    _displayValue = /\./.test(_displayKeys[i])
2390                                        ? scope.helper.namespace.call(
2391                                            scope,
2392                                            _displayKeys[i],
2393                                            _item,
2394                                            "get",
2395                                            ""
2396                                        )
2397                                        : _item[_displayKeys[i]];
2398
2399                                    if (
2400                                        typeof _displayValue === "undefined" ||
2401                                        _displayValue === ""
2402                                    )
2403                                        continue;
2404
2405                                    _display.push(_displayValue);
2406                                }
2407
2408                                _aHtml =
2409                                    '<span class="' +
2410                                    scope.options.selector.display +
2411                                    '">' +
2412                                    scope.helper.cleanStringFromScript(
2413                                        String(_display.join(" "))
2414                                    ) +
2415                                    "</span>";
2416                            }
2417
2418                            if (
2419                                (scope.options.highlight === true && _query && !_template) ||
2420                                scope.options.highlight === "any"
2421                            ) {
2422                                _aHtml = scope.helper.highlight.call(
2423                                    scope,
2424                                    _aHtml,
2425                                    _query.split(" "),
2426                                    scope.options.accent
2427                                );
2428                            }
2429
2430                            $(this).append(_aHtml);
2431                        }
2432                    })
2433                });
2434
2435                (function (i, item, liHtml) {
2436                    liHtml.on("click", function (e, originalEvent) {
2437                        if (item.disabled) {
2438                            e.preventDefault();
2439                            return;
2440                        }
2441
2442                        // #208 - Attach "keyboard Enter" original event
2443                        if (originalEvent && typeof originalEvent === "object") {
2444                            e.originalEvent = originalEvent;
2445                        }
2446
2447                        if (scope.options.mustSelectItem && scope.helper.isEmpty(item)) {
2448                            e.preventDefault();
2449                            return;
2450                        }
2451
2452                        if (!scope.options.multiselect) {
2453                            scope.item = item;
2454                        }
2455
2456                        if (
2457                            scope.helper.executeCallback.call(
2458                                scope,
2459                                scope.options.callback.onClickBefore,
2460                                [scope.node, $(this), item, e]
2461                            ) === false
2462                        ) return;
2463
2464                        if (
2465                            (e.originalEvent && e.originalEvent.defaultPrevented) ||
2466                            e.isDefaultPrevented()
2467                        ) return;
2468
2469                        if (scope.options.multiselect) {
2470                            scope.query = scope.rawQuery = "";
2471                            scope.addMultiselectItemLayout(item);
2472                        } else {
2473                            scope.focusOnly = true;
2474                            scope.query = scope.rawQuery = scope.getTemplateValue.call(scope, item);;
2475                            if (scope.isContentEditable) {
2476                                scope.node.text(scope.query);
2477                                scope.helper.setCaretAtEnd(scope.node[0]);
2478                            }
2479                        }
2480
2481                        scope.hideLayout();
2482
2483                        scope.node
2484                            .val(scope.query)
2485                            .focus();
2486
2487                        scope.options.cancelButton &&
2488                            scope.toggleCancelButtonVisibility();
2489
2490                        scope.helper.executeCallback.call(
2491                            scope,
2492                            scope.options.callback.onClickAfter,
2493                            [scope.node, $(this), item, e]
2494                        );
2495                    });
2496                    liHtml.on("mouseenter", function (e) {
2497                        if (!item.disabled) {
2498                            scope.clearActiveItem();
2499                            scope.addActiveItem($(this));
2500                        }
2501                        scope.helper.executeCallback.call(
2502                            scope,
2503                            scope.options.callback.onEnter,
2504                            [scope.node, $(this), item, e]
2505                        );
2506                    });
2507                    liHtml.on("mouseleave", function (e) {
2508                        if (!item.disabled) {
2509                            scope.clearActiveItem();
2510                        }
2511                        scope.helper.executeCallback.call(
2512                            scope,
2513                            scope.options.callback.onLeave,
2514                            [scope.node, $(this), item, e]
2515                        );
2516                    });
2517                })(i, _item, _liHtml);
2518
2519                (this.groupTemplate
2520                    ? groupTemplate.find('[data-group-template="' + _group + '"] ul')
2521                    : groupTemplate).append(_liHtml);
2522            }
2523
2524            if (this.result.length && _unusedGroups.length) {
2525                for (var i = 0, ii = _unusedGroups.length; i < ii; ++i) {
2526                    groupTemplate
2527                        .find('[data-group-template="' + _unusedGroups[i] + '"]')
2528                        .remove();
2529                }
2530            }
2531
2532            this.resultHtml = groupTemplate;
2533        },
2534
2535        generateHref: function (href, item) {
2536            var scope = this;
2537
2538            if (typeof href === "string") {
2539                href = href.replace(
2540                    /\{\{([^\|}]+)(?:\|([^}]+))*}}/gi,
2541                    function (match, index, options) {
2542                        var value = scope.helper.namespace.call(
2543                            scope,
2544                            index,
2545                            item,
2546                            "get",
2547                            ""
2548                        );
2549
2550                        // #151 Slugify should be an option, not enforced
2551                        options = (options && options.split("|")) || [];
2552                        if (~options.indexOf("slugify")) {
2553                            value = scope.helper.slugify.call(scope, value);
2554                        }
2555
2556                        return value;
2557                    }
2558                );
2559            } else if (typeof href === "function") {
2560                href = href.call(this, item);
2561            }
2562
2563            return href;
2564        },
2565
2566        getMultiselectComparedData: function (item) {
2567            var uniqueComparedItem = "";
2568            if (Array.isArray(this.options.multiselect.matchOn)) {
2569                for (
2570                    var i = 0, ii = this.options.multiselect.matchOn.length;
2571                    i < ii;
2572                    ++i
2573                ) {
2574                    uniqueComparedItem +=
2575                        typeof item[this.options.multiselect.matchOn[i]] !== "undefined"
2576                            ? item[this.options.multiselect.matchOn[i]]
2577                            : "";
2578                }
2579            } else {
2580                var tmpItem = JSON.parse(JSON.stringify(item)),
2581                    extraKeys = ["group", "matchedKey", "compiled", "href"];
2582
2583                for (var i = 0, ii = extraKeys.length; i < ii; ++i) {
2584                    delete tmpItem[extraKeys[i]];
2585                }
2586                uniqueComparedItem = JSON.stringify(tmpItem);
2587            }
2588            return uniqueComparedItem;
2589        },
2590
2591        buildBackdropLayout: function () {
2592            if (!this.options.backdrop) return;
2593
2594            if (!this.backdrop.container) {
2595                this.backdrop.css = $.extend(
2596                    {
2597                        opacity: 0.6,
2598                        filter: "alpha(opacity=60)",
2599                        position: "fixed",
2600                        top: 0,
2601                        right: 0,
2602                        bottom: 0,
2603                        left: 0,
2604                        "z-index": 1040,
2605                        "background-color": "#000"
2606                    },
2607                    this.options.backdrop
2608                );
2609
2610                this.backdrop.container = $("<div/>", {
2611                    class: this.options.selector.backdrop,
2612                    css: this.backdrop.css
2613                }).insertAfter(this.container);
2614            }
2615            this.container.addClass("backdrop").css({
2616                "z-index": this.backdrop.css["z-index"] + 1,
2617                position: "relative"
2618            });
2619        },
2620
2621        buildHintLayout: function (result) {
2622            if (!this.options.hint) return;
2623            // #144 hint doesn't overlap with the input when the query is too long
2624            if (this.node[0].scrollWidth > Math.ceil(this.node.innerWidth())) {
2625                this.hint.container && this.hint.container.val("");
2626                return;
2627            }
2628
2629            var scope = this,
2630                hint = "",
2631                result = result || this.result,
2632                query = this.query.toLowerCase();
2633
2634            if (this.options.accent) {
2635                query = this.helper.removeAccent.call(this, query);
2636            }
2637
2638            this.hintIndex = null;
2639
2640            if (this.searchGroups.length) {
2641                if (!this.hint.container) {
2642                    this.hint.css = $.extend(
2643                        {
2644                            "border-color": "transparent",
2645                            position: "absolute",
2646                            top: 0,
2647                            display: "inline",
2648                            "z-index": -1,
2649                            float: "none",
2650                            color: "silver",
2651                            "box-shadow": "none",
2652                            cursor: "default",
2653                            "-webkit-user-select": "none",
2654                            "-moz-user-select": "none",
2655                            "-ms-user-select": "none",
2656                            "user-select": "none"
2657                        },
2658                        this.options.hint
2659                    );
2660
2661                    this.hint.container = $("<" + this.node[0].nodeName + "/>", {
2662                        type: this.node.attr("type"),
2663                        class: this.node.attr("class"),
2664                        readonly: true,
2665                        unselectable: "on",
2666                        "aria-hidden": "true",
2667                        tabindex: -1,
2668                        click: function () {
2669                            // IE8 Fix
2670                            scope.node.focus();
2671                        }
2672                    })
2673                        .addClass(this.options.selector.hint)
2674                        .css(this.hint.css)
2675                        .insertAfter(this.node);
2676
2677                    this.node.parent().css({
2678                        position: "relative"
2679                    });
2680                }
2681
2682                this.hint.container.css("color", this.hint.css.color);
2683
2684                // Do not display hint for empty query
2685                if (query) {
2686                    var _displayKeys, _group, _comparedValue;
2687
2688                    for (var i = 0, ii = result.length; i < ii; i++) {
2689                        if (result[i].disabled) continue;
2690
2691                        _group = result[i].group;
2692                        _displayKeys =
2693                            this.options.source[_group].display || this.options.display;
2694
2695                        for (var k = 0, kk = _displayKeys.length; k < kk; k++) {
2696                            _comparedValue = String(result[i][_displayKeys[k]]).toLowerCase();
2697                            if (this.options.accent) {
2698                                _comparedValue = this.helper.removeAccent.call(
2699                                    this,
2700                                    _comparedValue
2701                                );
2702                            }
2703
2704                            if (_comparedValue.indexOf(query) === 0) {
2705                                hint = String(result[i][_displayKeys[k]]);
2706                                this.hintIndex = i;
2707                                break;
2708                            }
2709                        }
2710                        if (this.hintIndex !== null) {
2711                            break;
2712                        }
2713                    }
2714                }
2715
2716                var hintValue =
2717                    (hint.length > 0 &&
2718                    this.rawQuery + hint.substring(this.query.length)) ||
2719                    "";
2720                this.hint.container.val(hintValue);
2721
2722                if (this.isContentEditable) {
2723                    this.hint.container.text(hintValue);
2724                }
2725            }
2726        },
2727
2728        buildDropdownLayout: function () {
2729            if (!this.options.dropdownFilter) return;
2730
2731            var scope = this;
2732
2733            $("<span/>", {
2734                class: this.options.selector.filter,
2735                html: function () {
2736                    $(this).append(
2737                        $("<button/>", {
2738                            type: "button",
2739                            class: scope.options.selector.filterButton,
2740                            style: "display: none;",
2741                            click: function () {
2742                                scope.container.toggleClass("filter");
2743
2744                                var _ns = scope.namespace + "-dropdown-filter";
2745
2746                                $("html").off(_ns);
2747
2748                                if (scope.container.hasClass("filter")) {
2749                                    $("html").on("click" + _ns + " touchend" + _ns, function (e) {
2750                                        if (
2751                                            ($(e.target).closest(
2752                                                "." + scope.options.selector.filter
2753                                            )[0] &&
2754                                            $(e.target).closest(scope.container)[0]) ||
2755                                            scope.hasDragged
2756                                        )
2757                                            return;
2758                                        scope.container.removeClass("filter");
2759
2760                                        $("html").off(_ns);
2761                                    });
2762                                }
2763                            }
2764                        })
2765                    );
2766
2767                    $(this).append(
2768                        $("<ul/>", {
2769                            class: scope.options.selector.dropdown
2770                        })
2771                    );
2772                }
2773            }).insertAfter(scope.container.find("." + scope.options.selector.query));
2774        },
2775
2776        buildDropdownItemLayout: function (type) {
2777            if (!this.options.dropdownFilter) return;
2778
2779            var scope = this,
2780                template,
2781                all =
2782                    (typeof this.options.dropdownFilter === "string" &&
2783                    this.options.dropdownFilter) ||
2784                    "All",
2785                ulScope = this.container.find("." + this.options.selector.dropdown),
2786                filter;
2787
2788            // Use regular groups defined in options.source
2789            if (
2790                type === "static" &&
2791                (this.options.dropdownFilter === true ||
2792                typeof this.options.dropdownFilter === "string")
2793            ) {
2794                this.dropdownFilter.static.push({
2795                    key: "group",
2796                    template: "{{group}}",
2797                    all: all,
2798                    value: Object.keys(this.options.source)
2799                });
2800            }
2801
2802            for (var i = 0, ii = this.dropdownFilter[type].length; i < ii; i++) {
2803                filter = this.dropdownFilter[type][i];
2804
2805                if (!Array.isArray(filter.value)) {
2806                    filter.value = [filter.value];
2807                }
2808
2809                if (filter.all) {
2810                    this.dropdownFilterAll = filter.all;
2811                }
2812
2813                for (var k = 0, kk = filter.value.length; k <= kk; k++) {
2814                    // Only add "all" at the last filter iteration
2815                    if (k === kk && i !== ii - 1) {
2816                        continue;
2817                    } else if (k === kk && i === ii - 1) {
2818                        if (type === "static" && this.dropdownFilter.dynamic.length) {
2819                            continue;
2820                        }
2821                    }
2822
2823                    template = this.dropdownFilterAll || all;
2824                    if (filter.value[k]) {
2825                        if (filter.template) {
2826                            template = filter.template.replace(
2827                                new RegExp("{{" + filter.key + "}}", "gi"),
2828                                filter.value[k]
2829                            );
2830                        } else {
2831                            template = filter.value[k];
2832                        }
2833                    } else {
2834                        this.container
2835                            .find("." + scope.options.selector.filterButton)
2836                            .html(template);
2837                    }
2838
2839                    (function (k, filter, template) {
2840                        ulScope.append(
2841                            $("<li/>", {
2842                                class: scope.options.selector.dropdownItem +
2843                                " " +
2844                                scope.helper.slugify.call(
2845                                    scope,
2846                                    filter.key + "-" + (filter.value[k] || all)
2847                                ),
2848                                html: $("<a/>", {
2849                                    href: "javascript:;",
2850                                    html: template,
2851                                    click: function (e) {
2852                                        e.preventDefault();
2853                                        _selectFilter.call(scope, {
2854                                            key: filter.key,
2855                                            value: filter.value[k] || "*",
2856                                            template: template
2857                                        });
2858                                    }
2859                                })
2860                            })
2861                        );
2862                    })(k, filter, template);
2863                }
2864            }
2865
2866            if (this.dropdownFilter[type].length) {
2867                this.container
2868                    .find("." + scope.options.selector.filterButton)
2869                    .removeAttr("style");
2870            }
2871
2872            /**
2873             * @private
2874             * Select the filter and rebuild the result group
2875             *
2876             * @param {object} item
2877             */
2878            function _selectFilter(item) {
2879                if (item.value === "*") {
2880                    delete this.filters.dropdown;
2881                } else {
2882                    this.filters.dropdown = item;
2883                }
2884
2885                this.container
2886                    .removeClass("filter")
2887                    .find("." + this.options.selector.filterButton)
2888                    .html(item.template);
2889
2890                this.isDropdownEvent = true;
2891                this.node.trigger("input" + this.namespace);
2892
2893                if (this.options.multiselect) {
2894                    this.adjustInputSize();
2895                }
2896
2897                this.node.focus();
2898            }
2899        },
2900
2901        dynamicFilter: {
2902            isEnabled: false,
2903            init: function () {
2904                if (!this.options.dynamicFilter) return;
2905
2906                this.dynamicFilter.bind.call(this);
2907                this.dynamicFilter.isEnabled = true;
2908            },
2909
2910            validate: function (item) {
2911                var isValid,
2912                    softValid = null,
2913                    hardValid = null,
2914                    itemValue;
2915
2916                for (var key in this.filters.dynamic) {
2917                    if (!this.filters.dynamic.hasOwnProperty(key)) continue;
2918                    if (!!~key.indexOf(".")) {
2919                        itemValue = this.helper.namespace.call(this, key, item, "get");
2920                    } else {
2921                        itemValue = item[key];
2922                    }
2923
2924                    if (this.filters.dynamic[key].modifier === "|" && !softValid) {
2925                        softValid = itemValue == this.filters.dynamic[key].value || false;
2926                    }
2927
2928                    if (this.filters.dynamic[key].modifier === "&") {
2929                        // Leaving "==" in case of comparing number with string
2930                        if (itemValue == this.filters.dynamic[key].value) {
2931                            hardValid = true;
2932                        } else {
2933                            hardValid = false;
2934                            break;
2935                        }
2936                    }
2937                }
2938
2939                isValid = softValid;
2940                if (hardValid !== null) {
2941                    isValid = hardValid;
2942                    if (hardValid === true && softValid !== null) {
2943                        isValid = softValid;
2944                    }
2945                }
2946
2947                return !!isValid;
2948            },
2949
2950            set: function (key, value) {
2951                var matches = key.match(/^([|&])?(.+)/);
2952
2953                if (!value) {
2954                    delete this.filters.dynamic[matches[2]];
2955                } else {
2956                    this.filters.dynamic[matches[2]] = {
2957                        modifier: matches[1] || "|",
2958                        value: value
2959                    };
2960                }
2961
2962                if (this.dynamicFilter.isEnabled) {
2963                    this.generateSource();
2964                }
2965            },
2966            bind: function () {
2967                var scope = this,
2968                    filter;
2969
2970                for (var i = 0, ii = this.options.dynamicFilter.length; i < ii; i++) {
2971                    filter = this.options.dynamicFilter[i];
2972
2973                    if (typeof filter.selector === "string") {
2974                        filter.selector = $(filter.selector);
2975                    }
2976
2977                    if (
2978                        !(filter.selector instanceof $) || !filter.selector[0] || !filter.key
2979                    ) {
2980                        // {debug}
2981                        if (this.options.debug) {
2982                            _debug.log({
2983                                node: this.selector,
2984                                function: "buildDynamicLayout()",
2985                                message: 'Invalid jQuery selector or jQuery Object for "filter.selector" or missing filter.key'
2986                            });
2987
2988                            _debug.print();
2989                        }
2990                        // {/debug}
2991                        continue;
2992                    }
2993
2994                    (function (filter) {
2995                        filter.selector
2996                            .off(scope.namespace)
2997                            .on("change" + scope.namespace, function () {
2998                                scope.dynamicFilter.set.apply(scope, [
2999                                    filter.key,
3000                                    scope.dynamicFilter.getValue(this)
3001                                ]);
3002                            })
3003                            .trigger("change" + scope.namespace);
3004                    })(filter);
3005                }
3006            },
3007
3008            getValue: function (tag) {
3009                var value;
3010                if (tag.tagName === "SELECT") {
3011                    value = tag.value;
3012                } else if (tag.tagName === "INPUT") {
3013                    if (tag.type === "checkbox") {
3014                        value =
3015                            (tag.checked && tag.getAttribute("value")) || tag.checked || null;
3016                    } else if (tag.type === "radio" && tag.checked) {
3017                        value = tag.value;
3018                    }
3019                }
3020                return value;
3021            }
3022        },
3023
3024        buildMultiselectLayout: function () {
3025            if (!this.options.multiselect) return;
3026            var scope = this;
3027            var multiselectData;
3028
3029            this.label.container = $("<span/>", {
3030                class: this.options.selector.labelContainer,
3031                "data-padding-left": parseFloat(this.node.css("padding-left")) || 0,
3032                "data-padding-right": parseFloat(this.node.css("padding-right")) || 0,
3033                "data-padding-top": parseFloat(this.node.css("padding-top")) || 0,
3034                click: function (e) {
3035                    if ($(e.target).hasClass(scope.options.selector.labelContainer)) {
3036                        scope.node.focus();
3037                    }
3038                }
3039            });
3040
3041            this.node
3042                .closest("." + this.options.selector.query)
3043                .prepend(this.label.container);
3044
3045            if (!this.options.multiselect.data) return;
3046
3047            if (Array.isArray(this.options.multiselect.data)) {
3048                this.populateMultiselectData(this.options.multiselect.data);
3049            } else if (typeof this.options.multiselect.data === 'function') {
3050                multiselectData = this.options.multiselect.data.call(this);
3051                if (Array.isArray(multiselectData)) {
3052                    this.populateMultiselectData(multiselectData);
3053                } else if (typeof multiselectData.promise === "function") {
3054                    $.when(multiselectData).then(function (deferredData) {
3055                        if (deferredData && Array.isArray(deferredData)) {
3056                            scope.populateMultiselectData(deferredData);
3057                        }
3058                    });
3059                }
3060            }
3061        },
3062
3063        isMultiselectUniqueData: function (data) {
3064            var isUniqueData = true;
3065            for (var x = 0, xx = this.comparedItems.length; x < xx; ++x) {
3066                if (
3067                    this.comparedItems[x] ===
3068                    this.getMultiselectComparedData(data)
3069                ) {
3070                    isUniqueData = false;
3071                    break;
3072                }
3073            }
3074            return isUniqueData;
3075        },
3076
3077        populateMultiselectData: function (data) {
3078            for (var i = 0, ii = data.length; i < ii; ++i) {
3079                this.addMultiselectItemLayout(data[i]);
3080            }
3081
3082            this.node.trigger("search" + this.namespace, { origin: 'populateMultiselectData' });
3083        },
3084
3085        addMultiselectItemLayout: function (item) {
3086            if (!this.isMultiselectUniqueData(item)) return;
3087
3088            this.items.push(item);
3089            this.comparedItems.push(
3090                this.getMultiselectComparedData(item)
3091            );
3092
3093            var templateValue = this.getTemplateValue(item);
3094
3095            var scope = this,
3096                htmlTag = this.options.multiselect.href ? "a" : "span";
3097
3098            var label = $("<span/>", {
3099                class: this.options.selector.label,
3100                html: $("<" + htmlTag + "/>", {
3101                    text: templateValue,
3102                    click: function (e) {
3103                        var currentLabel = $(this).closest(
3104                                "." + scope.options.selector.label
3105                            ),
3106                            index = scope.label.container
3107                                .find("." + scope.options.selector.label)
3108                                .index(currentLabel);
3109
3110                        scope.options.multiselect.callback && scope.helper.executeCallback.call(
3111                            scope,
3112                            scope.options.multiselect.callback.onClick,
3113                            [scope.node, scope.items[index], e]
3114                        );
3115                    },
3116                    href: this.options.multiselect.href
3117                        ? (function (item) {
3118                            return scope.generateHref.call(
3119                                scope,
3120                                scope.options.multiselect.href,
3121                                item
3122                            );
3123                        })(scope.items[scope.items.length - 1])
3124                        : null
3125                })
3126            });
3127
3128            label.append(
3129                $("<span/>", {
3130                    class: this.options.selector.cancelButton,
3131                    html: "×",
3132                    click: function (e) {
3133                        var label = $(this).closest(
3134                                "." + scope.options.selector.label
3135                            ),
3136                            index = scope.label.container
3137                                .find("." + scope.options.selector.label)
3138                                .index(label);
3139
3140                        scope.cancelMultiselectItem(index, label, e);
3141                    }
3142                })
3143            );
3144
3145            this.label.container.append(label);
3146            this.adjustInputSize();
3147
3148            return true;
3149        },
3150
3151        cancelMultiselectItem: function (index, label, e) {
3152            var item = this.items[index];
3153
3154            label = label
3155                || this.label.container
3156                    .find('.' + this.options.selector.label)
3157                    .eq(index);
3158
3159            label.remove();
3160
3161            this.items.splice(index, 1);
3162            this.comparedItems.splice(index, 1);
3163
3164            this.options.multiselect.callback && this.helper.executeCallback.call(
3165                this,
3166                this.options.multiselect.callback.onCancel,
3167                [this.node, item, e]
3168            );
3169
3170            this.adjustInputSize();
3171
3172            this.focusOnly = true;
3173            this.node.focus().trigger('input' + this.namespace, { origin: 'cancelMultiselectItem' });
3174        },
3175
3176        adjustInputSize: function () {
3177            var nodeWidth =
3178                this.node[0].getBoundingClientRect().width -
3179                (parseFloat(this.label.container.data("padding-right")) || 0) -
3180                (parseFloat(this.label.container.css("padding-left")) || 0);
3181
3182            var labelOuterWidth = 0,
3183                numberOfRows = 0,
3184                currentRowWidth = 0,
3185                isRowAdded = false,
3186                labelOuterHeight = 0;
3187
3188            this.label.container
3189                .find("." + this.options.selector.label)
3190                .filter(function (i, v) {
3191                    if (i === 0) {
3192                        labelOuterHeight =
3193                            $(v)[0].getBoundingClientRect().height +
3194                            parseFloat($(v).css("margin-bottom") || 0);
3195                    }
3196
3197                    // labelOuterWidth = Math.round($(v)[0].getBoundingClientRect().width * 100) / 100 + parseFloat($(v).css('margin-right'));
3198                    labelOuterWidth =
3199                        $(v)[0].getBoundingClientRect().width +
3200                        parseFloat($(v).css("margin-right") || 0);
3201
3202                    if (
3203                        currentRowWidth + labelOuterWidth > nodeWidth * 0.7 && !isRowAdded
3204                    ) {
3205                        numberOfRows++;
3206                        isRowAdded = true;
3207                    }
3208
3209                    if (currentRowWidth + labelOuterWidth < nodeWidth) {
3210                        currentRowWidth += labelOuterWidth;
3211                    } else {
3212                        isRowAdded = false;
3213                        currentRowWidth = labelOuterWidth;
3214                    }
3215                });
3216
3217            var paddingLeft =
3218                parseFloat(this.label.container.data("padding-left") || 0) +
3219                (isRowAdded ? 0 : currentRowWidth);
3220            var paddingTop =
3221                numberOfRows * labelOuterHeight +
3222                parseFloat(this.label.container.data("padding-top") || 0);
3223
3224            this.container
3225                .find("." + this.options.selector.query)
3226                .find("input, textarea, [contenteditable], .typeahead__hint")
3227                .css({
3228                    paddingLeft: paddingLeft,
3229                    paddingTop: paddingTop
3230                });
3231        },
3232
3233        showLayout: function () {
3234            if (this.container.hasClass("result") ||
3235                (
3236                    !this.result.length && !this.displayEmptyTemplate && !this.options.backdropOnFocus
3237                )
3238            ) return;
3239
3240            _addHtmlListeners.call(this);
3241
3242            this.container.addClass(
3243                [
3244                    this.result.length ||
3245                    (this.searchGroups.length &&
3246                    this.displayEmptyTemplate)
3247                        ? "result "
3248                        : "",
3249                    this.options.hint && this.searchGroups.length ? "hint" : "",
3250                    this.options.backdrop || this.options.backdropOnFocus
3251                        ? "backdrop"
3252                        : ""
3253                ].join(" ")
3254            );
3255
3256            this.helper.executeCallback.call(
3257                this,
3258                this.options.callback.onShowLayout,
3259                [this.node, this.query]
3260            );
3261
3262            function _addHtmlListeners() {
3263                var scope = this;
3264
3265                // If Typeahead is blured by pressing the "Tab" Key, hide the results
3266                $("html")
3267                    .off("keydown" + this.namespace)
3268                    .on("keydown" + this.namespace, function (e) {
3269                        if (!e.keyCode || e.keyCode !== 9) return;
3270                        setTimeout(function () {
3271                            if (
3272                                !$(":focus")
3273                                    .closest(scope.container)
3274                                    .find(scope.node)[0]
3275                            ) {
3276                                scope.hideLayout();
3277                            }
3278                        }, 0);
3279                    });
3280
3281                // If Typeahead is blured by clicking outside, hide the results
3282                $("html")
3283                    .off("click" + this.namespace + " touchend" + this.namespace)
3284                    .on("click" + this.namespace + " touchend" + this.namespace, function (e) {
3285                        if ($(e.target).closest(scope.container)[0] ||
3286                            $(e.target).closest('.' + scope.options.selector.item)[0] ||
3287                            e.target.className === scope.options.selector.cancelButton ||
3288                            scope.hasDragged
3289                        ) return;
3290
3291                        scope.hideLayout();
3292                    });
3293            }
3294        },
3295
3296        hideLayout: function () {
3297            // Means the container is already hidden
3298            if (!this.container.hasClass("result") && !this.container.hasClass("backdrop")) return;
3299
3300            this.container.removeClass(
3301                "result hint filter" +
3302                (this.options.backdropOnFocus && $(this.node).is(":focus")
3303                    ? ""
3304                    : " backdrop")
3305            );
3306
3307            if (this.options.backdropOnFocus && this.container.hasClass("backdrop"))
3308                return;
3309
3310            // Make sure the event HTML gets cleared
3311            $("html").off(this.namespace);
3312
3313            this.helper.executeCallback.call(
3314                this,
3315                this.options.callback.onHideLayout,
3316                [this.node, this.query]
3317            );
3318        },
3319
3320        resetLayout: function () {
3321            this.result = [];
3322            this.tmpResult = {};
3323            this.groups = [];
3324            this.resultCount = 0;
3325            this.resultCountPerGroup = {};
3326            this.resultItemCount = 0;
3327            this.resultHtml = null;
3328
3329            if (this.options.hint && this.hint.container) {
3330                this.hint.container.val("");
3331                if (this.isContentEditable) {
3332                    this.hint.container.text("");
3333                }
3334            }
3335        },
3336
3337        resetInput: function () {
3338            this.node.val("");
3339            if (this.isContentEditable) {
3340                this.node.text("");
3341            }
3342            this.query = "";
3343            this.rawQuery = "";
3344        },
3345
3346        buildCancelButtonLayout: function () {
3347            if (!this.options.cancelButton) return;
3348            var scope = this;
3349
3350            $("<span/>", {
3351                class: this.options.selector.cancelButton,
3352                html: "×",
3353                mousedown: function (e) {
3354                    // Don't blur the input
3355                    e.stopImmediatePropagation();
3356                    e.preventDefault();
3357
3358                    scope.resetInput();
3359                    scope.node.trigger("input" + scope.namespace, [e]);
3360                }
3361            }).insertBefore(this.node);
3362        },
3363
3364        toggleCancelButtonVisibility: function () {
3365            this.container.toggleClass("cancel", !!this.query.length);
3366        },
3367
3368        __construct: function () {
3369            this.extendOptions();
3370
3371            if (!this.unifySourceFormat()) {
3372                return;
3373            }
3374
3375            this.dynamicFilter.init.apply(this);
3376
3377            this.init();
3378            this.buildDropdownLayout();
3379            this.buildDropdownItemLayout("static");
3380
3381            this.buildMultiselectLayout();
3382
3383            this.delegateEvents();
3384            this.buildCancelButtonLayout();
3385
3386            this.helper.executeCallback.call(this, this.options.callback.onReady, [
3387                this.node
3388            ]);
3389        },
3390
3391        helper: {
3392            isEmpty: function (obj) {
3393                for (var prop in obj) {
3394                    if (obj.hasOwnProperty(prop)) return false;
3395                }
3396
3397                return true;
3398            },
3399
3400            /**
3401             * Remove every accent(s) from a string
3402             *
3403             * @param {String} string
3404             * @returns {*}
3405             */
3406            removeAccent: function (string) {
3407                if (typeof string !== "string") {
3408                    return;
3409                }
3410
3411                var accent = _accent;
3412
3413                if (typeof this.options.accent === "object") {
3414                    accent = this.options.accent;
3415                }
3416
3417                string = string
3418                    .toLowerCase()
3419                    .replace(new RegExp("[" + accent.from + "]", "g"), function (match) {
3420                        return accent.to[accent.from.indexOf(match)];
3421                    });
3422
3423                return string;
3424            },
3425
3426            /**
3427             * Creates a valid url from string
3428             *
3429             * @param {String} string
3430             * @returns {string}
3431             */
3432            slugify: function (string) {
3433                string = String(string);
3434
3435                if (string !== "") {
3436                    string = this.helper.removeAccent.call(this, string);
3437                    string = string
3438                        .replace(/[^-a-z0-9]+/g, "-")
3439                        .replace(/-+/g, "-")
3440                        .replace(/^-|-$/g, "");
3441                }
3442
3443                return string;
3444            },
3445
3446            /**
3447             * Sort list of object by key
3448             *
3449             * @param {String|Array} field
3450             * @param {Boolean} reverse
3451             * @param {Function} primer
3452             * @returns {Function}
3453             */
3454            sort: function (field, reverse, primer) {
3455                var key = function (x) {
3456                    for (var i = 0, ii = field.length; i < ii; i++) {
3457                        if (typeof x[field[i]] !== "undefined") {
3458                            return primer(x[field[i]]);
3459                        }
3460                    }
3461                    return x;
3462                };
3463
3464                reverse = [-1, 1][+!!reverse];
3465
3466                return function (a, b) {
3467                    return (a = key(a)), (b = key(b)), reverse * ((a > b) - (b > a));
3468                };
3469            },
3470
3471            /**
3472             * Replace a string from-to index
3473             *
3474             * @param {String} string The complete string to replace into
3475             * @param {Number} offset The cursor position to start replacing from
3476             * @param {Number} length The length of the replacing string
3477             * @param {String} replace The replacing string
3478             * @returns {String}
3479             */
3480            replaceAt: function (string, offset, length, replace) {
3481                return (
3482                    string.substring(0, offset) +
3483                    replace +
3484                    string.substring(offset + length)
3485                );
3486            },
3487
3488            /**
3489             * Adds <strong> html around a matched string
3490             *
3491             * @param {String} string The complete string to match from
3492             * @param {String} key
3493             * @param {Boolean} [accents]
3494             * @returns {*}
3495             */
3496            highlight: function (string, keys, accents) {
3497                string = String(string);
3498
3499                var searchString =
3500                        (accents && this.helper.removeAccent.call(this, string)) || string,
3501                    matches = [];
3502
3503                if (!Array.isArray(keys)) {
3504                    keys = [keys];
3505                }
3506
3507                keys.sort(function (a, b) {
3508                    return b.length - a.length;
3509                });
3510
3511                // Make sure the '|' join will be safe!
3512                for (var i = keys.length - 1; i >= 0; i--) {
3513                    if (keys[i].trim() === "") {
3514                        keys.splice(i, 1);
3515                        continue;
3516                    }
3517                    keys[i] = keys[i].replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
3518                }
3519
3520                searchString.replace(
3521                    new RegExp("(?:" + keys.join("|") + ")(?!([^<]+)?>)", "gi"),
3522                    function (match, index, offset) {
3523                        matches.push({
3524                            offset: offset,
3525                            length: match.length
3526                        });
3527                    }
3528                );
3529
3530                for (var i = matches.length - 1; i >= 0; i--) {
3531                    string = this.helper.replaceAt(
3532                        string,
3533                        matches[i].offset,
3534                        matches[i].length,
3535                        "<strong>" +
3536                        string.substr(matches[i].offset, matches[i].length) +
3537                        "</strong>"
3538                    );
3539                }
3540
3541                return string;
3542            },
3543
3544            /**
3545             * Get caret position, used for right arrow navigation
3546             * when hint option is enabled
3547             * @param {Node} element
3548             * @returns {Number} Caret position
3549             */
3550            getCaret: function (element) {
3551                var caretPos = 0;
3552
3553                if (element.selectionStart) {
3554                    // Input & Textarea
3555                    return element.selectionStart;
3556                } else if (document.selection) {
3557                    var r = document.selection.createRange();
3558                    if (r === null) {
3559                        return caretPos;
3560                    }
3561
3562                    var re = element.createTextRange(),
3563                        rc = re.duplicate();
3564                    re.moveToBookmark(r.getBookmark());
3565                    rc.setEndPoint("EndToStart", re);
3566
3567                    caretPos = rc.text.length;
3568                } else if (window.getSelection) {
3569                    // Contenteditable
3570                    var sel = window.getSelection();
3571                    if (sel.rangeCount) {
3572                        var range = sel.getRangeAt(0);
3573                        if (range.commonAncestorContainer.parentNode == element) {
3574                            caretPos = range.endOffset;
3575                        }
3576                    }
3577                }
3578                return caretPos;
3579            },
3580
3581            /**
3582             * For [contenteditable] typeahead node only,
3583             * when an item is clicked set the cursor at the end
3584             * @param {Node} element
3585             */
3586            setCaretAtEnd: function (element) {
3587                if (
3588                    typeof window.getSelection !== "undefined" &&
3589                    typeof document.createRange !== "undefined"
3590                ) {
3591                    var range = document.createRange();
3592                    range.selectNodeContents(element);
3593                    range.collapse(false);
3594                    var sel = window.getSelection();
3595                    sel.removeAllRanges();
3596                    sel.addRange(range);
3597                } else if (typeof document.body.createTextRange !== "undefined") {
3598                    var textRange = document.body.createTextRange();
3599                    textRange.moveToElementText(element);
3600                    textRange.collapse(false);
3601                    textRange.select();
3602                }
3603            },
3604
3605            /**
3606             * Clean strings from possible XSS (script and iframe tags)
3607             * @param string
3608             * @returns {string}
3609             */
3610            cleanStringFromScript: function (string) {
3611                return (
3612                    (typeof string === "string" &&
3613                    string.replace(/<\/?(?:script|iframe)\b[^>]*>/gm, "")) ||
3614                    string
3615                );
3616            },
3617
3618            /**
3619             * Executes an anonymous function or a string reached from the window scope.
3620             *
3621             * @example
3622             * Note: These examples works with every configuration callbacks
3623             *
3624             * // An anonymous function inside the "onInit" option
3625             * onInit: function() { console.log(':D'); };
3626             *
3627             * // myFunction() located on window.coucou scope
3628             * onInit: 'window.coucou.myFunction'
3629             *
3630             * // myFunction(a,b) located on window.coucou scope passing 2 parameters
3631             * onInit: ['window.coucou.myFunction', [':D', ':)']];
3632             *
3633             * // Anonymous function to execute a local function
3634             * onInit: function () { myFunction(':D'); }
3635             *
3636             * @param {String|Array} callback The function to be called
3637             * @param {Array} [extraParams] In some cases the function can be called with Extra parameters (onError)
3638             * @returns {*}
3639             */
3640            executeCallback: function (callback, extraParams) {
3641                if (!callback) {
3642                    return;
3643                }
3644
3645                var _callback;
3646
3647                if (typeof callback === "function") {
3648                    _callback = callback;
3649                } else if (typeof callback === "string" || Array.isArray(callback)) {
3650                    if (typeof callback === "string") {
3651                        callback = [callback, []];
3652                    }
3653
3654                    _callback = this.helper.namespace.call(this, callback[0], window);
3655
3656                    if (typeof _callback !== "function") {
3657                        // {debug}
3658                        if (this.options.debug) {
3659                            _debug.log({
3660                                node: this.selector,
3661                                function: "executeCallback()",
3662                                arguments: JSON.stringify(callback),
3663                                message: 'WARNING - Invalid callback function"'
3664                            });
3665
3666                            _debug.print();
3667                        }
3668                        // {/debug}
3669                        return;
3670                    }
3671                }
3672
3673                return _callback.apply(
3674                    this,
3675                    (callback[1] || []).concat(extraParams ? extraParams : [])
3676                );
3677            },
3678
3679            namespace: function (string, object, method, defaultValue) {
3680                if (typeof string !== "string" || string === "") {
3681                    // {debug}
3682                    if (this.options.debug) {
3683                        _debug.log({
3684                            node: this.options.input || this.selector,
3685                            function: "helper.namespace()",
3686                            arguments: string,
3687                            message: 'ERROR - Missing string"'
3688                        });
3689
3690                        _debug.print();
3691                    }
3692                    // {/debug}
3693                    return false;
3694                }
3695
3696                var value = typeof defaultValue !== "undefined"
3697                    ? defaultValue
3698                    : undefined;
3699
3700                // Exit before looping if the string doesn't contain an object reference
3701                if (!~string.indexOf(".")) {
3702                    return object[string] || value;
3703                }
3704
3705                var parts = string.split("."),
3706                    parent = object || window,
3707                    method = method || "get",
3708                    currentPart = "";
3709
3710                for (var i = 0, length = parts.length; i < length; i++) {
3711                    currentPart = parts[i];
3712
3713                    if (typeof parent[currentPart] === "undefined") {
3714                        if (~["get", "delete"].indexOf(method)) {
3715                            return typeof defaultValue !== "undefined"
3716                                ? defaultValue
3717                                : undefined;
3718                        }
3719                        parent[currentPart] = {};
3720                    }
3721
3722                    if (~["set", "create", "delete"].indexOf(method)) {
3723                        if (i === length - 1) {
3724                            if (method === "set" || method === "create") {
3725                                parent[currentPart] = value;
3726                            } else {
3727                                delete parent[currentPart];
3728                                return true;
3729                            }
3730                        }
3731                    }
3732
3733                    parent = parent[currentPart];
3734                }
3735                return parent;
3736            },
3737
3738            typeWatch: (function () {
3739                var timer = 0;
3740                return function (callback, ms) {
3741                    clearTimeout(timer);
3742                    timer = setTimeout(callback, ms);
3743                };
3744            })()
3745        }
3746    };
3747
3748    /**
3749     * @public
3750     * Implement Typeahead on the selected input node.
3751     *
3752     * @param {Object} options
3753     * @return {Object} Modified DOM element
3754     */
3755    $.fn.typeahead = $.typeahead = function (options) {
3756        return _api.typeahead(this, options);
3757    };
3758
3759    /**
3760     * @private
3761     * API to handles Typeahead methods via jQuery.
3762     */
3763    var _api = {
3764        /**
3765         * Enable Typeahead
3766         *
3767         * @param {Object} node
3768         * @param {Object} options
3769         * @returns {*}
3770         */
3771        typeahead: function (node, options) {
3772            if (!options || !options.source || typeof options.source !== "object") {
3773                // {debug}
3774                _debug.log({
3775                    node: node.selector || (options && options.input),
3776                    function: "$.typeahead()",
3777                    arguments: JSON.stringify((options && options.source) || ""),
3778                    message: 'Undefined "options" or "options.source" or invalid source type - Typeahead dropped'
3779                });
3780
3781                _debug.print();
3782                // {/debug}
3783
3784                return;
3785            }
3786
3787            if (typeof node === "function") {
3788                if (!options.input) {
3789                    // {debug}
3790                    _debug.log({
3791                        node: node.selector,
3792                        function: "$.typeahead()",
3793                        //'arguments': JSON.stringify(options),
3794                        message: 'Undefined "options.input" - Typeahead dropped'
3795                    });
3796
3797                    _debug.print();
3798                    // {/debug}
3799
3800                    return;
3801                }
3802
3803                node = $(options.input);
3804            }
3805            if (!node.length) {
3806                // {debug}
3807                _debug.log({
3808                    node: node.selector,
3809                    function: "$.typeahead()",
3810                    arguments: JSON.stringify(options.input),
3811                    message: "Unable to find jQuery input element - Typeahead dropped"
3812                });
3813
3814                _debug.print();
3815                // {/debug}
3816
3817                return;
3818            }
3819            if (typeof node[0].value === "undefined") {
3820                node[0].value = node.text();
3821            }
3822
3823            // #270 Forcing node.selector, the property was deleted from jQuery3
3824            // In case of multiple init, each of the instances needs it's own selector!
3825            if (node.length === 1) {
3826                node[0].selector =
3827                    node.selector || options.input || node[0].nodeName.toLowerCase();
3828
3829                /*jshint boss:true */
3830                return (window.Typeahead[node[0].selector] = new Typeahead(node, options));
3831            } else {
3832                var instances = {},
3833                    instanceName;
3834
3835                for (var i = 0, ii = node.length; i < ii; ++i) {
3836                    instanceName = node[i].nodeName.toLowerCase();
3837                    if (typeof instances[instanceName] !== "undefined") {
3838                        instanceName += i;
3839                    }
3840                    node[i].selector = instanceName;
3841
3842                    window.Typeahead[instanceName] = instances[instanceName] = new Typeahead(node.eq(i), options);
3843                }
3844
3845                return instances;
3846            }
3847        }
3848    };
3849
3850    // {debug}
3851    var _debug = {
3852        table: {},
3853        log: function (debugObject) {
3854            if (!debugObject.message || typeof debugObject.message !== "string") {
3855                return;
3856            }
3857
3858            this.table[debugObject.message] = $.extend(
3859                {
3860                    node: "",
3861                    function: "",
3862                    arguments: ""
3863                },
3864                debugObject
3865            );
3866        },
3867        print: function () {
3868            if (
3869                Typeahead.prototype.helper.isEmpty(this.table) || !console || !console.table
3870            ) {
3871                return;
3872            }
3873
3874            if (console.group !== undefined || console.table !== undefined) {
3875                console.groupCollapsed("--- jQuery Typeahead Debug ---");
3876                console.table(this.table);
3877                console.groupEnd();
3878            }
3879
3880            this.table = {};
3881        }
3882    };
3883    _debug.log({
3884        message: "WARNING - You are using the DEBUG version. Use /dist/jquery.typeahead.min.js in production."
3885    });
3886
3887    _debug.print();
3888    // {/debug}
3889
3890    // IE8 Shims
3891    window.console = window.console || {
3892            log: function () {
3893            }
3894        };
3895
3896    if (!Array.isArray) {
3897        Array.isArray = function (arg) {
3898            return Object.prototype.toString.call(arg) === "[object Array]";
3899        };
3900    }
3901
3902    if (!("trim" in String.prototype)) {
3903        String.prototype.trim = function () {
3904            return this.replace(/^\s+/, "").replace(/\s+$/, "");
3905        };
3906    }
3907    if (!("indexOf" in Array.prototype)) {
3908        Array.prototype.indexOf = function (find, i /*opt*/) {
3909            if (i === undefined) i = 0;
3910            if (i < 0) i += this.length;
3911            if (i < 0) i = 0;
3912            for (var n = this.length; i < n; i++)
3913                if (i in this && this[i] === find) return i;
3914            return -1;
3915        };
3916    }
3917    if (!Object.keys) {
3918        Object.keys = function (obj) {
3919            var keys = [],
3920                k;
3921            for (k in obj) {
3922                if (Object.prototype.hasOwnProperty.call(obj, k)) {
3923                    keys.push(k);
3924                }
3925            }
3926            return keys;
3927        };
3928    }
3929
3930    return Typeahead;
3931});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.