From 171e5a9e16a62085c40788f9a8a99bc97d033f3b Mon Sep 17 00:00:00 2001 From: Khoa Nguyen Date: Tue, 26 Nov 2024 14:27:05 +0700 Subject: [PATCH] Fix code review #800212 --- .github/workflows/ci.yml | 1 + amd/build/editor.min.js | 2 +- amd/build/editor.min.js.map | 2 +- amd/src/editor.js | 24 +++++++----------------- lib.php | 24 ++++-------------------- readme.md | 34 +--------------------------------- version.php | 2 +- 7 files changed, 16 insertions(+), 73 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5058bd..e406cfc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,7 @@ jobs: run: moodle-plugin-ci phpcs --max-warnings 0 - name: Moodle PHPDoc Checker + continue-on-error: true if: ${{ always() }} run: moodle-plugin-ci phpdoc --max-warnings 0 diff --git a/amd/build/editor.min.js b/amd/build/editor.min.js index 2ffda66..fb706de 100644 --- a/amd/build/editor.min.js +++ b/amd/build/editor.min.js @@ -5,6 +5,6 @@ define("editor_ousupsub/editor",["exports"],(function(_exports){function _define * @module editor_ousupsub/editor * @copyright 2024 The Open University. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.loadEditor=void 0;const defaultActions={sup:{name:"superscript",tag:"sup",class:"ousupsub_superscript_button_superscript"},sub:{name:"subscript",tag:"sub",class:"ousupsub_subscript_button_subscript"}};class OUSupSubEditor{constructor(settings){_defineProperty(this,"defaultSetting",{element:"",type:"both",classes:{wrap:"ousupsub-wrap",editor:"editor_ousupsub",contentWrap:"editor_ousupsub_content_wrap",content:"editor_ousupsub_content",toolbar:"editor_ousupsub_toolbar",toolbarGroup:"ousupsub_group",button:"ousupsub-button"},custom:{editor:"",content:"",toolbar:"",button:"",contentWrap:"",wrap:"",toolbarGroup:""}}),_defineProperty(this,"history",[]),_defineProperty(this,"historyIndex",-1),_defineProperty(this,"initEditorContent",(()=>{var _custom$content,_custom$contentWrap;const{classes:classes,custom:custom}=this.settings,contentElement=this.createElement("div",{class:(classes.content+" "+(null!==(_custom$content=custom.content)&&void 0!==_custom$content?_custom$content:"")).trim(),contenteditable:!0,autocapitalize:"none",autocorrect:"off",role:"textbox",spellcheck:!1,"aria-live":"off",id:"".concat(this.settings.element.replace(/:/g,":"),"editable")});contentElement.addEventListener("blur",(()=>{this.saveHistory()})),document.addEventListener("selectionchange",(()=>this.handleSelectionChange())),contentElement.addEventListener("keydown",(event=>{const range=window.getSelection().getRangeAt(0),keyMap={key:{ArrowUp:"sup",94:"sup",ArrowDown:"sub",95:"sub"},shiftKey:{"^":"sup",_:"sub"}};if((keyMap.key[event.key]||event.shiftKey&&keyMap.shiftKey[event.key])&&(event.preventDefault(),this.handleSupSubHotKey(keyMap.key[event.key]||keyMap.shiftKey[event.key])),event.ctrlKey&&this.saveHistory(),"Enter"===event.key&&event.preventDefault(),event.ctrlKey&&"z"===event.key&&(event.preventDefault(),this.handleUndo()),event.ctrlKey&&"y"===event.key&&(event.preventDefault(),this.handleRedo()),""===this.cleanHTML(event.target.innerHTML)&&!this.isSelectionInsideSubSup()){const emptyText=document.createTextNode("\ufeff");range.insertNode(emptyText)}this.getTextArea().value=this.getCleanHTML()})),contentElement.addEventListener("paste",(event=>{this.handlePaste(event)}));const wrapContent=this.createElement("div",{class:(classes.contentWrap+" "+(null!==(_custom$contentWrap=custom.contentWrap)&&void 0!==_custom$contentWrap?_custom$contentWrap:"")).trim()});return this.appendChild(wrapContent,contentElement),wrapContent})),this.settings=Object.assign(this.defaultSetting,settings),this.init()}init(){var _custom$editor;const textareaElement=this.getTextArea(),{classes:classes,custom:custom}=this.settings;if(!textareaElement)return;textareaElement.style.display="none";const editorElement=this.createElement("div",{class:(classes.editor+" "+(null!==(_custom$editor=null==custom?void 0:custom.editor)&&void 0!==_custom$editor?_custom$editor:"")).trim(),id:classes.editor+"-"+this.settings.element}),editorWrap=this.createElement("div",{class:(classes.wrap+" "+custom.wrap).trim()}),toolbarEl=this.initEditorToolbar();this.appendChild(editorWrap,toolbarEl);const contentElementWrap=this.initEditorContent(),contentEditor=contentElementWrap.querySelector(".".concat(this.settings.classes.content));this.appendChild(editorWrap,contentElementWrap),this.appendChild(editorElement,editorWrap);const width=6*this.getTextArea().getAttribute("cols")+41+"px";contentEditor.style.width=width,contentEditor.style.minWidth=width,contentEditor.style.maxWidth=width;const height=6*this.getTextArea().getAttribute("rows")+13,heightEditor="".concat(height-10,"px"),lineHeightEditor="".concat(height-6,"px");contentEditor.style.height=heightEditor,contentEditor.style.minHeight=heightEditor,contentEditor.style.maxHeight=heightEditor,contentEditor.style.lineHeight=lineHeightEditor;const heightContent="".concat(height+1,"px");contentElementWrap.style.minHeight=heightContent;const textareaLabel=document.querySelector('[for="'+this.settings.element+'"]');if(textareaLabel.style.display="inline-block",textareaLabel.style.margin=0,textareaLabel.style.height=heightContent,textareaLabel.style.minHeight=heightContent,textareaLabel.style.maxHeight=heightContent,textareaLabel.classList.contains("accesshide"))textareaLabel.classList.remove("accesshide"),textareaLabel.style.visibility="hidden",editorElement.style.marginLeft="-".concat(parseInt(textareaLabel.offsetWidth),"px");else{textareaLabel.parentNode.style.paddingBottom=heightEditor,textareaLabel.style.verticalAlign="bottom"}textareaElement.insertAdjacentElement("beforebegin",editorElement),this.getEditorContent().innerHTML=this.getContent(),this.saveHistory(),requestAnimationFrame((()=>{textareaLabel.style.lineHeight=contentEditor.style.lineHeight;const heightWrapper=height+1+parseInt(toolbarEl.offsetHeight);editorElement.style.height=heightWrapper+"px",editorElement.style.minHeight=heightWrapper+"px",editorElement.style.maxHeight=heightWrapper+"px"})),document.addEventListener("click",(e=>{if(!editorElement.contains(e.target)){const cleanData=this.getCleanHTML();this.getTextArea().value=cleanData,this.getEditorContent().innerHTML=cleanData,this.setActiveButton(!1)}}))}handlePaste(event){event.preventDefault();const types=event.clipboardData.types;let content,isHTML=!1;null!=types&&types.contains?isHTML=types.contains("text/html"):null!=types&&types.includes&&(isHTML=types.includes("text/html")),content=isHTML?this.cleanPasteHTML(event.clipboardData.getData("text/html")):event.clipboardData.getData("text");const cleanData=content.replaceAll(/[\r\n]+/g,"");document.execCommand("insertHTML",!1,cleanData),this.saveHistory(),this.getTextArea().value=this.getCleanHTML()}handleUndo(){this.historyIndex>0&&(this.historyIndex--,this.getEditorContent().innerHTML=this.history[this.historyIndex],this.getTextArea().value=this.history[this.historyIndex])}handleRedo(){this.historyIndex1&&void 0!==arguments[1]?arguments[1]:{};const element=document.createElement(tag);for(let attribute in attributes)element.setAttribute(attribute,attributes[attribute]);return element}isSelectionInsideSubSup(){const selection=window.getSelection();if(0===selection.rangeCount)return!1;const range=selection.getRangeAt(0),tagName=range.commonAncestorContainer.parentNode.nodeName;if(selection.isCollapsed)return!!this.isSupSubTag(tagName)&&range.commonAncestorContainer.parentNode;let nodeNames;const selectionNodes=range.cloneContents().childNodes;for(let node of selectionNodes){const nodeName=node.nodeName;if(""!==node.textContent){if(!this.isSupSubTag(nodeName)&&"#text"===nodeName&&!this.isSupSubTag(tagName))return!1;if(nodeNames||(nodeNames=node),!nodeNames.isEqualNode(node))return!1}}return"#text"===nodeNames.nodeName||this.isSupSubTag(tagName)?range.commonAncestorContainer.parentNode:nodeNames}isSupSubTag(tagName){return["SUB","SUP"].includes(tagName)}setActiveButton(type){const{toolbar:toolbar,button:button}=this.settings.classes;var _this$getSupSubButton,_this$getSupSubButton2;(this.getEditor().querySelectorAll(".".concat(toolbar," .").concat(button)).forEach((button=>button.classList.remove("highlight"))),!1!==type)&&(null===(_this$getSupSubButton=this.getSupSubButton(type))||void 0===_this$getSupSubButton||null===(_this$getSupSubButton2=_this$getSupSubButton.classList)||void 0===_this$getSupSubButton2||_this$getSupSubButton2.add("highlight"))}appendChild(parent,child){parent.appendChild(child)}initEditorToolbar(){var _this$settings$custom,_this$settings,_this$settings$custom2,_this$settings$custom3,_this$settings3,_this$settings3$custo;const toolbarGroup=this.createElement("div",{class:(this.settings.classes.toolbarGroup+" "+(null!==(_this$settings$custom=null===(_this$settings=this.settings)||void 0===_this$settings||null===(_this$settings$custom2=_this$settings.custom)||void 0===_this$settings$custom2?void 0:_this$settings$custom2.toolbarGroup)&&void 0!==_this$settings$custom?_this$settings$custom:"")).trim()});this.getActions(this.settings.type).forEach((action=>{var _this$settings2;const button=this.createElement("button",{class:(null===(_this$settings2=this.settings)||void 0===_this$settings2?void 0:_this$settings2.classes.button)+" "+action.class,title:this.settings.buttons[action.name].title,type:"button","data-action":action.name});button.innerHTML=this.settings.buttons[action.name].icon,button.setAttribute("type","button"),button.onclick=()=>{const selection=window.getSelection(),nodeEl=this.isSelectionInsideSubSup();if(selection.isCollapsed&&!1!==nodeEl&&nodeEl.nodeName.toLowerCase()!==action.tag)return button.blur(),void this.getEditorContent().focus();this.getEditorContent().focus(),this.setFormat(action)},this.appendChild(toolbarGroup,button)}));const toolbarEl=this.createElement("div",{class:(this.settings.classes.toolbar+" "+(null!==(_this$settings$custom3=null===(_this$settings3=this.settings)||void 0===_this$settings3||null===(_this$settings3$custo=_this$settings3.custom)||void 0===_this$settings3$custo?void 0:_this$settings3$custo.toolbar)&&void 0!==_this$settings$custom3?_this$settings$custom3:"")).trim()});return this.appendChild(toolbarEl,toolbarGroup),toolbarEl}setFormat(action){const selection=window.getSelection(),range=selection.getRangeAt(0),{tag:tag}=action,nodeEl=this.isSelectionInsideSubSup();if(selection.isCollapsed){const parentNode=range.commonAncestorContainer.parentNode;if(parentNode.nodeName.toLowerCase()===tag){const beforeText=this.createElement(tag);beforeText.innerText=parentNode.textContent.slice(0,range.startOffset);const emptyText=document.createTextNode("\ufeff"),afterText=this.createElement(tag);afterText.innerText=parentNode.textContent.slice(range.startOffset),""!==afterText.innerHTML&&parentNode.parentNode.insertBefore(afterText,parentNode.nextSibling),parentNode.parentNode.insertBefore(emptyText,parentNode.nextSibling),""!==beforeText.innerHTML&&parentNode.parentNode.insertBefore(beforeText,parentNode.nextSibling),parentNode.remove(),range.setStart(emptyText,1),range.setEnd(emptyText,1),selection.removeAllRanges(),selection.addRange(range)}else{const node=this.createElement(tag);node.appendChild(document.createTextNode("\ufeff")),range.insertNode(node),range.setStart(node.firstChild,1),range.setEnd(node.firstChild,1),selection.removeAllRanges(),selection.addRange(range)}}else if(nodeEl){const selectedText=range.toString(),parentElement=nodeEl,nextSibling=parentElement.nextSibling,beforeText=parentElement.textContent.slice(0,range.startOffset),afterText=parentElement.textContent.slice(range.endOffset);if(beforeText){const start=this.createElement(parentElement.nodeName.toLowerCase());start.textContent=beforeText,parentElement.parentNode.insertBefore(start,nextSibling)}const textNode=document.createTextNode(selectedText);if(parentElement.parentNode.insertBefore(textNode,nextSibling),afterText){const end=this.createElement(parentElement.nodeName.toLowerCase());end.textContent=afterText,parentElement.parentNode.insertBefore(end,nextSibling)}parentElement.remove(),range.setStart(textNode,0),range.setEnd(textNode,selectedText.length),selection.removeAllRanges(),selection.addRange(range),this.getTextArea().value=this.getCleanHTML()}else{const selectedText=range.toString();range.deleteContents();const previousNode=range.commonAncestorContainer.previousSibling,nextNode=range.commonAncestorContainer.nextSibling;if(previousNode||nextNode){var _previousNode$nodeNam,_nextNode$nodeName;const newNode=this.createElement(tag);let startOffset=0,endOffset=0,content="";if(previousNode&&(null==previousNode||null===(_previousNode$nodeNam=previousNode.nodeName)||void 0===_previousNode$nodeNam?void 0:_previousNode$nodeNam.toLowerCase())===tag&&(content=previousNode.textContent,startOffset=content.length,previousNode.remove()),content+=selectedText,endOffset=content.length,nextNode&&(null==nextNode||null===(_nextNode$nodeName=nextNode.nodeName)||void 0===_nextNode$nodeName?void 0:_nextNode$nodeName.toLowerCase())===tag&&(content+=nextNode.textContent,nextNode.remove()),newNode.textContent=content,content!==selectedText)return range.insertNode(newNode),range.setStart(newNode.firstChild,startOffset),range.setEnd(newNode.firstChild,endOffset),void(this.getTextArea().value=this.getCleanHTML())}const newNode=document.createElement(tag);newNode.appendChild(document.createTextNode(selectedText)),selection.removeAllRanges(),range.insertNode(newNode),range.selectNodeContents(newNode.firstChild),selection.addRange(range),this.getEditorContent().childNodes.forEach((el=>{"#text"===el.nodeName&&""===el.textContent&&el.remove()})),this.getTextArea().value=this.getCleanHTML()}this.getEditorContent().childNodes.forEach((el=>{"#text"===el.nodeName&&""===el.textContent&&el.remove()})),this.saveHistory()}saveHistory(){const content=this.getCleanHTML();-1!==this.historyIndex&&content===this.history[this.historyIndex]||(this.history.splice(this.historyIndex+1),this.history.push(content),this.historyIndex++)}cleanPasteHTML(content){if(!content||0===content.length)return"";let rules=[{regex:/<\s*\/html\s*>([\s\S]+)$/gi,replace:""},{regex://gi,replace:""},{regex://gi,replace:""},{regex:/]*>[\s\S]*?<\/xml>/gi,replace:""},{regex:/<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi,replace:""},{regex:/<\/?\w+:[^>]*>/gi,replace:""}];if(content=this.filterContentWithRules(content,rules),0===(content=this.cleanHTML(content)).length||!/\S/.test(content))return content;const holder=document.createElement("div");return holder.innerHTML=content,content=holder.innerHTML,holder.innerHTML="",rules=[{regex:/(<[^>]*?style\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[-:][^>;"]*;?)+/gi,replace:"$1"},{regex:/(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[_a-zA-Z0-9-]*)+/gi,replace:"$1"},{regex:/(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*Apple-[_a-zA-Z0-9-]*)+/gi,replace:"$1"},{regex:/]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi,replace:""}],content=this.filterContentWithRules(content,rules),this.cleanHTML(content)}isSupportSupSub(action){const{type:type}=this.settings;return"both"===type||type===action}filterContentWithRules(content,rules){for(const element of rules)content=content.replace(element.regex,element.replace);return content}cleanHTML(content){return this.filterContentWithRules(content,[{regex:/]*>( |\s)*<\/p>/gi,replace:""},{regex:/]*( |\s)*>/gi,replace:""},{regex:/]*( |\s)*>/gi,replace:""},{regex:/ /gi,replace:" "},{regex:/<\/sup>(\s*)+/gi,replace:"$1"},{regex:/<\/sub>(\s*)+/gi,replace:"$1"},{regex:/(\s*)+/gi,replace:"$1"},{regex:/(\s*)+/gi,replace:"$1"},{regex:/(\s*)+<\/sup>/gi,replace:"$1"},{regex:/(\s*)+<\/sub>/gi,replace:"$1"},{regex:/
/gi,replace:""},{regex:/]*>[\s\S]*?<\/style>/gi,replace:""},{regex:/)/gi,replace:""},{regex:/]*>[\s\S]*?<\/script>/gi,replace:""},{regex:/<\/?(?:br|title|meta|style|std|font|html|body|link|a|ul|li|ol)[^>]*?>/gi,replace:""},{regex:/<\/?(?:b|i|u|ul|ol|li|img)[^>]*?>/gi,replace:""},{regex:/<\/?(?:abbr|address|area|article|aside|audio|base|bdi|bdo|blockquote)[^>]*?>/gi,replace:""},{regex:/<\/?(?:button|canvas|caption|cite|code|col|colgroup|content|data)[^>]*?>/gi,replace:""},{regex:/<\/?(?:datalist|dd|decorator|del|details|dialog|dfn|div|dl|dt|element)[^>]*?>/gi,replace:""},{regex:/<\/?(?:em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5)[^>]*?>/gi,replace:""},{regex:/<\/?(?:h6|header|hgroup|hr|iframe|input|ins|kbd|keygen|label|legend)[^>]*?>/gi,replace:""},{regex:/<\/?(?:main|map|mark|menu|menuitem|meter|nav|noscript|object|optgroup)[^>]*?>/gi,replace:""},{regex:/<\/?(?:option|output|p|param|pre|progress|q|rp|rt|rtc|ruby|samp)[^>]*?>/gi,replace:""},{regex:/<\/?(?:section|select|script|shadow|small|source|std|strong|summary)[^>]*?>/gi,replace:""},{regex:/<\/?(?:svg|table|tbody|td|template|textarea|time|tfoot|th|thead|tr|track)[^>]*?>/gi,replace:""},{regex:/<\/?(?:var|wbr|video)[^>]*?>/gi,replace:""},{regex:/<\/?(?:acronym|applet|basefont|big|blink|center|dir|frame|frameset|isindex)[^>]*?>/gi,replace:""},{regex:/<\/?(?:listing|noembed|plaintext|spacer|strike|tt|xmp)[^>]*?>/gi,replace:""},{regex:/<\/?(?:jsl|nobr)[^>]*?>/gi,replace:""},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi,replace:"$1"},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>( |\s)*<\/span>/gi,replace:""},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi,replace:"$1"},{regex:/]*>( |\s)*<\/sup>/gi,replace:""},{regex:/]*>( |\s)*<\/sub>/gi,replace:""},{regex:/(.*?)<\/xmlns.*?>/gi,replace:"$1"},{regex:/\uFEFF/gi,replace:""}])}getCleanHTML(){let html;html=this.getEditorContent().cloneNode(!0).innerHTML;return["

","


","
",'

','


','

','


',"

 

","


 

",'

 

','


 

','

 

','


 

'].includes(html)?"":this.cleanHTML(html)}getEditorContent(){return this.getEditor().querySelector(".".concat(this.settings.classes.content))}getEditor(){return document.getElementById("".concat(this.settings.classes.editor,"-").concat(this.settings.element))}getSupSubButton(type){const{toolbar:toolbar,button:button}=this.settings.classes;return this.getEditor().querySelector(".".concat(toolbar," .").concat(button,'[data-action^="').concat(type,'"]'))}getActions(type){return defaultActions[type]?[defaultActions[type]]:Object.values(defaultActions)}getButtonContainer(){return this.getEditor().querySelectorAll(".".concat(this.settings.classes.toolbar," .").concat(this.settings.classes.button))}getContent(){return this.getTextArea().value}getEditorId(){return this.settings.element}getTextArea(){return document.getElementById(this.settings.element)}}_exports.loadEditor=settings=>{const editor=new OUSupSubEditor(settings);window.OUSupSubEditor?window.OUSupSubEditor.addEditor(editor):window.OUSupSubEditor={instances:{[settings.element]:editor},addEditor:function(editor){this.instances[editor.getEditorId()]=editor},getEditorById:function(editorId){return this.instances[editorId]}}}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.loadEditor=void 0;const defaultActions={sup:{name:"superscript",tag:"sup",class:"ousupsub_superscript_button_superscript"},sub:{name:"subscript",tag:"sub",class:"ousupsub_subscript_button_subscript"}};class OUSupSubEditor{constructor(settings){_defineProperty(this,"defaultSetting",{element:"",type:"both",classes:{wrap:"ousupsub-wrap",editor:"editor_ousupsub",contentWrap:"editor_ousupsub_content_wrap",content:"editor_ousupsub_content",toolbar:"editor_ousupsub_toolbar",toolbarGroup:"ousupsub_group",button:"ousupsub-button"},custom:{editor:"",content:"",toolbar:"",button:"",contentWrap:"",wrap:"",toolbarGroup:""}}),_defineProperty(this,"history",[]),_defineProperty(this,"historyIndex",-1),_defineProperty(this,"initEditorContent",(()=>{var _custom$content,_custom$contentWrap;const{classes:classes,custom:custom}=this.settings,contentElement=this.createElement("div",{class:(classes.content+" "+(null!==(_custom$content=custom.content)&&void 0!==_custom$content?_custom$content:"")).trim(),contenteditable:!0,autocapitalize:"none",autocorrect:"off",role:"textbox",spellcheck:!1,"aria-live":"off",id:"".concat(this.settings.element.replace(/:/g,":"),"editable")});contentElement.addEventListener("blur",(()=>{this.saveHistory()})),document.addEventListener("selectionchange",(()=>this.handleSelectionChange())),contentElement.addEventListener("keydown",(event=>{const range=window.getSelection().getRangeAt(0),keyMap={key:{ArrowUp:"sup",94:"sup",ArrowDown:"sub",95:"sub"},shiftKey:{"^":"sup",_:"sub"}};if((keyMap.key[event.key]||event.shiftKey&&keyMap.shiftKey[event.key])&&(event.preventDefault(),this.handleSupSubHotKey(keyMap.key[event.key]||keyMap.shiftKey[event.key])),event.ctrlKey&&this.saveHistory(),"Enter"===event.key&&event.preventDefault(),event.ctrlKey&&"z"===event.key&&(event.preventDefault(),this.handleUndo()),event.ctrlKey&&"y"===event.key&&(event.preventDefault(),this.handleRedo()),""===this.cleanHTML(event.target.innerHTML)&&!this.isSelectionInsideSubSup()){const emptyText=document.createTextNode("\ufeff");range.insertNode(emptyText)}this.getTextArea().value=this.getCleanHTML()})),contentElement.addEventListener("paste",(event=>{this.handlePaste(event)}));const wrapContent=this.createElement("div",{class:(classes.contentWrap+" "+(null!==(_custom$contentWrap=custom.contentWrap)&&void 0!==_custom$contentWrap?_custom$contentWrap:"")).trim()});return wrapContent.appendChild(contentElement),wrapContent})),this.settings=Object.assign(this.defaultSetting,settings),this.init()}init(){var _custom$editor;const textareaElement=this.getTextArea(),{classes:classes,custom:custom}=this.settings;if(!textareaElement)return;textareaElement.style.display="none";const editorElement=this.createElement("div",{class:(classes.editor+" "+(null!==(_custom$editor=null==custom?void 0:custom.editor)&&void 0!==_custom$editor?_custom$editor:"")).trim(),id:classes.editor+"-"+this.settings.element}),editorWrap=this.createElement("div",{class:(classes.wrap+" "+custom.wrap).trim()}),toolbarEl=this.initEditorToolbar();editorWrap.appendChild(toolbarEl);const contentElementWrap=this.initEditorContent(),contentEditor=contentElementWrap.querySelector(".".concat(this.settings.classes.content));editorWrap.appendChild(contentElementWrap),editorElement.appendChild(editorWrap);const width=6*this.getTextArea().getAttribute("cols")+41+"px";contentEditor.style.width=width,contentEditor.style.minWidth=width,contentEditor.style.maxWidth=width;const height=6*this.getTextArea().getAttribute("rows")+13,heightEditor="".concat(height-10,"px"),lineHeightEditor="".concat(height-6,"px");contentEditor.style.height=heightEditor,contentEditor.style.minHeight=heightEditor,contentEditor.style.maxHeight=heightEditor,contentEditor.style.lineHeight=lineHeightEditor;const heightContent="".concat(height+1,"px");contentElementWrap.style.minHeight=heightContent;const textareaLabel=document.querySelector('[for="'+this.settings.element+'"]');if(textareaLabel.style.display="inline-block",textareaLabel.style.margin=0,textareaLabel.style.height=heightContent,textareaLabel.style.minHeight=heightContent,textareaLabel.style.maxHeight=heightContent,textareaLabel.classList.contains("accesshide"))textareaLabel.classList.remove("accesshide"),textareaLabel.style.visibility="hidden",editorElement.style.marginLeft="-".concat(parseInt(textareaLabel.offsetWidth),"px");else{textareaLabel.parentNode.style.paddingBottom=heightEditor,textareaLabel.style.verticalAlign="bottom"}textareaElement.insertAdjacentElement("beforebegin",editorElement),this.getEditorContent().innerHTML=this.getContent(),this.saveHistory(),requestAnimationFrame((()=>{textareaLabel.style.lineHeight=contentEditor.style.lineHeight;const heightWrapper=height+1+parseInt(toolbarEl.offsetHeight);editorElement.style.height=heightWrapper+"px",editorElement.style.minHeight=heightWrapper+"px",editorElement.style.maxHeight=heightWrapper+"px"})),document.addEventListener("click",(e=>{if(!editorElement.contains(e.target)){const cleanData=this.getCleanHTML();this.getTextArea().value=cleanData,this.getEditorContent().innerHTML=cleanData,this.setActiveButton(!1)}}))}handlePaste(event){event.preventDefault();const types=event.clipboardData.types;let content,isHTML=!1;null!=types&&types.contains?isHTML=types.contains("text/html"):null!=types&&types.includes&&(isHTML=types.includes("text/html")),content=isHTML?this.cleanPasteHTML(event.clipboardData.getData("text/html")):event.clipboardData.getData("text");const cleanData=content.replaceAll(/[\r\n]+/g,"");document.execCommand("insertHTML",!1,cleanData),this.saveHistory(),this.getTextArea().value=this.getCleanHTML()}handleUndo(){this.historyIndex>0&&(this.historyIndex--,this.getEditorContent().innerHTML=this.history[this.historyIndex],this.getTextArea().value=this.history[this.historyIndex])}handleRedo(){this.historyIndex1&&void 0!==arguments[1]?arguments[1]:{};const element=document.createElement(tag);for(let attribute in attributes)element.setAttribute(attribute,attributes[attribute]);return element}isSelectionInsideSubSup(){const selection=window.getSelection();if(0===selection.rangeCount)return!1;const range=selection.getRangeAt(0),tagName=range.commonAncestorContainer.parentNode.nodeName;if(selection.isCollapsed)return!!this.isSupSubTag(tagName)&&range.commonAncestorContainer.parentNode;let nodeNames;const selectionNodes=range.cloneContents().childNodes;for(let node of selectionNodes){const nodeName=node.nodeName;if(""!==node.textContent){if(!this.isSupSubTag(nodeName)&&"#text"===nodeName&&!this.isSupSubTag(tagName))return!1;if(nodeNames||(nodeNames=node),!nodeNames.isEqualNode(node))return!1}}return"#text"===nodeNames.nodeName||this.isSupSubTag(tagName)?range.commonAncestorContainer.parentNode:nodeNames}isSupSubTag(tagName){return["SUB","SUP"].includes(tagName)}setActiveButton(type){const{toolbar:toolbar,button:button}=this.settings.classes;var _this$getSupSubButton,_this$getSupSubButton2;(this.getEditor().querySelectorAll(".".concat(toolbar," .").concat(button)).forEach((button=>button.classList.remove("highlight"))),!1!==type)&&(null===(_this$getSupSubButton=this.getSupSubButton(type))||void 0===_this$getSupSubButton||null===(_this$getSupSubButton2=_this$getSupSubButton.classList)||void 0===_this$getSupSubButton2||_this$getSupSubButton2.add("highlight"))}initEditorToolbar(){var _this$settings$custom,_this$settings,_this$settings$custom2,_this$settings$custom3,_this$settings3,_this$settings3$custo;const toolbarGroup=this.createElement("div",{class:(this.settings.classes.toolbarGroup+" "+(null!==(_this$settings$custom=null===(_this$settings=this.settings)||void 0===_this$settings||null===(_this$settings$custom2=_this$settings.custom)||void 0===_this$settings$custom2?void 0:_this$settings$custom2.toolbarGroup)&&void 0!==_this$settings$custom?_this$settings$custom:"")).trim()});this.getActions(this.settings.type).forEach((action=>{var _this$settings2;const button=this.createElement("button",{class:(null===(_this$settings2=this.settings)||void 0===_this$settings2?void 0:_this$settings2.classes.button)+" "+action.class,title:this.settings.buttons[action.name].title,type:"button","data-action":action.name});button.innerHTML=this.settings.buttons[action.name].icon,button.setAttribute("type","button"),button.onclick=()=>{const selection=window.getSelection(),nodeEl=this.isSelectionInsideSubSup();if(selection.isCollapsed&&!1!==nodeEl&&nodeEl.nodeName.toLowerCase()!==action.tag)return button.blur(),void this.getEditorContent().focus();this.getEditorContent().focus(),this.setFormat(action)},toolbarGroup.appendChild(button)}));const toolbarEl=this.createElement("div",{class:(this.settings.classes.toolbar+" "+(null!==(_this$settings$custom3=null===(_this$settings3=this.settings)||void 0===_this$settings3||null===(_this$settings3$custo=_this$settings3.custom)||void 0===_this$settings3$custo?void 0:_this$settings3$custo.toolbar)&&void 0!==_this$settings$custom3?_this$settings$custom3:"")).trim()});return toolbarEl.appendChild(toolbarGroup),toolbarEl}setFormat(action){const selection=window.getSelection(),range=selection.getRangeAt(0),{tag:tag}=action,nodeEl=this.isSelectionInsideSubSup();if(selection.isCollapsed){const parentNode=range.commonAncestorContainer.parentNode;if(parentNode.nodeName.toLowerCase()===tag){const beforeText=this.createElement(tag);beforeText.innerText=parentNode.textContent.slice(0,range.startOffset);const emptyText=document.createTextNode("\ufeff"),afterText=this.createElement(tag);afterText.innerText=parentNode.textContent.slice(range.startOffset),""!==afterText.innerHTML&&parentNode.parentNode.insertBefore(afterText,parentNode.nextSibling),parentNode.parentNode.insertBefore(emptyText,parentNode.nextSibling),""!==beforeText.innerHTML&&parentNode.parentNode.insertBefore(beforeText,parentNode.nextSibling),parentNode.remove(),range.setStart(emptyText,1),range.setEnd(emptyText,1),selection.removeAllRanges(),selection.addRange(range)}else{const node=this.createElement(tag);node.appendChild(document.createTextNode("\ufeff")),range.insertNode(node),range.setStart(node.firstChild,1),range.setEnd(node.firstChild,1),selection.removeAllRanges(),selection.addRange(range)}}else if(nodeEl){const selectedText=range.toString(),parentElement=nodeEl,nextSibling=parentElement.nextSibling,beforeText=parentElement.textContent.slice(0,range.startOffset),afterText=parentElement.textContent.slice(range.endOffset);if(beforeText){const start=this.createElement(parentElement.nodeName.toLowerCase());start.textContent=beforeText,parentElement.parentNode.insertBefore(start,nextSibling)}const textNode=document.createTextNode(selectedText);if(parentElement.parentNode.insertBefore(textNode,nextSibling),afterText){const end=this.createElement(parentElement.nodeName.toLowerCase());end.textContent=afterText,parentElement.parentNode.insertBefore(end,nextSibling)}parentElement.remove(),range.setStart(textNode,0),range.setEnd(textNode,selectedText.length),selection.removeAllRanges(),selection.addRange(range),this.getTextArea().value=this.getCleanHTML()}else{const selectedText=range.toString();range.deleteContents();const previousNode=range.commonAncestorContainer.previousSibling,nextNode=range.commonAncestorContainer.nextSibling;if(previousNode||nextNode){var _previousNode$nodeNam,_nextNode$nodeName;const newNode=this.createElement(tag);let startOffset=0,endOffset=0,content="";if(previousNode&&(null==previousNode||null===(_previousNode$nodeNam=previousNode.nodeName)||void 0===_previousNode$nodeNam?void 0:_previousNode$nodeNam.toLowerCase())===tag&&(content=previousNode.textContent,startOffset=content.length,previousNode.remove()),content+=selectedText,endOffset=content.length,nextNode&&(null==nextNode||null===(_nextNode$nodeName=nextNode.nodeName)||void 0===_nextNode$nodeName?void 0:_nextNode$nodeName.toLowerCase())===tag&&(content+=nextNode.textContent,nextNode.remove()),newNode.textContent=content,content!==selectedText)return range.insertNode(newNode),range.setStart(newNode.firstChild,startOffset),range.setEnd(newNode.firstChild,endOffset),void(this.getTextArea().value=this.getCleanHTML())}const newNode=document.createElement(tag);newNode.appendChild(document.createTextNode(selectedText)),selection.removeAllRanges(),range.insertNode(newNode),range.selectNodeContents(newNode.firstChild),selection.addRange(range),this.getEditorContent().childNodes.forEach((el=>{"#text"===el.nodeName&&""===el.textContent&&el.remove()})),this.getTextArea().value=this.getCleanHTML()}this.getEditorContent().childNodes.forEach((el=>{"#text"===el.nodeName&&""===el.textContent&&el.remove()})),this.saveHistory()}saveHistory(){const content=this.getCleanHTML();-1!==this.historyIndex&&content===this.history[this.historyIndex]||(this.history.splice(this.historyIndex+1),this.history.push(content),this.historyIndex++)}cleanPasteHTML(content){if(!content||0===content.length)return"";let rules=[{regex:/<\s*\/html\s*>([\s\S]+)$/gi,replace:""},{regex://gi,replace:""},{regex://gi,replace:""},{regex:/]*>[\s\S]*?<\/xml>/gi,replace:""},{regex:/<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi,replace:""},{regex:/<\/?\w+:[^>]*>/gi,replace:""}];if(content=this.filterContentWithRules(content,rules),0===(content=this.cleanHTML(content)).length||!/\S/.test(content))return content;const holder=document.createElement("div");return holder.innerHTML=content,content=holder.innerHTML,holder.innerHTML="",rules=[{regex:/(<[^>]*?style\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[-:][^>;"]*;?)+/gi,replace:"$1"},{regex:/(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[_a-zA-Z0-9-]*)+/gi,replace:"$1"},{regex:/(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*Apple-[_a-zA-Z0-9-]*)+/gi,replace:"$1"},{regex:/
]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi,replace:""}],content=this.filterContentWithRules(content,rules),this.cleanHTML(content)}isSupportSupSub(action){const{type:type}=this.settings;return"both"===type||type===action}filterContentWithRules(content,rules){for(const element of rules)content=content.replace(element.regex,element.replace);return content}cleanHTML(content){return this.filterContentWithRules(content,[{regex:/]*>( |\s)*<\/p>/gi,replace:""},{regex:/]*( |\s)*>/gi,replace:""},{regex:/]*( |\s)*>/gi,replace:""},{regex:/ /gi,replace:" "},{regex:/<\/sup>(\s*)+/gi,replace:"$1"},{regex:/<\/sub>(\s*)+/gi,replace:"$1"},{regex:/(\s*)+/gi,replace:"$1"},{regex:/(\s*)+/gi,replace:"$1"},{regex:/(\s*)+<\/sup>/gi,replace:"$1"},{regex:/(\s*)+<\/sub>/gi,replace:"$1"},{regex:/
/gi,replace:""},{regex:/]*>[\s\S]*?<\/style>/gi,replace:""},{regex:/)/gi,replace:""},{regex:/]*>[\s\S]*?<\/script>/gi,replace:""},{regex:/<\/?(?:br|title|meta|style|std|font|html|body|link|a|ul|li|ol)[^>]*?>/gi,replace:""},{regex:/<\/?(?:b|i|u|ul|ol|li|img)[^>]*?>/gi,replace:""},{regex:/<\/?(?:abbr|address|area|article|aside|audio|base|bdi|bdo|blockquote)[^>]*?>/gi,replace:""},{regex:/<\/?(?:button|canvas|caption|cite|code|col|colgroup|content|data)[^>]*?>/gi,replace:""},{regex:/<\/?(?:datalist|dd|decorator|del|details|dialog|dfn|div|dl|dt|element)[^>]*?>/gi,replace:""},{regex:/<\/?(?:em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5)[^>]*?>/gi,replace:""},{regex:/<\/?(?:h6|header|hgroup|hr|iframe|input|ins|kbd|keygen|label|legend)[^>]*?>/gi,replace:""},{regex:/<\/?(?:main|map|mark|menu|menuitem|meter|nav|noscript|object|optgroup)[^>]*?>/gi,replace:""},{regex:/<\/?(?:option|output|p|param|pre|progress|q|rp|rt|rtc|ruby|samp)[^>]*?>/gi,replace:""},{regex:/<\/?(?:section|select|script|shadow|small|source|std|strong|summary)[^>]*?>/gi,replace:""},{regex:/<\/?(?:svg|table|tbody|td|template|textarea|time|tfoot|th|thead|tr|track)[^>]*?>/gi,replace:""},{regex:/<\/?(?:var|wbr|video)[^>]*?>/gi,replace:""},{regex:/<\/?(?:acronym|applet|basefont|big|blink|center|dir|frame|frameset|isindex)[^>]*?>/gi,replace:""},{regex:/<\/?(?:listing|noembed|plaintext|spacer|strike|tt|xmp)[^>]*?>/gi,replace:""},{regex:/<\/?(?:jsl|nobr)[^>]*?>/gi,replace:""},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi,replace:"$1"},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>( |\s)*<\/span>/gi,replace:""},{regex:/]*?rangySelectionBoundary[^>]*?)[^>]*>[\s\S]*?([\s\S]*?)<\/span>/gi,replace:"$1"},{regex:/]*>( |\s)*<\/sup>/gi,replace:""},{regex:/]*>( |\s)*<\/sub>/gi,replace:""},{regex:/(.*?)<\/xmlns.*?>/gi,replace:"$1"},{regex:/\uFEFF/gi,replace:""}])}getCleanHTML(){let html;html=this.getEditorContent().cloneNode(!0).innerHTML;return["

","


","
",'

','


','

','


',"

 

","


 

",'

 

','


 

','

 

','


 

'].includes(html)?"":this.cleanHTML(html)}getEditorContent(){return this.getEditor().querySelector(".".concat(this.settings.classes.content))}getEditor(){return document.getElementById("".concat(this.settings.classes.editor,"-").concat(this.settings.element))}getSupSubButton(type){const{toolbar:toolbar,button:button}=this.settings.classes;return this.getEditor().querySelector(".".concat(toolbar," .").concat(button,'[data-action^="').concat(type,'"]'))}getActions(type){return defaultActions[type]?[defaultActions[type]]:Object.values(defaultActions)}getButtonContainer(){return this.getEditor().querySelectorAll(".".concat(this.settings.classes.toolbar," .").concat(this.settings.classes.button))}getContent(){return this.getTextArea().value}getEditorId(){return this.settings.element}getTextArea(){return document.getElementById(this.settings.element)}}_exports.loadEditor=settings=>{const editor=new OUSupSubEditor(settings);window.OUSupSubEditor?window.OUSupSubEditor.addEditor(editor):window.OUSupSubEditor={instances:{[settings.element]:editor},addEditor:function(editor){this.instances[editor.getEditorId()]=editor},getEditorById:function(editorId){return this.instances[editorId]}}}})); //# sourceMappingURL=editor.min.js.map \ No newline at end of file diff --git a/amd/build/editor.min.js.map b/amd/build/editor.min.js.map index 4f0656d..a8ccbb0 100644 --- a/amd/build/editor.min.js.map +++ b/amd/build/editor.min.js.map @@ -1 +1 @@ -{"version":3,"file":"editor.min.js","sources":["../src/editor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * OUSupSub Editor Manager.\n *\n * @module editor_ousupsub/editor\n * @copyright 2024 The Open University.\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst defaultActions = {\n sup: {\n name: 'superscript',\n tag: 'sup',\n 'class': 'ousupsub_superscript_button_superscript',\n },\n sub: {\n name: 'subscript',\n tag: 'sub',\n 'class': 'ousupsub_subscript_button_subscript',\n },\n};\n\nclass OUSupSubEditor {\n\n // The editor's initial default settings.\n defaultSetting = {\n element: '',\n type: 'both',\n classes: {\n wrap: 'ousupsub-wrap',\n editor: 'editor_ousupsub',\n contentWrap: 'editor_ousupsub_content_wrap',\n content: 'editor_ousupsub_content',\n toolbar: 'editor_ousupsub_toolbar',\n toolbarGroup: 'ousupsub_group',\n button: 'ousupsub-button',\n },\n custom: {\n editor: '',\n content: '',\n toolbar: '',\n button: '',\n contentWrap: '',\n wrap: '',\n toolbarGroup: '',\n },\n };\n\n // Support for undo/redo with history data and history index.\n history = [];\n historyIndex = -1;\n\n /**\n * Constructor of the editor.\n * @constructor\n *\n * @param {Object} settings - The editor settings.\n */\n constructor(settings) {\n this.settings = Object.assign(this.defaultSetting, settings);\n this.init();\n }\n\n /**\n * Initial the editor.\n */\n init() {\n const textareaElement = this.getTextArea();\n const {classes, custom} = this.settings;\n\n if (!textareaElement) {\n return;\n }\n // Hidden origin text area.\n textareaElement.style.display = 'none';\n\n const editorElement = this.createElement('div', {\n 'class': (classes.editor + ' ' + (custom?.editor ?? '')).trim(),\n id: classes.editor + '-' + this.settings.element,\n });\n\n // Make editor container.\n const editorWrap = this.createElement('div', {\n 'class': (classes.wrap + ' ' + custom.wrap).trim(),\n });\n\n // Make toolbar containers.\n const toolbarEl = this.initEditorToolbar();\n this.appendChild(editorWrap, toolbarEl);\n\n // Make the content for editor.\n const contentElementWrap = this.initEditorContent();\n const contentEditor = contentElementWrap.querySelector(`.${this.settings.classes.content}`);\n\n // Append the editor's elements to the DOM.\n this.appendChild(editorWrap, contentElementWrap);\n this.appendChild(editorElement, editorWrap);\n\n // Calculate the editor size based on the attributes 'cols' and 'rows'.\n const width = (this.getTextArea().getAttribute('cols') * 6 + 41) + 'px';\n contentEditor.style.width = width;\n contentEditor.style.minWidth = width;\n contentEditor.style.maxWidth = width;\n\n const rows = this.getTextArea().getAttribute('rows');\n const height = (rows * 6 + 13);\n const heightEditor = `${height - 10}px`;\n const lineHeightEditor = `${height - 6}px`;\n\n // Set the size of the editor.\n contentEditor.style.height = heightEditor;\n contentEditor.style.minHeight = heightEditor;\n contentEditor.style.maxHeight = heightEditor;\n contentEditor.style.lineHeight = lineHeightEditor;\n\n const heightContent = `${height + 1}px`;\n contentElementWrap.style.minHeight = heightContent;\n\n const textareaLabel = document.querySelector('[for=\"' + this.settings.element + '\"]');\n\n textareaLabel.style.display = 'inline-block';\n textareaLabel.style.margin = 0;\n textareaLabel.style.height = heightContent;\n textareaLabel.style.minHeight = heightContent;\n textareaLabel.style.maxHeight = heightContent;\n\n // Align for the case using Supsub on the editor.\n if (textareaLabel.classList.contains('accesshide')) {\n textareaLabel.classList.remove('accesshide');\n textareaLabel.style.visibility = 'hidden';\n editorElement.style.marginLeft = `-${parseInt(textareaLabel.offsetWidth)}px`;\n } else {\n // Get parent node of the label.\n const labelParentNode = textareaLabel.parentNode;\n labelParentNode.style.paddingBottom = heightEditor;\n textareaLabel.style.verticalAlign = 'bottom';\n }\n\n textareaElement.insertAdjacentElement('beforebegin', editorElement);\n // Set the editor's content for the first time.\n this.getEditorContent().innerHTML = this.getContent();\n\n // Save the history from the beginning.\n this.saveHistory();\n\n // Wait until the editor element is added to the DOM before calculating\n // its size to ensure it aligns with the others elements.\n requestAnimationFrame(() => {\n textareaLabel.style.lineHeight = contentEditor.style.lineHeight;\n const heightWrapper = height + 1 + parseInt(toolbarEl.offsetHeight);\n editorElement.style.height = heightWrapper + 'px';\n editorElement.style.minHeight = heightWrapper + 'px';\n editorElement.style.maxHeight = heightWrapper + 'px';\n });\n\n document.addEventListener('click', (e) => {\n if (!editorElement.contains(e.target)) {\n // Get clean data.\n const cleanData = this.getCleanHTML();\n // Set it in both the hidden text area and the editor content.\n this.getTextArea().value = cleanData;\n this.getEditorContent().innerHTML = cleanData;\n this.setActiveButton(false);\n }\n });\n }\n\n /**\n * Make a content area for the editor.\n *\n * @return {HTMLElement} The content area element.\n */\n initEditorContent = () => {\n const {classes, custom} = this.settings;\n const contentElement = this.createElement('div', {\n 'class': (classes.content + ' ' + (custom.content ?? '')).trim(),\n contenteditable: true,\n autocapitalize: 'none',\n autocorrect: 'off',\n role: 'textbox',\n spellcheck: false,\n 'aria-live': 'off',\n id: `${this.settings.element.replace(/:/g, \":\")}editable`,\n });\n\n contentElement.addEventListener('blur', () => {\n this.saveHistory();\n });\n\n // Listen for the selection change event.\n document.addEventListener('selectionchange', () => this.handleSelectionChange());\n\n // Set up hotkeys for the editor and prevent the Enter key from making the text content a single line.\n contentElement.addEventListener('keydown', (event) => {\n // Selection range.\n const selection = window.getSelection();\n const range = selection.getRangeAt(0);\n const keyMap = {\n key: {\n 'ArrowUp': 'sup',\n '94': 'sup',\n 'ArrowDown': 'sub',\n '95': 'sub',\n\n },\n shiftKey: {\n '^': 'sup',\n '_': 'sub',\n }\n };\n if (keyMap.key[event.key] || (event.shiftKey && keyMap.shiftKey[event.key])) {\n event.preventDefault();\n this.handleSupSubHotKey(keyMap.key[event.key] || keyMap.shiftKey[event.key]);\n }\n\n if (event.ctrlKey) {\n this.saveHistory();\n }\n\n if (event.key === 'Enter') {\n event.preventDefault();\n }\n\n // Handle undo/redo action.\n if (event.ctrlKey && event.key === 'z') {\n event.preventDefault();\n this.handleUndo();\n }\n\n if (event.ctrlKey && event.key === 'y') {\n event.preventDefault();\n this.handleRedo();\n }\n\n // In case the editor is empty we need to reset format\n // to prevent it remember the previous format.\n if (this.cleanHTML(event.target.innerHTML) === '' &&\n !this.isSelectionInsideSubSup()) {\n const emptyText = document.createTextNode('\\uFEFF');\n range.insertNode(emptyText);\n }\n\n this.getTextArea().value = this.getCleanHTML();\n });\n\n contentElement.addEventListener('paste', event => {\n this.handlePaste(event);\n });\n\n const wrapContent = this.createElement('div', {\n 'class': (classes.contentWrap + ' ' + (custom.contentWrap ?? '')).trim(),\n });\n\n this.appendChild(wrapContent, contentElement);\n\n return wrapContent;\n };\n\n /**\n * Handle event paste.\n *\n * @param {Event} event Event object.\n */\n handlePaste(event) {\n event.preventDefault();\n const types = event.clipboardData.types;\n let isHTML = false;\n\n // Check for different methods to determine if 'text/html' is present\n if (types?.contains) {\n isHTML = types.contains('text/html');\n } else if (types?.includes) {\n isHTML = types.includes('text/html');\n }\n\n let content;\n if (isHTML) {\n content = this.cleanPasteHTML(event.clipboardData.getData('text/html'));\n } else {\n content = event.clipboardData.getData('text');\n }\n\n // We need to clean the data before inserting it into the editor.\n const cleanData = content.replaceAll(/[\\r\\n]+/g, '');\n document.execCommand('insertHTML', false, cleanData);\n this.saveHistory();\n this.getTextArea().value = this.getCleanHTML();\n }\n\n /**\n * Handle event undo.\n */\n handleUndo() {\n if (this.historyIndex > 0) {\n this.historyIndex--;\n this.getEditorContent().innerHTML = this.history[this.historyIndex];\n this.getTextArea().value = this.history[this.historyIndex];\n }\n }\n\n /**\n * Handle event redo.\n */\n handleRedo() {\n if (this.historyIndex < this.history.length - 1) {\n this.historyIndex++;\n this.getEditorContent().innerHTML = this.history[this.historyIndex];\n this.getTextArea().value = this.history[this.historyIndex];\n }\n }\n\n /**\n * Handle event sup/sub.\n *\n * @param {String} action The sup/sub action.\n */\n handleSupSubHotKey(action) {\n const nodeEl = this.isSelectionInsideSubSup();\n if (nodeEl) {\n const nodeName = nodeEl.nodeName.toLowerCase();\n if (nodeName !== action) {\n this.setFormat(this.getActions(nodeName === 'sup' ? 'sub' : 'sup')[0]);\n }\n return;\n }\n if (this.isSupportSupSub(action)) {\n this.setFormat(this.getActions(action)[0]);\n }\n }\n\n /**\n * Based on the user's selection change, we will detect the pointer position to determine whether\n * the cursor is inside the sup/sub tag. Depending on this result, we will activate the corresponding button.\n */\n handleSelectionChange() {\n const selection = window.getSelection();\n\n // When the user makes a selection change inside the editor.\n if (this.getEditorContent().contains(selection.anchorNode)) {\n // Detect whether the pointer is inside the sup/sub tag.\n const node = this.isSelectionInsideSubSup();\n\n if (node) {\n // Activate the corresponding button in the toolbar.\n this.setActiveButton(node.nodeName.toLowerCase());\n } else {\n // Deactivate all the buttons.\n this.setActiveButton(false);\n }\n }\n }\n\n /**\n * Utility function to create a element with attributes.\n *\n * @param {String} tag - HTML tag name.\n * @param {Object} attributes - The attributes of the element, such as class, id, etc.\n * @return {HTMLElement} The element that was created.\n */\n createElement(tag, attributes = {}) {\n const element = document.createElement(tag);\n for (let attribute in attributes) {\n element.setAttribute(attribute, attributes[attribute]);\n }\n\n return element;\n }\n\n /**\n * Utility function to check whether the current selection is inside the sup/sub tag. Returns false if it's not.\n *\n * @return {Boolean|ParentNode} Return the node if the selection is inside a sup/sub tag; otherwise, return false.\n */\n isSelectionInsideSubSup() {\n const selection = window.getSelection();\n if (selection.rangeCount === 0) {\n return false;\n }\n const range = selection.getRangeAt(0);\n const tagName = range.commonAncestorContainer.parentNode.nodeName;\n // If user doesn't select any text.\n if (selection.isCollapsed) {\n if (this.isSupSubTag(tagName)) {\n return range.commonAncestorContainer.parentNode;\n }\n return false;\n }\n let nodeNames;\n const selectionNodes = range.cloneContents().childNodes;\n for (let node of selectionNodes) {\n const nodeName = node.nodeName;\n if (node.textContent === '') {\n continue;\n }\n if (!(this.isSupSubTag(nodeName)) &&\n (nodeName === '#text' && !this.isSupSubTag(tagName))) {\n return false;\n }\n if (!nodeNames) {\n nodeNames = node;\n }\n if (!nodeNames.isEqualNode(node)) {\n return false;\n }\n }\n\n if (nodeNames.nodeName === '#text' || this.isSupSubTag(tagName)) {\n return range.commonAncestorContainer.parentNode;\n }\n\n return nodeNames;\n }\n\n /**\n * Check if the given tag name is 'sup' or 'sub'. Return true if it is.\n *\n * @param {String} tagName - Tag name need to check.\n * @return {Boolean|ParentNode} Return the node if the selection is inside a sup/sub tag; otherwise, return false.\n */\n isSupSubTag(tagName) {\n return ['SUB', 'SUP'].includes(tagName);\n }\n\n /**\n * Utility function to highlight the sup/sub button.\n *\n * @param {String|Boolean} type - The type of the button: sup or sub.\n */\n setActiveButton(type) {\n const {toolbar, button} = this.settings.classes;\n // Deactivate all the existing buttons.\n this.getEditor().querySelectorAll(`.${toolbar} .${button}`)\n .forEach(button => button.classList.remove('highlight'));\n if (type !== false) {\n this.getSupSubButton(type)?.classList?.add('highlight');\n }\n }\n\n /**\n * Utility function to append a child node to a parent node.\n *\n * @param {HTMLElement} parent - The parent node that will contain the child node.\n * @param {HTMLElement} child - The child node.\n */\n appendChild(parent, child) {\n parent.appendChild(child);\n }\n\n /**\n * Utility function to create a toolbar element that contains sup and sub buttons.\n *\n * @return {HTMLElement} The toolbar element.\n */\n initEditorToolbar() {\n const toolbarGroup = this.createElement('div', {\n 'class': (this.settings.classes.toolbarGroup + ' ' + (this.settings?.custom?.toolbarGroup ?? '')).trim(),\n });\n this.getActions(this.settings.type).forEach((action) => {\n const button = this.createElement('button', {\n 'class': this.settings?.classes.button + ' ' + action.class,\n title: this.settings.buttons[action.name].title,\n type: 'button',\n 'data-action': action.name,\n });\n\n button.innerHTML = this.settings.buttons[action.name].icon;\n button.setAttribute('type', 'button');\n button.onclick = () => {\n const selection = window.getSelection();\n const nodeEl = this.isSelectionInsideSubSup();\n if (selection.isCollapsed && nodeEl !== false) {\n if (nodeEl.nodeName.toLowerCase() !== action.tag) {\n button.blur();\n this.getEditorContent().focus();\n return;\n }\n }\n\n this.getEditorContent().focus();\n this.setFormat(action);\n };\n\n this.appendChild(toolbarGroup, button);\n });\n const toolbarEl = this.createElement('div', {\n 'class': (this.settings.classes.toolbar + ' ' + (this.settings?.custom?.toolbar ?? '')).trim(),\n });\n this.appendChild(toolbarEl, toolbarGroup);\n\n return toolbarEl;\n }\n\n /**\n * Based on the action (sup/sub), this function will format the selected text accordingly.\n *\n * @param {Object} action - The sup/sub action object.\n */\n setFormat(action) {\n // Selection text.\n const selection = window.getSelection();\n // Selection range.\n const range = selection.getRangeAt(0);\n const {tag} = action;\n const nodeEl = this.isSelectionInsideSubSup();\n // In case the user doesn't select any text.\n if (selection.isCollapsed) {\n // We need to check whether the current position of the pointer is inside a sub or sup tag.\n const parentNode = range.commonAncestorContainer.parentNode;\n if (parentNode.nodeName.toLowerCase() === tag) {\n // In this case, the pointer is inside a sub or sup tag, so we need to select all the text within the tag.\n // Then, we will slice it into two parts, using the current position of the cursor as the border.\n // The first part will extend from position 0 to the border, and the second part will span from\n // the border to the end.\n // After that, we will wrap each part in the corresponding sub or sup tag. The result will be:\n // First and Second.\n // Finally, we will create a text node with empty content (\\uFEFF) and place it at the border\n // of the two parts, resulting in:\n // First#textnode#Second.\n // Create the first part.\n const beforeText = this.createElement(tag);\n beforeText.innerText = parentNode.textContent.slice(0, range.startOffset);\n // Make an empty textnode.\n const emptyText = document.createTextNode('\\uFEFF');\n // Create an empty text node.\n const afterText = this.createElement(tag);\n afterText.innerText = parentNode.textContent.slice(range.startOffset);\n // Insert it into the DOM next to the parent node.\n if (afterText.innerHTML !== '') {\n parentNode.parentNode.insertBefore(afterText, parentNode.nextSibling);\n }\n parentNode.parentNode.insertBefore(emptyText, parentNode.nextSibling);\n if (beforeText.innerHTML !== '') {\n parentNode.parentNode.insertBefore(beforeText, parentNode.nextSibling);\n }\n\n // Remove the parent node.\n parentNode.remove();\n // We set the position of the cursor to be in the empty text node.\n range.setStart(emptyText, 1);\n range.setEnd(emptyText, 1);\n selection.removeAllRanges();\n selection.addRange(range);\n } else {\n // In case the user didn't select anything, we must create a sup/sub\n // tag with an empty string and move the cursor into it.\n const node = this.createElement(tag);\n // Zero-width space to keep the tag visible.\n node.appendChild(document.createTextNode('\\uFEFF'));\n // Update the new range within the existing one.\n range.insertNode(node);\n // Set the selection index at the next available space.\n range.setStart(node.firstChild, 1);\n range.setEnd(node.firstChild, 1);\n // Remove all existing ranges from the selection.\n selection.removeAllRanges();\n // Add the updated range object to the current selection.\n selection.addRange(range);\n }\n } else if (nodeEl) {\n // This means the user is selecting some text that is inside the sub or sup tag.\n // In this case, we only need to move the selected text inside the sub/sup tag outside of it.\n // For example: 123[456]789. If the selected text is 456, we will\n // move it outside the tag, resulting in 123456789.\n // Retrieve the selected text.\n // Retrieve the current tag (sub/sup) that wraps the selection\n const selectedText = range.toString();\n const parentElement = nodeEl;\n const nextSibling = parentElement.nextSibling;\n const beforeText = parentElement.textContent.slice(0, range.startOffset);\n const afterText = parentElement.textContent.slice(range.endOffset);\n if (beforeText) {\n const start = this.createElement(parentElement.nodeName.toLowerCase());\n start.textContent = beforeText;\n parentElement.parentNode.insertBefore(start, nextSibling);\n }\n // Create a text node based on the selected text.\n const textNode = document.createTextNode(selectedText);\n parentElement.parentNode.insertBefore(textNode, nextSibling);\n if (afterText) {\n const end = this.createElement(parentElement.nodeName.toLowerCase());\n end.textContent = afterText;\n parentElement.parentNode.insertBefore(end, nextSibling);\n }\n\n parentElement.remove();\n\n // Create a new range to select the inserted content\n range.setStart(textNode, 0);\n range.setEnd(textNode, selectedText.length);\n selection.removeAllRanges();\n selection.addRange(range);\n this.getTextArea().value = this.getCleanHTML();\n } else {\n // This case is user select a text that is not inside subsup.\n // We retrieve the selected text and then delete it in DOM.\n const selectedText = range.toString();\n range.deleteContents();\n const previousNode = range.commonAncestorContainer.previousSibling;\n const nextNode = range.commonAncestorContainer.nextSibling;\n // In addition, we will merge adjacent sup/sub tags into a single sup/sub tag.\n if (previousNode || nextNode) {\n const newNode = this.createElement(tag);\n let startOffset = 0;\n let endOffset = 0;\n let content = '';\n if (previousNode && previousNode?.nodeName?.toLowerCase() === tag) {\n content = previousNode.textContent;\n startOffset = content.length;\n previousNode.remove();\n }\n content += selectedText;\n endOffset = content.length;\n if (nextNode && nextNode?.nodeName?.toLowerCase() === tag) {\n content += nextNode.textContent;\n nextNode.remove();\n }\n newNode.textContent = content;\n if (content !== selectedText) {\n range.insertNode(newNode);\n range.setStart(newNode.firstChild, startOffset);\n range.setEnd(newNode.firstChild, endOffset);\n this.getTextArea().value = this.getCleanHTML();\n return;\n }\n }\n\n // Create a sup/sub tag that wrap the selected text.\n const newNode = document.createElement(tag);\n newNode.appendChild(document.createTextNode(selectedText));\n // Make a selection to the selected text.\n selection.removeAllRanges();\n // Insert it into DOM.\n range.insertNode(newNode);\n range.selectNodeContents(newNode.firstChild);\n selection.addRange(range);\n // Clean up all the empty text.\n this.getEditorContent().childNodes.forEach(el => {\n if (el.nodeName === '#text' && el.textContent === '') {\n el.remove();\n }\n });\n this.getTextArea().value = this.getCleanHTML();\n }\n // Clean up.\n this.getEditorContent().childNodes.forEach(el => {\n if (el.nodeName === '#text' && el.textContent === '') {\n el.remove();\n }\n });\n this.saveHistory();\n }\n\n /**\n * Save history for undo/redo actions.\n */\n saveHistory() {\n const content = this.getCleanHTML();\n if (this.historyIndex === -1 || content !== this.history[this.historyIndex]) {\n this.history.splice(this.historyIndex + 1);\n this.history.push(content);\n this.historyIndex++;\n }\n }\n\n /**\n * Cleanup html that comes from WYSIWYG paste events. These are more likely to contain messy code that we should strip.\n *\n * @param {String} content - The content data need to be clean.\n * @return {String} The clean text.\n */\n cleanPasteHTML(content) {\n // Return an empty string if passed an invalid or empty object.\n if (!content || content.length === 0) {\n return \"\";\n }\n\n // Rules that get rid of the real-nasties and don't care about normalize code (correct quotes, white spaces, etc.).\n let rules = [\n {regex: /<\\s*\\/html\\s*>([\\s\\S]+)$/gi, replace: \"\"},\n {regex: //gi, replace: \"\"},\n {regex: //gi, replace: \"\"},\n {regex: /]*>[\\s\\S]*?<\\/xml>/gi, replace: \"\"},\n {regex: /<\\?xml[^>]*>[\\s\\S]*?<\\\\\\?xml>/gi, replace: \"\"},\n {regex: /<\\/?\\w+:[^>]*>/gi, replace: \"\"}\n ];\n\n // Apply the first set of harsher rules.\n content = this.filterContentWithRules(content, rules);\n\n // Apply the standard rules, which mainly cleans things like headers, links, and style blocks.\n content = this.cleanHTML(content);\n\n // Check if the string is empty or only contains whitespace.\n if (content.length === 0 || !/\\S/.test(content)) {\n return content;\n }\n\n // Normalize the code by loading it into the DOM.\n const holder = document.createElement('div');\n holder.innerHTML = content;\n content = holder.innerHTML;\n\n // Free up the DOM memory.\n holder.innerHTML = \"\";\n\n // Run some more rules that care about quotes and whitespace.\n rules = [\n {regex: /(<[^>]*?style\\s*?=\\s*?\"[^>\"]*?)(?:[\\s]*MSO[-:][^>;\"]*;?)+/gi, replace: \"$1\"},\n {regex: /(<[^>]*?class\\s*?=\\s*?\"[^>\"]*?)(?:[\\s]*MSO[_a-zA-Z0-9-]*)+/gi, replace: \"$1\"},\n {regex: /(<[^>]*?class\\s*?=\\s*?\"[^>\"]*?)(?:[\\s]*Apple-[_a-zA-Z0-9-]*)+/gi, replace: \"$1\"},\n {regex: /
]*?name\\s*?=\\s*?\"OLE_LINK\\d*?\"[^>]*?>\\s*?<\\/a>/gi, replace: \"\"},\n ];\n\n // Apply the rules.\n content = this.filterContentWithRules(content, rules);\n\n // Reapply the standard cleaner to the content.\n return this.cleanHTML(content);\n }\n\n /**\n * Check if the editor allows the use of sub or sup features.\n *\n * @param {String} action - Sub/sup action to check.\n * @return {Boolean} The result after verifying whether it is allowed.\n */\n isSupportSupSub(action) {\n const {type} = this.settings;\n return type === 'both' || type === action;\n }\n\n /**\n * Utility function to filter the content based on the given rules.\n *\n * @param {String} content - The content need to be filtered.\n * @param {Object} rules - The rules list.\n * @return {String} The cleaned content will be returned.\n */\n filterContentWithRules(content, rules) {\n for (const element of rules) {\n content = content.replace(element.regex, element.replace);\n }\n return content;\n }\n\n /**\n * Utility function to clean the HTML.\n *\n * @param {String} content - The content need to be filter.\n * @return {String} The cleaned content will be returned.\n */\n cleanHTML(content) {\n // Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc.\n\n const rules = [\n // Remove empty paragraphs.\n {regex: /]*>( |\\s)*<\\/p>/gi, replace: \"\"},\n\n // Remove attributes on sup and sub tags.\n {regex: /]*( |\\s)*>/gi, replace: \"\"},\n {regex: /]*( |\\s)*>/gi, replace: \"\"},\n\n // Replace   with space.\n {regex: / /gi, replace: \" \"},\n\n // Combine matching tags with spaces in between.\n {regex: /<\\/sup>(\\s*)+/gi, replace: \"$1\"},\n {regex: /<\\/sub>(\\s*)+/gi, replace: \"$1\"},\n\n // Move spaces after start sup and sub tags to before.\n {regex: /(\\s*)+/gi, replace: \"$1\"},\n {regex: /(\\s*)+/gi, replace: \"$1\"},\n\n // Move spaces before end sup and sub tags to after.\n {regex: /(\\s*)+<\\/sup>/gi, replace: \"$1\"},\n {regex: /(\\s*)+<\\/sub>/gi, replace: \"$1\"},\n\n // Remove empty br tags.\n {regex: /
/gi, replace: \"\"},\n\n // Remove any style blocks. Some browsers do not work well with them in a contenteditable.\n // Plus style blocks are not allowed in body html, except with \"scoped\", which most browsers don't support as of 2015.\n // Reference: \"http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work\"\n {regex: /]*>[\\s\\S]*?<\\/style>/gi, replace: \"\"},\n\n // Remove any open HTML comment opens that are not followed by a close. This can completely break page layout.\n {regex: /)/gi, replace: \"\"},\n\n // Remove elements that can not contain visible text.\n {regex: /]*>[\\s\\S]*?<\\/script>/gi, replace: \"\"},\n\n // Source: \"http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html\"\n // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link.\n {regex: /<\\/?(?:br|title|meta|style|std|font|html|body|link|a|ul|li|ol)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:b|i|u|ul|ol|li|img)[^>]*?>/gi, replace: \"\"},\n // Source:\"https://developer.mozilla.org/en/docs/Web/HTML/Element\"\n // Remove all elements except sup and sub.\n {regex: /<\\/?(?:abbr|address|area|article|aside|audio|base|bdi|bdo|blockquote)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:button|canvas|caption|cite|code|col|colgroup|content|data)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:datalist|dd|decorator|del|details|dialog|dfn|div|dl|dt|element)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:h6|header|hgroup|hr|iframe|input|ins|kbd|keygen|label|legend)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:main|map|mark|menu|menuitem|meter|nav|noscript|object|optgroup)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:option|output|p|param|pre|progress|q|rp|rt|rtc|ruby|samp)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:section|select|script|shadow|small|source|std|strong|summary)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:svg|table|tbody|td|template|textarea|time|tfoot|th|thead|tr|track)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:var|wbr|video)[^>]*?>/gi, replace: \"\"},\n\n // Deprecated elements that might still be used by older sites.\n {regex: /<\\/?(?:acronym|applet|basefont|big|blink|center|dir|frame|frameset|isindex)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:listing|noembed|plaintext|spacer|strike|tt|xmp)[^>]*?>/gi, replace: \"\"},\n\n // Elements from common sites including google.com.\n {regex: /<\\/?(?:jsl|nobr)[^>]*?>/gi, replace: \"\"},\n\n {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>[\\s\\S]*?([\\s\\S]*?)<\\/span>/gi, replace: \"$1\"},\n\n // Remove empty spans, but not ones from Rangy.\n {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>( |\\s)*<\\/span>/gi, replace: \"\"},\n {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>[\\s\\S]*?([\\s\\S]*?)<\\/span>/gi, replace: \"$1\"},\n\n // Remove empty sup and sub tags that appear after pasting text.\n {regex: /]*>( |\\s)*<\\/sup>/gi, replace: \"\"},\n {regex: /]*>( |\\s)*<\\/sub>/gi, replace: \"\"},\n\n // Remove special xml namespace tag xmlns generate by browser plugin.\n {regex: /(.*?)<\\/xmlns.*?>/gi, replace: \"$1\"},\n {regex: /\\uFEFF/gi, replace: \"\"}\n ];\n\n return this.filterContentWithRules(content, rules);\n }\n\n /**\n * Clean the generated HTML content without modifying the editor content.\n *\n * This includes removing all YUI IDs from the generated content.\n *\n * @return {string} The cleaned HTML content.\n */\n getCleanHTML() {\n // Clone the editor so that we don't actually modify the real content.\n const editorClone = this.getEditorContent().cloneNode(true);\n let html;\n\n html = editorClone.innerHTML;\n\n // Define contents that are considered empty.\n const emptyContents = [\n '

',\n '


',\n '
',\n '

',\n '


',\n '

',\n '


',\n '

 

',\n '


 

',\n '

 

',\n '


 

',\n '

 

',\n '


 

'\n ];\n\n if (emptyContents.includes(html)) {\n return '';\n }\n\n // Clean the HTML content.\n return this.cleanHTML(html);\n }\n\n\n /**\n * Utility function to get the content element of the editor.\n *\n * @return {HTMLElement} The editor content element.\n */\n getEditorContent() {\n return this.getEditor().querySelector(`.${this.settings.classes.content}`);\n }\n\n /**\n * Utility function to get the editor element. This element will contain all the components of the editor.\n *\n * @return {HTMLElement} The editor element.\n */\n getEditor() {\n return document.getElementById(`${this.settings.classes.editor}-${this.settings.element}`);\n }\n\n /**\n * Utility function to retrieve the button element based on the given type.\n *\n * @param {String} type - The type of the button: sup or sub.\n * @return {HTMLElement} The corresponding button.\n */\n getSupSubButton(type) {\n const {toolbar, button} = this.settings.classes;\n return this.getEditor().querySelector(`.${toolbar} .${button}[data-action^=\"${type}\"]`);\n }\n\n /**\n * Utility function to get button settings (sup/sub) based on the given type.\n *\n * @param {String} type - The type of the button can be either sup or sub.\n * @return {Object} The settings for the given button.\n */\n getActions(type) {\n if (defaultActions[type]) {\n return [defaultActions[type]];\n }\n\n return Object.values(defaultActions);\n }\n\n /**\n * Utility to get the button container for the editor.\n *\n * @return {HTMLElement} The button container.\n */\n getButtonContainer() {\n return this.getEditor().querySelectorAll(`.${this.settings.classes.toolbar} .${this.settings.classes.button}`);\n }\n\n /**\n * Utility function to get the content of the original textarea.\n *\n * @return {String} The content.\n */\n getContent() {\n return this.getTextArea().value;\n }\n\n /**\n * Utility function to get id of the element.\n *\n * @return {String} The element id.\n */\n getEditorId() {\n return this.settings.element;\n }\n\n /**\n * Return the text area element.\n *\n * @return {HTMLElement} Text area element.\n */\n getTextArea() {\n return document.getElementById(this.settings.element);\n }\n\n}\n\n/**\n * Load editor based on the given setting.\n *\n * @param {Object} settings - The editor setting.\n */\nexport const loadEditor = settings => {\n const editor = new OUSupSubEditor(settings);\n // We need to do this for a specific reason, currently only for the Behat test.\n // We can easily utilize the editor's API.\n if (!window.OUSupSubEditor) {\n window.OUSupSubEditor = {\n instances: {\n [settings.element]: editor,\n },\n addEditor: function(editor) {\n this.instances[editor.getEditorId()] = editor;\n },\n getEditorById: function(editorId) {\n return this.instances[editorId];\n },\n };\n } else {\n window.OUSupSubEditor.addEditor(editor);\n }\n};\n"],"names":["defaultActions","sup","name","tag","sub","OUSupSubEditor","constructor","settings","element","type","classes","wrap","editor","contentWrap","content","toolbar","toolbarGroup","button","custom","this","contentElement","createElement","trim","contenteditable","autocapitalize","autocorrect","role","spellcheck","id","replace","addEventListener","saveHistory","document","handleSelectionChange","event","range","window","getSelection","getRangeAt","keyMap","key","shiftKey","preventDefault","handleSupSubHotKey","ctrlKey","handleUndo","handleRedo","cleanHTML","target","innerHTML","isSelectionInsideSubSup","emptyText","createTextNode","insertNode","getTextArea","value","getCleanHTML","handlePaste","wrapContent","appendChild","Object","assign","defaultSetting","init","textareaElement","style","display","editorElement","editorWrap","toolbarEl","initEditorToolbar","contentElementWrap","initEditorContent","contentEditor","querySelector","width","getAttribute","minWidth","maxWidth","height","heightEditor","lineHeightEditor","minHeight","maxHeight","lineHeight","heightContent","textareaLabel","margin","classList","contains","remove","visibility","marginLeft","parseInt","offsetWidth","parentNode","paddingBottom","verticalAlign","insertAdjacentElement","getEditorContent","getContent","requestAnimationFrame","heightWrapper","offsetHeight","e","cleanData","setActiveButton","types","clipboardData","isHTML","includes","cleanPasteHTML","getData","replaceAll","execCommand","historyIndex","history","length","action","nodeEl","nodeName","toLowerCase","setFormat","getActions","isSupportSupSub","selection","anchorNode","node","attributes","attribute","setAttribute","rangeCount","tagName","commonAncestorContainer","isCollapsed","isSupSubTag","nodeNames","selectionNodes","cloneContents","childNodes","textContent","isEqualNode","getEditor","querySelectorAll","forEach","getSupSubButton","add","parent","child","_this$settings","_this$settings$custom2","class","title","buttons","icon","onclick","blur","focus","_this$settings3","_this$settings3$custo","beforeText","innerText","slice","startOffset","afterText","insertBefore","nextSibling","setStart","setEnd","removeAllRanges","addRange","firstChild","selectedText","toString","parentElement","endOffset","start","textNode","end","deleteContents","previousNode","previousSibling","nextNode","newNode","selectNodeContents","el","splice","push","rules","regex","filterContentWithRules","test","holder","html","cloneNode","getElementById","values","getButtonContainer","getEditorId","addEditor","instances","getEditorById","editorId"],"mappings":";;;;;;;8FAuBMA,eAAiB,CACnBC,IAAK,CACDC,KAAM,cACNC,IAAK,YACI,2CAEbC,IAAK,CACDF,KAAM,YACNC,IAAK,YACI,8CAIXE,eAoCFC,YAAYC,gDAjCK,CACbC,QAAS,GACTC,KAAM,OACNC,QAAS,CACLC,KAAM,gBACNC,OAAQ,kBACRC,YAAa,+BACbC,QAAS,0BACTC,QAAS,0BACTC,aAAc,iBACdC,OAAQ,mBAEZC,OAAQ,CACJN,OAAQ,GACRE,QAAS,GACTC,QAAS,GACTE,OAAQ,GACRJ,YAAa,GACbF,KAAM,GACNK,aAAc,qCAKZ,yCACM,6CA0HI,mDACVN,QAACA,QAADQ,OAAUA,QAAUC,KAAKZ,SACzBa,eAAiBD,KAAKE,cAAc,MAAO,QACnCX,QAAQI,QAAU,6BAAOI,OAAOJ,mDAAW,KAAKQ,OAC1DC,iBAAiB,EACjBC,eAAgB,OAChBC,YAAa,MACbC,KAAM,UACNC,YAAY,cACC,MACbC,aAAOT,KAAKZ,SAASC,QAAQqB,QAAQ,KAAM,mBAG/CT,eAAeU,iBAAiB,QAAQ,UAC/BC,iBAITC,SAASF,iBAAiB,mBAAmB,IAAMX,KAAKc,0BAGxDb,eAAeU,iBAAiB,WAAYI,cAGlCC,MADYC,OAAOC,eACDC,WAAW,GAC7BC,OAAS,CACXC,IAAK,SACU,SACL,gBACO,SACP,OAGVC,SAAU,KACD,QACA,YAGTF,OAAOC,IAAIN,MAAMM,MAASN,MAAMO,UAAYF,OAAOE,SAASP,MAAMM,QAClEN,MAAMQ,sBACDC,mBAAmBJ,OAAOC,IAAIN,MAAMM,MAAQD,OAAOE,SAASP,MAAMM,OAGvEN,MAAMU,cACDb,cAGS,UAAdG,MAAMM,KACNN,MAAMQ,iBAINR,MAAMU,SAAyB,MAAdV,MAAMM,MACvBN,MAAMQ,sBACDG,cAGLX,MAAMU,SAAyB,MAAdV,MAAMM,MACvBN,MAAMQ,sBACDI,cAKsC,KAA3C3B,KAAK4B,UAAUb,MAAMc,OAAOC,aAC3B9B,KAAK+B,0BAA2B,OAC3BC,UAAYnB,SAASoB,eAAe,UAC1CjB,MAAMkB,WAAWF,gBAGhBG,cAAcC,MAAQpC,KAAKqC,kBAGpCpC,eAAeU,iBAAiB,SAASI,aAChCuB,YAAYvB,gBAGfwB,YAAcvC,KAAKE,cAAc,MAAO,QAChCX,QAAQG,YAAc,iCAAOK,OAAOL,+DAAe,KAAKS,qBAGjEqC,YAAYD,YAAatC,gBAEvBsC,oBApMFnD,SAAWqD,OAAOC,OAAO1C,KAAK2C,eAAgBvD,eAC9CwD,OAMTA,gCACUC,gBAAkB7C,KAAKmC,eACvB5C,QAACA,QAADQ,OAAUA,QAAUC,KAAKZ,aAE1ByD,uBAILA,gBAAgBC,MAAMC,QAAU,aAE1BC,cAAgBhD,KAAKE,cAAc,MAAO,QAClCX,QAAQE,OAAS,4BAAOM,MAAAA,cAAAA,OAAQN,gDAAU,KAAKU,OACzDM,GAAIlB,QAAQE,OAAS,IAAMO,KAAKZ,SAASC,UAIvC4D,WAAajD,KAAKE,cAAc,MAAO,QAC/BX,QAAQC,KAAO,IAAMO,OAAOP,MAAMW,SAI1C+C,UAAYlD,KAAKmD,yBAClBX,YAAYS,WAAYC,iBAGvBE,mBAAqBpD,KAAKqD,oBAC1BC,cAAgBF,mBAAmBG,yBAAkBvD,KAAKZ,SAASG,QAAQI,eAG5E6C,YAAYS,WAAYG,yBACxBZ,YAAYQ,cAAeC,kBAG1BO,MAAmD,EAA1CxD,KAAKmC,cAAcsB,aAAa,QAAc,GAAM,KACnEH,cAAcR,MAAMU,MAAQA,MAC5BF,cAAcR,MAAMY,SAAWF,MAC/BF,cAAcR,MAAMa,SAAWH,YAGzBI,OAAiB,EADV5D,KAAKmC,cAAcsB,aAAa,QAClB,GACrBI,uBAAkBD,OAAS,SAC3BE,2BAAsBF,OAAS,QAGrCN,cAAcR,MAAMc,OAASC,aAC7BP,cAAcR,MAAMiB,UAAYF,aAChCP,cAAcR,MAAMkB,UAAYH,aAChCP,cAAcR,MAAMmB,WAAaH,uBAE3BI,wBAAmBN,OAAS,QAClCR,mBAAmBN,MAAMiB,UAAYG,oBAE/BC,cAAgBtD,SAAS0C,cAAc,SAAWvD,KAAKZ,SAASC,QAAU,SAEhF8E,cAAcrB,MAAMC,QAAU,eAC9BoB,cAAcrB,MAAMsB,OAAS,EAC7BD,cAAcrB,MAAMc,OAASM,cAC7BC,cAAcrB,MAAMiB,UAAYG,cAChCC,cAAcrB,MAAMkB,UAAYE,cAG5BC,cAAcE,UAAUC,SAAS,cACjCH,cAAcE,UAAUE,OAAO,cAC/BJ,cAAcrB,MAAM0B,WAAa,SACjCxB,cAAcF,MAAM2B,sBAAiBC,SAASP,cAAcQ,uBACzD,CAEqBR,cAAcS,WACtB9B,MAAM+B,cAAgBhB,aACtCM,cAAcrB,MAAMgC,cAAgB,SAGxCjC,gBAAgBkC,sBAAsB,cAAe/B,oBAEhDgC,mBAAmBlD,UAAY9B,KAAKiF,kBAGpCrE,cAILsE,uBAAsB,KAClBf,cAAcrB,MAAMmB,WAAaX,cAAcR,MAAMmB,iBAC/CkB,cAAgBvB,OAAS,EAAIc,SAASxB,UAAUkC,cACtDpC,cAAcF,MAAMc,OAASuB,cAAgB,KAC7CnC,cAAcF,MAAMiB,UAAYoB,cAAgB,KAChDnC,cAAcF,MAAMkB,UAAYmB,cAAgB,QAGpDtE,SAASF,iBAAiB,SAAU0E,QAC3BrC,cAAcsB,SAASe,EAAExD,QAAS,OAE7ByD,UAAYtF,KAAKqC,oBAElBF,cAAcC,MAAQkD,eACtBN,mBAAmBlD,UAAYwD,eAC/BC,iBAAgB,OAqGjCjD,YAAYvB,OACRA,MAAMQ,uBACAiE,MAAQzE,MAAM0E,cAAcD,UAU9B7F,QATA+F,QAAS,EAGTF,MAAAA,OAAAA,MAAOlB,SACPoB,OAASF,MAAMlB,SAAS,aACjBkB,MAAAA,OAAAA,MAAOG,WACdD,OAASF,MAAMG,SAAS,cAKxBhG,QADA+F,OACU1F,KAAK4F,eAAe7E,MAAM0E,cAAcI,QAAQ,cAEhD9E,MAAM0E,cAAcI,QAAQ,cAIpCP,UAAY3F,QAAQmG,WAAW,WAAY,IACjDjF,SAASkF,YAAY,cAAc,EAAOT,gBACrC1E,mBACAuB,cAAcC,MAAQpC,KAAKqC,eAMpCX,aACQ1B,KAAKgG,aAAe,SACfA,oBACAhB,mBAAmBlD,UAAY9B,KAAKiG,QAAQjG,KAAKgG,mBACjD7D,cAAcC,MAAQpC,KAAKiG,QAAQjG,KAAKgG,eAOrDrE,aACQ3B,KAAKgG,aAAehG,KAAKiG,QAAQC,OAAS,SACrCF,oBACAhB,mBAAmBlD,UAAY9B,KAAKiG,QAAQjG,KAAKgG,mBACjD7D,cAAcC,MAAQpC,KAAKiG,QAAQjG,KAAKgG,eASrDxE,mBAAmB2E,cACTC,OAASpG,KAAK+B,6BAChBqE,cACMC,SAAWD,OAAOC,SAASC,cAC7BD,WAAaF,aACRI,UAAUvG,KAAKwG,WAAwB,QAAbH,SAAqB,MAAQ,OAAO,SAIvErG,KAAKyG,gBAAgBN,cAChBI,UAAUvG,KAAKwG,WAAWL,QAAQ,IAQ/CrF,8BACU4F,UAAYzF,OAAOC,kBAGrBlB,KAAKgF,mBAAmBV,SAASoC,UAAUC,YAAa,OAElDC,KAAO5G,KAAK+B,0BAEd6E,UAEKrB,gBAAgBqB,KAAKP,SAASC,oBAG9Bf,iBAAgB,IAYjCrF,cAAclB,SAAK6H,kEAAa,SACtBxH,QAAUwB,SAASX,cAAclB,SAClC,IAAI8H,aAAaD,WAClBxH,QAAQ0H,aAAaD,UAAWD,WAAWC,mBAGxCzH,QAQX0C,gCACU2E,UAAYzF,OAAOC,kBACI,IAAzBwF,UAAUM,kBACH,QAELhG,MAAQ0F,UAAUvF,WAAW,GAC7B8F,QAAUjG,MAAMkG,wBAAwBtC,WAAWyB,YAErDK,UAAUS,oBACNnH,KAAKoH,YAAYH,UACVjG,MAAMkG,wBAAwBtC,eAIzCyC,gBACEC,eAAiBtG,MAAMuG,gBAAgBC,eACxC,IAAIZ,QAAQU,eAAgB,OACvBjB,SAAWO,KAAKP,YACG,KAArBO,KAAKa,iBAGHzH,KAAKoH,YAAYf,WACL,UAAbA,WAAyBrG,KAAKoH,YAAYH,gBACpC,KAENI,YACDA,UAAYT,OAEXS,UAAUK,YAAYd,aAChB,SAIY,UAAvBS,UAAUhB,UAAwBrG,KAAKoH,YAAYH,SAC5CjG,MAAMkG,wBAAwBtC,WAGlCyC,UASXD,YAAYH,eACD,CAAC,MAAO,OAAOtB,SAASsB,SAQnC1B,gBAAgBjG,YACNM,QAACA,QAADE,OAAUA,QAAUE,KAAKZ,SAASG,+DAEnCoI,YAAYC,4BAAqBhI,qBAAYE,SAC7C+H,SAAQ/H,QAAUA,OAAOuE,UAAUE,OAAO,gBAClC,IAATjF,2CACKwI,gBAAgBxI,6FAAO+E,oEAAW0D,IAAI,cAUnDvF,YAAYwF,OAAQC,OAChBD,OAAOxF,YAAYyF,OAQvB9E,uJACUtD,aAAeG,KAAKE,cAAc,MAAO,QACjCF,KAAKZ,SAASG,QAAQM,aAAe,0DAAOG,KAAKZ,mEAAL8I,eAAenI,gDAAfoI,uBAAuBtI,oEAAgB,KAAKM,cAEjGqG,WAAWxG,KAAKZ,SAASE,MAAMuI,SAAS1B,mCACnCrG,OAASE,KAAKE,cAAc,SAAU,qCAC1Bd,2DAAUG,QAAQO,QAAS,IAAMqG,OAAOiC,MACtDC,MAAOrI,KAAKZ,SAASkJ,QAAQnC,OAAOpH,MAAMsJ,MAC1C/I,KAAM,uBACS6G,OAAOpH,OAG1Be,OAAOgC,UAAY9B,KAAKZ,SAASkJ,QAAQnC,OAAOpH,MAAMwJ,KACtDzI,OAAOiH,aAAa,OAAQ,UAC5BjH,OAAO0I,QAAU,WACP9B,UAAYzF,OAAOC,eACnBkF,OAASpG,KAAK+B,6BAChB2E,UAAUS,cAA0B,IAAXf,QACrBA,OAAOC,SAASC,gBAAkBH,OAAOnH,WACzCc,OAAO2I,iBACFzD,mBAAmB0D,aAK3B1D,mBAAmB0D,aACnBnC,UAAUJ,cAGd3D,YAAY3C,aAAcC,iBAE7BoD,UAAYlD,KAAKE,cAAc,MAAO,QAC9BF,KAAKZ,SAASG,QAAQK,QAAU,4DAAOI,KAAKZ,mEAALuJ,gBAAe5I,+CAAf6I,sBAAuBhJ,iEAAW,KAAKO,qBAEvFqC,YAAYU,UAAWrD,cAErBqD,UAQXqD,UAAUJ,cAEAO,UAAYzF,OAAOC,eAEnBF,MAAQ0F,UAAUvF,WAAW,IAC7BnC,IAACA,KAAOmH,OACRC,OAASpG,KAAK+B,6BAEhB2E,UAAUS,YAAa,OAEjBvC,WAAa5D,MAAMkG,wBAAwBtC,cAC7CA,WAAWyB,SAASC,gBAAkBtH,IAAK,OAWrC6J,WAAa7I,KAAKE,cAAclB,KACtC6J,WAAWC,UAAYlE,WAAW6C,YAAYsB,MAAM,EAAG/H,MAAMgI,mBAEvDhH,UAAYnB,SAASoB,eAAe,UAEpCgH,UAAYjJ,KAAKE,cAAclB,KACrCiK,UAAUH,UAAYlE,WAAW6C,YAAYsB,MAAM/H,MAAMgI,aAE7B,KAAxBC,UAAUnH,WACV8C,WAAWA,WAAWsE,aAAaD,UAAWrE,WAAWuE,aAE7DvE,WAAWA,WAAWsE,aAAalH,UAAW4C,WAAWuE,aAC5B,KAAzBN,WAAW/G,WACX8C,WAAWA,WAAWsE,aAAaL,WAAYjE,WAAWuE,aAI9DvE,WAAWL,SAEXvD,MAAMoI,SAASpH,UAAW,GAC1BhB,MAAMqI,OAAOrH,UAAW,GACxB0E,UAAU4C,kBACV5C,UAAU6C,SAASvI,WAChB,OAGG4F,KAAO5G,KAAKE,cAAclB,KAEhC4H,KAAKpE,YAAY3B,SAASoB,eAAe,WAEzCjB,MAAMkB,WAAW0E,MAEjB5F,MAAMoI,SAASxC,KAAK4C,WAAY,GAChCxI,MAAMqI,OAAOzC,KAAK4C,WAAY,GAE9B9C,UAAU4C,kBAEV5C,UAAU6C,SAASvI,aAEpB,GAAIoF,OAAQ,OAOTqD,aAAezI,MAAM0I,WACrBC,cAAgBvD,OAChB+C,YAAcQ,cAAcR,YAC5BN,WAAac,cAAclC,YAAYsB,MAAM,EAAG/H,MAAMgI,aACtDC,UAAYU,cAAclC,YAAYsB,MAAM/H,MAAM4I,cACpDf,WAAY,OACNgB,MAAQ7J,KAAKE,cAAcyJ,cAActD,SAASC,eACxDuD,MAAMpC,YAAcoB,WACpBc,cAAc/E,WAAWsE,aAAaW,MAAOV,mBAG3CW,SAAWjJ,SAASoB,eAAewH,iBACzCE,cAAc/E,WAAWsE,aAAaY,SAAUX,aAC5CF,UAAW,OACLc,IAAM/J,KAAKE,cAAcyJ,cAActD,SAASC,eACtDyD,IAAItC,YAAcwB,UAClBU,cAAc/E,WAAWsE,aAAaa,IAAKZ,aAG/CQ,cAAcpF,SAGdvD,MAAMoI,SAASU,SAAU,GACzB9I,MAAMqI,OAAOS,SAAUL,aAAavD,QACpCQ,UAAU4C,kBACV5C,UAAU6C,SAASvI,YACdmB,cAAcC,MAAQpC,KAAKqC,mBAC7B,OAGGoH,aAAezI,MAAM0I,WAC3B1I,MAAMgJ,uBACAC,aAAejJ,MAAMkG,wBAAwBgD,gBAC7CC,SAAWnJ,MAAMkG,wBAAwBiC,eAE3Cc,cAAgBE,SAAU,oDACpBC,QAAUpK,KAAKE,cAAclB,SAC/BgK,YAAc,EACdY,UAAY,EACZjK,QAAU,MACVsK,eAAgBA,MAAAA,4CAAAA,aAAc5D,uEAAUC,iBAAkBtH,MAC1DW,QAAUsK,aAAaxC,YACvBuB,YAAcrJ,QAAQuG,OACtB+D,aAAa1F,UAEjB5E,SAAW8J,aACXG,UAAYjK,QAAQuG,OAChBiE,WAAYA,MAAAA,qCAAAA,SAAU9D,iEAAUC,iBAAkBtH,MAClDW,SAAWwK,SAAS1C,YACpB0C,SAAS5F,UAEb6F,QAAQ3C,YAAc9H,QAClBA,UAAY8J,oBACZzI,MAAMkB,WAAWkI,SACjBpJ,MAAMoI,SAASgB,QAAQZ,WAAYR,aACnChI,MAAMqI,OAAOe,QAAQZ,WAAYI,qBAC5BzH,cAAcC,MAAQpC,KAAKqC,sBAMlC+H,QAAUvJ,SAASX,cAAclB,KACvCoL,QAAQ5H,YAAY3B,SAASoB,eAAewH,eAE5C/C,UAAU4C,kBAEVtI,MAAMkB,WAAWkI,SACjBpJ,MAAMqJ,mBAAmBD,QAAQZ,YACjC9C,UAAU6C,SAASvI,YAEdgE,mBAAmBwC,WAAWK,SAAQyC,KACnB,UAAhBA,GAAGjE,UAA2C,KAAnBiE,GAAG7C,aAC9B6C,GAAG/F,iBAGNpC,cAAcC,MAAQpC,KAAKqC,oBAG/B2C,mBAAmBwC,WAAWK,SAAQyC,KACnB,UAAhBA,GAAGjE,UAA2C,KAAnBiE,GAAG7C,aAC9B6C,GAAG/F,iBAGN3D,cAMTA,oBACUjB,QAAUK,KAAKqC,gBACM,IAAvBrC,KAAKgG,cAAuBrG,UAAYK,KAAKiG,QAAQjG,KAAKgG,qBACrDC,QAAQsE,OAAOvK,KAAKgG,aAAe,QACnCC,QAAQuE,KAAK7K,cACbqG,gBAUbJ,eAAejG,aAENA,SAA8B,IAAnBA,QAAQuG,aACb,OAIPuE,MAAQ,CACR,CAACC,MAAO,6BAA8BhK,QAAS,IAC/C,CAACgK,MAAO,+BAAgChK,QAAS,IACjD,CAACgK,MAAO,+BAAgChK,QAAS,IACjD,CAACgK,MAAO,8BAA+BhK,QAAS,IAChD,CAACgK,MAAO,kCAAmChK,QAAS,IACpD,CAACgK,MAAO,mBAAoBhK,QAAS,QAIzCf,QAAUK,KAAK2K,uBAAuBhL,QAAS8K,OAMxB,KAHvB9K,QAAUK,KAAK4B,UAAUjC,UAGbuG,SAAiB,KAAK0E,KAAKjL,gBAC5BA,cAILkL,OAAShK,SAASX,cAAc,cACtC2K,OAAO/I,UAAYnC,QACnBA,QAAUkL,OAAO/I,UAGjB+I,OAAO/I,UAAY,GAGnB2I,MAAQ,CACJ,CAACC,MAAO,8DAA+DhK,QAAS,MAChF,CAACgK,MAAO,+DAAgEhK,QAAS,MACjF,CAACgK,MAAO,kEAAmEhK,QAAS,MACpF,CAACgK,MAAO,yDAA0DhK,QAAS,KAI/Ef,QAAUK,KAAK2K,uBAAuBhL,QAAS8K,OAGxCzK,KAAK4B,UAAUjC,SAS1B8G,gBAAgBN,cACN7G,KAACA,MAAQU,KAAKZ,eACJ,SAATE,MAAmBA,OAAS6G,OAUvCwE,uBAAuBhL,QAAS8K,WACvB,MAAMpL,WAAWoL,MAClB9K,QAAUA,QAAQe,QAAQrB,QAAQqL,MAAOrL,QAAQqB,gBAE9Cf,QASXiC,UAAUjC,gBA+ECK,KAAK2K,uBAAuBhL,QA5ErB,CAEV,CAAC+K,MAAO,8BAA+BhK,QAAS,IAGhD,CAACgK,MAAO,2BAA4BhK,QAAS,SAC7C,CAACgK,MAAO,2BAA4BhK,QAAS,SAG7C,CAACgK,MAAO,WAAYhK,QAAS,KAG7B,CAACgK,MAAO,uBAAwBhK,QAAS,MACzC,CAACgK,MAAO,uBAAwBhK,QAAS,MAGzC,CAACgK,MAAO,gBAAiBhK,QAAS,WAClC,CAACgK,MAAO,gBAAiBhK,QAAS,WAGlC,CAACgK,MAAO,kBAAmBhK,QAAS,YACpC,CAACgK,MAAO,kBAAmBhK,QAAS,YAGpC,CAACgK,MAAO,SAAUhK,QAAS,IAK3B,CAACgK,MAAO,kCAAmChK,QAAS,IAGpD,CAACgK,MAAO,wBAAyBhK,QAAS,IAG1C,CAACgK,MAAO,oCAAqChK,QAAS,IAItD,CAACgK,MAAO,0EAA2EhK,QAAS,IAC5F,CAACgK,MAAO,sCAAuChK,QAAS,IAGxD,CAACgK,MAAO,iFAAkFhK,QAAS,IACnG,CAACgK,MAAO,6EAA8EhK,QAAS,IAC/F,CAACgK,MAAO,kFAAmFhK,QAAS,IACpG,CAACgK,MAAO,kFAAmFhK,QAAS,IACpG,CAACgK,MAAO,gFAAiFhK,QAAS,IAClG,CAACgK,MAAO,kFAAmFhK,QAAS,IACpG,CAACgK,MAAO,4EAA6EhK,QAAS,IAC9F,CAACgK,MAAO,gFAAiFhK,QAAS,IAClG,CAACgK,MAAO,qFAAsFhK,QAAS,IACvG,CAACgK,MAAO,iCAAkChK,QAAS,IAGnD,CAACgK,MAAO,uFAAwFhK,QAAS,IACzG,CAACgK,MAAO,kEAAmEhK,QAAS,IAGpF,CAACgK,MAAO,4BAA6BhK,QAAS,IAE9C,CAACgK,MAAO,gFAAiFhK,QAAS,MAGlG,CAACgK,MAAO,0EAA2EhK,QAAS,IAC5F,CAACgK,MAAO,gFAAiFhK,QAAS,MAGlG,CAACgK,MAAO,kCAAmChK,QAAS,IACpD,CAACgK,MAAO,kCAAmChK,QAAS,IAGpD,CAACgK,MAAO,gCAAiChK,QAAS,MAClD,CAACgK,MAAO,WAAYhK,QAAS,MAarC2B,mBAGQyI,KAEJA,KAHoB9K,KAAKgF,mBAAmB+F,WAAU,GAGnCjJ,gBAGG,CAClB,UACA,cACA,OACA,+CACA,mDACA,8CACA,kDACA,gBACA,oBACA,qDACA,yDACA,oDACA,yDAGc6D,SAASmF,MAChB,GAIJ9K,KAAK4B,UAAUkJ,MAS1B9F,0BACWhF,KAAK2H,YAAYpE,yBAAkBvD,KAAKZ,SAASG,QAAQI,UAQpEgI,mBACW9G,SAASmK,yBAAkBhL,KAAKZ,SAASG,QAAQE,mBAAUO,KAAKZ,SAASC,UASpFyI,gBAAgBxI,YACNM,QAACA,QAADE,OAAUA,QAAUE,KAAKZ,SAASG,eACjCS,KAAK2H,YAAYpE,yBAAkB3D,qBAAYE,iCAAwBR,YASlFkH,WAAWlH,aACHT,eAAeS,MACR,CAACT,eAAeS,OAGpBmD,OAAOwI,OAAOpM,gBAQzBqM,4BACWlL,KAAK2H,YAAYC,4BAAqB5H,KAAKZ,SAASG,QAAQK,qBAAYI,KAAKZ,SAASG,QAAQO,SAQzGmF,oBACWjF,KAAKmC,cAAcC,MAQ9B+I,qBACWnL,KAAKZ,SAASC,QAQzB8C,qBACWtB,SAASmK,eAAehL,KAAKZ,SAASC,8BAU3BD,iBAChBK,OAAS,IAAIP,eAAeE,UAG7B6B,OAAO/B,eAaR+B,OAAO/B,eAAekM,UAAU3L,QAZhCwB,OAAO/B,eAAiB,CACpBmM,UAAW,EACNjM,SAASC,SAAUI,QAExB2L,UAAW,SAAS3L,aACX4L,UAAU5L,OAAO0L,eAAiB1L,QAE3C6L,cAAe,SAASC,iBACbvL,KAAKqL,UAAUE"} \ No newline at end of file +{"version":3,"file":"editor.min.js","sources":["../src/editor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * OUSupSub Editor Manager.\n *\n * @module editor_ousupsub/editor\n * @copyright 2024 The Open University.\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst defaultActions = {\n sup: {\n name: 'superscript',\n tag: 'sup',\n 'class': 'ousupsub_superscript_button_superscript',\n },\n sub: {\n name: 'subscript',\n tag: 'sub',\n 'class': 'ousupsub_subscript_button_subscript',\n },\n};\n\nclass OUSupSubEditor {\n\n // The editor's initial default settings.\n defaultSetting = {\n element: '',\n type: 'both',\n classes: {\n wrap: 'ousupsub-wrap',\n editor: 'editor_ousupsub',\n contentWrap: 'editor_ousupsub_content_wrap',\n content: 'editor_ousupsub_content',\n toolbar: 'editor_ousupsub_toolbar',\n toolbarGroup: 'ousupsub_group',\n button: 'ousupsub-button',\n },\n custom: {\n editor: '',\n content: '',\n toolbar: '',\n button: '',\n contentWrap: '',\n wrap: '',\n toolbarGroup: '',\n },\n };\n\n // Support for undo/redo with history data and history index.\n history = [];\n historyIndex = -1;\n\n /**\n * Constructor of the editor.\n * @constructor\n *\n * @param {Object} settings - The editor settings.\n */\n constructor(settings) {\n this.settings = Object.assign(this.defaultSetting, settings);\n this.init();\n }\n\n /**\n * Initial the editor.\n */\n init() {\n const textareaElement = this.getTextArea();\n const {classes, custom} = this.settings;\n\n if (!textareaElement) {\n return;\n }\n // Hidden origin text area.\n textareaElement.style.display = 'none';\n\n const editorElement = this.createElement('div', {\n 'class': (classes.editor + ' ' + (custom?.editor ?? '')).trim(),\n id: classes.editor + '-' + this.settings.element,\n });\n\n // Make editor container.\n const editorWrap = this.createElement('div', {\n 'class': (classes.wrap + ' ' + custom.wrap).trim(),\n });\n\n // Make toolbar containers.\n const toolbarEl = this.initEditorToolbar();\n editorWrap.appendChild(toolbarEl);\n\n // Make the content for editor.\n const contentElementWrap = this.initEditorContent();\n const contentEditor = contentElementWrap.querySelector(`.${this.settings.classes.content}`);\n\n // Append the editor's elements to the DOM.\n editorWrap.appendChild(contentElementWrap);\n editorElement.appendChild(editorWrap);\n\n // Calculate the editor size based on the attributes 'cols' and 'rows'.\n const width = (this.getTextArea().getAttribute('cols') * 6 + 41) + 'px';\n contentEditor.style.width = width;\n contentEditor.style.minWidth = width;\n contentEditor.style.maxWidth = width;\n\n const rows = this.getTextArea().getAttribute('rows');\n const height = (rows * 6 + 13);\n const heightEditor = `${height - 10}px`;\n const lineHeightEditor = `${height - 6}px`;\n\n // Set the size of the editor.\n contentEditor.style.height = heightEditor;\n contentEditor.style.minHeight = heightEditor;\n contentEditor.style.maxHeight = heightEditor;\n contentEditor.style.lineHeight = lineHeightEditor;\n\n const heightContent = `${height + 1}px`;\n contentElementWrap.style.minHeight = heightContent;\n\n const textareaLabel = document.querySelector('[for=\"' + this.settings.element + '\"]');\n\n textareaLabel.style.display = 'inline-block';\n textareaLabel.style.margin = 0;\n textareaLabel.style.height = heightContent;\n textareaLabel.style.minHeight = heightContent;\n textareaLabel.style.maxHeight = heightContent;\n\n // Align for the case using Supsub on the editor.\n if (textareaLabel.classList.contains('accesshide')) {\n textareaLabel.classList.remove('accesshide');\n textareaLabel.style.visibility = 'hidden';\n editorElement.style.marginLeft = `-${parseInt(textareaLabel.offsetWidth)}px`;\n } else {\n // Get parent node of the label.\n const labelParentNode = textareaLabel.parentNode;\n labelParentNode.style.paddingBottom = heightEditor;\n textareaLabel.style.verticalAlign = 'bottom';\n }\n\n textareaElement.insertAdjacentElement('beforebegin', editorElement);\n // Set the editor's content for the first time.\n this.getEditorContent().innerHTML = this.getContent();\n\n // Save the history from the beginning.\n this.saveHistory();\n\n // Wait until the editor element is added to the DOM before calculating\n // its size to ensure it aligns with the others elements.\n requestAnimationFrame(() => {\n textareaLabel.style.lineHeight = contentEditor.style.lineHeight;\n const heightWrapper = height + 1 + parseInt(toolbarEl.offsetHeight);\n editorElement.style.height = heightWrapper + 'px';\n editorElement.style.minHeight = heightWrapper + 'px';\n editorElement.style.maxHeight = heightWrapper + 'px';\n });\n\n document.addEventListener('click', (e) => {\n if (!editorElement.contains(e.target)) {\n // Get clean data.\n const cleanData = this.getCleanHTML();\n // Set it in both the hidden text area and the editor content.\n this.getTextArea().value = cleanData;\n this.getEditorContent().innerHTML = cleanData;\n this.setActiveButton(false);\n }\n });\n }\n\n /**\n * Make a content area for the editor.\n *\n * @return {HTMLElement} The content area element.\n */\n initEditorContent = () => {\n const {classes, custom} = this.settings;\n const contentElement = this.createElement('div', {\n 'class': (classes.content + ' ' + (custom.content ?? '')).trim(),\n contenteditable: true,\n autocapitalize: 'none',\n autocorrect: 'off',\n role: 'textbox',\n spellcheck: false,\n 'aria-live': 'off',\n id: `${this.settings.element.replace(/:/g, \":\")}editable`,\n });\n\n contentElement.addEventListener('blur', () => {\n this.saveHistory();\n });\n\n // Listen for the selection change event.\n document.addEventListener('selectionchange', () => this.handleSelectionChange());\n\n // Set up hotkeys for the editor and prevent the Enter key from making the text content a single line.\n contentElement.addEventListener('keydown', (event) => {\n // Selection range.\n const selection = window.getSelection();\n const range = selection.getRangeAt(0);\n const keyMap = {\n key: {\n 'ArrowUp': 'sup',\n '94': 'sup',\n 'ArrowDown': 'sub',\n '95': 'sub',\n\n },\n shiftKey: {\n '^': 'sup',\n '_': 'sub',\n }\n };\n if (keyMap.key[event.key] || (event.shiftKey && keyMap.shiftKey[event.key])) {\n event.preventDefault();\n this.handleSupSubHotKey(keyMap.key[event.key] || keyMap.shiftKey[event.key]);\n }\n\n if (event.ctrlKey) {\n this.saveHistory();\n }\n\n if (event.key === 'Enter') {\n event.preventDefault();\n }\n\n // Handle undo/redo action.\n if (event.ctrlKey && event.key === 'z') {\n event.preventDefault();\n this.handleUndo();\n }\n\n if (event.ctrlKey && event.key === 'y') {\n event.preventDefault();\n this.handleRedo();\n }\n\n // In case the editor is empty we need to reset format\n // to prevent it remember the previous format.\n if (this.cleanHTML(event.target.innerHTML) === '' &&\n !this.isSelectionInsideSubSup()) {\n const emptyText = document.createTextNode('\\uFEFF');\n range.insertNode(emptyText);\n }\n\n this.getTextArea().value = this.getCleanHTML();\n });\n\n contentElement.addEventListener('paste', event => {\n this.handlePaste(event);\n });\n\n const wrapContent = this.createElement('div', {\n 'class': (classes.contentWrap + ' ' + (custom.contentWrap ?? '')).trim(),\n });\n\n wrapContent.appendChild(contentElement);\n\n return wrapContent;\n };\n\n /**\n * Handle event paste.\n *\n * @param {Event} event Event object.\n */\n handlePaste(event) {\n event.preventDefault();\n const types = event.clipboardData.types;\n let isHTML = false;\n\n // Check for different methods to determine if 'text/html' is present\n if (types?.contains) {\n isHTML = types.contains('text/html');\n } else if (types?.includes) {\n isHTML = types.includes('text/html');\n }\n\n let content;\n if (isHTML) {\n content = this.cleanPasteHTML(event.clipboardData.getData('text/html'));\n } else {\n content = event.clipboardData.getData('text');\n }\n\n // We need to clean the data before inserting it into the editor.\n const cleanData = content.replaceAll(/[\\r\\n]+/g, '');\n document.execCommand('insertHTML', false, cleanData);\n this.saveHistory();\n this.getTextArea().value = this.getCleanHTML();\n }\n\n /**\n * Handle event undo.\n */\n handleUndo() {\n if (this.historyIndex > 0) {\n this.historyIndex--;\n this.getEditorContent().innerHTML = this.history[this.historyIndex];\n this.getTextArea().value = this.history[this.historyIndex];\n }\n }\n\n /**\n * Handle event redo.\n */\n handleRedo() {\n if (this.historyIndex < this.history.length - 1) {\n this.historyIndex++;\n this.getEditorContent().innerHTML = this.history[this.historyIndex];\n this.getTextArea().value = this.history[this.historyIndex];\n }\n }\n\n /**\n * Handle event sup/sub.\n *\n * @param {String} action The sup/sub action.\n */\n handleSupSubHotKey(action) {\n const nodeEl = this.isSelectionInsideSubSup();\n if (nodeEl) {\n const nodeName = nodeEl.nodeName.toLowerCase();\n if (nodeName !== action) {\n this.setFormat(this.getActions(nodeName)[0]);\n }\n return;\n }\n if (this.isSupportSupSub(action)) {\n this.setFormat(this.getActions(action)[0]);\n }\n }\n\n /**\n * Based on the user's selection change, we will detect the pointer position to determine whether\n * the cursor is inside the sup/sub tag. Depending on this result, we will activate the corresponding button.\n */\n handleSelectionChange() {\n const selection = window.getSelection();\n\n // When the user makes a selection change inside the editor.\n if (this.getEditorContent().contains(selection.anchorNode)) {\n // Detect whether the pointer is inside the sup/sub tag.\n const node = this.isSelectionInsideSubSup();\n\n if (node) {\n // Activate the corresponding button in the toolbar.\n this.setActiveButton(node.nodeName.toLowerCase());\n } else {\n // Deactivate all the buttons.\n this.setActiveButton(false);\n }\n }\n }\n\n /**\n * Utility function to create a element with attributes.\n *\n * @param {String} tag - HTML tag name.\n * @param {Object} attributes - The attributes of the element, such as class, id, etc.\n * @return {HTMLElement} The element that was created.\n */\n createElement(tag, attributes = {}) {\n const element = document.createElement(tag);\n for (let attribute in attributes) {\n element.setAttribute(attribute, attributes[attribute]);\n }\n\n return element;\n }\n\n /**\n * Utility function to check whether the current selection is inside the sup/sub tag. Returns false if it's not.\n *\n * @return {Boolean|ParentNode} Return the node if the selection is inside a sup/sub tag; otherwise, return false.\n */\n isSelectionInsideSubSup() {\n const selection = window.getSelection();\n if (selection.rangeCount === 0) {\n return false;\n }\n const range = selection.getRangeAt(0);\n const tagName = range.commonAncestorContainer.parentNode.nodeName;\n // If user doesn't select any text.\n if (selection.isCollapsed) {\n if (this.isSupSubTag(tagName)) {\n return range.commonAncestorContainer.parentNode;\n }\n return false;\n }\n let nodeNames;\n const selectionNodes = range.cloneContents().childNodes;\n for (let node of selectionNodes) {\n const nodeName = node.nodeName;\n if (node.textContent === '') {\n continue;\n }\n if (!(this.isSupSubTag(nodeName)) &&\n (nodeName === '#text' && !this.isSupSubTag(tagName))) {\n return false;\n }\n if (!nodeNames) {\n nodeNames = node;\n }\n if (!nodeNames.isEqualNode(node)) {\n return false;\n }\n }\n\n if (nodeNames.nodeName === '#text' || this.isSupSubTag(tagName)) {\n return range.commonAncestorContainer.parentNode;\n }\n\n return nodeNames;\n }\n\n /**\n * Check if the given tag name is 'sup' or 'sub'. Return true if it is.\n *\n * @param {String} tagName - Tag name need to check.\n * @return {Boolean|ParentNode} Return the node if the selection is inside a sup/sub tag; otherwise, return false.\n */\n isSupSubTag(tagName) {\n return ['SUB', 'SUP'].includes(tagName);\n }\n\n /**\n * Utility function to highlight the sup/sub button.\n *\n * @param {String|Boolean} type - The type of the button: sup or sub.\n */\n setActiveButton(type) {\n const {toolbar, button} = this.settings.classes;\n // Deactivate all the existing buttons.\n this.getEditor().querySelectorAll(`.${toolbar} .${button}`)\n .forEach(button => button.classList.remove('highlight'));\n if (type !== false) {\n this.getSupSubButton(type)?.classList?.add('highlight');\n }\n }\n\n /**\n * Utility function to create a toolbar element that contains sup and sub buttons.\n *\n * @return {HTMLElement} The toolbar element.\n */\n initEditorToolbar() {\n const toolbarGroup = this.createElement('div', {\n 'class': (this.settings.classes.toolbarGroup + ' ' + (this.settings?.custom?.toolbarGroup ?? '')).trim(),\n });\n this.getActions(this.settings.type).forEach((action) => {\n const button = this.createElement('button', {\n 'class': this.settings?.classes.button + ' ' + action.class,\n title: this.settings.buttons[action.name].title,\n type: 'button',\n 'data-action': action.name,\n });\n\n button.innerHTML = this.settings.buttons[action.name].icon;\n button.setAttribute('type', 'button');\n button.onclick = () => {\n const selection = window.getSelection();\n const nodeEl = this.isSelectionInsideSubSup();\n if (selection.isCollapsed && nodeEl !== false) {\n if (nodeEl.nodeName.toLowerCase() !== action.tag) {\n button.blur();\n this.getEditorContent().focus();\n return;\n }\n }\n\n this.getEditorContent().focus();\n this.setFormat(action);\n };\n\n toolbarGroup.appendChild(button);\n });\n const toolbarEl = this.createElement('div', {\n 'class': (this.settings.classes.toolbar + ' ' + (this.settings?.custom?.toolbar ?? '')).trim(),\n });\n toolbarEl.appendChild(toolbarGroup);\n\n return toolbarEl;\n }\n\n /**\n * Based on the action (sup/sub), this function will format the selected text accordingly.\n *\n * @param {Object} action - The sup/sub action object.\n */\n setFormat(action) {\n // Selection text.\n const selection = window.getSelection();\n // Selection range.\n const range = selection.getRangeAt(0);\n const {tag} = action;\n const nodeEl = this.isSelectionInsideSubSup();\n // In case the user doesn't select any text.\n if (selection.isCollapsed) {\n // We need to check whether the current position of the pointer is inside a sub or sup tag.\n const parentNode = range.commonAncestorContainer.parentNode;\n if (parentNode.nodeName.toLowerCase() === tag) {\n // In this case, the pointer is inside a sub or sup tag, so we need to select all the text within the tag.\n // Then, we will slice it into two parts, using the current position of the cursor as the border.\n // The first part will extend from position 0 to the border, and the second part will span from\n // the border to the end.\n // After that, we will wrap each part in the corresponding sub or sup tag. The result will be:\n // First and Second.\n // Finally, we will create a text node with empty content (\\uFEFF) and place it at the border\n // of the two parts, resulting in:\n // First#textnode#Second.\n // Create the first part.\n const beforeText = this.createElement(tag);\n beforeText.innerText = parentNode.textContent.slice(0, range.startOffset);\n // Make an empty textnode.\n const emptyText = document.createTextNode('\\uFEFF');\n // Create an empty text node.\n const afterText = this.createElement(tag);\n afterText.innerText = parentNode.textContent.slice(range.startOffset);\n // Insert it into the DOM next to the parent node.\n if (afterText.innerHTML !== '') {\n parentNode.parentNode.insertBefore(afterText, parentNode.nextSibling);\n }\n parentNode.parentNode.insertBefore(emptyText, parentNode.nextSibling);\n if (beforeText.innerHTML !== '') {\n parentNode.parentNode.insertBefore(beforeText, parentNode.nextSibling);\n }\n\n // Remove the parent node.\n parentNode.remove();\n // We set the position of the cursor to be in the empty text node.\n range.setStart(emptyText, 1);\n range.setEnd(emptyText, 1);\n selection.removeAllRanges();\n selection.addRange(range);\n } else {\n // In case the user didn't select anything, we must create a sup/sub\n // tag with an empty string and move the cursor into it.\n const node = this.createElement(tag);\n // Zero-width space to keep the tag visible.\n node.appendChild(document.createTextNode('\\uFEFF'));\n // Update the new range within the existing one.\n range.insertNode(node);\n // Set the selection index at the next available space.\n range.setStart(node.firstChild, 1);\n range.setEnd(node.firstChild, 1);\n // Remove all existing ranges from the selection.\n selection.removeAllRanges();\n // Add the updated range object to the current selection.\n selection.addRange(range);\n }\n } else if (nodeEl) {\n // This means the user is selecting some text that is inside the sub or sup tag.\n // In this case, we only need to move the selected text inside the sub/sup tag outside of it.\n // For example: 123[456]789. If the selected text is 456, we will\n // move it outside the tag, resulting in 123456789.\n // Retrieve the selected text.\n // Retrieve the current tag (sub/sup) that wraps the selection\n const selectedText = range.toString();\n const parentElement = nodeEl;\n const nextSibling = parentElement.nextSibling;\n const beforeText = parentElement.textContent.slice(0, range.startOffset);\n const afterText = parentElement.textContent.slice(range.endOffset);\n if (beforeText) {\n const start = this.createElement(parentElement.nodeName.toLowerCase());\n start.textContent = beforeText;\n parentElement.parentNode.insertBefore(start, nextSibling);\n }\n // Create a text node based on the selected text.\n const textNode = document.createTextNode(selectedText);\n parentElement.parentNode.insertBefore(textNode, nextSibling);\n if (afterText) {\n const end = this.createElement(parentElement.nodeName.toLowerCase());\n end.textContent = afterText;\n parentElement.parentNode.insertBefore(end, nextSibling);\n }\n\n parentElement.remove();\n\n // Create a new range to select the inserted content\n range.setStart(textNode, 0);\n range.setEnd(textNode, selectedText.length);\n selection.removeAllRanges();\n selection.addRange(range);\n this.getTextArea().value = this.getCleanHTML();\n } else {\n // This case is user select a text that is not inside subsup.\n // We retrieve the selected text and then delete it in DOM.\n const selectedText = range.toString();\n range.deleteContents();\n const previousNode = range.commonAncestorContainer.previousSibling;\n const nextNode = range.commonAncestorContainer.nextSibling;\n // In addition, we will merge adjacent sup/sub tags into a single sup/sub tag.\n if (previousNode || nextNode) {\n const newNode = this.createElement(tag);\n let startOffset = 0;\n let endOffset = 0;\n let content = '';\n if (previousNode && previousNode?.nodeName?.toLowerCase() === tag) {\n content = previousNode.textContent;\n startOffset = content.length;\n previousNode.remove();\n }\n content += selectedText;\n endOffset = content.length;\n if (nextNode && nextNode?.nodeName?.toLowerCase() === tag) {\n content += nextNode.textContent;\n nextNode.remove();\n }\n newNode.textContent = content;\n if (content !== selectedText) {\n range.insertNode(newNode);\n range.setStart(newNode.firstChild, startOffset);\n range.setEnd(newNode.firstChild, endOffset);\n this.getTextArea().value = this.getCleanHTML();\n return;\n }\n }\n\n // Create a sup/sub tag that wrap the selected text.\n const newNode = document.createElement(tag);\n newNode.appendChild(document.createTextNode(selectedText));\n // Make a selection to the selected text.\n selection.removeAllRanges();\n // Insert it into DOM.\n range.insertNode(newNode);\n range.selectNodeContents(newNode.firstChild);\n selection.addRange(range);\n // Clean up all the empty text.\n this.getEditorContent().childNodes.forEach(el => {\n if (el.nodeName === '#text' && el.textContent === '') {\n el.remove();\n }\n });\n this.getTextArea().value = this.getCleanHTML();\n }\n // Clean up.\n this.getEditorContent().childNodes.forEach(el => {\n if (el.nodeName === '#text' && el.textContent === '') {\n el.remove();\n }\n });\n this.saveHistory();\n }\n\n /**\n * Save history for undo/redo actions.\n */\n saveHistory() {\n const content = this.getCleanHTML();\n if (this.historyIndex === -1 || content !== this.history[this.historyIndex]) {\n this.history.splice(this.historyIndex + 1);\n this.history.push(content);\n this.historyIndex++;\n }\n }\n\n /**\n * Cleanup html that comes from WYSIWYG paste events. These are more likely to contain messy code that we should strip.\n *\n * @param {String} content - The content data need to be clean.\n * @return {String} The clean text.\n */\n cleanPasteHTML(content) {\n // Return an empty string if passed an invalid or empty object.\n if (!content || content.length === 0) {\n return \"\";\n }\n\n // Rules that get rid of the real-nasties and don't care about normalize code (correct quotes, white spaces, etc.).\n let rules = [\n {regex: /<\\s*\\/html\\s*>([\\s\\S]+)$/gi, replace: \"\"},\n {regex: //gi, replace: \"\"},\n {regex: //gi, replace: \"\"},\n {regex: /]*>[\\s\\S]*?<\\/xml>/gi, replace: \"\"},\n {regex: /<\\?xml[^>]*>[\\s\\S]*?<\\\\\\?xml>/gi, replace: \"\"},\n {regex: /<\\/?\\w+:[^>]*>/gi, replace: \"\"}\n ];\n\n // Apply the first set of harsher rules.\n content = this.filterContentWithRules(content, rules);\n\n // Apply the standard rules, which mainly cleans things like headers, links, and style blocks.\n content = this.cleanHTML(content);\n\n // Check if the string is empty or only contains whitespace.\n if (content.length === 0 || !/\\S/.test(content)) {\n return content;\n }\n\n // Normalize the code by loading it into the DOM.\n const holder = document.createElement('div');\n holder.innerHTML = content;\n content = holder.innerHTML;\n\n // Free up the DOM memory.\n holder.innerHTML = \"\";\n\n // Run some more rules that care about quotes and whitespace.\n rules = [\n {regex: /(<[^>]*?style\\s*?=\\s*?\"[^>\"]*?)(?:[\\s]*MSO[-:][^>;\"]*;?)+/gi, replace: \"$1\"},\n {regex: /(<[^>]*?class\\s*?=\\s*?\"[^>\"]*?)(?:[\\s]*MSO[_a-zA-Z0-9-]*)+/gi, replace: \"$1\"},\n {regex: /(<[^>]*?class\\s*?=\\s*?\"[^>\"]*?)(?:[\\s]*Apple-[_a-zA-Z0-9-]*)+/gi, replace: \"$1\"},\n {regex: /
]*?name\\s*?=\\s*?\"OLE_LINK\\d*?\"[^>]*?>\\s*?<\\/a>/gi, replace: \"\"},\n ];\n\n // Apply the rules.\n content = this.filterContentWithRules(content, rules);\n\n // Reapply the standard cleaner to the content.\n return this.cleanHTML(content);\n }\n\n /**\n * Check if the editor allows the use of sub or sup features.\n *\n * @param {String} action - Sub/sup action to check.\n * @return {Boolean} The result after verifying whether it is allowed.\n */\n isSupportSupSub(action) {\n const {type} = this.settings;\n return type === 'both' || type === action;\n }\n\n /**\n * Utility function to filter the content based on the given rules.\n *\n * @param {String} content - The content need to be filtered.\n * @param {Object} rules - The rules list.\n * @return {String} The cleaned content will be returned.\n */\n filterContentWithRules(content, rules) {\n for (const element of rules) {\n content = content.replace(element.regex, element.replace);\n }\n return content;\n }\n\n /**\n * Utility function to clean the HTML.\n *\n * @param {String} content - The content need to be filter.\n * @return {String} The cleaned content will be returned.\n */\n cleanHTML(content) {\n // Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc.\n\n const rules = [\n // Remove empty paragraphs.\n {regex: /]*>( |\\s)*<\\/p>/gi, replace: \"\"},\n\n // Remove attributes on sup and sub tags.\n {regex: /]*( |\\s)*>/gi, replace: \"\"},\n {regex: /]*( |\\s)*>/gi, replace: \"\"},\n\n // Replace   with space.\n {regex: / /gi, replace: \" \"},\n\n // Combine matching tags with spaces in between.\n {regex: /<\\/sup>(\\s*)+/gi, replace: \"$1\"},\n {regex: /<\\/sub>(\\s*)+/gi, replace: \"$1\"},\n\n // Move spaces after start sup and sub tags to before.\n {regex: /(\\s*)+/gi, replace: \"$1\"},\n {regex: /(\\s*)+/gi, replace: \"$1\"},\n\n // Move spaces before end sup and sub tags to after.\n {regex: /(\\s*)+<\\/sup>/gi, replace: \"$1\"},\n {regex: /(\\s*)+<\\/sub>/gi, replace: \"$1\"},\n\n // Remove empty br tags.\n {regex: /
/gi, replace: \"\"},\n\n // Remove any style blocks. Some browsers do not work well with them in a contenteditable.\n // Plus style blocks are not allowed in body html, except with \"scoped\", which most browsers don't support as of 2015.\n // Reference: \"http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work\"\n {regex: /]*>[\\s\\S]*?<\\/style>/gi, replace: \"\"},\n\n // Remove any open HTML comment opens that are not followed by a close. This can completely break page layout.\n {regex: /)/gi, replace: \"\"},\n\n // Remove elements that can not contain visible text.\n {regex: /]*>[\\s\\S]*?<\\/script>/gi, replace: \"\"},\n\n // Source: \"http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html\"\n // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link.\n {regex: /<\\/?(?:br|title|meta|style|std|font|html|body|link|a|ul|li|ol)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:b|i|u|ul|ol|li|img)[^>]*?>/gi, replace: \"\"},\n // Source:\"https://developer.mozilla.org/en/docs/Web/HTML/Element\"\n // Remove all elements except sup and sub.\n {regex: /<\\/?(?:abbr|address|area|article|aside|audio|base|bdi|bdo|blockquote)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:button|canvas|caption|cite|code|col|colgroup|content|data)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:datalist|dd|decorator|del|details|dialog|dfn|div|dl|dt|element)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:h6|header|hgroup|hr|iframe|input|ins|kbd|keygen|label|legend)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:main|map|mark|menu|menuitem|meter|nav|noscript|object|optgroup)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:option|output|p|param|pre|progress|q|rp|rt|rtc|ruby|samp)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:section|select|script|shadow|small|source|std|strong|summary)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:svg|table|tbody|td|template|textarea|time|tfoot|th|thead|tr|track)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:var|wbr|video)[^>]*?>/gi, replace: \"\"},\n\n // Deprecated elements that might still be used by older sites.\n {regex: /<\\/?(?:acronym|applet|basefont|big|blink|center|dir|frame|frameset|isindex)[^>]*?>/gi, replace: \"\"},\n {regex: /<\\/?(?:listing|noembed|plaintext|spacer|strike|tt|xmp)[^>]*?>/gi, replace: \"\"},\n\n // Elements from common sites including google.com.\n {regex: /<\\/?(?:jsl|nobr)[^>]*?>/gi, replace: \"\"},\n\n {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>[\\s\\S]*?([\\s\\S]*?)<\\/span>/gi, replace: \"$1\"},\n\n // Remove empty spans, but not ones from Rangy.\n {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>( |\\s)*<\\/span>/gi, replace: \"\"},\n {regex: /]*?rangySelectionBoundary[^>]*?)[^>]*>[\\s\\S]*?([\\s\\S]*?)<\\/span>/gi, replace: \"$1\"},\n\n // Remove empty sup and sub tags that appear after pasting text.\n {regex: /]*>( |\\s)*<\\/sup>/gi, replace: \"\"},\n {regex: /]*>( |\\s)*<\\/sub>/gi, replace: \"\"},\n\n // Remove special xml namespace tag xmlns generate by browser plugin.\n {regex: /(.*?)<\\/xmlns.*?>/gi, replace: \"$1\"},\n {regex: /\\uFEFF/gi, replace: \"\"}\n ];\n\n return this.filterContentWithRules(content, rules);\n }\n\n /**\n * Clean the generated HTML content without modifying the editor content.\n *\n * This includes removing all YUI IDs from the generated content.\n *\n * @return {string} The cleaned HTML content.\n */\n getCleanHTML() {\n // Clone the editor so that we don't actually modify the real content.\n const editorClone = this.getEditorContent().cloneNode(true);\n let html;\n\n html = editorClone.innerHTML;\n\n // Define contents that are considered empty.\n const emptyContents = [\n '

',\n '


',\n '
',\n '

',\n '


',\n '

',\n '


',\n '

 

',\n '


 

',\n '

 

',\n '


 

',\n '

 

',\n '


 

'\n ];\n\n if (emptyContents.includes(html)) {\n return '';\n }\n\n // Clean the HTML content.\n return this.cleanHTML(html);\n }\n\n\n /**\n * Utility function to get the content element of the editor.\n *\n * @return {HTMLElement} The editor content element.\n */\n getEditorContent() {\n return this.getEditor().querySelector(`.${this.settings.classes.content}`);\n }\n\n /**\n * Utility function to get the editor element. This element will contain all the components of the editor.\n *\n * @return {HTMLElement} The editor element.\n */\n getEditor() {\n return document.getElementById(`${this.settings.classes.editor}-${this.settings.element}`);\n }\n\n /**\n * Utility function to retrieve the button element based on the given type.\n *\n * @param {String} type - The type of the button: sup or sub.\n * @return {HTMLElement} The corresponding button.\n */\n getSupSubButton(type) {\n const {toolbar, button} = this.settings.classes;\n return this.getEditor().querySelector(`.${toolbar} .${button}[data-action^=\"${type}\"]`);\n }\n\n /**\n * Utility function to get button settings (sup/sub) based on the given type.\n *\n * @param {String} type - The type of the button can be either sup or sub.\n * @return {Object} The settings for the given button.\n */\n getActions(type) {\n if (defaultActions[type]) {\n return [defaultActions[type]];\n }\n\n return Object.values(defaultActions);\n }\n\n /**\n * Utility to get the button container for the editor.\n *\n * @return {HTMLElement} The button container.\n */\n getButtonContainer() {\n return this.getEditor().querySelectorAll(`.${this.settings.classes.toolbar} .${this.settings.classes.button}`);\n }\n\n /**\n * Utility function to get the content of the original textarea.\n *\n * @return {String} The content.\n */\n getContent() {\n return this.getTextArea().value;\n }\n\n /**\n * Utility function to get id of the element.\n *\n * @return {String} The element id.\n */\n getEditorId() {\n return this.settings.element;\n }\n\n /**\n * Return the text area element.\n *\n * @return {HTMLElement} Text area element.\n */\n getTextArea() {\n return document.getElementById(this.settings.element);\n }\n\n}\n\n/**\n * Load editor based on the given setting.\n *\n * @param {Object} settings - The editor setting.\n */\nexport const loadEditor = settings => {\n const editor = new OUSupSubEditor(settings);\n // We need to do this for a specific reason, currently only for the Behat test.\n // We can easily utilize the editor's API.\n if (!window.OUSupSubEditor) {\n window.OUSupSubEditor = {\n instances: {\n [settings.element]: editor,\n },\n addEditor: function(editor) {\n this.instances[editor.getEditorId()] = editor;\n },\n getEditorById: function(editorId) {\n return this.instances[editorId];\n },\n };\n } else {\n window.OUSupSubEditor.addEditor(editor);\n }\n};\n"],"names":["defaultActions","sup","name","tag","sub","OUSupSubEditor","constructor","settings","element","type","classes","wrap","editor","contentWrap","content","toolbar","toolbarGroup","button","custom","this","contentElement","createElement","trim","contenteditable","autocapitalize","autocorrect","role","spellcheck","id","replace","addEventListener","saveHistory","document","handleSelectionChange","event","range","window","getSelection","getRangeAt","keyMap","key","shiftKey","preventDefault","handleSupSubHotKey","ctrlKey","handleUndo","handleRedo","cleanHTML","target","innerHTML","isSelectionInsideSubSup","emptyText","createTextNode","insertNode","getTextArea","value","getCleanHTML","handlePaste","wrapContent","appendChild","Object","assign","defaultSetting","init","textareaElement","style","display","editorElement","editorWrap","toolbarEl","initEditorToolbar","contentElementWrap","initEditorContent","contentEditor","querySelector","width","getAttribute","minWidth","maxWidth","height","heightEditor","lineHeightEditor","minHeight","maxHeight","lineHeight","heightContent","textareaLabel","margin","classList","contains","remove","visibility","marginLeft","parseInt","offsetWidth","parentNode","paddingBottom","verticalAlign","insertAdjacentElement","getEditorContent","getContent","requestAnimationFrame","heightWrapper","offsetHeight","e","cleanData","setActiveButton","types","clipboardData","isHTML","includes","cleanPasteHTML","getData","replaceAll","execCommand","historyIndex","history","length","action","nodeEl","nodeName","toLowerCase","setFormat","getActions","isSupportSupSub","selection","anchorNode","node","attributes","attribute","setAttribute","rangeCount","tagName","commonAncestorContainer","isCollapsed","isSupSubTag","nodeNames","selectionNodes","cloneContents","childNodes","textContent","isEqualNode","getEditor","querySelectorAll","forEach","getSupSubButton","add","_this$settings","_this$settings$custom2","class","title","buttons","icon","onclick","blur","focus","_this$settings3","_this$settings3$custo","beforeText","innerText","slice","startOffset","afterText","insertBefore","nextSibling","setStart","setEnd","removeAllRanges","addRange","firstChild","selectedText","toString","parentElement","endOffset","start","textNode","end","deleteContents","previousNode","previousSibling","nextNode","newNode","selectNodeContents","el","splice","push","rules","regex","filterContentWithRules","test","holder","html","cloneNode","getElementById","values","getButtonContainer","getEditorId","addEditor","instances","getEditorById","editorId"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/editor.js b/amd/src/editor.js index 473ab1c..e9d6c86 100644 --- a/amd/src/editor.js +++ b/amd/src/editor.js @@ -100,15 +100,15 @@ class OUSupSubEditor { // Make toolbar containers. const toolbarEl = this.initEditorToolbar(); - this.appendChild(editorWrap, toolbarEl); + editorWrap.appendChild(toolbarEl); // Make the content for editor. const contentElementWrap = this.initEditorContent(); const contentEditor = contentElementWrap.querySelector(`.${this.settings.classes.content}`); // Append the editor's elements to the DOM. - this.appendChild(editorWrap, contentElementWrap); - this.appendChild(editorElement, editorWrap); + editorWrap.appendChild(contentElementWrap); + editorElement.appendChild(editorWrap); // Calculate the editor size based on the attributes 'cols' and 'rows'. const width = (this.getTextArea().getAttribute('cols') * 6 + 41) + 'px'; @@ -265,7 +265,7 @@ class OUSupSubEditor { 'class': (classes.contentWrap + ' ' + (custom.contentWrap ?? '')).trim(), }); - this.appendChild(wrapContent, contentElement); + wrapContent.appendChild(contentElement); return wrapContent; }; @@ -333,7 +333,7 @@ class OUSupSubEditor { if (nodeEl) { const nodeName = nodeEl.nodeName.toLowerCase(); if (nodeName !== action) { - this.setFormat(this.getActions(nodeName === 'sup' ? 'sub' : 'sup')[0]); + this.setFormat(this.getActions(nodeName)[0]); } return; } @@ -450,16 +450,6 @@ class OUSupSubEditor { } } - /** - * Utility function to append a child node to a parent node. - * - * @param {HTMLElement} parent - The parent node that will contain the child node. - * @param {HTMLElement} child - The child node. - */ - appendChild(parent, child) { - parent.appendChild(child); - } - /** * Utility function to create a toolbar element that contains sup and sub buttons. * @@ -494,12 +484,12 @@ class OUSupSubEditor { this.setFormat(action); }; - this.appendChild(toolbarGroup, button); + toolbarGroup.appendChild(button); }); const toolbarEl = this.createElement('div', { 'class': (this.settings.classes.toolbar + ' ' + (this.settings?.custom?.toolbar ?? '')).trim(), }); - this.appendChild(toolbarEl, toolbarGroup); + toolbarEl.appendChild(toolbarGroup); return toolbarEl; } diff --git a/lib.php b/lib.php index 87d6652..1a7ca3e 100644 --- a/lib.php +++ b/lib.php @@ -82,28 +82,12 @@ public function use_editor($elementid, ?array $options = null, $fpoptions = null $options['supsub'] = 'both'; } - switch ($options['supsub']) { - case 'both': - $groups = ['style1' => ['superscript', 'subscript']]; - break; - - case 'sup': - $groups = ['style1' => ['superscript']]; - break; - - case 'sub': - $groups = ['style1' => ['subscript']]; - break; - - default: - throw new coding_exception("Invalid value '" .$options['supsub'] . - "' for option 'supsub'. Must be one of 'both', 'sup' or 'sub'."); + if (!in_array($options['supsub'], ['sup', 'sub', 'both'])) { + throw new coding_exception("Invalid value '" .$options['supsub'] . + "' for option 'supsub'. Must be one of 'both', 'sup' or 'sub'."); } - $groupplugins = []; - foreach ($groups['style1'] as $plugin) { - $groupplugins[] = ['name' => $plugin, 'params' => []]; - } + $options['supsub'] = 'both'; $PAGE->requires->js_call_amd('editor_ousupsub/editor', 'loadEditor', [ [ diff --git a/readme.md b/readme.md index 0487a52..b18bb69 100644 --- a/readme.md +++ b/readme.md @@ -53,28 +53,9 @@ However, this only works if the surrounding text is styled not to extremely. We * https://moodle.org/plugins/qtype_varnumunit * https://moodle.org/plugins/qtype_combined (Only work if the surrounding text is styled not to extremely) -## Standalone version - -More details are in readme_standalone.txt that gets added to the /standalone folder -A standalone/offline version of the editor is also provided in the /standalone folder. This provides all the -functionality of the editor in a package that works in an ereader or mobile app or on a desktop to demonstrate -the functionality of the editor outside of moodle. There feature is currently in beta. - -The standalone version is kept up to date by running the buildstandalone.php in the same way you run a behat script -The full command we use is php lib/editor/ousupsub/buildstandalone.php - -Running this script outputs a list of files and features that have been created. First it deletes the contents of the -standalone folder and then it recreates the standalone files. This ensures the standalone version is as up to date with -the plugin. This task is performed during development of the editor. If you are using it out of the box you shouldn't -need to run this script. - ## Testing -Automated testing is through behat and custom javascript unit tests. There is a behat test for the moodle plugin and an -identical test for the standalone version - -The javascript unit tests run in a browser. Load /tests/fixtures/testcleanup.html in a specific browser to see if the -tests pass in that browser. +Automated testing is through behat and custom javascript unit tests. The editor will work any where moodle editors work but it's designed to be used with specific OU question types The main places to test are: @@ -98,16 +79,3 @@ Then we check that subscript was applied correctly. Then I should see "Superscript and Subscript" in the "Description" ousupsub editor That is how you read the behat tests and how you know what to expect the editor to do. - -## Third-party code - -Thanks to the creators of the rangy software library, which we use. - -1) Rangy (version 1.2.3) - * Download the latest stable release; - * Copy the content of the 'currentrelease/uncompressed' folder into yui/src/rangy/js - * Run shifter against yui/src/rangy - - Notes: - * We have patched 1.2.3 with a backport fix from the next release of Rangy which addresses an incompatibility - between Rangy and HTML5Shiv which is used in the bootstrapclean theme. See MDL-44798 for further information. diff --git a/version.php b/version.php index 678456c..a998061 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2022041100; +$plugin->version = 2024112600; $plugin->requires = 2020061500; $plugin->component = 'editor_ousupsub'; $plugin->maturity = MATURITY_STABLE;