Subversion Repositories cheapmusic

Rev

Rev 65 | Go to most recent revision | Details | Last modification | View Log | RSS feed

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