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