diff --git a/amd/build/constants.min.js b/amd/build/constants.min.js new file mode 100644 index 0000000..d2b288b --- /dev/null +++ b/amd/build/constants.min.js @@ -0,0 +1,11 @@ +define("tiny_multilang2/constants",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.trim=_exports.spanMultilangEnd=_exports.spanMultilangBegin=_exports.isNull=_exports.blockTags=void 0; +/** + * Some constants that are required throughout the plugin. + * + * @module tiny_multilang2 + * @copyright 2024 Stephan Robotta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const spanFixedAttrs='{mlang %lang}';_exports.spanMultilangBegin=spanMultilangBegin;const spanMultilangEnd=spanFixedAttrs.replace("begin","end")+">{mlang}";_exports.spanMultilangEnd=spanMultilangEnd;_exports.trim=v=>v.toString().replace(/^\s+/,"").replace(/\s+$/,"");_exports.isNull=a=>null==a;_exports.blockTags=["address","article","aside","blockquote","dd","div","dl","dt","figcaption","h1","h2","h3","h4","h5","h6","li","ol","p","pre","section","tfoot","ul"]})); + +//# sourceMappingURL=constants.min.js.map \ No newline at end of file diff --git a/amd/build/constants.min.js.map b/amd/build/constants.min.js.map new file mode 100644 index 0000000..d0ef585 --- /dev/null +++ b/amd/build/constants.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"constants.min.js","sources":["../src/constants.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Some constants that are required throughout the plugin.\n *\n * @module tiny_multilang2\n * @copyright 2024 Stephan Robotta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// This class inside a identified the {mlang} tag that is encapsulated in a span.\nconst spanClass = 'multilang-begin mceNonEditable';\n// This is the element with the data attribute.\nconst spanFixedAttrs = '{mlang %lang}';\n// The end span doesn't need information about the used language.\nexport const spanMultilangEnd = spanFixedAttrs.replace('begin', 'end') + '>{mlang}';\n// Helper functions\nexport const trim = v => v.toString().replace(/^\\s+/, '').replace(/\\s+$/, '');\nexport const isNull = a => a === null || a === undefined;\n// These are HTML block elements that are not allowed to be inside a {mlang} tag.\nexport const blockTags = ['address', 'article', 'aside', 'blockquote',\n 'dd', 'div', 'dl', 'dt', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n 'li', 'ol', 'p', 'pre', 'section', 'tfoot', 'ul'];"],"names":["spanFixedAttrs","spanMultilangBegin","spanMultilangEnd","replace","v","toString","a"],"mappings":";;;;;;;;MA0BMA,eAAiB,wGAEVC,mBAAqBD,eAAiB,2GAEtCE,iBAAmBF,eAAeG,QAAQ,QAAS,OAAS,2EAErDC,GAAKA,EAAEC,WAAWF,QAAQ,OAAQ,IAAIA,QAAQ,OAAQ,oBACpDG,GAAKA,MAAAA,qBAEF,CAAC,UAAW,UAAW,QAAS,aACrD,KAAM,MAAO,KAAM,KAAM,aAAc,KAAM,KAAM,KAAM,KAAM,KAAM,KACrE,KAAM,KAAM,IAAK,MAAO,UAAW,QAAS"} \ No newline at end of file diff --git a/amd/build/htmlparser.min.js b/amd/build/htmlparser.min.js new file mode 100644 index 0000000..0a33969 --- /dev/null +++ b/amd/build/htmlparser.min.js @@ -0,0 +1,12 @@ +define("tiny_multilang2/htmlparser",["exports","./constants"],(function(_exports,_constants){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.parseEditorContent=void 0; +/** + * Handling of the editor content to add and remove the visual styling and + * helper nodes to modify language settings. + * + * @module tiny_multilang2 + * @copyright 2024 Stephan Robotta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class HTMLParser{constructor(){this.onTagOpen=null,this.onTagClose=null,this.onText=null,this.chunk="",this.parse=function(input){let content=input;for(;content.length>0;){let match=content.match(/<[^>]*>/);if(match){let index=match.index;if(index>0&&(this.chunk=content.substring(0,index),"function"==typeof this.onText&&this.onText(this.chunk)),this.chunk=match[0],"/"===match[0].charAt(1))"function"==typeof this.onTagClose&&this.onTagClose(match[0].substring(2,match[0].length-1).trim());else if("function"==typeof this.onTagOpen){const attr1=this.mapAttrs(match[0].match(/([\w\-_]+)="([^"]*)"/g)),attr2=this.mapAttrs(match[0].match(/([\w\-_]+)='([^']*)'/g)),tag=match[0].match(/^<(\w+)/);this.onTagOpen(tag[1].toLowerCase(),{...attr1,...attr2})}content=content.substring(index+match[0].length)}else"function"==typeof this.onText&&this.onText(content),this.chunk=content,content=""}},this.getChunk=function(){return this.chunk},this.mapAttrs=function(attrs){let res={};if(attrs)for(let i=0;i-1?mlang++:"span"===tag&&attr.class&&attr.class.indexOf("multilang-end")>-1&&(mlang--,inClose=!0),newHtml+=parser.getChunk()},parser.onTagClose=function(tag){if(_constants.blockTags.indexOf(tag)>-1&&0!=mlang)if(mlang>0)newHtml+=_constants.spanMultilangEnd,mlang--;else{const t=newHtml.lastIndexOf(_constants.spanMultilangEnd);newHtml=newHtml.substring(0,t)+_constants.spanMultilangBegin.replace(new RegExp("%lang","g"),"other")+newHtml.substring(t),mlang++}else"span"===tag&&inClose&&(inClose=!1),newHtml+=parser.getChunk()},parser.onText=function(text){if(mlang>0||inClose)return void(newHtml+=text);const intermediateReplacements=[];for(;;){const m=text.match(new RegExp("{\\s*mlang(\\s+([^}]+?))?\\s*}","i"));if(!m)break;const textBefore=text.substring(0,m.index),textAfter=text.substring(m.index+m[0].length);let r=m[0];m[2]?(r=_constants.spanMultilangBegin.replace(new RegExp("%lang","g"),m[2]),mlang++):(r=_constants.spanMultilangEnd,mlang--),intermediateReplacements.push(r),text="".concat(textBefore,"___~~").concat(intermediateReplacements.length,"~~___").concat(textAfter)}for(let i=0;i.\n\n/**\n * Handling of the editor content to add and remove the visual styling and\n * helper nodes to modify language settings.\n *\n * @module tiny_multilang2\n * @copyright 2024 Stephan Robotta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {spanMultilangBegin, spanMultilangEnd, blockTags} from './constants';\n\n/**\n * This class is used to parse HTML content and call a callback function\n * when a tag is opened, closed or text is found.\n */\nclass HTMLParser {\n constructor() {\n this.onTagOpen = null;\n this.onTagClose = null;\n this.onText = null;\n this.chunk = '';\n this.parse = function(input) {\n let content = input;\n while (content.length > 0) {\n let match = content.match(/<[^>]*>/);\n if (match) {\n let index = match.index;\n if (index > 0) {\n this.chunk = content.substring(0, index);\n if (typeof this.onText === 'function') {\n this.onText(this.chunk);\n }\n }\n this.chunk = match[0];\n if (match[0].charAt(1) === '/') {\n if (typeof this.onTagClose === 'function') {\n this.onTagClose(match[0].substring(2, match[0].length - 1).trim());\n }\n } else if (typeof this.onTagOpen === 'function') {\n const attr1 = this.mapAttrs(match[0].match(/([\\w\\-_]+)=\"([^\"]*)\"/g));\n const attr2 = this.mapAttrs(match[0].match(/([\\w\\-_]+)='([^']*)'/g));\n const tag = match[0].match(/^<(\\w+)/);\n this.onTagOpen(tag[1].toLowerCase(), {...attr1, ...attr2});\n }\n content = content.substring(index + match[0].length);\n } else {\n if (typeof this.onText === 'function') {\n this.onText(content);\n }\n this.chunk = content;\n content = '';\n }\n }\n };\n this.getChunk = function() {\n return this.chunk;\n };\n this.mapAttrs = function(attrs) {\n let res = {};\n if (attrs) {\n for (let i = 0; i < attrs.length; i++) {\n let [k, v] = attrs[i].split('=');\n res[k] = v ? v.substring(1, v.length) : null;\n }\n }\n return res;\n };\n }\n}\n\nexport const parseEditorContent = function(html) {\n let newHtml = '';\n let mlang = 0;\n let inClose = false;\n const parser = new HTMLParser();\n parser.onTagOpen = function(tag, attr) {\n if (tag === 'span' && attr.class && attr.class.indexOf('multilang-begin') > -1) {\n mlang++;\n } else if (tag === 'span' && attr.class && attr.class.indexOf('multilang-end') > -1) {\n mlang--;\n inClose = true;\n }\n newHtml += parser.getChunk();\n };\n parser.onTagClose = function(tag) {\n if (blockTags.indexOf(tag) > -1 && mlang != 0) {\n if (mlang > 0) {\n newHtml += spanMultilangEnd;\n mlang--;\n } else {\n const t = newHtml.lastIndexOf(spanMultilangEnd);\n newHtml = newHtml.substring(0, t)\n + spanMultilangBegin.replace(new RegExp('%lang', 'g'), 'other')\n + newHtml.substring(t);\n mlang++;\n }\n return;\n }\n if (tag === 'span' && inClose) {\n inClose = false;\n }\n newHtml += parser.getChunk();\n };\n parser.onText = function(text) {\n if (mlang > 0 || inClose) {\n newHtml += text;\n return;\n }\n const intermediateReplacements = [];\n // eslint-disable-next-line no-constant-condition\n while (1) {\n const m = text.match(new RegExp('{\\\\s*mlang(\\\\s+([^}]+?))?\\\\s*}', 'i'));\n if (!m) {\n break;\n }\n const textBefore = text.substring(0, m.index);\n const textAfter = text.substring(m.index + m[0].length);\n let r = m[0];\n if (!m[2]) {\n r = spanMultilangEnd;\n mlang--;\n } else {\n r = spanMultilangBegin.replace(new RegExp('%lang', 'g'), m[2]);\n mlang++;\n }\n intermediateReplacements.push(r);\n text = `${textBefore}___~~${intermediateReplacements.length}~~___${textAfter}`;\n }\n // Revert all placeholders back to the original {mlang} tags.\n for (let i = 0; i < intermediateReplacements.length; i++) {\n text = text.replace(`___~~${i + 1}~~___`, intermediateReplacements[i]);\n }\n newHtml += text;\n };\n parser.parse(html);\n return newHtml;\n};\n"],"names":["HTMLParser","constructor","onTagOpen","onTagClose","onText","chunk","parse","input","content","length","match","index","substring","this","charAt","trim","attr1","mapAttrs","attr2","tag","toLowerCase","getChunk","attrs","res","i","k","v","split","html","newHtml","mlang","inClose","parser","attr","class","indexOf","blockTags","spanMultilangEnd","t","lastIndexOf","spanMultilangBegin","replace","RegExp","text","intermediateReplacements","m","textBefore","textAfter","r","push"],"mappings":";;;;;;;;;MA8BMA,WACFC,mBACSC,UAAY,UACZC,WAAa,UACbC,OAAS,UACTC,MAAQ,QACRC,MAAQ,SAASC,WACdC,QAAUD,WACPC,QAAQC,OAAS,GAAG,KACnBC,MAAQF,QAAQE,MAAM,cACtBA,MAAO,KACHC,MAAQD,MAAMC,SACdA,MAAQ,SACHN,MAAQG,QAAQI,UAAU,EAAGD,OACP,mBAAhBE,KAAKT,aACPA,OAAOS,KAAKR,aAGpBA,MAAQK,MAAM,GACQ,MAAvBA,MAAM,GAAGI,OAAO,GACe,mBAApBD,KAAKV,iBACPA,WAAWO,MAAM,GAAGE,UAAU,EAAGF,MAAM,GAAGD,OAAS,GAAGM,aAE5D,GAA8B,mBAAnBF,KAAKX,UAA0B,OACvCc,MAAQH,KAAKI,SAASP,MAAM,GAAGA,MAAM,0BACrCQ,MAAQL,KAAKI,SAASP,MAAM,GAAGA,MAAM,0BACrCS,IAAMT,MAAM,GAAGA,MAAM,gBACtBR,UAAUiB,IAAI,GAAGC,cAAe,IAAIJ,SAAUE,QAEvDV,QAAUA,QAAQI,UAAUD,MAAQD,MAAM,GAAGD,YAElB,mBAAhBI,KAAKT,aACPA,OAAOI,cAEXH,MAAQG,QACbA,QAAU,UAIjBa,SAAW,kBACLR,KAAKR,YAEXY,SAAW,SAASK,WACjBC,IAAM,MACND,UACK,IAAIE,EAAI,EAAGA,EAAIF,MAAMb,OAAQe,IAAK,KAC9BC,EAAGC,GAAKJ,MAAME,GAAGG,MAAM,KAC5BJ,IAAIE,GAAKC,EAAIA,EAAEd,UAAU,EAAGc,EAAEjB,QAAU,YAGzCc,kCAKe,SAASK,UACnCC,QAAU,GACVC,MAAQ,EACRC,SAAU,QACRC,OAAS,IAAIhC,kBACnBgC,OAAO9B,UAAY,SAASiB,IAAKc,MACjB,SAARd,KAAkBc,KAAKC,OAASD,KAAKC,MAAMC,QAAQ,oBAAsB,EACzEL,QACe,SAARX,KAAkBc,KAAKC,OAASD,KAAKC,MAAMC,QAAQ,kBAAoB,IAC9EL,QACAC,SAAU,GAEdF,SAAWG,OAAOX,YAEtBW,OAAO7B,WAAa,SAASgB,QACrBiB,qBAAUD,QAAQhB,MAAQ,GAAc,GAATW,SAC3BA,MAAQ,EACRD,SAAWQ,4BACXP,YACG,OACGQ,EAAIT,QAAQU,YAAYF,6BAC9BR,QAAUA,QAAQjB,UAAU,EAAG0B,GACzBE,8BAAmBC,QAAQ,IAAIC,OAAO,QAAS,KAAM,SACrDb,QAAQjB,UAAU0B,GACxBR,YAII,SAARX,KAAkBY,UAClBA,SAAU,GAEdF,SAAWG,OAAOX,YAEtBW,OAAO5B,OAAS,SAASuC,SACjBb,MAAQ,GAAKC,oBACbF,SAAWc,YAGTC,yBAA2B,UAEvB,OACAC,EAAIF,KAAKjC,MAAM,IAAIgC,OAAO,iCAAkC,UAC7DG,cAGCC,WAAaH,KAAK/B,UAAU,EAAGiC,EAAElC,OACjCoC,UAAYJ,KAAK/B,UAAUiC,EAAElC,MAAQkC,EAAE,GAAGpC,YAC5CuC,EAAIH,EAAE,GACLA,EAAE,IAIHG,EAAIR,8BAAmBC,QAAQ,IAAIC,OAAO,QAAS,KAAMG,EAAE,IAC3Df,UAJAkB,EAAIX,4BACJP,SAKJc,yBAAyBK,KAAKD,GAC9BL,eAAUG,2BAAkBF,yBAAyBnC,uBAAcsC,eAGlE,IAAIvB,EAAI,EAAGA,EAAIoB,yBAAyBnC,OAAQe,IACjDmB,KAAOA,KAAKF,uBAAgBjB,EAAI,WAAUoB,yBAAyBpB,IAEvEK,SAAWc,MAEfX,OAAO1B,MAAMsB,MACNC"} \ No newline at end of file diff --git a/amd/build/ui.min.js b/amd/build/ui.min.js index 4b34376..52a4185 100644 --- a/amd/build/ui.min.js +++ b/amd/build/ui.min.js @@ -1,4 +1,4 @@ -define("tiny_multilang2/ui",["exports","./options"],(function(_exports,_options2){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.onSubmit=_exports.onInit=_exports.onFocus=_exports.onDelete=_exports.onBeforeGetContent=_exports.applyLanguage=void 0; +define("tiny_multilang2/ui",["exports","./options","./constants","./htmlparser"],(function(_exports,_options2,_constants,_htmlparser){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.onSubmit=_exports.onInit=_exports.onFocus=_exports.onDelete=_exports.onBeforeGetContent=_exports.applyLanguage=void 0; /** * Commands for the plugin logic of the Moodle tiny_multilang2 plugin. * @@ -9,6 +9,6 @@ define("tiny_multilang2/ui",["exports","./options"],(function(_exports,_options2 * @copyright 2015 onwards Iñaki Arenaza & Mondragon Unibertsitatea * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const spanClass="multilang-begin mceNonEditable",spanFixedAttrs='{mlang %lang}',spanMultilangEnd=spanFixedAttrs.replace("begin","end")+">{mlang}",isNull=a=>null==a;let _isSubmit=!1;const _options={},addVisualStyling=function(ed){let content=ed.getContent();if(-1!==content.indexOf(spanClass))return content;let intermediateReplacements=[];const replaceHelper=function(m0,m1,p,c){const textBefore=c.substring(0,p),textAfter=c.substring(p+m0.length);return textBefore.lastIndexOf("<")")&&(m0=m1?spanMultilangBegin.replace(new RegExp("%lang","g"),m1):spanMultilangEnd),intermediateReplacements.push(m0),"".concat(textBefore,"___~~").concat(intermediateReplacements.length,"~~___").concat(textAfter)};for(;;){const m=content.match(new RegExp("{\\s*mlang(\\s+([^}]+?))?\\s*}","i"));if(!m)break;content=replaceHelper(m[0],m[2],m.index,m.input)}for(let i=0;i').concat(innerHTML,"");for(end of(ed.dom.setOuterHTML(span,newHTML),toRemove))ed.dom.remove(end)}}}else ed.dom.setOuterHTML(span,span.innerHTML.toLowerCase())}))},getHighlightNodeFromSelect=function(ed,search){let span;return ed.dom.getParents(ed.selection.getStart(),(elm=>{if(!isNull(elm.classList)){const pair="begin"===search?"end":"begin";if(elm.classList.contains("multilang-"+pair)){span=elm;do{span="begin"===search?span.previousSibling:span.nextSibling}while(!isNull(span)&&(isNull(span.classList)||!span.classList.contains("multilang-"+search)))}else elm.classList.contains("multilang-"+search)&&(span=elm)}})),span},hideContentToolbar=function(el){for(;!isNull(el);){if(el.nodeType===Node.ELEMENT_NODE&&!isNull(el.getAttribute("class"))&&-1!=el.getAttribute("class").indexOf("tox-pop-"))return void(el.style.display="none");el=el.parentNode}};_exports.onInit=function(ed,options){Object.keys(options).forEach((function(key){_options[key]=options[key]})),ed.setContent(addVisualStyling(ed)),(0,_options2.isContentToHighlight)(ed)&&ed.dom.addStyle((0,_options2.getHighlightCss)(ed))};_exports.onBeforeGetContent=function(ed,content){if(!isNull(content.source_view)&&!0===content.source_view){const onClose=function(ed){ed.off("close",onClose),ed.setContent(addVisualStyling(ed))};new MutationObserver(((mutations,obs)=>{const viewSrcModal=document.querySelector('[data-region="modal"]');if(viewSrcModal)return viewSrcModal.addEventListener("click",(event=>{const{action:action}=event.target.dataset;["cancel","save","hide"].includes(action)&&onClose(ed)})),void obs.disconnect();document.querySelector(".tox-dialog-wrap")&&(ed.on("CloseWindow",(()=>{onClose(ed)})),obs.disconnect())})).observe(document.body,{childList:!0,subtree:!0}),removeVisualStyling(ed)}};_exports.onFocus=function(ed){_isSubmit&&(ed.setContent(addVisualStyling(ed),{no_events:!0}),_isSubmit=!1)};_exports.onSubmit=function(ed){removeVisualStyling(ed),_isSubmit=!0};_exports.onDelete=function(ed,event){if(event.isComposing||isNull(event.clientX)&&46!==event.keyCode&&8!==event.keyCode)return;if(!isNull(event.clientX)&&(event.target.nodeType!==Node.ELEMENT_NODE||"path"!==event.target.nodeName&&"svg"!==event.target.nodeName))return;const begin=getHighlightNodeFromSelect(ed,"begin"),end=getHighlightNodeFromSelect(ed,"end");isNull(begin)||isNull(end)||(event.preventDefault(),ed.dom.remove(begin),ed.dom.remove(end),isNull(event.clientX)||hideContentToolbar(event.target),cleanupBogus(ed))};const cleanupBogus=function(ed){for(const span of ed.dom.select('span[class*="multilang"')){const p=span.parentElement;!isNull(p.classList)&&p.classList.contains("mce-offscreen-selection")&&ed.dom.remove(p)}};_exports.applyLanguage=function(ed,iso,event){if(isNull(iso))return;if("remove"===iso){return void ed.contentDocument.body.querySelectorAll(".multilang-begin, .multilang-end").forEach((element=>{ed.dom.remove(element)}))}const regexLang=/%lang/g;let text=ed.selection.getContent();if(""===text.toString().replace(/^\s+/,"").replace(/\s+$/,"")){if(!isNull(event))return void hideContentToolbar(event.target);let newtext=spanMultilangBegin.replace(regexLang,iso)+" "+spanMultilangEnd;return(0,_options2.mlangFilterExists)(ed)||(newtext=newtext.replaceAll("mceNonEditable","mceNonEditable fallback")),void ed.insertContent(newtext)}isNull(event)||hideContentToolbar(event.target);const span=getHighlightNodeFromSelect(ed,"begin");if(!isNull(span)){let replacement=spanMultilangBegin.replace(regexLang,iso);return span.classList.contains("fallback")&&(replacement=replacement.replace("mceNonEditable","mceNonEditable fallback")),ed.dom.setOuterHTML(span,replacement),void cleanupBogus(ed)}if(-1!==text.indexOf("multilang-begin")||-1!==text.indexOf("multilang-end"))return void ed.notificationManager.open({text:_options.langInSelectionErrMsg,type:"error"});const block=function(text){let result={el:null,cnt:0};const body=(new DOMParser).parseFromString(text,"text/html").body;if(body.firstChild.nodeType!==Node.ELEMENT_NODE)return result;const blockTags=["address","article","aside","blockquote","dd","div","dl","dt","figcaption","h1","h2","h3","h4","h5","h6","li","ol","p","pre","section","tfoot","ul"];for(let i=0;i').concat(innerHTML,"");for(end of(ed.dom.setOuterHTML(span,newHTML),toRemove))ed.dom.remove(end)}}}else ed.dom.setOuterHTML(span,span.innerHTML.toLowerCase())}))},getHighlightNodeFromSelect=function(ed,search){let span;return ed.dom.getParents(ed.selection.getStart(),(elm=>{if(!(0,_constants.isNull)(elm.classList)){const pair="begin"===search?"end":"begin";if(elm.classList.contains("multilang-"+pair)){span=elm;do{span="begin"===search?span.previousSibling:span.nextSibling}while(!(0,_constants.isNull)(span)&&((0,_constants.isNull)(span.classList)||!span.classList.contains("multilang-"+search)))}else elm.classList.contains("multilang-"+search)&&(span=elm)}})),span},hideContentToolbar=function(el){for(;!(0,_constants.isNull)(el);){if(el.nodeType===Node.ELEMENT_NODE&&!(0,_constants.isNull)(el.getAttribute("class"))&&-1!=el.getAttribute("class").indexOf("tox-pop-"))return void(el.style.display="none");el=el.parentNode}};_exports.onInit=function(ed,options){Object.keys(options).forEach((function(key){_options[key]=options[key]})),ed.setContent(addVisualStyling(ed)),(0,_options2.isContentToHighlight)(ed)&&ed.dom.addStyle((0,_options2.getHighlightCss)(ed))};_exports.onBeforeGetContent=function(ed,content){if(!(0,_constants.isNull)(content.source_view)&&!0===content.source_view){const onClose=function(ed){ed.off("close",onClose),ed.setContent(addVisualStyling(ed))};new MutationObserver(((mutations,obs)=>{const viewSrcModal=document.querySelector('[data-region="modal"]');if(viewSrcModal)return viewSrcModal.addEventListener("click",(event=>{const{action:action}=event.target.dataset;["cancel","save","hide"].includes(action)&&onClose(ed)})),void obs.disconnect();document.querySelector(".tox-dialog-wrap")&&(ed.on("CloseWindow",(()=>{onClose(ed)})),obs.disconnect())})).observe(document.body,{childList:!0,subtree:!0}),removeVisualStyling(ed)}};_exports.onFocus=function(ed){_isSubmit&&(ed.setContent(addVisualStyling(ed),{no_events:!0}),_isSubmit=!1)};_exports.onSubmit=function(ed){removeVisualStyling(ed),_isSubmit=!0};_exports.onDelete=function(ed,event){if(event.isComposing||(0,_constants.isNull)(event.clientX)&&46!==event.keyCode&&8!==event.keyCode)return;if(!(0,_constants.isNull)(event.clientX)&&(event.target.nodeType!==Node.ELEMENT_NODE||"path"!==event.target.nodeName&&"svg"!==event.target.nodeName))return;const begin=getHighlightNodeFromSelect(ed,"begin"),end=getHighlightNodeFromSelect(ed,"end");(0,_constants.isNull)(begin)||(0,_constants.isNull)(end)||(event.preventDefault(),ed.dom.remove(begin),ed.dom.remove(end),(0,_constants.isNull)(event.clientX)||hideContentToolbar(event.target),cleanupBogus(ed))};const cleanupBogus=function(ed){for(const span of ed.dom.select('span[class*="multilang"')){const p=span.parentElement;!(0,_constants.isNull)(p.classList)&&p.classList.contains("mce-offscreen-selection")&&ed.dom.remove(p)}};_exports.applyLanguage=function(ed,iso,event){if((0,_constants.isNull)(iso))return;if("remove"===iso){return void ed.contentDocument.body.querySelectorAll(".multilang-begin, .multilang-end").forEach((element=>{ed.dom.remove(element)}))}const regexLang=/%lang/g;let text=ed.selection.getContent();if(""===(0,_constants.trim)(text)){if(!(0,_constants.isNull)(event))return void hideContentToolbar(event.target);let newtext=_constants.spanMultilangBegin.replace(regexLang,iso)+" "+_constants.spanMultilangEnd;return(0,_options2.mlangFilterExists)(ed)||(newtext=newtext.replaceAll("mceNonEditable","mceNonEditable fallback")),void ed.insertContent(newtext)}(0,_constants.isNull)(event)||hideContentToolbar(event.target);const span=getHighlightNodeFromSelect(ed,"begin");if(!(0,_constants.isNull)(span)){let replacement=_constants.spanMultilangBegin.replace(regexLang,iso);return span.classList.contains("fallback")&&(replacement=replacement.replace("mceNonEditable","mceNonEditable fallback")),ed.dom.setOuterHTML(span,replacement),void cleanupBogus(ed)}if(-1!==text.indexOf("multilang-begin")||-1!==text.indexOf("multilang-end"))return void ed.notificationManager.open({text:_options.langInSelectionErrMsg,type:"error"});const block=function(text){let result={el:null,cnt:0};const body=(new DOMParser).parseFromString(text,"text/html").body;if(body.firstChild.nodeType!==Node.ELEMENT_NODE)return result;for(let i=0;i.\n\n/**\n * Commands for the plugin logic of the Moodle tiny_multilang2 plugin.\n *\n * @module tiny_multilang2\n * @author Iñaki Arenaza \n * @author Stephan Robotta \n * @author Tai Le Tan \n * @copyright 2015 onwards Iñaki Arenaza & Mondragon Unibertsitatea\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getHighlightCss, isContentToHighlight, mlangFilterExists} from './options';\n\n// This class inside a identified the {mlang} tag that is encapsulated in a span.\nconst spanClass = 'multilang-begin mceNonEditable';\n// This is the element with the data attribute.\nconst spanFixedAttrs = '{mlang %lang}';\n// The end span doesn't need information about the used language.\nconst spanMultilangEnd = spanFixedAttrs.replace('begin', 'end') + '>{mlang}';\n// Helper functions\nconst trim = v => v.toString().replace(/^\\s+/, '').replace(/\\s+$/, '');\nconst isNull = a => a === null || a === undefined;\n\n/**\n * Marker to remember that the submit button was hit.\n * @type {boolean}\n * @private\n */\nlet _isSubmit = false;\n\n/**\n * @type {object}\n * @private\n */\nconst _options = {};\n\n/**\n * Convert {mlang xx} and {mlang} strings to spans, so we can style them visually.\n * Remove superflous whitespace while at it.\n * @param {tinymce.Editor} ed\n * @return {string}\n */\nconst addVisualStyling = function(ed) {\n\n let content = ed.getContent();\n\n // Do not use a variable whether text is already highlighted, do a check for the existing class\n // because this is safe for many tiny element windows at one page.\n if (content.indexOf(spanClass) !== -1) {\n return content;\n }\n\n let intermediateReplacements = [];\n // Helper function to check wether te matching {mland} is inside a html node or not.\n const replaceHelper = function(m0, m1, p, c) {\n // Check if we are inside a html node by checking where the next > and < are.\n const textBefore = c.substring(0, p);\n const textAfter = c.substring(p + m0.length);\n const open = textBefore.lastIndexOf('<');\n const close = textBefore.lastIndexOf('>');\n // If open < close we are outside a html node and can modify the original content\n if (open < close) {\n if (!m1) {\n m0 = spanMultilangEnd;\n } else {\n m0 = spanMultilangBegin.replace(new RegExp('%lang', 'g'), m1);\n }\n } // If open > close we are inside a html node and do not modify the original content.\n // Store the matched {mlang} tag in the replacements array.\n intermediateReplacements.push(m0);\n return `${textBefore}___~~${intermediateReplacements.length}~~___${textAfter}`;\n };\n // First look for any {mlang} tags in the content string and do a preg_replace with the corresponding\n // tag that encapsulated the {mlang} tag so that the {mlang} is highlighted.\n // eslint-disable-next-line no-constant-condition\n while (1) {\n const m = content.match(new RegExp('{\\\\s*mlang(\\\\s+([^}]+?))?\\\\s*}', 'i'));\n if (!m) {\n break;\n }\n content = replaceHelper(m[0], m[2], m.index, m.input);\n }\n // Revert all placeholders back to the original {mlang} tags.\n for (let i = 0; i < intermediateReplacements.length; i++) {\n content = content.replace(`___~~${i + 1}~~___`, intermediateReplacements[i]);\n }\n\n // Check for the traditional tags, in case these were used as well in the text.\n // Any tag must be replaced with a {mlang XX}\n // and the corresponding closing must be replaced by {mlang}.\n // To handle this, we must convert the string into a DOMDocument so that any span.multilang tag can be searched\n // and replaced.\n const dom = new DOMParser();\n const doc = dom.parseFromString(content, 'text/html');\n if (doc.children.length === 0) { // Should not happen, but anyway, keep the check.\n return content;\n }\n const nodes = doc.querySelectorAll('span.multilang');\n if (nodes.length === 0) {\n return content;\n }\n for (const span of nodes) {\n const newSpan = spanMultilangBegin\n .replace(new RegExp('%lang', 'g'), span.getAttribute('lang'))\n .replace('mceNonEditable', 'mceNonEditable fallback')\n + span.innerHTML\n + spanMultilangEnd\n .replace('mceNonEditable', 'mceNonEditable fallback');\n // Insert the replacement string after the span tag itself by converting it into a html fragment.\n span.insertAdjacentHTML('afterend', newSpan);\n // Once the new tags are placed at the correct position, we can remove the original span tag.\n span.remove();\n }\n // Convert the DOMDocument into a string again.\n return doc.getElementsByTagName('body')[0].innerHTML;\n};\n\n/**\n * Remove the spans we added in _add_visual_styling() to leave only the {mlang xx} and {mlang} tags.\n * Also make sure we lowercase the multilang 'tags'\n * @param {tinymce.Editor} ed\n */\nconst removeVisualStyling = function(ed) {\n ['begin', 'end'].forEach(function(t) {\n for (const span of ed.dom.select('span.multilang-' + t)) {\n if (t === 'begin' && span.classList.contains('fallback')) {\n // This placeholder tag was created from an oldstyle tag.\n let innerHTML = '';\n let end = span;\n let toRemove = [];\n // Search the corresponding closing tag.\n while (end) {\n end = end.nextSibling;\n if (isNull(end)) { // Got a parent that does not exist. Stop here.\n break;\n }\n if (!isNull(end.classList) && end.classList.contains('multilang-end')) {\n // We found the multilang-end node, that needs to be removed, and also, we can stop here.\n toRemove.push(end);\n break;\n }\n // Sibling inside the tags need to be preserved, but moved to the innerHTML of the real\n // span tag. Therefore, collect the node content as string and remember the real nodes\n // to remove them later.\n if (end.nodeType === 3) {\n innerHTML += end.nodeValue;\n } else if (end.nodeType === 1) {\n innerHTML += end.outerHTML;\n }\n toRemove.push(end);\n }\n if (!isNull(end)) {\n // Extract the language from the {mlang XX} tag.\n const lang = span.innerHTML.match(new RegExp('{\\\\s*mlang\\\\s+([^}]+?)\\\\s*}', 'i'));\n if (lang) {\n /* Do not add the dir attribute as it breaks the Moodle language filter.\n // Right to left default languages.\n const rtlLanguages = getRTLLanguages();\n const langCode = lang[1];\n // Add dir=\"rtl\" to the html tag any time the overall document direction is right-to-left.\n const dir = rtlLanguages.includes(langCode) ? 'rtl' : 'ltr';\n // Do not add the dir attribute as it breaks the Moodle language filter.\n const newHTML = `${innerHTML}`;\n */\n const newHTML = `${innerHTML}`;\n ed.dom.setOuterHTML(span, newHTML);\n // And remove the other siblings.\n for (end of toRemove) {\n ed.dom.remove(end);\n }\n }\n }\n } else {\n // Normal placeholder tag, just restore the innerHTML that is {mlang XX} or {mlang}-\n ed.dom.setOuterHTML(span, span.innerHTML.toLowerCase());\n }\n }\n });\n};\n\n/**\n * At the current selection lookup for the current node. If we are inside a special span that encapsulates\n * the {lang} tag, then look for the corresponding opening or closing tag, depending on what's set in the\n * search param.\n * @param {tinymce.Editor} ed\n * @param {string} search\n * @return {Node|null} The encapsulating span tag if found.\n */\nconst getHighlightNodeFromSelect = function(ed, search) {\n let span;\n ed.dom.getParents(ed.selection.getStart(), elm => {\n // Are we in a span that highlights the lang tag.\n if (!isNull(elm.classList)) {\n // If we are on an opening/closing lang tag, we need to search for the corresponding opening/closing tag.\n const pair = search === 'begin' ? 'end' : 'begin';\n if (elm.classList.contains('multilang-' + pair)) {\n span = elm;\n do {\n // If we look for begin, go back siblings, otherwise look fnext siblings until end is found.\n span = search === 'begin' ? span.previousSibling : span.nextSibling;\n } while (!isNull(span) && (isNull(span.classList) || !span.classList.contains('multilang-' + search)));\n } else if (elm.classList.contains('multilang-' + search)) {\n // We are already on the correct tag we search for\n span = elm;\n }\n }\n });\n return span;\n};\n\n/**\n * From the given text (that is derived from a selection) we try to check if we have block elements selected and\n * in case yes, how many.\n * Return an object with:\n * el: the first block element node from the string\n * cnt: number of block elements found on the first level\n * In case the text fragment is not valid parsable HTML, then null and 0 is returned.\n * @param {string} text\n * @return {object}\n */\nconst getBlockElement = function(text) {\n let result = {el: null, cnt: 0};\n const dom = new DOMParser();\n const body = dom.parseFromString(text, 'text/html').body;\n // If the children nodes start with no block element, then just quit here.\n if (body.firstChild.nodeType !== Node.ELEMENT_NODE) {\n return result;\n }\n // These are not all block elements, we check for some only where the lang tags should be placed inside.\n const blockTags = ['address', 'article', 'aside', 'blockquote',\n 'dd', 'div', 'dl', 'dt', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n 'li', 'ol', 'p', 'pre', 'section', 'tfoot', 'ul'];\n for (let i = 0; i < body.children.length; i++) {\n if (body.children[i].nodeType !== Node.ELEMENT_NODE) {\n continue;\n }\n if (blockTags.indexOf(body.children[i].tagName.toString().toLowerCase()) !== -1) {\n result.cnt += 1;\n if (isNull(result.el)) {\n result.el = body.children[i];\n }\n }\n }\n return result;\n};\n\n/**\n * Check for the parent hierarchy elements, if there's a context toolbar container, then hide it.\n * @param {Node} el\n */\nconst hideContentToolbar = function(el) {\n while (!isNull(el)) {\n if (el.nodeType === Node.ELEMENT_NODE &&\n !isNull(el.getAttribute('class')) &&\n el.getAttribute('class').indexOf('tox-pop-') != -1\n ) {\n el.style.display = 'none';\n return;\n }\n el = el.parentNode;\n }\n};\n\n/**\n * When loading the editor for the first time, add the spans for highlighting the lang tags.\n * These are highlighted with the appropriate css only.\n * In addition pass some options to the plugin instance.\n * @param {tinymce.Editor} ed\n * @param {object} options\n */\nconst onInit = function(ed, options) {\n Object.keys(options).forEach(function(key) {\n _options[key] = options[key];\n });\n ed.setContent(addVisualStyling(ed));\n if (isContentToHighlight(ed)) {\n ed.dom.addStyle(getHighlightCss(ed));\n }\n};\n\n/**\n * When the source code view dialogue is show, we must remove the highlight spans from the editor content\n * and also add them again when the dialogue is closed.\n * @param {tinymce.Editor} ed\n * @param {object} content\n */\nconst onBeforeGetContent = function(ed, content) {\n if (!isNull(content.source_view) && content.source_view === true) {\n // If the user clicks on 'Cancel' or the close button on the html\n // source code dialog view, make sure we re-add the visual styling.\n const onClose = function(ed) {\n ed.off('close', onClose);\n ed.setContent(addVisualStyling(ed));\n };\n const observer = new MutationObserver((mutations, obs) => {\n const viewSrcModal = document.querySelector('[data-region=\"modal\"]');\n if (viewSrcModal) {\n viewSrcModal.addEventListener('click', (event) => {\n const {action} = event.target.dataset;\n if (['cancel', 'save', 'hide'].includes(action)) {\n onClose(ed);\n }\n });\n // Stop observing once the modal is found.\n obs.disconnect();\n return;\n }\n const tinyMceModal = document.querySelector('.tox-dialog-wrap');\n if (tinyMceModal) {\n ed.on('CloseWindow', () => {\n onClose(ed);\n });\n obs.disconnect();\n }\n });\n observer.observe(document.body, {childList: true, subtree: true});\n removeVisualStyling(ed);\n }\n};\n\n/**\n * When the submit button is hit, the marker spans are removed. However, if there's an error\n * in saving the content (via ajax) the editor remains with the cleaned content. Therefore,\n * we need to add the marker span elements once again when the user tries to change the content\n * of the editor.\n * @param {tinymce.Editor} ed\n */\nconst onFocus = function(ed) {\n if (_isSubmit) {\n // eslint-disable-next-line camelcase\n ed.setContent(addVisualStyling(ed), {no_events: true});\n _isSubmit = false;\n }\n};\n\n/**\n * Fires when the form containing the editor is submitted. Remove all the marker span elements.\n * @param {tinymce.Editor} ed\n */\nconst onSubmit = function(ed) {\n removeVisualStyling(ed);\n _isSubmit = true;\n};\n\n/**\n * Check for key press when something is deleted. If that happens inside a highlight span\n * tag, then remove this tag and the corresponding that open/closes this lang tag.\n * @param {tinymce.Editor} ed\n * @param {Object} event\n */\nconst onDelete = function(ed, event) {\n // We are not in composing mode, have not clicked and key or was not pressed.\n if (event.isComposing || (isNull(event.clientX) && event.keyCode !== 46 && event.keyCode !== 8)) {\n return;\n }\n // In case we clicked, check that we clicked an icon (this must have been the trash icon in the context menu).\n if (!isNull(event.clientX) &&\n (event.target.nodeType !== Node.ELEMENT_NODE || (event.target.nodeName !== 'path' && event.target.nodeName !== 'svg'))) {\n return;\n }\n // Conditions match either key or was pressed, or an click on an svg icon was done.\n // Check if we are inside a span for the language tag.\n const begin = getHighlightNodeFromSelect(ed, 'begin');\n const end = getHighlightNodeFromSelect(ed, 'end');\n // Only if both, start and end tags are found, then delete the nodes here and prevent the default handling\n // because the stuff to be deleted is already gone.\n if (!isNull(begin) && !isNull(end)) {\n event.preventDefault();\n ed.dom.remove(begin);\n ed.dom.remove(end);\n if (!isNull(event.clientX)) {\n hideContentToolbar(event.target);\n }\n cleanupBogus(ed);\n }\n};\n\n/**\n * In the tinyMCE of Moodle 4.1 and 4.2 some leftovers from the element selection can be seen when the source code\n * is displayed. Remove these. Apparently 4.3 does not have this problem anymore.\n * @param {tinymce.Editor} ed\n */\nconst cleanupBogus = function(ed) {\n for (const span of ed.dom.select('span[class*=\"multilang\"')) {\n const p = span.parentElement;\n if (!isNull(p.classList) && p.classList.contains('mce-offscreen-selection')) {\n ed.dom.remove(p);\n }\n }\n};\n\n/**\n * The action when a language icon or menu entry is clicked. This adds the {mlang} tags at the current content\n * position or around the selection.\n * @param {tinymce.Editor} ed\n * @param {string} iso\n * @param {Event} event\n */\nconst applyLanguage = function(ed, iso, event) {\n if (isNull(iso)) {\n return;\n }\n if (iso === \"remove\") {\n const elements = ed.contentDocument.body;\n // Find all elements with the class \"multilang-begin\" or \"multilang-end\".\n const multiLangElements = elements.querySelectorAll('.multilang-begin, .multilang-end');\n multiLangElements.forEach(element => {\n ed.dom.remove(element);\n });\n return;\n }\n const regexLang = /%lang/g;\n let text = ed.selection.getContent();\n // Selection is empty, just insert the lang opening and closing tag\n // together with a space where the user may add the content.\n if (trim(text) === '') {\n // Event is set when the context menu was hit, here the editor lost the previously selected node. Therfore,\n // don't do anything.\n if (!isNull(event)) {\n hideContentToolbar(event.target);\n return;\n }\n let newtext = spanMultilangBegin.replace(regexLang, iso) + ' ' + spanMultilangEnd;\n if (!mlangFilterExists(ed)) {\n // No mlang filter, add the fallback class to the highlight spans so that these are translated\n // to the standard elements.\n newtext = newtext.replaceAll('mceNonEditable', 'mceNonEditable fallback');\n }\n ed.insertContent(newtext);\n return;\n }\n // Hide context toolbar, because at any subsequent call the node is not selected anymore.\n if (!isNull(event)) {\n hideContentToolbar(event.target);\n }\n // No matter if we have syntax highlighting enabled or not, the spans around the language tags exist\n // in the WYSIWYG mode. So check if we are on a special span that encapsulates the language tags. Search\n // for the start span tag.\n const span = getHighlightNodeFromSelect(ed, 'begin');\n // If we have a span, then it's the opening tag, and we just replace this one with the new iso.\n if (!isNull(span)) {\n let replacement = spanMultilangBegin.replace(regexLang, iso);\n if (span.classList.contains('fallback')) {\n replacement = replacement.replace('mceNonEditable', 'mceNonEditable fallback');\n }\n ed.dom.setOuterHTML(span, replacement);\n cleanupBogus(ed);\n return;\n }\n // Check if we have language tags inside the selection:\n if (text.indexOf('multilang-begin') !== -1 || text.indexOf('multilang-end') !== -1) {\n ed.notificationManager.open({\n text: _options.langInSelectionErrMsg,\n type: 'error',\n });\n return;\n }\n const block = getBlockElement(text);\n if (!isNull(block.el)) {\n if (block.cnt === 1) {\n // We have a block element selected, such as a hX or p tag. Then keep this tag and place the\n // language tags inside but around the content of the block element.\n let newtext = spanMultilangBegin.replace(regexLang, iso) + block.el.innerHTML + spanMultilangEnd;\n if (!mlangFilterExists(ed)) { // No mlang filter, add the fallback class to the highlight spans.\n newtext = newtext.replaceAll('mceNonEditable', 'mceNonEditable fallback');\n }\n block.el.innerHTML = newtext;\n ed.selection.setContent(block.el.outerHTML);\n return;\n }\n if (!mlangFilterExists(ed)) {\n ed.notificationManager.open({\n text: _options.multipleBlocksErrMsg,\n type: 'error',\n });\n return;\n }\n }\n // Not inside a lang tag, insert a new opening and closing tag with the selection inside.\n let newtext = spanMultilangBegin.replace(regexLang, iso) + text + spanMultilangEnd;\n if (!mlangFilterExists(ed)) { // No mlang filter, add the fallback class to the highlight spans.\n newtext = newtext.replaceAll('mceNonEditable', 'mceNonEditable fallback');\n }\n ed.selection.setContent(newtext);\n};\n\nexport {\n onInit,\n onBeforeGetContent,\n onFocus,\n onSubmit,\n onDelete,\n applyLanguage\n};\n"],"names":["spanClass","spanFixedAttrs","spanMultilangBegin","spanMultilangEnd","replace","isNull","a","_isSubmit","_options","addVisualStyling","ed","content","getContent","indexOf","intermediateReplacements","replaceHelper","m0","m1","p","c","textBefore","substring","textAfter","length","lastIndexOf","RegExp","push","m","match","index","input","i","doc","DOMParser","parseFromString","children","nodes","querySelectorAll","span","newSpan","getAttribute","innerHTML","insertAdjacentHTML","remove","getElementsByTagName","removeVisualStyling","forEach","t","dom","select","classList","contains","end","toRemove","nextSibling","nodeType","nodeValue","outerHTML","lang","newHTML","setOuterHTML","toLowerCase","getHighlightNodeFromSelect","search","getParents","selection","getStart","elm","pair","previousSibling","hideContentToolbar","el","Node","ELEMENT_NODE","style","display","parentNode","options","Object","keys","key","setContent","addStyle","source_view","onClose","off","MutationObserver","mutations","obs","viewSrcModal","document","querySelector","addEventListener","event","action","target","dataset","includes","disconnect","on","observe","body","childList","subtree","no_events","isComposing","clientX","keyCode","nodeName","begin","preventDefault","cleanupBogus","parentElement","iso","contentDocument","element","regexLang","text","toString","newtext","replaceAll","insertContent","replacement","notificationManager","open","langInSelectionErrMsg","type","block","result","cnt","firstChild","blockTags","tagName","getBlockElement","multipleBlocksErrMsg"],"mappings":";;;;;;;;;;;MA6BMA,UAAY,iCAEZC,eAAiB,wCAA0CD,UAAY,qCAEvEE,mBAAqBD,eAAiB,sDAEtCE,iBAAmBF,eAAeG,QAAQ,QAAS,OAAS,kBAG5DC,OAASC,GAAKA,MAAAA,MAOhBC,WAAY,QAMVC,SAAW,GAQXC,iBAAmB,SAASC,QAE1BC,QAAUD,GAAGE,iBAImB,IAAhCD,QAAQE,QAAQb,kBACTW,YAGPG,yBAA2B,SAEzBC,cAAgB,SAASC,GAAIC,GAAIC,EAAGC,SAEhCC,WAAaD,EAAEE,UAAU,EAAGH,GAC5BI,UAAYH,EAAEE,UAAUH,EAAIF,GAAGO,eACxBH,WAAWI,YAAY,KACtBJ,WAAWI,YAAY,OAM7BR,GAHCC,GAGIf,mBAAmBE,QAAQ,IAAIqB,OAAO,QAAS,KAAMR,IAFrDd,kBAMbW,yBAAyBY,KAAKV,cACpBI,2BAAkBN,yBAAyBS,uBAAcD,mBAK7D,OACAK,EAAIhB,QAAQiB,MAAM,IAAIH,OAAO,iCAAkC,UAChEE,QAGLhB,QAAUI,cAAcY,EAAE,GAAIA,EAAE,GAAIA,EAAEE,MAAOF,EAAEG,WAG9C,IAAIC,EAAI,EAAGA,EAAIjB,yBAAyBS,OAAQQ,IACjDpB,QAAUA,QAAQP,uBAAgB2B,EAAI,WAAUjB,yBAAyBiB,UASvEC,KADM,IAAIC,WACAC,gBAAgBvB,QAAS,gBACb,IAAxBqB,IAAIG,SAASZ,cACNZ,cAELyB,MAAQJ,IAAIK,iBAAiB,qBACd,IAAjBD,MAAMb,cACCZ,YAEN,MAAM2B,QAAQF,MAAO,OAChBG,QAAUrC,mBACXE,QAAQ,IAAIqB,OAAO,QAAS,KAAMa,KAAKE,aAAa,SACpDpC,QAAQ,iBAAkB,2BAC3BkC,KAAKG,UACLtC,iBACCC,QAAQ,iBAAkB,2BAE/BkC,KAAKI,mBAAmB,WAAYH,SAEpCD,KAAKK,gBAGFX,IAAIY,qBAAqB,QAAQ,GAAGH,WAQzCI,oBAAsB,SAASnC,KAChC,QAAS,OAAOoC,SAAQ,SAASC,OACzB,MAAMT,QAAQ5B,GAAGsC,IAAIC,OAAO,kBAAoBF,MACvC,UAANA,GAAiBT,KAAKY,UAAUC,SAAS,YAAa,KAElDV,UAAY,GACZW,IAAMd,KACNe,SAAW,QAERD,MACHA,IAAMA,IAAIE,aACNjD,OAAO+C,OAFH,KAKH/C,OAAO+C,IAAIF,YAAcE,IAAIF,UAAUC,SAAS,iBAAkB,CAEnEE,SAAS3B,KAAK0B,WAMG,IAAjBA,IAAIG,SACJd,WAAaW,IAAII,UACO,IAAjBJ,IAAIG,WACXd,WAAaW,IAAIK,WAErBJ,SAAS3B,KAAK0B,SAEb/C,OAAO+C,KAAM,OAERM,KAAOpB,KAAKG,UAAUb,MAAM,IAAIH,OAAO,8BAA+B,SACxEiC,KAAM,OAUAC,gDAA2CD,KAAK,gBAAOjB,yBAGxDW,OAFL1C,GAAGsC,IAAIY,aAAatB,KAAMqB,SAEdN,UACR3C,GAAGsC,IAAIL,OAAOS,YAM1B1C,GAAGsC,IAAIY,aAAatB,KAAMA,KAAKG,UAAUoB,mBAcnDC,2BAA6B,SAASpD,GAAIqD,YACxCzB,YACJ5B,GAAGsC,IAAIgB,WAAWtD,GAAGuD,UAAUC,YAAYC,UAElC9D,OAAO8D,IAAIjB,WAAY,OAElBkB,KAAkB,UAAXL,OAAqB,MAAQ,WACtCI,IAAIjB,UAAUC,SAAS,aAAeiB,MAAO,CAC7C9B,KAAO6B,OAGH7B,KAAkB,UAAXyB,OAAqBzB,KAAK+B,gBAAkB/B,KAAKgB,mBAClDjD,OAAOiC,QAAUjC,OAAOiC,KAAKY,aAAeZ,KAAKY,UAAUC,SAAS,aAAeY,eACtFI,IAAIjB,UAAUC,SAAS,aAAeY,UAE7CzB,KAAO6B,SAIZ7B,MA2CLgC,mBAAqB,SAASC,UACxBlE,OAAOkE,KAAK,IACZA,GAAGhB,WAAaiB,KAAKC,eACpBpE,OAAOkE,GAAG/B,aAAa,YACyB,GAAjD+B,GAAG/B,aAAa,SAAS3B,QAAQ,wBAEjC0D,GAAGG,MAAMC,QAAU,QAGvBJ,GAAKA,GAAGK,6BAWD,SAASlE,GAAImE,SACxBC,OAAOC,KAAKF,SAAS/B,SAAQ,SAASkC,KAClCxE,SAASwE,KAAOH,QAAQG,QAE5BtE,GAAGuE,WAAWxE,iBAAiBC,MAC3B,kCAAqBA,KACrBA,GAAGsC,IAAIkC,UAAS,6BAAgBxE,kCAUb,SAASA,GAAIC,aAC/BN,OAAOM,QAAQwE,eAAwC,IAAxBxE,QAAQwE,YAAsB,OAGxDC,QAAU,SAAS1E,IACrBA,GAAG2E,IAAI,QAASD,SAChB1E,GAAGuE,WAAWxE,iBAAiBC,MAElB,IAAI4E,kBAAiB,CAACC,UAAWC,aACxCC,aAAeC,SAASC,cAAc,4BACxCF,oBACAA,aAAaG,iBAAiB,SAAUC,cAC9BC,OAACA,QAAUD,MAAME,OAAOC,QAC1B,CAAC,SAAU,OAAQ,QAAQC,SAASH,SACpCV,QAAQ1E,YAIhB8E,IAAIU,aAGaR,SAASC,cAAc,sBAExCjF,GAAGyF,GAAG,eAAe,KACjBf,QAAQ1E,OAEZ8E,IAAIU,iBAGHE,QAAQV,SAASW,KAAM,CAACC,WAAW,EAAMC,SAAS,IAC3D1D,oBAAoBnC,uBAWZ,SAASA,IACjBH,YAEAG,GAAGuE,WAAWxE,iBAAiBC,IAAK,CAAC8F,WAAW,IAChDjG,WAAY,sBAQH,SAASG,IACtBmC,oBAAoBnC,IACpBH,WAAY,qBASC,SAASG,GAAImF,UAEtBA,MAAMY,aAAgBpG,OAAOwF,MAAMa,UAA8B,KAAlBb,MAAMc,SAAoC,IAAlBd,MAAMc,mBAI5EtG,OAAOwF,MAAMa,WACbb,MAAME,OAAOxC,WAAaiB,KAAKC,cAA2C,SAA1BoB,MAAME,OAAOa,UAAiD,QAA1Bf,MAAME,OAAOa,uBAKhGC,MAAQ/C,2BAA2BpD,GAAI,SACvC0C,IAAMU,2BAA2BpD,GAAI,OAGtCL,OAAOwG,QAAWxG,OAAO+C,OAC1ByC,MAAMiB,iBACNpG,GAAGsC,IAAIL,OAAOkE,OACdnG,GAAGsC,IAAIL,OAAOS,KACT/C,OAAOwF,MAAMa,UACdpC,mBAAmBuB,MAAME,QAE7BgB,aAAarG,YASfqG,aAAe,SAASrG,QACrB,MAAM4B,QAAQ5B,GAAGsC,IAAIC,OAAO,2BAA4B,OACnD/B,EAAIoB,KAAK0E,eACV3G,OAAOa,EAAEgC,YAAchC,EAAEgC,UAAUC,SAAS,4BAC7CzC,GAAGsC,IAAIL,OAAOzB,4BAYJ,SAASR,GAAIuG,IAAKpB,UAChCxF,OAAO4G,eAGC,WAARA,IAAkB,aACDvG,GAAGwG,gBAAgBb,KAEDhE,iBAAiB,oCAClCS,SAAQqE,UACtBzG,GAAGsC,IAAIL,OAAOwE,kBAIhBC,UAAY,aACdC,KAAO3G,GAAGuD,UAAUrD,gBAGL,KAAVyG,KA3YOC,WAAWlH,QAAQ,OAAQ,IAAIA,QAAQ,OAAQ,IA2YxC,KAGdC,OAAOwF,mBACRvB,mBAAmBuB,MAAME,YAGzBwB,QAAUrH,mBAAmBE,QAAQgH,UAAWH,KAAO,IAAM9G,wBAC5D,+BAAkBO,MAGnB6G,QAAUA,QAAQC,WAAW,iBAAkB,iCAEnD9G,GAAG+G,cAAcF,SAIhBlH,OAAOwF,QACRvB,mBAAmBuB,MAAME,cAKvBzD,KAAOwB,2BAA2BpD,GAAI,aAEvCL,OAAOiC,MAAO,KACXoF,YAAcxH,mBAAmBE,QAAQgH,UAAWH,YACpD3E,KAAKY,UAAUC,SAAS,cACxBuE,YAAcA,YAAYtH,QAAQ,iBAAkB,4BAExDM,GAAGsC,IAAIY,aAAatB,KAAMoF,kBAC1BX,aAAarG,QAIwB,IAArC2G,KAAKxG,QAAQ,qBAAgE,IAAnCwG,KAAKxG,QAAQ,6BACvDH,GAAGiH,oBAAoBC,KAAK,CACpBP,KAAM7G,SAASqH,sBACfC,KAAM,gBAIZC,MA7Oc,SAASV,UACzBW,OAAS,CAACzD,GAAI,KAAM0D,IAAK,SAEvB5B,MADM,IAAIpE,WACCC,gBAAgBmF,KAAM,aAAahB,QAEhDA,KAAK6B,WAAW3E,WAAaiB,KAAKC,oBAC3BuD,aAGLG,UAAY,CAAC,UAAW,UAAW,QAAS,aAC9C,KAAM,MAAO,KAAM,KAAM,aAAc,KAAM,KAAM,KAAM,KAAM,KAAM,KACrE,KAAM,KAAM,IAAK,MAAO,UAAW,QAAS,UAC3C,IAAIpG,EAAI,EAAGA,EAAIsE,KAAKlE,SAASZ,OAAQQ,IAClCsE,KAAKlE,SAASJ,GAAGwB,WAAaiB,KAAKC,eAGuC,IAA1E0D,UAAUtH,QAAQwF,KAAKlE,SAASJ,GAAGqG,QAAQd,WAAWzD,iBACtDmE,OAAOC,KAAO,EACV5H,OAAO2H,OAAOzD,MACdyD,OAAOzD,GAAK8B,KAAKlE,SAASJ,YAI/BiG,OAsNOK,CAAgBhB,UACzBhH,OAAO0H,MAAMxD,IAAK,IACD,IAAdwD,MAAME,IAAW,KAGbV,QAAUrH,mBAAmBE,QAAQgH,UAAWH,KAAOc,MAAMxD,GAAG9B,UAAYtC,wBAC3E,+BAAkBO,MACnB6G,QAAUA,QAAQC,WAAW,iBAAkB,4BAEnDO,MAAMxD,GAAG9B,UAAY8E,aACrB7G,GAAGuD,UAAUgB,WAAW8C,MAAMxD,GAAGd,gBAGhC,+BAAkB/C,gBACnBA,GAAGiH,oBAAoBC,KAAK,CACxBP,KAAM7G,SAAS8H,qBACfR,KAAM,cAMdP,QAAUrH,mBAAmBE,QAAQgH,UAAWH,KAAOI,KAAOlH,kBAC7D,+BAAkBO,MACnB6G,QAAUA,QAAQC,WAAW,iBAAkB,4BAEnD9G,GAAGuD,UAAUgB,WAAWsC"} \ No newline at end of file +{"version":3,"file":"ui.min.js","sources":["../src/ui.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Commands for the plugin logic of the Moodle tiny_multilang2 plugin.\n *\n * @module tiny_multilang2\n * @author Iñaki Arenaza \n * @author Stephan Robotta \n * @author Tai Le Tan \n * @copyright 2015 onwards Iñaki Arenaza & Mondragon Unibertsitatea\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getHighlightCss, isContentToHighlight, mlangFilterExists} from './options';\nimport {isNull, trim, blockTags, spanMultilangBegin, spanMultilangEnd} from './constants';\nimport {parseEditorContent} from './htmlparser';\n/**\n * Marker to remember that the submit button was hit.\n * @type {boolean}\n * @private\n */\nlet _isSubmit = false;\n\n/**\n * @type {object}\n * @private\n */\nconst _options = {};\n\n/**\n * Convert {mlang xx} and {mlang} strings to spans, so we can style them visually.\n * Remove superflous whitespace while at it.\n * @param {tinymce.Editor} ed\n * @return {string}\n */\nconst addVisualStyling = function(ed) {\n\n // Parse the editor content and check for all {mlang} tags that are in the html content.\n let content = parseEditorContent(ed.getContent());\n\n // Check for the traditional tags, in case these were used as well in the text.\n // Any tag must be replaced with a {mlang XX}\n // and the corresponding closing must be replaced by {mlang}.\n // To handle this, we must convert the string into a DOMDocument so that any span.multilang tag can be searched\n // and replaced.\n const dom = new DOMParser();\n const doc = dom.parseFromString(content, 'text/html');\n if (doc.children.length === 0) { // Should not happen, but anyway, keep the check.\n return content;\n }\n const nodes = doc.querySelectorAll('span.multilang');\n if (nodes.length === 0) {\n return content;\n }\n for (const span of nodes) {\n const newSpan = spanMultilangBegin\n .replace(new RegExp('%lang', 'g'), span.getAttribute('lang'))\n .replace('mceNonEditable', 'mceNonEditable fallback')\n + span.innerHTML\n + spanMultilangEnd\n .replace('mceNonEditable', 'mceNonEditable fallback');\n // Insert the replacement string after the span tag itself by converting it into a html fragment.\n span.insertAdjacentHTML('afterend', newSpan);\n // Once the new tags are placed at the correct position, we can remove the original span tag.\n span.remove();\n }\n // Convert the DOMDocument into a string again.\n return doc.getElementsByTagName('body')[0].innerHTML;\n};\n\n/**\n * Remove the spans we added in _add_visual_styling() to leave only the {mlang xx} and {mlang} tags.\n * Also make sure we lowercase the multilang 'tags'\n * @param {tinymce.Editor} ed\n */\nconst removeVisualStyling = function(ed) {\n ['begin', 'end'].forEach(function(t) {\n for (const span of ed.dom.select('span.multilang-' + t)) {\n if (t === 'begin' && span.classList.contains('fallback')) {\n // This placeholder tag was created from an oldstyle tag.\n let innerHTML = '';\n let end = span;\n let toRemove = [];\n // Search the corresponding closing tag.\n while (end) {\n end = end.nextSibling;\n if (isNull(end)) { // Got a parent that does not exist. Stop here.\n break;\n }\n if (!isNull(end.classList) && end.classList.contains('multilang-end')) {\n // We found the multilang-end node, that needs to be removed, and also, we can stop here.\n toRemove.push(end);\n break;\n }\n // Sibling inside the tags need to be preserved, but moved to the innerHTML of the real\n // span tag. Therefore, collect the node content as string and remember the real nodes\n // to remove them later.\n if (end.nodeType === 3) {\n innerHTML += end.nodeValue;\n } else if (end.nodeType === 1) {\n innerHTML += end.outerHTML;\n }\n toRemove.push(end);\n }\n if (!isNull(end)) {\n // Extract the language from the {mlang XX} tag.\n const lang = span.innerHTML.match(new RegExp('{\\\\s*mlang\\\\s+([^}]+?)\\\\s*}', 'i'));\n if (lang) {\n /* Do not add the dir attribute as it breaks the Moodle language filter.\n // Right to left default languages.\n const rtlLanguages = getRTLLanguages();\n const langCode = lang[1];\n // Add dir=\"rtl\" to the html tag any time the overall document direction is right-to-left.\n const dir = rtlLanguages.includes(langCode) ? 'rtl' : 'ltr';\n // Do not add the dir attribute as it breaks the Moodle language filter.\n const newHTML = `${innerHTML}`;\n */\n const newHTML = `${innerHTML}`;\n ed.dom.setOuterHTML(span, newHTML);\n // And remove the other siblings.\n for (end of toRemove) {\n ed.dom.remove(end);\n }\n }\n }\n } else {\n // Normal placeholder tag, just restore the innerHTML that is {mlang XX} or {mlang}-\n ed.dom.setOuterHTML(span, span.innerHTML.toLowerCase());\n }\n }\n });\n};\n\n/**\n * At the current selection lookup for the current node. If we are inside a special span that encapsulates\n * the {lang} tag, then look for the corresponding opening or closing tag, depending on what's set in the\n * search param.\n * @param {tinymce.Editor} ed\n * @param {string} search\n * @return {Node|null} The encapsulating span tag if found.\n */\nconst getHighlightNodeFromSelect = function(ed, search) {\n let span;\n ed.dom.getParents(ed.selection.getStart(), elm => {\n // Are we in a span that highlights the lang tag.\n if (!isNull(elm.classList)) {\n // If we are on an opening/closing lang tag, we need to search for the corresponding opening/closing tag.\n const pair = search === 'begin' ? 'end' : 'begin';\n if (elm.classList.contains('multilang-' + pair)) {\n span = elm;\n do {\n // If we look for begin, go back siblings, otherwise look fnext siblings until end is found.\n span = search === 'begin' ? span.previousSibling : span.nextSibling;\n } while (!isNull(span) && (isNull(span.classList) || !span.classList.contains('multilang-' + search)));\n } else if (elm.classList.contains('multilang-' + search)) {\n // We are already on the correct tag we search for\n span = elm;\n }\n }\n });\n return span;\n};\n\n/**\n * From the given text (that is derived from a selection) we try to check if we have block elements selected and\n * in case yes, how many.\n * Return an object with:\n * el: the first block element node from the string\n * cnt: number of block elements found on the first level\n * In case the text fragment is not valid parsable HTML, then null and 0 is returned.\n * @param {string} text\n * @return {object}\n */\nconst getBlockElement = function(text) {\n let result = {el: null, cnt: 0};\n const dom = new DOMParser();\n const body = dom.parseFromString(text, 'text/html').body;\n // If the children nodes start with no block element, then just quit here.\n if (body.firstChild.nodeType !== Node.ELEMENT_NODE) {\n return result;\n }\n // Lang tags should be placed inside block elements.\n for (let i = 0; i < body.children.length; i++) {\n if (body.children[i].nodeType !== Node.ELEMENT_NODE) {\n continue;\n }\n if (blockTags.indexOf(body.children[i].tagName.toString().toLowerCase()) !== -1) {\n result.cnt += 1;\n if (isNull(result.el)) {\n result.el = body.children[i];\n }\n }\n }\n return result;\n};\n\n/**\n * Check for the parent hierarchy elements, if there's a context toolbar container, then hide it.\n * @param {Node} el\n */\nconst hideContentToolbar = function(el) {\n while (!isNull(el)) {\n if (el.nodeType === Node.ELEMENT_NODE &&\n !isNull(el.getAttribute('class')) &&\n el.getAttribute('class').indexOf('tox-pop-') != -1\n ) {\n el.style.display = 'none';\n return;\n }\n el = el.parentNode;\n }\n};\n\n/**\n * When loading the editor for the first time, add the spans for highlighting the lang tags.\n * These are highlighted with the appropriate css only.\n * In addition pass some options to the plugin instance.\n * @param {tinymce.Editor} ed\n * @param {object} options\n */\nconst onInit = function(ed, options) {\n Object.keys(options).forEach(function(key) {\n _options[key] = options[key];\n });\n ed.setContent(addVisualStyling(ed));\n if (isContentToHighlight(ed)) {\n ed.dom.addStyle(getHighlightCss(ed));\n }\n};\n\n/**\n * When the source code view dialogue is show, we must remove the highlight spans from the editor content\n * and also add them again when the dialogue is closed.\n * @param {tinymce.Editor} ed\n * @param {object} content\n */\nconst onBeforeGetContent = function(ed, content) {\n if (!isNull(content.source_view) && content.source_view === true) {\n // If the user clicks on 'Cancel' or the close button on the html\n // source code dialog view, make sure we re-add the visual styling.\n const onClose = function(ed) {\n ed.off('close', onClose);\n ed.setContent(addVisualStyling(ed));\n };\n const observer = new MutationObserver((mutations, obs) => {\n const viewSrcModal = document.querySelector('[data-region=\"modal\"]');\n if (viewSrcModal) {\n viewSrcModal.addEventListener('click', (event) => {\n const {action} = event.target.dataset;\n if (['cancel', 'save', 'hide'].includes(action)) {\n onClose(ed);\n }\n });\n // Stop observing once the modal is found.\n obs.disconnect();\n return;\n }\n const tinyMceModal = document.querySelector('.tox-dialog-wrap');\n if (tinyMceModal) {\n ed.on('CloseWindow', () => {\n onClose(ed);\n });\n obs.disconnect();\n }\n });\n observer.observe(document.body, {childList: true, subtree: true});\n removeVisualStyling(ed);\n }\n};\n\n/**\n * When the submit button is hit, the marker spans are removed. However, if there's an error\n * in saving the content (via ajax) the editor remains with the cleaned content. Therefore,\n * we need to add the marker span elements once again when the user tries to change the content\n * of the editor.\n * @param {tinymce.Editor} ed\n */\nconst onFocus = function(ed) {\n if (_isSubmit) {\n // eslint-disable-next-line camelcase\n ed.setContent(addVisualStyling(ed), {no_events: true});\n _isSubmit = false;\n }\n};\n\n/**\n * Fires when the form containing the editor is submitted. Remove all the marker span elements.\n * @param {tinymce.Editor} ed\n */\nconst onSubmit = function(ed) {\n removeVisualStyling(ed);\n _isSubmit = true;\n};\n\n/**\n * Check for key press when something is deleted. If that happens inside a highlight span\n * tag, then remove this tag and the corresponding that open/closes this lang tag.\n * @param {tinymce.Editor} ed\n * @param {Object} event\n */\nconst onDelete = function(ed, event) {\n // We are not in composing mode, have not clicked and key or was not pressed.\n if (event.isComposing || (isNull(event.clientX) && event.keyCode !== 46 && event.keyCode !== 8)) {\n return;\n }\n // In case we clicked, check that we clicked an icon (this must have been the trash icon in the context menu).\n if (!isNull(event.clientX) &&\n (event.target.nodeType !== Node.ELEMENT_NODE || (event.target.nodeName !== 'path' && event.target.nodeName !== 'svg'))) {\n return;\n }\n // Conditions match either key or was pressed, or an click on an svg icon was done.\n // Check if we are inside a span for the language tag.\n const begin = getHighlightNodeFromSelect(ed, 'begin');\n const end = getHighlightNodeFromSelect(ed, 'end');\n // Only if both, start and end tags are found, then delete the nodes here and prevent the default handling\n // because the stuff to be deleted is already gone.\n if (!isNull(begin) && !isNull(end)) {\n event.preventDefault();\n ed.dom.remove(begin);\n ed.dom.remove(end);\n if (!isNull(event.clientX)) {\n hideContentToolbar(event.target);\n }\n cleanupBogus(ed);\n }\n};\n\n/**\n * In the tinyMCE of Moodle 4.1 and 4.2 some leftovers from the element selection can be seen when the source code\n * is displayed. Remove these. Apparently 4.3 does not have this problem anymore.\n * @param {tinymce.Editor} ed\n */\nconst cleanupBogus = function(ed) {\n for (const span of ed.dom.select('span[class*=\"multilang\"')) {\n const p = span.parentElement;\n if (!isNull(p.classList) && p.classList.contains('mce-offscreen-selection')) {\n ed.dom.remove(p);\n }\n }\n};\n\n/**\n * The action when a language icon or menu entry is clicked. This adds the {mlang} tags at the current content\n * position or around the selection.\n * @param {tinymce.Editor} ed\n * @param {string} iso\n * @param {Event} event\n */\nconst applyLanguage = function(ed, iso, event) {\n if (isNull(iso)) {\n return;\n }\n if (iso === \"remove\") {\n const elements = ed.contentDocument.body;\n // Find all elements with the class \"multilang-begin\" or \"multilang-end\".\n const multiLangElements = elements.querySelectorAll('.multilang-begin, .multilang-end');\n multiLangElements.forEach(element => {\n ed.dom.remove(element);\n });\n return;\n }\n const regexLang = /%lang/g;\n let text = ed.selection.getContent();\n // Selection is empty, just insert the lang opening and closing tag\n // together with a space where the user may add the content.\n if (trim(text) === '') {\n // Event is set when the context menu was hit, here the editor lost the previously selected node. Therfore,\n // don't do anything.\n if (!isNull(event)) {\n hideContentToolbar(event.target);\n return;\n }\n let newtext = spanMultilangBegin.replace(regexLang, iso) + ' ' + spanMultilangEnd;\n if (!mlangFilterExists(ed)) {\n // No mlang filter, add the fallback class to the highlight spans so that these are translated\n // to the standard elements.\n newtext = newtext.replaceAll('mceNonEditable', 'mceNonEditable fallback');\n }\n ed.insertContent(newtext);\n return;\n }\n // Hide context toolbar, because at any subsequent call the node is not selected anymore.\n if (!isNull(event)) {\n hideContentToolbar(event.target);\n }\n // No matter if we have syntax highlighting enabled or not, the spans around the language tags exist\n // in the WYSIWYG mode. So check if we are on a special span that encapsulates the language tags. Search\n // for the start span tag.\n const span = getHighlightNodeFromSelect(ed, 'begin');\n // If we have a span, then it's the opening tag, and we just replace this one with the new iso.\n if (!isNull(span)) {\n let replacement = spanMultilangBegin.replace(regexLang, iso);\n if (span.classList.contains('fallback')) {\n replacement = replacement.replace('mceNonEditable', 'mceNonEditable fallback');\n }\n ed.dom.setOuterHTML(span, replacement);\n cleanupBogus(ed);\n return;\n }\n // Check if we have language tags inside the selection:\n if (text.indexOf('multilang-begin') !== -1 || text.indexOf('multilang-end') !== -1) {\n ed.notificationManager.open({\n text: _options.langInSelectionErrMsg,\n type: 'error',\n });\n return;\n }\n const block = getBlockElement(text);\n if (!isNull(block.el)) {\n if (block.cnt === 1) {\n // We have a block element selected, such as a hX or p tag. Then keep this tag and place the\n // language tags inside but around the content of the block element.\n let newtext = spanMultilangBegin.replace(regexLang, iso) + block.el.innerHTML + spanMultilangEnd;\n if (!mlangFilterExists(ed)) { // No mlang filter, add the fallback class to the highlight spans.\n newtext = newtext.replaceAll('mceNonEditable', 'mceNonEditable fallback');\n }\n block.el.innerHTML = newtext;\n ed.selection.setContent(block.el.outerHTML);\n return;\n }\n if (!mlangFilterExists(ed)) {\n ed.notificationManager.open({\n text: _options.multipleBlocksErrMsg,\n type: 'error',\n });\n return;\n }\n }\n // Not inside a lang tag, insert a new opening and closing tag with the selection inside.\n let newtext = spanMultilangBegin.replace(regexLang, iso) + text + spanMultilangEnd;\n if (!mlangFilterExists(ed)) { // No mlang filter, add the fallback class to the highlight spans.\n newtext = newtext.replaceAll('mceNonEditable', 'mceNonEditable fallback');\n }\n ed.selection.setContent(newtext);\n};\n\nexport {\n onInit,\n onBeforeGetContent,\n onFocus,\n onSubmit,\n onDelete,\n applyLanguage\n};\n"],"names":["_isSubmit","_options","addVisualStyling","ed","content","getContent","doc","DOMParser","parseFromString","children","length","nodes","querySelectorAll","span","newSpan","spanMultilangBegin","replace","RegExp","getAttribute","innerHTML","spanMultilangEnd","insertAdjacentHTML","remove","getElementsByTagName","removeVisualStyling","forEach","t","dom","select","classList","contains","end","toRemove","nextSibling","push","nodeType","nodeValue","outerHTML","lang","match","newHTML","setOuterHTML","toLowerCase","getHighlightNodeFromSelect","search","getParents","selection","getStart","elm","pair","previousSibling","hideContentToolbar","el","Node","ELEMENT_NODE","indexOf","style","display","parentNode","options","Object","keys","key","setContent","addStyle","source_view","onClose","off","MutationObserver","mutations","obs","viewSrcModal","document","querySelector","addEventListener","event","action","target","dataset","includes","disconnect","on","observe","body","childList","subtree","no_events","isComposing","clientX","keyCode","nodeName","begin","preventDefault","cleanupBogus","p","parentElement","iso","contentDocument","element","regexLang","text","newtext","replaceAll","insertContent","replacement","notificationManager","open","langInSelectionErrMsg","type","block","result","cnt","firstChild","i","blockTags","tagName","toString","getBlockElement","multipleBlocksErrMsg"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/constants.js b/amd/src/constants.js new file mode 100644 index 0000000..26bc4f4 --- /dev/null +++ b/amd/src/constants.js @@ -0,0 +1,38 @@ +// This file is part of Moodle - https://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Some constants that are required throughout the plugin. + * + * @module tiny_multilang2 + * @copyright 2024 Stephan Robotta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// This class inside a identified the {mlang} tag that is encapsulated in a span. +const spanClass = 'multilang-begin mceNonEditable'; +// This is the element with the data attribute. +const spanFixedAttrs = '{mlang %lang}'; +// The end span doesn't need information about the used language. +export const spanMultilangEnd = spanFixedAttrs.replace('begin', 'end') + '>{mlang}'; +// Helper functions +export const trim = v => v.toString().replace(/^\s+/, '').replace(/\s+$/, ''); +export const isNull = a => a === null || a === undefined; +// These are HTML block elements that are not allowed to be inside a {mlang} tag. +export const blockTags = ['address', 'article', 'aside', 'blockquote', + 'dd', 'div', 'dl', 'dt', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'li', 'ol', 'p', 'pre', 'section', 'tfoot', 'ul']; \ No newline at end of file diff --git a/amd/src/htmlparser.js b/amd/src/htmlparser.js new file mode 100644 index 0000000..5a893a3 --- /dev/null +++ b/amd/src/htmlparser.js @@ -0,0 +1,152 @@ +// This file is part of Moodle - https://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Handling of the editor content to add and remove the visual styling and + * helper nodes to modify language settings. + * + * @module tiny_multilang2 + * @copyright 2024 Stephan Robotta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {spanMultilangBegin, spanMultilangEnd, blockTags} from './constants'; + +/** + * This class is used to parse HTML content and call a callback function + * when a tag is opened, closed or text is found. + */ +class HTMLParser { + constructor() { + this.onTagOpen = null; + this.onTagClose = null; + this.onText = null; + this.chunk = ''; + this.parse = function(input) { + let content = input; + while (content.length > 0) { + let match = content.match(/<[^>]*>/); + if (match) { + let index = match.index; + if (index > 0) { + this.chunk = content.substring(0, index); + if (typeof this.onText === 'function') { + this.onText(this.chunk); + } + } + this.chunk = match[0]; + if (match[0].charAt(1) === '/') { + if (typeof this.onTagClose === 'function') { + this.onTagClose(match[0].substring(2, match[0].length - 1).trim()); + } + } else if (typeof this.onTagOpen === 'function') { + const attr1 = this.mapAttrs(match[0].match(/([\w\-_]+)="([^"]*)"/g)); + const attr2 = this.mapAttrs(match[0].match(/([\w\-_]+)='([^']*)'/g)); + const tag = match[0].match(/^<(\w+)/); + this.onTagOpen(tag[1].toLowerCase(), {...attr1, ...attr2}); + } + content = content.substring(index + match[0].length); + } else { + if (typeof this.onText === 'function') { + this.onText(content); + } + this.chunk = content; + content = ''; + } + } + }; + this.getChunk = function() { + return this.chunk; + }; + this.mapAttrs = function(attrs) { + let res = {}; + if (attrs) { + for (let i = 0; i < attrs.length; i++) { + let [k, v] = attrs[i].split('='); + res[k] = v ? v.substring(1, v.length) : null; + } + } + return res; + }; + } +} + +export const parseEditorContent = function(html) { + let newHtml = ''; + let mlang = 0; + let inClose = false; + const parser = new HTMLParser(); + parser.onTagOpen = function(tag, attr) { + if (tag === 'span' && attr.class && attr.class.indexOf('multilang-begin') > -1) { + mlang++; + } else if (tag === 'span' && attr.class && attr.class.indexOf('multilang-end') > -1) { + mlang--; + inClose = true; + } + newHtml += parser.getChunk(); + }; + parser.onTagClose = function(tag) { + if (blockTags.indexOf(tag) > -1 && mlang != 0) { + if (mlang > 0) { + newHtml += spanMultilangEnd; + mlang--; + } else { + const t = newHtml.lastIndexOf(spanMultilangEnd); + newHtml = newHtml.substring(0, t) + + spanMultilangBegin.replace(new RegExp('%lang', 'g'), 'other') + + newHtml.substring(t); + mlang++; + } + return; + } + if (tag === 'span' && inClose) { + inClose = false; + } + newHtml += parser.getChunk(); + }; + parser.onText = function(text) { + if (mlang > 0 || inClose) { + newHtml += text; + return; + } + const intermediateReplacements = []; + // eslint-disable-next-line no-constant-condition + while (1) { + const m = text.match(new RegExp('{\\s*mlang(\\s+([^}]+?))?\\s*}', 'i')); + if (!m) { + break; + } + const textBefore = text.substring(0, m.index); + const textAfter = text.substring(m.index + m[0].length); + let r = m[0]; + if (!m[2]) { + r = spanMultilangEnd; + mlang--; + } else { + r = spanMultilangBegin.replace(new RegExp('%lang', 'g'), m[2]); + mlang++; + } + intermediateReplacements.push(r); + text = `${textBefore}___~~${intermediateReplacements.length}~~___${textAfter}`; + } + // Revert all placeholders back to the original {mlang} tags. + for (let i = 0; i < intermediateReplacements.length; i++) { + text = text.replace(`___~~${i + 1}~~___`, intermediateReplacements[i]); + } + newHtml += text; + }; + parser.parse(html); + return newHtml; +}; diff --git a/amd/src/ui.js b/amd/src/ui.js index 804bf03..e12da12 100644 --- a/amd/src/ui.js +++ b/amd/src/ui.js @@ -25,19 +25,8 @@ */ import {getHighlightCss, isContentToHighlight, mlangFilterExists} from './options'; - -// This class inside a identified the {mlang} tag that is encapsulated in a span. -const spanClass = 'multilang-begin mceNonEditable'; -// This is the element with the data attribute. -const spanFixedAttrs = '{mlang %lang}'; -// The end span doesn't need information about the used language. -const spanMultilangEnd = spanFixedAttrs.replace('begin', 'end') + '>{mlang}'; -// Helper functions -const trim = v => v.toString().replace(/^\s+/, '').replace(/\s+$/, ''); -const isNull = a => a === null || a === undefined; - +import {isNull, trim, blockTags, spanMultilangBegin, spanMultilangEnd} from './constants'; +import {parseEditorContent} from './htmlparser'; /** * Marker to remember that the submit button was hit. * @type {boolean} @@ -59,48 +48,8 @@ const _options = {}; */ const addVisualStyling = function(ed) { - let content = ed.getContent(); - - // Do not use a variable whether text is already highlighted, do a check for the existing class - // because this is safe for many tiny element windows at one page. - if (content.indexOf(spanClass) !== -1) { - return content; - } - - let intermediateReplacements = []; - // Helper function to check wether te matching {mland} is inside a html node or not. - const replaceHelper = function(m0, m1, p, c) { - // Check if we are inside a html node by checking where the next > and < are. - const textBefore = c.substring(0, p); - const textAfter = c.substring(p + m0.length); - const open = textBefore.lastIndexOf('<'); - const close = textBefore.lastIndexOf('>'); - // If open < close we are outside a html node and can modify the original content - if (open < close) { - if (!m1) { - m0 = spanMultilangEnd; - } else { - m0 = spanMultilangBegin.replace(new RegExp('%lang', 'g'), m1); - } - } // If open > close we are inside a html node and do not modify the original content. - // Store the matched {mlang} tag in the replacements array. - intermediateReplacements.push(m0); - return `${textBefore}___~~${intermediateReplacements.length}~~___${textAfter}`; - }; - // First look for any {mlang} tags in the content string and do a preg_replace with the corresponding - // tag that encapsulated the {mlang} tag so that the {mlang} is highlighted. - // eslint-disable-next-line no-constant-condition - while (1) { - const m = content.match(new RegExp('{\\s*mlang(\\s+([^}]+?))?\\s*}', 'i')); - if (!m) { - break; - } - content = replaceHelper(m[0], m[2], m.index, m.input); - } - // Revert all placeholders back to the original {mlang} tags. - for (let i = 0; i < intermediateReplacements.length; i++) { - content = content.replace(`___~~${i + 1}~~___`, intermediateReplacements[i]); - } + // Parse the editor content and check for all {mlang} tags that are in the html content. + let content = parseEditorContent(ed.getContent()); // Check for the traditional tags, in case these were used as well in the text. // Any tag must be replaced with a