vendor: 57,192 bytes, lines 1-1829
1if (!Array.prototype.find) { 2 Array.prototype.find = function(predicate) { 3 if (this === null) { 4 throw new TypeError('Array.prototype.find called on null or undefined') 5 } 6 if (typeof predicate !== 'function') { 7 throw new TypeError('predicate must be a function') 8 } 9 var list = Object(this); 10 var length = list.length >>> 0; 11 var thisArg = arguments[1]; 12 var value; 13 14 for (var i = 0; i < length; i++) { 15 value = list[i]; 16 if (predicate.call(thisArg, value, i, list)) { 17 return value 18 } 19 } 20 return undefined 21 }; 22} 23 24if (window && typeof window.CustomEvent !== "function") { 25 function CustomEvent$1(event, params) { 26 params = params || { 27 bubbles: false, 28 cancelable: false, 29 detail: undefined 30 }; 31 var evt = document.createEvent('CustomEvent'); 32 evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); 33 return evt 34 } 35 36 if (typeof window.Event !== 'undefined') { 37 CustomEvent$1.prototype = window.Event.prototype; 38 } 39 40 window.CustomEvent = CustomEvent$1; 41} 42 43class TributeEvents { 44 constructor(tribute) { 45 this.tribute = tribute; 46 this.tribute.events = this; 47 } 48 49 static keys() { 50 return [ 51 { 52 key: 9, 53 value: "TAB" 54 }, 55 { 56 key: 8, 57 value: "DELETE" 58 }, 59 { 60 key: 13, 61 value: "ENTER" 62 }, 63 { 64 key: 27, 65 value: "ESCAPE" 66 }, 67 { 68 key: 32, 69 value: "SPACE" 70 }, 71 { 72 key: 38, 73 value: "UP" 74 }, 75 { 76 key: 40, 77 value: "DOWN" 78 } 79 ]; 80 } 81 82 bind(element) { 83 element.boundKeydown = this.keydown.bind(element, this); 84 element.boundKeyup = this.keyup.bind(element, this); 85 element.boundInput = this.input.bind(element, this); 86 87 element.addEventListener("keydown", element.boundKeydown, false); 88 element.addEventListener("keyup", element.boundKeyup, false); 89 element.addEventListener("input", element.boundInput, false); 90 } 91 92 unbind(element) { 93 element.removeEventListener("keydown", element.boundKeydown, false); 94 element.removeEventListener("keyup", element.boundKeyup, false); 95 element.removeEventListener("input", element.boundInput, false); 96 97 delete element.boundKeydown; 98 delete element.boundKeyup; 99 delete element.boundInput; 100 } 101 102 keydown(instance, event) { 103 if (instance.shouldDeactivate(event)) { 104 instance.tribute.isActive = false; 105 instance.tribute.hideMenu(); 106 } 107 108 let element = this; 109 instance.commandEvent = false; 110 111 TributeEvents.keys().forEach(o => { 112 if (o.key === event.keyCode) { 113 instance.commandEvent = true; 114 instance.callbacks()[o.value.toLowerCase()](event, element); 115 } 116 }); 117 } 118 119 input(instance, event) { 120 instance.inputEvent = true; 121 instance.keyup.call(this, instance, event); 122 } 123 124 click(instance, event) { 125 let tribute = instance.tribute; 126 if (tribute.menu && tribute.menu.contains(event.target)) { 127 let li = event.target; 128 event.preventDefault(); 129 event.stopPropagation(); 130 while (li.nodeName.toLowerCase() !== "li") { 131 li = li.parentNode; 132 if (!li || li === tribute.menu) { 133 throw new Error("cannot find the <li> container for the click"); 134 } 135 } 136 tribute.selectItemAtIndex(li.getAttribute("data-index"), event); 137 tribute.hideMenu(); 138 139 // TODO: should fire with externalTrigger and target is outside of menu 140 } else if (tribute.current.element && !tribute.current.externalTrigger) { 141 tribute.current.externalTrigger = false; 142 setTimeout(() => tribute.hideMenu()); 143 } 144 } 145 146 keyup(instance, event) { 147 if (instance.inputEvent) { 148 instance.inputEvent = false; 149 } 150 instance.updateSelection(this); 151 152 if (event.keyCode === 27) return; 153 154 if (!instance.tribute.allowSpaces && instance.tribute.hasTrailingSpace) { 155 instance.tribute.hasTrailingSpace = false; 156 instance.commandEvent = true; 157 instance.callbacks()["space"](event, this); 158 return; 159 } 160 161 if (!instance.tribute.isActive) { 162 if (instance.tribute.autocompleteMode) { 163 instance.callbacks().triggerChar(event, this, ""); 164 } else { 165 let keyCode = instance.getKeyCode(instance, this, event); 166 167 if (isNaN(keyCode) || !keyCode) return; 168 169 let trigger = instance.tribute.triggers().find(trigger => { 170 return trigger.charCodeAt(0) === keyCode; 171 }); 172 173 if (typeof trigger !== "undefined") { 174 instance.callbacks().triggerChar(event, this, trigger); 175 } 176 } 177 } 178 179 if ( 180 instance.tribute.current.mentionText.length < 181 instance.tribute.current.collection.menuShowMinLength 182 ) { 183 return; 184 } 185 186 if ( 187 ((instance.tribute.current.trigger || 188 instance.tribute.autocompleteMode) && 189 instance.commandEvent === false) || 190 (instance.tribute.isActive && event.keyCode === 8) 191 ) { 192 instance.tribute.showMenuFor(this, true); 193 } 194 } 195 196 shouldDeactivate(event) { 197 if (!this.tribute.isActive) return false; 198 199 if (this.tribute.current.mentionText.length === 0) { 200 let eventKeyPressed = false; 201 TributeEvents.keys().forEach(o => { 202 if (event.keyCode === o.key) eventKeyPressed = true; 203 }); 204 205 return !eventKeyPressed; 206 } 207 208 return false; 209 } 210 211 getKeyCode(instance, el, event) { 212 let tribute = instance.tribute; 213 let info = tribute.range.getTriggerInfo( 214 false, 215 tribute.hasTrailingSpace, 216 true, 217 tribute.allowSpaces, 218 tribute.autocompleteMode 219 ); 220 221 if (info) { 222 return info.mentionTriggerChar.charCodeAt(0); 223 } else { 224 return false; 225 } 226 } 227 228 updateSelection(el) { 229 this.tribute.current.element = el; 230 let info = this.tribute.range.getTriggerInfo( 231 false, 232 this.tribute.hasTrailingSpace, 233 true, 234 this.tribute.allowSpaces, 235 this.tribute.autocompleteMode 236 ); 237 238 if (info) { 239 this.tribute.current.selectedPath = info.mentionSelectedPath; 240 this.tribute.current.mentionText = info.mentionText; 241 this.tribute.current.selectedOffset = info.mentionSelectedOffset; 242 } 243 } 244 245 callbacks() { 246 return { 247 triggerChar: (e, el, trigger) => { 248 let tribute = this.tribute; 249 tribute.current.trigger = trigger; 250 251 let collectionItem = tribute.collection.find(item => { 252 return item.trigger === trigger; 253 }); 254 255 tribute.current.collection = collectionItem; 256 257 if ( 258 tribute.current.mentionText.length >= 259 tribute.current.collection.menuShowMinLength && 260 tribute.inputEvent 261 ) { 262 tribute.showMenuFor(el, true); 263 } 264 }, 265 enter: (e, el) => { 266 // choose selection 267 if (this.tribute.isActive && this.tribute.current.filteredItems) { 268 e.preventDefault(); 269 e.stopPropagation(); 270 setTimeout(() => { 271 this.tribute.selectItemAtIndex(this.tribute.menuSelected, e); 272 this.tribute.hideMenu(); 273 }, 0); 274 } 275 }, 276 escape: (e, el) => { 277 if (this.tribute.isActive) { 278 e.preventDefault(); 279 e.stopPropagation(); 280 this.tribute.isActive = false; 281 this.tribute.hideMenu(); 282 } 283 }, 284 tab: (e, el) => { 285 // choose first match 286 this.callbacks().enter(e, el); 287 }, 288 space: (e, el) => { 289 if (this.tribute.isActive) { 290 if (this.tribute.spaceSelectsMatch) { 291 this.callbacks().enter(e, el); 292 } else if (!this.tribute.allowSpaces) { 293 e.stopPropagation(); 294 setTimeout(() => { 295 this.tribute.hideMenu(); 296 this.tribute.isActive = false; 297 }, 0); 298 } 299 } 300 }, 301 up: (e, el) => { 302 // navigate up ul 303 if (this.tribute.isActive && this.tribute.current.filteredItems) { 304 e.preventDefault(); 305 e.stopPropagation(); 306 let count = this.tribute.current.filteredItems.length, 307 selected = this.tribute.menuSelected; 308 309 if (count > selected && selected > 0) { 310 this.tribute.menuSelected--; 311 this.setActiveLi(); 312 } else if (selected === 0) { 313 this.tribute.menuSelected = count - 1; 314 this.setActiveLi(); 315 this.tribute.menu.scrollTop = this.tribute.menu.scrollHeight; 316 } 317 } 318 }, 319 down: (e, el) => { 320 // navigate down ul 321 if (this.tribute.isActive && this.tribute.current.filteredItems) { 322 e.preventDefault(); 323 e.stopPropagation(); 324 let count = this.tribute.current.filteredItems.length - 1, 325 selected = this.tribute.menuSelected; 326 327 if (count > selected) { 328 this.tribute.menuSelected++; 329 this.setActiveLi(); 330 } else if (count === selected) { 331 this.tribute.menuSelected = 0; 332 this.setActiveLi(); 333 this.tribute.menu.scrollTop = 0; 334 } 335 } 336 }, 337 delete: (e, el) => { 338 if ( 339 this.tribute.isActive && 340 this.tribute.current.mentionText.length < 1 341 ) { 342 this.tribute.hideMenu(); 343 } else if (this.tribute.isActive) { 344 this.tribute.showMenuFor(el); 345 } 346 } 347 }; 348 } 349 350 setActiveLi(index) { 351 let lis = this.tribute.menu.querySelectorAll("li"), 352 length = lis.length >>> 0; 353 354 if (index) this.tribute.menuSelected = parseInt(index); 355 356 for (let i = 0; i < length; i++) { 357 let li = lis[i]; 358 if (i === this.tribute.menuSelected) { 359 li.classList.add(this.tribute.current.collection.selectClass); 360 361 let liClientRect = li.getBoundingClientRect(); 362 let menuClientRect = this.tribute.menu.getBoundingClientRect(); 363 364 if (liClientRect.bottom > menuClientRect.bottom) { 365 let scrollDistance = liClientRect.bottom - menuClientRect.bottom; 366 this.tribute.menu.scrollTop += scrollDistance; 367 } else if (liClientRect.top < menuClientRect.top) { 368 let scrollDistance = menuClientRect.top - liClientRect.top; 369 this.tribute.menu.scrollTop -= scrollDistance; 370 } 371 } else { 372 li.classList.remove(this.tribute.current.collection.selectClass); 373 } 374 } 375 } 376 377 getFullHeight(elem, includeMargin) { 378 let height = elem.getBoundingClientRect().height; 379 380 if (includeMargin) { 381 let style = elem.currentStyle || window.getComputedStyle(elem); 382 return ( 383 height + parseFloat(style.marginTop) + parseFloat(style.marginBottom) 384 ); 385 } 386 387 return height; 388 } 389} 390 391class TributeMenuEvents { 392 constructor(tribute) { 393 this.tribute = tribute; 394 this.tribute.menuEvents = this; 395 this.menu = this.tribute.menu; 396 } 397 398 bind(menu) { 399 this.menuClickEvent = this.tribute.events.click.bind(null, this); 400 this.menuContainerScrollEvent = this.debounce( 401 () => { 402 if (this.tribute.isActive) { 403 this.tribute.showMenuFor(this.tribute.current.element, false); 404 } 405 }, 406 300, 407 false 408 ); 409 this.windowResizeEvent = this.debounce( 410 () => { 411 if (this.tribute.isActive) { 412 this.tribute.range.positionMenuAtCaret(true); 413 } 414 }, 415 300, 416 false 417 ); 418 419 // fixes IE11 issues with mousedown 420 this.tribute.range 421 .getDocument() 422 .addEventListener("MSPointerDown", this.menuClickEvent, false); 423 this.tribute.range 424 .getDocument() 425 .addEventListener("mousedown", this.menuClickEvent, false); 426 window.addEventListener("resize", this.windowResizeEvent); 427 428 if (this.menuContainer) { 429 this.menuContainer.addEventListener( 430 "scroll", 431 this.menuContainerScrollEvent, 432 false 433 ); 434 } else { 435 window.addEventListener("scroll", this.menuContainerScrollEvent); 436 } 437 } 438 439 unbind(menu) { 440 this.tribute.range 441 .getDocument() 442 .removeEventListener("mousedown", this.menuClickEvent, false); 443 this.tribute.range 444 .getDocument() 445 .removeEventListener("MSPointerDown", this.menuClickEvent, false); 446 window.removeEventListener("resize", this.windowResizeEvent); 447 448 if (this.menuContainer) { 449 this.menuContainer.removeEventListener( 450 "scroll", 451 this.menuContainerScrollEvent, 452 false 453 ); 454 } else { 455 window.removeEventListener("scroll", this.menuContainerScrollEvent); 456 } 457 } 458 459 debounce(func, wait, immediate) { 460 var timeout; 461 return () => { 462 var context = this, 463 args = arguments; 464 var later = () => { 465 timeout = null; 466 if (!immediate) func.apply(context, args); 467 }; 468 var callNow = immediate && !timeout; 469 clearTimeout(timeout); 470 timeout = setTimeout(later, wait); 471 if (callNow) func.apply(context, args); 472 }; 473 } 474} 475 476// Thanks to https://github.com/jeff-collins/ment.io 477 478class TributeRange { 479 constructor(tribute) { 480 this.tribute = tribute; 481 this.tribute.range = this; 482 } 483 484 getDocument() { 485 let iframe; 486 if (this.tribute.current.collection) { 487 iframe = this.tribute.current.collection.iframe; 488 } 489 490 if (!iframe) { 491 return document 492 } 493 494 return iframe.contentWindow.document 495 } 496 497 positionMenuAtCaret(scrollTo) { 498 let context = this.tribute.current, 499 coordinates; 500 501 let info = this.getTriggerInfo(false, this.tribute.hasTrailingSpace, true, this.tribute.allowSpaces, this.tribute.autocompleteMode); 502 503 if (typeof info !== 'undefined') { 504 505 if(!this.tribute.positionMenu){ 506 this.tribute.menu.style.cssText = `display: block;`; 507 return 508 } 509 510 if (!this.isContentEditable(context.element)) { 511 coordinates = this.getTextAreaOrInputUnderlinePosition(this.tribute.current.element, 512 info.mentionPosition); 513 } 514 else { 515 coordinates = this.getContentEditableCaretPosition(info.mentionPosition); 516 } 517 518 this.tribute.menu.style.cssText = `top: ${coordinates.top}px; 519 left: ${coordinates.left}px; 520 right: ${coordinates.right}px; 521 bottom: ${coordinates.bottom}px; 522 position: absolute; 523 display: block;`; 524 525 if (coordinates.left === 'auto') { 526 this.tribute.menu.style.left = 'auto'; 527 } 528 529 if (coordinates.top === 'auto') { 530 this.tribute.menu.style.top = 'auto'; 531 } 532 533 if (scrollTo) this.scrollIntoView(); 534 535 window.setTimeout(() => { 536 let menuDimensions = { 537 width: this.tribute.menu.offsetWidth, 538 height: this.tribute.menu.offsetHeight 539 }; 540 let menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); 541 542 let menuIsOffScreenHorizontally = window.innerWidth > menuDimensions.width && (menuIsOffScreen.left || menuIsOffScreen.right); 543 let menuIsOffScreenVertically = window.innerHeight > menuDimensions.height && (menuIsOffScreen.top || menuIsOffScreen.bottom); 544 if (menuIsOffScreenHorizontally || menuIsOffScreenVertically) { 545 this.tribute.menu.style.cssText = 'display: none'; 546 this.positionMenuAtCaret(scrollTo); 547 } 548 }, 0); 549 550 } else { 551 this.tribute.menu.style.cssText = 'display: none'; 552 } 553 } 554 555 get menuContainerIsBody() { 556 return this.tribute.menuContainer === document.body || !this.tribute.menuContainer; 557 } 558 559 560 selectElement(targetElement, path, offset) { 561 let range; 562 let elem = targetElement; 563 564 if (path) { 565 for (var i = 0; i < path.length; i++) { 566 elem = elem.childNodes[path[i]]; 567 if (elem === undefined) { 568 return 569 } 570 while (elem.length < offset) { 571 offset -= elem.length; 572 elem = elem.nextSibling; 573 } 574 if (elem.childNodes.length === 0 && !elem.length) { 575 elem = elem.previousSibling; 576 } 577 } 578 } 579 let sel = this.getWindowSelection(); 580 581 range = this.getDocument().createRange(); 582 range.setStart(elem, offset); 583 range.setEnd(elem, offset); 584 range.collapse(true); 585 586 try { 587 sel.removeAllRanges(); 588 } catch (error) {} 589 590 sel.addRange(range); 591 targetElement.focus(); 592 } 593 594 replaceTriggerText(text, requireLeadingSpace, hasTrailingSpace, originalEvent, item) { 595 let info = this.getTriggerInfo(true, hasTrailingSpace, requireLeadingSpace, this.tribute.allowSpaces, this.tribute.autocompleteMode); 596 597 if (info !== undefined) { 598 let context = this.tribute.current; 599 let replaceEvent = new CustomEvent('tribute-replaced', { 600 detail: { 601 item: item, 602 instance: context, 603 context: info, 604 event: originalEvent, 605 } 606 }); 607 608 if (!this.isContentEditable(context.element)) { 609 let myField = this.tribute.current.element; 610 let textSuffix = typeof this.tribute.replaceTextSuffix == 'string' 611 ? this.tribute.replaceTextSuffix 612 : ' '; 613 text += textSuffix; 614 let startPos = info.mentionPosition; 615 let endPos = info.mentionPosition + info.mentionText.length + textSuffix.length; 616 if (!this.tribute.autocompleteMode) { 617 endPos += info.mentionTriggerChar.length - 1; 618 } 619 myField.value = myField.value.substring(0, startPos) + text + 620 myField.value.substring(endPos, myField.value.length); 621 myField.selectionStart = startPos + text.length; 622 myField.selectionEnd = startPos + text.length; 623 } else { 624 // add a space to the end of the pasted text 625 let textSuffix = typeof this.tribute.replaceTextSuffix == 'string' 626 ? this.tribute.replaceTextSuffix 627 : '\xA0'; 628 text += textSuffix; 629 let endPos = info.mentionPosition + info.mentionText.length; 630 if (!this.tribute.autocompleteMode) { 631 endPos += info.mentionTriggerChar.length; 632 } 633 this.pasteHtml(text, info.mentionPosition, endPos); 634 } 635 636 context.element.dispatchEvent(new CustomEvent('input', { bubbles: true })); 637 context.element.dispatchEvent(replaceEvent); 638 } 639 } 640 641 pasteHtml(html, startPos, endPos) { 642 let range, sel; 643 sel = this.getWindowSelection(); 644 range = this.getDocument().createRange(); 645 range.setStart(sel.anchorNode, startPos); 646 range.setEnd(sel.anchorNode, endPos); 647 range.deleteContents(); 648 649 let el = this.getDocument().createElement('div'); 650 el.innerHTML = html; 651 let frag = this.getDocument().createDocumentFragment(), 652 node, lastNode; 653 while ((node = el.firstChild)) { 654 lastNode = frag.appendChild(node); 655 } 656 range.insertNode(frag); 657 658 // Preserve the selection 659 if (lastNode) { 660 range = range.cloneRange(); 661 range.setStartAfter(lastNode); 662 range.collapse(true); 663 sel.removeAllRanges(); 664 sel.addRange(range); 665 } 666 } 667 668 getWindowSelection() { 669 if (this.tribute.collection.iframe) { 670 return this.tribute.collection.iframe.contentWindow.getSelection() 671 } 672 673 return window.getSelection() 674 } 675 676 getNodePositionInParent(element) { 677 if (element.parentNode === null) { 678 return 0 679 } 680 681 for (var i = 0; i < element.parentNode.childNodes.length; i++) { 682 let node = element.parentNode.childNodes[i]; 683 684 if (node === element) { 685 return i 686 } 687 } 688 } 689 690 getContentEditableSelectedPath(ctx) { 691 let sel = this.getWindowSelection(); 692 let selected = sel.anchorNode; 693 let path = []; 694 let offset; 695 696 if (selected != null) { 697 let i; 698 let ce = selected.contentEditable; 699 while (selected !== null && ce !== 'true') { 700 i = this.getNodePositionInParent(selected); 701 path.push(i); 702 selected = selected.parentNode; 703 if (selected !== null) { 704 ce = selected.contentEditable; 705 } 706 } 707 path.reverse(); 708 709 // getRangeAt may not exist, need alternative 710 offset = sel.getRangeAt(0).startOffset; 711 712 return { 713 selected: selected, 714 path: path, 715 offset: offset 716 } 717 } 718 } 719 720 getTextPrecedingCurrentSelection() { 721 let context = this.tribute.current, 722 text = ''; 723 724 if (!this.isContentEditable(context.element)) { 725 let textComponent = this.tribute.current.element; 726 if (textComponent) { 727 let startPos = textComponent.selectionStart; 728 if (textComponent.value && startPos >= 0) { 729 text = textComponent.value.substring(0, startPos); 730 } 731 } 732 733 } else { 734 let selectedElem = this.getWindowSelection().anchorNode; 735 736 if (selectedElem != null) { 737 let workingNodeContent = selectedElem.textContent; 738 let selectStartOffset = this.getWindowSelection().getRangeAt(0).startOffset; 739 740 if (workingNodeContent && selectStartOffset >= 0) { 741 text = workingNodeContent.substring(0, selectStartOffset); 742 } 743 } 744 } 745 746 return text 747 } 748 749 getLastWordInText(text) { 750 text = text.replace(/\u00A0/g, ' '); // https://stackoverflow.com/questions/29850407/how-do-i-replace-unicode-character-u00a0-with-a-space-in-javascript 751 let wordsArray = text.split(/\s+/); 752 let worldsCount = wordsArray.length - 1; 753 return wordsArray[worldsCount].trim() 754 } 755 756 getTriggerInfo(menuAlreadyActive, hasTrailingSpace, requireLeadingSpace, allowSpaces, isAutocomplete) { 757 let ctx = this.tribute.current; 758 let selected, path, offset; 759 760 if (!this.isContentEditable(ctx.element)) { 761 selected = this.tribute.current.element; 762 } else { 763 let selectionInfo = this.getContentEditableSelectedPath(ctx); 764 765 if (selectionInfo) { 766 selected = selectionInfo.selected; 767 path = selectionInfo.path; 768 offset = selectionInfo.offset; 769 } 770 } 771 772 let effectiveRange = this.getTextPrecedingCurrentSelection(); 773 let lastWordOfEffectiveRange = this.getLastWordInText(effectiveRange); 774 775 if (isAutocomplete) { 776 return { 777 mentionPosition: effectiveRange.length - lastWordOfEffectiveRange.length, 778 mentionText: lastWordOfEffectiveRange, 779 mentionSelectedElement: selected, 780 mentionSelectedPath: path, 781 mentionSelectedOffset: offset 782 } 783 } 784 785 if (effectiveRange !== undefined && effectiveRange !== null) { 786 let mostRecentTriggerCharPos = -1; 787 let triggerChar; 788 789 this.tribute.collection.forEach(config => { 790 let c = config.trigger; 791 let idx = config.requireLeadingSpace ? 792 this.lastIndexWithLeadingSpace(effectiveRange, c) : 793 effectiveRange.lastIndexOf(c); 794 795 if (idx > mostRecentTriggerCharPos) { 796 mostRecentTriggerCharPos = idx; 797 triggerChar = c; 798 requireLeadingSpace = config.requireLeadingSpace; 799 } 800 }); 801 802 if (mostRecentTriggerCharPos >= 0 && 803 ( 804 mostRecentTriggerCharPos === 0 || 805 !requireLeadingSpace || 806 /[\xA0\s]/g.test( 807 effectiveRange.substring( 808 mostRecentTriggerCharPos - 1, 809 mostRecentTriggerCharPos) 810 ) 811 ) 812 ) { 813 let currentTriggerSnippet = effectiveRange.substring(mostRecentTriggerCharPos + triggerChar.length, 814 effectiveRange.length); 815 816 triggerChar = effectiveRange.substring(mostRecentTriggerCharPos, mostRecentTriggerCharPos + triggerChar.length); 817 let firstSnippetChar = currentTriggerSnippet.substring(0, 1); 818 let leadingSpace = currentTriggerSnippet.length > 0 && 819 ( 820 firstSnippetChar === ' ' || 821 firstSnippetChar === '\xA0' 822 ); 823 if (hasTrailingSpace) { 824 currentTriggerSnippet = currentTriggerSnippet.trim(); 825 } 826 827 let regex = allowSpaces ? /[^\S ]/g : /[\xA0\s]/g; 828 829 this.tribute.hasTrailingSpace = regex.test(currentTriggerSnippet); 830 831 if (!leadingSpace && (menuAlreadyActive || !(regex.test(currentTriggerSnippet)))) { 832 return { 833 mentionPosition: mostRecentTriggerCharPos, 834 mentionText: currentTriggerSnippet, 835 mentionSelectedElement: selected, 836 mentionSelectedPath: path, 837 mentionSelectedOffset: offset, 838 mentionTriggerChar: triggerChar 839 } 840 } 841 } 842 } 843 } 844 845 lastIndexWithLeadingSpace (str, trigger) { 846 let reversedStr = str.split('').reverse().join(''); 847 let index = -1; 848 849 for (let cidx = 0, len = str.length; cidx < len; cidx++) { 850 let firstChar = cidx === str.length - 1; 851 let leadingSpace = /\s/.test(reversedStr[cidx + 1]); 852 853 let match = true; 854 for (let triggerIdx = trigger.length - 1; triggerIdx >= 0; triggerIdx--) { 855 if (trigger[triggerIdx] !== reversedStr[cidx-triggerIdx]) { 856 match = false; 857 break 858 } 859 } 860 861 if (match && (firstChar || leadingSpace)) { 862 index = str.length - 1 - cidx; 863 break 864 } 865 } 866 867 return index 868 } 869 870 isContentEditable(element) { 871 return element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' 872 } 873 874 isMenuOffScreen(coordinates, menuDimensions) { 875 let windowWidth = window.innerWidth; 876 let windowHeight = window.innerHeight; 877 let doc = document.documentElement; 878 let windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); 879 let windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); 880 881 let menuTop = typeof coordinates.top === 'number' ? coordinates.top : windowTop + windowHeight - coordinates.bottom - menuDimensions.height; 882 let menuRight = typeof coordinates.right === 'number' ? coordinates.right : coordinates.left + menuDimensions.width; 883 let menuBottom = typeof coordinates.bottom === 'number' ? coordinates.bottom : coordinates.top + menuDimensions.height; 884 let menuLeft = typeof coordinates.left === 'number' ? coordinates.left : windowLeft + windowWidth - coordinates.right - menuDimensions.width; 885 886 return { 887 top: menuTop < Math.floor(windowTop), 888 right: menuRight > Math.ceil(windowLeft + windowWidth), 889 bottom: menuBottom > Math.ceil(windowTop + windowHeight), 890 left: menuLeft < Math.floor(windowLeft) 891 } 892 } 893 894 getMenuDimensions() { 895 // Width of the menu depends of its contents and position 896 // We must check what its width would be without any obstruction 897 // This way, we can achieve good positioning for flipping the menu 898 let dimensions = { 899 width: null, 900 height: null 901 }; 902 903 this.tribute.menu.style.cssText = `top: 0px; 904 left: 0px; 905 position: fixed; 906 display: block; 907 visibility; hidden;`; 908 dimensions.width = this.tribute.menu.offsetWidth; 909 dimensions.height = this.tribute.menu.offsetHeight; 910 911 this.tribute.menu.style.cssText = `display: none;`; 912 913 return dimensions 914 } 915 916 getTextAreaOrInputUnderlinePosition(element, position, flipped) { 917 let properties = ['direction', 'boxSizing', 'width', 'height', 'overflowX', 918 'overflowY', 'borderTopWidth', 'borderRightWidth', 919 'borderBottomWidth', 'borderLeftWidth', 'paddingTop', 920 'paddingRight', 'paddingBottom', 'paddingLeft', 921 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 922 'fontSize', 'fontSizeAdjust', 'lineHeight', 'fontFamily', 923 'textAlign', 'textTransform', 'textIndent', 924 'textDecoration', 'letterSpacing', 'wordSpacing' 925 ]; 926 927 let isFirefox = (window.mozInnerScreenX !== null); 928 929 let div = this.getDocument().createElement('div'); 930 div.id = 'input-textarea-caret-position-mirror-div'; 931 this.getDocument().body.appendChild(div); 932 933 let style = div.style; 934 let computed = window.getComputedStyle ? getComputedStyle(element) : element.currentStyle; 935 936 style.whiteSpace = 'pre-wrap'; 937 if (element.nodeName !== 'INPUT') { 938 style.wordWrap = 'break-word'; 939 } 940 941 // position off-screen 942 style.position = 'absolute'; 943 style.visibility = 'hidden'; 944 945 // transfer the element's properties to the div 946 properties.forEach(prop => { 947 style[prop] = computed[prop]; 948 }); 949 950 if (isFirefox) { 951 style.width = `${(parseInt(computed.width) - 2)}px`; 952 if (element.scrollHeight > parseInt(computed.height)) 953 style.overflowY = 'scroll'; 954 } else { 955 style.overflow = 'hidden'; 956 } 957 958 div.textContent = element.value.substring(0, position); 959 960 if (element.nodeName === 'INPUT') { 961 div.textContent = div.textContent.replace(/\s/g, ' '); 962 } 963 964 let span = this.getDocument().createElement('span'); 965 span.textContent = element.value.substring(position) || '.'; 966 div.appendChild(span); 967 968 let rect = element.getBoundingClientRect(); 969 let doc = document.documentElement; 970 let windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); 971 let windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); 972 973 let top = 0; 974 let left = 0; 975 if (this.menuContainerIsBody) { 976 top = rect.top; 977 left = rect.left; 978 } 979 980 let coordinates = { 981 top: top + windowTop + span.offsetTop + parseInt(computed.borderTopWidth) + parseInt(computed.fontSize) - element.scrollTop, 982 left: left + windowLeft + span.offsetLeft + parseInt(computed.borderLeftWidth) 983 }; 984 985 let windowWidth = window.innerWidth; 986 let windowHeight = window.innerHeight; 987 988 let menuDimensions = this.getMenuDimensions(); 989 let menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); 990 991 if (menuIsOffScreen.right) { 992 coordinates.right = windowWidth - coordinates.left; 993 coordinates.left = 'auto'; 994 } 995 996 let parentHeight = this.tribute.menuContainer 997 ? this.tribute.menuContainer.offsetHeight 998 : this.getDocument().body.offsetHeight; 999 1000 if (menuIsOffScreen.bottom) { 1001 let parentRect = this.tribute.menuContainer 1002 ? this.tribute.menuContainer.getBoundingClientRect() 1003 : this.getDocument().body.getBoundingClientRect(); 1004 let scrollStillAvailable = parentHeight - (windowHeight - parentRect.top); 1005 1006 coordinates.bottom = scrollStillAvailable + (windowHeight - rect.top - span.offsetTop); 1007 coordinates.top = 'auto'; 1008 } 1009 1010 menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); 1011 if (menuIsOffScreen.left) { 1012 coordinates.left = windowWidth > menuDimensions.width 1013 ? windowLeft + windowWidth - menuDimensions.width 1014 : windowLeft; 1015 delete coordinates.right; 1016 } 1017 if (menuIsOffScreen.top) { 1018 coordinates.top = windowHeight > menuDimensions.height 1019 ? windowTop + windowHeight - menuDimensions.height 1020 : windowTop; 1021 delete coordinates.bottom; 1022 } 1023 1024 this.getDocument().body.removeChild(div); 1025 return coordinates 1026 } 1027 1028 getContentEditableCaretPosition(selectedNodePosition) { 1029 let range; 1030 let sel = this.getWindowSelection(); 1031 1032 range = this.getDocument().createRange(); 1033 range.setStart(sel.anchorNode, selectedNodePosition); 1034 range.setEnd(sel.anchorNode, selectedNodePosition); 1035 1036 range.collapse(false); 1037 1038 let rect = range.getBoundingClientRect(); 1039 let doc = document.documentElement; 1040 let windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); 1041 let windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); 1042 1043 let left = rect.left; 1044 let top = rect.top; 1045 1046 let coordinates = { 1047 left: left + windowLeft, 1048 top: top + rect.height + windowTop 1049 }; 1050 let windowWidth = window.innerWidth; 1051 let windowHeight = window.innerHeight; 1052 1053 let menuDimensions = this.getMenuDimensions(); 1054 let menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); 1055 1056 if (menuIsOffScreen.right) { 1057 coordinates.left = 'auto'; 1058 coordinates.right = windowWidth - rect.left - windowLeft; 1059 } 1060 1061 let parentHeight = this.tribute.menuContainer 1062 ? this.tribute.menuContainer.offsetHeight 1063 : this.getDocument().body.offsetHeight; 1064 1065 if (menuIsOffScreen.bottom) { 1066 let parentRect = this.tribute.menuContainer 1067 ? this.tribute.menuContainer.getBoundingClientRect() 1068 : this.getDocument().body.getBoundingClientRect(); 1069 let scrollStillAvailable = parentHeight - (windowHeight - parentRect.top); 1070 1071 coordinates.top = 'auto'; 1072 coordinates.bottom = scrollStillAvailable + (windowHeight - rect.top); 1073 } 1074 1075 menuIsOffScreen = this.isMenuOffScreen(coordinates, menuDimensions); 1076 if (menuIsOffScreen.left) { 1077 coordinates.left = windowWidth > menuDimensions.width 1078 ? windowLeft + windowWidth - menuDimensions.width 1079 : windowLeft; 1080 delete coordinates.right; 1081 } 1082 if (menuIsOffScreen.top) { 1083 coordinates.top = windowHeight > menuDimensions.height 1084 ? windowTop + windowHeight - menuDimensions.height 1085 : windowTop; 1086 delete coordinates.bottom; 1087 } 1088 1089 if (!this.menuContainerIsBody) { 1090 coordinates.left = coordinates.left ? coordinates.left - this.tribute.menuContainer.offsetLeft : coordinates.left; 1091 coordinates.top = coordinates.top ? coordinates.top - this.tribute.menuContainer.offsetTop : coordinates.top; 1092 } 1093 1094 return coordinates 1095 } 1096 1097 scrollIntoView(elem) { 1098 let reasonableBuffer = 20, 1099 clientRect; 1100 let maxScrollDisplacement = 100; 1101 let e = this.menu; 1102 1103 if (typeof e === 'undefined') return; 1104 1105 while (clientRect === undefined || clientRect.height === 0) { 1106 clientRect = e.getBoundingClientRect(); 1107 1108 if (clientRect.height === 0) { 1109 e = e.childNodes[0]; 1110 if (e === undefined || !e.getBoundingClientRect) { 1111 return 1112 } 1113 } 1114 } 1115 1116 let elemTop = clientRect.top; 1117 let elemBottom = elemTop + clientRect.height; 1118 1119 if (elemTop < 0) { 1120 window.scrollTo(0, window.pageYOffset + clientRect.top - reasonableBuffer); 1121 } else if (elemBottom > window.innerHeight) { 1122 let maxY = window.pageYOffset + clientRect.top - reasonableBuffer; 1123 1124 if (maxY - window.pageYOffset > maxScrollDisplacement) { 1125 maxY = window.pageYOffset + maxScrollDisplacement; 1126 } 1127 1128 let targetY = window.pageYOffset - (window.innerHeight - elemBottom); 1129 1130 if (targetY > maxY) { 1131 targetY = maxY; 1132 } 1133 1134 window.scrollTo(0, targetY); 1135 } 1136 } 1137} 1138 1139// Thanks to https://github.com/mattyork/fuzzy 1140class TributeSearch { 1141 constructor(tribute) { 1142 this.tribute = tribute; 1143 this.tribute.search = this; 1144 } 1145 1146 simpleFilter(pattern, array) { 1147 return array.filter(string => { 1148 return this.test(pattern, string) 1149 }) 1150 } 1151 1152 test(pattern, string) { 1153 return this.match(pattern, string) !== null 1154 } 1155 1156 match(pattern, string, opts) { 1157 opts = opts || {}; 1158 let len = string.length, 1159 pre = opts.pre || '', 1160 post = opts.post || '', 1161 compareString = opts.caseSensitive && string || string.toLowerCase(); 1162 1163 if (opts.skip) { 1164 return {rendered: string, score: 0} 1165 } 1166 1167 pattern = opts.caseSensitive && pattern || pattern.toLowerCase(); 1168 1169 let patternCache = this.traverse(compareString, pattern, 0, 0, []); 1170 if (!patternCache) { 1171 return null 1172 } 1173 return { 1174 rendered: this.render(string, patternCache.cache, pre, post), 1175 score: patternCache.score 1176 } 1177 } 1178 1179 traverse(string, pattern, stringIndex, patternIndex, patternCache) { 1180 // if the pattern search at end 1181 if (pattern.length === patternIndex) { 1182 1183 // calculate score and copy the cache containing the indices where it's found 1184 return { 1185 score: this.calculateScore(patternCache), 1186 cache: patternCache.slice() 1187 } 1188 } 1189 1190 // if string at end or remaining pattern > remaining string 1191 if (string.length === stringIndex || pattern.length - patternIndex > string.length - stringIndex) { 1192 return undefined 1193 } 1194 1195 let c = pattern[patternIndex]; 1196 let index = string.indexOf(c, stringIndex); 1197 let best, temp; 1198 1199 while (index > -1) { 1200 patternCache.push(index); 1201 temp = this.traverse(string, pattern, index + 1, patternIndex + 1, patternCache); 1202 patternCache.pop(); 1203 1204 // if downstream traversal failed, return best answer so far 1205 if (!temp) { 1206 return best 1207 } 1208 1209 if (!best || best.score < temp.score) { 1210 best = temp; 1211 } 1212 1213 index = string.indexOf(c, index + 1); 1214 } 1215 1216 return best 1217 } 1218 1219 calculateScore(patternCache) { 1220 let score = 0; 1221 let temp = 1; 1222 1223 patternCache.forEach((index, i) => { 1224 if (i > 0) { 1225 if (patternCache[i - 1] + 1 === index) { 1226 temp += temp + 1; 1227 } 1228 else { 1229 temp = 1; 1230 } 1231 } 1232 1233 score += temp; 1234 }); 1235 1236 return score 1237 } 1238 1239 render(string, indices, pre, post) { 1240 var rendered = string.substring(0, indices[0]); 1241 1242 indices.forEach((index, i) => { 1243 rendered += pre + string[index] + post + 1244 string.substring(index + 1, (indices[i + 1]) ? indices[i + 1] : string.length); 1245 }); 1246 1247 return rendered 1248 } 1249 1250 filter(pattern, arr, opts) { 1251 opts = opts || {}; 1252 return arr 1253 .reduce((prev, element, idx, arr) => { 1254 let str = element; 1255 1256 if (opts.extract) { 1257 str = opts.extract(element); 1258 1259 if (!str) { // take care of undefineds / nulls / etc. 1260 str = ''; 1261 } 1262 } 1263 1264 let rendered = this.match(pattern, str, opts); 1265 1266 if (rendered != null) { 1267 prev[prev.length] = { 1268 string: rendered.rendered, 1269 score: rendered.score, 1270 index: idx, 1271 original: element 1272 }; 1273 } 1274 1275 return prev 1276 }, []) 1277 1278 .sort((a, b) => { 1279 let compare = b.score - a.score; 1280 if (compare) return compare 1281 return a.index - b.index 1282 }) 1283 } 1284} 1285 1286class Tribute { 1287 constructor({ 1288 values = null, 1289 iframe = null, 1290 selectClass = "highlight", 1291 containerClass = "tribute-container", 1292 itemClass = "", 1293 trigger = "@", 1294 autocompleteMode = false, 1295 selectTemplate = null, 1296 menuItemTemplate = null, 1297 lookup = "key", 1298 fillAttr = "value", 1299 collection = null, 1300 menuContainer = null, 1301 noMatchTemplate = null, 1302 requireLeadingSpace = true, 1303 allowSpaces = false, 1304 replaceTextSuffix = null, 1305 positionMenu = true, 1306 spaceSelectsMatch = false, 1307 searchOpts = {}, 1308 menuItemLimit = null, 1309 menuShowMinLength = 0 1310 }) { 1311 this.autocompleteMode = autocompleteMode; 1312 this.menuSelected = 0; 1313 this.current = {}; 1314 this.inputEvent = false; 1315 this.isActive = false; 1316 this.menuContainer = menuContainer; 1317 this.allowSpaces = allowSpaces; 1318 this.replaceTextSuffix = replaceTextSuffix; 1319 this.positionMenu = positionMenu; 1320 this.hasTrailingSpace = false; 1321 this.spaceSelectsMatch = spaceSelectsMatch; 1322 1323 if (this.autocompleteMode) { 1324 trigger = ""; 1325 allowSpaces = false; 1326 } 1327 1328 if (values) { 1329 this.collection = [ 1330 { 1331 // symbol that starts the lookup 1332 trigger: trigger, 1333 1334 // is it wrapped in an iframe 1335 iframe: iframe, 1336 1337 // class applied to selected item 1338 selectClass: selectClass, 1339 1340 // class applied to the Container 1341 containerClass: containerClass, 1342 1343 // class applied to each item 1344 itemClass: itemClass, 1345 1346 // function called on select that retuns the content to insert 1347 selectTemplate: ( 1348 selectTemplate || Tribute.defaultSelectTemplate 1349 ).bind(this), 1350 1351 // function called that returns content for an item 1352 menuItemTemplate: ( 1353 menuItemTemplate || Tribute.defaultMenuItemTemplate 1354 ).bind(this), 1355 1356 // function called when menu is empty, disables hiding of menu. 1357 noMatchTemplate: (t => { 1358 if (typeof t === "string") { 1359 if (t.trim() === "") return null; 1360 return t; 1361 } 1362 if (typeof t === "function") { 1363 return t.bind(this); 1364 } 1365 1366 return ( 1367 noMatchTemplate || 1368 function() { 1369 return "<li>No Match Found!</li>"; 1370 }.bind(this) 1371 ); 1372 })(noMatchTemplate), 1373 1374 // column to search against in the object 1375 lookup: lookup, 1376 1377 // column that contains the content to insert by default 1378 fillAttr: fillAttr, 1379 1380 // array of objects or a function returning an array of objects 1381 values: values, 1382 1383 requireLeadingSpace: requireLeadingSpace, 1384 1385 searchOpts: searchOpts, 1386 1387 menuItemLimit: menuItemLimit, 1388 1389 menuShowMinLength: menuShowMinLength 1390 } 1391 ]; 1392 } else if (collection) { 1393 if (this.autocompleteMode) 1394 console.warn( 1395 "Tribute in autocomplete mode does not work for collections" 1396 ); 1397 this.collection = collection.map(item => { 1398 return { 1399 trigger: item.trigger || trigger, 1400 iframe: item.iframe || iframe, 1401 selectClass: item.selectClass || selectClass, 1402 containerClass: item.containerClass || containerClass, 1403 itemClass: item.itemClass || itemClass, 1404 selectTemplate: ( 1405 item.selectTemplate || Tribute.defaultSelectTemplate 1406 ).bind(this), 1407 menuItemTemplate: ( 1408 item.menuItemTemplate || Tribute.defaultMenuItemTemplate 1409 ).bind(this), 1410 // function called when menu is empty, disables hiding of menu. 1411 noMatchTemplate: (t => { 1412 if (typeof t === "string") { 1413 if (t.trim() === "") return null; 1414 return t; 1415 } 1416 if (typeof t === "function") { 1417 return t.bind(this); 1418 } 1419 1420 return ( 1421 noMatchTemplate || 1422 function() { 1423 return "<li>No Match Found!</li>"; 1424 }.bind(this) 1425 ); 1426 })(noMatchTemplate), 1427 lookup: item.lookup || lookup, 1428 fillAttr: item.fillAttr || fillAttr, 1429 values: item.values, 1430 requireLeadingSpace: item.requireLeadingSpace, 1431 searchOpts: item.searchOpts || searchOpts, 1432 menuItemLimit: item.menuItemLimit || menuItemLimit, 1433 menuShowMinLength: item.menuShowMinLength || menuShowMinLength 1434 }; 1435 }); 1436 } else { 1437 throw new Error("[Tribute] No collection specified."); 1438 } 1439 1440 new TributeRange(this); 1441 new TributeEvents(this); 1442 new TributeMenuEvents(this); 1443 new TributeSearch(this); 1444 } 1445 1446 get isActive() { 1447 return this._isActive; 1448 } 1449 1450 set isActive(val) { 1451 if (this._isActive != val) { 1452 this._isActive = val; 1453 if (this.current.element) { 1454 let noMatchEvent = new CustomEvent(`tribute-active-${val}`); 1455 this.current.element.dispatchEvent(noMatchEvent); 1456 } 1457 } 1458 } 1459 1460 static defaultSelectTemplate(item) { 1461 if (typeof item === "undefined") 1462 return `${this.current.collection.trigger}${this.current.mentionText}`; 1463 if (this.range.isContentEditable(this.current.element)) { 1464 return ( 1465 '<span class="tribute-mention">' + 1466 (this.current.collection.trigger + 1467 item.original[this.current.collection.fillAttr]) + 1468 "</span>" 1469 ); 1470 } 1471 1472 return ( 1473 this.current.collection.trigger + 1474 item.original[this.current.collection.fillAttr] 1475 ); 1476 } 1477 1478 static defaultMenuItemTemplate(matchItem) { 1479 return matchItem.string; 1480 } 1481 1482 static inputTypes() { 1483 return ["TEXTAREA", "INPUT"]; 1484 } 1485 1486 triggers() { 1487 return this.collection.map(config => { 1488 return config.trigger; 1489 }); 1490 } 1491 1492 attach(el) { 1493 if (!el) { 1494 throw new Error("[Tribute] Must pass in a DOM node or NodeList."); 1495 } 1496 1497 // Check if it is a jQuery collection 1498 if (typeof jQuery !== "undefined" && el instanceof jQuery) { 1499 el = el.get(); 1500 } 1501 1502 // Is el an Array/Array-like object? 1503 if ( 1504 el.constructor === NodeList || 1505 el.constructor === HTMLCollection || 1506 el.constructor === Array 1507 ) { 1508 let length = el.length; 1509 for (var i = 0; i < length; ++i) { 1510 this._attach(el[i]); 1511 } 1512 } else { 1513 this._attach(el); 1514 } 1515 } 1516 1517 _attach(el) { 1518 if (el.hasAttribute("data-tribute")) { 1519 console.warn("Tribute was already bound to " + el.nodeName); 1520 } 1521 1522 this.ensureEditable(el); 1523 this.events.bind(el); 1524 el.setAttribute("data-tribute", true); 1525 } 1526 1527 ensureEditable(element) { 1528 if (Tribute.inputTypes().indexOf(element.nodeName) === -1) { 1529 if (element.contentEditable) { 1530 element.contentEditable = true; 1531 } else { 1532 throw new Error("[Tribute] Cannot bind to " + element.nodeName); 1533 } 1534 } 1535 } 1536 1537 createMenu(containerClass) { 1538 let wrapper = this.range.getDocument().createElement("div"), 1539 ul = this.range.getDocument().createElement("ul"); 1540 wrapper.className = containerClass; 1541 wrapper.appendChild(ul); 1542 1543 if (this.menuContainer) { 1544 return this.menuContainer.appendChild(wrapper); 1545 } 1546 1547 return this.range.getDocument().body.appendChild(wrapper); 1548 } 1549 1550 showMenuFor(element, scrollTo) { 1551 // Only proceed if menu isn't already shown for the current element & mentionText 1552 if ( 1553 this.isActive && 1554 this.current.element === element && 1555 this.current.mentionText === this.currentMentionTextSnapshot 1556 ) { 1557 return; 1558 } 1559 this.currentMentionTextSnapshot = this.current.mentionText; 1560 1561 // create the menu if it doesn't exist. 1562 if (!this.menu) { 1563 this.menu = this.createMenu(this.current.collection.containerClass); 1564 element.tributeMenu = this.menu; 1565 this.menuEvents.bind(this.menu); 1566 } 1567 1568 this.isActive = true; 1569 this.menuSelected = 0; 1570 1571 if (!this.current.mentionText) { 1572 this.current.mentionText = ""; 1573 } 1574 1575 const processValues = values => { 1576 // Tribute may not be active any more by the time the value callback returns 1577 if (!this.isActive) { 1578 return; 1579 } 1580 1581 let items = this.search.filter(this.current.mentionText, values, { 1582 pre: this.current.collection.searchOpts.pre || "<span>", 1583 post: this.current.collection.searchOpts.post || "</span>", 1584 skip: this.current.collection.searchOpts.skip, 1585 extract: el => { 1586 if (typeof this.current.collection.lookup === "string") { 1587 return el[this.current.collection.lookup]; 1588 } else if (typeof this.current.collection.lookup === "function") { 1589 return this.current.collection.lookup(el, this.current.mentionText); 1590 } else { 1591 throw new Error( 1592 "Invalid lookup attribute, lookup must be string or function." 1593 ); 1594 } 1595 } 1596 }); 1597 1598 if (this.current.collection.menuItemLimit) { 1599 items = items.slice(0, this.current.collection.menuItemLimit); 1600 } 1601 1602 this.current.filteredItems = items; 1603 1604 let ul = this.menu.querySelector("ul"); 1605 1606 this.range.positionMenuAtCaret(scrollTo); 1607 1608 if (!items.length) { 1609 let noMatchEvent = new CustomEvent("tribute-no-match", { 1610 detail: this.menu 1611 }); 1612 this.current.element.dispatchEvent(noMatchEvent); 1613 if ( 1614 (typeof this.current.collection.noMatchTemplate === "function" && 1615 !this.current.collection.noMatchTemplate()) || 1616 !this.current.collection.noMatchTemplate 1617 ) { 1618 this.hideMenu(); 1619 } else { 1620 typeof this.current.collection.noMatchTemplate === "function" 1621 ? (ul.innerHTML = this.current.collection.noMatchTemplate()) 1622 : (ul.innerHTML = this.current.collection.noMatchTemplate); 1623 } 1624 1625 return; 1626 } 1627 1628 ul.innerHTML = ""; 1629 let fragment = this.range.getDocument().createDocumentFragment(); 1630 1631 items.forEach((item, index) => { 1632 let li = this.range.getDocument().createElement("li"); 1633 li.setAttribute("data-index", index); 1634 li.className = this.current.collection.itemClass; 1635 li.addEventListener("mousemove", e => { 1636 let [li, index] = this._findLiTarget(e.target); 1637 if (e.movementY !== 0) { 1638 this.events.setActiveLi(index); 1639 } 1640 }); 1641 if (this.menuSelected === index) { 1642 li.classList.add(this.current.collection.selectClass); 1643 } 1644 li.innerHTML = this.current.collection.menuItemTemplate(item); 1645 fragment.appendChild(li); 1646 }); 1647 ul.appendChild(fragment); 1648 }; 1649 1650 if (typeof this.current.collection.values === "function") { 1651 this.current.collection.values(this.current.mentionText, processValues); 1652 } else { 1653 processValues(this.current.collection.values); 1654 } 1655 } 1656 1657 _findLiTarget(el) { 1658 if (!el) return []; 1659 const index = el.getAttribute("data-index"); 1660 return !index ? this._findLiTarget(el.parentNode) : [el, index]; 1661 } 1662 1663 showMenuForCollection(element, collectionIndex) { 1664 if (element !== document.activeElement) { 1665 this.placeCaretAtEnd(element); 1666 } 1667 1668 this.current.collection = this.collection[collectionIndex || 0]; 1669 this.current.externalTrigger = true; 1670 this.current.element = element; 1671 1672 if (element.isContentEditable) 1673 this.insertTextAtCursor(this.current.collection.trigger); 1674 else this.insertAtCaret(element, this.current.collection.trigger); 1675 1676 this.showMenuFor(element); 1677 } 1678 1679 // TODO: make sure this works for inputs/textareas 1680 placeCaretAtEnd(el) { 1681 el.focus(); 1682 if ( 1683 typeof window.getSelection != "undefined" && 1684 typeof document.createRange != "undefined" 1685 ) { 1686 var range = document.createRange(); 1687 range.selectNodeContents(el); 1688 range.collapse(false); 1689 var sel = window.getSelection(); 1690 sel.removeAllRanges(); 1691 sel.addRange(range); 1692 } else if (typeof document.body.createTextRange != "undefined") { 1693 var textRange = document.body.createTextRange(); 1694 textRange.moveToElementText(el); 1695 textRange.collapse(false); 1696 textRange.select(); 1697 } 1698 } 1699 1700 // for contenteditable 1701 insertTextAtCursor(text) { 1702 var sel, range; 1703 sel = window.getSelection(); 1704 range = sel.getRangeAt(0); 1705 range.deleteContents(); 1706 var textNode = document.createTextNode(text); 1707 range.insertNode(textNode); 1708 range.selectNodeContents(textNode); 1709 range.collapse(false); 1710 sel.removeAllRanges(); 1711 sel.addRange(range); 1712 } 1713 1714 // for regular inputs 1715 insertAtCaret(textarea, text) { 1716 var scrollPos = textarea.scrollTop; 1717 var caretPos = textarea.selectionStart; 1718 1719 var front = textarea.value.substring(0, caretPos); 1720 var back = textarea.value.substring( 1721 textarea.selectionEnd, 1722 textarea.value.length 1723 ); 1724 textarea.value = front + text + back; 1725 caretPos = caretPos + text.length; 1726 textarea.selectionStart = caretPos; 1727 textarea.selectionEnd = caretPos; 1728 textarea.focus(); 1729 textarea.scrollTop = scrollPos; 1730 } 1731 1732 hideMenu() { 1733 if (this.menu) { 1734 this.menu.style.cssText = "display: none;"; 1735 this.isActive = false; 1736 this.menuSelected = 0; 1737 this.current = {}; 1738 } 1739 } 1740 1741 selectItemAtIndex(index, originalEvent) { 1742 index = parseInt(index); 1743 if (typeof index !== "number" || isNaN(index)) return; 1744 let item = this.current.filteredItems[index]; 1745 let content = this.current.collection.selectTemplate(item); 1746 if (content !== null) this.replaceText(content, originalEvent, item); 1747 } 1748 1749 replaceText(content, originalEvent, item) { 1750 this.range.replaceTriggerText(content, true, true, originalEvent, item); 1751 } 1752 1753 _append(collection, newValues, replace) { 1754 if (typeof collection.values === "function") { 1755 throw new Error("Unable to append to values, as it is a function."); 1756 } else if (!replace) { 1757 collection.values = collection.values.concat(newValues); 1758 } else { 1759 collection.values = newValues; 1760 } 1761 } 1762 1763 append(collectionIndex, newValues, replace) { 1764 let index = parseInt(collectionIndex); 1765 if (typeof index !== "number") 1766 throw new Error("please provide an index for the collection to update."); 1767 1768 let collection = this.collection[index]; 1769 1770 this._append(collection, newValues, replace); 1771 } 1772 1773 appendCurrent(newValues, replace) { 1774 if (this.isActive) { 1775 this._append(this.current.collection, newValues, replace); 1776 } else { 1777 throw new Error( 1778 "No active state. Please use append instead and pass an index." 1779 ); 1780 } 1781 } 1782 1783 detach(el) { 1784 if (!el) { 1785 throw new Error("[Tribute] Must pass in a DOM node or NodeList."); 1786 } 1787 1788 // Check if it is a jQuery collection 1789 if (typeof jQuery !== "undefined" && el instanceof jQuery) { 1790 el = el.get(); 1791 } 1792 1793 // Is el an Array/Array-like object? 1794 if ( 1795 el.constructor === NodeList || 1796 el.constructor === HTMLCollection || 1797 el.constructor === Array 1798 ) { 1799 let length = el.length; 1800 for (var i = 0; i < length; ++i) { 1801 this._detach(el[i]); 1802 } 1803 } else { 1804 this._detach(el); 1805 } 1806 } 1807 1808 _detach(el) { 1809 this.events.unbind(el); 1810 if (el.tributeMenu) { 1811 this.menuEvents.unbind(el.tributeMenu); 1812 } 1813 1814 setTimeout(() => { 1815 el.removeAttribute("data-tribute"); 1816 this.isActive = false; 1817 if (el.tributeMenu) { 1818 el.tributeMenu.remove(); 1819 } 1820 }); 1821 } 1822} 1823 1824/** 1825 * Tribute.js 1826 * Native ES6 JavaScript @mention Plugin 1827 **/ 1828 1829export default Tribute;
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.