1/*! 2 * jQuery Typeahead 3 * Copyright (C) 2019 RunningCoder.org 4 * Licensed under the MIT license 5 * 6 * @author Tom Bertrand 7 * @version 2.11.0 (2019-10-31) 8 * @link http://www.runningcoder.org/jquerytypeahead/ 9 */ 10(function (factory) { 11 if (typeof define === "function" && define.amd) { 12 define("jquery-typeahead", ["jquery"], function (jQuery) { 13 return factory(jQuery); 14 }); 15 } else if (typeof module === "object" && module.exports) { 16 module.exports = (function (jQuery, root) { 17 if (jQuery === undefined) { 18 if (typeof window !== "undefined") { 19 jQuery = require("jquery"); 20 } else { 21 jQuery = require("jquery")(root); 22 } 23 } 24 return factory(jQuery); 25 })(); 26 } else { 27 factory(jQuery); 28 } 29})(function ($) { 30 "use strict"; 31 32 window.Typeahead = { 33 version: '2.11.0' 34 }; 35 36 /** 37 * @private 38 * Default options 39 * @link http://www.runningcoder.org/jquerytypeahead/documentation/ 40 */ 41 var _options = { 42 input: null, // *RECOMMENDED*, jQuery selector to reach Typeahead's input for initialization 43 minLength: 2, // Accepts 0 to search on focus, minimum character length to perform a search 44 maxLength: false, // False as "Infinity" will not put character length restriction for searching results 45 maxItem: 8, // Accepts 0 / false as "Infinity" meaning all the results will be displayed 46 dynamic: false, // When true, Typeahead will get a new dataset from the source option on every key press 47 delay: 300, // delay in ms when dynamic option is set to true 48 order: null, // "asc" or "desc" to sort results 49 offset: false, // Set to true to match items starting from their first character
50 hint: false, // Added support for excessive "space" characters 51 accent: false, // Will allow to type accent and give letter equivalent results, also can define a custom replacement object 52 highlight: true, // Added "any" to highlight any word in the template, by default true will only highlight display keys 53 multiselect: null, // Multiselect configuration object, see documentation for all options 54 group: false, // Improved feature, Boolean,string,object(key, template (string, function)) 55 groupOrder: null, // New feature, order groups "asc", "desc", Array, Function 56 maxItemPerGroup: null, // Maximum number of result per Group 57 dropdownFilter: false, // Take group options string and create a dropdown filter 58 dynamicFilter: null, // Filter the typeahead results based on dynamic value, Ex: Players based on TeamID 59 backdrop: false, // Add a backdrop behind Typeahead results 60 backdropOnFocus: false, // Display the backdrop option as the Typeahead input is :focused 61 cache: false, // Improved option, true OR 'localStorage' OR 'sessionStorage' 62 ttl: 3600000, // Cache time to live in ms 63 compression: false, // Requires LZString library 64 searchOnFocus: false, // Display search results on input focus 65 blurOnTab: true, // Blur Typeahead when Tab key is pressed, if false Tab will go though search results 66 resultContainer: null, // List the results inside any container string or jQuery object 67 generateOnLoad: null, // Forces the source to be generated on page load even if the input is not focused! 68 mustSelectItem: false, // The submit function only gets called if an item is selected 69 href: null, // String or Function to format the url for right-click & open in new tab on link results 70 display: ["display"], // Allows search in multiple item keys ["display1", "display2"] 71 template: null, // Display template of each of the result list 72 templateValue: null, // Set the input value template when an item is clicked 73 groupTemplate: null, // Set a custom template for the groups 74 correlativeTemplate: false, // Compile display keys, enables multiple key search from the template string 75 emptyTemplate: false, // Display an empty template if no result 76 cancelButton: true, // If text is detected in the input, a cancel button will be available to reset the input (pressing ESC also cancels) 77 loadingAnimation: true, // Display a loading animation when typeahead is doing request / searching for results 78 asyncResult: false, // If set to true, the search results will be displayed as they are beging received from the requests / async data function 79 filter: true, // Set to false or function to bypass Typeahead filtering. WARNING: accent, correlativeTemplate, offset & matcher will not be interpreted 80 matcher: null, // Add an extra filtering function after the typeahead functions 81 source: null, // Source of data for Typeahead to filter 82 callback: { 83 onInit: null, // When Typeahead is first initialized (happens only once) 84 onReady: null, // When the Typeahead initial preparation is completed 85 onShowLayout: null, // Called when the layout is shown 86 onHideLayout: null, // Called when the layout is hidden 87 onSearch: null, // When data is being fetched & analyzed to give search results 88 onResult: null, // When the result container is displayed 89 onLayoutBuiltBefore: null, // When the result HTML is build, modify it before it get showed 90 onLayoutBuiltAfter: null, // Modify the dom right after the results gets inserted in the result container 91 onNavigateBefore: null, // When a key is pressed to navigate the results, before the navigation happens 92 onNavigateAfter: null, // When a key is pressed to navigate the results 93 onEnter: null, // When an item in the result list is focused 94 onLeave: null, // When an item in the result list is blurred 95 onClickBefore: null, // Possibility to e.preventDefault() to prevent the Typeahead behaviors 96 onClickAfter: null, // Happens after the default clicked behaviors has been executed 97 onDropdownFilter: null, // When the dropdownFilter is changed, trigger this callback 98 onSendRequest: null, // Gets called when the Ajax request(s) are sent 99 onReceiveRequest: null, // Gets called when the Ajax request(s) are all received 100 onPopulateSource: null, // Perform operation on the source data before it gets in Typeahead data 101 onCacheSave: null, // Perform operation on the source data before it gets in Typeahead cache 102 onSubmit: null, // When Typeahead form is submitted 103 onCancel: null // Triggered if the typeahead had text inside and is cleared 104 }, 105 selector: { 106 container: "typeahead__container", 107 result: "typeahead__result", 108 list: "typeahead__list", 109 group: "typeahead__group", 110 item: "typeahead__item", 111 empty: "typeahead__empty", 112 display: "typeahead__display", 113 query: "typeahead__query", 114 filter: "typeahead__filter", 115 filterButton: "typeahead__filter-button", 116 dropdown: "typeahead__dropdown", 117 dropdownItem: "typeahead__dropdown-item", 118 labelContainer: "typeahead__label-container", 119 label: "typeahead__label", 120 button: "typeahead__button", 121 backdrop: "typeahead__backdrop", 122 hint: "typeahead__hint", 123 cancelButton: "typeahead__cancel-button" 124 }, 125 debug: false // Display debug information (RECOMMENDED for dev environment) 126 }; 127 128 /** 129 * @private 130 * Event namespace 131 */ 132 var _namespace = ".typeahead"; 133 134 /** 135 * @private 136 * Accent equivalents 137 */ 138 var _accent = { 139 from: "ãà áäâẽèéëêìÃïîõòóöôùúüûñç", 140 to: "aaaaaeeeeeiiiiooooouuuunc" 141 }; 142 143 /** 144 * #62 IE9 doesn't trigger "input" event when text gets removed (backspace, ctrl+x, etc) 145 * @private 146 */ 147 var _isIE9 = ~window.navigator.appVersion.indexOf("MSIE 9."); 148 149 /** 150 * #193 Clicking on a suggested option does not select it on IE10/11 151 * @private 152 */ 153 var _isIE10 = ~window.navigator.appVersion.indexOf("MSIE 10"); 154 var _isIE11 = ~window.navigator.userAgent.indexOf("Trident") 155 ? ~window.navigator.userAgent.indexOf("rv:11") 156 : false;
157 158 // SOURCE GROUP RESERVED WORDS: ajax, data, url 159 // SOURCE ITEMS RESERVED KEYS: group, display, data, matchedKey, compiled, href 160 161 /** 162 * @constructor 163 * Typeahead Class 164 * 165 * @param {object} node jQuery input object 166 * @param {object} options User defined options 167 */ 168 var Typeahead = function (node, options) { 169 this.rawQuery = node.val() || ""; // Unmodified input query 170 this.query = node.val() || ""; // Input query 171 this.selector = node[0].selector; // Typeahead instance selector (to reach from window.Typeahead[SELECTOR]) 172 this.deferred = null; // Promise when "input" event in triggered, this.node.triggerHandler('input').then(() => {}) 173 this.tmpSource = {}; // Temp var to preserve the source order for the searchResult function 174 this.source = {}; // The generated source kept in memory 175 this.dynamicGroups = []; // Store the source groups that are defined as dynamic 176 this.hasDynamicGroups = false; // Boolean if at least one of the groups has a dynamic source 177 this.generatedGroupCount = 0; // Number of groups generated, if limit reached the search can be done 178 this.groupBy = "group"; // This option will change according to filtering or custom grouping 179 this.groups = []; // Array of all the available groups, used to build the groupTemplate 180 this.searchGroups = []; // Array of groups to generate when Typeahead searches data 181 this.generateGroups = []; // Array of groups to generate when Typeahead requests data 182 this.requestGroups = []; // Array of groups to request via Ajax 183 this.result = []; // Results based on Source-query match (only contains the displayed elements) 184 this.tmpResult = {}; // Temporary object of results, before they get passed to the buildLayout function 185 this.groupTemplate = ""; // Result template at the {{group}} level 186 this.resultHtml = null; // HTML Results (displayed elements) 187 this.resultCount = 0; // Total results based on Source-query match 188 this.resultCountPerGroup = {}; // Total results based on Source-query match per group 189 this.options = options; // Typeahead options (Merged default & user defined) 190 this.node = node; // jQuery object of the Typeahead <input> 191 this.namespace = 192 "." + 193 this.helper.slugify.call(this, this.selector) + 194 _namespace; // Every Typeahead instance gets its own namespace for events 195 this.isContentEditable = typeof this.node.attr('contenteditable') !== "undefined" 196 && this.node.attr('contenteditable') !== "false"; 197 this.container = null; // Typeahead container, usually right after <form> 198 this.resultContainer = null; // Typeahead result container (html) 199 this.item = null; // Selected item 200 this.items = null; // Multiselect selected items 201 this.comparedItems = null; // Multiselect items stored for comparison 202 this.xhr = {}; // Ajax request(s) stack 203 this.hintIndex = null; // Numeric value of the hint index in the result list 204 this.filters = { // Filter list for searching, dropdown and dynamic(s) 205 dropdown: {}, // Dropdown menu if options.dropdownFilter is set 206 dynamic: {} // Checkbox / Radio / Select to filter the source data 207 }; 208 this.dropdownFilter = { 209 static: [], // Objects that has a value 210 dynamic: [] 211 }; 212 this.dropdownFilterAll = null; // The last "all" definition 213 this.isDropdownEvent = false; // If a dropdownFilter is clicked, this will be true to trigger the callback 214 215 this.requests = {}; // Store the group:request instead of generating them every time 216 217 this.backdrop = {}; // The backdrop object 218 this.hint = {}; // The hint object 219 this.label = {}; // The label object 220 this.hasDragged = false;
220 // Will cancel mouseend events if true 221 this.focusOnly = false; // Focus the input preventing any operations 222 this.displayEmptyTemplate; // Display the empty template in the result list 223 224 this.__construct(); 225 }; 226 227 Typeahead.prototype = { 228 _validateCacheMethod: function (cache) { 229 var supportedCache = ["localStorage", "sessionStorage"], 230 supported; 231 232 if (cache === true) { 233 cache = "localStorage"; 234 } else if (typeof cache === "string" && !~supportedCache.indexOf(cache)) { 235 // {debug} 236 if (this.options.debug) { 237 _debug.log({ 238 node: this.selector, 239 function: "extendOptions()", 240 message: 'Invalid options.cache, possible options are "localStorage" or "sessionStorage"' 241 }); 242 243 _debug.print(); 244 } 245 // {/debug} 246 return false; 247 } 248 249 supported = typeof window[cache] !== "undefined"; 250 251 try { 252 window[cache].setItem("typeahead", "typeahead"); 253 window[cache].removeItem("typeahead"); 254 } catch (e) { 255 supported = false; 256 } 257 258 return (supported && cache) || false; 259 }, 260 261 extendOptions: function () { 262 this.options.cache = this._validateCacheMethod(this.options.cache); 263 264 if (this.options.compression) { 265 if (typeof LZString !== "object" || !this.options.cache) { 266 // {debug} 267 if (this.options.debug) { 268 _debug.log({ 269 node: this.selector, 270 function: "extendOptions()", 271 message: "Missing LZString Library or options.cache, no compression will occur." 272 }); 273 274 _debug.print(); 275 } 276 // {/debug} 277 this.options.compression = false; 278 } 279 } 280 281 if (!this.options.maxLength || isNaN(this.options.maxLength)) { 282 this.options.maxLength = Infinity; 283 } 284 285 if ( 286 typeof this.options.maxItem !== "undefined" && ~[0, false].indexOf(this.options.maxItem) 287 ) { 288 this.options.maxItem = Infinity; 289 } 290 291 if ( 292 this.options.maxItemPerGroup && !/^\d+$/.test(this.options.maxItemPerGroup) 293 ) { 294 this.options.maxItemPerGroup = null; 295 } 296 297 if (this.options.display && !Array.isArray(this.options.display)) { 298 this.options.display = [this.options.display]; 299 } 300 301 if (this.options.multiselect) { 302 this.items = []; 303 this.comparedItems = []; 304 if (typeof this.options.multiselect.matchOn === "string") { 305 this.options.multiselect.matchOn = [this.options.multiselect.matchOn]; 306 } 307 } 308 309 if (this.options.group) { 310 if (!Array.isArray(this.options.group)) { 311 if (typeof this.options.group === "string") { 312 this.options.group = { 313 key: this.options.group 314 }; 315 } else if (typeof this.options.group === "boolean") { 316 this.options.group = { 317 key: "group" 318 }; 319 } 320 321 this.options.group.key = this.options.group.key || "group"; 322 } else { 323 // {debug} 324 if (this.options.debug) { 325 _debug.log({ 326 node: this.selector, 327 function: "extendOptions()", 328 message: "options.group must be a boolean|string|object as of 2.5.0" 329 }); 330 331 _debug.print(); 332 } 333 // {/debug} 334 } 335 } 336 337 if (this.options.highlight && !~["any", true].indexOf(this.options.highlight)) { 338 this.options.highlight = false;
339 } 340 341 if ( 342 this.options.dropdownFilter && 343 this.options.dropdownFilter instanceof Object 344 ) { 345 if (!Array.isArray(this.options.dropdownFilter)) { 346 this.options.dropdownFilter = [this.options.dropdownFilter]; 347 } 348 for (var i = 0, ii = this.options.dropdownFilter.length; i < ii; ++i) { 349 this.dropdownFilter[ 350 this.options.dropdownFilter[i].value ? "static" : "dynamic" 351 ].push(this.options.dropdownFilter[i]); 352 } 353 } 354 355 if (this.options.dynamicFilter && !Array.isArray(this.options.dynamicFilter)) { 356 this.options.dynamicFilter = [this.options.dynamicFilter]; 357 } 358 359 if (this.options.accent) { 360 if (typeof this.options.accent === "object") { 361 if ( 362 this.options.accent.from && 363 this.options.accent.to && 364 this.options.accent.from.length !== this.options.accent.to.length 365 ) { 366 // {debug} 367 if (this.options.debug) { 368 _debug.log({ 369 node: this.selector, 370 function: "extendOptions()", 371 message: 'Invalid "options.accent", from and to must be defined and same length.' 372 }); 373 374 _debug.print(); 375 } 376 // {/debug} 377 } 378 379 } else { 380 this.options.accent = _accent; 381 } 382 } 383 384 if (this.options.groupTemplate) { 385 this.groupTemplate = this.options.groupTemplate; 386 } 387 388 if (this.options.resultContainer) { 389 if (typeof this.options.resultContainer === "string") { 390 this.options.resultContainer = $(this.options.resultContainer); 391 } 392 393 if ( 394 !(this.options.resultContainer instanceof $) || !this.options.resultContainer[0] 395 ) { 396 // {debug} 397 if (this.options.debug) { 398 _debug.log({ 399 node: this.selector, 400 function: "extendOptions()", 401 message: 'Invalid jQuery selector or jQuery Object for "options.resultContainer".' 402 }); 403 404 _debug.print(); 405 } 406 // {/debug} 407 } else { 408 this.resultContainer = this.options.resultContainer; 409 } 410 } 411 412 if ( 413 this.options.group && 414 this.options.group.key 415 ) { 416 this.groupBy = this.options.group.key; 417 } 418 419 // Compatibility onClick callback 420 if (this.options.callback && this.options.callback.onClick) { 421 this.options.callback.onClickBefore = this.options.callback.onClick; 422 delete this.options.callback.onClick; 423 } 424 425 // Compatibility onNavigate callback 426 if (this.options.callback && this.options.callback.onNavigate) { 427 this.options.callback.onNavigateBefore = this.options.callback.onNavigate; 428 delete this.options.callback.onNavigate; 429 } 430 431 this.options = $.extend(true, {}, _options, this.options); 432 }, 433 434 unifySourceFormat: function () { 435 this.dynamicGroups = []; 436 437 // source: ['item1', 'item2', 'item3'] 438 if (Array.isArray(this.options.source)) { 439 this.options.source = { 440 group: { 441 data: this.options.source 442 } 443 }; 444 } 445 446 // source: "http://www.test.com/url.json" 447 if (typeof this.options.source === "string") { 448 this.options.source = { 449 group: { 450 ajax: { 451 url: this.options.source 452 } 453 } 454 }; 455 } 456 457 if (this.options.source.ajax) { 458 this.options.source = { 459 group: { 460 ajax: this.options.source.ajax 461 } 462 }; 463 } 464 465 // source: {data: ['item1', 'item2'], url: "http://www.test.com/url.json"} 466 if (this.options.source.url || this.options.source.data) { 467 this.options.source = { 468 group: this.options.source 469 }; 470 } 471 472 var group, groupSource, tmpAjax; 473 474 for (group in this.options.source) { 475 if (!this.options.source.hasOwnProperty(group)) continue; 476 477 groupSource = this.options.source[group]; 478 479 // source: {group: "http://www.test.com/url.json"} 480 if (typeof groupSource === "string") { 481 groupSource = { 482 ajax: { 483 url: groupSource 484 } 485 }; 486 } 487 488 // source: {group: {url: ["http://www.test.com/url.json", "json.path"]}} 489 tmpAjax = groupSource.url || groupSource.ajax; 490 if (Array.isArray(tmpAjax)) { 491 groupSource.ajax = 492 typeof tmpAjax[0] === "string" 493 ? { 494 url: tmpAjax[0] 495 } 496 : tmpAjax[0]; 497 groupSource.ajax.path = groupSource.ajax.path || tmpAjax[1] || null; 498 delete groupSource.url; 499 } else { 500 // source: {group: {url: {url: "http://www.test.com/url.json", method: "GET"}}} 501 // source: {group: {url: "http://www.test.com/url.json", dataType: "jsonp"}} 502 if (typeof groupSource.url === "object") { 503 groupSource.ajax = groupSource.url; 504 } else if (typeof groupSource.url === "string") { 505 groupSource.ajax = { 506 url: groupSource.url 507 }; 508 } 509 delete groupSource.url; 510 } 511 512 if (!groupSource.data && !groupSource.ajax) { 513 // {debug} 514 if (this.options.debug) { 515 _debug.log({ 516 node: this.selector, 517 function: "unifySourceFormat()", 518 arguments: JSON.stringify(this.options.source), 519 message: 'Undefined "options.source.' + 520 group + 521 '.[data|ajax]" is Missing - Typeahead dropped' 522 }); 523 524 _debug.print(); 525 } 526 // {/debug} 527 528 return false;
529 } 530 531 if (groupSource.display && !Array.isArray(groupSource.display)) { 532 groupSource.display = [groupSource.display]; 533 } 534 535 groupSource.minLength = 536 typeof groupSource.minLength === "number" 537 ? groupSource.minLength 538 : this.options.minLength; 539 groupSource.maxLength = 540 typeof groupSource.maxLength === "number" 541 ? groupSource.maxLength 542 : this.options.maxLength; 543 groupSource.dynamic = 544 typeof groupSource.dynamic === "boolean" || this.options.dynamic; 545 546 if (groupSource.minLength > groupSource.maxLength) { 547 groupSource.minLength = groupSource.maxLength; 548 } 549 this.options.source[group] = groupSource; 550 551 if (this.options.source[group].dynamic) { 552 this.dynamicGroups.push(group); 553 } 554 555 groupSource.cache = 556 typeof groupSource.cache !== "undefined" 557 ? this._validateCacheMethod(groupSource.cache) 558 : this.options.cache; 559 560 if (groupSource.compression) { 561 if (typeof LZString !== "object" || !groupSource.cache) { 562 // {debug} 563 if (this.options.debug) { 564 _debug.log({ 565 node: this.selector, 566 function: "unifySourceFormat()", 567 message: "Missing LZString Library or group.cache, no compression will occur on group: " + 568 group 569 }); 570 571 _debug.print(); 572 } 573 // {/debug} 574 groupSource.compression = false; 575 } 576 } 577 } 578 579 this.hasDynamicGroups = 580 this.options.dynamic || !!this.dynamicGroups.length; 581 582 return true; 583 }, 584 585 init: function () { 586 this.helper.executeCallback.call(this, this.options.callback.onInit, [ 587 this.node 588 ]); 589 590 this.container = this.node.closest("." + this.options.selector.container); 591 592 // {debug} 593 if (this.options.debug) { 594 _debug.log({ 595 node: this.selector, 596 function: "init()", 597 //'arguments': JSON.stringify(this.options), 598 message: "OK - Typeahead activated on " + this.selector 599 }); 600 601 _debug.print(); 602 } 603 // {/debug} 604 }, 605 606 delegateEvents: function () { 607 var scope = this, 608 events = [ 609 "focus" + this.namespace, 610 "input" + this.namespace, 611 "propertychange" + this.namespace, // IE8 Fix 612 "keydown" + this.namespace, 613 "keyup" + this.namespace, // IE9 Fix 614 "search" + this.namespace, 615 "generate" + this.namespace 616 ]; 617 618 // #149 - Adding support for Mobiles 619 $("html") 620 .on("touchmove", function () { 621 scope.hasDragged = true; 622 }) 623 .on("touchstart", function () { 624 scope.hasDragged = false; 625 }); 626 627 this.node 628 .closest("form") 629 .on("submit", function (e) { 630 if ( 631 scope.options.mustSelectItem && 632 scope.helper.isEmpty(scope.item) 633 ) { 634 e.preventDefault(); 635 return; 636 } 637 638 if (!scope.options.backdropOnFocus) { 639 scope.hideLayout(); 640 } 641 642 if (scope.options.callback.onSubmit) { 643 return scope.helper.executeCallback.call( 644 scope, 645 scope.options.callback.onSubmit, 646 [scope.node, this, scope.item || scope.items, e] 647 ); 648 } 649 }) 650 .on("reset", function () { 651 // #221 - Reset Typeahead on form reset. 652 // setTimeout to re-queue the `input.typeahead` event at the end 653 setTimeout(function () { 654 scope.node.trigger("input" + scope.namespace); 655 // #243 - minLength: 0 opens the Typeahead results 656 scope.hideLayout(); 657 }); 658 }); 659 660 // IE8 fix 661 var preventNextEvent = false;
662 663 // IE10/11 fix 664 if (this.node.attr("placeholder") && (_isIE10 || _isIE11)) { 665 var preventInputEvent = true; 666 667 this.node.on("focusin focusout", function () { 668 preventInputEvent = !!(!this.value && this.placeholder); 669 }); 670 671 this.node.on("input", function (e) { 672 if (preventInputEvent) { 673 e.stopImmediatePropagation(); 674 preventInputEvent = false; 675 } 676 }); 677 } 678 679 this.node 680 .off(this.namespace) 681 .on(events.join(" "), function (e, data) { 682 switch (e.type) { 683 case "generate": 684 scope.generateSource(Object.keys(scope.options.source)); 685 break; 686 case "focus": 687 if (scope.focusOnly) { 688 scope.focusOnly = false; 689 break; 690 } 691 if (scope.options.backdropOnFocus) { 692 scope.buildBackdropLayout(); 693 scope.showLayout(); 694 } 695 if (scope.options.searchOnFocus && !scope.item) { 696 scope.deferred = $.Deferred(); 697 scope.assignQuery(); 698 scope.generateSource(); 699 } 700 break; 701 case "keydown": 702 if (e.keyCode === 8 703 && scope.options.multiselect 704 && scope.options.multiselect.cancelOnBackspace 705 && scope.query === '' 706 && scope.items.length 707 ) { 708 scope.cancelMultiselectItem(scope.items.length - 1, null, e); 709 } else if (e.keyCode && ~[9, 13, 27, 38, 39, 40].indexOf(e.keyCode)) { 710 preventNextEvent = true; 711 scope.navigate(e); 712 } 713 break; 714 case "keyup": 715 if ( 716 _isIE9 && 717 scope.node[0].value.replace(/^\s+/, "").toString().length < 718 scope.query.length 719 ) { 720 scope.node.trigger("input" + scope.namespace); 721 } 722 break; 723 case "propertychange": 724 if (preventNextEvent) { 725 preventNextEvent = false; 726 break; 727 } 728 case "input": 729 scope.deferred = $.Deferred(); 730 scope.assignQuery(); 731 732 // #195 Trigger an onCancel event if the Typeahead is cleared 733 if (scope.rawQuery === "" && scope.query === "") { 734 e.originalEvent = data || {}; 735 scope.helper.executeCallback.call( 736 scope, 737 scope.options.callback.onCancel, 738 [scope.node, scope.item, e] 739 ); 740 scope.item = null; 741 } 742 743 scope.options.cancelButton && 744 scope.toggleCancelButtonVisibility(); 745 746 if ( 747 scope.options.hint && 748 scope.hint.container && 749 scope.hint.container.val() !== "" 750 ) { 751 if (scope.hint.container.val().indexOf(scope.rawQuery) !== 0) { 752 scope.hint.container.val(""); 753 if (scope.isContentEditable) { 754 scope.hint.container.text(""); 755 } 756 } 757 } 758 759 if (scope.hasDynamicGroups) { 760 scope.helper.typeWatch(function () { 761 scope.generateSource(); 762 }, scope.options.delay); 763 } else { 764 scope.generateSource(); 765 } 766 break; 767 case "search": 768 scope.searchResult(); 769 scope.buildLayout(); 770 771 if (scope.result.length || 772 (scope.searchGroups.length && 773 scope.displayEmptyTemplate) 774 ) { 775 scope.showLayout(); 776 } else { 777 scope.hideLayout(); 778 } 779 780 scope.deferred && scope.deferred.resolve(); 781 break; 782 } 783 784 return scope.deferred && scope.deferred.promise(); 785 }); 786 787 if (this.options.generateOnLoad) { 788 this.node.trigger("generate" + this.namespace); 789 } 790 }, 791 792 assignQuery: function () { 793 if (this.isContentEditable) { 794 this.rawQuery = this.node.text(); 795 } else { 796 this.rawQuery = this.node.val().toString(); 797 } 798 this.rawQuery = this.rawQuery.replace(/^\s+/, ""); 799 800 if (this.rawQuery !== this.query) { 801 this.query = this.rawQuery; 802 } 803 }, 804 805 filterGenerateSource: function () { 806 this.searchGroups = []; 807 this.generateGroups = []; 808 809 if (this.focusOnly && !this.options.multiselect) return; 810 811 for (var group in this.options.source) { 812 if (!this.options.source.hasOwnProperty(group)) continue; 813 if ( 814 this.query.length >= this.options.source[group].minLength && 815 this.query.length <= this.options.source[group].maxLength 816 ) { 817 if ( 818 this.filters.dropdown && 819 this.filters.dropdown.key === 'group' && 820 this.filters.dropdown.value !== group 821 ) { 822 continue; 823 } 824 825 this.searchGroups.push(group); 826 if (!this.options.source[group].dynamic && this.source[group]) { 827 continue; 828 } 829 this.generateGroups.push(group); 830 } 831 } 832 }, 833 834 generateSource: function (generateGroups) { 835 this.filterGenerateSource(); 836 837 this.generatedGroupCount = 0; 838 839 if (Array.isArray(generateGroups) && generateGroups.length) { 840 this.generateGroups = generateGroups; 841 } else if (!this.generateGroups.length) { 842 this.node.trigger("search" + this.namespace); 843 return; 844 } 845 846 this.requestGroups = []; 847 this.options.loadingAnimation && this.container.addClass("loading"); 848 849 if (!this.helper.isEmpty(this.xhr)) { 850 for (var i in this.xhr) { 851 if (!this.xhr.hasOwnProperty(i)) continue; 852 this.xhr[i].abort(); 853 } 854 this.xhr = {}; 855 } 856 857 var scope = this, 858 group, 859 groupData, 860 groupSource, 861 cache, 862 compression, 863 dataInStorage, 864 isValidStorage; 865 866 for (var i = 0, ii = this.generateGroups.length; i < ii; ++i) { 867 group = this.generateGroups[i]; 868 groupSource = this.options.source[group]; 869 cache = groupSource.cache; 870 compression = groupSource.compression; 871 872 if (this.options.asyncResult) { 873 delete this.source[group]; 874 } 875 876 if (cache) { 877 dataInStorage = window[cache].getItem( 878 "TYPEAHEAD_" + this.selector + ":" + group 879 ); 880 if (dataInStorage) { 881 if (compression) { 882 dataInStorage = LZString.decompressFromUTF16(dataInStorage); 883 } 884 885 isValidStorage = false;
886 try { 887 dataInStorage = JSON.parse(dataInStorage + ""); 888 889 if ( 890 dataInStorage.data && 891 dataInStorage.ttl > new Date().getTime() 892 ) { 893 this.populateSource(dataInStorage.data, group); 894 isValidStorage = true; 895 896 // {debug} 897 if (this.options.debug) { 898 _debug.log({ 899 node: this.selector, 900 function: "generateSource()", 901 message: 'Source for group "' + group + '" found in ' + cache 902 }); 903 _debug.print(); 904 } 905 // {/debug} 906 } else { 907 window[cache].removeItem( 908 "TYPEAHEAD_" + this.selector + ":" + group 909 ); 910 } 911 } catch (error) { 912 } 913 914 if (isValidStorage) continue; 915 } 916 } 917 918 if (groupSource.data && !groupSource.ajax) { 919 // #198 Add support for async data source 920 if (typeof groupSource.data === "function") { 921 groupData = groupSource.data.call(this); 922 if (Array.isArray(groupData)) { 923 scope.populateSource(groupData, group); 924 } else if (typeof groupData.promise === "function") { 925 (function (group) { 926 $.when(groupData).then(function (deferredData) { 927 if (deferredData && Array.isArray(deferredData)) { 928 scope.populateSource(deferredData, group); 929 } 930 }); 931 })(group); 932 } 933 } else { 934 this.populateSource($.extend(true, [], groupSource.data), group); 935 } 936 continue; 937 } 938 939 if (groupSource.ajax) { 940 if (!this.requests[group]) { 941 this.requests[group] = this.generateRequestObject(group); 942 } 943 this.requestGroups.push(group); 944 } 945 } 946 947 if (this.requestGroups.length) { 948 this.handleRequests(); 949 } 950 951 if (this.options.asyncResult && this.searchGroups.length !== this.generateGroups) { 952 this.node.trigger("search" + this.namespace); 953 } 954 955 return !!this.generateGroups.length; 956 }, 957 958 generateRequestObject: function (group) { 959 var scope = this, 960 groupSource = this.options.source[group]; 961 962 var xhrObject = { 963 request: { 964 url: groupSource.ajax.url || null, 965 dataType: "json", 966 beforeSend: function (jqXHR, options) { 967 // Important to call .abort() in case of dynamic requests 968 scope.xhr[group] = jqXHR; 969 970 var beforeSend = 971 scope.requests[group].callback.beforeSend || 972 groupSource.ajax.beforeSend; 973 typeof beforeSend === "function" && 974 beforeSend.apply(null, arguments); 975 } 976 }, 977 callback: { 978 beforeSend: null, 979 done: null, 980 fail: null, 981 then: null, 982 always: null 983 }, 984 extra: { 985 path: groupSource.ajax.path || null, 986 group: group 987 }, 988 validForGroup: [group] 989 }; 990 991 if (typeof groupSource.ajax !== "function") { 992 if (groupSource.ajax instanceof Object) { 993 xhrObject = this.extendXhrObject(xhrObject, groupSource.ajax); 994 } 995 996 if (Object.keys(this.options.source).length > 1) { 997 for (var _group in this.requests) { 998 if (!this.requests.hasOwnProperty(_group)) continue; 999 if (this.requests[_group].isDuplicated) continue; 1000 1001 if ( 1002 xhrObject.request.url && 1003 xhrObject.request.url === this.requests[_group].request.url 1004 ) { 1005 this.requests[_group].validForGroup.push(group); 1006 xhrObject.isDuplicated = true; 1007 delete xhrObject.validForGroup; 1008 } 1009 } 1010 } 1011 } 1012 1013 return xhrObject; 1014 }, 1015 1016 extendXhrObject: function (xhrObject, groupRequest) { 1017 if (typeof groupRequest.callback === "object") { 1018 xhrObject.callback = groupRequest.callback; 1019 delete groupRequest.callback; 1020 } 1021 1022 // #132 Fixed beforeSend when using a function as the request object 1023 if (typeof groupRequest.beforeSend === "function") { 1024 xhrObject.callback.beforeSend = groupRequest.beforeSend; 1025 delete groupRequest.beforeSend; 1026 } 1027 1028 // Fixes #105 Allow user to define their beforeSend function. 1029 // Fixes #181 IE8 incompatibility 1030 xhrObject.request = $.extend(true, xhrObject.request, groupRequest); 1031 1032 // JSONP needs a unique jsonpCallback to run concurrently 1033 if ( 1034 xhrObject.request.dataType.toLowerCase() === "jsonp" && !xhrObject.request.jsonpCallback 1035 ) { 1036 xhrObject.request.jsonpCallback = "callback_" + xhrObject.extra.group; 1037 } 1038 1039 return xhrObject; 1040 }, 1041 1042 handleRequests: function () { 1043 var scope = this, 1044 group, 1045 requestsCount = this.requestGroups.length; 1046 1047 if ( 1048 this.helper.executeCallback.call( 1049 this, 1050 this.options.callback.onSendRequest, 1051 [this.node, this.query] 1052 ) === false 1053 ) { 1054 return; 1055 } 1056 1057 for (var i = 0, ii = this.requestGroups.length; i < ii; ++i) { 1058 group = this.requestGroups[i]; 1059 if (this.requests[group].isDuplicated) continue; 1060 1061 (function (group, xhrObject) { 1062 if (typeof scope.options.source[group].ajax === "function") { 1063 var _groupRequest = scope.options.source[group].ajax.call( 1064 scope, 1065 scope.query 1066 ); 1067 1068 // Fixes #271 Data is cached inside the xhrObject 1069 xhrObject = scope.extendXhrObject( 1070 scope.generateRequestObject(group), 1071 typeof _groupRequest === "object" ? _groupRequest : {} 1072 ); 1073 1074 if ( 1075 typeof xhrObject.request !== "object" || !xhrObject.request.url 1076 ) { 1077 // {debug} 1078 if (scope.options.debug) { 1079 _debug.log({ 1080 node: scope.selector, 1081 function: "handleRequests", 1082 message: 'Source function must return an object c
1082ontaining ".url" key for group "' + 1083 group + 1084 '"' 1085 }); 1086 _debug.print(); 1087 } 1088 // {/debug} 1089 scope.populateSource([], group); 1090 return; 1091 } 1092 scope.requests[group] = xhrObject; 1093 } 1094 1095 var _request, 1096 _isExtended = false, // Prevent the main request from being changed 1097 _groupData = {}; 1098 1099 if (~xhrObject.request.url.indexOf("{{query}}")) { 1100 if (!_isExtended) { 1101 xhrObject = $.extend(true, {}, xhrObject); 1102 _isExtended = true; 1103 } 1104 // #184 Invalid encoded characters on dynamic requests for `{{query}}` 1105 xhrObject.request.url = xhrObject.request.url.replace( 1106 "{{query}}", 1107 encodeURIComponent(scope.query) 1108 ); 1109 } 1110 1111 if (xhrObject.request.data) { 1112 for (var i in xhrObject.request.data) { 1113 if (!xhrObject.request.data.hasOwnProperty(i)) continue; 1114 if (~String(xhrObject.request.data[i]).indexOf("{{query}}")) { 1115 if (!_isExtended) { 1116 xhrObject = $.extend(true, {}, xhrObject); 1117 _isExtended = true; 1118 } 1119 // jQuery handles encodeURIComponent when the query is inside the data object 1120 xhrObject.request.data[i] = xhrObject.request.data[i].replace( 1121 "{{query}}", 1122 scope.query 1123 ); 1124 break; 1125 } 1126 } 1127 } 1128 1129 $.ajax(xhrObject.request) 1130 .done(function (data, textStatus, jqXHR) { 1131 var _group; 1132 1133 for ( 1134 var i = 0, ii = xhrObject.validForGroup.length; 1135 i < ii; 1136 i++ 1137 ) { 1138 _group = xhrObject.validForGroup[i]; 1139 _request = scope.requests[_group]; 1140 1141 if (typeof _request.callback.done === 'function') { 1142 _groupData[_group] = _request.callback.done.call( 1143 scope, 1144 data, 1145 textStatus, 1146 jqXHR 1147 ); 1148 1149 // {debug} 1150 if ( 1151 !Array.isArray(_groupData[_group]) || 1152 typeof _groupData[_group] !== "object" 1153 ) { 1154 if (scope.options.debug) { 1155 _debug.log({ 1156 node: scope.selector, 1157 function: "Ajax.callback.done()", 1158 message: "Invalid returned data has to be an Array" 1159 }); 1160 _debug.print(); 1161 } 1162 } 1163 // {/debug} 1164 } 1165 } 1166 }) 1167 .fail(function (jqXHR, textStatus, errorThrown) { 1168 for ( 1169 var i = 0, ii = xhrObject.validForGroup.length; 1170 i < ii; 1171 i++ 1172 ) { 1173 _request = scope.requests[xhrObject.validForGroup[i]]; 1174 _request.callback.fail instanceof Function && 1175 _request.callback.fail.call( 1176 scope, 1177 jqXHR, 1178 textStatus, 1179 errorThrown 1180 ); 1181 } 1182 1183 // {debug} 1184 if (scope.options.debug) { 1185 _debug.log({ 1186 node: scope.selector, 1187 function: "Ajax.callback.fail()", 1188 arguments: JSON.stringify(xhrObject.request), 1189 message: textStatus 1190 }); 1191 1192 console.log(errorThrown); 1193 1194 _debug.print(); 1195 } 1196 // {/debug} 1197 }) 1198 .always(function (data, textStatus, jqXHR) { 1199 var _group; 1200 for ( 1201 var i = 0, ii = xhrObject.validForGroup.length; 1202 i < ii; 1203 i++ 1204 ) { 1205 _group = xhrObject.validForGroup[i]; 1206 _request = scope.requests[_group]; 1207 _request.callback.always instanceof Function && 1208 _request.callback.always.call(scope, data, textStatus, jqXHR); 1209 1210 // #248, #303 Aborted requests would call populate with invali
1210d data 1211 if (textStatus === 'abort') return; 1212 1213 // #265 Modified data from ajax.callback.done is not being registered (use of _groupData[_group]) 1214 scope.populateSource( 1215 (data !== null && typeof data.promise === "function" && []) || 1216 _groupData[_group] || 1217 data, 1218 _request.extra.group, 1219 _request.extra.path || _request.request.path 1220 ); 1221 1222 requestsCount -= 1; 1223 if (requestsCount === 0) { 1224 scope.helper.executeCallback.call( 1225 scope, 1226 scope.options.callback.onReceiveRequest, 1227 [scope.node, scope.query] 1228 ); 1229 } 1230 } 1231 }) 1232 .then(function (jqXHR, textStatus) { 1233 for ( 1234 var i = 0, ii = xhrObject.validForGroup.length; 1235 i < ii; 1236 i++ 1237 ) { 1238 _request = scope.requests[xhrObject.validForGroup[i]]; 1239 _request.callback.then instanceof Function && 1240 _request.callback.then.call(scope, jqXHR, textStatus); 1241 } 1242 }); 1243 })(group, this.requests[group]); 1244 } 1245 }, 1246 1247 /** 1248 * Build the source groups to be cycled for matched results 1249 * 1250 * @param {Array} data Array of Strings or Array of Objects 1251 * @param {String} group 1252 * @param {String} [path] 1253 * @return {*} 1254 */ 1255 populateSource: function (data, group, path) { 1256 var scope = this, 1257 groupSource = this.options.source[group], 1258 extraData = groupSource.ajax && groupSource.data; 1259 1260 if (path && typeof path === "string") { 1261 data = this.helper.namespace.call(this, path, data); 1262 } 1263 1264 if (typeof data === "undefined") { 1265 // {debug} 1266 if (this.options.debug) { 1267 _debug.log({ 1268 node: this.selector, 1269 function: "populateSource()", 1270 arguments: path, 1271 message: "Invalid data path." 1272 }); 1273 1274 _debug.print(); 1275 } 1276 // {/debug} 1277 } 1278 1279 if (!Array.isArray(data)) { 1280 // {debug} 1281 if (this.options.debug) { 1282 _debug.log({ 1283 node: this.selector, 1284 function: "populateSource()", 1285 arguments: JSON.stringify({group: group}), 1286 message: "Invalid data type, must be Array type." 1287 }); 1288 _debug.print(); 1289 } 1290 // {/debug} 1291 data = []; 1292 } 1293 1294 if (extraData) { 1295 if (typeof extraData === "function") { 1296 extraData = extraData(); 1297 } 1298 1299 if (Array.isArray(extraData)) { 1300 data = data.concat(extraData); 1301 } else { 1302 // {debug} 1303 if (this.options.debug) { 1304 _debug.log({ 1305 node: this.selector, 1306 function: "populateSource()", 1307 arguments: JSON.stringify(extraData), 1308 message: "WARNING - this.options.source." + 1309 group + 1310 ".data Must be an Array or a function that returns an Array." 1311 }); 1312 1313 _debug.print(); 1314 } 1315 // {/debug} 1316 } 1317 } 1318 1319 var tmpObj, 1320 display = groupSource.display 1321 ? groupSource.display[0] === "compiled" 1322 ? groupSource.display[1] 1323 : groupSource.display[0] 1324 : this.options.display[0] === "compiled" 1325 ? this.options.display[1] 1326 : this.options.display[0]; 1327 1328 for (var i = 0, ii = data.length; i < ii; i++) { 1329 if (data[i] === null || typeof data[i] === "boolean") { 1330 // {debug} 1331 if (this.options.debug) { 1332 _debug.log({ 1333 node: this.selector, 1334 function: "populateSource()", 1335 message: "WARNING - NULL/BOOLEAN value inside " + 1336 group + 1337 "! The data was skipped." 1338 }); 1339 1340 _debug.print(); 1341 } 1342 // {/debug} 1343 continue; 1344 } 1345 if (typeof data[i] === "string") { 1346 tmpObj = {}; 1347 tmpObj[display] = data[i]; 1348 data[i] = tmpObj; 1349 } 1350 data[i].group = group; 1351 } 1352 1353 if (!this.hasDynamicGroups && this.dropdownFilter.dynamic.length) { 1354 var key, 1355 value, 1356 tmpValues = {}; 1357 1358 for (var i = 0, ii = data.length; i < ii; i++) { 1359 for ( 1360 var k = 0, kk = this.dropdownFilter.dynamic.length; 1361 k < kk; 1362 k++ 1363 ) { 1364 key = this.dropdownFilter.dynamic[k].key; 1365 1366 value = data[i][key]; 1367 if (!value) continue; 1368 if (!this.dropdownFilter.dynamic[k].value) { 1369 this.dropdownFilter.dynamic[k].value = []; 1370 } 1371 if (!tmpValues[key]) { 1372 tmpValues[key] = []; 1373 } 1374 if (!~tmpValues[key].indexOf(value.toLowerCase())) { 1375 tmpValues[key].push(value.toLowerCase()); 1376 this.dropdownFilter.dynamic[k].value.push(value); 1377 } 1378 } 1379 } 1380 } 1381 1382 if (this.options.correlativeTemplate) { 1383 var template = groupSource.template || this.options.template, 1384 compiledTemplate = ""; 1385 1386 if (typeof template === "function") { 1387 template = template.call(this, "", {}); 1388 } 1389 1390 if (!template) { 1391 // {debug} 1392 if (this.options.debug) { 1393 _debug.log({ 1394 node: this.selector, 1395 function: "populateSource()", 1396 arguments: String(group), 1397 message: "WARNING - this.options.correlativeTemplate is enabled but no template was found." 1398 }); 1399 1400 _debug.print(); 1401 } 1402 // {/debug} 1403 } else { 1404 // #109 correlativeTemplate can be an array of display keys instead of the complete template 1405 if (Array.isArray(this.options.correlativeTemplate)) { 1406 for ( 1407 var i = 0, ii = this.options.correlativeTemplate.length; 1408 i < ii; 1409 i++ 1410 ) { 1411 compiledTemplate += 1412 "{{" + this.options.correlativeTemplate[i] + "}} "; 1413 } 1414 } else {
1415 // Strip down the html tags, #351 if the template needs "<>" use html entities instead <{{email}}> 1416 compiledTemplate = template 1417 .replace(/<.+?>/g, " ") 1418 .replace(/\s{2,}/, " ") 1419 .trim(); 1420 } 1421 1422 for (var i = 0, ii = data.length; i < ii; i++) { 1423 // Fix #351, convert htmlEntities from the template string 1424 data[i].compiled = $("<textarea />") 1425 .html( 1426 compiledTemplate 1427 .replace(/\{\{([\w\-\.]+)(?:\|(\w+))?}}/g, function (match, 1428 index) { 1429 return scope.helper.namespace.call( 1430 scope, 1431 index, 1432 data[i], 1433 "get", 1434 "" 1435 ); 1436 }) 1437 .trim() 1438 ) 1439 .text(); 1440 } 1441 1442 if (groupSource.display) { 1443 if (!~groupSource.display.indexOf("compiled")) { 1444 groupSource.display.unshift("compiled"); 1445 } 1446 } else if (!~this.options.display.indexOf("compiled")) { 1447 this.options.display.unshift("compiled"); 1448 } 1449 } 1450 } 1451 1452 if (this.options.callback.onPopulateSource) { 1453 data = this.helper.executeCallback.call( 1454 this, 1455 this.options.callback.onPopulateSource, 1456 [this.node, data, group, path] 1457 ); 1458 1459 // {debug} 1460 if (this.options.debug) { 1461 if (!data || !Array.isArray(data)) { 1462 _debug.log({ 1463 node: this.selector, 1464 function: "callback.populateSource()", 1465 message: 'callback.onPopulateSource must return the "data" parameter' 1466 }); 1467 1468 _debug.print(); 1469 } 1470 } 1471 // {/debug} 1472 } 1473 1474 // Save the data inside tmpSource to re-order once every requests are completed 1475 this.tmpSource[group] = (Array.isArray(data) && data) || []; 1476 1477 var cache = this.options.source[group].cache, 1478 compression = this.options.source[group].compression, 1479 ttl = this.options.source[group].ttl || this.options.ttl; 1480 1481 if ( 1482 cache && !window[cache].getItem("TYPEAHEAD_" + this.selector + ":" + group) 1483 ) { 1484 if (this.options.callback.onCacheSave) { 1485 data = this.helper.executeCallback.call( 1486 this, 1487 this.options.callback.onCacheSave, 1488 [this.node, data, group, path] 1489 ); 1490 1491 // {debug} 1492 if (this.options.debug) { 1493 if (!data || !Array.isArray(data)) { 1494 _debug.log({ 1495 node: this.selector, 1496 function: "callback.populateSource()", 1497 message: 'callback.onCacheSave must return the "data" parameter' 1498 }); 1499 1500 _debug.print(); 1501 } 1502 } 1503 // {/debug} 1504 } 1505 1506 var storage = JSON.stringify({ 1507 data: data, 1508 ttl: new Date().getTime() + ttl 1509 }); 1510 1511 if (compression) { 1512 storage = LZString.compressToUTF16(storage); 1513 } 1514 1515 window[cache].setItem( 1516 "TYPEAHEAD_" + this.selector + ":" + group, 1517 storage 1518 ); 1519 } 1520 1521 this.incrementGeneratedGroup(group); 1522 }, 1523 1524 incrementGeneratedGroup: function (group) { 1525 this.generatedGroupCount++; 1526 if (this.generatedGroupCount !== this.generateGroups.length && !this.options.asyncResult) { 1527 return; 1528 } 1529 1530 if (this.xhr && this.xhr[group]) { 1531 delete this.xhr[group]; 1532 } 1533 1534 for (var i = 0, ii = this.generateGroups.length; i < ii; i++) { 1535 this.source[this.generateGroups[i]] = this.tmpSource[ 1536 this.generateGroups[i] 1537 ]; 1538 } 1539 1540 if (!this.hasDynamicGroups) { 1541 this.buildDropdownItemLayout("dynamic"); 1542 } 1543 1544 if (this.generatedGroupCount === this.generateGroups.length) { 1545 this.xhr = {}; 1546 this.options.loadingAnimation && this.container.removeClass("loading"); 1547 } 1548 this.node.trigger("search" + this.namespace); 1549 }, 1550 1551 /** 1552 * Key Navigation 1553 * tab 9: if option is enabled, blur Typeahead 1554 * Up 38: select previous item, skip "group" item 1555 * Down 40: select next item, skip "group" item 1556 * Right 39: change charAt, if last char fill hint (if options is true) 1557 * Esc 27: clears input (is not empty) / blur (if empty) 1558 * Enter 13: Select item + submit search 1559 * 1560 * @param {Object} e Event object 1561 * @returns {*} 1562 */ 1563 navigate: function (e) { 1564 this.helper.executeCallback.call( 1565 this, 1566 this.options.callback.onNavigateBefore, 1567 [this.node, this.query, e] 1568 ); 1569 1570 if (e.keyCode === 27) { 1571 // #166 Different browsers do not have the same behaviors by default, lets enforce what we want instead 1572 e.preventDefault(); 1573 if (this.query.length) { 1574 this.resetInput(); 1575 this.node.trigger("input" + this.namespace, [e]); 1576 } else { 1577 this.node.blur(); 1578 this.hideLayout(); 1579 } 1580 return; 1581 } 1582 1583 if (!this.result.length) return; 1584 1585 var itemList = this.resultContainer 1586 .find("." + this.options.selector.item) 1587 .not("[disabled]"), 1588 activeItem = itemList.filter(".active"), 1589 activeItemIndex = activeItem[0] ? itemList.index(activeItem) : null, 1590 activeDataIndex = activeItem[0] ? activeItem.attr("data-index") : null, 1591 newActiveItemIndex = null, 1592 newActiveDataIndex = null; 1593 1594 this.clearActiveItem(); 1595 1596 this.helper.executeCallback.call(this, this.options.callback.onLeave, [ 1597 this.node, 1598 (activeItemIndex !== null && itemList.eq(activeItemIndex)) || undefined, 1599 (activeDataIndex !== null && this.result[activeDataIndex]) || undefined, 1600 e 1601 ]); 1602 1603 if (e.keyCode === 13) { 1604 // Chrome needs preventDefault else the input search event is triggered 1605 e.preventDefault(); 1606 if (activeItem.length > 0) { 1607 // #311 When href is defined and "enter" is pressed, it needs to act as a "clicked" link 1608 if (activeItem.find("a:first")[0].href === "javascript:;") { 1609 activeItem.find("a:first").trigger("click", e); 1610 } else { 1611 activeItem.find("a:first")[0].click(); 1612 } 1613 } else { 1614 this.node 1615 .closest("form") 1616 .trigger("submit"); 1617 } 1618 return; 1619 } 1620 1621 if (e.keyCode === 39) { 1622 if (activeItemIndex !== null) { 1623 itemList 1624 .eq(activeItemIndex) 1625 .find("a:first")[0] 1626 .click(); 1627 } else if ( 1628 this.options.hint && 1629 this.hint.container.val() !== "" && 1630 this.helper.getCaret(this.node[0]) >= this.query.length 1631 ) { 1632 itemList 1633 .filter('[data-index="' + this.hintIndex + '"]') 1634 .find("a:first")[0] 1635 .click(); 1636 } 1637 return; 1638 } 1639 1640 // #284 Blur Typeahead when "Tab" key is pressed 1641 // #326 Improve Up / Down / Tab navigation to have only 1 "selected" item 1642 if (e.keyCode === 9) { 1643 if (this.options.blurOnTab) { 1644 this.hideLayout(); 1645 } else { 1646 if (activeItem.length > 0) { 1647 if (activeItemIndex + 1 < itemList.length) { 1648 e.preventDefault(); 1649 newActiveItemIndex = activeItemIndex + 1; 1650 this.addActiveItem(itemList.eq(newActiveItemIndex)); 1651 } else { 1652 this.hideLayout(); 1653 } 1654 } else { 1655 if (itemList.length) { 1656 e.preventDefault(); 1657 newActiveItemIndex = 0; 1658 this.addActiveItem(itemList.first()); 1659 } else { 1660 this.hideLayout(); 1661 } 1662 } 1663 } 1664 } else if (e.keyCode === 38) { 1665 e.preventDefault(); 1666 1667 if (activeItem.length > 0) { 1668 if (activeItemIndex - 1 >= 0) { 1669 newActiveItemIndex = activeItemIndex - 1; 1670 this.addActiveItem(itemList.eq(newActiveItemIndex)); 1671 } 1672 } else if (itemList.length) { 1673 newActiveItemIndex = itemList.length - 1; 1674 this.addActiveItem(itemList.last()); 1675 } 1676 } else if (e.keyCode === 40) { 1677 e.preventDefault(); 1678 1679 if (activeItem.length > 0) { 1680 if (activeItemIndex + 1 < itemList.length) { 1681 newActiveItemIndex = activeItemIndex + 1; 1682 this.addActiveItem(itemList.eq(newActiveItemIndex)); 1683 } 1684 } else if (itemList.length) { 1685 newActiveItemIndex = 0; 1686 this.addActiveItem(itemList.first()); 1687 } 1688 } 1689 1690 newActiveDataIndex = 1691 newActiveItemIndex !== null 1692 ? itemList.eq(newActiveItemIndex).attr("data-index") 1693 : null; 1694 1695 this.helper.executeCallback.call(this, this.options.callback.onEnter, [ 1696 this.node, 1697 (newActiveItemIndex !== null && itemList.eq(newActiveItemIndex)) || 1698 undefined, 1699 (newActiveDataIndex !== null && this.result[newActiveDataIndex]) || 1700 undefined, 1701 e 1702 ]); 1703 1704 // #115 Prevent the input from changing when navigating (arrow up / down) the results 1705 if (e.preventInputChange && ~[38, 40].indexOf(e.keyCode)) { 1706 this.buildHintLayout( 1707 newActiveDataIndex !== null && newActiveDataIndex < this.result.length 1708 ? [this.result[newActiveDataIndex]] 1709 : null 1710 ); 1711 } 1712 1713 if (this.options.hint && this.hint.container) { 1714 this.hint.container.css( 1715 "color", 1716 e.preventInputChange 1717 ? this.hint.css.color 1718 : (newActiveDataIndex === null && this.hint.css.color) || 1719 this.hint.container.css("background-color") || 1720 "fff" 1721 ); 1722 } 1723 1724 var nodeValue = 1725 newActiveDataIndex === null || e.preventInputChange 1726 ? this.rawQuery 1727 : this.getTemplateValue.call(this, this.result[newActiveDataIndex]); 1728 1729 this.node.val(nodeValue); 1730 if (this.isContentEditable) { 1731 this.node.text(nodeValue); 1732 } 1733 1734 this.helper.executeCallback.call( 1735 this, 1736 this.options.callback.onNavigateAfter, 1737 [ 1738 this.node, 1739 itemList, 1740 (newActiveItemIndex !== null && 1741 itemList.eq(newActiveItemIndex).find("a:first")) || 1742 undefined, 1743 (newActiveDataIndex !== null && this.result[newActiveDataIndex]) || 1744 undefined, 1745 this.query, 1746 e 1747 ] 1748 ); 1749 }, 1750 1751 getTemplateValue: function (item) { 1752 if (!item) return; 1753 var templateValue = 1754 (item.group && this.options.source[item.group].templateValue) || 1755 this.options.templateValue; 1756 if (typeof templateValue === "function") { 1757 templateValue = templateValue.call(this); 1758 } 1759 if (!templateValue) { 1760 return this.helper.namespace 1761 .call(this, item.matchedKey, item) 1762 .toString(); 1763 } 1764 var scope = this; 1765 1766 return templateValue.replace(/\{\{([\w\-.]+)}}/gi, function (match, index) { 1767 return scope.helper.namespace.call(scope, index, item, "get", ""); 1768 }); 1769 }, 1770 1771 clearActiveItem: function () { 1772 this.resultContainer 1773 .find("." + this.options.selector.item) 1774 .removeClass("active"); 1775 }, 1776 1777 addActiveItem: function (item) { 1778 item.addClass("active"); 1779 }, 1780 1781 searchResult: function () { 1782 this.resetLayout(); 1783 1784 if ( 1785 this.helper.executeCallback.call(this, this.options.callback.onSearch, [ 1786 this.node, 1787 this.query 1788 ]) === false 1789 ) return; 1790 1791 if ( 1792 this.searchGroups.length && !( 1793 this.options.multiselect && 1794 this.options.multiselect.limit && 1795 this.items.length >= this.options.multiselect.limit 1796 ) 1797 ) { 1798 this.searchResultData(); 1799 } 1800 1801 this.helper.executeCallback.call(this, this.options.callback.onResult, [ 1802 this.node, 1803 this.query, 1804 this.result, 1805 this.resultCount, 1806 this.resultCountPerGroup 1807 ]); 1808 1809 if (this.isDropdownEvent) { 1810 this.helper.executeCallback.call( 1811 this, 1812 this.options.callback.onDropdownFilter, 1813 [this.node, this.query, this.filters.dropdown, this.result] 1814 ); 1815 this.isDropdownEvent = false;
1816 } 1817 }, 1818 1819 searchResultData: function () { 1820 var scope = this, 1821 group, 1822 groupBy = this.groupBy, 1823 groupReference = null, 1824 item, 1825 match, 1826 comparedDisplay, 1827 comparedQuery = this.query.toLowerCase(), 1828 maxItem = this.options.maxItem, 1829 maxItemPerGroup = this.options.maxItemPerGroup, 1830 hasDynamicFilters = 1831 this.filters.dynamic && !this.helper.isEmpty(this.filters.dynamic), 1832 displayKeys, 1833 displayValue, 1834 missingDisplayKey = {}, 1835 groupFilter, 1836 groupFilterResult, 1837 groupMatcher, 1838 groupMatcherResult, 1839 matcher = 1840 typeof this.options.matcher === "function" && this.options.matcher, 1841 correlativeMatch, 1842 correlativeQuery, 1843 correlativeDisplay; 1844 1845 if (this.options.accent) { 1846 comparedQuery = this.helper.removeAccent.call(this, comparedQuery); 1847 } 1848 1849 for (var i = 0, ii = this.searchGroups.length; i < ii; ++i) { 1850 group = this.searchGroups[i]; 1851 1852 if ( 1853 this.filters.dropdown && 1854 this.filters.dropdown.key === "group" && 1855 this.filters.dropdown.value !== group 1856 ) 1857 continue; 1858 1859 groupFilter = 1860 typeof this.options.source[group].filter !== "undefined" 1861 ? this.options.source[group].filter 1862 : this.options.filter; 1863 groupMatcher = 1864 (typeof this.options.source[group].matcher === "function" && 1865 this.options.source[group].matcher) || 1866 matcher; 1867 1868 if (!this.source[group]) continue; 1869 1870 for (var k = 0, kk = this.source[group].length; k < kk; k++) { 1871 if (this.resultItemCount >= maxItem && !this.options.callback.onResult) break; 1872 if (hasDynamicFilters && !this.dynamicFilter.validate.apply(this, [this.source[group][k]])) continue; 1873 1874 item = this.source[group][k]; 1875 // Validation over null item 1876 if (item === null || typeof item === "boolean") continue; 1877 if (this.options.multiselect && !this.isMultiselectUniqueData(item)) continue; 1878 1879 // dropdownFilter by custom groups 1880 if ( 1881 this.filters.dropdown && 1882 (item[this.filters.dropdown.key] || "").toLowerCase() !== 1883 (this.filters.dropdown.value || "").toLowerCase() 1884 ) { 1885 continue; 1886 } 1887 1888 groupReference = 1889 groupBy === "group" 1890 ? group 1891 : item[groupBy] ? item[groupBy] : item.group; 1892 1893 if (groupReference && !this.tmpResult[groupReference]) { 1894 this.tmpResult[groupReference] = []; 1895 this.resultCountPerGroup[groupReference] = 0; 1896 } 1897 1898 if (maxItemPerGroup) { 1899 if ( 1900 groupBy === "group" && 1901 this.tmpResult[groupReference].length >= maxItemPerGroup && !this.options.callback.onResult 1902 ) { 1903 break; 1904 } 1905 } 1906 1907 displayKeys = this.options.source[group].display || this.options.display; 1908 for (var v = 0, vv = displayKeys.length; v < vv; ++v) { 1909 // #286 option.filter: false shouldn't bother about the option.display keys 1910 if (groupFilter !== false) { 1911 // #183 Allow searching for deep source object keys 1912 displayValue = /\./.test(displayKeys[v]) 1913 ? this.helper.namespace.call(this, displayKeys[v], item)
1914 : item[displayKeys[v]]; 1915 1916 // #182 Continue looping if empty or undefined key 1917 if (typeof displayValue === "undefined" || displayValue === "") { 1918 // {debug} 1919 if (this.options.debug) { 1920 missingDisplayKey[v] = { 1921 display: displayKeys[v], 1922 data: item 1923 }; 1924 } 1925 // {/debug} 1926 continue; 1927 } 1928 1929 displayValue = this.helper.cleanStringFromScript(displayValue); 1930 } 1931 1932 if (typeof groupFilter === "function") { 1933 groupFilterResult = groupFilter.call(this, item, displayValue); 1934 1935 // return undefined to skip to next item 1936 // return false to attempt the matching function on the next displayKey 1937 // return true to add the item to the result list 1938 // return item object to modify the item and add it to the result list 1939 1940 if (groupFilterResult === undefined) break; 1941 if (!groupFilterResult) continue; 1942 if (typeof groupFilterResult === "object") { 1943 item = groupFilterResult; 1944 } 1945 } 1946 1947 if (~[undefined, true].indexOf(groupFilter)) { 1948 if (displayValue === null) continue; 1949 1950 comparedDisplay = displayValue; 1951 comparedDisplay = comparedDisplay.toString().toLowerCase(); 1952 1953 if (this.options.accent) { 1954 comparedDisplay = this.helper.removeAccent.call( 1955 this, 1956 comparedDisplay 1957 ); 1958 } 1959 1960 match = comparedDisplay.indexOf(comparedQuery); 1961 1962 if ( 1963 this.options.correlativeTemplate && 1964 displayKeys[v] === "compiled" && 1965 match < 0 && 1966 /\s/.test(comparedQuery) 1967 ) { 1968 correlativeMatch = true; 1969 correlativeQuery = comparedQuery.split(" "); 1970 correlativeDisplay = comparedDisplay; 1971 for (var x = 0, xx = correlativeQuery.length; x < xx; x++) { 1972 if (correlativeQuery[x] === "") continue; 1973 if (!~correlativeDisplay.indexOf(correlativeQuery[x])) { 1974 correlativeMatch = false; 1975 break; 1976 } 1977 correlativeDisplay = correlativeDisplay.replace( 1978 correlativeQuery[x], 1979 "" 1980 ); 1981 } 1982 } 1983 1984 if (match < 0 && !correlativeMatch) continue; 1985 if (this.options.offset && match !== 0) continue; 1986 1987 if (groupMatcher) { 1988 groupMatcherResult = groupMatcher.call( 1989 this, 1990 item, 1991 displayValue 1992 ); 1993 1994 // return undefined to skip to next item 1995 // return false to attempt the matching function on the next displayKey 1996 // return true to add the item to the result list 1997 // return item object to modify the item and add it to the result list 1998 1999 if (groupMatcherResult === undefined) break; 2000 if (!groupMatcherResult) continue; 2001 if (typeof groupMatcherResult === "object") { 2002 item = groupMatcherResult; 2003 } 2004 } 2005 } 2006 2007 this.resultCount++; 2008 this.resultCountPerGroup[groupReference]++; 2009 2010 if (this.resultItemCount < maxItem) { 2011 if ( 2012 maxItemPerGroup && 2013 this.tmpResult[groupReference].length >= maxItemPerGroup 2014 ) { 2015 break; 2016 } 2017 2018 this.tmpResult[groupReference].push( 2019 $.extend(true, {matchedKey: displayKeys[v]}, item) 2020 ); 2021 this.resultItemCount++; 2022 } 2023 break; 2024 } 2025 2026 if (!this.options.callback.onResult) { 2027 if (this.resultItemCount >= maxItem) { 2028 break; 2029 } 2030 if ( 2031 maxItemPerGroup && 2032 this.tmpResult[groupReference].length >= maxItemPerGroup 2033 ) { 2034 if (groupBy === "group") { 2035 break; 2036 } 2037 } 2038 } 2039 } 2040 } 2041 2042 // {debug} 2043 if (this.options.debug) { 2044 if (!this.helper.isEmpty(missingDisplayKey)) { 2045 _debug.log({ 2046 node: this.selector, 2047 function: "searchResult()", 2048 arguments: JSON.stringify(missingDisplayKey), 2049 message: "Missing keys for display, make sure options.display is set properly." 2050 }); 2051 2052 _debug.print(); 2053 } 2054 } 2055 // {/debug} 2056 2057 if (this.options.order) { 2058 var displayKeys = [], 2059 displayKey; 2060 2061 for (var group in this.tmpResult) { 2062 if (!this.tmpResult.hasOwnProperty(group)) continue; 2063 for (var i = 0, ii = this.tmpResult[group].length; i < ii; i++) { 2064 displayKey = 2065 this.options.source[this.tmpResult[group][i].group].display || 2066 this.options.display; 2067 if (!~displayKeys.indexOf(displayKey[0])) { 2068 displayKeys.push(displayKey[0]); 2069 } 2070 } 2071 this.tmpResult[group].sort( 2072 scope.helper.sort( 2073 displayKeys, 2074 scope.options.order === "asc", 2075 function (a) { 2076 if (a) { 2077 return a.toString().toUpperCase(); 2078 } else { 2079 return ""; 2080 } 2081 } 2082 ) 2083 ); 2084 } 2085 } 2086 2087 var concatResults = [], 2088 groupOrder = []; 2089 2090 if (typeof this.options.groupOrder === "function") { 2091 groupOrder = this.options.groupOrder.apply(this, [ 2092 this.node, 2093 this.query, 2094 this.tmpResult, 2095 this.resultCount, 2096 this.resultCountPerGroup 2097 ]); 2098 } else if (Array.isArray(this.options.groupOrder)) { 2099 groupOrder = this.options.groupOrder; 2100 } else if ( 2101 typeof this.options.groupOrder === "string" && ~["asc", "desc"].indexOf(this.options.groupOrder) 2102 ) { 2103 groupOrder = Object.keys(this.tmpResult).sort( 2104 scope.helper.sort([], scope.options.groupOrder === "asc", function (a) { 2105 return a.toString().toUpperCase(); 2106 }) 2107 ); 2108 } else { 2109 groupOrder = Object.keys(this.tmpResult); 2110 } 2111 2112 for (var i = 0, ii = groupOrder.length; i < ii; i++) { 2113 concatResults = concatResults.concat(this.tmpResult[groupOrder[i]] || []); 2114 } 2115 2116 // #286 groupTemplate option was deleting group reference Array 2117 this.groups = JSON.parse(JSON.stringify(groupOrder)); 2118 2119 this.result = concatResults; 2120 }, 2121 2122 buildLayout: function () { 2123 this.buildHtmlLayout(); 2124 2125 this.buildBackdropLayout(); 2126 2127 this.buildHintLayout(); 2128 2129 if (this.options.callback.onLayoutBuiltBefore) { 2130 this.tmpResultHtml = this.helper.executeCallback.call( 2131 this, 2132 this.options.callback.onLayoutBuiltBefore, 2133 [this.node, this.query, this.result, this.resultHtml] 2134 ); 2135 } 2136 2137 if (this.tmpResultHtml instanceof $) { 2138 this.resultContainer.html(this.tmpResultHtml); 2139 } else if (this.resultHtml instanceof $) { 2140 this.resultContainer.html(this.resultHtml); 2141 } 2142 2143 if (this.options.callback.onLayoutBuiltAfter) { 2144 this.helper.executeCallback.call( 2145 this, 2146 this.options.callback.onLayoutBuiltAfter, 2147 [this.node, this.query, this.result] 2148 ); 2149 } 2150 }, 2151 2152 buildHtmlLayout: function () { 2153 // #150 Add the option to have no resultList but still perform the search and trigger the callbacks 2154 if (this.options.resultContainer === false) return; 2155 2156 if (!this.resultContainer) { 2157 this.resultContainer = $("<div/>", { 2158 class: this.options.selector.result 2159 }); 2160 2161 this.container.append(this.resultContainer); 2162 } 2163 2164 var emptyTemplate; 2165 if (!this.result.length && this.generatedGroupCount === this.generateGroups.length) { 2166 if ( 2167 this.options.multiselect && 2168 this.options.multiselect.limit && 2169 this.items.length >= this.options.multiselect.limit 2170 ) { 2171 if (this.options.multiselect.limitTemplate) { 2172 emptyTemplate = 2173 typeof this.options.multiselect.limitTemplate === "function" 2174 ? this.options.multiselect.limitTemplate.call(this, this.query) 2175 : this.options.multiselect.limitTemplate.replace( 2176 /\{\{query}}/gi, 2177 $("<div>") 2178 .text(this.helper.cleanStringFromScript(this.query)) 2179 .html() 2180 ); 2181 } else { 2182 emptyTemplate = 2183 "Can't select more than " + this.items.length + " items."; 2184 } 2185 } else if (this.options.emptyTemplate && this.query !== "") { 2186 emptyTemplate = 2187 typeof this.options.emptyTemplate === "function" 2188 ? this.options.emptyTemplate.call(this, this.query) 2189 : this.options.emptyTemplate.replace( 2190 /\{\{query}}/gi, 2191 $("<div>") 2192 .text(this.helper.cleanStringFromScript(this.query)) 2193 .html() 2194 ); 2195 } else { 2196 return; 2197 } 2198 } 2199 this.displayEmptyTemplate = !!emptyTemplate; 2200 2201 var _query = this.query.toLowerCase(); 2202 if (this.options.accent) { 2203 _query = this.helper.removeAccent.call(this, _query
2203); 2204 } 2205 2206 var scope = this, 2207 groupTemplate = this.groupTemplate || "<ul></ul>", 2208 hasEmptyTemplate = false; 2209 2210 if (this.groupTemplate) { 2211 groupTemplate = $( 2212 groupTemplate.replace( 2213 /<([^>]+)>\{\{(.+?)}}<\/[^>]+>/g, 2214 function (match, tag, group, offset, string) { 2215 var template = "", 2216 groups = group === "group" ? scope.groups : [group]; 2217 2218 if (!scope.result.length) { 2219 if (hasEmptyTemplate === true) return ""; 2220 hasEmptyTemplate = true; 2221 2222 return ( 2223 "<" + tag + ' class="' + scope.options.selector.empty + '">' + emptyTemplate + "</" + tag + ">" 2224 ); 2225 } 2226 2227 for (var i = 0, ii = groups.length; i < ii; ++i) { 2228 template += "<" + tag + ' data-group-template="' + groups[i] + '"><ul></ul></' + tag + ">"; 2229 } 2230 2231 return template; 2232 } 2233 ) 2234 ); 2235 } else { 2236 groupTemplate = $(groupTemplate); 2237 if (!this.result.length) { 2238 groupTemplate.append( 2239 emptyTemplate instanceof $ 2240 ? emptyTemplate 2241 : '<li class="' + 2242 scope.options.selector.empty + 2243 '">' + 2244 emptyTemplate + 2245 "</li>" 2246 ); 2247 } 2248 } 2249 2250 groupTemplate.addClass( 2251 this.options.selector.list + 2252 (this.helper.isEmpty(this.result) ? " empty" : "") 2253 ); 2254 2255 var _group, 2256 _groupTemplate, 2257 _item, 2258 _href, 2259 _liHtml, 2260 _template, 2261 _aHtml, 2262 _display, 2263 _displayKeys, 2264 _displayValue, 2265 _unusedGroups = 2266 (this.groupTemplate && this.result.length && scope.groups) || [], 2267 _tmpIndexOf; 2268 2269 for (var i = 0, ii = this.result.length; i < ii; ++i) { 2270 _item = this.result[i]; 2271 _group = _item.group; 2272 _href = 2273 (!this.options.multiselect && 2274 this.options.source[_item.group].href) || 2275 this.options.href; 2276 _display = []; 2277 _displayKeys = 2278 this.options.source[_item.group].display || this.options.display; 2279 2280 if (this.options.group) { 2281 _group = _item[this.options.group.key]; 2282 if (this.options.group.template) { 2283 if (typeof this.options.group.template === "function") { 2284 _groupTemplate = this.options.group.template.call(this, _item); 2285 } else if (typeof this.options.group.template === "string") { 2286 _groupTemplate = this.options.group.template.replace( 2287 /\{\{([\w\-\.]+)}}/gi, 2288 function (match, index) { 2289 return scope.helper.namespace.call( 2290 scope, 2291 index, 2292 _item, 2293 "get", 2294 "" 2295 ); 2296 } 2297 ); 2298 } 2299 } 2300 2301 if (!groupTemplate.find('[data-search-group="' + _group + '"]')[0]) { 2302 (this.groupTemplate 2303 ? groupTemplate.find('[data-group-template="' + _group + '"] ul') 2304 : groupTemplate).append( 2305 $("<li/>", { 2306 class: scope.options.selector.group, 2307 html: $("<a/>", { 2308 href: "javascript:;", 2309 html: _groupTemplate || _group, 2310 tabindex: -1 2311 }), 2312 "data-search-group": _group 2313 }) 2314 ); 2315 } 2316 } 2317 2318 if (this.groupTemplate && _unusedGroups.length) { 2319 _tmpIndexOf = _unusedGroups.indexOf(_group || _item.group); 2320 if (~_tmpIndexOf) { 2321 _unusedGroups.splice(_tmpIndexOf, 1); 2322 } 2323 } 2324 2325 _liHtml = $("<li/>", { 2326 class: scope.options.selector.item + " " + scope.options.selector.group + "-" + this.helper.slugify.call(this, _group), 2327 disabled: _item.disabled ? true : false, 2328 "data-group": _group, 2329 "data-index": i, 2330 html: $("<a/>", { 2331 href: _href && !_item.disabled 2332 ? (function (href, item) { 2333 return item.href = scope.generateHref.call( 2334 scope, 2335 href, 2336 item 2337 ); 2338 })(_href, _item) 2339 : "javascript:;", 2340 html: function () { 2341 _template = 2342 (_item.group && scope.options.source[_item.group].template) || 2343 scope.options.template; 2344 2345 if (_template) { 2346 if (typeof _template === "function") { 2347 _template = _template.call(scope, scope.query, _item); 2348 } 2349 2350 _aHtml = _template.replace( 2351 /\{\{([^\|}]+)(?:\|([^}]+))*}}/gi, 2352 function (match, index, options) { 2353 var value = scope.helper.cleanStringFromScript( 2354 String( 2355 scope.helper.namespace.call( 2356 scope, 2357 index, 2358 _item, 2359 "get", 2360 "" 2361 ) 2362 ) 2363 ); 2364 2365 // #151 Slugify should be an option, not enforced 2366 options = (options && options.split("|")) || []; 2367 if (~options.indexOf("slugify")) { 2368 value = scope.helper.slugify.call(scope, value); 2369 } 2370 2371 if (!~options.indexOf("raw")) { 2372 if ( 2373 scope.options.highlight === true && 2374 _query && ~_displayKeys.indexOf(index) 2375 ) { 2376 value = scope.helper.highlight.call( 2377 scope, 2378 value, 2379 _query.split(" "), 2380 scope.options.accent
2381 ); 2382 } 2383 } 2384 return value; 2385 } 2386 ); 2387 } else { 2388 for (var i = 0, ii = _displayKeys.length; i < ii; i++) { 2389 _displayValue = /\./.test(_displayKeys[i]) 2390 ? scope.helper.namespace.call( 2391 scope, 2392 _displayKeys[i], 2393 _item, 2394 "get", 2395 "" 2396 ) 2397 : _item[_displayKeys[i]]; 2398 2399 if ( 2400 typeof _displayValue === "undefined" || 2401 _displayValue === "" 2402 ) 2403 continue; 2404 2405 _display.push(_displayValue); 2406 } 2407 2408 _aHtml = 2409 '<span class="' + 2410 scope.options.selector.display + 2411 '">' + 2412 scope.helper.cleanStringFromScript( 2413 String(_display.join(" ")) 2414 ) + 2415 "</span>"; 2416 } 2417 2418 if ( 2419 (scope.options.highlight === true && _query && !_template) || 2420 scope.options.highlight === "any" 2421 ) { 2422 _aHtml = scope.helper.highlight.call( 2423 scope, 2424 _aHtml, 2425 _query.split(" "), 2426 scope.options.accent 2427 ); 2428 } 2429 2430 $(this).append(_aHtml); 2431 } 2432 }) 2433 }); 2434 2435 (function (i, item, liHtml) { 2436 liHtml.on("click", function (e, originalEvent) { 2437 if (item.disabled) { 2438 e.preventDefault(); 2439 return; 2440 } 2441 2442 // #208 - Attach "keyboard Enter" original event 2443 if (originalEvent && typeof originalEvent === "object") { 2444 e.originalEvent = originalEvent; 2445 } 2446 2447 if (scope.options.mustSelectItem && scope.helper.isEmpty(item)) { 2448 e.preventDefault(); 2449 return; 2450 } 2451 2452 if (!scope.options.multiselect) { 2453 scope.item = item; 2454 } 2455 2456 if ( 2457 scope.helper.executeCallback.call( 2458 scope, 2459 scope.options.callback.onClickBefore, 2460 [scope.node, $(this), item, e] 2461 ) === false 2462 ) return; 2463 2464 if ( 2465 (e.originalEvent && e.originalEvent.defaultPrevented) || 2466 e.isDefaultPrevented() 2467 ) return; 2468 2469 if (scope.options.multiselect) { 2470 scope.query = scope.rawQuery = ""; 2471 scope.addMultiselectItemLayout(item); 2472 } else { 2473 scope.focusOnly = true; 2474 scope.query = scope.rawQuery = scope.getTemplateValue.call(scope, item);; 2475 if (scope.isContentEditable) { 2476 scope.node.text(scope.query); 2477 scope.helper.setCaretAtEnd(scope.node[0]); 2478 } 2479 } 2480 2481 scope.hideLayout(); 2482 2483 scope.node 2484 .val(scope.query) 2485 .focus(); 2486 2487 scope.options.cancelButton && 2488 scope.toggleCancelButtonVisibility(); 2489 2490 scope.helper.executeCallback.call( 2491 scope, 2492 scope.options.callback.onClickAfter, 2493 [scope.node, $(this), item, e] 2494 ); 2495 }); 2496 liHtml.on("mouseenter", function (e) { 2497 if (!item.disabled) { 2498 scope.clearActiveItem(); 2499 scope.addActiveItem($(this)); 2500 } 2501 scope.helper.executeCallback.call( 2502 scope, 2503 scope.options.callback.onEnter, 2504 [scope.node, $(this), item, e] 2505 ); 2506 }); 2507 liHtml.on("mouseleave", function (e) { 2508 if (!item.disabled) { 2509 scope.clearActiveItem(); 2510 } 2511 scope.helper.executeCallback.call( 2512 scope, 2513 scope.options.callback.onLeave, 2514 [scope.node, $(this), item, e] 2515 ); 2516 }); 2517 })(i, _item, _liHtml); 2518 2519 (this.groupTemplate 2520 ? groupTemplate.find('[data-group-template="' + _group + '"] ul') 2521 : groupTemplate).append(_liHtml); 2522 } 2523 2524 if (this.result.length && _unusedGroups.length) { 2525 for (var i = 0, ii = _unusedGroups.length; i < ii; ++i) { 2526 groupTemplate 2527 .find('[data-group-template="' + _unusedGroups[i] + '"]') 2528 .remove(); 2529 } 2530 } 2531 2532 this.resultHtml = groupTemplate; 2533 }, 2534 2535 generateHref: function (href, item) { 2536 var scope = this; 2537 2538 if (typeof href === "string") { 2539 href = href.replace( 2540 /\{\{([^\|}]+)(?:\|([^}]+))*}}/gi, 2541 function (match, index, options) { 2542 var value = scope.helper.namespace.call( 2543 scope, 2544 index, 2545 item, 2546 "get", 2547 "" 2548 ); 2549 2550 // #151 Slugify should be an option, not enforced 2551 options = (options && options.split("|")) || []; 2552 if (~options.indexOf("slugify")) { 2553 value = scope.helper.slugify.call(scope, value); 2554 } 2555 2556 return value; 2557 } 2558 ); 2559 } else if (typeof href === "function") { 2560 href = href.call(this, item); 2561 } 2562 2563 return href; 2564 }, 2565 2566 getMultiselectComparedData: function (item) { 2567 var uniqueComparedItem = ""; 2568 if (Array.isArray(this.options.multiselect.matchOn)) { 2569 for ( 2570 var i = 0, ii = this.options.multiselect.matchOn.length; 2571 i < ii; 2572 ++i 2573 ) { 2574 uniqueComparedItem += 2575 typeof item[this.options.multiselect.matchOn[i]] !== "undefined" 2576 ? item[this.options.multiselect.matchOn[i]] 2577 : ""; 2578 } 2579 } else { 2580 var tmpItem = JSON.parse(JSON.stringify(item)), 2581 extraKeys = ["group", "matchedKey", "compiled", "href"]; 2582 2583 for (var i = 0, ii = extraKeys.length; i < ii; ++i) { 2584 delete tmpItem[extraKeys[i]]; 2585 } 2586 uniqueComparedItem = JSON.stringify(tmpItem); 2587 } 2588 return uniqueComparedItem; 2589 }, 2590 2591 buildBackdropLayout: function () { 2592 if (!this.options.backdrop) return; 2593 2594 if (!this.backdrop.container) { 2595 this.backdrop.css = $.extend( 2596 { 2597 opacity: 0.6, 2598 filter: "alpha(opacity=60)", 2599 position: "fixed", 2600 top: 0, 2601 right: 0, 2602 bottom: 0, 2603 left: 0, 2604 "z-index": 1040, 2605 "background-color": "#000" 2606 }, 2607 this.options.backdrop 2608 ); 2609 2610 this.backdrop.container = $("<div/>", { 2611 class: this.options.selector.backdrop, 2612 css: this.backdrop.css 2613 }).insertAfter(this.container); 2614 } 2615 this.container.addClass("backdrop").css({ 2616 "z-index": this.backdrop.css["z-index"] + 1, 2617 position: "relative" 2618 }); 2619 }, 2620 2621 buildHintLayout: function (result) { 2622 if (!this.options.hint) return; 2623 // #144 hint doesn't overlap with the input when the query is too long 2624 if (this.node[0].scrollWidth > Math.ceil(this.node.innerWidth())) { 2625 this.hint.container && this.hint.container.val(""); 2626 return; 2627 } 2628 2629 var scope = this, 2630 hint = "", 2631 result = result || this.result, 2632 query = this.query.toLowerCase(); 2633 2634 if (this.options.accent) { 2635 query = this.helper.removeAccent.call(this, query); 2636 } 2637 2638 this.hintIndex = null; 2639 2640 if (this.searchGroups.length) { 2641 if (!this.hint.container) { 2642 this.hint.css = $.extend( 2643 { 2644 "border-color": "transparent", 2645 position: "absolute", 2646 top: 0, 2647 display: "inline", 2648 "z-index": -1, 2649 float: "none", 2650 color: "silver", 2651 "box-shadow": "none", 2652 cursor: "default", 2653 "-webkit-user-select": "none", 2654 "-moz-user-select": "none", 2655 "-ms-user-select": "none", 2656 "user-select": "none" 2657 }, 2658 this.options.hint 2659 ); 2660 2661 this.hint.container = $("<" + this.node[0].nodeName + "/>", { 2662 type: this.node.attr("type"), 2663 class: this.node.attr("class"), 2664 readonly: true, 2665 unselectable: "on", 2666 "aria-hidden": "true", 2667 tabindex: -1, 2668 click: function () { 2669 // IE8 Fix 2670 scope.node.focus(); 2671 } 2672 }) 2673 .addClass(this.options.selector.hint) 2674 .css(this.hint.css) 2675 .insertAfter(this.node); 2676 2677 this.node.parent().css({ 2678 position: "relative" 2679 }); 2680 } 2681 2682 this.hint.container.css("color", this.hint.css.color); 2683
2684 // Do not display hint for empty query 2685 if (query) { 2686 var _displayKeys, _group, _comparedValue; 2687 2688 for (var i = 0, ii = result.length; i < ii; i++) { 2689 if (result[i].disabled) continue; 2690 2691 _group = result[i].group; 2692 _displayKeys = 2693 this.options.source[_group].display || this.options.display; 2694 2695 for (var k = 0, kk = _displayKeys.length; k < kk; k++) { 2696 _comparedValue = String(result[i][_displayKeys[k]]).toLowerCase(); 2697 if (this.options.accent) { 2698 _comparedValue = this.helper.removeAccent.call( 2699 this, 2700 _comparedValue 2701 ); 2702 } 2703 2704 if (_comparedValue.indexOf(query) === 0) { 2705 hint = String(result[i][_displayKeys[k]]); 2706 this.hintIndex = i; 2707 break; 2708 } 2709 } 2710 if (this.hintIndex !== null) { 2711 break; 2712 } 2713 } 2714 } 2715 2716 var hintValue = 2717 (hint.length > 0 && 2718 this.rawQuery + hint.substring(this.query.length)) || 2719 ""; 2720 this.hint.container.val(hintValue); 2721 2722 if (this.isContentEditable) { 2723 this.hint.container.text(hintValue); 2724 } 2725 } 2726 }, 2727 2728 buildDropdownLayout: function () { 2729 if (!this.options.dropdownFilter) return; 2730 2731 var scope = this; 2732 2733 $("<span/>", { 2734 class: this.options.selector.filter, 2735 html: function () { 2736 $(this).append( 2737 $("<button/>", { 2738 type: "button", 2739 class: scope.options.selector.filterButton, 2740 style: "display: none;", 2741 click: function () { 2742 scope.container.toggleClass("filter"); 2743 2744 var _ns = scope.namespace + "-dropdown-filter"; 2745 2746 $("html").off(_ns); 2747 2748 if (scope.container.hasClass("filter")) { 2749 $("html").on("click" + _ns + " touchend" + _ns, function (e) { 2750 if ( 2751 ($(e.target).closest( 2752 "." + scope.options.selector.filter 2753 )[0] && 2754 $(e.target).closest(scope.container)[0]) || 2755 scope.hasDragged 2756 ) 2757 return; 2758 scope.container.removeClass("filter"); 2759 2760 $("html").off(_ns); 2761 }); 2762 } 2763 } 2764 }) 2765 ); 2766 2767 $(this).append( 2768 $("<ul/>", { 2769 class: scope.options.selector.dropdown 2770 }) 2771 ); 2772 } 2773 }).insertAfter(scope.container.find("." + scope.options.selector.query)); 2774 }, 2775 2776 buildDropdownItemLayout: function (type) { 2777 if (!this.options.dropdownFilter) return; 2778 2779 var scope = this, 2780 template, 2781 all = 2782 (typeof this.options.dropdownFilter === "string" && 2783 this.options.dropdownFilter) || 2784 "All", 2785 ulScope = this.container.find("." + this.options.selector.dropdown), 2786 filter; 2787 2788 // Use regular groups defined in options.source 2789 if ( 2790 type === "static" && 2791 (this.options.dropdownFilter === true || 2792 typeof this.options.dropdownFilter === "string") 2793 ) { 2794 this.dropdownFilter.static.push({ 2795 key: "group", 2796 template: "{{group}}", 2797 all: all, 2798 value: Object.keys(this.options.source) 2799 }); 2800 } 2801 2802 for (var i = 0, ii = this.dropdownFilter[type].length; i < ii; i++) { 2803 filter = this.dropdownFilter[type][i]; 2804 2805 if (!Array.isArray(filter.value)) { 2806 filter.value = [filter.value]; 2807 } 2808 2809 if (filter.all) { 2810 this.dropdownFilterAll = filter.all; 2811 } 2812 2813 for (var k = 0, kk = filter.value.length; k <= kk; k++) { 2814 // Only add "all" at the last filter iteration 2815 if (k === kk && i !== ii - 1) { 2816 continue; 2817 } else if (k === kk && i === ii - 1) { 2818 if (type === "static" && this.dropdownFilter.dynamic.length) { 2819 continue; 2820 } 2821 } 2822 2823 template = this.dropdownFilterAll || all; 2824 if (filter.value[k]) { 2825 if (filter.template) { 2826 template = filter.template.replace( 2827 new RegExp("{{" + filter.key + "}}", "gi"), 2828 filter.value[k] 2829 ); 2830 } else { 2831 template = filter.value[k]; 2832 } 2833 } else { 2834 this.container 2835 .find("." + scope.options.selector.filterButton) 2836 .html(template); 2837 } 2838 2839 (function (k, filter, template) { 2840 ulScope.append( 2841 $("<li/>", { 2842 class: scope.options.selector.dropdownItem + 2843 " " + 2844 scope.helper.slugify.call( 2845 scope, 2846 filter.key + "-" + (filter.value[k] || all) 2847 ), 2848 html: $("<a/>", { 2849 href: "javascript:;", 2850 html: template, 2851 click: function (e) { 2852 e.preventDefault(); 2853 _selectFilter.call(scope, { 2854 key: filter.key, 2855 value: filter.value[k] || "*", 2856 template: template 2857 }); 2858 } 2859 }) 2860 }) 2861 ); 2862 })(k, filter, template); 2863 } 2864 } 2865 2866 if (this.dropdownFilter[type].length) { 2867 this.container 2868 .find("." + scope.options.selector.filterButton) 2869 .removeAttr("style"); 2870 } 2871 2872 /** 2873 * @private 2874 * Select the filter and rebuild the result group 2875 * 2876 * @param {object} item 2877 */ 2878 function _selectFilter(item) { 2879 if (item.value === "*") { 2880 delete this.filters.dropdown; 2881 } else { 2882 this.filters.dropdown = item; 2883 } 2884 2885 this.container 2886 .removeClass("filter") 2887 .find("." + this.options.selector.filterButton) 2888 .html(item.template); 2889 2890 this.isDropdownEvent = true; 2891 this.node.trigger("input" + this.namespace); 2892 2893 if (this.options.multiselect) { 2894 this.adjustInputSize(); 2895 } 2896 2897 this.node.focus(); 2898 } 2899 }, 2900 2901 dynamicFilter: { 2902 isEnabled: false, 2903 init: function () { 2904 if (!this.options.dynamicFilter) return; 2905
2906 this.dynamicFilter.bind.call(this); 2907 this.dynamicFilter.isEnabled = true; 2908 }, 2909 2910 validate: function (item) { 2911 var isValid, 2912 softValid = null, 2913 hardValid = null, 2914 itemValue; 2915 2916 for (var key in this.filters.dynamic) { 2917 if (!this.filters.dynamic.hasOwnProperty(key)) continue; 2918 if (!!~key.indexOf(".")) { 2919 itemValue = this.helper.namespace.call(this, key, item, "get"); 2920 } else { 2921 itemValue = item[key]; 2922 } 2923 2924 if (this.filters.dynamic[key].modifier === "|" && !softValid) { 2925 softValid = itemValue == this.filters.dynamic[key].value || false; 2926 } 2927 2928 if (this.filters.dynamic[key].modifier === "&") { 2929 // Leaving "==" in case of comparing number with string 2930 if (itemValue == this.filters.dynamic[key].value) { 2931 hardValid = true; 2932 } else { 2933 hardValid = false; 2934 break; 2935 } 2936 } 2937 } 2938 2939 isValid = softValid; 2940 if (hardValid !== null) { 2941 isValid = hardValid; 2942 if (hardValid === true && softValid !== null) { 2943 isValid = softValid; 2944 } 2945 } 2946 2947 return !!isValid; 2948 }, 2949 2950 set: function (key, value) { 2951 var matches = key.match(/^([|&])?(.+)/); 2952 2953 if (!value) { 2954 delete this.filters.dynamic[matches[2]]; 2955 } else { 2956 this.filters.dynamic[matches[2]] = { 2957 modifier: matches[1] || "|", 2958 value: value 2959 }; 2960 } 2961 2962 if (this.dynamicFilter.isEnabled) { 2963 this.generateSource(); 2964 } 2965 }, 2966 bind: function () { 2967 var scope = this, 2968 filter; 2969 2970 for (var i = 0, ii = this.options.dynamicFilter.length; i < ii; i++) { 2971 filter = this.options.dynamicFilter[i]; 2972 2973 if (typeof filter.selector === "string") { 2974 filter.selector = $(filter.selector); 2975 } 2976 2977 if ( 2978 !(filter.selector instanceof $) || !filter.selector[0] || !filter.key 2979 ) { 2980 // {debug} 2981 if (this.options.debug) { 2982 _debug.log({ 2983 node: this.selector, 2984 function: "buildDynamicLayout()", 2985 message: 'Invalid jQuery selector or jQuery Object for "filter.selector" or missing filter.key' 2986 }); 2987 2988 _debug.print(); 2989 } 2990 // {/debug} 2991 continue; 2992 } 2993 2994 (function (filter) { 2995 filter.selector 2996 .off(scope.namespace) 2997 .on("change" + scope.namespace, function () { 2998 scope.dynamicFilter.set.apply(scope, [ 2999 filter.key, 3000 scope.dynamicFilter.getValue(this) 3001 ]); 3002 }) 3003 .trigger("change" + scope.namespace); 3004 })(filter); 3005 } 3006 }, 3007 3008 getValue: function (tag) { 3009 var value; 3010 if (tag.tagName === "SELECT") { 3011 value = tag.value; 3012 } else if (tag.tagName === "INPUT") { 3013 if (tag.type === "checkbox") { 3014 value = 3015 (tag.checked && tag.getAttribute("value")) || tag.checked || null; 3016 } else if (tag.type === "radio" && tag.checked) { 3017 value = tag.value; 3018 } 3019 } 3020 return value; 3021 } 3022 }, 3023 3024 buildMultiselectLayout: function () { 3025 if (!this.options.multiselect) return; 3026 var scope = this; 3027 var multiselectData; 3028 3029 this.label.container = $("<span/>", { 3030 class: this.options.selector.labelContainer, 3031 "data-padding-left": parseFloat(this.node.css("padding-left")) || 0, 3032 "data-padding-right": parseFloat(this.node.css("padding-right")) || 0, 3033 "data-padding-top": parseFloat(this.node.css("padding-top")) || 0, 3034 click: function (e) { 3035 if ($(e.target).hasClass(scope.options.selector.labelContainer)) { 3036 scope.node.focus(); 3037 } 3038 } 3039 }); 3040 3041 this.node 3042 .closest("." + this.options.selector.query) 3043 .prepend(this.label.container); 3044 3045 if (!this.options.multiselect.data) return; 3046 3047 if (Array.isArray(this.options.multiselect.data)) { 3048 this.populateMultiselectData(this.options.multiselect.data); 3049 } else if (typeof this.options.multiselect.data === 'function') { 3050 multiselectData = this.options.multiselect.data.call(this); 3051 if (Array.isArray(multiselectData)) { 3052 this.populateMultiselectData(multiselectData); 3053 } else if (typeof multiselectData.promise === "function") { 3054 $.when(multiselectData).then(function (deferredData) { 3055 if (deferredData && Array.isArray(deferredData)) { 3056 scope.populateMultiselectData(deferredData); 3057 } 3058 }); 3059 } 3060 } 3061 }, 3062 3063 isMultiselectUniqueData: function (data) { 3064 var isUniqueData = true; 3065 for (var x = 0, xx = this.comparedItems.length; x < xx; ++x) { 3066 if ( 3067 this.comparedItems[x] === 3068 this.getMultiselectComparedData(data) 3069 ) { 3070 isUniqueData = false;
3071 break; 3072 } 3073 } 3074 return isUniqueData; 3075 }, 3076 3077 populateMultiselectData: function (data) { 3078 for (var i = 0, ii = data.length; i < ii; ++i) { 3079 this.addMultiselectItemLayout(data[i]); 3080 } 3081 3082 this.node.trigger("search" + this.namespace, { origin: 'populateMultiselectData' }); 3083 }, 3084 3085 addMultiselectItemLayout: function (item) { 3086 if (!this.isMultiselectUniqueData(item)) return; 3087 3088 this.items.push(item); 3089 this.comparedItems.push( 3090 this.getMultiselectComparedData(item) 3091 ); 3092 3093 var templateValue = this.getTemplateValue(item); 3094 3095 var scope = this, 3096 htmlTag = this.options.multiselect.href ? "a" : "span"; 3097 3098 var label = $("<span/>", { 3099 class: this.options.selector.label, 3100 html: $("<" + htmlTag + "/>", { 3101 text: templateValue, 3102 click: function (e) { 3103 var currentLabel = $(this).closest( 3104 "." + scope.options.selector.label 3105 ), 3106 index = scope.label.container 3107 .find("." + scope.options.selector.label) 3108 .index(currentLabel); 3109 3110 scope.options.multiselect.callback && scope.helper.executeCallback.call( 3111 scope, 3112 scope.options.multiselect.callback.onClick, 3113 [scope.node, scope.items[index], e] 3114 ); 3115 }, 3116 href: this.options.multiselect.href 3117 ? (function (item) { 3118 return scope.generateHref.call( 3119 scope, 3120 scope.options.multiselect.href, 3121 item 3122 ); 3123 })(scope.items[scope.items.length - 1]) 3124 : null 3125 }) 3126 }); 3127 3128 label.append( 3129 $("<span/>", { 3130 class: this.options.selector.cancelButton, 3131 html: "Ã", 3132 click: function (e) { 3133 var label = $(this).closest( 3134 "." + scope.options.selector.label 3135 ), 3136 index = scope.label.container 3137 .find("." + scope.options.selector.label) 3138 .index(label); 3139 3140 scope.cancelMultiselectItem(index, label, e); 3141 } 3142 }) 3143 ); 3144 3145 this.label.container.append(label); 3146 this.adjustInputSize(); 3147 3148 return true; 3149 }, 3150 3151 cancelMultiselectItem: function (index, label, e) { 3152 var item = this.items[index]; 3153 3154 label = label 3155 || this.label.container 3156 .find('.' + this.options.selector.label) 3157 .eq(index); 3158 3159 label.remove(); 3160 3161 this.items.splice(index, 1); 3162 this.comparedItems.splice(index, 1); 3163 3164 this.options.multiselect.callback && this.helper.executeCallback.call( 3165 this, 3166 this.options.multiselect.callback.onCancel, 3167 [this.node, item, e] 3168 ); 3169 3170 this.adjustInputSize(); 3171 3172 this.focusOnly = true; 3173 this.node.focus().trigger('input' + this.namespace, { origin: 'cancelMultiselectItem' }); 3174 }, 3175 3176 adjustInputSize: function () { 3177 var nodeWidth = 3178 this.node[0].getBoundingClientRect().width - 3179 (parseFloat(this.label.container.data("padding-right")) || 0) - 3180 (parseFloat(this.label.container.css("padding-left")) || 0); 3181 3182 var labelOuterWidth = 0, 3183 numberOfRows = 0, 3184 currentRowWidth = 0, 3185 isRowAdded = false, 3186 labelOuterHeight = 0; 3187 3188 this.label.container 3189 .find("." + this.options.selector.label) 3190 .filter(function (i, v) { 3191 if (i === 0) { 3192 labelOuterHeight = 3193 $(v)[0].getBoundingClientRect().height + 3194 parseFloat($(v).css("margin-bottom") || 0); 3195 } 3196 3197 // labelOuterWidth = Math.round($(v)[0].getBoundingClientRect().width * 100) / 100 + parseFloat($(v).css('margin-right')); 3198 labelOuterWidth = 3199 $(v)[0].getBoundingClientRect().width + 3200 parseFloat($(v).css("margin-right") || 0); 3201 3202 if ( 3203 currentRowWidth + labelOuterWidth > nodeWidth * 0.7 && !isRowAdded 3204 ) { 3205 numberOfRows++; 3206 isRowAdded = true; 3207 } 3208 3209 if (currentRowWidth + labelOuterWidth < nodeWidth) { 3210 currentRowWidth += labelOuterWidth; 3211 } else { 3212 isRowAdded = false;
3213 currentRowWidth = labelOuterWidth; 3214 } 3215 }); 3216 3217 var paddingLeft = 3218 parseFloat(this.label.container.data("padding-left") || 0) + 3219 (isRowAdded ? 0 : currentRowWidth); 3220 var paddingTop = 3221 numberOfRows * labelOuterHeight + 3222 parseFloat(this.label.container.data("padding-top") || 0); 3223 3224 this.container 3225 .find("." + this.options.selector.query) 3226 .find("input, textarea, [contenteditable], .typeahead__hint") 3227 .css({ 3228 paddingLeft: paddingLeft, 3229 paddingTop: paddingTop 3230 }); 3231 }, 3232 3233 showLayout: function () { 3234 if (this.container.hasClass("result") || 3235 ( 3236 !this.result.length && !this.displayEmptyTemplate && !this.options.backdropOnFocus 3237 ) 3238 ) return; 3239 3240 _addHtmlListeners.call(this); 3241 3242 this.container.addClass( 3243 [ 3244 this.result.length || 3245 (this.searchGroups.length && 3246 this.displayEmptyTemplate) 3247 ? "result " 3248 : "", 3249 this.options.hint && this.searchGroups.length ? "hint" : "", 3250 this.options.backdrop || this.options.backdropOnFocus 3251 ? "backdrop" 3252 : "" 3253 ].join(" ") 3254 ); 3255 3256 this.helper.executeCallback.call( 3257 this, 3258 this.options.callback.onShowLayout, 3259 [this.node, this.query] 3260 ); 3261 3262 function _addHtmlListeners() { 3263 var scope = this; 3264 3265 // If Typeahead is blured by pressing the "Tab" Key, hide the results 3266 $("html") 3267 .off("keydown" + this.namespace) 3268 .on("keydown" + this.namespace, function (e) { 3269 if (!e.keyCode || e.keyCode !== 9) return; 3270 setTimeout(function () { 3271 if ( 3272 !$(":focus") 3273 .closest(scope.container) 3274 .find(scope.node)[0] 3275 ) { 3276 scope.hideLayout(); 3277 } 3278 }, 0); 3279 }); 3280 3281 // If Typeahead is blured by clicking outside, hide the results 3282 $("html") 3283 .off("click" + this.namespace + " touchend" + this.namespace) 3284 .on("click" + this.namespace + " touchend" + this.namespace, function (e) { 3285 if ($(e.target).closest(scope.container)[0] || 3286 $(e.target).closest('.' + scope.options.selector.item)[0] || 3287 e.target.className === scope.options.selector.cancelButton || 3288 scope.hasDragged 3289 ) return; 3290 3291 scope.hideLayout(); 3292 }); 3293 } 3294 }, 3295 3296 hideLayout: function () { 3297 // Means the container is already hidden 3298 if (!this.container.hasClass("result") && !this.container.hasClass("backdrop")) return; 3299 3300 this.container.removeClass( 3301 "result hint filter" + 3302 (this.options.backdropOnFocus && $(this.node).is(":focus") 3303 ? "" 3304 : " backdrop") 3305 ); 3306 3307 if (this.options.backdropOnFocus && this.container.hasClass("backdrop")) 3308 return; 3309 3310 // Make sure the event HTML gets cleared 3311 $("html").off(this.namespace); 3312 3313 this.helper.executeCallback.call( 3314 this, 3315 this.options.callback.onHideLayout, 3316 [this.node, this.query] 3317 ); 3318 }, 3319 3320 resetLayout: function () { 3321 this.result = []; 3322 this.tmpResult = {}; 3323 this.groups = []; 3324 this.resultCount = 0; 3325 this.resultCountPerGroup = {}; 3326 this.resultItemCount = 0; 3327 this.resultHtml = null; 3328 3329 if (this.options.hint && this.hint.container) { 3330 this.hint.container.val(""); 3331 if (this.isContentEditable) { 3332 this.hint.container.text(""); 3333 } 3334 } 3335 }, 3336 3337 resetInput: function () { 3338 this.node.val(""); 3339 if (this.isContentEditable) { 3340 this.node.text(""); 3341 } 3342 this.query = ""; 3343 this.rawQuery = ""; 3344 }, 3345 3346 buildCancelButtonLayout: function () { 3347 if (!this.options.cancelButton) return; 3348 var scope = this; 3349 3350 $("<span/>", { 3351 class: this.options.selector.cancelButton, 3352 html: "Ã", 3353 mousedown: function (e) { 3354 // Don't blur the input 3355 e.stopImmediatePropagation(); 3356 e.preventDefault(); 3357 3358 scope.resetInput(); 3359 scope.node.trigger("input" + scope.namespace, [e]); 3360 } 3361 }).insertBefore(this.node); 3362 }, 3363 3364 toggleCancelButtonVisibility: function () { 3365 this.container.toggleClass("cancel", !!this.query.length); 3366 }, 3367 3368 __construct: function () { 3369 this.extendOptions(); 3370 3371 if (!this.unifySourceFormat()) { 3372 return; 3373 } 3374
3375 this.dynamicFilter.init.apply(this); 3376 3377 this.init(); 3378 this.buildDropdownLayout(); 3379 this.buildDropdownItemLayout("static"); 3380 3381 this.buildMultiselectLayout(); 3382 3383 this.delegateEvents(); 3384 this.buildCancelButtonLayout(); 3385 3386 this.helper.executeCallback.call(this, this.options.callback.onReady, [ 3387 this.node 3388 ]); 3389 }, 3390 3391 helper: { 3392 isEmpty: function (obj) { 3393 for (var prop in obj) { 3394 if (obj.hasOwnProperty(prop)) return false; 3395 } 3396 3397 return true; 3398 }, 3399 3400 /** 3401 * Remove every accent(s) from a string 3402 * 3403 * @param {String} string 3404 * @returns {*} 3405 */ 3406 removeAccent: function (string) { 3407 if (typeof string !== "string") { 3408 return; 3409 } 3410 3411 var accent = _accent; 3412 3413 if (typeof this.options.accent === "object") { 3414 accent = this.options.accent; 3415 } 3416 3417 string = string 3418 .toLowerCase() 3419 .replace(new RegExp("[" + accent.from + "]", "g"), function (match) { 3420 return accent.to[accent.from.indexOf(match)]; 3421 }); 3422 3423 return string; 3424 }, 3425 3426 /** 3427 * Creates a valid url from string 3428 * 3429 * @param {String} string 3430 * @returns {string} 3431 */ 3432 slugify: function (string) { 3433 string = String(string); 3434 3435 if (string !== "") { 3436 string = this.helper.removeAccent.call(this, string); 3437 string = string 3438 .replace(/[^-a-z0-9]+/g, "-") 3439 .replace(/-+/g, "-") 3440 .replace(/^-|-$/g, ""); 3441 } 3442 3443 return string; 3444 }, 3445 3446 /** 3447 * Sort list of object by key 3448 * 3449 * @param {String|Array} field 3450 * @param {Boolean} reverse 3451 * @param {Function} primer 3452 * @returns {Function} 3453 */ 3454 sort: function (field, reverse, primer) { 3455 var key = function (x) { 3456 for (var i = 0, ii = field.length; i < ii; i++) { 3457 if (typeof x[field[i]] !== "undefined") { 3458 return primer(x[field[i]]); 3459 } 3460 } 3461 return x; 3462 }; 3463 3464 reverse = [-1, 1][+!!reverse]; 3465 3466 return function (a, b) { 3467 return (a = key(a)), (b = key(b)), reverse * ((a > b) - (b > a)); 3468 }; 3469 }, 3470 3471 /** 3472 * Replace a string from-to index 3473 * 3474 * @param {String} string The complete string to replace into 3475 * @param {Number} offset The cursor position to start replacing from 3476 * @param {Number} length The length of the replacing string 3477 * @param {String} replace The replacing string 3478 * @returns {String} 3479 */ 3480 replaceAt: function (string, offset, length, replace) { 3481 return ( 3482 string.substring(0, offset) + 3483 replace + 3484 string.substring(offset + length) 3485 ); 3486 }, 3487 3488 /** 3489 * Adds <strong> html around a matched string 3490 * 3491 * @param {String} string The complete string to match from 3492 * @param {String} key 3493 * @param {Boolean} [accents] 3494 * @returns {*} 3495 */ 3496 highlight: function (string, keys, accents) { 3497 string = String(string); 3498 3499 var searchString = 3500 (accents && this.helper.removeAccent.call(this, string)) || string, 3501 matches = []; 3502 3503 if (!Array.isArray(keys)) { 3504 keys = [keys]; 3505 } 3506 3507 keys.sort(function (a, b) { 3508 return b.length - a.length; 3509 }); 3510 3511 // Make sure the '|' join will be safe! 3512 for (var i = keys.length - 1; i >= 0; i--) { 3513 if (keys[i].trim() === "") { 3514 keys.splice(i, 1); 3515 continue; 3516 } 3517 keys[i] = keys[i].replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 3518 } 3519 3520 searchString.replace( 3521 new RegExp("(?:" + keys.join("|") + ")(?!([^<]+)?>)", "gi"), 3522 function (match, index, offset) { 3523 matches.push({ 3524 offset: offset, 3525 length: match.length 3526 }); 3527 } 3528 ); 3529 3530 for (var i = matches.length - 1; i >= 0; i--) {
3531 string = this.helper.replaceAt( 3532 string, 3533 matches[i].offset, 3534 matches[i].length, 3535 "<strong>" + 3536 string.substr(matches[i].offset, matches[i].length) + 3537 "</strong>" 3538 ); 3539 } 3540 3541 return string; 3542 }, 3543 3544 /** 3545 * Get caret position, used for right arrow navigation 3546 * when hint option is enabled 3547 * @param {Node} element 3548 * @returns {Number} Caret position 3549 */ 3550 getCaret: function (element) { 3551 var caretPos = 0; 3552 3553 if (element.selectionStart) { 3554 // Input & Textarea 3555 return element.selectionStart; 3556 } else if (document.selection) { 3557 var r = document.selection.createRange(); 3558 if (r === null) { 3559 return caretPos; 3560 } 3561 3562 var re = element.createTextRange(), 3563 rc = re.duplicate(); 3564 re.moveToBookmark(r.getBookmark()); 3565 rc.setEndPoint("EndToStart", re); 3566 3567 caretPos = rc.text.length; 3568 } else if (window.getSelection) { 3569 // Contenteditable 3570 var sel = window.getSelection(); 3571 if (sel.rangeCount) { 3572 var range = sel.getRangeAt(0); 3573 if (range.commonAncestorContainer.parentNode == element) { 3574 caretPos = range.endOffset; 3575 } 3576 } 3577 } 3578 return caretPos; 3579 }, 3580 3581 /** 3582 * For [contenteditable] typeahead node only, 3583 * when an item is clicked set the cursor at the end 3584 * @param {Node} element 3585 */ 3586 setCaretAtEnd: function (element) { 3587 if ( 3588 typeof window.getSelection !== "undefined" && 3589 typeof document.createRange !== "undefined" 3590 ) { 3591 var range = document.createRange(); 3592 range.selectNodeContents(element); 3593 range.collapse(false); 3594 var sel = window.getSelection(); 3595 sel.removeAllRanges(); 3596 sel.addRange(range); 3597 } else if (typeof document.body.createTextRange !== "undefined") { 3598 var textRange = document.body.createTextRange(); 3599 textRange.moveToElementText(element); 3600 textRange.collapse(false); 3601 textRange.select(); 3602 } 3603 }, 3604 3605 /** 3606 * Clean strings from possible XSS (script and iframe tags) 3607 * @param string 3608 * @returns {string} 3609 */ 3610 cleanStringFromScript: function (string) { 3611 return ( 3612 (typeof string === "string" && 3613 string.replace(/<\/?(?:script|iframe)\b[^>]*>/gm, "")) || 3614 string 3615 ); 3616 }, 3617 3618 /** 3619 * Executes an anonymous function or a string reached from the window scope. 3620 * 3621 * @example 3622 * Note: These examples works with every configuration callbacks 3623 * 3624 * // An anonymous function inside the "onInit" option 3625 * onInit: function() { console.log(':D'); }; 3626 * 3627 * // myFunction() located on window.coucou scope 3628 * onInit: 'window.coucou.myFunction' 3629 * 3630 * // myFunction(a,b) located on window.coucou scope passing 2 parameters 3631 * onInit: ['window.coucou.myFunction', [':D', ':)']]; 3632 * 3633 * // Anonymous function to execute a local function 3634 * onInit: function () { myFunction(':D'); } 3635 * 3636 * @param {String|Array} callback The function to be called 3637 * @param {Array} [extraParams] In some cases the function can be called with Extra parameters (onError) 3638 * @returns {*} 3639 */ 3640 executeCallback: function (callback, extraParams) { 3641 if (!callback) { 3642 return; 3643 } 3644 3645 var _callback; 3646 3647 if (typeof callback === "function") { 3648 _callback = callback; 3649 } else if (typeof callback === "string" || Array.isArray(callback)) { 3650 if (typeof callback === "string") { 3651 callback = [callback, []]; 3652 } 3653 3654 _callback = this.helper.namespace.call(this, callback[0], window); 3655
3656 if (typeof _callback !== "function") { 3657 // {debug} 3658 if (this.options.debug) { 3659 _debug.log({ 3660 node: this.selector, 3661 function: "executeCallback()", 3662 arguments: JSON.stringify(callback), 3663 message: 'WARNING - Invalid callback function"' 3664 }); 3665 3666 _debug.print(); 3667 } 3668 // {/debug} 3669 return; 3670 } 3671 } 3672 3673 return _callback.apply( 3674 this, 3675 (callback[1] || []).concat(extraParams ? extraParams : []) 3676 ); 3677 }, 3678 3679 namespace: function (string, object, method, defaultValue) { 3680 if (typeof string !== "string" || string === "") { 3681 // {debug} 3682 if (this.options.debug) { 3683 _debug.log({ 3684 node: this.options.input || this.selector, 3685 function: "helper.namespace()", 3686 arguments: string, 3687 message: 'ERROR - Missing string"' 3688 }); 3689 3690 _debug.print(); 3691 } 3692 // {/debug} 3693 return false; 3694 } 3695 3696 var value = typeof defaultValue !== "undefined" 3697 ? defaultValue 3698 : undefined; 3699 3700 // Exit before looping if the string doesn't contain an object reference 3701 if (!~string.indexOf(".")) { 3702 return object[string] || value; 3703 } 3704 3705 var parts = string.split("."), 3706 parent = object || window, 3707 method = method || "get", 3708 currentPart = ""; 3709 3710 for (var i = 0, length = parts.length; i < length; i++) { 3711 currentPart = parts[i]; 3712 3713 if (typeof parent[currentPart] === "undefined") { 3714 if (~["get", "delete"].indexOf(method)) { 3715 return typeof defaultValue !== "undefined" 3716 ? defaultValue 3717 : undefined; 3718 } 3719 parent[currentPart] = {}; 3720 } 3721 3722 if (~["set", "create", "delete"].indexOf(method)) { 3723 if (i === length - 1) { 3724 if (method === "set" || method === "create") { 3725 parent[currentPart] = value; 3726 } else { 3727 delete parent[currentPart]; 3728 return true; 3729 } 3730 } 3731 } 3732 3733 parent = parent[currentPart]; 3734 } 3735 return parent; 3736 }, 3737 3738 typeWatch: (function () { 3739 var timer = 0; 3740 return function (callback, ms) { 3741 clearTimeout(timer); 3742 timer = setTimeout(callback, ms); 3743 }; 3744 })() 3745 } 3746 }; 3747 3748 /** 3749 * @public 3750 * Implement Typeahead on the selected input node. 3751 * 3752 * @param {Object} options 3753 * @return {Object} Modified DOM element 3754 */ 3755 $.fn.typeahead = $.typeahead = function (options) { 3756 return _api.typeahead(this, options); 3757 }; 3758 3759 /** 3760 * @private 3761 * API to handles Typeahead methods via jQuery. 3762 */ 3763 var _api = { 3764 /** 3765 * Enable Typeahead 3766 * 3767 * @param {Object} node 3768 * @param {Object} options 3769 * @returns {*} 3770 */ 3771 typeahead: function (node, options) { 3772 if (!options || !options.source || typeof options.source !== "object") { 3773 // {debug} 3774 _debug.log({ 3775 node: node.selector || (options && options.input), 3776 function: "$.typeahead()", 3777 arguments: JSON.stringify((options && options.source) || ""), 3778 message: 'Undefined "options" or "options.source" or invalid source type - Typeahead dropped' 3779 }); 3780 3781 _debug.print(); 3782 // {/debug} 3783 3784 return; 3785 } 3786 3787 if (typeof node === "function") { 3788 if (!options.input) { 3789 // {debug} 3790 _debug.log({ 3791 node: node.selector, 3792 function: "$.typeahead()", 3793 //'arguments': JSON.stringify(options), 3794 message: 'Undefined "options.input" - Typeahead dropped' 3795 }); 3796 3797 _debug.print(); 3798 // {/debug} 3799 3800 return; 3801 } 3802 3803 node = $(options.input); 3804 } 3805 if (!node.length) { 3806 // {debug} 3807 _debug.log({ 3808 node: node.selector, 3809 function: "$.typeahead()", 3810 arguments: JSON.stringify(options.input), 3811 message: "Unable to find jQuery input element - Typeahead dropped" 3812 }); 3813 3814 _debug.print(); 3815 // {/debug} 3816 3817 return; 3818 } 3819 if (typeof node[0].value === "undefined") { 3820 node[0].value = node.text(); 3821 } 3822 3823 // #270 Forcing node.selector, the property was deleted from jQuery3 3824 // In case of multiple init, each of the instances needs it's own selector! 3825 if (node.length === 1) { 3826 node[0].selector = 3827 node.selector || options.input || node[0].nodeName.toLowerCase(); 3828 3829 /*jshint boss:true */ 3830 return (window.Typeahead[node[0].selector] = new Typeahead(node, options)); 3831 } else { 3832 var instances = {}, 3833 instanceName; 3834 3835 for (var i = 0, ii = node.length; i < ii; ++i) { 3836 instanceName = node[i].nodeName.toLowerCase(); 3837 if (typeof instances[instanceName] !== "undefined") { 3838 instanceName += i; 3839 } 3840 node[i].selector = instanceName; 3841 3842 window.Typeahead[instanceName] = instances[instanceName] = new Typeahead(node.eq(i), options); 3843 } 3844 3845 return instances; 3846 } 3847 } 3848 }; 3849 3850 // {debug} 3851 var _debug = { 3852 table: {}, 3853 log: function (debugObject) { 3854 if (!debugObject.message || typeof debugObject.message !== "string") { 3855 return; 3856 } 3857 3858 this.table[debugObject.message] = $.extend( 3859 { 3860 node: "", 3861 function: "", 3862 arguments: "" 3863 }, 3864 debugObject 3865 ); 3866 }, 3867 print: function () { 3868 if ( 3869 Typeahead.prototype.helper.isEmpty(this.table) || !console || !console.table 3870 ) { 3871 return; 3872 } 3873 3874 if (console.group !== undefined || console.table !== undefined) { 3875 console.groupCollapsed("--- jQuery Typeahead Debug ---"); 3876 console.table(this.table); 3877 console.groupEnd(); 3878 } 3879 3880 this.table = {}; 3881 } 3882 }; 3883 _debug.log({ 3884 message: "WARNING - You are using the DEBUG version. Use /dist/jquery.typeahead.min.js in production." 3885 }); 3886 3887 _debug.print(); 3888 // {/debug} 3889 3890 // IE8 Shims 3891 window.console = window.console || { 3892 log: function () { 3893 } 3894 }; 3895 3896 if (!Array.isArray) { 3897 Array.isArray = function (arg) { 3898 return Object.prototype.toString.call(arg) === "[object Array]"; 3899 }; 3900 } 3901 3902 if (!("trim" in String.prototype)) { 3903 String.prototype.trim = function () { 3904 return this.replace(/^\s+/, "").replace(/\s+$/, ""); 3905 }; 3906 } 3907 if (!("indexOf" in Array.prototype)) { 3908 Array.prototype.indexOf = function (find, i /*opt*/) { 3909 if (i === undefined) i = 0; 3910 if (i < 0) i += this.length; 3911 if (i < 0) i = 0; 3912 for (var n = this.length; i < n; i++) 3913 if (i in this && this[i] === find) return i; 3914 return -1; 3915 }; 3916 } 3917 if (!Object.keys) { 3918 Object.keys = function (obj) { 3919 var keys = [], 3920 k; 3921 for (k in obj) { 3922 if (Object.prototype.hasOwnProperty.call(obj, k)) { 3923 keys.push(k); 3924 } 3925 } 3926 return keys; 3927 }; 3928 } 3929 3930 return Typeahead; 3931});
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.