quill-mention
Version:
@mentions for the Quill rich text editor
2 lines (1 loc) • 15.2 kB
JavaScript
!function(t){"use strict";const e=t.import("blots/embed");class i extends Event{constructor(t,e){super(t,e),this.value={},this.event=new Event(t)}}class n extends e{constructor(t,e){super(t,e),this.mounted=!1}static create(t){const e=super.create();if(!function(t){return"object"==typeof t&&null!==t&&"value"in t&&"string"==typeof t.value&&"denotationChar"in t&&"string"==typeof t.denotationChar}(t)||e instanceof HTMLElement==!1)return e;const i=document.createElement("span");if(i.className="ql-mention-denotation-char",i.innerText=t.denotationChar,e.appendChild(i),"function"==typeof this.render)e.appendChild(this.render(t));else{const i=document.createElement("span");i.className="ql-mention-value",i.innerText=t.value,e.appendChild(i)}return n.setDataValues(e,t)}static setDataValues(t,e){const i=t;return Object.keys(e).forEach((t=>{i.dataset[t]=e[t]})),i}static value(t){return t.dataset}attach(){super.attach(),this.mounted||(this.mounted=!0,this.clickHandler=this.getClickHandler(),this.hoverHandler=this.getHoverHandler(),this.domNode.addEventListener("click",this.clickHandler,!1),this.domNode.addEventListener("mouseenter",this.hoverHandler,!1))}detach(){super.detach(),this.mounted=!1,this.clickHandler&&(this.domNode.removeEventListener("click",this.clickHandler),this.clickHandler=void 0)}getClickHandler(){return t=>{const e=this.buildEvent("mention-clicked",t);window.dispatchEvent(e),t.preventDefault()}}getHoverHandler(){return t=>{const e=this.buildEvent("mention-hovered",t);window.dispatchEvent(e),t.preventDefault()}}buildEvent(t,e){const n=new i(t,{bubbles:!0,cancelable:!0});return n.value=Object.assign({},this.domNode.dataset),n.event=e,n}}n.blotName="mention",n.tagName="span",n.className="mention";const s="Tab",o="Enter",h="Escape",a="ArrowUp",r="ArrowDown";function l(t,e,i){const n=t;return Object.keys(e).forEach((t=>{i.indexOf(t)>-1?n.dataset[t]=e[t]:delete n.dataset[t]})),n}function d(t,e){null!==e&&("object"==typeof e?t.appendChild(e):t.innerText=e)}const m=t.import("core/module");class c extends m{constructor(t,e){super(t,e),this.isOpen=!1,this.itemIndex=0,this.values=[],this.suspendMouseEnter=!1,Array.isArray(e?.dataAttributes)&&(this.options.dataAttributes=this.options.dataAttributes?this.options.dataAttributes.concat(e.dataAttributes):e.dataAttributes);for(let t in this.options){const e=t,i=this.options[e];"function"==typeof i&&(this.options[e]=i.bind(this))}this.mentionContainer=document.createElement("div"),this.mentionContainer.className=this.options.mentionContainerClass?this.options.mentionContainerClass:"",this.mentionContainer.style.cssText="display: none; position: absolute;",this.mentionContainer.onmousemove=this.onContainerMouseMove.bind(this),this.options.fixMentionsToQuill&&(this.mentionContainer.style.width="auto"),this.mentionList=document.createElement("ul"),this.mentionList.id="quill-mention-list",t.root.setAttribute("aria-owns","quill-mention-list"),this.mentionList.className=this.options.mentionListClass?this.options.mentionListClass:"",this.mentionContainer.appendChild(this.mentionList),t.on("text-change",this.onTextChange.bind(this)),t.on("selection-change",this.onSelectionChange.bind(this)),t.container.addEventListener("paste",(()=>{setTimeout((()=>{const e=t.getSelection();this.onSelectionChange(e)}))})),t.keyboard.addBinding({key:s},this.selectHandler.bind(this)),t.keyboard.bindings[s].unshift(t.keyboard.bindings[s].pop());for(let e of this.options.selectKeys??[])t.keyboard.addBinding({key:e},this.selectHandler.bind(this));t.keyboard.bindings[o].unshift(t.keyboard.bindings[o].pop()),t.keyboard.addBinding({key:h},this.escapeHandler.bind(this)),t.keyboard.addBinding({key:a},this.upHandler.bind(this)),t.keyboard.addBinding({key:r},this.downHandler.bind(this))}selectHandler(){return!(this.isOpen&&!this.existingSourceExecutionToken)||(this.selectItem(),!1)}escapeHandler(){return!this.isOpen||(this.existingSourceExecutionToken&&(this.existingSourceExecutionToken.abandoned=!0),this.hideMentionList(),!1)}upHandler(){return!(this.isOpen&&!this.existingSourceExecutionToken)||(this.prevItem(),!1)}downHandler(){return!(this.isOpen&&!this.existingSourceExecutionToken)||(this.nextItem(),!1)}showMentionList(){"fixed"===this.options.positioningStrategy?document.body.appendChild(this.mentionContainer):this.quill.container.appendChild(this.mentionContainer),this.mentionContainer.style.visibility="hidden",this.mentionContainer.style.display="",this.mentionContainer.scrollTop=0,this.setMentionContainerPosition(),this.setIsOpen(!0)}hideMentionList(){this.options.onBeforeClose&&this.options.onBeforeClose(),this.mentionContainer.style.display="none",this.mentionContainer.remove(),this.setIsOpen(!1),this.quill.root.removeAttribute("aria-activedescendant")}highlightItem(t=!0){for(let t=0;t<this.mentionList.childNodes.length;t+=1){const e=this.mentionList.childNodes[t];e instanceof HTMLElement&&e.classList.remove("selected")}const e=this.mentionList.childNodes[this.itemIndex];if(-1!==this.itemIndex&&"true"!==e.dataset.disabled&&(e.classList.add("selected"),this.quill.root.setAttribute("aria-activedescendant",e.id),t)){const t=e.offsetHeight,i=e.offsetTop,n=this.mentionContainer.scrollTop,s=n+this.mentionContainer.offsetHeight;i<n?this.mentionContainer.scrollTop=i:i>s-t&&(this.mentionContainer.scrollTop+=i-s+t)}}onContainerMouseMove(){this.suspendMouseEnter=!1}selectItem(){if(-1===this.itemIndex)return;const t=this.mentionList.childNodes[this.itemIndex].dataset;t.disabled||(this.options.onSelect?.(t,((t,e=!1,i={})=>this.insertItem(t,e,i))),this.hideMentionList())}insertItem(e,i,n={}){const s=e;if(null===s||void 0===this.mentionCharPos||void 0===this.cursorPos)return;const o={...this.options,...n};let h;o.showDenotationChar||(s.denotationChar=""),i?h=this.cursorPos:(h=this.mentionCharPos,this.quill.deleteText(this.mentionCharPos,this.cursorPos-this.mentionCharPos,t.sources.USER));const a=this.quill.insertEmbed(h,o.blotName??c.DEFAULTS.blotName,s,t.sources.USER);return o.spaceAfterInsert?(this.quill.insertText(h+1," ",t.sources.USER),this.quill.setSelection(h+2,t.sources.USER)):this.quill.setSelection(h+1,t.sources.USER),this.hideMentionList(),a}onItemMouseEnter(t){if(this.suspendMouseEnter||t.target instanceof HTMLElement==!1)return;const e=Number(t.target?.dataset.index);Number.isNaN(e)||e===this.itemIndex||(this.itemIndex=e,this.highlightItem(!1))}onDisabledItemMouseEnter(){this.suspendMouseEnter||(this.itemIndex=-1,this.highlightItem(!1))}onItemClick(t){t.preventDefault(),t.stopImmediatePropagation(),t.currentTarget instanceof HTMLElement!=!1&&(this.itemIndex=t.currentTarget?.dataset.index?Number.parseInt(t.currentTarget.dataset.index):-1,this.highlightItem(),this.selectItem())}onItemMouseDown(t){t.preventDefault(),t.stopImmediatePropagation()}renderLoading(){const t=this.options.renderLoading?.()??void 0;if(void 0===t)return;if(this.mentionContainer.getElementsByClassName("ql-mention-loading").length>0)return void this.showMentionList();this.mentionList.innerHTML="";const e=document.createElement("div");e.className="ql-mention-loading",d(e,t),this.mentionContainer.append(e),this.showMentionList()}removeLoading(){const t=this.mentionContainer.getElementsByClassName("ql-mention-loading");t.length>0&&t[0].remove()}renderList(t,e,i){if(e&&e.length>0){this.removeLoading(),this.values=e,this.mentionList.innerText="";let n=-1;for(let s=0;s<e.length;s+=1){const o=document.createElement("li");o.id="quill-mention-item-"+s,o.className=this.options.listItemClass?this.options.listItemClass:"",e[s].disabled?(o.className+=" disabled",o.setAttribute("aria-hidden","true")):-1===n&&(n=s),o.dataset.index=s.toString();d(o,this.options.renderItem(e[s],i)),e[s].disabled?o.onmouseenter=this.onDisabledItemMouseEnter.bind(this):(o.onmouseenter=this.onItemMouseEnter.bind(this),o.onmouseup=this.onItemClick.bind(this),o.onmousedown=this.onItemMouseDown.bind(this)),o.dataset.denotationChar=t,this.mentionList.appendChild(l(o,e[s],this.options.dataAttributes))}this.itemIndex=n,this.highlightItem(),this.showMentionList()}else this.hideMentionList()}nextItem(){let t,e,i=0;do{if(i++,t=(this.itemIndex+i)%this.values.length,e="true"===this.mentionList.childNodes[t].dataset.disabled,i===this.values.length+1){t=-1;break}}while(e);this.itemIndex=t,this.suspendMouseEnter=!0,this.highlightItem()}prevItem(){let t,e,i=0;do{if(i++,t=(this.itemIndex+this.values.length-i)%this.values.length,e="true"===this.mentionList.childNodes[t].dataset.disabled,i===this.values.length+1){t=-1;break}}while(e);this.itemIndex=t,this.suspendMouseEnter=!0,this.highlightItem()}containerBottomIsNotVisible(t,e){return t+this.mentionContainer.offsetHeight+e.top>window.scrollY+window.innerHeight}containerRightIsNotVisible(t,e){if(this.options.fixMentionsToQuill)return!1;return t+this.mentionContainer.offsetWidth+e.left>window.scrollX+document.documentElement.clientWidth}setIsOpen(t){this.isOpen!==t&&(t?this.options.onOpen?.():this.options.onClose?.(),this.isOpen=t)}setMentionContainerPosition(){"fixed"===this.options.positioningStrategy?this.setMentionContainerPosition_Fixed():this.setMentionContainerPosition_Normal()}setMentionContainerPosition_Normal(){if(void 0===this.mentionCharPos)return;const t=this.quill.container.getBoundingClientRect(),e=this.quill.getBounds(this.mentionCharPos);if(null===e)return;const i=this.mentionContainer.offsetHeight;let n=this.options.offsetTop,s=this.options.offsetLeft;if(this.options.fixMentionsToQuill){const t=0;this.mentionContainer.style.right=`${t}px`}else s+=e.left;if(this.containerRightIsNotVisible(s,t)){const e=this.mentionContainer.offsetWidth+this.options.offsetLeft;s=t.width-e}if("top"===this.options.defaultMenuOrientation){if(n=this.options.fixMentionsToQuill?-1*(i+this.options.offsetTop):e.top-(i+this.options.offsetTop),n+t.top<=0){let i=this.options.offsetTop;this.options.fixMentionsToQuill?i+=t.height:i+=e.bottom,n=i}}else if(this.options.fixMentionsToQuill?n+=t.height:n+=e.bottom,this.containerBottomIsNotVisible(n,t)){let t=-1*this.options.offsetTop;this.options.fixMentionsToQuill||(t+=e.top),n=t-i}n>=0?this.options.mentionContainerClass?.split(" ").forEach((t=>{this.mentionContainer.classList.add(`${t}-bottom`),this.mentionContainer.classList.remove(`${t}-top`)})):this.options.mentionContainerClass?.split(" ").forEach((t=>{this.mentionContainer.classList.add(`${t}-top`),this.mentionContainer.classList.remove(`${t}-bottom`)})),this.mentionContainer.style.top=`${n}px`,this.mentionContainer.style.left=`${s}px`,this.mentionContainer.style.visibility="visible"}setMentionContainerPosition_Fixed(){if(void 0===this.mentionCharPos)return;this.mentionContainer.style.position="fixed",this.mentionContainer.style.height="";const t=this.quill.container.getBoundingClientRect(),e=this.quill.getBounds(this.mentionCharPos);if(null===e)return;const i={right:t.right-e.right,left:t.left+e.left,top:t.top+e.top,width:0,height:e.height},n=this.options.fixMentionsToQuill?t:i;let s=this.options.offsetTop,o=this.options.offsetLeft;if(this.options.fixMentionsToQuill){const t=n.right;this.mentionContainer.style.right=`${t}px`}else o+=n.left,o+this.mentionContainer.offsetWidth>document.documentElement.clientWidth&&(o-=o+this.mentionContainer.offsetWidth-document.documentElement.clientWidth);const h=n.top,a=document.documentElement.clientHeight-(n.top+n.height),r=this.mentionContainer.offsetHeight<=a,l=this.mentionContainer.offsetHeight<=h;let d;d="top"===this.options.defaultMenuOrientation&&l?"top":"bottom"===this.options.defaultMenuOrientation&&r||a>h?"bottom":"top","bottom"===d?(s=n.top+n.height,r||(this.mentionContainer.style.height=a-3+"px"),this.options.mentionContainerClass?.split(" ").forEach((t=>{this.mentionContainer.classList.add(`${t}-bottom`),this.mentionContainer.classList.remove(`${t}-top`)}))):(s=n.top-this.mentionContainer.offsetHeight,l||(this.mentionContainer.style.height=h-3+"px",s=3),this.options.mentionContainerClass?.split(" ").forEach((t=>{this.mentionContainer.classList.add(`${t}-top`),this.mentionContainer.classList.remove(`${t}-bottom`)}))),this.mentionContainer.style.top=`${s}px`,this.mentionContainer.style.left=`${o}px`,this.mentionContainer.style.visibility="visible"}getTextBeforeCursor(){const t=Math.max(0,(this.cursorPos??0)-this.options.maxChars);return this.quill.getText(t,(this.cursorPos??0)-t)}onSomethingChange(){const t=this.quill.getSelection();if(null==t)return;this.cursorPos=t.index;const e=this.getTextBeforeCursor(),i=Math.max(0,this.cursorPos-this.options.maxChars),n=i?this.quill.getText(i-1,i):"",{mentionChar:s,mentionCharIndex:o}=(h=e,a=this.options.mentionDenotationChars,r=this.options.isolateCharacter,l=this.options.allowInlineMentionChar,a.reduce(((t,e)=>{let i;if(r&&l){const n=new RegExp(`^${e}|\\s${e}`,"g"),s=(h.match(n)||[]).pop();if(!s)return{mentionChar:t.mentionChar,mentionCharIndex:t.mentionCharIndex};i=s!==e?h.lastIndexOf(s)+s.length-e.length:0}else i=h.lastIndexOf(e);return i>t.mentionCharIndex?{mentionChar:e,mentionCharIndex:i}:{mentionChar:t.mentionChar,mentionCharIndex:t.mentionCharIndex}}),{mentionChar:null,mentionCharIndex:-1}));var h,a,r,l;if(null!==s&&function(t,e,i,n){if(-1===t)return!1;if(!i)return!0;const s=t?e[t-1]:n;return!s||!!s.match(/\s/)}(o,e,this.options.isolateCharacter,n)){const t=this.cursorPos-(e.length-o);this.mentionCharPos=t;const i=e.substring(o+s.length);if(i.length>=this.options.minChars&&function(t,e){return e.test(t)}(i,this.getAllowedCharsRegex(s))){this.existingSourceExecutionToken&&(this.existingSourceExecutionToken.abandoned=!0),this.renderLoading();const t={abandoned:!1};this.existingSourceExecutionToken=t,this.options.source?.(i,((e,i)=>{t.abandoned||(this.existingSourceExecutionToken=void 0,this.renderList(s,e,i))}),s)}else this.existingSourceExecutionToken&&(this.existingSourceExecutionToken.abandoned=!0),this.hideMentionList()}else this.existingSourceExecutionToken&&(this.existingSourceExecutionToken.abandoned=!0),this.hideMentionList()}getAllowedCharsRegex(t){return this.options.allowedChars instanceof RegExp?this.options.allowedChars:this.options.allowedChars?.(t)??/^[a-zA-Z0-9_]*$/}onTextChange(t,e,i){"user"===i&&setTimeout(this.onSomethingChange.bind(this),50)}onSelectionChange(t){null!==t&&0===t.length?this.onSomethingChange():this.hideMentionList()}openMenu(t){const e=this.quill.getSelection(!0);this.quill.insertText(e.index,t),this.quill.blur(),this.quill.focus()}}c.DEFAULTS={mentionDenotationChars:["@"],showDenotationChar:!0,allowedChars:/^[a-zA-Z0-9_]*$/,minChars:0,maxChars:31,offsetTop:2,offsetLeft:0,isolateCharacter:!1,allowInlineMentionChar:!1,fixMentionsToQuill:!1,positioningStrategy:"normal",defaultMenuOrientation:"bottom",blotName:"mention",dataAttributes:["id","value","denotationChar","link","target","disabled"],linkTarget:"_blank",listItemClass:"ql-mention-list-item",mentionContainerClass:"ql-mention-list-container",mentionListClass:"ql-mention-list",spaceAfterInsert:!0,selectKeys:[o],source:(t,e,i)=>{e([],t)},renderItem:({value:t})=>`${t}`,onSelect:(t,e)=>e(t),onOpen:()=>!0,onBeforeClose:()=>!0,onClose:()=>!0,renderLoading:()=>null},t.register({"blots/mention":n,"modules/mention":c})}(Quill);