PageSourceSearch

https://biblestock.org/flexdatalist/jquery.flexdatalist.js

js biblestock.org collected 2026-10-06 08:47:56 UTC 77,380 bytes, 2,083 lines download raw bytes

1/**
2 * jQuery Flexdatalist.
3 * Autocomplete input fields, with support for datalists.
4 *
5 * Version:
6 * 2.3.0
7 *
8 * Depends:
9 * jquery.js > 1.8.3
10 *
11 * Demo and Documentation:
12 * http://projects.sergiodinislopes.pt/flexdatalist/
13 *
14 * Github:
15 * https://github.com/sergiodlopes/jquery-flexdatalist/
16 *
17 */
18
19jQuery.fn.flexdatalist = function (_option, _value) {
20    'use strict';
21
22    var destroy = function ($flex, clear) {
23        $flex.each(function () {
24            var $this = $(this),
25                data = $this.data(),
26                options = data.flexdatalist,
27                $aliascontainer = data.aliascontainer;
28
29            if ($aliascontainer) {
30                $this.removeClass('flexdatalist-set')
31                    .attr({'style': null, 'tabindex': null})
32                    .val((options && options.originalValue && !clear ? options.originalValue : ''))
33                    .removeData('flexdatalist')
34                    .removeData('aliascontainer')
35                    .off();
36                $aliascontainer.remove();
37            }
38        });
39    }
40
41    // Callable stuff
42    if (typeof _option === 'string' && _option !== 'reset') {
43        if (typeof this[0] === 'object' && typeof this[0].fvalue !== 'undefined') {
44            var target = this[0];
45            if (_option === 'destroy') {
46                destroy(this, _value);
47            // Get/Set value
48            } else if (_option === 'value') {
49                if (typeof _value === 'undefined') {
50                    return target.fvalue.get();
51                }
52                target.fvalue.set(_value);
53            // Add value
54            } else if (_option === 'add') {
55                if (typeof _value === 'undefined') {
56                    return target.debug('Missing value to add!');
57                }
58                target.fvalue.add(_value);
59            // Toggle value
60            } else if (_option === 'toggle') {
61                if (typeof _value === 'undefined') {
62                    return target.debug('Missing value to toggle!');
63                }
64                target.fvalue.toggle(_value);
65            // Remove value
66            } else if (_option === 'remove') {
67                if (typeof _value === 'undefined') {
68                    return target.debug('Missing value to remove!');
69                }
70                target.fvalue.remove(_value);
71            // Disabled/enabled
72            } else if (_option === 'disabled') {
73                if (typeof _value === 'undefined') {
74                    return target.fdisabled();
75                }
76                target.fdisabled(_value);
77            // Option(s)
78            } else if (typeof _option === 'string') {
79                if (typeof _value === 'undefined') {
80                    return target.options.get(_option);
81                }
82                target.options.set(_option, _value);
83            }
84            return this;
85        }
86        _option = {_option: _value};
87    }
88
89    // Destroy if already set
90    if (this.length > 0 && typeof this[0].fvalue !== 'undefined') {
91        destroy(this);
92    }
93
94    var _options = $.extend({
95        url: null,
96        data: [],
97        params: {},
98        relatives: null,
99        chainedRelatives: false,
100        cache: true,
101        cacheLifetime: 60,
102        minLength: 3,
103        groupBy: false,
104        selectionRequired: false,
105        focusFirstResult: false,
106        textProperty: null,
107        valueProperty: null,
108        visibleProperties: [],
109        iconProperty: 'thumb',
110        searchIn: ['label'],
111        searchContain: false,
112        searchEqual: false,
113        searchByWord: false,
114        searchDisabled: false,
115        searchDelay: 400,
116        normalizeString: null,
117        multiple: null,
118        disabled: null,
119        maxShownResults: 100,
120        removeOnBackspace: true,
121        noResultsText: 'No results found for "{keyword}"',
122        toggleSelected: false,
123        allowDuplicateValues: false,
124        redoSearchOnFocus: true,
125        requestType: 'get',
126        requestContentType: 'x-www-form-urlencoded',
127        requestHeaders: null,
128        resultsProperty: 'results',
129        keywordParamName: 'keyword',
130        searchContainParamName: 'contain',
131        limitOfValues: 0,
132        valuesSeparator: ',',
133        debug: true
134    }, _option);
135
136    return this.each(function (id) {
137        var $this = $(this),
138            _this = this,
139            _searchTimeout = null,
140            _values = [],
141            fid = 'flex' + id,
142            $alias = null,
143            $multiple = null;
144
145    /**
146     * Initialization
147     */
148        this.init = function () {
149            var options = this.options.init();
150            this.set.up();
151
152            $alias
153            // Focusin
154            .on('focusin', function (event) {
155                _this.action.redoSearchFocus(event);
156                _this.action.showAllResults(event);
157                if ($multiple) {
158                    $multiple.addClass('focus');
159                }
160            })
161            // Keydown
162            .on('input keydown', function (event) {
163                if (_this.keyNum(event) === 9) {
164                    _this.results.remove();
165                }
166                _this.action.keypressValue(event, 188);
167                _this.action.backSpaceKeyRemove(event);
168            })
169            // Keyup
170            .on('input keyup', function (event) {
171                _this.action.keypressValue(event, 13);
172                _this.action.keypressSearch(event);
173                _this.action.copyValue(event);
174                _this.action.backSpaceKeyRemove(event);
175                _this.action.showAllResults(event);
176                _this.action.clearValue(event);
177                _this.action.removeResults(event);
178                _this.action.inputWidth(event);
179            })
180            // Focusout
181            .on('focusout', function (event) {
182                if ($multiple) {
183                    $multiple.removeClass('focus');
184                }
185                _this.action.clearText(event);
186                _this.action.clearValue(event);
187            });
188
189            window.onresize = function (event) {
190                _this.position();
191            };
192
193            // Run garbage collector
194            this.cache.gc();
195
196            if (options.selectionRequired) {
197                _this.fvalue.clear(true, true);
198            }
199            this.fvalue._load(options.originalValue, function (values, matches) {
200                _this.fdisabled(options.disabled);
201                $this.trigger('init:flexdatalist', [options]);
202            }, true);
203        }
204
205    /**
206     * Handle user actions.
207     */
208        this.action = {
209        /**
210         * Add value on comma or enter keypress.
211         */
212            keypressValue: function (event, keyCode) {
213                var key = _this.keyNum(event),
214                    val = $alias[0].value,
215                    options = _this.options.get();
216
217                if (val.length > 0
218                    && key === keyCode
219                    && !options.selectionRequired
220                    && options.multiple) {
221                        var val = $alias[0].value;
222                        event.preventDefault();
223                        event.stopPropagation();
224                        _this.fvalue.extract(val);
225                        _this.results.remove();
226                }
227            },
228        /**
229         * Check if keypress is valid.
230         */
231            keypressSearch: function (event) {
232                var key = _this.keyNum(event),
233                    keyword = $alias.val(),
234                    length = keyword.length,
235                    options = _this.options.get();
236
237                clearTimeout(_searchTimeout);
238                if (!key || (key !== 13 && (key < 37 || key > 40))) {
239                    _searchTimeout = setTimeout(function () {
240                        if ((options.minLength === 0 && length > 0) || (options.minLength > 0 && length >= options.minLength)) {
241                            _this.data.load(function (data) {
242                                _this.search.get(keyword, data, function (matches) {
243                                    _this.results.show(matches);
244                                });
245                            });
246                        }
247                    }, options.searchDelay);
248                }
249            },
250        /**
251         * Redo search if input get's back on focus and no value selected.
252         */
253            redoSearchFocus: function (event) {
254                var val = _this.fvalue.get(),
255                    options = _this.options.get(),
256                    alias = $alias.val();
257                if (options.redoSearchOnFocus && ((alias.length > 0 && options.multiple) || (alias.length > 0 && val.length === 0))) {
258                    this.keypressSearch(event);
259                }
260            },
261        /**
262         * Copy value from alias to target input.
263         */
264            copyValue: function (event) {
265                if (_this.keyNum(event) !== 13) {
266                    var keyword = $alias.val(),
267                        val = _this.fvalue.get(true),
268                        options = _this.options.get();
269                    if (!options.multiple && !options.selectionRequired && keyword.length !== val.length) {
270                        _this.fvalue.extract(keyword);
271                    }
272                }
273            },
274        /**
275         * Remove value on backspace key (multiple input only).
276         */
277            backSpaceKeyRemove: function (event) {
278                var options = _this.options.get();
279                if (options.removeOnBackspace && options.multiple) {
280                    var val = $alias.val(),
281                        $remove = $alias.data('_remove');
282                    if (_this.keyNum(event) === 8) {
283                        if (val.length === 0) {
284                            if ($remove) {
285                                _this.fvalue.remove($remove);
286                                $alias.data('_remove', null);
287                            } else {
288                                $alias.data('_remove', $alias.parents('li:eq(0)').prev());
289                            }
290                        } else {
291                            $alias.data('_remove', null);
292                        }
293                    }
294                }
295            },
296        /**
297         * Show all results if minLength option is 0.
298         */
299            showAllResults: function (event) {
300                var val = $alias.val();
301                val = $.trim(val);
302                if (val === '' && _this.options.get('minLength') === 0) {
303                    _this.data.load(function (data) {
304                        _this.results.show(data);
305                    });
306                }
307            },
308        /**
309         * Calculate input width by number of characters.
310         */
311            inputWidth: function (event) {
312                var options = _this.options.get();
313                if (options.multiple) {
314                    var keyword = $alias.val(),
315                        fontSize = parseInt($alias.css('fontSize').replace('px', '')),
316                        minWidth = 40,
317                        maxWidth = $this.innerWidth(),
318                        width = ((keyword.length + 1) * fontSize);
319
320                    if (width >= minWidth && width <= maxWidth) {
321                        $alias[0].style.width = width + 'px';
322                    }
323                }
324            },
325        /**
326         * Clear text/alias input when criteria is met.
327         */
328            clearText: function (event) {
329                var val = _this.fvalue.get(),
330                    options = _this.options.get();
331
332                if (!options.multiple && options.selectionRequired && val.length === 0) {
333                    $alias[0].value = '';
334                }
335            },
336        /**
337         * Clear value when criteria is met.
338         */
339            clearValue: function (event) {
340                var val = _this.fvalue.get(),
341                    keyword = $alias.val(),
342                    options = _this.options.get();
343
344                if (!options.multiple && options.selectionRequired && keyword.length <= options.minLength) {
345                    _this.fvalue.clear();
346                }
347            },
348        /**
349         * Remove results when criteria is met.
350         */
351            removeResults: function (event) {
352                var keyword = $alias.val(),
353                    options = _this.options.get();
354                if (options.minLength > 0 && keyword.length < options.minLength) {
355                    _this.results.remove();
356                }
357            }
358        }
359
360    /**
361     * Setup flex.
362     */
363        this.set = {
364        /**
365         * Prepare input replacement.
366         */
367            up: function () {
368                $alias = this.getAlias();
369                if (_this.options.get('multiple')) {
370                    $multiple = this.multipleInput($alias);
371                } else {
372                    $alias.insertAfter($this);
373                }
374
375                this.accessibility($alias);
376
377                // Respect autofocus attribute
378                if ($this.attr('autofocus')) {
379                    $alias.trigger('focus');
380                }
381
382                $this.data('aliascontainer', ($multiple ? $multiple : $alias)).addClass('flexdatalist flexdatalist-set').css({
383                    'position': 'absolute',
384                    'top': -14000,
385                    'left': -14000
386                }).attr('tabindex', -1);
387
388                // update input label with alias id
389                var inputId = $this.attr('id'),
390                    aliasId = $alias.attr('id');
391                $('label[for="' + inputId + '"]').attr('for', aliasId);
392
393                this.chained();
394            },
395        /**
396         * Create and return the alias input field for single value input.
397         */
398            getAlias: function () {
399                var aliasid = ($this.attr('id') ? $this.attr('id') + '-flexdatalist' : fid);
400                var $alias = $('<input type="text">')
401                    .attr({
402                        'class': $this.attr('class'),
403                        'name': ($this.attr('name') ? 'flexdatalist-' + $this.attr('name') : null),
404                        'id': aliasid,
405                        'placeholder': $this.attr('placeholder')
406                    })
407                    .addClass('flexdatalist-alias ' + aliasid)
408                    .removeClass('flexdatalist')
409                    .attr('autocomplete', 'off');
410                return $alias;
411            },
412        /**
413         * Multiple values input/list
414         */
415            multipleInput: function ($alias) {
416                $multiple = $('<ul tabindex="1">')
417                    .addClass('flexdatalist-multiple ' + fid)
418                    .css({
419                        'border-color': $this.css('border-left-color'),
420                        'border-width': $this.css('border-left-width'),
421                        'border-style': $this.css('border-left-style'),
422                        'border-radius': $this.css('border-top-left-radius'),
423                        'background-color': $this.css('background-color')
424                    })
425                    .insertAfter($this).on('click', function () {
426                        $(this).find('input').trigger('focus');
427                    });
428
429                $('<li class="input-container">')
430                    .addClass('flexdatalist-multiple-value')
431                    .append($alias)
432                    .appendTo($multiple);
433
434                return $multiple;
435            },
436        /**
437         * Chained inputs handling.
438         */
439            chained: function () {
440                var options = _this.options.get();
441                if (options.relatives && options.chainedRelatives) {
442                    var toggle = function (init) {
443                        options.relatives.each(function () {
444                            var emptyRelative = _this.isEmpty($(this).val()),
445                                empty = _this.isEmpty(_this.value);
446                            // If disabling, clear all values
447                            if (emptyRelative || !empty) {
448                                _this.fvalue.clear();
449                            }
450                            _this.fdisabled(emptyRelative);
451                        });
452                    };
453                    options.relatives.on('change', function () {
454                        toggle();
455                    });
456                    toggle(true);
457                }
458            },
459        /**
460         * Accessibility.
461         */
462            accessibility: function ($input) {
463                var aliasid = ($this.attr('id') ? $this.attr('id') + '-flexdatalist' : fid);
464                var scrReaderAttr = {
465                    'aria-autocomplete': 'list',
466                    'aria-expanded': 'false',
467                    'aria-owns': aliasid + '-results',
468                };
469
470                $input.attr(scrReaderAttr);
471            }
472        }
473
474    /**
475     * Process input value(s) (where the magic happens).
476     */
477        this.fvalue = {
478        /**
479         * Get value(s).
480         */
481            get: function (asString) {
482                var val = _this.value,
483                    options = _this.options.get();
484                if ((options.multiple || this.isJSON()) && !asString) {
485                    return this.toObj(val);
486                }
487                return val;
488            },
489        /**
490         * Set value.
491         * Parse value if necessary.
492         */
493            set: function (val, append) {
494                if (!_this.fdisabled()) {
495                    if (!append) {
496                        this.clear(true);
497                    }
498                    this._load(val);
499                }
500                return $this;
501            },
502        /**
503         * Add value.
504         */
505            add: function (val) {
506                if (_this.options.get('multiple')) {
507                    this.set(val, true);
508                }
509                return this;
510            },
511        /**
512         * Toggle value.
513         */
514            toggle: function (val) {
515                if (!_this.fdisabled()) {
516                    this.multiple.toggle(val);
517                }
518                return this;
519            },
520        /**
521         * Remove value.
522         */
523            remove: function (val) {
524                if (!_this.fdisabled()) {
525                    val = this.toObj(val);
526                    $this.trigger('before:flexdatalist.remove', [val]);
527                    var result = [];
528                    if (_this.isArray(val)) {
529                        $.each(val, function (i, value) {
530                            var removed = _this.fvalue.multiple.remove(value);
531                            if (removed) {
532                                result.push(removed);
533                            }
534                        });
535                    } else {
536                        var _result = this.multiple.remove(val);
537                        if (_result) {
538                            result.push(_result);
539                        }
540                    }
541                    $this
542                        .trigger('after:flexdatalist.remove', [val, result])
543                        .trigger('change:flexdatalist', [result, _this.options.get()])
544                        .trigger('change');
545                }
546                return this;
547            },
548        /**
549         * Load (remote?) value(s).
550         */
551            _load: function (values, callback, init) {
552                var options = _this.options.get(),
553                    valueProp = options.valueProperty,
554                    _values = this.toStr(values),
555                    _val = this.get(true);
556
557                callback = (callback ? callback : $.noop);
558                // If nothing changes, return
559                if (_values.length == 0 && _val.length == 0) {
560                    callback(values);
561                    return;
562                }
563                values = this.toObj(values);
564                if (!_this.isEmpty(values) && !_this.isEmpty(valueProp) && valueProp !== '*') {
565                    if (!_this.isObject(valueProp)) {
566                        valueProp = valueProp.split(',');
567                    }
568                    // Load data
569                    _this.data.load(function (data) {
570                        if (!_this.isObject(values)) {
571                            values = values.split(',');
572                        } else if (!_this.isArray(values)) {
573                            values = [values];
574                        }
575                        var found = [];
576                        for (var idxv = 0; idxv < values.length; idxv++) {
577                            var value = values[idxv];
578                            for (var i = 0; i < data.length; i++) {
579                                var item = data[i];
580                                for (var idx = 0; idx < valueProp.length; idx++) {
581                                    var prop = valueProp[idx],
582                                    value = _this.isDefined(value, prop) ? value[prop] : value;
583                                    if (_this.isDefined(item, prop) && value === item[prop]) {
584                                        found.push(item);
585                                    }
586                                }
587                            }
588                        }
589                        if (found.length > 0) {
590                            _this.fvalue.extract(found, true);
591                        }
592                        callback(values);
593                    }, values);
594                    return;
595                }
596                callback(values);
597                _this.fvalue.extract(values, init);
598            },
599        /**
600         * Extract value and text.
601         */
602            extract: function (values, init) {
603                var options = _this.options.get(),
604                    result = [];
605
606                if (!init) {
607                    $this.trigger('before:flexdatalist.value', [values, options]);
608                }
609
610                if (_this.isArray(values)) {
611                    $.each(values, function (i, value) {
612                        result.push(_this.fvalue._extract(value));
613                    });
614                } else {
615                    result = _this.fvalue._extract(values);
616                }
617
618                if (!init) {
619                    $this
620                        .data('result_selected', values)
621                        .trigger('after:flexdatalist.value', [result, options])
622                        .trigger('change:flexdatalist', [result, options])
623                        .trigger('change');
624                }
625            },
626        /**
627         * @inherited.
628         */
629            _extract: function (val) {
630                var txt = this.text(val),
631                    value = this.value(val),
632                    options = _this.options.get();
633
634                if (options.multiple) {
635                    // For allowDuplicateValues
636                    if (!_this.isEmpty(txt)) {
637                        if (_this.isDup(txt)) {
638                            return;
639                        }
640                        _values.push(txt);
641                        this.multiple.add(value, txt);
642                    }
643                } else {
644                    this.single(value, txt);
645                }
646
647                return {value: value, text: txt};
648            },
649        /**
650         * Default input value.
651         */
652            single: function (val, txt) {
653                if (txt && txt !== $alias.val()) {
654                    $alias[0].value = txt;
655                }
656                _this.value = val;
657            },
658        /**
659         * Input with multiple values.
660         */
661            multiple: {
662            /**
663             * Add value and item on list.
664             */
665                add: function (val, txt) {
666                    var _multiple = this,
667                        $li = this.li(val, txt);
668
669                    // Toggle
670                    $li.on('click', function () {
671                        _multiple.toggle($(this));
672                    // Remove
673                    }).find('.fdl-remove').on('click', function () {
674                        _this.fvalue.remove($(this).parent());
675                    });
676
677                    this.push(val);
678                    $alias[0].value = '';
679                    this.handleLimit();
680                },
681            /**
682             * Push value to input.
683             */
684                push: function (val, index) {
685                    var current = _this.fvalue.get();
686                    if (current.includes(val)) {
687                        return;
688                    }
689                    val = _this.fvalue.toObj(val);
690                    current.push(val);
691                    val = _this.fvalue.toStr(current);
692                    _this.value = val;
693                },
694            /**
695             * Toggle value.
696             */
697                toggle: function (val) {
698                    var options = _this.options.get();
699                    if (!options.toggleSelected) {
700                        return;
701                    }
702
703                    var $li = this.findLi(val);
704                    if (!$li) {
705                        return;
706                    }
707
708                    var data = $li.data(),
709                        action = $li.hasClass('disabled') ? 'enable' : 'disable',
710                        eventArgs = [{value: data.value, text: data.text, action: action}, options];
711
712                    $this.trigger('before:flexdatalist.toggle', eventArgs);
713
714                    if (action === 'enable') {
715                        $li.removeClass('disabled');
716                    } else {
717                        $li.addClass('disabled');
718                    }
719
720                    var current = [];
721                    $multiple.find('li.toggle:not(.disabled)').each(function () {
722                        var $item = $(this);
723                        current.push($item.data('value'));
724                    });
725
726                    current = _this.fvalue.toStr(current);
727                    _this.value = current;
728
729                    $this
730                        .trigger('after:flexdatalist.toggle', eventArgs)
731                        .trigger('change:flexdatalist', eventArgs)
732                        .trigger('change');
733
734                },
735            /**
736             * Remove value from input.
737             */
738                remove: function (val) {
739                    var $li = this.findLi(val);
740                    if (!$li) {
741                        return;
742                    }
743
744                    var values = _this.fvalue.get(),
745                        index = $li.index(),
746                        data = $li.data(),
747                        arg = {value: data.value, text: data.text};
748
749                    values.splice(index, 1);
750                    values = _this.fvalue.toStr(values);
751                    _this.value = values;
752                    $li.remove();
753                    _this.fvalue.multiple.handleLimit();
754
755                    // For allowDuplicateValues
756                    _values.splice(index, 1);
757
758                    this.handleLimit();
759
760                    return arg;
761                },
762            /**
763             * Remove all.
764             */
765                removeAll: function () {
766                    var values = _this.fvalue.get(),
767                        options = _this.options.get();
768
769                    $this.trigger('before:flexdatalist.remove.all', [values, options]);
770
771                    $multiple.find('li:not(.input-container)').remove();
772
773                    _this.value = '';
774                    _values = [];
775
776                    this.handleLimit();
777
778                    $this.trigger('after:flexdatalist.remove.all', [values, options]);
779                },
780            /**
781             * Create new item and return it.
782             */
783                li: function (val, txt) {
784                    var $inputContainer = $multiple.find('li.input-container');
785                    var options = _this.options.get();
786                    return $('<li>')
787                        .addClass('value' + (options.toggleSelected ? ' toggle' : ''))
788                        .append('<span class="text">' + txt + '</span>')
789                        .append('<span class="fdl-remove">&times;</span>')
790                        .data({
791                            'text': txt,
792                            'value': _this.fvalue.toStr(val)
793                        })
794                        .insertBefore($inputContainer);
795                },
796            /**
797             * Handle the limit.
798             *
799             * @return void
800             */
801                handleLimit: function () {
802                    var isAtLimit = this.isAtLimit();
803                    var $input = $multiple.find('li.input-container');
804                    isAtLimit ? $input.hide() : $input.show();
805                },
806            /**
807             * Check the limit of items.
808             *
809             * @return bool True if reached the limit, false otherwise
810             */
811                isAtLimit: function () {
812                    var limit = _this.options.get('limitOfValues');
813                    if (!(limit > 0)) {
814                        return false;
815                    }
816                    return limit == _values.length;
817                },
818            /**
819             * Get li item from value.
820             */
821                findLi: function ($li) {
822                    if (!($li instanceof jQuery)) {
823                        var val = $li;
824                        $li = null;
825                        $multiple.find('li:not(.input-container)').each(function () {
826                            var $_li = $(this);
827                            if ($_li.data('value') === val) {
828                                $li = $_li;
829                                return false;
830                            }
831                        });
832                    } else if ($li.length === 0) {
833                        $li = null;
834                    }
835                    return $li;
836                },
837            /**
838             * Get li item from value.
839             */
840                isEmpty: function () {
841                    return this.get().length > 0;
842                }
843            },
844        /**
845         * Get value that will be set on input field.
846         */
847            value: function (item) {
848                var value = item,
849                    options = _this.options.get(),
850                    valueProperty = options.valueProperty;
851
852                if (_this.isObject(item)) {
853                    if (this.isJSON() || this.isMixed()) {
854                        delete item.name_highlight;
855                        if (_this.isArray(valueProperty)) {
856                            var _value = {};
857                            for (var i = 0; i < valueProperty.length; i++) {
858                                var propValue = _this.getPropertyValue(item, valueProperty[i]);
859                                if (propValue) {
860                                    _value[valueProperty[i]] = propValue;
861                                }
862                            }
863                            value = this.toStr(_value);
864                        } else {
865                            value = this.toStr(item);
866                        }
867                    } else if (_this.isDefined(item, valueProperty)) {
868                        value = _this.getPropertyValue(item, valueProperty);
869                    } else if (_this.isDefined(item, options.searchIn[0])) {
870                        value = _this.getPropertyValue(item, options.searchIn[0]);
871                    } else {
872                        value = null;
873                    }
874                }
875                return value;
876            },
877        /**
878         * Get text that will be shown to user on the alias input field.
879         */
880            text: function (item) {
881                var text = item,
882                    options = _this.options.get();
883
884                if (_this.isObject(item)) {
885                    text = _this.getPropertyValue(item, options.searchIn[0]);
886                    if (_this.isDefined(item, options.textProperty)) {
887                        text = _this.getPropertyValue(item, options.textProperty);
888                    } else {
889                        text = this.placeholders.replace(item, options.textProperty, text);
890                    }
891                }
892
893                //text = _this.escapeHtml(text);
894
895                return text;
896            },
897        /**
898         * Process text placeholders.
899         */
900            placeholders: {
901                replace: function (item, pattern, fallback) {
902                    if (_this.isObject(item) && typeof pattern === 'string') {
903                        var properties = this.parse(pattern);
904                        if (!_this.isEmpty(item) && properties) {
905                            $.each(properties, function (string, property) {
906                                if (_this.isDefined(item, property)) {
907                                    pattern = pattern.replace(string, _this.getPropertyValue(item, property));
908                                }
909                            });
910                            return pattern;
911                        }
912                    }
913                    return fallback;
914                },
915                parse: function (pattern) {
916                    var matches = pattern.match(/\{.+?\}/g);
917                    if (!matches) {
918                        return false;
919                    }
920                    var properties = {};
921                    matches.map(function (string) {
922                        properties[string] = string.slice(1, -1);
923                    });
924                    return properties;
925                }
926            },
927        /**
928         * Clear input value(s).
929         */
930            clear: function (alias, init) {
931                var current = _this.value,
932                    options = _this.options.get();
933
934                if (options.multiple) {
935                    this.multiple.removeAll();
936                }
937
938                _this.value = '';
939                if (alias) {
940                    $alias[0].value = '';
941                }
942                if (current !== '' && !init) {
943                    $this
944                        .trigger('change:flexdatalist', [{value: '', text: ''}, options])
945                        .trigger('clear:flexdatalist', [{value: '', text: ''}, options])
946                        .trigger('change');
947                }
948                _values = [];
949                return this;
950            },
951        /**
952         * Value to object.
953         */
954            toObj: function (val) {
955                if (typeof val !== 'object') {
956                    var options = _this.options.get();
957                    if (_this.isEmpty(val) || !_this.isDefined(val)) {
958                        val = options.multiple ? [] : (this.isJSON() ? {} : '');
959                    } else if (this.isCSV()) {
960                        val = val.toString().split(options.valuesSeparator);
961                        val = val.map(function (v) {
962                            return v.trim();
963                        });
964                    } else if ((this.isMixed() || this.isJSON()) && this.isJSON(val)) {
965                        val = JSON.parse(val);
966                    } else if (typeof val === 'number') {
967                        val = val.toString();
968                    }
969                }
970                return val;
971            },
972        /**
973         * Is value expected to be JSON (either object or string).
974         */
975            toStr: function (val) {
976                if (typeof val !== 'string') {
977                    if (_this.isEmpty(val) || !_this.isDefined(val)) {
978                        val = '';
979                    } else if (typeof val === 'number') {
980                        val = val.toString();
981                    } else if (this.isCSV()) {
982                        val = val.join(_this.options.get('valuesSeparator'));
983                    } else if (this.isJSON() || this.isMixed()) {
984                        val = JSON.stringify(val);
985                    }
986                }
987                return $.trim(val);
988            },
989        /**
990         * If argument is passed, will check if is a valid JSON object/string.
991         * otherwise will check if JSON is the value expected for input
992         */
993            isJSON: function (str) {
994                if (typeof str !== 'undefined') {
995                    if (_this.isObject(str)) {
996                        str = JSON.stringify(str);
997                    } else if (typeof str !== 'string') {
998                        return false;
999                    }
1000                    return (str.indexOf('{') === 0 || str.indexOf('[{') === 0);
1001                }
1002                var options = _this.options.get(),
1003                    prop = options.valueProperty;
1004                return (_this.isObject(prop) || prop === '*');
1005            },
1006        /**
1007         * Is value expected to be JSON (either object or string).
1008         */
1009            isMixed: function () {
1010                var options = _this.options.get();
1011                return !options.selectionRequired && options.valueProperty === '*';
1012            },
1013        /**
1014         * Is value expected to be CSV?
1015         */
1016            isCSV: function () {
1017                return (!this.isJSON() && _this.options.get('multiple'));
1018            }
1019        }
1020
1021    /**
1022     * Data.
1023     */
1024        this.data = {
1025        /**
1026         * Load data from all sources.
1027         */
1028            load: function (callback, load) {
1029                var __this = this,
1030                    data = [];
1031                $this.trigger('before:flexdatalist.data');
1032                // Remote data
1033                this.url(function (remote) {
1034                    data = data.concat(remote);
1035                    // Static data
1036                    __this.static(function (_static) {
1037                        data = data.concat(_static);
1038                        // Datalist
1039                        __this.datalist(function (list) {
1040                            data = data.concat(list);
1041
1042                            $this.trigger('after:flexdatalist.data', [data]);
1043                            callback(data);
1044                        });
1045                    });
1046                }, load);
1047            },
1048        /**
1049         * Get static data.
1050         */
1051            static: function (callback) {
1052                var __this = this,
1053                    options = _this.options.get();
1054                // Remote source
1055                if (typeof options.data === 'string') {
1056                    var url = options.data,
1057                        cache = _this.cache.read(url, true);
1058                    if (cache) {
1059                        callback(cache);
1060                        return;
1061                    }
1062                    this.remote({
1063                        url: url,
1064                        success: function (data) {
1065                            options.data = data;
1066                            callback(data);
1067                            _this.cache.write(url, data, options.cacheLifetime, true);
1068                        }
1069                    });
1070                } else {
1071                    if (typeof options.data !== 'object') {
1072                        options.data = [];
1073                    }
1074                    callback(options.data);
1075                }
1076            },
1077        /**
1078         * Get datalist values.
1079         */
1080            datalist: function (callback) {
1081                var list = $this.attr('list'),
1082                    datalist = [];
1083                if (!_this.isEmpty(list)) {
1084                    $('#' + list).find('option').each(function () {
1085                        var $option = $(this),
1086                            val = $option.val(),
1087                            label = $option.text();
1088                        datalist.push({
1089                            label: (label.length > 0 ? label : val),
1090                            value: val
1091                        });
1092                    });
1093                }
1094                callback(datalist);
1095            },
1096        /**
1097         * Get remote data.
1098         */
1099            url: function (callback, load) {
1100                var keyword = $alias.val(),
1101                    options = _this.options.get(),
1102                    keywordParamName = options.keywordParamName,
1103                    searchContainParamName = options.searchContainParamName,
1104                    value = _this.fvalue.get(),
1105                    relatives = this.relativesData();
1106
1107                if (_this.isEmpty(options.url)) {
1108                    return callback([]);
1109                }
1110
1111                var _opts = {};
1112                if (options.requestType === 'post') {
1113                    $.each(options, function (option, value) {
1114                        if (option.indexOf('_') == 0 || option == 'data') {
1115                            return;
1116                        }
1117                        _opts[option] = value;
1118                    });
1119                    delete _opts.relatives;
1120                }
1121
1122                // Cache
1123                var cacheKey = _this.cache.keyGen({
1124                        relative: relatives,
1125                        load: load,
1126                        keyword: keyword,
1127                        contain: options.searchContain
1128                    }, options.url),
1129                    cache = _this.cache.read(cacheKey, true);
1130
1131                if (cache) {
1132                    callback(cache);
1133                    return;
1134                }
1135
1136                var params = typeof(options.params) == 'function' ?
1137                        options.params.call($this[0], keyword) :
1138                        options.params;
1139
1140                var data = $.extend(
1141                    relatives,
1142                    params,
1143                    {
1144                        load: load,
1145                        selected: value,
1146                        original: options.originalValue,
1147                        options: _opts
1148                    }
1149                );
1150
1151                data[keywordParamName] = keyword;
1152                data[searchContainParamName] = options.searchContain;
1153
1154                this.remote({
1155                    url: options.url,
1156                    data: data,
1157                    success: function (_data) {
1158                        var _keyword = $alias.val();
1159                        // Is this really needed?
1160                        if (_keyword.length >= keyword.length) {
1161                            callback(_data);
1162                        }
1163                        _this.cache.write(cacheKey, _data, options.cacheLifetime, true);
1164                    }
1165                });
1166            },
1167        /**
1168         * AJAX request.
1169         */
1170            remote: function (settings) {
1171                var __this = this,
1172                    options = _this.options.get();
1173
1174                    // Prevent get data when pressing backspace button
1175                if ($this.hasClass('flexdatalist-loading')) {
1176                    return;
1177                }
1178                $this.addClass('flexdatalist-loading');
1179
1180                if (options.requestContentType === 'json') {
1181                    settings.data = JSON.stringify(settings.data);
1182                }
1183
1184                $.ajax($.extend(
1185                    {
1186                        type: options.requestType,
1187                        dataType: 'json',
1188                        headers: options.requestHeaders,
1189                        contentType: 'application/' + options.requestContentType + '; charset=UTF-8',
1190                        complete: function () {
1191                            $this.removeClass('flexdatalist-loading');
1192                        }
1193                    }, settings, {
1194                        success: function (data) {
1195                            data = __this.extractRemoteData(data);
1196                            settings.success(data);
1197                        }
1198                    }
1199                ));
1200            },
1201        /**
1202         * Extract remote data from server response.
1203         */
1204            extractRemoteData: function (data) {
1205                var options = _this.options.get(),
1206                    _data = _this.isDefined(data, options.resultsProperty) ? data[options.resultsProperty] : data;
1207
1208                if (typeof _data === 'string' && _data.indexOf('[{') === 0) {
1209                    _data = JSON.parse(_data);
1210                }
1211                if (_data && _data.options) {
1212                    _this.options.set($.extend({}, options, _data.options));
1213                }
1214                if (_this.isObject(_data)) {
1215                    return _data;
1216                }
1217                return [];
1218            },
1219        /**
1220         * Extract remote data from server response.
1221         */
1222            relativesData: function () {
1223                var relatives = _this.options.get('relatives'),
1224                    data = {};
1225                if (relatives) {
1226                    data['relatives'] = {};
1227                    relatives.each(function () {
1228                        var $_input = $(this),
1229                            name = $_input.attr('name')
1230                                .split('][').join('-')
1231                                .split(']').join('-')
1232                                .split('[').join('-')
1233                                .replace(/^\|+|\-+$/g, '');
1234                        data['relatives'][name] = $_input.val();
1235                    });
1236                }
1237                return data;
1238            }
1239        }
1240
1241    /**
1242     * Search.
1243     */
1244        this.search = {
1245        /**
1246         * Search for keywords in data and return matches to given callback.
1247         */
1248            get: function (keywords, data, callback) {
1249                var __this = this,
1250                    options = _this.options.get(),
1251                    matches = data;
1252
1253                if (!options.searchDisabled) {
1254                    var matches = _this.cache.read(keywords);
1255                    if (!matches) {
1256                        $this.trigger('before:flexdatalist.search', [keywords, data]);
1257                        if (!_this.isEmpty(keywords)) {
1258                            matches = [];
1259                            var words = __this.split(keywords);
1260                            for (var index = 0; index < data.length; index++) {
1261                                var item = data[index];
1262                                if (_this.isDup(item)) {
1263                                    continue;
1264                                }
1265                                item = __this.matches(item, words);
1266                                if (item) {
1267                                    matches.push(item);
1268                                }
1269                            }
1270                        }
1271                        _this.cache.write(keywords, matches, 2);
1272                        $this.trigger('after:flexdatalist.search', [keywords, data, matches]);
1273                    }
1274                }
1275
1276                callback(matches);
1277            },
1278        /**
1279         * Match against searchable properties.
1280         */
1281            matches: function (item, keywords) {
1282                var _item = $.extend({}, item),
1283                    found = [],
1284                    options = _this.options.get(),
1285                    searchIn = options.searchIn;
1286
1287                if (keywords.length > 0) {
1288                    for (var index = 0; index < searchIn.length; index++) {
1289                        var searchProperty = searchIn[index];
1290                        if (!_this.isDefined(item, searchProperty) || !item[searchProperty]) {
1291                            continue;
1292                        }
1293
1294                        var text = item[searchProperty].toString(),
1295                            highlight = text,
1296                            strings = this.split(text);
1297
1298                        for (var kwindex = 0; kwindex < keywords.length; kwindex++) {
1299                            var keyword = keywords[kwindex];
1300                            if (this.find(keyword, strings)) {
1301                                found.push(keyword);
1302                                highlight = this.highlight(keyword, highlight);
1303                            }
1304                        }
1305
1306                        if (highlight !== text) {
1307                            _item[searchProperty + '_highlight'] = this.highlight(highlight);
1308                        }
1309                    }
1310
1311                }
1312
1313                if (found.length === 0 || (options.searchByWord && found.length < (keywords.length - 1))) {
1314                    return false;
1315                }
1316
1317                return _item;
1318            },
1319        /**
1320         * Wrap found keyword with span.highlight.
1321         */
1322            highlight: function (keyword, text) {
1323                if (text) {
1324                    // Fix by https://github.com/antunesl
1325                    keyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1326                    return text.replace(
1327                        new RegExp(keyword, (_this.options.get('searchContain') ? "ig" : "i")),
1328                        '|:|$&|::|'
1329                    );
1330                }
1331                keyword = keyword.split('|:|').join('<span class="highlight">');
1332                return keyword.split('|::|').join('</span>');
1333            },
1334        /**
1335         * Search for keyword(s) in string.
1336         */
1337            find: function (keyword, splitted) {
1338                var options = _this.options.get();
1339                for (var index = 0; index < splitted.length; index++) {
1340                    var text = splitted[index];
1341                    text = this.normalizeString(text),
1342                    keyword = this.normalizeString(keyword);
1343                    if (options.searchEqual) {
1344                        return text == keyword;
1345                    }
1346                    if ((options.searchContain ? (text.indexOf(keyword) >= 0) : (text.indexOf(keyword) === 0))) {
1347                        return true;
1348                    }
1349                }
1350                return false;
1351            },
1352        /**
1353         * Split string by words if needed.
1354         */
1355            split: function (keywords) {
1356                if (typeof keywords === 'string') {
1357                    keywords = [$.trim(keywords)];
1358                }
1359                if (_this.options.get('searchByWord')) {
1360                    for (var index = 0; index < keywords.length; index++) {
1361                        var keyword = $.trim(keywords[index]);
1362                        if (keyword.indexOf(' ') > 0) {
1363                            var words = keyword.split(' ');
1364                            $.merge(keywords, words);
1365                        }
1366                    }
1367                }
1368                return keywords;
1369            },
1370        /**
1371         * Normalize string to a consistent one to perform the search/match.
1372         */
1373            normalizeString: function (string) {
1374                if (typeof string === 'string') {
1375                    var normalizeString = _this.options.get('normalizeString');
1376                    if (typeof normalizeString === 'function') {
1377                        string = normalizeString(string);
1378                    }
1379                    string = string.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
1380                    return string.toUpperCase();
1381                }
1382                return string;
1383            }
1384        }
1385
1386    /**
1387     * Handle results.
1388     */
1389        this.results = {
1390        /**
1391         * Save key = value data in local storage (if supported)
1392         *
1393         * @param string key Data key string
1394         */
1395            show: function (results) {
1396                var __this = this,
1397                    options = _this.options.get();
1398
1399                this.remove(true);
1400
1401                if (!results) {
1402                    return;
1403                } else if(results.length === 0) {
1404                    this.empty(options.noResultsText);
1405                    return;
1406                }
1407
1408                var $ul = this.container();
1409                if (!options.groupBy) {
1410                    this.items(results, $ul);
1411                } else {
1412                    results = this.group(results);
1413                    Object.keys(results).forEach(function (groupName, index) {
1414                        var items = results[groupName],
1415                            property = options.groupBy,
1416                            groupText = _this.results.highlight(items[0], property, groupName);
1417
1418                        var $li = $('<li>')
1419                                .addClass('group')
1420                                .append($('<span>')
1421                                    .addClass('group-name')
1422                                    .html(groupText)
1423                                )
1424                                .append($('<span>')
1425                                    .addClass('group-item-count')
1426                                    .text(' ' + items.length)
1427                                )
1428                                .appendTo($ul);
1429
1430                        __this.items(items, $ul);
1431                    });
1432                }
1433
1434                var $li = $ul.find('li:not(.group)');
1435
1436                // Listen to result's item events
1437                $li.on('click', function (event) {
1438                    var item = $(this).data('item');
1439                    if (item) {
1440                        _this.fvalue.extract(item);
1441                        __this.remove();
1442                        $this.trigger('select:flexdatalist', [item, options]);
1443                    }
1444                }).on('hover', function () {
1445                    $li.removeClass('active');
1446                    $(this).addClass('active').trigger('active:flexdatalist.results', [$(this).data('item')]);
1447                }, function () {
1448                    $(this).removeClass('active');
1449                });
1450
1451                if (options.focusFirstResult) {
1452                    $li.filter(':first').addClass('active');
1453                }
1454            },
1455        /**
1456         * Remove results container.
1457         */
1458            empty: function (text) {
1459                if (_this.isEmpty(text)) {
1460                    return;
1461                }
1462                var $container = this.container(),
1463                    keyword = $alias.val();
1464
1465                text = text.split('{keyword}').join(keyword);
1466                $('<li>')
1467                    .addClass('item no-results')
1468                    .append(text)
1469                    .appendTo($container)
1470
1471                $this.trigger('empty:flexdatalist.results', [text]);
1472            },
1473        /**
1474         * Items iteration.
1475         */
1476            items: function (items, $resultsContainer) {
1477                var max = _this.options.get('maxShownResults');
1478
1479                $this.trigger('show:flexdatalist.results', [items]);
1480
1481                for (var index = 0; index < items.length; index++) {
1482                    if (max > 0 && max === index) {
1483                        break;
1484                    }
1485                    this.item(items[index], index, items.length).appendTo($resultsContainer);
1486                }
1487
1488                $this.trigger('shown:flexdatalist.results', [items]);
1489            },
1490        /**
1491         * Result item creation.
1492         */
1493            item: function (item, index, total) {
1494                var $li = $('<li>')
1495                        .attr({
1496                            'role': 'option',
1497                            'tabindex': '-1',
1498                            'aria-posinset': index + 1,
1499                            'aria-setsize': total
1500                        })
1501                        .data('item', item)
1502                        .addClass('item'),
1503                    options = _this.options.get(),
1504                    visibleProperties = options.visibleProperties;
1505
1506                for (var index = 0; index < visibleProperties.length; index++) {
1507                    var visibleProperty = visibleProperties[index];
1508
1509                    if (visibleProperty.indexOf('{') > -1) {
1510                        var str = _this.fvalue.placeholders.replace(item, visibleProperty),
1511                            parsed = _this.fvalue.placeholders.parse(visibleProperty);
1512                        $item = $('<span>')
1513                            .addClass('item item-' + Object.values(parsed).join('-'))
1514                            .html(str + ' ');
1515                    } else {
1516                        if (options.groupBy && options.groupBy === visibleProperty || !_this.isDefined(item, visibleProperty)) {
1517                            continue;
1518                        }
1519                        var $item = {};
1520                        if (visibleProperty === options.iconProperty) {
1521                            // Icon
1522                            $item = $('<img>')
1523                                .addClass('item item-' + visibleProperty)
1524                                .attr('src', item[visibleProperty]);
1525                        } else {
1526                            var propertyText = _this.results.highlight(item, visibleProperty);
1527                            // Other text properties
1528                            $item = $('<span>')
1529                                .addClass('item item-' + visibleProperty)
1530                                .html(propertyText + ' ');
1531                        }
1532                    }
1533
1534                    $item.appendTo($li);
1535                }
1536
1537                $this.trigger('item:flexdatalist.results', [$li, item]);
1538
1539                return $li;
1540            },
1541        /**
1542         * Results container
1543         */
1544            container: function () {
1545                var $target = $this;
1546
1547                if ($multiple) {
1548                    $target = $multiple;
1549                }
1550
1551                var $container = $('ul.flexdatalist-results');
1552
1553                if ($container.length === 0) {
1554                    $container = $('<ul>')
1555                        .addClass('flexdatalist-results ')
1556                        .appendTo('body')
1557                        .attr({
1558                            'id': $alias.attr('id') + '-results',
1559                            'role': 'listbox'
1560                        })
1561                        .css({
1562                            'border-color': $target.css("border-left-color"),
1563                            'border-width': '1px',
1564                            'border-bottom-left-radius': $target.css("border-bottom-left-radius"),
1565                            'border-bottom-right-radius': $target.css("border-bottom-right-radius")
1566                        }).data({
1567                            target: ($multiple ? $multiple : $alias),
1568                            input: $this
1569                        });
1570                    _this.position($alias);
1571                }
1572
1573                return $container;
1574            },
1575        /**
1576         * Results container
1577         */
1578            group: function (results) {
1579                var data = [],
1580                    groupProperty = _this.options.get('groupBy');
1581
1582                for (var index = 0; index < results.length; index++) {
1583                    var _data = results[index];
1584                    if (_this.isDefined(_data, groupProperty)) {
1585                        var propertyValue = _data[groupProperty];
1586                        if (!_this.isDefined(data, propertyValue)) {
1587                            data[propertyValue] = [];
1588                        }
1589                        data[propertyValue].push(_data);
1590                    }
1591                }
1592
1593                return data;
1594            },
1595        /**
1596         * Check if highlighted property value exists,
1597         * if true, return it, if not, fallback to given string
1598         */
1599            highlight: function (item, property, fallback) {
1600                if (_this.isDefined(item, property + '_highlight')) {
1601                    return item[property + '_highlight'];
1602                }
1603                return (_this.isDefined(item, property) ? item[property] : fallback);
1604            },
1605        /**
1606         * Set given item as active
1607         */
1608            active: function ($item) {
1609
1610            },
1611        /**
1612         * Remove results
1613         */
1614            remove: function (itemsOnly) {
1615                var selector = 'ul.flexdatalist-results';
1616                if (itemsOnly) {
1617                    selector = 'ul.flexdatalist-results li';
1618                }
1619                $this.trigger('remove:flexdatalist.results');
1620                $(selector).remove();
1621                $this.trigger('removed:flexdatalist.results');
1622            }
1623        }
1624
1625    /**
1626    * Interface for localStorage.
1627    */
1628        this.cache = {
1629        /**
1630         * Save key = value data in local storage (if supported)
1631         *
1632         * @param string key Data key string
1633         * @param mixed value Value to be saved
1634         * @param int lifetime In Seconds
1635         * @return mixed
1636         */
1637            write: function (key, value, lifetime, global) {
1638                if (_this.cache.isSupported()) {
1639                    key = this.keyGen(key, undefined, global);
1640                    var object = {
1641                        value: value,
1642                        // Get current UNIX timestamp
1643                        timestamp: _this.unixtime(),
1644                        lifetime: (lifetime ? lifetime : false)
1645                    };
1646                    localStorage.setItem(key, JSON.stringify(object));
1647                }
1648            },
1649       /**
1650        * Read data associated with given key
1651        *
1652        * @param string key Data key string
1653        * @return mixed
1654        */
1655            read: function (key, global) {
1656                if (_this.cache.isSupported()) {
1657                    key = this.keyGen(key, undefined, global);
1658                    var data = localStorage.getItem(key);
1659                    if (data) {
1660                        var object = JSON.parse(data);
1661                        if (!this.expired(object)) {
1662                            return object.value;
1663                        }
1664                        localStorage.removeItem(key);
1665                    }
1666                }
1667                return null;
1668            },
1669        /**
1670         * Remove data associated with given key.
1671         *
1672         * @param string key Data key string
1673         */
1674            delete: function (key, global) {
1675                if (_this.cache.isSupported()) {
1676                    key = this.keyGen(key, undefined, global);
1677                    localStorage.removeItem(key);
1678                }
1679            },
1680        /**
1681         * Clear all data.
1682         */
1683            clear: function () {
1684                if (_this.cache.isSupported()) {
1685                    for (var key in localStorage){
1686                        if (key.indexOf(fid) > -1 || key.indexOf('global') > -1) {
1687                            localStorage.removeItem(key);
1688                        }
1689                    }
1690                    localStorage.clear();
1691                }
1692            },
1693       /**
1694        * Run cache garbage collector to prevent using all localStorage's
1695        * available space.
1696        */
1697            gc: function () {
1698                if (_this.cache.isSupported()) {
1699                    for (var key in localStorage){
1700                        if (key.indexOf(fid) > -1 || key.indexOf('global') > -1) {
1701                            var data = localStorage.getItem(key);
1702                            data = JSON.parse(data);
1703                            if (this.expired(data)) {
1704                                localStorage.removeItem(key);
1705                            }
1706                        }
1707                    }
1708                }
1709            },
1710       /**
1711        * Check if browser supports localtorage.
1712        *
1713        * @return boolean True if supports, false otherwise
1714        */
1715            isSupported: function () {
1716                if (_this.options.get('cache')) {
1717                    try {
1718                        return 'localStorage' in window && window['localStorage'] !== null;
1719                    } catch (e) {
1720                        return false;
1721                    }
1722                }
1723                return false;
1724            },
1725       /**
1726        * Check if cache data as expired.
1727        *
1728        * @param object object Data to check
1729        * @return boolean True if expired, false otherwise
1730        */
1731            expired: function (object) {
1732                if (object.lifetime) {
1733                    var diff = (_this.unixtime() - object.timestamp);
1734                    return object.lifetime <= diff;
1735                }
1736                return false;
1737            },
1738       /**
1739        * Generate cache key from object or string.
1740        *
1741        * @return string Cache key
1742        */
1743            keyGen: function (str, seed, global) {
1744                if (typeof str === 'object') {
1745                    str = JSON.stringify(str);
1746                }
1747                var i, l,
1748                    hval = (seed === undefined) ? 0x811c9dc5 : seed;
1749
1750                for (i = 0, l = str.length; i < l; i++) {
1751                    hval ^= str.charCodeAt(i);
1752                    hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
1753                }
1754                return (global ? 'global' : fid) + ("0000000" + (hval >>> 0).toString(16)).substr(-8);
1755            }
1756        }
1757
1758    /**
1759     * Options handler.
1760     */
1761        this.options = {
1762            init: function () {
1763                var options = $.extend({},
1764                    _options,
1765                    $this.data(),
1766                    {
1767                        multiple: (_options.multiple === null ? $this.is('[multiple]') : _options.multiple),
1768                        disabled: (_options.disabled === null ? $this.is('[disabled]') : _options.disabled),
1769                        originalValue: _this.value
1770                    }
1771                );
1772                this.set(options);
1773                return options;
1774            },
1775            get: function (option) {
1776                var options = $this.data('flexdatalist');
1777                if (!option) {
1778                    return options ? options : {};
1779                }
1780                return _this.isDefined(options, option) ? options[option] : null;
1781            },
1782            set: function (option, value) {
1783                var options = this.get();
1784                if (_this.isDefined(options, option) && _this.isDefined(value)) {
1785                    options[option] = value;
1786                } else if (_this.isObject(option)) {
1787                    options = this._normalize(option);
1788                }
1789                $this.data('flexdatalist', options);
1790                return $this;
1791            },
1792            _normalize: function (options) {
1793                options.searchIn = _this.csvToArray(options.searchIn);
1794                options.relatives = options.relatives && $(options.relatives).length > 0 ? $(options.relatives) : null;
1795                options.textProperty = options.textProperty === null ? options.searchIn[0] : options.textProperty;
1796                options.visibleProperties = _this.csvToArray(options.visibleProperties, options.searchIn);
1797                if (options.valueProperty === '*' && options.multiple && !options.selectionRequired) {
1798                    throw new Error('Selection must be required for multiple, JSON fields!');
1799                }
1800                return options;
1801            }
1802        }
1803
1804    /**
1805     * Position results below parent element.
1806     */
1807        this.position = function () {
1808            var $results = $('ul.flexdatalist-results'),
1809                $target = $results.data('target');
1810            if ($results.length > 0) {
1811                // Set some required CSS properties
1812                $results.css({
1813                    'width': $target.outerWidth() + 'px',
1814                    'top': (($target.offset().top + $target.outerHeight())) + 'px',
1815                    'left': $target.offset().left + 'px'
1816                });
1817            }
1818        }
1819
1820    /**
1821     * Handle disabled state.
1822     */
1823        this.fdisabled = function (disabled) {
1824            if (this.isDefined(disabled)) {
1825                $this.prop('disabled', disabled);
1826                $alias.prop('disabled', disabled);
1827                if ($multiple) {
1828                    $multiple.css('background-color', $this.css('background-color'));
1829                    var $btns = $multiple.find('li .fdl-remove'),
1830                        $input = $multiple.find('li.input-container');
1831                    if (disabled) {
1832                        $multiple.addClass('disabled');
1833                        if ($btns.length > 0) {
1834                            $input.hide();
1835                        }
1836                        $btns.hide();
1837                    } else {
1838                        $multiple.removeClass('disabled');
1839                        $input.show();
1840                        $btns.show();
1841                    }
1842                }
1843                this.options.set('disabled', disabled);
1844            }
1845            return this.options.get('disabled');
1846        }
1847
1848    /**
1849     * Check for dup values.
1850     */
1851        this.isDup = function (val) {
1852            if (!this.options.get('allowDuplicateValues')) {
1853                return _values.length > 0 && _values.indexOf(this.fvalue.text(val)) > -1;
1854            }
1855            return false;
1856        }
1857
1858    /**
1859     * Get key code from event.
1860     */
1861        this.keyNum = function (event) {
1862            return event.which || event.keyCode;
1863        }
1864
1865    /**
1866     * Is variable empty.
1867     */
1868        this.isEmpty = function (value) {
1869            if (!_this.isDefined(value)) {
1870                return true;
1871            } else if (value === null) {
1872                return true;
1873            } else if (value === true) {
1874                return false;
1875            } else if (this.length(value) === 0) {
1876                return true;
1877            } else if ($.trim(value) === '') {
1878                return true;
1879            }
1880            return false;
1881        }
1882
1883    /**
1884     * Is variable an object.
1885     */
1886        this.isObject = function (value) {
1887            return (value && typeof value === 'object');
1888        }
1889
1890    /**
1891     * Get length of variable.
1892     */
1893        this.length = function (value) {
1894            if (this.isObject(value)) {
1895                return Object.keys(value).length;
1896            } else if (typeof value === 'number' || typeof value.length === 'number') {
1897                return value.toString().length;
1898            }
1899            return 0;
1900        }
1901
1902    /**
1903     * Check if variable (and optionally property) is defined.
1904     */
1905        this.isDefined = function (variable, property) {
1906            var _variable = (typeof variable !== 'undefined');
1907            if (_variable && typeof property !== 'undefined') {
1908                return (typeof this.getPropertyValue(variable, property) !== 'undefined');
1909            }
1910            return _variable;
1911        }
1912
1913    /**
1914     * Check if variable is an array.
1915     */
1916        this.isArray = function (variable) {
1917            return Object.prototype.toString.call(variable) === '[object Array]';
1918        }
1919
1920    /**
1921     * Get unixtime stamp.
1922     *
1923     * @return boolean True if supports, false otherwise
1924     */
1925        this.unixtime = function (time) {
1926            var date = new Date();
1927            if (time) {
1928                date = new Date(time);
1929            }
1930            return Math.round(date.getTime() / 1000);
1931        }
1932
1933    /**
1934     * To array.
1935     */
1936        this.csvToArray = function (value, _default) {
1937            if (value.length === 0) {
1938                return _default;
1939            }
1940            return typeof value === 'string' ? value.split(_this.options.get('valuesSeparator')) : value;
1941        },
1942    /**
1943     * A function to take a string written in dot notation style, and use it to
1944     * find a nested object property inside of an object.
1945     *
1946     * Useful in a plugin or module that accepts a JSON array of objects, but
1947     * you want to let the user specify where to find various bits of data
1948     * inside of each custom object instead of forcing a standardized
1949     * property list.
1950     *
1951     * Thanks to https://github.com/sylvainblot for the PR.
1952     *
1953     * @param object object (optional) The object to search
1954     * @param string path A dot notation style path to the value (ie "urls.small")
1955     * @return the value of the property in question
1956     * @see https://github.com/sergiodlopes/jquery-flexdatalist/pull/195
1957     */
1958        this.getPropertyValue = function (obj, path) {
1959            if (!obj || typeof path !== 'string') {
1960                return undefined;
1961            }
1962
1963            var parts = path.split('.');
1964            while (parts.length && (obj = obj[parts.shift()]));
1965            return obj;
1966        }
1967    /**
1968     * Escape HTML special characters.
1969     *
1970     * @param string str String to escape HTML
1971     * @return string String with HTML special characters escaped
1972     */
1973        this.escapeHtml = function (str) {
1974            return str
1975                 .replace(/&/g, "&amp;")
1976                 .replace(/</g, "&lt;")
1977                 .replace(/>/g, "&gt;")
1978                 .replace(/"/g, "&quot;")
1979                 .replace(/'/g, "&#039;");
1980        }
1981
1982    /**
1983     * Plugin warnings for debug.
1984     */
1985        this.debug = function (msg, data) {
1986            var options = _this.options.get();
1987            if (!options.debug) {
1988                return;
1989            }
1990            if (!data) {
1991                data = {};
1992            }
1993            msg = 'Flexdatalist: ' + msg;
1994            console.warn(msg);
1995            console.log($.extend({
1996                inputName: $this.attr('name'),
1997                options: options
1998            }, data));
1999            console.log('--- /flexdatalist ---');
2000        }
2001
2002    // Go!
2003        this.init();
2004    });
2005}
2006
2007jQuery(function ($) {
2008    var $document = $(document);
2009    // Handle results selection list keyboard shortcuts and events.
2010    if (!$document.data('flexdatalist')) {
2011        // Remove results on outside click
2012        $(document).on('mouseup', function (event) {
2013            var $container = $('.flexdatalist-results'),
2014                $target = $container.data('target');
2015            if ((!$target || !$target.is(':focus')) && !$container.is(event.target) && $container.has(event.target).length === 0) {
2016                $container.remove();
2017            }
2018        // Keyboard navigation
2019        }).on('keydown', function (event) {
2020            var $ul = $('.flexdatalist-results'),
2021                $li = $ul.find('li'),
2022                $active = $li.filter('.active'),
2023                index = $active.index(),
2024                length = $li.length,
2025                keynum = event.which || event.keyCode;
2026
2027            if (length === 0) {
2028                return;
2029            }
2030
2031            // on escape key, remove results
2032            if (keynum === 27) {
2033                $ul.remove();
2034                return;
2035            }
2036
2037            // Enter/tab key
2038            if (keynum === 13) {
2039                event.preventDefault();
2040                $active.trigger('click');
2041            // Up/Down key
2042            } else if (keynum === 40 || keynum === 38) {
2043                event.preventDefault();
2044                // Down key
2045                if (keynum === 40) {
2046                    if (index < length && $active.nextAll('.item').first().length > 0) {
2047                        $active = $active.removeClass('active').nextAll('.item').first().addClass('active');
2048                    } else {
2049                        $active = $li.removeClass('active').filter('.item:first').addClass('active');
2050                    }
2051                // Up key
2052                } else if (keynum === 38) {
2053                    if (index > 0 && $active.prevAll('.item').first().length > 0) {
2054                        $active = $active.removeClass('active').prevAll('.item').first().addClass('active');
2055                    } else {
2056                        $active = $li.removeClass('active').filter('.item:last').addClass('active');
2057                    }
2058                }
2059
2060                $active.trigger('active:flexdatalist.results', [$active.data('item')]);
2061
2062                // Scroll to
2063                var position = ($active.prev().length === 0 ? $active : $active.prev()).position().top;
2064                $ul.animate({
2065                    scrollTop: position + $ul.scrollTop()
2066                }, 100);
2067            }
2068        }).data('flexdatalist', true);
2069    }
2070
2071    jQuery('input.flexdatalist:not(.flexdatalist-set):not(.autodiscover-disabled)').flexdatalist();
2072});
2073
2074(function ($) {
2075    var jVal = $.fn.val;
2076    $.fn.val = function (value) {
2077        var isFlex = this.length > 0 && typeof this[0].fvalue !== 'undefined';
2078        if (typeof value === 'undefined') {
2079            return isFlex ? this[0].fvalue.get(true) : jVal.call(this);
2080        }
2081        return isFlex ? this[0].fvalue.set(value) : jVal.call(this, value);
2082    };
2083})(jQuery);

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.