PageSourceSearch

https://wip.co/assets/tributejs-f98ea8e9.js

js wip.co collected 2026-10-02 05:01:29 UTC 57,192 bytes, 1,829 lines download raw bytes

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.