1/* ============================================================= 2 * bootstrap3-typeahead.js v4.0.2 3 * https://github.com/bassjobsen/Bootstrap-3-Typeahead 4 * ============================================================= 5 * Original written by @mdo and @fat 6 * ============================================================= 7 * Copyright 2014 Bass Jobsen @bassjobsen 8 * 9 * Licensed under the Apache License, Version 2.0 (the 'License'); 10 * you may not use this file except in compliance with the License. 11 * You may obtain a copy of the License at 12 * 13 * http://www.apache.org/licenses/LICENSE-2.0 14 * 15 * Unless required by applicable law or agreed to in writing, software 16 * distributed under the License is distributed on an 'AS IS' BASIS, 17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 * See the License for the specific language governing permissions and 19 * limitations under the License. 20 * ============================================================ */ 21 22 23'use strict'; 24// jshint laxcomma: true 25 26 27/* TYPEAHEAD PUBLIC CLASS DEFINITION 28 * ================================= */ 29 30var Typeahead = function (element, options) { 31 this.$element = jQuery(element); 32 this.options = jQuery.extend({}, Typeahead.defaults, options); 33 this.matcher = this.options.matcher || this.matcher; 34 this.sorter = this.options.sorter || this.sorter; 35 this.select = this.options.select || this.select; 36 this.autoSelect = typeof this.options.autoSelect == 'boolean' ? this.options.autoSelect : true; 37 this.highlighter = this.options.highlighter || this.highlighter; 38 this.render = this.options.render || this.render; 39 this.updater = this.options.updater || this.updater; 40 this.displayText = this.options.displayText || this.displayText; 41 this.itemLink = this.options.itemLink || this.itemLink; 42 this.itemTitle = this.options.itemTitle || this.itemTitle; 43 this.followLinkOnSelect = this.options.followLinkOnSelect || this.followLinkOnSelect; 44 this.source = this.options.source; 45 this.delay = this.options.delay; 46 this.theme = this.options.theme && this.options.themes && this.options.themes[this.options.theme] || Typeahead.defaults.themes[Typeahead.defaults.theme]; 47 this.$menu = jQuery(this.options.menu || this.theme.menu); 48 this.$appendTo = this.options.appendTo ? jQuery(this.options.appendTo) : null; 49 this.fitToElement = typeof this.options.fitToElement == 'boolean' ? this.options.fitToElement : false; 50 this.shown = false; 51 this.listen(); 52 this.showHintOnFocus = typeof this.options.showHintOnFocus == 'boolean' || this.options.showHintOnFocus === 'all' ? this.options.showHintOnFocus : false; 53 this.afterSelect = this.options.afterSelect; 54 this.afterEmptySelect = this.options.afterEmptySelect; 55 this.addItem = false; 56 this.value = this.$element.val() || this.$element.text(); 57 this.keyPressed = false; 58 this.focused = this.$element.is(':focus'); 59 this.changeInputOnSelect = this.options.changeInputOnSelect || this.changeInputOnSelect; 60 this.changeInputOnMove = this.options.changeInputOnMove || this.changeInputOnMove; 61 this.openLinkInNewTab = this.options.openLinkInNewTab || this.openLinkInNewTab; 62 this.selectOnBlur = this.options.selectOnBlur || this.selectOnBlur; 63 this.showCategoryHeader = this.options.showCategoryHeader || this.showCategoryHeader; 64}; 65 66Typeahead.prototype = { 67 68 constructor: Typeahead, 69 70 71 setDefault: function (val) { 72 // var val = this.$menu.find('.active').data('value'); 73 this.$element.data('active', val); 74 if (this.autoSelect || val) { 75 var newVal = this.updater(val); 76 // Updater can be set to any random functions via "options" parameter in constructor above. 77 // Add null check for cases when updater returns void or undefined. 78 if (!newVal) { 79 newVal = ''; 80 } 81 this.$element 82 .val(this.displayText(newVal) || newVal) 83 .text(this.displayText(newVal) || newVal) 84 .change(); 85 this.afterSelect(newVal); 86 } 87 return this.hide(); 88 }, 89 90 select: function () { 91 var val = this.$menu.find('.active').data('value'); 92 93 this.$element.data('active', val); 94 if (this.autoSelect || val) { 95 var newVal = this.updater(val); 96 // Updater can be set to any random functions via "options" parameter in constructor above. 97 // Add null check for cases when updater returns void or undefined. 98 if (!newVal) { 99 newVal = ''; 100 } 101 102 if (this.changeInputOnSelect) { 103 this.$element 104 .val(this.displayText(newVal) || newVal) 105 .text(this.displayText(newVal) || newVal) 106 .change(); 107 } 108 109 if (this.followLinkOnSelect && this.itemLink(val)) { 110 if (this.openLinkInNewTab) { 111 window.open(this.itemLink(val), '_blank'); 112 } else { 113 document.location = this.itemLink(val); 114 } 115 this.afterSelect(newVal); 116 } else if (this.followLinkOnSelect && !this.itemLink(val)) { 117 this.afterEmptySelect(newVal); 118 } else { 119 this.afterSelect(newVal); 120 } 121 } else { 122 this.afterEmptySelect(); 123 } 124 125 return this.hide(); 126 }, 127 128 updater: function (item) { 129 return item; 130 }, 131 132 setSource: function (source) { 133 this.source = source; 134 }, 135 136 show: function () { 137 var pos = jQuery.extend({}, this.$element.position(), { 138 height: this.$element[0].offsetHeight 139 }); 140 141 var scrollHeight = typeof this.options.scrollHeight == 'function' ? 142 this.options.scrollHeight.call() : 143 this.options.scrollHeight; 144 145 var element; 146 if (this.shown) { 147 element = this.$menu; 148 } else if (this.$appendTo) { 149 element = this.$menu.appendTo(this.$appendTo); 150 this.hasSameParent = this.$appendTo.is(this.$element.parent()); 151 } else { 152 element = this.$menu.insertAfter(this.$element); 153 this.hasSameParent = true; 154 } 155 156 if (!this.hasSameParent) { 157 // We cannot rely on the element position, need to position relative to the window 158 element.css('position', 'fixed'); 159 var offset = this.$element.offset(); 160 pos.top = offset.top; 161 pos.left = offset.left; 162 } 163 // The rules for bootstrap are: 'dropup' in the parent and 'dropdown-menu-right' in the element. 164 // Note that to get right alignment, you'll need to specify `menu` in the options to be: 165 // '<ul class="typeahead dropdown-menu" role="listbox"></ul>' 166 var dropup = jQuery(element).parent().hasClass('dropup'); 167 var newTop = dropup ? 'auto' : (pos.top + pos.height + scrollHeight); 168 var right = jQuery(element).hasClass('dropdown-menu-right'); 169 var newLeft = right ? 'auto' : pos.left; 170 // it seems like setting the css is a bad idea (just let Bootstrap do it), but I'll keep the old 171 // logic in place except for the dropup/right-align cases. 172 element.css({ top: newTop, left: newLeft }).show(); 173 174 if (this.options.fitToElement === true) { 175 element.css('width', this.$element.outerWidth() + 'px'); 176 } 177 178 this.shown = true; 179 return this; 180 }, 181 182 hide: function () { 183 this.$menu.hide(); 184 this.shown = false; 185 return this; 186 }, 187 188 lookup: function (query) { 189 if (typeof (query) != 'undefined' && query !== null) { 190 this.query = query; 191 } else { 192 this.query = this.$element.val(); 193 } 194 195 if (this.query.length < this.options.minLength && !this.options.showHintOnFocus) { 196 return this.shown ? this.hide() : this; 197 } 198 199 var worker = jQuery.proxy(function () { 200 201 // Bloodhound (since 0.11) needs three arguments. 202 // Two of them are callback functions (sync and async) for local and remote data processing 203 // see https://github.com/twitter/typeahead.js/blob/master/src/bloodhound/bloodhound.js#L132 204 if (jQuery.isFunction(this.source) && this.source.length === 3) { 205 this.source(this.query, jQuery.proxy(this.process, this), jQuery.proxy(this.process, this)); 206 } else if (jQuery.isFunction(this.source)) { 207 this.source(this.query, jQuery.proxy(this.process, this)); 208 } else if (this.source) { 209 this.process(this.source); 210 } 211 }, this); 212 213 clearTimeout(this.lookupWorker); 214 this.lookupWorker = setTimeout(worker, this.delay); 215 }, 216 217 process: function (items) { 218 var that = this; 219 220 items = jQuery.grep(items, function (item) { 221 return that.matcher(item); 222 }); 223 224 items = this.sorter(items); 225 226 if (!items.length && !this.options.addItem) { 227 return this.shown ? this.hide() : this; 228 } 229 230 if (items.length > 0) { 231 this.$element.data('active', items[0]); 232 } else { 233 this.$element.data('active', null); 234 } 235 236 if (this.options.items != 'all') { 237 items = items.slice(0, this.options.items); 238 } 239 240 // Add item 241 if (this.options.addItem) { 242 items.push(this.options.addItem); 243 } 244 245 return this.render(items).show(); 246 }, 247 248 matcher: function (item) { 249 var it = this.displayText(item); 250 return ~it.toLowerCase().indexOf(this.query.toLowerCase()); 251 }, 252 253 sorter: function (items) { 254 var beginswith = []; 255 var caseSensitive = []; 256 var caseInsensitive = []; 257 var item; 258 259 while ((item = items.shift())) { 260 var it = this.displayText(item); 261 if (!it.toLowerCase().indexOf(this.query.toLowerCase())) { 262 beginswith.push(item); 263 } else if (~it.indexOf(this.query)) { 264 caseSensitive.push(item); 265 } else { 266 caseInsensitive.push(item); 267 } 268 } 269 270 return beginswith.concat(caseSensitive, caseInsensitive); 271 }, 272 273 highlighter: function (item) { 274 var text = this.query; 275 if (text === '') { 276 return item; 277 } 278 var matches = item.match(/(>)([^<]*)(<)/g); 279 var first = []; 280 var second = []; 281 var i; 282 if (matches && matches.length) { 283 // html 284 for (i = 0; i < matches.length; ++i) { 285 if (matches[i].length > 2) {// escape '><' 286 first.push(matches[i]); 287 } 288 } 289 } else { 290 // text 291 first = []; 292 first.push(item); 293 } 294 text = text.replace((/[\(\)\/\.\*\+\?\[\]]/g), function (mat) { 295 return '\\' + mat; 296 }); 297 var reg = new RegExp(text, 'g'); 298 var m; 299 for (i = 0; i < first.length; ++i) { 300 m = first[i].match(reg); 301 if (m && m.length > 0) {// find all text nodes matches 302 second.push(first[i]); 303 } 304 } 305 for (i = 0; i < second.length; ++i) { 306 item = item.replace(second[i], second[i].replace(reg, '<strong>$&</strong>')); 307 } 308 return item; 309 }, 310 311 render: function (items) { 312 var that = this; 313 var self = this; 314 var activeFound = false; 315 var data = []; 316 var _category = that.options.separator; 317 318 jQuery.each(items, function (key, value) { 319 // inject separator 320 if (key > 0 && value[_category] !== items[key - 1][_category]) { 321 data.push({ 322 __type: 'divider' 323 }); 324 } 325 326 console.log("Show header:"); 327 console.log(this.showCategoryHeader); 328 this.showCategoryHeader = true; 329 if (this.showCategoryHeader) { 330 // inject category header 331 if (value[_category] && (key === 0 || value[_category] !== items[key - 1][_category])) { 332 data.push({ 333 __type: 'category', 334 name: value[_category] 335 }); 336 } 337 } 338 339 data.push(value); 340 }); 341 342 items = jQuery(data).map(function (i, item) { 343 if ((item.__type || false) == 'category') { 344 return jQuery(that.options.headerHtml || that.theme.headerHtml).text(item.name)[0]; 345 } 346 347 if ((item.__type || false) == 'divider') { 348 return jQuery(that.options.headerDivider || that.theme.headerDivider)[0]; 349 } 350 351 var text = self.displayText(item); 352 i = jQuery(that.options.item || that.theme.item).data('value', item); 353 i.find(that.options.itemContentSelector || that.theme.itemContentSelector) 354 .addBack(that.options.itemContentSelector || that.theme.itemContentSelector) 355 .html(that.highlighter(text, item)); 356 if (that.options.followLinkOnSelect) { 357 i.find('a').attr('href', self.itemLink(item)); 358 } 359 i.find('a').attr('title', self.itemTitle(item)); 360 if (text == self.$element.val()) { 361 i.addClass('active'); 362 self.$element.data('active', item); 363 activeFound = true; 364 } 365 return i[0]; 366 }); 367 368 if (this.autoSelect && !activeFound) { 369 items.filter(':not(.dropdown-header)').first().addClass('active'); 370 this.$element.data('active', items.first().data('value')); 371 } 372 this.$menu.html(items); 373 return this; 374 }, 375 376 displayText: function (item) { 377 return typeof item !== 'undefined' && typeof item.name != 'undefined' ? item.name : item; 378 }, 379 380 itemLink: function (item) { 381 return null; 382 }, 383 384 itemTitle: function (item) { 385 return null; 386 }, 387 388 next: function (event) { 389 var active = this.$menu.find('.active').removeClass('active'); 390 var next = active.next(); 391 392 if (!next.length) { 393 next = jQuery(this.$menu.find(jQuery(this.options.item || this.theme.item).prop('tagName'))[0]); 394 } 395 396 while (next.hasClass('divider') || next.hasClass('dropdown-header')) { 397 next = next.next(); 398 } 399 400 next.addClass('active'); 401 // added for screen reader 402 var newVal = this.updater(next.data('value')); 403 if (this.changeInputOnMove) { 404 this.$element.val(this.displayText(newVal) || newVal); 405 } 406 }, 407 408 prev: function (event) { 409 var active = this.$menu.find('.active').removeClass('active'); 410 var prev = active.prev(); 411 412 if (!prev.length) { 413 prev = this.$menu.find(jQuery(this.options.item || this.theme.item).prop('tagName')).last(); 414 } 415 416 while (prev.hasClass('divider') || prev.hasClass('dropdown-header')) { 417 prev = prev.prev(); 418 } 419 420 prev.addClass('active'); 421 // added for screen reader 422 var newVal = this.updater(prev.data('value')); 423 if (this.changeInputOnMove) { 424 this.$element.val(this.displayText(newVal) || newVal); 425 } 426 }, 427 428 listen: function () { 429 this.$element 430 .on('focus.bootstrap3Typeahead', jQuery.proxy(this.focus, this)) 431 .on('blur.bootstrap3Typeahead', jQuery.proxy(this.blur, this)) 432 .on('keypress.bootstrap3Typeahead', jQuery.proxy(this.keypress, this)) 433 .on('propertychange.bootstrap3Typeahead input.bootstrap3Typeahead', jQuery.proxy(this.input, this)) 434 .on('keyup.bootstrap3Typeahead', jQuery.proxy(this.keyup, this)); 435 436 if (this.eventSupported('keydown')) { 437 this.$element.on('keydown.bootstrap3Typeahead', jQuery.proxy(this.keydown, this)); 438 } 439 440 var itemTagName = jQuery(this.options.item || this.theme.item).prop('tagName'); 441 if ('ontouchstart' in document.documentElement && 'onmousemove' in document.documentElement) { 442 this.$menu 443 .on('touchstart', itemTagName, jQuery.proxy(this.touchstart, this)) 444 .on('touchend', itemTagName, jQuery.proxy(this.click, this)) 445 .on('click', jQuery.proxy(this.click, this)) 446 .on('mouseenter', itemTagName, jQuery.proxy(this.mouseenter, this)) 447 .on('mouseleave', itemTagName, jQuery.proxy(this.mouseleave, this)) 448 .on('mousedown', jQuery.proxy(this.mousedown, this)); 449 } else if ('ontouchstart' in document.documentElement) { 450 this.$menu 451 .on('touchstart', itemTagName, jQuery.proxy(this.touchstart, this)) 452 .on('touchend', itemTagName, jQuery.proxy(this.click, this)); 453 } else { 454 this.$menu 455 .on('click', jQuery.proxy(this.click, this)) 456 .on('mouseenter', itemTagName, jQuery.proxy(this.mouseenter, this)) 457 .on('mouseleave', itemTagName, jQuery.proxy(this.mouseleave, this)) 458 .on('mousedown', jQuery.proxy(this.mousedown, this)); 459 } 460 }, 461 462 destroy: function () { 463 this.$element.data('typeahead', null); 464 this.$element.data('active', null); 465 this.$element 466 .unbind('focus.bootstrap3Typeahead') 467 .unbind('blur.bootstrap3Typeahead') 468 .unbind('keypress.bootstrap3Typeahead') 469 .unbind('propertychange.bootstrap3Typeahead input.bootstrap3Typeahead') 470 .unbind('keyup.bootstrap3Typeahead'); 471 472 if (this.eventSupported('keydown')) { 473 this.$element.unbind('keydown.bootstrap3-typeahead'); 474 } 475 476 this.$menu.remove(); 477 this.destroyed = true; 478 }, 479 480 eventSupported: function (eventName) { 481 var isSupported = eventName in this.$element; 482 if (!isSupported) { 483 this.$element.setAttribute(eventName, 'return;'); 484 isSupported = typeof this.$element[eventName] === 'function'; 485 } 486 return isSupported; 487 }, 488 489 move: function (e) { 490 if (!this.shown) { 491 return; 492 } 493 494 switch (e.keyCode) { 495 case 9: // tab 496 case 13: // enter 497 case 27: // escape 498 e.preventDefault(); 499 break; 500 501 case 38: // up arrow 502 // with the shiftKey (this is actually the left parenthesis) 503 if (e.shiftKey) { 504 return; 505 } 506 e.preventDefault(); 507 this.prev(); 508 break; 509 510 case 40: // down arrow 511 // with the shiftKey (this is actually the right parenthesis) 512 if (e.shiftKey) { 513 return; 514 } 515 e.preventDefault(); 516 this.next(); 517 break; 518 } 519 }, 520 521 keydown: function (e) { 522 /** 523 * Prevent to make an ajax call while copying and pasting. 524 * 525 * @author Simone Sacchi 526 * @version 2018/01/18 527 */ 528 if (e.keyCode === 17) { // ctrl 529 return; 530 } 531 this.keyPressed = true; 532 this.suppressKeyPressRepeat = ~jQuery.inArray(e.keyCode, [40, 38, 9, 13, 27]); 533 if (!this.shown && e.keyCode == 40) { 534 this.lookup(); 535 } else { 536 this.move(e); 537 } 538 }, 539 540 keypress: function (e) { 541 if (this.suppressKeyPressRepeat) { 542 return; 543 } 544 this.move(e); 545 }, 546 547 input: function (e) { 548 // This is a fixed for IE10/11 that fires the input event when a placehoder is changed 549 // (https://connect.microsoft.com/IE/feedback/details/810538/ie-11-fires-input-event-on-focus) 550 var currentValue = this.$element.val() || this.$element.text(); 551 if (this.value !== currentValue) { 552 this.value = currentValue; 553 this.lookup(); 554 } 555 }, 556 557 keyup: function (e) { 558 if (this.destroyed) { 559 return; 560 } 561 switch (e.keyCode) { 562 case 40: // down arrow 563 case 38: // up arrow 564 case 16: // shift 565 case 17: // ctrl 566 case 18: // alt 567 break; 568 569 case 9: // tab 570 if (!this.shown || (this.showHintOnFocus && !this.keyPressed)) { 571 return; 572 } 573 this.select(); 574 break; 575 case 13: // enter 576 if (!this.shown) { 577 return; 578 } 579 this.select(); 580 break; 581 582 case 27: // escape 583 if (!this.shown) { 584 return; 585 } 586 this.hide(); 587 break; 588 } 589 590 }, 591 592 focus: function (e) { 593 if (!this.focused) { 594 this.focused = true; 595 this.keyPressed = false; 596 if (this.options.showHintOnFocus && this.skipShowHintOnFocus !== true) { 597 if (this.options.showHintOnFocus === 'all') { 598 this.lookup(''); 599 } else { 600 this.lookup(); 601 } 602 } 603 } 604 if (this.skipShowHintOnFocus) { 605 this.skipShowHintOnFocus = false; 606 } 607 }, 608 609 blur: function (e) { 610 if (!this.mousedover && !this.mouseddown && this.shown) { 611 if (this.selectOnBlur) { 612 this.select(); 613 } 614 this.hide(); 615 this.focused = false; 616 this.keyPressed = false; 617 } else if (this.mouseddown) { 618 // This is for IE that blurs the input when user clicks on scroll. 619 // We set the focus back on the input and prevent the lookup to occur again 620 this.skipShowHintOnFocus = true; 621 this.$element.focus(); 622 this.mouseddown = false; 623 } 624 }, 625 626 click: function (e) { 627 e.preventDefault(); 628 this.skipShowHintOnFocus = true; 629 this.select(); 630 this.$element.focus(); 631 this.hide(); 632 }, 633 634 mouseenter: function (e) { 635 this.mousedover = true; 636 this.$menu.find('.active').removeClass('active'); 637 jQuery(e.currentTarget).addClass('active'); 638 }, 639 640 mouseleave: function (e) { 641 this.mousedover = false; 642 if (!this.focused && this.shown) { 643 this.hide(); 644 } 645 }, 646 647 /** 648 * We track the mousedown for IE. When clicking on the menu scrollbar, IE makes the input blur thus hiding the menu. 649 */ 650 mousedown: function (e) { 651 this.mouseddown = true; 652 this.$menu.one('mouseup', function (e) { 653 // IE won't fire this, but FF and Chrome will so we reset our flag for them here 654 this.mouseddown = false; 655 }.bind(this)); 656 }, 657 658 touchstart: function (e) { 659 e.preventDefault(); 660 this.$menu.find('.active').removeClass('active'); 661 jQuery(e.currentTarget).addClass('active'); 662 }, 663 664 touchend: function (e) { 665 e.preventDefault(); 666 this.select(); 667 this.$element.focus(); 668 } 669 670}; 671 672 673/* TYPEAHEAD PLUGIN DEFINITION 674 * =========================== */ 675 676var old = jQuery.fn.typeahead; 677 678jQuery.fn.typeahead = function (option) { 679 var arg = arguments; 680 if (typeof option == 'string' && option == 'getActive') { 681 return this.data('active'); 682 } 683 return this.each(function () { 684 var $this = jQuery(this); 685 var data = $this.data('typeahead'); 686 var options = typeof option == 'object' && option; 687 if (!data) { 688 $this.data('typeahead', (data = new Typeahead(this, options))); 689 } 690 if (typeof option == 'string' && data[option]) { 691 if (arg.length > 1) { 692 data[option].apply(data, Array.prototype.slice.call(arg, 1)); 693 } else { 694 data[option](); 695 } 696 } 697 }); 698}; 699 700Typeahead.defaults = { 701 source: [], 702 items: 8, 703 minLength: 1, 704 scrollHeight: 0, 705 autoSelect: true, 706 afterSelect: jQuery.noop, 707 afterEmptySelect: jQuery.noop, 708 addItem: false, 709 followLinkOnSelect: false, 710 delay: 0, 711 separator: 'category', 712 changeInputOnSelect: true, 713 changeInputOnMove: true, 714 openLinkInNewTab: false, 715 selectOnBlur: true, 716 showCategoryHeader: true, 717 theme: "bootstrap4", 718 themes: { 719 bootstrap3: { 720 menu: '<ul class="typeahead mikio-dropdown" role="listbox"></ul>', 721 item: '<li><a class="mikio-dropdown-item" href="#" role="option"></a></li>', 722 itemContentSelector: "a", 723 headerHtml: '<li class="mikio-dropdown-header"></li>', 724 headerDivider: '<li class="mikio-divider" role="separator"></li>' 725 }, 726 bootstrap4: { 727 menu: '<div class="typeahead mikio-dropdown" role="listbox"></div>', 728 item: '<a class="mikio-dropdown-item" href="#" role="option"></a>', 729 itemContentSelector: '.mikio-dropdown-item', 730 headerHtml: '<h6 class="mikio-dropdown-header"></h6>', 731 headerDivider: '<div class="mikio-dropdown-divider"></div>' 732 } 733 } 734}; 735 736jQuery.fn.typeahead.Constructor = Typeahead; 737 738/* TYPEAHEAD NO CONFLICT 739 * =================== */ 740 741jQuery.fn.typeahead.noConflict = function () { 742 jQuery.fn.typeahead = old; 743 return this; 744}; 745 746 747/* TYPEAHEAD DATA-API 748 * ================== */ 749 750jQuery(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { 751 var $this = jQuery(this); 752 if ($this.data('typeahead')) { 753 return; 754 } 755 $this.typeahead($this.data()); 756}); 757