Subversion Repositories cheapmusic

Rev

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