diff --git a/apps/cory/ssblit.json b/apps/cory/ssblit.json
index 6c8aaa17..99d521f1 100644
--- a/apps/cory/ssblit.json
+++ b/apps/cory/ssblit.json
@@ -1 +1 @@
-{"type":"tildefriends-app","files":{"app.js":"&XCpiJOtpMzQz5Zo+Hu9f3ppQON9PxFdV4XnS2Ae+Ye8=.sha256","lit-all.min.js":"&N4A12AsifdQgwdpII0SFtG513BfoLpmPjdJ9VTDftpg=.sha256","index.html":"&WH8A5tF25xlfPDGei2TCQc2/HJFJf5DuRN1GRSYQhhk=.sha256","script.js":"&G8puK9Q4MngHy3D4ppcKyT49WKbHD2OCeUcAw2ghTDE=.sha256","lit-all.min.js.map":"&oFY9wO4MnujgfGNGv4VggHc5V5JwX4C8csqKZ6KJYbE=.sha256","tf-id-picker.js":"&9BDffV4HY9FqhL7XI4it+UQJB4cYwbDNsY3S1cxy2vw=.sha256","tf-app.js":"&qqpOZHnkJQevhLGPUEJ7br2S/LNH+nQtC91vz3CrYrE=.sha256","tf-message.js":"&NC1Fysqht3f3sP5sHKmxuWjJx1HyJa3nx77BlOX9eZY=.sha256","tf-user.js":"&bXTedgBudTQLXEBPY9R8OLfQ/ZLpo8YRU9Oq/wuGG3Y=.sha256","tf-utils.js":"&N2yKZwFnb2GbPeipgQtu6xFvezENNOgud9G7EhCQ/K0=.sha256","commonmark.min.js":"&bfBaMLU19d1p/vPBF9hlARqDX002KXG/UOfxOahZhe4=.sha256","tf-compose.js":"&+qbmbM5zr068m9wyvP9RLz6doVujflQ3oY+rC/ul70U=.sha256","emojis.json":"&h3P4pez+AI4aYdsN0dJ3pbUEFR0276t9AM20caj/W/s=.sha256","emojis.js":"&pqYLDE/13PyEt2ceeFqvnwZ8NqWfPfpDBt4vP8SeHbs=.sha256","tf-styles.js":"&Zw90HptAvGwX/vBnEhRVfNrYjMSssFnnKpp8bzwXQH0=.sha256","tf-profile.js":"&vRKjsnYvOiHCQahzEfznCvP5YDwUPtltlpWf+pxwZ1Y=.sha256","commonmark-linkify.js":"&X+hNNkmSRvKY86khyAun+cXksquXbMakZdINbGbx30g=.sha256","tf-tab-search.js":"&ESt2vMG19sH5j6ungKua/ZuvIGslyuWyb3juXdOCecg=.sha256","tf-tab-news.js":"&6ialbh/M2eBCDH6wFyoOHyDqG9QHbyIrzvRK+CgeyRc=.sha256","tf-tab-connections.js":"&jSnF/5NmgqxRze1XQAEGOW5mPzOV1/8aCyrDRZu34IQ=.sha256","tf-news.js":"&K3azL6eFcgbM9xpLHU3L5oSOApi1fXEvJZrhPkZgDpQ=.sha256"}}
\ No newline at end of file
+{"type":"tildefriends-app","files":{"app.js":"&XCpiJOtpMzQz5Zo+Hu9f3ppQON9PxFdV4XnS2Ae+Ye8=.sha256","lit-all.min.js":"&N4A12AsifdQgwdpII0SFtG513BfoLpmPjdJ9VTDftpg=.sha256","index.html":"&Vpp3ezlQiD5guf1P6yZhpcNMnO0u+uQoil3hNkwiIp4=.sha256","script.js":"&G8puK9Q4MngHy3D4ppcKyT49WKbHD2OCeUcAw2ghTDE=.sha256","lit-all.min.js.map":"&oFY9wO4MnujgfGNGv4VggHc5V5JwX4C8csqKZ6KJYbE=.sha256","tf-id-picker.js":"&9BDffV4HY9FqhL7XI4it+UQJB4cYwbDNsY3S1cxy2vw=.sha256","tf-app.js":"&qqpOZHnkJQevhLGPUEJ7br2S/LNH+nQtC91vz3CrYrE=.sha256","tf-message.js":"&NC1Fysqht3f3sP5sHKmxuWjJx1HyJa3nx77BlOX9eZY=.sha256","tf-user.js":"&bXTedgBudTQLXEBPY9R8OLfQ/ZLpo8YRU9Oq/wuGG3Y=.sha256","tf-utils.js":"&N2yKZwFnb2GbPeipgQtu6xFvezENNOgud9G7EhCQ/K0=.sha256","commonmark.min.js":"&bfBaMLU19d1p/vPBF9hlARqDX002KXG/UOfxOahZhe4=.sha256","tf-compose.js":"&HqoEyjhK0ShnF/bjTezHri2JoX1wxu+/p5UxJFx9Lgs=.sha256","emojis.json":"&h3P4pez+AI4aYdsN0dJ3pbUEFR0276t9AM20caj/W/s=.sha256","emojis.js":"&pqYLDE/13PyEt2ceeFqvnwZ8NqWfPfpDBt4vP8SeHbs=.sha256","tf-styles.js":"&Zw90HptAvGwX/vBnEhRVfNrYjMSssFnnKpp8bzwXQH0=.sha256","tf-profile.js":"&vRKjsnYvOiHCQahzEfznCvP5YDwUPtltlpWf+pxwZ1Y=.sha256","commonmark-linkify.js":"&X+hNNkmSRvKY86khyAun+cXksquXbMakZdINbGbx30g=.sha256","tf-tab-search.js":"&ESt2vMG19sH5j6ungKua/ZuvIGslyuWyb3juXdOCecg=.sha256","tf-tab-news.js":"&6ialbh/M2eBCDH6wFyoOHyDqG9QHbyIrzvRK+CgeyRc=.sha256","tf-tab-connections.js":"&jSnF/5NmgqxRze1XQAEGOW5mPzOV1/8aCyrDRZu34IQ=.sha256","tf-news.js":"&K3azL6eFcgbM9xpLHU3L5oSOApi1fXEvJZrhPkZgDpQ=.sha256","tribute.css":"&9FogMzZHKXCfGb7mlh7z+/wiNZzBsOB/tKoh6MfYJno=.sha256","tribute.esm.js":"&P1wKqCfYULpR/ahSB98JP8xaxfikuZwwtT6I/SAo7/Y=.sha256"}}
\ No newline at end of file
diff --git a/apps/cory/ssblit/index.html b/apps/cory/ssblit/index.html
index 837846f7..0f872525 100644
--- a/apps/cory/ssblit/index.html
+++ b/apps/cory/ssblit/index.html
@@ -3,6 +3,12 @@
@@ -105,6 +119,7 @@ class TfComposeElement extends LitElement {
`;
+ return result;
}
}
diff --git a/apps/cory/ssblit/tribute.css b/apps/cory/ssblit/tribute.css
new file mode 100644
index 00000000..3268c09a
--- /dev/null
+++ b/apps/cory/ssblit/tribute.css
@@ -0,0 +1,32 @@
+.tribute-container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: auto;
+ overflow: auto;
+ display: block;
+ z-index: 999999;
+}
+.tribute-container ul {
+ margin: 0;
+ margin-top: 2px;
+ padding: 0;
+ list-style: none;
+ background: #efefef;
+}
+.tribute-container li {
+ padding: 5px 5px;
+ cursor: pointer;
+}
+.tribute-container li.highlight {
+ background: #ddd;
+}
+.tribute-container li span {
+ font-weight: bold;
+}
+.tribute-container li.no-match {
+ cursor: default;
+}
+.tribute-container .menu-highlighted {
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/apps/cory/ssblit/tribute.esm.js b/apps/cory/ssblit/tribute.esm.js
new file mode 100644
index 00000000..98ee826f
--- /dev/null
+++ b/apps/cory/ssblit/tribute.esm.js
@@ -0,0 +1,1797 @@
+if (!Array.prototype.find) {
+ Array.prototype.find = function(predicate) {
+ if (this === null) {
+ throw new TypeError('Array.prototype.find called on null or undefined')
+ }
+ if (typeof predicate !== 'function') {
+ throw new TypeError('predicate must be a function')
+ }
+ var list = Object(this);
+ var length = list.length >>> 0;
+ var thisArg = arguments[1];
+ var value;
+
+ for (var i = 0; i < length; i++) {
+ value = list[i];
+ if (predicate.call(thisArg, value, i, list)) {
+ return value
+ }
+ }
+ return undefined
+ };
+}
+
+if (window && typeof window.CustomEvent !== "function") {
+ function CustomEvent$1(event, params) {
+ params = params || {
+ bubbles: false,
+ cancelable: false,
+ detail: undefined
+ };
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
+ return evt
+ }
+
+ if (typeof window.Event !== 'undefined') {
+ CustomEvent$1.prototype = window.Event.prototype;
+ }
+
+ window.CustomEvent = CustomEvent$1;
+}
+
+class TributeEvents {
+ constructor(tribute) {
+ this.tribute = tribute;
+ this.tribute.events = this;
+ }
+
+ static keys() {
+ return [
+ {
+ key: 9,
+ value: "TAB"
+ },
+ {
+ key: 8,
+ value: "DELETE"
+ },
+ {
+ key: 13,
+ value: "ENTER"
+ },
+ {
+ key: 27,
+ value: "ESCAPE"
+ },
+ {
+ key: 32,
+ value: "SPACE"
+ },
+ {
+ key: 38,
+ value: "UP"
+ },
+ {
+ key: 40,
+ value: "DOWN"
+ }
+ ];
+ }
+
+ bind(element) {
+ element.boundKeydown = this.keydown.bind(element, this);
+ element.boundKeyup = this.keyup.bind(element, this);
+ element.boundInput = this.input.bind(element, this);
+
+ element.addEventListener("keydown", element.boundKeydown, false);
+ element.addEventListener("keyup", element.boundKeyup, false);
+ element.addEventListener("input", element.boundInput, false);
+ }
+
+ unbind(element) {
+ element.removeEventListener("keydown", element.boundKeydown, false);
+ element.removeEventListener("keyup", element.boundKeyup, false);
+ element.removeEventListener("input", element.boundInput, false);
+
+ delete element.boundKeydown;
+ delete element.boundKeyup;
+ delete element.boundInput;
+ }
+
+ keydown(instance, event) {
+ if (instance.shouldDeactivate(event)) {
+ instance.tribute.isActive = false;
+ instance.tribute.hideMenu();
+ }
+
+ let element = this;
+ instance.commandEvent = false;
+
+ TributeEvents.keys().forEach(o => {
+ if (o.key === event.keyCode) {
+ instance.commandEvent = true;
+ instance.callbacks()[o.value.toLowerCase()](event, element);
+ }
+ });
+ }
+
+ input(instance, event) {
+ instance.inputEvent = true;
+ instance.keyup.call(this, instance, event);
+ }
+
+ click(instance, event) {
+ let tribute = instance.tribute;
+ if (tribute.menu && tribute.menu.contains(event.target)) {
+ let li = event.target;
+ event.preventDefault();
+ event.stopPropagation();
+ while (li.nodeName.toLowerCase() !== "li") {
+ li = li.parentNode;
+ if (!li || li === tribute.menu) {
+ throw new Error("cannot find the
container for the click");
+ }
+ }
+ tribute.selectItemAtIndex(li.getAttribute("data-index"), event);
+ tribute.hideMenu();
+
+ // TODO: should fire with externalTrigger and target is outside of menu
+ } else if (tribute.current.element && !tribute.current.externalTrigger) {
+ tribute.current.externalTrigger = false;
+ setTimeout(() => tribute.hideMenu());
+ }
+ }
+
+ keyup(instance, event) {
+ if (instance.inputEvent) {
+ instance.inputEvent = false;
+ }
+ instance.updateSelection(this);
+
+ if (event.keyCode === 27) return;
+
+ if (!instance.tribute.allowSpaces && instance.tribute.hasTrailingSpace) {
+ instance.tribute.hasTrailingSpace = false;
+ instance.commandEvent = true;
+ instance.callbacks()["space"](event, this);
+ return;
+ }
+
+ if (!instance.tribute.isActive) {
+ if (instance.tribute.autocompleteMode) {
+ instance.callbacks().triggerChar(event, this, "");
+ } else {
+ let keyCode = instance.getKeyCode(instance, this, event);
+
+ if (isNaN(keyCode) || !keyCode) return;
+
+ let trigger = instance.tribute.triggers().find(trigger => {
+ return trigger.charCodeAt(0) === keyCode;
+ });
+
+ if (typeof trigger !== "undefined") {
+ instance.callbacks().triggerChar(event, this, trigger);
+ }
+ }
+ }
+
+ if (
+ instance.tribute.current.mentionText.length <
+ instance.tribute.current.collection.menuShowMinLength
+ ) {
+ return;
+ }
+
+ if (
+ ((instance.tribute.current.trigger ||
+ instance.tribute.autocompleteMode) &&
+ instance.commandEvent === false) ||
+ (instance.tribute.isActive && event.keyCode === 8)
+ ) {
+ instance.tribute.showMenuFor(this, true);
+ }
+ }
+
+ shouldDeactivate(event) {
+ if (!this.tribute.isActive) return false;
+
+ if (this.tribute.current.mentionText.length === 0) {
+ let eventKeyPressed = false;
+ TributeEvents.keys().forEach(o => {
+ if (event.keyCode === o.key) eventKeyPressed = true;
+ });
+
+ return !eventKeyPressed;
+ }
+
+ return false;
+ }
+
+ getKeyCode(instance, el, event) {
+ let tribute = instance.tribute;
+ let info = tribute.range.getTriggerInfo(
+ false,
+ tribute.hasTrailingSpace,
+ true,
+ tribute.allowSpaces,
+ tribute.autocompleteMode
+ );
+
+ if (info) {
+ return info.mentionTriggerChar.charCodeAt(0);
+ } else {
+ return false;
+ }
+ }
+
+ updateSelection(el) {
+ this.tribute.current.element = el;
+ let info = this.tribute.range.getTriggerInfo(
+ false,
+ this.tribute.hasTrailingSpace,
+ true,
+ this.tribute.allowSpaces,
+ this.tribute.autocompleteMode
+ );
+
+ if (info) {
+ this.tribute.current.selectedPath = info.mentionSelectedPath;
+ this.tribute.current.mentionText = info.mentionText;
+ this.tribute.current.selectedOffset = info.mentionSelectedOffset;
+ }
+ }
+
+ callbacks() {
+ return {
+ triggerChar: (e, el, trigger) => {
+ let tribute = this.tribute;
+ tribute.current.trigger = trigger;
+
+ let collectionItem = tribute.collection.find(item => {
+ return item.trigger === trigger;
+ });
+
+ tribute.current.collection = collectionItem;
+
+ if (
+ tribute.current.mentionText.length >=
+ tribute.current.collection.menuShowMinLength &&
+ tribute.inputEvent
+ ) {
+ tribute.showMenuFor(el, true);
+ }
+ },
+ enter: (e, el) => {
+ // choose selection
+ if (this.tribute.isActive && this.tribute.current.filteredItems) {
+ e.preventDefault();
+ e.stopPropagation();
+ setTimeout(() => {
+ this.tribute.selectItemAtIndex(this.tribute.menuSelected, e);
+ this.tribute.hideMenu();
+ }, 0);
+ }
+ },
+ escape: (e, el) => {
+ if (this.tribute.isActive) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.tribute.isActive = false;
+ this.tribute.hideMenu();
+ }
+ },
+ tab: (e, el) => {
+ // choose first match
+ this.callbacks().enter(e, el);
+ },
+ space: (e, el) => {
+ if (this.tribute.isActive) {
+ if (this.tribute.spaceSelectsMatch) {
+ this.callbacks().enter(e, el);
+ } else if (!this.tribute.allowSpaces) {
+ e.stopPropagation();
+ setTimeout(() => {
+ this.tribute.hideMenu();
+ this.tribute.isActive = false;
+ }, 0);
+ }
+ }
+ },
+ up: (e, el) => {
+ // navigate up ul
+ if (this.tribute.isActive && this.tribute.current.filteredItems) {
+ e.preventDefault();
+ e.stopPropagation();
+ let count = this.tribute.current.filteredItems.length,
+ selected = this.tribute.menuSelected;
+
+ if (count > selected && selected > 0) {
+ this.tribute.menuSelected--;
+ this.setActiveLi();
+ } else if (selected === 0) {
+ this.tribute.menuSelected = count - 1;
+ this.setActiveLi();
+ this.tribute.menu.scrollTop = this.tribute.menu.scrollHeight;
+ }
+ }
+ },
+ down: (e, el) => {
+ // navigate down ul
+ if (this.tribute.isActive && this.tribute.current.filteredItems) {
+ e.preventDefault();
+ e.stopPropagation();
+ let count = this.tribute.current.filteredItems.length - 1,
+ selected = this.tribute.menuSelected;
+
+ if (count > selected) {
+ this.tribute.menuSelected++;
+ this.setActiveLi();
+ } else if (count === selected) {
+ this.tribute.menuSelected = 0;
+ this.setActiveLi();
+ this.tribute.menu.scrollTop = 0;
+ }
+ }
+ },
+ delete: (e, el) => {
+ if (
+ this.tribute.isActive &&
+ this.tribute.current.mentionText.length < 1
+ ) {
+ this.tribute.hideMenu();
+ } else if (this.tribute.isActive) {
+ this.tribute.showMenuFor(el);
+ }
+ }
+ };
+ }
+
+ setActiveLi(index) {
+ let lis = this.tribute.menu.querySelectorAll("li"),
+ length = lis.length >>> 0;
+
+ if (index) this.tribute.menuSelected = parseInt(index);
+
+ for (let i = 0; i < length; i++) {
+ let li = lis[i];
+ if (i === this.tribute.menuSelected) {
+ li.classList.add(this.tribute.current.collection.selectClass);
+
+ let liClientRect = li.getBoundingClientRect();
+ let menuClientRect = this.tribute.menu.getBoundingClientRect();
+
+ if (liClientRect.bottom > menuClientRect.bottom) {
+ let scrollDistance = liClientRect.bottom - menuClientRect.bottom;
+ this.tribute.menu.scrollTop += scrollDistance;
+ } else if (liClientRect.top < menuClientRect.top) {
+ let scrollDistance = menuClientRect.top - liClientRect.top;
+ this.tribute.menu.scrollTop -= scrollDistance;
+ }
+ } else {
+ li.classList.remove(this.tribute.current.collection.selectClass);
+ }
+ }
+ }
+
+ getFullHeight(elem, includeMargin) {
+ let height = elem.getBoundingClientRect().height;
+
+ if (includeMargin) {
+ let style = elem.currentStyle || window.getComputedStyle(elem);
+ return (
+ height + parseFloat(style.marginTop) + parseFloat(style.marginBottom)
+ );
+ }
+
+ return height;
+ }
+}
+
+class TributeMenuEvents {
+ constructor(tribute) {
+ this.tribute = tribute;
+ this.tribute.menuEvents = this;
+ this.menu = this.tribute.menu;
+ }
+
+ bind(menu) {
+ this.menuClickEvent = this.tribute.events.click.bind(null, this);
+ this.menuContainerScrollEvent = this.debounce(
+ () => {
+ if (this.tribute.isActive) {
+ this.tribute.hideMenu();
+ }
+ },
+ 10,
+ false
+ );
+ this.windowResizeEvent = this.debounce(
+ () => {
+ if (this.tribute.isActive) {
+ this.tribute.hideMenu();
+ }
+ },
+ 10,
+ false
+ );
+
+ // fixes IE11 issues with mousedown
+ this.tribute.range
+ .getDocument()
+ .addEventListener("MSPointerDown", this.menuClickEvent, false);
+ this.tribute.range
+ .getDocument()
+ .addEventListener("mousedown", this.menuClickEvent, false);
+ window.addEventListener("resize", this.windowResizeEvent);
+
+ if (this.menuContainer) {
+ this.menuContainer.addEventListener(
+ "scroll",
+ this.menuContainerScrollEvent,
+ false
+ );
+ } else {
+ window.addEventListener("scroll", this.menuContainerScrollEvent);
+ }
+ }
+
+ unbind(menu) {
+ this.tribute.range
+ .getDocument()
+ .removeEventListener("mousedown", this.menuClickEvent, false);
+ this.tribute.range
+ .getDocument()
+ .removeEventListener("MSPointerDown", this.menuClickEvent, false);
+ window.removeEventListener("resize", this.windowResizeEvent);
+
+ if (this.menuContainer) {
+ this.menuContainer.removeEventListener(
+ "scroll",
+ this.menuContainerScrollEvent,
+ false
+ );
+ } else {
+ window.removeEventListener("scroll", this.menuContainerScrollEvent);
+ }
+ }
+
+ debounce(func, wait, immediate) {
+ var timeout;
+ return () => {
+ var context = this,
+ args = arguments;
+ var later = () => {
+ timeout = null;
+ if (!immediate) func.apply(context, args);
+ };
+ var callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) func.apply(context, args);
+ };
+ }
+}
+
+// Thanks to https://github.com/jeff-collins/ment.io
+
+class TributeRange {
+ constructor(tribute) {
+ this.tribute = tribute;
+ this.tribute.range = this;
+ }
+
+ getDocument() {
+ let iframe;
+ if (this.tribute.current.collection) {
+ iframe = this.tribute.current.collection.iframe;
+ }
+
+ if (!iframe) {
+ return document
+ }
+
+ return iframe.contentWindow.document
+ }
+
+ positionMenuAtCaret(scrollTo) {
+ let context = this.tribute.current,
+ coordinates;
+
+ let info = this.getTriggerInfo(false, this.tribute.hasTrailingSpace, true, this.tribute.allowSpaces, this.tribute.autocompleteMode);
+
+ if (typeof info !== 'undefined') {
+
+ if(!this.tribute.positionMenu){
+ this.tribute.menu.style.cssText = `display: block;`;
+ return
+ }
+
+ if (!this.isContentEditable(context.element)) {
+ coordinates = this.getTextAreaOrInputUnderlinePosition(this.tribute.current.element,
+ info.mentionPosition);
+ }
+ else {
+ coordinates = this.getContentEditableCaretPosition(info.mentionPosition);
+ }
+
+ this.tribute.menu.style.cssText = `top: ${coordinates.top}px;
+ left: ${coordinates.left}px;
+ right: ${coordinates.right}px;
+ bottom: ${coordinates.bottom}px;
+ max-height: ${coordinates.maxHeight || 500}px;
+ max-width: ${coordinates.maxWidth || 300}px;
+ position: ${coordinates.position || 'absolute'};
+ display: block;`;
+
+ if (coordinates.left === 'auto') {
+ this.tribute.menu.style.left = 'auto';
+ }
+
+ if (coordinates.top === 'auto') {
+ this.tribute.menu.style.top = 'auto';
+ }
+
+ if (scrollTo) this.scrollIntoView();
+
+ } else {
+ this.tribute.menu.style.cssText = 'display: none';
+ }
+ }
+
+ get menuContainerIsBody() {
+ return this.tribute.menuContainer === document.body || !this.tribute.menuContainer;
+ }
+
+
+ selectElement(targetElement, path, offset) {
+ let range;
+ let elem = targetElement;
+
+ if (path) {
+ for (var i = 0; i < path.length; i++) {
+ elem = elem.childNodes[path[i]];
+ if (elem === undefined) {
+ return
+ }
+ while (elem.length < offset) {
+ offset -= elem.length;
+ elem = elem.nextSibling;
+ }
+ if (elem.childNodes.length === 0 && !elem.length) {
+ elem = elem.previousSibling;
+ }
+ }
+ }
+ let sel = this.getWindowSelection();
+
+ range = this.getDocument().createRange();
+ range.setStart(elem, offset);
+ range.setEnd(elem, offset);
+ range.collapse(true);
+
+ try {
+ sel.removeAllRanges();
+ } catch (error) {}
+
+ sel.addRange(range);
+ targetElement.focus();
+ }
+
+ replaceTriggerText(text, requireLeadingSpace, hasTrailingSpace, originalEvent, item) {
+ let info = this.getTriggerInfo(true, hasTrailingSpace, requireLeadingSpace, this.tribute.allowSpaces, this.tribute.autocompleteMode);
+
+ if (info !== undefined) {
+ let context = this.tribute.current;
+ let replaceEvent = new CustomEvent('tribute-replaced', {
+ detail: {
+ item: item,
+ instance: context,
+ context: info,
+ event: originalEvent,
+ }
+ });
+
+ if (!this.isContentEditable(context.element)) {
+ let myField = this.tribute.current.element;
+ let textSuffix = typeof this.tribute.replaceTextSuffix == 'string'
+ ? this.tribute.replaceTextSuffix
+ : ' ';
+ text += textSuffix;
+ let startPos = info.mentionPosition;
+ let endPos = info.mentionPosition + info.mentionText.length + textSuffix.length;
+ if (!this.tribute.autocompleteMode) {
+ endPos += info.mentionTriggerChar.length - 1;
+ }
+ myField.value = myField.value.substring(0, startPos) + text +
+ myField.value.substring(endPos, myField.value.length);
+ myField.selectionStart = startPos + text.length;
+ myField.selectionEnd = startPos + text.length;
+ } else {
+ // add a space to the end of the pasted text
+ let textSuffix = typeof this.tribute.replaceTextSuffix == 'string'
+ ? this.tribute.replaceTextSuffix
+ : '\xA0';
+ text += textSuffix;
+ let endPos = info.mentionPosition + info.mentionText.length;
+ if (!this.tribute.autocompleteMode) {
+ endPos += info.mentionTriggerChar.length;
+ }
+ this.pasteHtml(text, info.mentionPosition, endPos);
+ }
+
+ context.element.dispatchEvent(new CustomEvent('input', { bubbles: true }));
+ context.element.dispatchEvent(replaceEvent);
+ }
+ }
+
+ pasteHtml(html, startPos, endPos) {
+ let range, sel;
+ sel = this.getWindowSelection();
+ range = this.getDocument().createRange();
+ range.setStart(sel.anchorNode, startPos);
+ range.setEnd(sel.anchorNode, endPos);
+ range.deleteContents();
+
+ let el = this.getDocument().createElement('div');
+ el.innerHTML = html;
+ let frag = this.getDocument().createDocumentFragment(),
+ node, lastNode;
+ while ((node = el.firstChild)) {
+ lastNode = frag.appendChild(node);
+ }
+ range.insertNode(frag);
+
+ // Preserve the selection
+ if (lastNode) {
+ range = range.cloneRange();
+ range.setStartAfter(lastNode);
+ range.collapse(true);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+ }
+
+ getWindowSelection() {
+ if (this.tribute.collection.iframe) {
+ return this.tribute.collection.iframe.contentWindow.getSelection()
+ }
+
+ return window.getSelection()
+ }
+
+ getNodePositionInParent(element) {
+ if (element.parentNode === null) {
+ return 0
+ }
+
+ for (var i = 0; i < element.parentNode.childNodes.length; i++) {
+ let node = element.parentNode.childNodes[i];
+
+ if (node === element) {
+ return i
+ }
+ }
+ }
+
+ getContentEditableSelectedPath(ctx) {
+ let sel = this.getWindowSelection();
+ let selected = sel.anchorNode;
+ let path = [];
+ let offset;
+
+ if (selected != null) {
+ let i;
+ let ce = selected.contentEditable;
+ while (selected !== null && ce !== 'true') {
+ i = this.getNodePositionInParent(selected);
+ path.push(i);
+ selected = selected.parentNode;
+ if (selected !== null) {
+ ce = selected.contentEditable;
+ }
+ }
+ path.reverse();
+
+ // getRangeAt may not exist, need alternative
+ offset = sel.getRangeAt(0).startOffset;
+
+ return {
+ selected: selected,
+ path: path,
+ offset: offset
+ }
+ }
+ }
+
+ getTextPrecedingCurrentSelection() {
+ let context = this.tribute.current,
+ text = '';
+
+ if (!this.isContentEditable(context.element)) {
+ let textComponent = this.tribute.current.element;
+ if (textComponent) {
+ let startPos = textComponent.selectionStart;
+ if (textComponent.value && startPos >= 0) {
+ text = textComponent.value.substring(0, startPos);
+ }
+ }
+
+ } else {
+ let selectedElem = this.getWindowSelection().anchorNode;
+
+ if (selectedElem != null) {
+ let workingNodeContent = selectedElem.textContent;
+ let selectStartOffset = this.getWindowSelection().getRangeAt(0).startOffset;
+
+ if (workingNodeContent && selectStartOffset >= 0) {
+ text = workingNodeContent.substring(0, selectStartOffset);
+ }
+ }
+ }
+
+ return text
+ }
+
+ getLastWordInText(text) {
+ text = text.replace(/\u00A0/g, ' '); // https://stackoverflow.com/questions/29850407/how-do-i-replace-unicode-character-u00a0-with-a-space-in-javascript
+ var wordsArray;
+ if (this.tribute.autocompleteSeparator) {
+ wordsArray = text.split(this.tribute.autocompleteSeparator);
+ } else {
+ wordsArray = text.split(/\s+/);
+ }
+ var worldsCount = wordsArray.length - 1;
+ return wordsArray[worldsCount].trim();
+ }
+
+ getTriggerInfo(menuAlreadyActive, hasTrailingSpace, requireLeadingSpace, allowSpaces, isAutocomplete) {
+ let ctx = this.tribute.current;
+ let selected, path, offset;
+
+ if (!this.isContentEditable(ctx.element)) {
+ selected = this.tribute.current.element;
+ } else {
+ let selectionInfo = this.getContentEditableSelectedPath(ctx);
+
+ if (selectionInfo) {
+ selected = selectionInfo.selected;
+ path = selectionInfo.path;
+ offset = selectionInfo.offset;
+ }
+ }
+
+ let effectiveRange = this.getTextPrecedingCurrentSelection();
+ let lastWordOfEffectiveRange = this.getLastWordInText(effectiveRange);
+
+ if (isAutocomplete) {
+ return {
+ mentionPosition: effectiveRange.length - lastWordOfEffectiveRange.length,
+ mentionText: lastWordOfEffectiveRange,
+ mentionSelectedElement: selected,
+ mentionSelectedPath: path,
+ mentionSelectedOffset: offset
+ }
+ }
+
+ if (effectiveRange !== undefined && effectiveRange !== null) {
+ let mostRecentTriggerCharPos = -1;
+ let triggerChar;
+
+ this.tribute.collection.forEach(config => {
+ let c = config.trigger;
+ let idx = config.requireLeadingSpace ?
+ this.lastIndexWithLeadingSpace(effectiveRange, c) :
+ effectiveRange.lastIndexOf(c);
+
+ if (idx > mostRecentTriggerCharPos) {
+ mostRecentTriggerCharPos = idx;
+ triggerChar = c;
+ requireLeadingSpace = config.requireLeadingSpace;
+ }
+ });
+
+ if (mostRecentTriggerCharPos >= 0 &&
+ (
+ mostRecentTriggerCharPos === 0 ||
+ !requireLeadingSpace ||
+ /[\xA0\s]/g.test(
+ effectiveRange.substring(
+ mostRecentTriggerCharPos - 1,
+ mostRecentTriggerCharPos)
+ )
+ )
+ ) {
+ let currentTriggerSnippet = effectiveRange.substring(mostRecentTriggerCharPos + triggerChar.length,
+ effectiveRange.length);
+
+ triggerChar = effectiveRange.substring(mostRecentTriggerCharPos, mostRecentTriggerCharPos + triggerChar.length);
+ let firstSnippetChar = currentTriggerSnippet.substring(0, 1);
+ let leadingSpace = currentTriggerSnippet.length > 0 &&
+ (
+ firstSnippetChar === ' ' ||
+ firstSnippetChar === '\xA0'
+ );
+ if (hasTrailingSpace) {
+ currentTriggerSnippet = currentTriggerSnippet.trim();
+ }
+
+ let regex = allowSpaces ? /[^\S ]/g : /[\xA0\s]/g;
+
+ this.tribute.hasTrailingSpace = regex.test(currentTriggerSnippet);
+
+ if (!leadingSpace && (menuAlreadyActive || !(regex.test(currentTriggerSnippet)))) {
+ return {
+ mentionPosition: mostRecentTriggerCharPos,
+ mentionText: currentTriggerSnippet,
+ mentionSelectedElement: selected,
+ mentionSelectedPath: path,
+ mentionSelectedOffset: offset,
+ mentionTriggerChar: triggerChar
+ }
+ }
+ }
+ }
+ }
+
+ lastIndexWithLeadingSpace (str, trigger) {
+ let reversedStr = str.split('').reverse().join('');
+ let index = -1;
+
+ for (let cidx = 0, len = str.length; cidx < len; cidx++) {
+ let firstChar = cidx === str.length - 1;
+ let leadingSpace = /\s/.test(reversedStr[cidx + 1]);
+
+ let match = true;
+ for (let triggerIdx = trigger.length - 1; triggerIdx >= 0; triggerIdx--) {
+ if (trigger[triggerIdx] !== reversedStr[cidx-triggerIdx]) {
+ match = false;
+ break
+ }
+ }
+
+ if (match && (firstChar || leadingSpace)) {
+ index = str.length - 1 - cidx;
+ break
+ }
+ }
+
+ return index
+ }
+
+ isContentEditable(element) {
+ return element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA'
+ }
+
+ isMenuOffScreen(coordinates, menuDimensions) {
+ let windowWidth = window.innerWidth;
+ let windowHeight = window.innerHeight;
+ let doc = document.documentElement;
+ let windowLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
+ let windowTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
+
+ let menuTop = typeof coordinates.top === 'number' ? coordinates.top : windowTop + windowHeight - coordinates.bottom - menuDimensions.height;
+ let menuRight = typeof coordinates.right === 'number' ? coordinates.right : coordinates.left + menuDimensions.width;
+ let menuBottom = typeof coordinates.bottom === 'number' ? coordinates.bottom : coordinates.top + menuDimensions.height;
+ let menuLeft = typeof coordinates.left === 'number' ? coordinates.left : windowLeft + windowWidth - coordinates.right - menuDimensions.width;
+
+ return {
+ top: menuTop < Math.floor(windowTop),
+ right: menuRight > Math.ceil(windowLeft + windowWidth),
+ bottom: menuBottom > Math.ceil(windowTop + windowHeight),
+ left: menuLeft < Math.floor(windowLeft)
+ }
+ }
+
+ getMenuDimensions() {
+ // Width of the menu depends of its contents and position
+ // We must check what its width would be without any obstruction
+ // This way, we can achieve good positioning for flipping the menu
+ let dimensions = {
+ width: null,
+ height: null
+ };
+
+ this.tribute.menu.style.cssText = `top: 0px;
+ left: 0px;
+ position: fixed;
+ display: block;
+ visibility; hidden;
+ max-height:500px;`;
+ dimensions.width = this.tribute.menu.offsetWidth;
+ dimensions.height = this.tribute.menu.offsetHeight;
+
+ this.tribute.menu.style.cssText = `display: none;`;
+
+ return dimensions
+ }
+
+ getTextAreaOrInputUnderlinePosition(element, position, flipped) {
+ let properties = ['direction', 'boxSizing', 'width', 'height', 'overflowX',
+ 'overflowY', 'borderTopWidth', 'borderRightWidth',
+ 'borderBottomWidth', 'borderLeftWidth', 'borderStyle', 'paddingTop',
+ 'paddingRight', 'paddingBottom', 'paddingLeft',
+ 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch',
+ 'fontSize', 'fontSizeAdjust', 'lineHeight', 'fontFamily',
+ 'textAlign', 'textTransform', 'textIndent',
+ 'textDecoration', 'letterSpacing', 'wordSpacing'
+ ];
+
+ let div = this.getDocument().createElement('div');
+ div.id = 'input-textarea-caret-position-mirror-div';
+ this.getDocument().body.appendChild(div);
+
+ let style = div.style;
+ let computed = window.getComputedStyle ? getComputedStyle(element) : element.currentStyle;
+
+ style.whiteSpace = 'pre-wrap';
+ if (element.nodeName !== 'INPUT') {
+ style.wordWrap = 'break-word';
+ }
+
+ style.position = 'absolute';
+ style.visibility = 'hidden';
+
+ // transfer the element's properties to the div
+ properties.forEach(prop => {
+ style[prop] = computed[prop];
+ });
+
+ //NOT SURE WHY THIS IS HERE AND IT DOESNT SEEM HELPFUL
+ // if (isFirefox) {
+ // style.width = `${(parseInt(computed.width) - 2)}px`
+ // if (element.scrollHeight > parseInt(computed.height))
+ // style.overflowY = 'scroll'
+ // } else {
+ // style.overflow = 'hidden'
+ // }
+
+ let span0 = document.createElement('span');
+ span0.textContent = element.value.substring(0, position);
+ div.appendChild(span0);
+
+ if (element.nodeName === 'INPUT') {
+ div.textContent = div.textContent.replace(/\s/g, ' ');
+ }
+
+ //Create a span in the div that represents where the cursor
+ //should be
+ let span = this.getDocument().createElement('span');
+ //we give it no content as this represents the cursor
+ span.textContent = '';
+ div.appendChild(span);
+
+ let span2 = this.getDocument().createElement('span');
+ span2.textContent = element.value.substring(position);
+ div.appendChild(span2);
+
+ let rect = element.getBoundingClientRect();
+
+ //position the div exactly over the element
+ //so we can get the bounding client rect for the span and
+ //it should represent exactly where the cursor is
+ div.style.position = 'fixed';
+ div.style.left = rect.left + 'px';
+ div.style.top = rect.top + 'px';
+ div.style.width = rect.width + 'px';
+ div.style.height = rect.height + 'px';
+ div.scrollTop = element.scrollTop;
+
+ var spanRect = span.getBoundingClientRect();
+ this.getDocument().body.removeChild(div);
+ return this.getFixedCoordinatesRelativeToRect(spanRect);
+ }
+
+ getContentEditableCaretPosition(selectedNodePosition) {
+ let range;
+ let sel = this.getWindowSelection();
+
+ range = this.getDocument().createRange();
+ range.setStart(sel.anchorNode, selectedNodePosition);
+ range.setEnd(sel.anchorNode, selectedNodePosition);
+
+ range.collapse(false);
+
+ let rect = range.getBoundingClientRect();
+
+ return this.getFixedCoordinatesRelativeToRect(rect);
+ }
+
+ getFixedCoordinatesRelativeToRect(rect) {
+ let coordinates = {
+ position: 'fixed',
+ left: rect.left,
+ top: rect.top + rect.height
+ };
+
+ let menuDimensions = this.getMenuDimensions();
+
+ var availableSpaceOnTop = rect.top;
+ var availableSpaceOnBottom = window.innerHeight - (rect.top + rect.height);
+
+ //check to see where's the right place to put the menu vertically
+ if (availableSpaceOnBottom < menuDimensions.height) {
+ if (availableSpaceOnTop >= menuDimensions.height || availableSpaceOnTop > availableSpaceOnBottom) {
+ coordinates.top = 'auto';
+ coordinates.bottom = window.innerHeight - rect.top;
+ if (availableSpaceOnBottom < menuDimensions.height) {
+ coordinates.maxHeight = availableSpaceOnTop;
+ }
+ } else {
+ if (availableSpaceOnTop < menuDimensions.height) {
+ coordinates.maxHeight = availableSpaceOnBottom;
+ }
+ }
+ }
+
+ var availableSpaceOnLeft = rect.left;
+ var availableSpaceOnRight = window.innerWidth - rect.left;
+
+ //check to see where's the right place to put the menu horizontally
+ if (availableSpaceOnRight < menuDimensions.width) {
+ if (availableSpaceOnLeft >= menuDimensions.width || availableSpaceOnLeft > availableSpaceOnRight) {
+ coordinates.left = 'auto';
+ coordinates.right = window.innerWidth - rect.left;
+ if (availableSpaceOnRight < menuDimensions.width) {
+ coordinates.maxWidth = availableSpaceOnLeft;
+ }
+ } else {
+ if (availableSpaceOnLeft < menuDimensions.width) {
+ coordinates.maxWidth = availableSpaceOnRight;
+ }
+ }
+ }
+
+ return coordinates
+ }
+
+ scrollIntoView(elem) {
+ let reasonableBuffer = 20,
+ clientRect;
+ let maxScrollDisplacement = 100;
+ let e = this.menu;
+
+ if (typeof e === 'undefined') return;
+
+ while (clientRect === undefined || clientRect.height === 0) {
+ clientRect = e.getBoundingClientRect();
+
+ if (clientRect.height === 0) {
+ e = e.childNodes[0];
+ if (e === undefined || !e.getBoundingClientRect) {
+ return
+ }
+ }
+ }
+
+ let elemTop = clientRect.top;
+ let elemBottom = elemTop + clientRect.height;
+
+ if (elemTop < 0) {
+ window.scrollTo(0, window.pageYOffset + clientRect.top - reasonableBuffer);
+ } else if (elemBottom > window.innerHeight) {
+ let maxY = window.pageYOffset + clientRect.top - reasonableBuffer;
+
+ if (maxY - window.pageYOffset > maxScrollDisplacement) {
+ maxY = window.pageYOffset + maxScrollDisplacement;
+ }
+
+ let targetY = window.pageYOffset - (window.innerHeight - elemBottom);
+
+ if (targetY > maxY) {
+ targetY = maxY;
+ }
+
+ window.scrollTo(0, targetY);
+ }
+ }
+}
+
+// Thanks to https://github.com/mattyork/fuzzy
+class TributeSearch {
+ constructor(tribute) {
+ this.tribute = tribute;
+ this.tribute.search = this;
+ }
+
+ simpleFilter(pattern, array) {
+ return array.filter(string => {
+ return this.test(pattern, string)
+ })
+ }
+
+ test(pattern, string) {
+ return this.match(pattern, string) !== null
+ }
+
+ match(pattern, string, opts) {
+ opts = opts || {};
+ let len = string.length,
+ pre = opts.pre || '',
+ post = opts.post || '',
+ compareString = opts.caseSensitive && string || string.toLowerCase();
+
+ if (opts.skip) {
+ return {rendered: string, score: 0}
+ }
+
+ pattern = opts.caseSensitive && pattern || pattern.toLowerCase();
+
+ let patternCache = this.traverse(compareString, pattern, 0, 0, []);
+ if (!patternCache) {
+ return null
+ }
+ return {
+ rendered: this.render(string, patternCache.cache, pre, post),
+ score: patternCache.score
+ }
+ }
+
+ traverse(string, pattern, stringIndex, patternIndex, patternCache) {
+ if (this.tribute.autocompleteSeparator) {
+ // if the pattern search at end
+ pattern = pattern.split(this.tribute.autocompleteSeparator).splice(-1)[0];
+ }
+
+ if (pattern.length === patternIndex) {
+
+ // calculate score and copy the cache containing the indices where it's found
+ return {
+ score: this.calculateScore(patternCache),
+ cache: patternCache.slice()
+ }
+ }
+
+ // if string at end or remaining pattern > remaining string
+ if (string.length === stringIndex || pattern.length - patternIndex > string.length - stringIndex) {
+ return undefined
+ }
+
+ let c = pattern[patternIndex];
+ let index = string.indexOf(c, stringIndex);
+ let best, temp;
+
+ while (index > -1) {
+ patternCache.push(index);
+ temp = this.traverse(string, pattern, index + 1, patternIndex + 1, patternCache);
+ patternCache.pop();
+
+ // if downstream traversal failed, return best answer so far
+ if (!temp) {
+ return best
+ }
+
+ if (!best || best.score < temp.score) {
+ best = temp;
+ }
+
+ index = string.indexOf(c, index + 1);
+ }
+
+ return best
+ }
+
+ calculateScore(patternCache) {
+ let score = 0;
+ let temp = 1;
+
+ patternCache.forEach((index, i) => {
+ if (i > 0) {
+ if (patternCache[i - 1] + 1 === index) {
+ temp += temp + 1;
+ }
+ else {
+ temp = 1;
+ }
+ }
+
+ score += temp;
+ });
+
+ return score
+ }
+
+ render(string, indices, pre, post) {
+ var rendered = string.substring(0, indices[0]);
+
+ indices.forEach((index, i) => {
+ rendered += pre + string[index] + post +
+ string.substring(index + 1, (indices[i + 1]) ? indices[i + 1] : string.length);
+ });
+
+ return rendered
+ }
+
+ filter(pattern, arr, opts) {
+ opts = opts || {};
+ return arr
+ .reduce((prev, element, idx, arr) => {
+ let str = element;
+
+ if (opts.extract) {
+ str = opts.extract(element);
+
+ if (!str) { // take care of undefineds / nulls / etc.
+ str = '';
+ }
+ }
+
+ let rendered = this.match(pattern, str, opts);
+
+ if (rendered != null) {
+ prev[prev.length] = {
+ string: rendered.rendered,
+ score: rendered.score,
+ index: idx,
+ original: element
+ };
+ }
+
+ return prev
+ }, [])
+
+ .sort((a, b) => {
+ let compare = b.score - a.score;
+ if (compare) return compare
+ return a.index - b.index
+ })
+ }
+}
+
+class Tribute {
+ constructor({
+ values = null,
+ loadingItemTemplate = null,
+ iframe = null,
+ selectClass = "highlight",
+ containerClass = "tribute-container",
+ itemClass = "",
+ trigger = "@",
+ autocompleteMode = false,
+ autocompleteSeparator = null,
+ selectTemplate = null,
+ menuItemTemplate = null,
+ lookup = "key",
+ fillAttr = "value",
+ collection = null,
+ menuContainer = null,
+ noMatchTemplate = null,
+ requireLeadingSpace = true,
+ allowSpaces = false,
+ replaceTextSuffix = null,
+ positionMenu = true,
+ spaceSelectsMatch = false,
+ searchOpts = {},
+ menuItemLimit = null,
+ menuShowMinLength = 0
+ }) {
+ this.autocompleteMode = autocompleteMode;
+ this.autocompleteSeparator = autocompleteSeparator;
+ this.menuSelected = 0;
+ this.current = {};
+ this.inputEvent = false;
+ this.isActive = false;
+ this.menuContainer = menuContainer;
+ this.allowSpaces = allowSpaces;
+ this.replaceTextSuffix = replaceTextSuffix;
+ this.positionMenu = positionMenu;
+ this.hasTrailingSpace = false;
+ this.spaceSelectsMatch = spaceSelectsMatch;
+
+ if (this.autocompleteMode) {
+ trigger = "";
+ allowSpaces = false;
+ }
+
+ if (values) {
+ this.collection = [
+ {
+ // symbol that starts the lookup
+ trigger: trigger,
+
+ // is it wrapped in an iframe
+ iframe: iframe,
+
+ // class applied to selected item
+ selectClass: selectClass,
+
+ // class applied to the Container
+ containerClass: containerClass,
+
+ // class applied to each item
+ itemClass: itemClass,
+
+ // function called on select that retuns the content to insert
+ selectTemplate: (
+ selectTemplate || Tribute.defaultSelectTemplate
+ ).bind(this),
+
+ // function called that returns content for an item
+ menuItemTemplate: (
+ menuItemTemplate || Tribute.defaultMenuItemTemplate
+ ).bind(this),
+
+ // function called when menu is empty, disables hiding of menu.
+ noMatchTemplate: (t => {
+ if (typeof t === "string") {
+ if (t.trim() === "") return null;
+ return t;
+ }
+ if (typeof t === "function") {
+ return t.bind(this);
+ }
+
+ return (
+ noMatchTemplate ||
+ function() {
+ return " No Match Found! ";
+ }.bind(this)
+ );
+ })(noMatchTemplate),
+
+ // column to search against in the object
+ lookup: lookup,
+
+ // column that contains the content to insert by default
+ fillAttr: fillAttr,
+
+ // array of objects or a function returning an array of objects
+ values: values,
+
+ // useful for when values is an async function
+ loadingItemTemplate: loadingItemTemplate,
+
+ requireLeadingSpace: requireLeadingSpace,
+
+ searchOpts: searchOpts,
+
+ menuItemLimit: menuItemLimit,
+
+ menuShowMinLength: menuShowMinLength
+ }
+ ];
+ } else if (collection) {
+ if (this.autocompleteMode)
+ console.warn(
+ "Tribute in autocomplete mode does not work for collections"
+ );
+ this.collection = collection.map(item => {
+ return {
+ trigger: item.trigger || trigger,
+ iframe: item.iframe || iframe,
+ selectClass: item.selectClass || selectClass,
+ containerClass: item.containerClass || containerClass,
+ itemClass: item.itemClass || itemClass,
+ selectTemplate: (
+ item.selectTemplate || Tribute.defaultSelectTemplate
+ ).bind(this),
+ menuItemTemplate: (
+ item.menuItemTemplate || Tribute.defaultMenuItemTemplate
+ ).bind(this),
+ // function called when menu is empty, disables hiding of menu.
+ noMatchTemplate: (t => {
+ if (typeof t === "string") {
+ if (t.trim() === "") return null;
+ return t;
+ }
+ if (typeof t === "function") {
+ return t.bind(this);
+ }
+
+ return (
+ noMatchTemplate ||
+ function() {
+ return "
No Match Found! ";
+ }.bind(this)
+ );
+ })(noMatchTemplate),
+ lookup: item.lookup || lookup,
+ fillAttr: item.fillAttr || fillAttr,
+ values: item.values,
+ loadingItemTemplate: item.loadingItemTemplate,
+ requireLeadingSpace: item.requireLeadingSpace,
+ searchOpts: item.searchOpts || searchOpts,
+ menuItemLimit: item.menuItemLimit || menuItemLimit,
+ menuShowMinLength: item.menuShowMinLength || menuShowMinLength
+ };
+ });
+ } else {
+ throw new Error("[Tribute] No collection specified.");
+ }
+
+ new TributeRange(this);
+ new TributeEvents(this);
+ new TributeMenuEvents(this);
+ new TributeSearch(this);
+ }
+
+ get isActive() {
+ return this._isActive;
+ }
+
+ set isActive(val) {
+ if (this._isActive != val) {
+ this._isActive = val;
+ if (this.current.element) {
+ let noMatchEvent = new CustomEvent(`tribute-active-${val}`);
+ this.current.element.dispatchEvent(noMatchEvent);
+ }
+ }
+ }
+
+ static defaultSelectTemplate(item) {
+ if (typeof item === "undefined")
+ return `${this.current.collection.trigger}${this.current.mentionText}`;
+ if (this.range.isContentEditable(this.current.element)) {
+ return (
+ '
' +
+ (this.current.collection.trigger +
+ item.original[this.current.collection.fillAttr]) +
+ " "
+ );
+ }
+
+ return (
+ this.current.collection.trigger +
+ item.original[this.current.collection.fillAttr]
+ );
+ }
+
+ static defaultMenuItemTemplate(matchItem) {
+ return matchItem.string;
+ }
+
+ static inputTypes() {
+ return ["TEXTAREA", "INPUT"];
+ }
+
+ triggers() {
+ return this.collection.map(config => {
+ return config.trigger;
+ });
+ }
+
+ attach(el) {
+ if (!el) {
+ throw new Error("[Tribute] Must pass in a DOM node or NodeList.");
+ }
+
+ // Check if it is a jQuery collection
+ if (typeof jQuery !== "undefined" && el instanceof jQuery) {
+ el = el.get();
+ }
+
+ // Is el an Array/Array-like object?
+ if (
+ el.constructor === NodeList ||
+ el.constructor === HTMLCollection ||
+ el.constructor === Array
+ ) {
+ let length = el.length;
+ for (var i = 0; i < length; ++i) {
+ this._attach(el[i]);
+ }
+ } else {
+ this._attach(el);
+ }
+ }
+
+ _attach(el) {
+ if (el.hasAttribute("data-tribute")) {
+ console.warn("Tribute was already bound to " + el.nodeName);
+ }
+
+ this.ensureEditable(el);
+ this.events.bind(el);
+ el.setAttribute("data-tribute", true);
+ }
+
+ ensureEditable(element) {
+ if (Tribute.inputTypes().indexOf(element.nodeName) === -1) {
+ if (element.contentEditable) {
+ element.contentEditable = true;
+ } else {
+ throw new Error("[Tribute] Cannot bind to " + element.nodeName);
+ }
+ }
+ }
+
+ createMenu(containerClass) {
+ let wrapper = this.range.getDocument().createElement("div"),
+ ul = this.range.getDocument().createElement("ul");
+ wrapper.className = containerClass;
+ wrapper.appendChild(ul);
+
+ if (this.menuContainer) {
+ return this.menuContainer.appendChild(wrapper);
+ }
+
+ return this.range.getDocument().body.appendChild(wrapper);
+ }
+
+ showMenuFor(element, scrollTo) {
+ // Only proceed if menu isn't already shown for the current element & mentionText
+ if (
+ this.isActive &&
+ this.current.element === element &&
+ this.current.mentionText === this.currentMentionTextSnapshot
+ ) {
+ return;
+ }
+ this.currentMentionTextSnapshot = this.current.mentionText;
+
+ // create the menu if it doesn't exist.
+ if (!this.menu) {
+ this.menu = this.createMenu(this.current.collection.containerClass);
+ element.tributeMenu = this.menu;
+ this.menuEvents.bind(this.menu);
+ }
+
+ this.isActive = true;
+ this.menuSelected = 0;
+
+ if (!this.current.mentionText) {
+ this.current.mentionText = "";
+ }
+
+ const processValues = values => {
+ // Tribute may not be active any more by the time the value callback returns
+ if (!this.isActive) {
+ return;
+ }
+
+ let items = this.search.filter(this.current.mentionText, values, {
+ pre: this.current.collection.searchOpts.pre || "
",
+ post: this.current.collection.searchOpts.post || " ",
+ skip: this.current.collection.searchOpts.skip,
+ extract: el => {
+ if (typeof this.current.collection.lookup === "string") {
+ return el[this.current.collection.lookup];
+ } else if (typeof this.current.collection.lookup === "function") {
+ return this.current.collection.lookup(el, this.current.mentionText);
+ } else {
+ throw new Error(
+ "Invalid lookup attribute, lookup must be string or function."
+ );
+ }
+ }
+ });
+
+ if (this.current.collection.menuItemLimit) {
+ items = items.slice(0, this.current.collection.menuItemLimit);
+ }
+
+ this.current.filteredItems = items;
+
+ let ul = this.menu.querySelector("ul");
+
+ if (!items.length) {
+ let noMatchEvent = new CustomEvent("tribute-no-match", {
+ detail: this.menu
+ });
+ this.current.element.dispatchEvent(noMatchEvent);
+ if (
+ (typeof this.current.collection.noMatchTemplate === "function" &&
+ !this.current.collection.noMatchTemplate()) ||
+ !this.current.collection.noMatchTemplate
+ ) {
+ this.hideMenu();
+ } else {
+ typeof this.current.collection.noMatchTemplate === "function"
+ ? (ul.innerHTML = this.current.collection.noMatchTemplate())
+ : (ul.innerHTML = this.current.collection.noMatchTemplate);
+ this.range.positionMenuAtCaret(scrollTo);
+ }
+
+ return;
+ }
+
+ ul.innerHTML = "";
+ let fragment = this.range.getDocument().createDocumentFragment();
+
+ items.forEach((item, index) => {
+ let li = this.range.getDocument().createElement("li");
+ li.setAttribute("data-index", index);
+ li.className = this.current.collection.itemClass;
+ li.addEventListener("mousemove", e => {
+ let [li, index] = this._findLiTarget(e.target);
+ if (e.movementY !== 0) {
+ this.events.setActiveLi(index);
+ }
+ });
+ if (this.menuSelected === index) {
+ li.classList.add(this.current.collection.selectClass);
+ }
+ li.innerHTML = this.current.collection.menuItemTemplate(item);
+ fragment.appendChild(li);
+ });
+ ul.appendChild(fragment);
+
+ this.range.positionMenuAtCaret(scrollTo);
+ };
+
+ if (typeof this.current.collection.values === "function") {
+ if (this.current.collection.loadingItemTemplate) {
+ this.menu.querySelector("ul").innerHTML = this.current.collection.loadingItemTemplate;
+ this.range.positionMenuAtCaret(scrollTo);
+ }
+
+ this.current.collection.values(this.current.mentionText, processValues);
+ } else {
+ processValues(this.current.collection.values);
+ }
+ }
+
+ _findLiTarget(el) {
+ if (!el) return [];
+ const index = el.getAttribute("data-index");
+ return !index ? this._findLiTarget(el.parentNode) : [el, index];
+ }
+
+ showMenuForCollection(element, collectionIndex) {
+ if (element !== document.activeElement) {
+ this.placeCaretAtEnd(element);
+ }
+
+ this.current.collection = this.collection[collectionIndex || 0];
+ this.current.externalTrigger = true;
+ this.current.element = element;
+
+ if (element.isContentEditable)
+ this.insertTextAtCursor(this.current.collection.trigger);
+ else this.insertAtCaret(element, this.current.collection.trigger);
+
+ this.showMenuFor(element);
+ }
+
+ // TODO: make sure this works for inputs/textareas
+ placeCaretAtEnd(el) {
+ el.focus();
+ if (
+ typeof window.getSelection != "undefined" &&
+ typeof document.createRange != "undefined"
+ ) {
+ var range = document.createRange();
+ range.selectNodeContents(el);
+ range.collapse(false);
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+ } else if (typeof document.body.createTextRange != "undefined") {
+ var textRange = document.body.createTextRange();
+ textRange.moveToElementText(el);
+ textRange.collapse(false);
+ textRange.select();
+ }
+ }
+
+ // for contenteditable
+ insertTextAtCursor(text) {
+ var sel, range;
+ sel = window.getSelection();
+ range = sel.getRangeAt(0);
+ range.deleteContents();
+ var textNode = document.createTextNode(text);
+ range.insertNode(textNode);
+ range.selectNodeContents(textNode);
+ range.collapse(false);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+
+ // for regular inputs
+ insertAtCaret(textarea, text) {
+ var scrollPos = textarea.scrollTop;
+ var caretPos = textarea.selectionStart;
+
+ var front = textarea.value.substring(0, caretPos);
+ var back = textarea.value.substring(
+ textarea.selectionEnd,
+ textarea.value.length
+ );
+ textarea.value = front + text + back;
+ caretPos = caretPos + text.length;
+ textarea.selectionStart = caretPos;
+ textarea.selectionEnd = caretPos;
+ textarea.focus();
+ textarea.scrollTop = scrollPos;
+ }
+
+ hideMenu() {
+ if (this.menu) {
+ this.menu.style.cssText = "display: none;";
+ this.isActive = false;
+ this.menuSelected = 0;
+ this.current = {};
+ }
+ }
+
+ selectItemAtIndex(index, originalEvent) {
+ index = parseInt(index);
+ if (typeof index !== "number" || isNaN(index)) return;
+ let item = this.current.filteredItems[index];
+ let content = this.current.collection.selectTemplate(item);
+ if (content !== null) this.replaceText(content, originalEvent, item);
+ }
+
+ replaceText(content, originalEvent, item) {
+ this.range.replaceTriggerText(content, true, true, originalEvent, item);
+ }
+
+ _append(collection, newValues, replace) {
+ if (typeof collection.values === "function") {
+ throw new Error("Unable to append to values, as it is a function.");
+ } else if (!replace) {
+ collection.values = collection.values.concat(newValues);
+ } else {
+ collection.values = newValues;
+ }
+ }
+
+ append(collectionIndex, newValues, replace) {
+ let index = parseInt(collectionIndex);
+ if (typeof index !== "number")
+ throw new Error("please provide an index for the collection to update.");
+
+ let collection = this.collection[index];
+
+ this._append(collection, newValues, replace);
+ }
+
+ appendCurrent(newValues, replace) {
+ if (this.isActive) {
+ this._append(this.current.collection, newValues, replace);
+ } else {
+ throw new Error(
+ "No active state. Please use append instead and pass an index."
+ );
+ }
+ }
+
+ detach(el) {
+ if (!el) {
+ throw new Error("[Tribute] Must pass in a DOM node or NodeList.");
+ }
+
+ // Check if it is a jQuery collection
+ if (typeof jQuery !== "undefined" && el instanceof jQuery) {
+ el = el.get();
+ }
+
+ // Is el an Array/Array-like object?
+ if (
+ el.constructor === NodeList ||
+ el.constructor === HTMLCollection ||
+ el.constructor === Array
+ ) {
+ let length = el.length;
+ for (var i = 0; i < length; ++i) {
+ this._detach(el[i]);
+ }
+ } else {
+ this._detach(el);
+ }
+ }
+
+ _detach(el) {
+ this.events.unbind(el);
+ if (el.tributeMenu) {
+ this.menuEvents.unbind(el.tributeMenu);
+ }
+
+ setTimeout(() => {
+ el.removeAttribute("data-tribute");
+ this.isActive = false;
+ if (el.tributeMenu) {
+ el.tributeMenu.remove();
+ }
+ });
+ }
+}
+
+/**
+ * Tribute.js
+ * Native ES6 JavaScript @mention Plugin
+ **/
+
+export default Tribute;