diff --git a/amd/build/ui.min.js b/amd/build/ui.min.js index c3c3298..52f2fc3 100644 --- a/amd/build/ui.min.js +++ b/amd/build/ui.min.js @@ -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;content=content.replace(new RegExp("{\\s*mlang\\s+([^}]+?)\\s*}","ig"),(function(match,p1){return spanMultilangBegin.replace(new RegExp("%lang","g"),p1)})),content=content.replace(new RegExp("{\\s*mlang\\s*}","ig"),spanMultilangEnd);const doc=(new DOMParser).parseFromString(content,"text/html");if(0===doc.children.length)return content;const nodes=doc.querySelectorAll("span.multilang");if(0===nodes.length)return content;for(const span of nodes){const newSpan=spanMultilangBegin.replace(new RegExp("%lang","g"),span.getAttribute("lang")).replace("mceNonEditable","mceNonEditable fallback")+span.innerHTML+spanMultilangEnd.replace("mceNonEditable","mceNonEditable fallback");span.insertAdjacentHTML("afterend",newSpan),span.remove()}return doc.getElementsByTagName("body")[0].innerHTML},removeVisualStyling=function(ed){["begin","end"].forEach((function(t){for(const span of ed.dom.select("span.multilang-"+t))if("begin"===t&&span.classList.contains("fallback")){let innerHTML="",end=span,toRemove=[];for(;end&&(end=end.nextSibling,!isNull(end));){if(!isNull(end.classList)&&end.classList.contains("multilang-end")){toRemove.push(end);break}3===end.nodeType?innerHTML+=end.nodeValue:1===end.nodeType&&(innerHTML+=end.outerHTML),toRemove.push(end)}if(!isNull(end)){const lang=span.innerHTML.match(new RegExp("{\\s*mlang\\s+([^}]+?)\\s*}","i")),rtlLanguages=(0,_options2.getRTLLanguages)();if(lang){const langCode=lang[1],dir=rtlLanguages.includes(langCode)?"rtl":"ltr",newHTML=''+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){var onClose=function(ed){ed.off("close",onClose),ed.setContent(addVisualStyling(ed))};ed.on("CloseWindow",(()=>{onClose(ed)})),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))};_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")),void ed.dom.setOuterHTML(span,replacement)}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{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;content=content.replace(new RegExp("{\\s*mlang\\s+([^}]+?)\\s*}","ig"),(function(match,p1){return spanMultilangBegin.replace(new RegExp("%lang","g"),p1)})),content=content.replace(new RegExp("{\\s*mlang\\s*}","ig"),spanMultilangEnd);const doc=(new DOMParser).parseFromString(content,"text/html");if(0===doc.children.length)return content;const nodes=doc.querySelectorAll("span.multilang");if(0===nodes.length)return content;for(const span of nodes){const newSpan=spanMultilangBegin.replace(new RegExp("%lang","g"),span.getAttribute("lang")).replace("mceNonEditable","mceNonEditable fallback")+span.innerHTML+spanMultilangEnd.replace("mceNonEditable","mceNonEditable fallback");span.insertAdjacentHTML("afterend",newSpan),span.remove()}return doc.getElementsByTagName("body")[0].innerHTML},removeVisualStyling=function(ed){["begin","end"].forEach((function(t){for(const span of ed.dom.select("span.multilang-"+t))if("begin"===t&&span.classList.contains("fallback")){let innerHTML="",end=span,toRemove=[];for(;end&&(end=end.nextSibling,!isNull(end));){if(!isNull(end.classList)&&end.classList.contains("multilang-end")){toRemove.push(end);break}3===end.nodeType?innerHTML+=end.nodeValue:1===end.nodeType&&(innerHTML+=end.outerHTML),toRemove.push(end)}if(!isNull(end)){const lang=span.innerHTML.match(new RegExp("{\\s*mlang\\s+([^}]+?)\\s*}","i")),rtlLanguages=(0,_options2.getRTLLanguages)();if(lang){const langCode=lang[1],dir=rtlLanguages.includes(langCode)?"rtl":"ltr",newHTML=''+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){var onClose=function(ed){ed.off("close",onClose),ed.setContent(addVisualStyling(ed))};ed.on("CloseWindow",(()=>{onClose(ed)})),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.\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, getRTLLanguages} 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 // 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 content = content.replace(new RegExp('{\\\\s*mlang\\\\s+([^}]+?)\\\\s*}', 'ig'), function(match, p1) {\n return spanMultilangBegin.replace(new RegExp('%lang', 'g'), p1);\n });\n content = content.replace(new RegExp('{\\\\s*mlang\\\\s*}', 'ig'), spanMultilangEnd);\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 // Right to left default languages.\n const rtlLanguages = getRTLLanguages();\n if (lang) {\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 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 var onClose = function(ed) {\n ed.off('close', onClose);\n ed.setContent(addVisualStyling(ed));\n };\n ed.on('CloseWindow', () => {\n onClose(ed);\n });\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 }\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 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","RegExp","match","p1","doc","DOMParser","parseFromString","children","length","nodes","querySelectorAll","span","newSpan","getAttribute","innerHTML","insertAdjacentHTML","remove","getElementsByTagName","removeVisualStyling","forEach","t","dom","select","classList","contains","end","toRemove","nextSibling","push","nodeType","nodeValue","outerHTML","lang","rtlLanguages","langCode","dir","includes","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","on","no_events","event","isComposing","clientX","keyCode","target","nodeName","begin","preventDefault","iso","contentDocument","body","element","regexLang","text","toString","newtext","replaceAll","insertContent","replacement","notificationManager","open","langInSelectionErrMsg","type","block","result","cnt","firstChild","blockTags","i","tagName","getBlockElement","multipleBlocksErrMsg"],"mappings":"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"} \ 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, getRTLLanguages} 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 // 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 content = content.replace(new RegExp('{\\\\s*mlang\\\\s+([^}]+?)\\\\s*}', 'ig'), function(match, p1) {\n return spanMultilangBegin.replace(new RegExp('%lang', 'g'), p1);\n });\n content = content.replace(new RegExp('{\\\\s*mlang\\\\s*}', 'ig'), spanMultilangEnd);\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 // Right to left default languages.\n const rtlLanguages = getRTLLanguages();\n if (lang) {\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 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 var onClose = function(ed) {\n ed.off('close', onClose);\n ed.setContent(addVisualStyling(ed));\n };\n ed.on('CloseWindow', () => {\n onClose(ed);\n });\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","RegExp","match","p1","doc","DOMParser","parseFromString","children","length","nodes","querySelectorAll","span","newSpan","getAttribute","innerHTML","insertAdjacentHTML","remove","getElementsByTagName","removeVisualStyling","forEach","t","dom","select","classList","contains","end","toRemove","nextSibling","push","nodeType","nodeValue","outerHTML","lang","rtlLanguages","langCode","dir","includes","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","on","no_events","event","isComposing","clientX","keyCode","target","nodeName","begin","preventDefault","cleanupBogus","p","parentElement","iso","contentDocument","body","element","regexLang","text","toString","newtext","replaceAll","insertContent","replacement","notificationManager","open","langInSelectionErrMsg","type","block","result","cnt","firstChild","blockTags","i","tagName","getBlockElement","multipleBlocksErrMsg"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/ui.js b/amd/src/ui.js index 8628ed2..aa4dc75 100644 --- a/amd/src/ui.js +++ b/amd/src/ui.js @@ -337,6 +337,21 @@ const onDelete = function(ed, event) { if (!isNull(event.clientX)) { hideContentToolbar(event.target); } + cleanupBogus(ed); + } +}; + +/** + * In the tinyMCE of Moodle 4.1 and 4.2 some leftovers from the element selection can be seen when the source code + * is displayed. Remove these. Apparently 4.3 does not have this problem anymore. + * @param {tinymce.Editor} ed + */ +const cleanupBogus = function(ed) { + for (const span of ed.dom.select('span[class*="multilang"')) { + const p = span.parentElement; + if (!isNull(p.classList) && p.classList.contains('mce-offscreen-selection')) { + ed.dom.remove(p); + } } }; @@ -395,6 +410,7 @@ const applyLanguage = function(ed, iso, event) { replacement = replacement.replace('mceNonEditable', 'mceNonEditable fallback'); } ed.dom.setOuterHTML(span, replacement); + cleanupBogus(ed); return; } // Check if we have language tags inside the selection: