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 this.showCategoryHeader = true; 327 if (this.showCategoryHeader) { 328 // inject category header 329 if (value[_category] && (key === 0 || value[_category] !== items[key - 1][_category])) { 330 data.push({ 331 __type: 'category', 332 name: value[_category] 333 }); 334 } 335 } 336 337 data.push(value); 338 }); 339 340 items = jQuery(data).map(function (i, item) { 341 if ((item.__type || false) == 'category') { 342 return jQuery(that.options.headerHtml || that.theme.headerHtml).text(item.name)[0]; 343 } 344 345 if ((item.__type || false) == 'divider') { 346 return jQuery(that.options.headerDivider || that.theme.headerDivider)[0]; 347 } 348 349 var text = self.displayText(item); 350 i = jQuery(that.options.item || that.theme.item).data('value', item); 351 i.find(that.options.itemContentSelector || that.theme.itemContentSelector) 352 .addBack(that.options.itemContentSelector || that.theme.itemContentSelector) 353 .html(that.highlighter(text, item)); 354 if (that.options.followLinkOnSelect) { 355 i.find('a').attr('href', self.itemLink(item)); 356 } 357 i.find('a').attr('title', self.itemTitle(item)); 358 if (text == self.$element.val()) { 359 i.addClass('active'); 360 self.$element.data('active', item); 361 activeFound = true; 362 } 363 return i[0]; 364 }); 365 366 if (this.autoSelect && !activeFound) { 367 items.filter(':not(.dropdown-header)').first().addClass('active'); 368 this.$element.data('active', items.first().data('value')); 369 } 370 this.$menu.html(items); 371 return this; 372 }, 373 374 displayText: function (item) { 375 return typeof item !== 'undefined' && typeof item.name != 'undefined' ? item.name : item; 376 }, 377 378 itemLink: function (item) { 379 return null; 380 }, 381 382 itemTitle: function (item) { 383 return null; 384 }, 385 386 next: function (event) { 387 var active = this.$menu.find('.active').removeClass('active'); 388 var next = active.next(); 389 390 if (!next.length) { 391 next = jQuery(this.$menu.find(jQuery(this.options.item || this.theme.item).prop('tagName'))[0]); 392 } 393 394 while (next.hasClass('divider') || next.hasClass('dropdown-header')) { 395 next = next.next(); 396 } 397 398 next.addClass('active'); 399 // added for screen reader 400 var newVal = this.updater(next.data('value')); 401 if (this.changeInputOnMove) { 402 this.$element.val(this.displayText(newVal) || newVal); 403 } 404 }, 405 406 prev: function (event) { 407 var active = this.$menu.find('.active').removeClass('active'); 408 var prev = active.prev(); 409 410 if (!prev.length) { 411 prev = this.$menu.find(jQuery(this.options.item || this.theme.item).prop('tagName')).last(); 412 } 413 414 while (prev.hasClass('divider') || prev.hasClass('dropdown-header')) { 415 prev = prev.prev(); 416 } 417 418 prev.addClass('active'); 419 // added for screen reader 420 var newVal = this.updater(prev.data('value')); 421 if (this.changeInputOnMove) { 422 this.$element.val(this.displayText(newVal) || newVal); 423 } 424 }, 425 426 listen: function () { 427 this.$element 428 .on('focus.bootstrap3Typeahead', jQuery.proxy(this.focus, this)) 429 .on('blur.bootstrap3Typeahead', jQuery.proxy(this.blur, this)) 430 .on('keypress.bootstrap3Typeahead', jQuery.proxy(this.keypress, this)) 431 .on('propertychange.bootstrap3Typeahead input.bootstrap3Typeahead', jQuery.proxy(this.input, this)) 432 .on('keyup.bootstrap3Typeahead', jQuery.proxy(this.keyup, this)); 433 434 if (this.eventSupported('keydown')) { 435 this.$element.on('keydown.bootstrap3Typeahead', jQuery.proxy(this.keydown, this)); 436 } 437 438 var itemTagName = jQuery(this.options.item || this.theme.item).prop('tagName'); 439 if ('ontouchstart' in document.documentElement && 'onmousemove' in document.documentElement) { 440 this.$menu 441 .on('touchstart', itemTagName, jQuery.proxy(this.touchstart, this)) 442 .on('touchend', itemTagName, jQuery.proxy(this.click, this)) 443 .on('click', jQuery.proxy(this.click, this)) 444 .on('mouseenter', itemTagName, jQuery.proxy(this.mouseenter, this)) 445 .on('mouseleave', itemTagName, jQuery.proxy(this.mouseleave, this)) 446 .on('mousedown', jQuery.proxy(this.mousedown, this)); 447 } else if ('ontouchstart' in document.documentElement) { 448 this.$menu 449 .on('touchstart', itemTagName, jQuery.proxy(this.touchstart, this)) 450 .on('touchend', itemTagName, jQuery.proxy(this.click, this)); 451 } else { 452 this.$menu 453 .on('click', jQuery.proxy(this.click, this)) 454 .on('mouseenter', itemTagName, jQuery.proxy(this.mouseenter, this)) 455 .on('mouseleave', itemTagName, jQuery.proxy(this.mouseleave, this)) 456 .on('mousedown', jQuery.proxy(this.mousedown, this)); 457 } 458 }, 459 460 destroy: function () { 461 this.$element.data('typeahead', null); 462 this.$element.data('active', null); 463 this.$element 464 .unbind('focus.bootstrap3Typeahead') 465 .unbind('blur.bootstrap3Typeahead') 466 .unbind('keypress.bootstrap3Typeahead') 467 .unbind('propertychange.bootstrap3Typeahead input.bootstrap3Typeahead') 468 .unbind('keyup.bootstrap3Typeahead'); 469 470 if (this.eventSupported('keydown')) { 471 this.$element.unbind('keydown.bootstrap3-typeahead'); 472 } 473 474 this.$menu.remove(); 475 this.destroyed = true; 476 }, 477 478 eventSupported: function (eventName) { 479 var isSupported = eventName in this.$element; 480 if (!isSupported) { 481 this.$element.setAttribute(eventName, 'return;'); 482 isSupported = typeof this.$element[eventName] === 'function'; 483 } 484 return isSupported; 485 }, 486 487 move: function (e) { 488 if (!this.shown) { 489 return; 490 } 491 492 switch (e.keyCode) { 493 case 9: // tab 494 case 13: // enter 495 case 27: // escape 496 e.preventDefault(); 497 break; 498 499 case 38: // up arrow 500 // with the shiftKey (this is actually the left parenthesis) 501 if (e.shiftKey) { 502 return; 503 } 504 e.preventDefault(); 505 this.prev(); 506 break; 507 508 case 40: // down arrow 509 // with the shiftKey (this is actually the right parenthesis) 510 if (e.shiftKey) { 511 return; 512 } 513 e.preventDefault(); 514 this.next(); 515 break; 516 } 517 }, 518 519 keydown: function (e) { 520 /** 521 * Prevent to make an ajax call while copying and pasting. 522 * 523 * @author Simone Sacchi 524 * @version 2018/01/18 525 */ 526 if (e.keyCode === 17) { // ctrl 527 return; 528 } 529 this.keyPressed = true; 530 this.suppressKeyPressRepeat = ~jQuery.inArray(e.keyCode, [40, 38, 9, 13, 27]); 531 if (!this.shown && e.keyCode == 40) { 532 this.lookup(); 533 } else { 534 this.move(e); 535 } 536 }, 537 538 keypress: function (e) { 539 if (this.suppressKeyPressRepeat) { 540 return; 541 } 542 this.move(e); 543 }, 544 545 input: function (e) { 546 // This is a fixed for IE10/11 that fires the input event when a placehoder is changed 547 // (https://connect.microsoft.com/IE/feedback/details/810538/ie-11-fires-input-event-on-focus) 548 var currentValue = this.$element.val() || this.$element.text(); 549 if (this.value !== currentValue) { 550 this.value = currentValue; 551 this.lookup(); 552 } 553 }, 554 555 keyup: function (e) { 556 if (this.destroyed) { 557 return; 558 } 559 switch (e.keyCode) { 560 case 40: // down arrow 561 case 38: // up arrow 562 case 16: // shift 563 case 17: // ctrl 564 case 18: // alt 565 break; 566 567 case 9: // tab 568 if (!this.shown || (this.showHintOnFocus && !this.keyPressed)) { 569 return; 570 } 571 this.select(); 572 break; 573 case 13: // enter 574 if (!this.shown) { 575 return; 576 } 577 this.select(); 578 break; 579 580 case 27: // escape 581 if (!this.shown) { 582 return; 583 } 584 this.hide(); 585 break; 586 } 587 588 }, 589 590 focus: function (e) { 591 if (!this.focused) { 592 this.focused = true; 593 this.keyPressed = false; 594 if (this.options.showHintOnFocus && this.skipShowHintOnFocus !== true) { 595 if (this.options.showHintOnFocus === 'all') { 596 this.lookup(''); 597 } else { 598 this.lookup(); 599 } 600 } 601 } 602 if (this.skipShowHintOnFocus) { 603 this.skipShowHintOnFocus = false; 604 } 605 }, 606 607 blur: function (e) { 608 if (!this.mousedover && !this.mouseddown && this.shown) { 609 if (this.selectOnBlur) { 610 this.select(); 611 } 612 this.hide(); 613 this.focused = false; 614 this.keyPressed = false; 615 } else if (this.mouseddown) { 616 // This is for IE that blurs the input when user clicks on scroll. 617 // We set the focus back on the input and prevent the lookup to occur again 618 this.skipShowHintOnFocus = true; 619 this.$element.focus(); 620 this.mouseddown = false; 621 } 622 }, 623 624 click: function (e) { 625 e.preventDefault(); 626 this.skipShowHintOnFocus = true; 627 this.select(); 628 this.$element.focus(); 629 this.hide(); 630 }, 631 632 mouseenter: function (e) { 633 this.mousedover = true; 634 this.$menu.find('.active').removeClass('active'); 635 jQuery(e.currentTarget).addClass('active'); 636 }, 637 638 mouseleave: function (e) { 639 this.mousedover = false; 640 if (!this.focused && this.shown) { 641 this.hide(); 642 } 643 }, 644 645 /** 646 * We track the mousedown for IE. When clicking on the menu scrollbar, IE makes the input blur thus hiding the menu. 647 */ 648 mousedown: function (e) { 649 this.mouseddown = true; 650 this.$menu.one('mouseup', function (e) { 651 // IE won't fire this, but FF and Chrome will so we reset our flag for them here 652 this.mouseddown = false; 653 }.bind(this)); 654 }, 655 656 touchstart: function (e) { 657 e.preventDefault(); 658 this.$menu.find('.active').removeClass('active'); 659 jQuery(e.currentTarget).addClass('active'); 660 }, 661 662 touchend: function (e) { 663 e.preventDefault(); 664 this.select(); 665 this.$element.focus(); 666 } 667 668}; 669 670 671/* TYPEAHEAD PLUGIN DEFINITION 672 * =========================== */ 673 674var old = jQuery.fn.typeahead; 675 676jQuery.fn.typeahead = function (option) { 677 var arg = arguments; 678 if (typeof option == 'string' && option == 'getActive') { 679 return this.data('active'); 680 } 681 return this.each(function () { 682 var $this = jQuery(this); 683 var data = $this.data('typeahead'); 684 var options = typeof option == 'object' && option; 685 if (!data) { 686 $this.data('typeahead', (data = new Typeahead(this, options))); 687 } 688 if (typeof option == 'string' && data[option]) { 689 if (arg.length > 1) { 690 data[option].apply(data, Array.prototype.slice.call(arg, 1)); 691 } else { 692 data[option](); 693 } 694 } 695 }); 696}; 697 698Typeahead.defaults = { 699 source: [], 700 items: 8, 701 minLength: 1, 702 scrollHeight: 0, 703 autoSelect: true, 704 afterSelect: jQuery.noop, 705 afterEmptySelect: jQuery.noop, 706 addItem: false, 707 followLinkOnSelect: false, 708 delay: 0, 709 separator: 'category', 710 changeInputOnSelect: true, 711 changeInputOnMove: true, 712 openLinkInNewTab: false, 713 selectOnBlur: true, 714 showCategoryHeader: true, 715 theme: "bootstrap4", 716 themes: { 717 bootstrap3: { 718 menu: '<ul class="typeahead mikio-dropdown" role="listbox"></ul>', 719 item: '<li><a class="mikio-dropdown-item" href="#" role="option"></a></li>', 720 itemContentSelector: "a", 721 headerHtml: '<li class="mikio-dropdown-header"></li>', 722 headerDivider: '<li class="mikio-divider" role="separator"></li>' 723 }, 724 bootstrap4: { 725 menu: '<div class="typeahead mikio-dropdown" role="listbox"></div>', 726 item: '<a class="mikio-dropdown-item" href="#" role="option"></a>', 727 itemContentSelector: '.mikio-dropdown-item', 728 headerHtml: '<h6 class="mikio-dropdown-header"></h6>', 729 headerDivider: '<div class="mikio-dropdown-divider"></div>' 730 } 731 } 732}; 733 734jQuery.fn.typeahead.Constructor = Typeahead; 735 736/* TYPEAHEAD NO CONFLICT 737 * =================== */ 738 739jQuery.fn.typeahead.noConflict = function () { 740 jQuery.fn.typeahead = old; 741 return this; 742}; 743 744 745/* TYPEAHEAD DATA-API 746 * ================== */ 747 748jQuery(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { 749 var $this = jQuery(this); 750 if ($this.data('typeahead')) { 751 return; 752 } 753 $this.typeahead($this.data()); 754}); 755