Subversion Repositories cheapmusic

Rev

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