From f26b42d000ec17994c8e935b2822cf3b3f7907d2 Mon Sep 17 00:00:00 2001 From: meirzamoodle Date: Wed, 16 Aug 2023 17:17:56 +0700 Subject: [PATCH 1/2] MDL-77979 tiny_noautolink: Add a TinyMCE noautolink plugin --- lib/classes/plugin_manager.php | 1 + .../noautolink/amd/build/commands.min.js | 3 + .../noautolink/amd/build/commands.min.js.map | 1 + .../noautolink/amd/build/common.min.js | 3 + .../noautolink/amd/build/common.min.js.map | 1 + .../noautolink/amd/build/configuration.min.js | 3 + .../amd/build/configuration.min.js.map | 1 + .../noautolink/amd/build/noautolink.min.js | 10 + .../amd/build/noautolink.min.js.map | 1 + .../noautolink/amd/build/plugin.min.js | 10 + .../noautolink/amd/build/plugin.min.js.map | 1 + .../plugins/noautolink/amd/src/commands.js | 77 ++++++ .../tiny/plugins/noautolink/amd/src/common.js | 29 +++ .../noautolink/amd/src/configuration.js | 34 +++ .../plugins/noautolink/amd/src/noautolink.js | 236 ++++++++++++++++++ .../tiny/plugins/noautolink/amd/src/plugin.js | 52 ++++ .../plugins/noautolink/classes/plugininfo.php | 53 ++++ .../noautolink/classes/privacy/provider.php | 37 +++ .../tiny/plugins/noautolink/db/install.php | 32 +++ .../noautolink/lang/en/tiny_noautolink.php | 31 +++ .../tiny/plugins/noautolink/pix/icon.svg | 1 + .../noautolink/tests/behat/noautolink.feature | 21 ++ .../tiny/plugins/noautolink/version.php | 29 +++ 23 files changed, 667 insertions(+) create mode 100644 lib/editor/tiny/plugins/noautolink/amd/build/commands.min.js create mode 100644 lib/editor/tiny/plugins/noautolink/amd/build/commands.min.js.map create mode 100644 lib/editor/tiny/plugins/noautolink/amd/build/common.min.js create mode 100644 lib/editor/tiny/plugins/noautolink/amd/build/common.min.js.map create mode 100644 lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js create mode 100644 lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js.map create mode 100644 lib/editor/tiny/plugins/noautolink/amd/build/noautolink.min.js create mode 100644 lib/editor/tiny/plugins/noautolink/amd/build/noautolink.min.js.map create mode 100644 lib/editor/tiny/plugins/noautolink/amd/build/plugin.min.js create mode 100644 lib/editor/tiny/plugins/noautolink/amd/build/plugin.min.js.map create mode 100644 lib/editor/tiny/plugins/noautolink/amd/src/commands.js create mode 100644 lib/editor/tiny/plugins/noautolink/amd/src/common.js create mode 100644 lib/editor/tiny/plugins/noautolink/amd/src/configuration.js create mode 100644 lib/editor/tiny/plugins/noautolink/amd/src/noautolink.js create mode 100644 lib/editor/tiny/plugins/noautolink/amd/src/plugin.js create mode 100644 lib/editor/tiny/plugins/noautolink/classes/plugininfo.php create mode 100644 lib/editor/tiny/plugins/noautolink/classes/privacy/provider.php create mode 100644 lib/editor/tiny/plugins/noautolink/db/install.php create mode 100644 lib/editor/tiny/plugins/noautolink/lang/en/tiny_noautolink.php create mode 100644 lib/editor/tiny/plugins/noautolink/pix/icon.svg create mode 100644 lib/editor/tiny/plugins/noautolink/tests/behat/noautolink.feature create mode 100644 lib/editor/tiny/plugins/noautolink/version.php diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index cf49ea22f1f..3a758374d18 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -2038,6 +2038,7 @@ class core_plugin_manager { 'recordrtc', 'link', 'html', + 'noautolink', ], 'theme' => array( diff --git a/lib/editor/tiny/plugins/noautolink/amd/build/commands.min.js b/lib/editor/tiny/plugins/noautolink/amd/build/commands.min.js new file mode 100644 index 00000000000..3cbabef3507 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/build/commands.min.js @@ -0,0 +1,3 @@ +define("tiny_noautolink/commands",["exports","editor_tiny/utils","core/str","tiny_noautolink/common","tiny_noautolink/noautolink"],(function(_exports,_utils,_str,_common,_noautolink){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0;_exports.getSetup=async()=>{const[buttonText,errorInvalidURL,infoEmptySelection,infoAddSuccess,infoRemoveSuccess,buttonImage]=await Promise.all([(0,_str.get_string)("buttontitle",_common.component),(0,_str.get_string)("errorinvalidurl",_common.component),(0,_str.get_string)("infoemptyselection",_common.component),(0,_str.get_string)("infoaddsuccess",_common.component),(0,_str.get_string)("inforemovesuccess",_common.component),(0,_utils.getButtonImage)("icon",_common.component)]);return editor=>{const messages={errorInvalidURL:errorInvalidURL,infoEmptySelection:infoEmptySelection,infoAddSuccess:infoAddSuccess,infoRemoveSuccess:infoRemoveSuccess};editor.ui.registry.addIcon(_common.buttonIcon,buttonImage.html),editor.ui.registry.addToggleButton(_common.buttonName,{icon:_common.buttonIcon,tooltip:buttonText,onAction:()=>{(0,_noautolink.handleAction)(editor,messages)},onSetup:(0,_noautolink.toggleActiveState)(editor)}),editor.ui.registry.addMenuItem(_common.buttonName,{icon:_common.buttonIcon,text:buttonText,onAction:()=>{(0,_noautolink.handleAction)(editor,messages)}})}}})); + +//# sourceMappingURL=commands.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/build/commands.min.js.map b/lib/editor/tiny/plugins/noautolink/amd/build/commands.min.js.map new file mode 100644 index 00000000000..5475dd5fc0d --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/build/commands.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.min.js","sources":["../src/commands.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {getButtonImage} from 'editor_tiny/utils';\nimport {get_string as getString} from 'core/str';\nimport {component, buttonName, buttonIcon} from 'tiny_noautolink/common';\nimport {handleAction, toggleActiveState} from 'tiny_noautolink/noautolink';\n\n/**\n * Tiny noautolink commands.\n *\n * @module tiny_noautolink/commands\n * @copyright 2023 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport const getSetup = async() => {\n const [\n buttonText,\n errorInvalidURL,\n infoEmptySelection,\n infoAddSuccess,\n infoRemoveSuccess,\n buttonImage,\n ] = await Promise.all([\n getString('buttontitle', component),\n getString('errorinvalidurl', component),\n getString('infoemptyselection', component),\n getString('infoaddsuccess', component),\n getString('inforemovesuccess', component),\n getButtonImage('icon', component),\n ]);\n\n return (editor) => {\n\n const messages = {\n errorInvalidURL: errorInvalidURL,\n infoEmptySelection: infoEmptySelection,\n infoAddSuccess: infoAddSuccess,\n infoRemoveSuccess: infoRemoveSuccess\n };\n\n // Register the noautolink Icon.\n editor.ui.registry.addIcon(buttonIcon, buttonImage.html);\n\n // Register the noautolink button.\n editor.ui.registry.addToggleButton(buttonName, {\n icon: buttonIcon,\n tooltip: buttonText,\n onAction: () => {\n handleAction(editor, messages);\n },\n onSetup: toggleActiveState(editor),\n });\n\n // Register the noautolink item.\n editor.ui.registry.addMenuItem(buttonName, {\n icon: buttonIcon,\n text: buttonText,\n onAction: () => {\n handleAction(editor, messages);\n },\n });\n };\n};\n"],"names":["async","buttonText","errorInvalidURL","infoEmptySelection","infoAddSuccess","infoRemoveSuccess","buttonImage","Promise","all","component","editor","messages","ui","registry","addIcon","buttonIcon","html","addToggleButton","buttonName","icon","tooltip","onAction","onSetup","addMenuItem","text"],"mappings":"0RA4BwBA,gBAEhBC,WACAC,gBACAC,mBACAC,eACAC,kBACAC,mBACMC,QAAQC,IAAI,EAClB,mBAAU,cAAeC,oBACzB,mBAAU,kBAAmBA,oBAC7B,mBAAU,qBAAsBA,oBAChC,mBAAU,iBAAkBA,oBAC5B,mBAAU,oBAAqBA,oBAC/B,yBAAe,OAAQA,4BAGnBC,eAEEC,SAAW,CACbT,gBAAiBA,gBACjBC,mBAAoBA,mBACpBC,eAAgBA,eAChBC,kBAAmBA,mBAIvBK,OAAOE,GAAGC,SAASC,QAAQC,mBAAYT,YAAYU,MAGnDN,OAAOE,GAAGC,SAASI,gBAAgBC,mBAAY,CAC3CC,KAAMJ,mBACNK,QAASnB,WACToB,SAAU,kCACOX,OAAQC,WAEzBW,SAAS,iCAAkBZ,UAI/BA,OAAOE,GAAGC,SAASU,YAAYL,mBAAY,CACvCC,KAAMJ,mBACNS,KAAMvB,WACNoB,SAAU,kCACOX,OAAQC"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/build/common.min.js b/lib/editor/tiny/plugins/noautolink/amd/build/common.min.js new file mode 100644 index 00000000000..5c14c0cdf23 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/build/common.min.js @@ -0,0 +1,3 @@ +define("tiny_noautolink/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={pluginName:"tiny_noautolink/plugin",component:"tiny_noautolink",buttonName:"tiny_noautolink",buttonIcon:"tiny_noautolink"},_exports.default})); + +//# sourceMappingURL=common.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/build/common.min.js.map b/lib/editor/tiny/plugins/noautolink/amd/build/common.min.js.map new file mode 100644 index 00000000000..a1a56f191c0 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/build/common.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"common.min.js","sources":["../src/common.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny noautolink common values.\n *\n * @module tiny_noautolink/common\n * @copyright 2023 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n pluginName: 'tiny_noautolink/plugin',\n component: 'tiny_noautolink',\n buttonName: 'tiny_noautolink',\n buttonIcon: 'tiny_noautolink',\n};\n"],"names":["pluginName","component","buttonName","buttonIcon"],"mappings":"wKAuBe,CACXA,WAAY,yBACZC,UAAW,kBACXC,WAAY,kBACZC,WAAY"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js b/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js new file mode 100644 index 00000000000..93a8920f149 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js @@ -0,0 +1,3 @@ +define("tiny_noautolink/configuration",["exports","./common","editor_tiny/utils"],(function(_exports,_common,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>({toolbar:(0,_utils.addToolbarButton)(instanceConfig.toolbar,"content",_common.component),menu:(0,_utils.addMenubarItem)(instanceConfig.menu,"format"," | "+_common.component)})})); + +//# sourceMappingURL=configuration.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js.map b/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js.map new file mode 100644 index 00000000000..badaa6a316f --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"configuration.min.js","sources":["../src/configuration.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny noautolink configuration.\n *\n * @module tiny_noautolink/configuration\n * @copyright 2023 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport {component as buttonName} from './common';\nimport {addToolbarButton, addMenubarItem} from 'editor_tiny/utils';\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the noautolink menu option to the menus and toolbars.\n return {\n toolbar: addToolbarButton(instanceConfig.toolbar, 'content', buttonName),\n menu: addMenubarItem(instanceConfig.menu, 'format', ' | ' + buttonName),\n };\n};\n"],"names":["instanceConfig","toolbar","buttonName","menu"],"mappings":"0NA2B0BA,iBAEf,CACHC,SAAS,2BAAiBD,eAAeC,QAAS,UAAWC,mBAC7DC,MAAM,yBAAeH,eAAeG,KAAM,SAAU,MAAQD"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/build/noautolink.min.js b/lib/editor/tiny/plugins/noautolink/amd/build/noautolink.min.js new file mode 100644 index 00000000000..0dae29b7414 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/build/noautolink.min.js @@ -0,0 +1,10 @@ +define("tiny_noautolink/noautolink",["exports","core/pending"],(function(_exports,_pending){var obj; +/** + * Helper for Tiny noautolink plugin. + * + * @module tiny_noautolink/noautolink + * @copyright 2023 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.toggleActiveState=_exports.handleAction=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};_exports.handleAction=(editor,messages)=>{const toggleState=isInAnchor(editor,editor.selection.getNode()),urlString=getSelectedContent(editor);toggleState||""===urlString?toggleState?unsetNoAutoLink(editor,messages,urlString):editor.notificationManager.open({text:messages.infoEmptySelection,type:"info",timeout:2e3}):setNoAutoLink(editor,messages,urlString)};const setNoAutoLink=(editor,messages,urlString)=>{if(isValidUrl(urlString)){const pendingPromise=new _pending.default("tiny_noautolink/setNoautolink");setNoautolinkOnSelection(editor,urlString).catch((error=>{editor.notificationManager.open({text:error,type:"error",timeout:2e3})})).finally((()=>{editor.notificationManager.open({text:messages.infoAddSuccess,type:"success",timeout:2e3}),pendingPromise.resolve()}))}else editor.notificationManager.open({text:messages.errorInvalidURL,type:"error",timeout:2e3})},unsetNoAutoLink=(editor,messages)=>{const nodeString=editor.selection.getNode().outerHTML.trim(),wrapper=document.createElement("div");wrapper.innerHTML=nodeString;if(wrapper.firstChild.classList.contains("nolink")){const pendingPromise=new _pending.default("tiny_noautolink/setNoautolink");unsetNoautolinkOnSelection(editor,nodeString).catch((error=>{editor.notificationManager.open({text:error,type:"error",timeout:2e3}),pendingPromise.reject(error)})).finally((()=>{editor.notificationManager.open({text:messages.infoRemoveSuccess,type:"success",timeout:2e3}),pendingPromise.resolve()}))}},getSelectedContent=editor=>{const selection=editor.selection;let content=selection.getContent({format:"text"}).trim();if(""==content){const range=selection.getRng();if(range.startContainer.nodeType===Node.TEXT_NODE){const textContent=range.startContainer.textContent,cursorOffset=range.startOffset;let wordStart=cursorOffset;for(;wordStart>0&&/\S/.test(textContent[wordStart-1]);)wordStart--;let wordEnd=cursorOffset;for(;wordEnd{const newContent="<".concat("span",' class="').concat("nolink",'">').concat(url,"");editor.selection.setContent(newContent);const currentNode=editor.selection.getNode(),currentDOM=editor.dom.select("".concat("span",".").concat("nolink"),currentNode);currentDOM.forEach((function(value,index){value.outerHTML!=newContent||editor.selection.select(currentDOM[index])}))},unsetNoautolinkOnSelection=async(editor,url)=>{const regex=new RegExp("]*>"),"g");url=url.replace(regex,"");editor.dom.getParent(editor.selection.getNode(),"span").outerHTML=url},isValidUrl=urlString=>!!new RegExp("^((http|https):\\/\\/|www\\.)((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$","i").test(urlString),isInAnchor=(editor,selectedElm)=>null!==((editor,selectedElm)=>(selectedElm=selectedElm||editor.selection.getNode(),editor.dom.getParent(selectedElm,"".concat("span",".").concat("nolink"))))(editor,selectedElm);_exports.toggleActiveState=editor=>api=>{const updateState=()=>api.setActive(!editor.mode.isReadOnly()&&isInAnchor(editor,editor.selection.getNode()));return updateState(),((editor,toggler)=>(editor.on("NodeChange",toggler),()=>editor.off("NodeChange",toggler)))(editor,updateState)}})); + +//# sourceMappingURL=noautolink.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/build/noautolink.min.js.map b/lib/editor/tiny/plugins/noautolink/amd/build/noautolink.min.js.map new file mode 100644 index 00000000000..06a59883556 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/build/noautolink.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"noautolink.min.js","sources":["../src/noautolink.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Helper for Tiny noautolink plugin.\n *\n * @module tiny_noautolink/noautolink\n * @copyright 2023 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Pending from 'core/pending';\n\nconst noautolinkClassName = 'nolink';\nconst noautolinkTagHTML = 'span';\nconst notificationTimeout = 2000;\n\n/**\n * Handle action.\n *\n * @param {TinyMCE} editor\n * @param {object} messages\n */\nexport const handleAction = (editor, messages) => {\n const toggleState = isInAnchor(editor, editor.selection.getNode());\n const urlString = getSelectedContent(editor);\n if (!toggleState && urlString !== '') {\n setNoAutoLink(editor, messages, urlString);\n } else if (toggleState) {\n unsetNoAutoLink(editor, messages, urlString);\n } else {\n editor.notificationManager.open({text: messages.infoEmptySelection, type: 'info', timeout: notificationTimeout});\n }\n};\n\n/**\n * Display notification feedback when applying the noautolink to the selected text.\n *\n * @param {TinyMCE} editor\n * @param {object} messages\n * @param {String} urlString\n */\nconst setNoAutoLink = (editor, messages, urlString) => {\n // Check whether the string is a URL. Otherwise, show an error notification.\n if (isValidUrl(urlString)) {\n const pendingPromise = new Pending('tiny_noautolink/setNoautolink');\n // Applying the auto-link prevention.\n setNoautolinkOnSelection(editor, urlString)\n .catch(error => {\n editor.notificationManager.open({text: error, type: 'error', timeout: notificationTimeout});\n })\n .finally(() => {\n editor.notificationManager.open({text: messages.infoAddSuccess, type: 'success', timeout: notificationTimeout});\n pendingPromise.resolve();\n });\n } else {\n editor.notificationManager.open({text: messages.errorInvalidURL, type: 'error', timeout: notificationTimeout});\n }\n};\n\n/**\n * Display notification feedback when removing the noautolink to the selected text.\n *\n * @param {TinyMCE} editor\n * @param {object} messages\n */\nconst unsetNoAutoLink = (editor, messages) => {\n const nodeString = editor.selection.getNode().outerHTML.trim();\n // Convert HTML string to DOM element to get nolink class.\n const wrapper = document.createElement('div');\n wrapper.innerHTML = nodeString;\n const tempElement = wrapper.firstChild;\n if (tempElement.classList.contains('nolink')) {\n const pendingPromise = new Pending('tiny_noautolink/setNoautolink');\n // Removing the auto-link prevention.\n unsetNoautolinkOnSelection(editor, nodeString)\n .catch(error => {\n editor.notificationManager.open({text: error, type: 'error', timeout: notificationTimeout});\n pendingPromise.reject(error); // Handle the error as needed.\n })\n .finally(() => {\n editor.notificationManager.open({text: messages.infoRemoveSuccess, type: 'success', timeout: notificationTimeout});\n pendingPromise.resolve();\n });\n }\n};\n\n/**\n * Return the full string based on the position of the cursor within the string.\n *\n * @param {TinyMCE} editor\n * @returns {String}\n */\nconst getSelectedContent = (editor) => {\n const selection = editor.selection; // Get the selection object.\n let content = selection.getContent({format: 'text'}).trim();\n if (content == '') {\n const range = selection.getRng(); // Get the range object.\n\n // Check if the cursor is within a text node.\n if (range.startContainer.nodeType === Node.TEXT_NODE) {\n const textContent = range.startContainer.textContent;\n const cursorOffset = range.startOffset;\n\n // Find the word boundaries around the cursor.\n let wordStart = cursorOffset;\n while (wordStart > 0 && /\\S/.test(textContent[wordStart - 1])) {\n wordStart--;\n }\n\n let wordEnd = cursorOffset;\n while (wordEnd < textContent.length && /\\S/.test(textContent[wordEnd])) {\n wordEnd++;\n }\n\n // Set the selection range to the word.\n selection.setRng({\n startContainer: range.startContainer,\n startOffset: wordStart,\n endContainer: range.startContainer,\n endOffset: wordEnd,\n });\n content = selection.getContent({format: 'text'}).trim();\n }\n }\n return content;\n};\n\n/**\n * Wrap the selection with the nolink class.\n *\n * @param {TinyMCE} editor\n * @param {String} url URL the link will point to.\n */\nconst setNoautolinkOnSelection = async(editor, url) => {\n const newContent = `<${noautolinkTagHTML} class=\"${noautolinkClassName}\">${url}`;\n editor.selection.setContent(newContent);\n\n // Select the new content.\n const currentNode = editor.selection.getNode();\n const currentDOM = editor.dom.select(`${noautolinkTagHTML}.${noautolinkClassName}`, currentNode);\n currentDOM.forEach(function(value, index) {\n if (value.outerHTML == newContent) {\n editor.selection.select(currentDOM[index]);\n return;\n }\n });\n};\n\n/**\n * Remove the nolink on the selection.\n *\n * @param {TinyMCE} editor\n * @param {String} url URL the link will point to.\n */\nconst unsetNoautolinkOnSelection = async(editor, url) => {\n const regex = new RegExp(`]*>`, \"g\");\n url = url.replace(regex, \"\");\n const currentSpan = editor.dom.getParent(editor.selection.getNode(), noautolinkTagHTML);\n currentSpan.outerHTML = url;\n};\n\n/**\n * Check if given string is a valid URL.\n *\n * @param {String} urlString URL the link will point to.\n * @returns {boolean} True is valid, otherwise false.\n */\nconst isValidUrl = urlString => {\n const urlPattern = new RegExp('^((http|https):\\\\/\\\\/|www\\\\.)' + // A URL must have one of these https/https/www.\n '((([a-z\\\\d]([a-z\\\\d-]*[a-z\\\\d])*)\\\\.)+[a-z]{2,}|' + // Validate domain name.\n '((\\\\d{1,3}\\\\.){3}\\\\d{1,3}))' + // Validate ip (v4) address.\n '(\\\\:\\\\d+)?(\\\\/[-a-z\\\\d%_.~+]*)*' + // Validate port and path.\n '(\\\\?[;&a-z\\\\d%_.~+=-]*)?' + // Validate query string.\n '(\\\\#[-a-z\\\\d_]*)?$', 'i'); // Validate fragment locator.\n\n return !!urlPattern.test(urlString);\n};\n\n/**\n * Get anchor element.\n *\n * @param {TinyMCE} editor\n * @param {Element} selectedElm\n * @returns {Element}\n */\nconst getAnchorElement = (editor, selectedElm) => {\n selectedElm = selectedElm || editor.selection.getNode();\n return editor.dom.getParent(selectedElm, `${noautolinkTagHTML}.${noautolinkClassName}`);\n};\n\n\n/**\n * Check the current selected element is an anchor or not.\n *\n * @param {TinyMCE} editor\n * @param {Element} selectedElm\n * @returns {boolean}\n */\nconst isInAnchor = (editor, selectedElm) => getAnchorElement(editor, selectedElm) !== null;\n\n/**\n * Change state of button.\n *\n * @param {TinyMCE} editor\n * @param {function()} toggler\n * @returns {function()}\n */\nconst toggleState = (editor, toggler) => {\n editor.on('NodeChange', toggler);\n return () => editor.off('NodeChange', toggler);\n};\n\n/**\n * Change the active state of button.\n *\n * @param {TinyMCE} editor\n * @returns {function(*): function(): *}\n */\nexport const toggleActiveState = (editor) => (api) => {\n const updateState = () => api.setActive(!editor.mode.isReadOnly() && isInAnchor(editor, editor.selection.getNode()));\n updateState();\n return toggleState(editor, updateState);\n};"],"names":["editor","messages","toggleState","isInAnchor","selection","getNode","urlString","getSelectedContent","unsetNoAutoLink","notificationManager","open","text","infoEmptySelection","type","timeout","setNoAutoLink","isValidUrl","pendingPromise","Pending","setNoautolinkOnSelection","catch","error","finally","infoAddSuccess","resolve","errorInvalidURL","nodeString","outerHTML","trim","wrapper","document","createElement","innerHTML","firstChild","classList","contains","unsetNoautolinkOnSelection","reject","infoRemoveSuccess","content","getContent","format","range","getRng","startContainer","nodeType","Node","TEXT_NODE","textContent","cursorOffset","startOffset","wordStart","test","wordEnd","length","setRng","endContainer","endOffset","async","url","newContent","setContent","currentNode","currentDOM","dom","select","forEach","value","index","regex","RegExp","replace","getParent","selectedElm","getAnchorElement","api","updateState","setActive","mode","isReadOnly","toggler","on","off"],"mappings":"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"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/build/plugin.min.js b/lib/editor/tiny/plugins/noautolink/amd/build/plugin.min.js new file mode 100644 index 00000000000..dc9b022f714 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/build/plugin.min.js @@ -0,0 +1,10 @@ +define("tiny_noautolink/plugin",["exports","editor_tiny/loader","editor_tiny/utils","./common","./commands","./configuration"],(function(_exports,_loader,_utils,_common,Commands,Configuration){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} +/** + * Tiny noautolink plugin. + * + * @module tiny_noautolink/plugin + * @copyright 2023 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Commands=_interopRequireWildcard(Commands),Configuration=_interopRequireWildcard(Configuration);var _default=new Promise((async resolve=>{const[tinyMCE,setupCommands,pluginMetadata]=await Promise.all([(0,_loader.getTinyMCE)(),Commands.getSetup(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName)]);tinyMCE.PluginManager.add("".concat(_common.component,"/plugin"),(editor=>(setupCommands(editor),pluginMetadata))),resolve(["".concat(_common.component,"/plugin"),Configuration])}));return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=plugin.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/build/plugin.min.js.map b/lib/editor/tiny/plugins/noautolink/amd/build/plugin.min.js.map new file mode 100644 index 00000000000..b890b8b2923 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/build/plugin.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"plugin.min.js","sources":["../src/plugin.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny noautolink plugin.\n *\n * @module tiny_noautolink/plugin\n * @copyright 2023 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getTinyMCE} from 'editor_tiny/loader';\nimport {getPluginMetadata} from 'editor_tiny/utils';\n\nimport {component, pluginName} from './common';\nimport * as Commands from './commands';\nimport * as Configuration from './configuration';\n\n// eslint-disable-next-line no-async-promise-executor\nexport default new Promise(async(resolve) => {\n const [\n tinyMCE,\n setupCommands,\n pluginMetadata,\n ] = await Promise.all([\n getTinyMCE(),\n Commands.getSetup(),\n getPluginMetadata(component, pluginName),\n ]);\n\n tinyMCE.PluginManager.add(`${component}/plugin`, (editor) => {\n // Setup the Commands (buttons, menu items, and so on).\n setupCommands(editor);\n\n return pluginMetadata;\n });\n\n // Resolve the noautolink Plugin and include configuration.\n resolve([`${component}/plugin`, Configuration]);\n});\n"],"names":["Promise","async","tinyMCE","setupCommands","pluginMetadata","all","Commands","getSetup","component","pluginName","PluginManager","add","editor","resolve","Configuration"],"mappings":";;;;;;;kMA+Be,IAAIA,SAAQC,MAAAA,gBAEnBC,QACAC,cACAC,sBACMJ,QAAQK,IAAI,EAClB,wBACAC,SAASC,YACT,4BAAkBC,kBAAWC,sBAGjCP,QAAQQ,cAAcC,cAAOH,8BAAqBI,SAE9CT,cAAcS,QAEPR,kBAIXS,QAAQ,WAAIL,6BAAoBM"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/src/commands.js b/lib/editor/tiny/plugins/noautolink/amd/src/commands.js new file mode 100644 index 00000000000..5912dea3da1 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/src/commands.js @@ -0,0 +1,77 @@ +// This file is part of Moodle - http://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 . + +import {getButtonImage} from 'editor_tiny/utils'; +import {get_string as getString} from 'core/str'; +import {component, buttonName, buttonIcon} from 'tiny_noautolink/common'; +import {handleAction, toggleActiveState} from 'tiny_noautolink/noautolink'; + +/** + * Tiny noautolink commands. + * + * @module tiny_noautolink/commands + * @copyright 2023 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +export const getSetup = async() => { + const [ + buttonText, + errorInvalidURL, + infoEmptySelection, + infoAddSuccess, + infoRemoveSuccess, + buttonImage, + ] = await Promise.all([ + getString('buttontitle', component), + getString('errorinvalidurl', component), + getString('infoemptyselection', component), + getString('infoaddsuccess', component), + getString('inforemovesuccess', component), + getButtonImage('icon', component), + ]); + + return (editor) => { + + const messages = { + errorInvalidURL: errorInvalidURL, + infoEmptySelection: infoEmptySelection, + infoAddSuccess: infoAddSuccess, + infoRemoveSuccess: infoRemoveSuccess + }; + + // Register the noautolink Icon. + editor.ui.registry.addIcon(buttonIcon, buttonImage.html); + + // Register the noautolink button. + editor.ui.registry.addToggleButton(buttonName, { + icon: buttonIcon, + tooltip: buttonText, + onAction: () => { + handleAction(editor, messages); + }, + onSetup: toggleActiveState(editor), + }); + + // Register the noautolink item. + editor.ui.registry.addMenuItem(buttonName, { + icon: buttonIcon, + text: buttonText, + onAction: () => { + handleAction(editor, messages); + }, + }); + }; +}; diff --git a/lib/editor/tiny/plugins/noautolink/amd/src/common.js b/lib/editor/tiny/plugins/noautolink/amd/src/common.js new file mode 100644 index 00000000000..18e3bd8840f --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/src/common.js @@ -0,0 +1,29 @@ +// This file is part of Moodle - http://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 . + +/** + * Tiny noautolink common values. + * + * @module tiny_noautolink/common + * @copyright 2023 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +export default { + pluginName: 'tiny_noautolink/plugin', + component: 'tiny_noautolink', + buttonName: 'tiny_noautolink', + buttonIcon: 'tiny_noautolink', +}; diff --git a/lib/editor/tiny/plugins/noautolink/amd/src/configuration.js b/lib/editor/tiny/plugins/noautolink/amd/src/configuration.js new file mode 100644 index 00000000000..c48fd2fb4f0 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/src/configuration.js @@ -0,0 +1,34 @@ +// This file is part of Moodle - http://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 . + +/** + * Tiny noautolink configuration. + * + * @module tiny_noautolink/configuration + * @copyright 2023 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +import {component as buttonName} from './common'; +import {addToolbarButton, addMenubarItem} from 'editor_tiny/utils'; + +export const configure = (instanceConfig) => { + // Update the instance configuration to add the noautolink menu option to the menus and toolbars. + return { + toolbar: addToolbarButton(instanceConfig.toolbar, 'content', buttonName), + menu: addMenubarItem(instanceConfig.menu, 'format', ' | ' + buttonName), + }; +}; diff --git a/lib/editor/tiny/plugins/noautolink/amd/src/noautolink.js b/lib/editor/tiny/plugins/noautolink/amd/src/noautolink.js new file mode 100644 index 00000000000..37e5814bf69 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/src/noautolink.js @@ -0,0 +1,236 @@ +// This file is part of Moodle - http://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 . + +/** + * Helper for Tiny noautolink plugin. + * + * @module tiny_noautolink/noautolink + * @copyright 2023 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Pending from 'core/pending'; + +const noautolinkClassName = 'nolink'; +const noautolinkTagHTML = 'span'; +const notificationTimeout = 2000; + +/** + * Handle action. + * + * @param {TinyMCE} editor + * @param {object} messages + */ +export const handleAction = (editor, messages) => { + const toggleState = isInAnchor(editor, editor.selection.getNode()); + const urlString = getSelectedContent(editor); + if (!toggleState && urlString !== '') { + setNoAutoLink(editor, messages, urlString); + } else if (toggleState) { + unsetNoAutoLink(editor, messages, urlString); + } else { + editor.notificationManager.open({text: messages.infoEmptySelection, type: 'info', timeout: notificationTimeout}); + } +}; + +/** + * Display notification feedback when applying the noautolink to the selected text. + * + * @param {TinyMCE} editor + * @param {object} messages + * @param {String} urlString + */ +const setNoAutoLink = (editor, messages, urlString) => { + // Check whether the string is a URL. Otherwise, show an error notification. + if (isValidUrl(urlString)) { + const pendingPromise = new Pending('tiny_noautolink/setNoautolink'); + // Applying the auto-link prevention. + setNoautolinkOnSelection(editor, urlString) + .catch(error => { + editor.notificationManager.open({text: error, type: 'error', timeout: notificationTimeout}); + }) + .finally(() => { + editor.notificationManager.open({text: messages.infoAddSuccess, type: 'success', timeout: notificationTimeout}); + pendingPromise.resolve(); + }); + } else { + editor.notificationManager.open({text: messages.errorInvalidURL, type: 'error', timeout: notificationTimeout}); + } +}; + +/** + * Display notification feedback when removing the noautolink to the selected text. + * + * @param {TinyMCE} editor + * @param {object} messages + */ +const unsetNoAutoLink = (editor, messages) => { + const nodeString = editor.selection.getNode().outerHTML.trim(); + // Convert HTML string to DOM element to get nolink class. + const wrapper = document.createElement('div'); + wrapper.innerHTML = nodeString; + const tempElement = wrapper.firstChild; + if (tempElement.classList.contains('nolink')) { + const pendingPromise = new Pending('tiny_noautolink/setNoautolink'); + // Removing the auto-link prevention. + unsetNoautolinkOnSelection(editor, nodeString) + .catch(error => { + editor.notificationManager.open({text: error, type: 'error', timeout: notificationTimeout}); + pendingPromise.reject(error); // Handle the error as needed. + }) + .finally(() => { + editor.notificationManager.open({text: messages.infoRemoveSuccess, type: 'success', timeout: notificationTimeout}); + pendingPromise.resolve(); + }); + } +}; + +/** + * Return the full string based on the position of the cursor within the string. + * + * @param {TinyMCE} editor + * @returns {String} + */ +const getSelectedContent = (editor) => { + const selection = editor.selection; // Get the selection object. + let content = selection.getContent({format: 'text'}).trim(); + if (content == '') { + const range = selection.getRng(); // Get the range object. + + // Check if the cursor is within a text node. + if (range.startContainer.nodeType === Node.TEXT_NODE) { + const textContent = range.startContainer.textContent; + const cursorOffset = range.startOffset; + + // Find the word boundaries around the cursor. + let wordStart = cursorOffset; + while (wordStart > 0 && /\S/.test(textContent[wordStart - 1])) { + wordStart--; + } + + let wordEnd = cursorOffset; + while (wordEnd < textContent.length && /\S/.test(textContent[wordEnd])) { + wordEnd++; + } + + // Set the selection range to the word. + selection.setRng({ + startContainer: range.startContainer, + startOffset: wordStart, + endContainer: range.startContainer, + endOffset: wordEnd, + }); + content = selection.getContent({format: 'text'}).trim(); + } + } + return content; +}; + +/** + * Wrap the selection with the nolink class. + * + * @param {TinyMCE} editor + * @param {String} url URL the link will point to. + */ +const setNoautolinkOnSelection = async(editor, url) => { + const newContent = `<${noautolinkTagHTML} class="${noautolinkClassName}">${url}`; + editor.selection.setContent(newContent); + + // Select the new content. + const currentNode = editor.selection.getNode(); + const currentDOM = editor.dom.select(`${noautolinkTagHTML}.${noautolinkClassName}`, currentNode); + currentDOM.forEach(function(value, index) { + if (value.outerHTML == newContent) { + editor.selection.select(currentDOM[index]); + return; + } + }); +}; + +/** + * Remove the nolink on the selection. + * + * @param {TinyMCE} editor + * @param {String} url URL the link will point to. + */ +const unsetNoautolinkOnSelection = async(editor, url) => { + const regex = new RegExp(`]*>`, "g"); + url = url.replace(regex, ""); + const currentSpan = editor.dom.getParent(editor.selection.getNode(), noautolinkTagHTML); + currentSpan.outerHTML = url; +}; + +/** + * Check if given string is a valid URL. + * + * @param {String} urlString URL the link will point to. + * @returns {boolean} True is valid, otherwise false. + */ +const isValidUrl = urlString => { + const urlPattern = new RegExp('^((http|https):\\/\\/|www\\.)' + // A URL must have one of these https/https/www. + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Validate domain name. + '((\\d{1,3}\\.){3}\\d{1,3}))' + // Validate ip (v4) address. + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // Validate port and path. + '(\\?[;&a-z\\d%_.~+=-]*)?' + // Validate query string. + '(\\#[-a-z\\d_]*)?$', 'i'); // Validate fragment locator. + + return !!urlPattern.test(urlString); +}; + +/** + * Get anchor element. + * + * @param {TinyMCE} editor + * @param {Element} selectedElm + * @returns {Element} + */ +const getAnchorElement = (editor, selectedElm) => { + selectedElm = selectedElm || editor.selection.getNode(); + return editor.dom.getParent(selectedElm, `${noautolinkTagHTML}.${noautolinkClassName}`); +}; + + +/** + * Check the current selected element is an anchor or not. + * + * @param {TinyMCE} editor + * @param {Element} selectedElm + * @returns {boolean} + */ +const isInAnchor = (editor, selectedElm) => getAnchorElement(editor, selectedElm) !== null; + +/** + * Change state of button. + * + * @param {TinyMCE} editor + * @param {function()} toggler + * @returns {function()} + */ +const toggleState = (editor, toggler) => { + editor.on('NodeChange', toggler); + return () => editor.off('NodeChange', toggler); +}; + +/** + * Change the active state of button. + * + * @param {TinyMCE} editor + * @returns {function(*): function(): *} + */ +export const toggleActiveState = (editor) => (api) => { + const updateState = () => api.setActive(!editor.mode.isReadOnly() && isInAnchor(editor, editor.selection.getNode())); + updateState(); + return toggleState(editor, updateState); +}; \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/src/plugin.js b/lib/editor/tiny/plugins/noautolink/amd/src/plugin.js new file mode 100644 index 00000000000..bac130d94c0 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/amd/src/plugin.js @@ -0,0 +1,52 @@ +// This file is part of Moodle - http://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 . + +/** + * Tiny noautolink plugin. + * + * @module tiny_noautolink/plugin + * @copyright 2023 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {getTinyMCE} from 'editor_tiny/loader'; +import {getPluginMetadata} from 'editor_tiny/utils'; + +import {component, pluginName} from './common'; +import * as Commands from './commands'; +import * as Configuration from './configuration'; + +// eslint-disable-next-line no-async-promise-executor +export default new Promise(async(resolve) => { + const [ + tinyMCE, + setupCommands, + pluginMetadata, + ] = await Promise.all([ + getTinyMCE(), + Commands.getSetup(), + getPluginMetadata(component, pluginName), + ]); + + tinyMCE.PluginManager.add(`${component}/plugin`, (editor) => { + // Setup the Commands (buttons, menu items, and so on). + setupCommands(editor); + + return pluginMetadata; + }); + + // Resolve the noautolink Plugin and include configuration. + resolve([`${component}/plugin`, Configuration]); +}); diff --git a/lib/editor/tiny/plugins/noautolink/classes/plugininfo.php b/lib/editor/tiny/plugins/noautolink/classes/plugininfo.php new file mode 100644 index 00000000000..ce2fa5c21b9 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/classes/plugininfo.php @@ -0,0 +1,53 @@ +. + +namespace tiny_noautolink; + +use editor_tiny\plugin; +use editor_tiny\plugin_with_buttons; +use editor_tiny\plugin_with_menuitems; + +/** + * Tiny noautolink plugin. + * + * @package tiny_noautolink + * @copyright 2023 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menuitems { + + /** + * Get a list of the buttons provided by this plugin. + * + * @return string[] + */ + public static function get_available_buttons(): array { + return [ + 'tiny_noautolink/tiny_noautolink_link', + ]; + } + + /** + * Get a list of the menu items provided by this plugin. + * + * @return string[] + */ + public static function get_available_menuitems(): array { + return [ + 'tiny_noautolink/tiny_noautolink_link', + ]; + } +} diff --git a/lib/editor/tiny/plugins/noautolink/classes/privacy/provider.php b/lib/editor/tiny/plugins/noautolink/classes/privacy/provider.php new file mode 100644 index 00000000000..95ef63112b3 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/classes/privacy/provider.php @@ -0,0 +1,37 @@ +. + +namespace tiny_noautolink\privacy; + +/** + * Privacy Subsystem for tiny_noautolink implementing null_provider. + * + * @package tiny_noautolink + * @copyright 2023 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/lib/editor/tiny/plugins/noautolink/db/install.php b/lib/editor/tiny/plugins/noautolink/db/install.php new file mode 100644 index 00000000000..28152284281 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/db/install.php @@ -0,0 +1,32 @@ +. + +/** + * Tiny noautolink plugin install code. + * + * @package tiny_noautolink + * @copyright 2023 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Install the Tiny noautolink plugin in a disabled state. + */ +function xmldb_tiny_noautolink_install() { + $tinymanager = \core_plugin_manager::resolve_plugininfo_class('tiny'); + // Disabled the plugin. + $tinymanager::enable_plugin('noautolink', 0); +} diff --git a/lib/editor/tiny/plugins/noautolink/lang/en/tiny_noautolink.php b/lib/editor/tiny/plugins/noautolink/lang/en/tiny_noautolink.php new file mode 100644 index 00000000000..a0758250309 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/lang/en/tiny_noautolink.php @@ -0,0 +1,31 @@ +. + +/** + * Strings for component 'tiny_noautolink', language 'en'. + * + * @package tiny_noautolink + * @copyright 2023 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['buttontitle'] = 'No auto-link'; +$string['errorinvalidurl'] = 'The selected content is not a URL. Please try again.'; +$string['infoaddsuccess'] = 'Added the auto-link prevention.'; +$string['infoemptyselection'] = 'Select a URL and try again.'; +$string['inforemovesuccess'] = 'Removed the auto-link prevention.'; +$string['pluginname'] = 'Tiny no auto-link'; +$string['privacy:metadata'] = 'The tiny_noautolink plugin does not store any personal data.'; diff --git a/lib/editor/tiny/plugins/noautolink/pix/icon.svg b/lib/editor/tiny/plugins/noautolink/pix/icon.svg new file mode 100644 index 00000000000..962eb5cc747 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/pix/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/tests/behat/noautolink.feature b/lib/editor/tiny/plugins/noautolink/tests/behat/noautolink.feature new file mode 100644 index 00000000000..b72297ea217 --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/tests/behat/noautolink.feature @@ -0,0 +1,21 @@ +@editor @editor_tiny @tiny_noautolink +Feature: Tiny noautolink + To avoid auto-linking, users need to wrap the URL with the 'nolink' class. + + @javascript + Scenario: Insert a link, add and remove the auto-link prevention + Given I log in as "admin" + And I navigate to "Plugins > Text editors > TinyMCE editor > General settings" in site administration + And I click on "Enable Tiny no auto-link" "link" + When I open my profile in edit mode + And I set the field "Description" to "

https://moodle.org

" + + # Add auto-link prevention. + And I select the "p" element in position "0" of the "Description" TinyMCE editor + And I click on the "No auto-link" button for the "Description" TinyMCE editor + Then the field "Description" matches value "

https://moodle.org

" + + # Remove auto-link prevention. + And I select the "span" element in position "0" of the "Description" TinyMCE editor + And I click on the "No auto-link" button for the "Description" TinyMCE editor + Then the field "Description" matches value "

https://moodle.org

" diff --git a/lib/editor/tiny/plugins/noautolink/version.php b/lib/editor/tiny/plugins/noautolink/version.php new file mode 100644 index 00000000000..bb254b6a0eb --- /dev/null +++ b/lib/editor/tiny/plugins/noautolink/version.php @@ -0,0 +1,29 @@ +. + +/** + * Tiny noautolink plugin version details. + * + * @package tiny_noautolink + * @copyright 2023 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2023081600; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2023081400; // Requires this Moodle version. +$plugin->component = 'tiny_noautolink'; // Full name of the plugin (used for diagnostics). From 9526b815f5a584361c3008598f98f209efd3906c Mon Sep 17 00:00:00 2001 From: meirzamoodle Date: Tue, 22 Aug 2023 10:32:35 +0700 Subject: [PATCH 2/2] MDL-77979 tiny_noautolink: Add the noautolink plugin to the quickbar Modified the addQuickbarsToolbarItem function on the util to make the additional toolbar item work. --- lib/editor/tiny/amd/build/utils.min.js | 2 +- lib/editor/tiny/amd/build/utils.min.js.map | 2 +- lib/editor/tiny/amd/src/utils.js | 18 ++++++++++-------- .../noautolink/amd/build/configuration.min.js | 2 +- .../amd/build/configuration.min.js.map | 2 +- .../noautolink/amd/src/configuration.js | 5 +++-- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/editor/tiny/amd/build/utils.min.js b/lib/editor/tiny/amd/build/utils.min.js index a672847db29..10cc11e2f85 100644 --- a/lib/editor/tiny/amd/build/utils.min.js +++ b/lib/editor/tiny/amd/build/utils.min.js @@ -1,3 +1,3 @@ -define("editor_tiny/utils",["exports","core/templates","./options","core/str"],(function(_exports,_templates,_options,_str){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.updateEditorState=_exports.removeToolbarButtons=_exports.removeToolbarButton=_exports.removeSubmenuItem=_exports.removeMenubarItems=_exports.removeMenubarItem=_exports.getPluginMetadata=_exports.getImagePath=_exports.getDocumentationLink=_exports.getButtonImage=_exports.ensureEditorIsValid=_exports.displayFilepicker=_exports.addToolbarSection=_exports.addToolbarButtons=_exports.addToolbarButton=_exports.addQuickbarsToolbarItem=_exports.addMenubarItem=_exports.addContextmenuItem=void 0;const getImagePath=function(identifier){let component=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"editor_tiny";return Promise.resolve(M.util.image_url(identifier,component))};_exports.getImagePath=getImagePath;_exports.getButtonImage=async function(identifier){let component=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"editor_tiny";return(0,_templates.renderForPromise)("editor_tiny/toolbar_button",{image:await getImagePath(identifier,component)})};_exports.displayFilepicker=(editor,filetype)=>new Promise(((resolve,reject)=>{const configuration=(0,_options.getFilePicker)(editor,filetype);if(configuration){const options={...configuration,formcallback:resolve};M.core_filepicker.show(Y,options)}else reject("Unknown filetype ".concat(filetype))}));_exports.addToolbarButton=(toolbar,section,button)=>{if(!toolbar)return[{name:section,items:[button]}];return JSON.parse(JSON.stringify(toolbar)).map((item=>(item.name===section&&item.items.push(button),item)))};_exports.addToolbarButtons=(toolbar,section,buttons)=>{if(!toolbar)return[{name:section,items:buttons}];return JSON.parse(JSON.stringify(toolbar)).map((item=>(item.name===section&&buttons.forEach((button=>item.items.push(button))),item)))};_exports.addToolbarSection=function(toolbar,name,relativeTo){let append=!(arguments.length>3&&void 0!==arguments[3])||arguments[3];const newSection={name:name,items:[]},sectionInserted=toolbar.some(((section,index)=>section.name===relativeTo&&(append?toolbar.splice(index+1,0,newSection):toolbar.splice(index,0,newSection),!0)));return sectionInserted||(append?toolbar.push(newSection):toolbar.unshift(newSection)),toolbar};_exports.addMenubarItem=(menubar,section,menuitem)=>{if(!menubar){({})[section]={title:section,items:menuitem}}const mutatedMenubar=JSON.parse(JSON.stringify(menubar));return Array.from(Object.entries(mutatedMenubar)).forEach((_ref=>{let[name,menu]=_ref;name===section&&(menu.items="".concat(menu.items," ").concat(menuitem))})),mutatedMenubar};_exports.addContextmenuItem=function(contextmenu){const contextmenuItems=(null!=contextmenu?contextmenu:"").split(" ");for(var _len=arguments.length,menuitems=new Array(_len>1?_len-1:0),_key=1;_key<_len;_key++)menuitems[_key-1]=arguments[_key];return contextmenuItems.concat(menuitems).filter((item=>""!==item)).join(" ")};_exports.addQuickbarsToolbarItem=function(toolbar){return toolbar};const getDocumentationLink=pluginName=>"https://docs.moodle.org/en/editor_tiny/".concat(pluginName);_exports.getDocumentationLink=getDocumentationLink;_exports.getPluginMetadata=async function(component,pluginName){let url=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const name=await(0,_str.get_string)("helplinktext",component);return{getMetadata:()=>({name:name,url:null!=url?url:getDocumentationLink(pluginName)})}};_exports.ensureEditorIsValid=editor=>editor.getElement().isConnected?editor:null;_exports.removeToolbarButton=(toolbar,section,button)=>{if(!toolbar)return[{name:section,items:[button]}];return JSON.parse(JSON.stringify(toolbar)).map((item=>(item.name===section&&item.items.splice(item.items.indexOf(button),1),item)))};_exports.removeToolbarButtons=(toolbar,section,buttons)=>{if(!toolbar)return[{name:section,items:buttons}];return JSON.parse(JSON.stringify(toolbar)).map((item=>(item.name===section&&buttons.forEach((button=>item.items.splice(item.items.indexOf(button),1))),item)))};_exports.removeSubmenuItem=async(editor,section,submenuitem)=>{const menuItems=editor.ui.registry.getAll().menuItems[section],submenuitemtitle=await(0,_str.get_string)(submenuitem,"editor_tiny");menuItems&&editor.ui.registry.addNestedMenuItem(section,{text:menuItems.text,getSubmenuItems:()=>{let newSubmenu=[];return menuItems.getSubmenuItems().forEach((item=>{item.text.trim()!=submenuitemtitle&&newSubmenu.push(item)})),newSubmenu}})};_exports.removeMenubarItem=(menubar,section,menuitem)=>(menubar[section].items=menubar[section].items.replace(menuitem,""),menubar);_exports.removeMenubarItems=(menubar,section,menuitems)=>{const regexPattern=new RegExp(menuitems.join("|"),"ig");return menubar[section].items=menubar[section].items.replace(regexPattern,""),menubar};_exports.updateEditorState=(editor,target)=>{target.hasAttribute("readonly")?editor.mode.set("readonly"):editor.mode.set("design")}})); +define("editor_tiny/utils",["exports","core/templates","./options","core/str"],(function(_exports,_templates,_options,_str){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.updateEditorState=_exports.removeToolbarButtons=_exports.removeToolbarButton=_exports.removeSubmenuItem=_exports.removeMenubarItems=_exports.removeMenubarItem=_exports.getPluginMetadata=_exports.getImagePath=_exports.getDocumentationLink=_exports.getButtonImage=_exports.ensureEditorIsValid=_exports.displayFilepicker=_exports.addToolbarSection=_exports.addToolbarButtons=_exports.addToolbarButton=_exports.addQuickbarsToolbarItem=_exports.addMenubarItem=_exports.addContextmenuItem=void 0;const getImagePath=function(identifier){let component=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"editor_tiny";return Promise.resolve(M.util.image_url(identifier,component))};_exports.getImagePath=getImagePath;_exports.getButtonImage=async function(identifier){let component=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"editor_tiny";return(0,_templates.renderForPromise)("editor_tiny/toolbar_button",{image:await getImagePath(identifier,component)})};_exports.displayFilepicker=(editor,filetype)=>new Promise(((resolve,reject)=>{const configuration=(0,_options.getFilePicker)(editor,filetype);if(configuration){const options={...configuration,formcallback:resolve};M.core_filepicker.show(Y,options)}else reject("Unknown filetype ".concat(filetype))}));_exports.addToolbarButton=(toolbar,section,button)=>{if(!toolbar)return[{name:section,items:[button]}];return JSON.parse(JSON.stringify(toolbar)).map((item=>(item.name===section&&item.items.push(button),item)))};_exports.addToolbarButtons=(toolbar,section,buttons)=>{if(!toolbar)return[{name:section,items:buttons}];return JSON.parse(JSON.stringify(toolbar)).map((item=>(item.name===section&&buttons.forEach((button=>item.items.push(button))),item)))};_exports.addToolbarSection=function(toolbar,name,relativeTo){let append=!(arguments.length>3&&void 0!==arguments[3])||arguments[3];const newSection={name:name,items:[]},sectionInserted=toolbar.some(((section,index)=>section.name===relativeTo&&(append?toolbar.splice(index+1,0,newSection):toolbar.splice(index,0,newSection),!0)));return sectionInserted||(append?toolbar.push(newSection):toolbar.unshift(newSection)),toolbar};_exports.addMenubarItem=(menubar,section,menuitem)=>{if(!menubar){({})[section]={title:section,items:menuitem}}const mutatedMenubar=JSON.parse(JSON.stringify(menubar));return Array.from(Object.entries(mutatedMenubar)).forEach((_ref=>{let[name,menu]=_ref;name===section&&(menu.items="".concat(menu.items," ").concat(menuitem))})),mutatedMenubar};_exports.addContextmenuItem=function(contextmenu){const contextmenuItems=(null!=contextmenu?contextmenu:"").split(" ");for(var _len=arguments.length,menuitems=new Array(_len>1?_len-1:0),_key=1;_key<_len;_key++)menuitems[_key-1]=arguments[_key];return contextmenuItems.concat(menuitems).filter((item=>""!==item)).join(" ")};_exports.addQuickbarsToolbarItem=function(quicktoolbar){const quicktoolbarItems=(null!=quicktoolbar?quicktoolbar:"").split(" ");for(var _len2=arguments.length,toolbaritems=new Array(_len2>1?_len2-1:0),_key2=1;_key2<_len2;_key2++)toolbaritems[_key2-1]=arguments[_key2];return quicktoolbarItems.concat(toolbaritems).filter((item=>""!==item)).join(" ")};const getDocumentationLink=pluginName=>"https://docs.moodle.org/en/editor_tiny/".concat(pluginName);_exports.getDocumentationLink=getDocumentationLink;_exports.getPluginMetadata=async function(component,pluginName){let url=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const name=await(0,_str.get_string)("helplinktext",component);return{getMetadata:()=>({name:name,url:null!=url?url:getDocumentationLink(pluginName)})}};_exports.ensureEditorIsValid=editor=>editor.getElement().isConnected?editor:null;_exports.removeToolbarButton=(toolbar,section,button)=>{if(!toolbar)return[{name:section,items:[button]}];return JSON.parse(JSON.stringify(toolbar)).map((item=>(item.name===section&&item.items.splice(item.items.indexOf(button),1),item)))};_exports.removeToolbarButtons=(toolbar,section,buttons)=>{if(!toolbar)return[{name:section,items:buttons}];return JSON.parse(JSON.stringify(toolbar)).map((item=>(item.name===section&&buttons.forEach((button=>item.items.splice(item.items.indexOf(button),1))),item)))};_exports.removeSubmenuItem=async(editor,section,submenuitem)=>{const menuItems=editor.ui.registry.getAll().menuItems[section],submenuitemtitle=await(0,_str.get_string)(submenuitem,"editor_tiny");menuItems&&editor.ui.registry.addNestedMenuItem(section,{text:menuItems.text,getSubmenuItems:()=>{let newSubmenu=[];return menuItems.getSubmenuItems().forEach((item=>{item.text.trim()!=submenuitemtitle&&newSubmenu.push(item)})),newSubmenu}})};_exports.removeMenubarItem=(menubar,section,menuitem)=>(menubar[section].items=menubar[section].items.replace(menuitem,""),menubar);_exports.removeMenubarItems=(menubar,section,menuitems)=>{const regexPattern=new RegExp(menuitems.join("|"),"ig");return menubar[section].items=menubar[section].items.replace(regexPattern,""),menubar};_exports.updateEditorState=(editor,target)=>{target.hasAttribute("readonly")?editor.mode.set("readonly"):editor.mode.set("design")}})); //# sourceMappingURL=utils.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/amd/build/utils.min.js.map b/lib/editor/tiny/amd/build/utils.min.js.map index d40d9b35255..2dcce0a09af 100644 --- a/lib/editor/tiny/amd/build/utils.min.js.map +++ b/lib/editor/tiny/amd/build/utils.min.js.map @@ -1 +1 @@ -{"version":3,"file":"utils.min.js","sources":["../src/utils.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {renderForPromise} from 'core/templates';\nimport {getFilePicker} from './options';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Get the image path for the specified image.\n *\n * @param {string} identifier The name of the image\n * @param {string} component The component name\n * @return {string} The image URL path\n */\nexport const getImagePath = (identifier, component = 'editor_tiny') => Promise.resolve(M.util.image_url(identifier, component));\n\nexport const getButtonImage = async(identifier, component = 'editor_tiny') => renderForPromise('editor_tiny/toolbar_button', {\n image: await getImagePath(identifier, component),\n});\n\n/**\n * Helper to display a filepicker and return a Promise.\n *\n * The Promise will resolve when a file is selected, or reject if the file type is not found.\n *\n * @param {TinyMCE} editor\n * @param {string} filetype\n * @returns {Promise} The file object returned by the filepicker\n */\nexport const displayFilepicker = (editor, filetype) => new Promise((resolve, reject) => {\n const configuration = getFilePicker(editor, filetype);\n if (configuration) {\n const options = {\n ...configuration,\n formcallback: resolve,\n };\n M.core_filepicker.show(Y, options);\n return;\n }\n reject(`Unknown filetype ${filetype}`);\n});\n\n/**\n * Given a TinyMCE Toolbar configuration, add the specified button to the named section.\n *\n * @param {object} toolbar\n * @param {string} section\n * @param {string} button\n * @returns {object} The toolbar configuration\n */\nexport const addToolbarButton = (toolbar, section, button) => {\n if (!toolbar) {\n return [{\n name: section,\n items: [button],\n }];\n }\n\n const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));\n return mutatedToolbar.map((item) => {\n if (item.name === section) {\n item.items.push(button);\n }\n\n return item;\n });\n};\n\n/**\n * Given a TinyMCE Toolbar configuration, add the specified buttons to the named section.\n *\n * @param {object} toolbar\n * @param {string} section\n * @param {Array} buttons\n * @returns {object} The toolbar configuration\n */\nexport const addToolbarButtons = (toolbar, section, buttons) => {\n if (!toolbar) {\n return [{\n name: section,\n items: buttons,\n }];\n }\n\n const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));\n return mutatedToolbar.map((item) => {\n if (item.name === section) {\n buttons.forEach(button => item.items.push(button));\n }\n\n return item;\n });\n};\n\n/**\n * Insert a new section into the toolbar.\n *\n * @param {array} toolbar The TinyMCE.editor.settings.toolbar configuration\n * @param {string} name The new section name to add\n * @param {string} relativeTo Insert relative to this section name\n * @param {boolean} append Append or Prepend\n * @returns {array}\n */\nexport const addToolbarSection = (toolbar, name, relativeTo, append = true) => {\n const newSection = {\n name,\n items: [],\n };\n const sectionInserted = toolbar.some((section, index) => {\n if (section.name === relativeTo) {\n if (append) {\n toolbar.splice(index + 1, 0, newSection);\n } else {\n toolbar.splice(index, 0, newSection);\n }\n return true;\n }\n return false;\n });\n\n if (!sectionInserted) {\n // Relative section not found.\n if (append) {\n toolbar.push(newSection);\n } else {\n toolbar.unshift(newSection);\n }\n }\n\n return toolbar;\n};\n\n/**\n * Given a TinyMCE Menubar configuration, add the specified button to the named section.\n *\n * @param {object} menubar\n * @param {string} section\n * @param {string} menuitem\n * @returns {object}\n */\nexport const addMenubarItem = (menubar, section, menuitem) => {\n if (!menubar) {\n const emptyMenubar = {};\n emptyMenubar[section] = {\n title: section,\n items: menuitem,\n };\n }\n\n const mutatedMenubar = JSON.parse(JSON.stringify(menubar));\n Array.from(Object.entries(mutatedMenubar)).forEach(([name, menu]) => {\n if (name === section) {\n menu.items = `${menu.items} ${menuitem}`;\n }\n });\n\n return mutatedMenubar;\n};\n\n/**\n * Given a TinyMCE contextmenu configuration, add the specified button to the end.\n *\n * @param {string} contextmenu\n * @param {string[]} menuitems\n * @returns {string}\n */\nexport const addContextmenuItem = (contextmenu, ...menuitems) => {\n const contextmenuItems = (contextmenu ?? '').split(' ');\n\n return contextmenuItems\n .concat(menuitems)\n .filter((item) => item !== '')\n .join(' ');\n};\n\n/**\n * Given a TinyMCE quickbars configuration, add items to the meun.\n *\n * @param {string} toolbar\n * @param {string[]} menuitems\n * @returns {string}\n */\n// eslint-disable-next-line no-unused-vars\nexport const addQuickbarsToolbarItem = (toolbar, ...menuitems) => {\n // For the moment we have disabled use of this menu.\n // The configuration is left in place to allow plugins to declare that they would like to support it in the future.\n return toolbar;\n};\n\n/**\n * Get the link to the user documentation for the named plugin.\n *\n * @param {string} pluginName\n * @returns {string}\n */\nexport const getDocumentationLink = (pluginName) => `https://docs.moodle.org/en/editor_tiny/${pluginName}`;\n\n/**\n * Get the default plugin metadata for the named plugin.\n * If no URL is provided, then a URL is generated pointing to the standard Moodle Documentation.\n *\n * @param {string} component The component name\n * @param {string} pluginName The plugin name\n * @param {string|null} [url=null] An optional URL to the plugin documentation\n * @returns {object}\n */\nexport const getPluginMetadata = async(component, pluginName, url = null) => {\n const name = await getString('helplinktext', component);\n return {\n getMetadata: () => ({\n name,\n url: url ?? getDocumentationLink(pluginName),\n }),\n };\n};\n\n/**\n * Ensure that the editor is still in the DOM, removing it if it is not.\n *\n * @param {TinyMCE} editor\n * @returns {TinyMCE|null}\n */\nexport const ensureEditorIsValid = (editor) => {\n // TinyMCE uses the element ID as a map key internally, even if the target has changed.\n // In cases such as where an editor is in a modal form which has been detached from the DOM, but the editor not removed,\n // we need to manually destroy the editor.\n // We could theoretically do this with a Mutation Observer, but in some cases the Node may be moved,\n // or added back elsewhere in the DOM.\n if (!editor.getElement().isConnected) {\n return null;\n }\n\n return editor;\n};\n\n/**\n * Given a TinyMCE Toolbar configuration, remove the specified button from the named section.\n *\n * @param {object} toolbar\n * @param {string} section\n * @param {string} button\n * @returns {object} The toolbar configuration\n */\n export const removeToolbarButton = (toolbar, section, button) => {\n if (!toolbar) {\n return [{\n name: section,\n items: [button],\n }];\n }\n\n const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));\n return mutatedToolbar.map((item) => {\n if (item.name === section) {\n item.items.splice(item.items.indexOf(button), 1);\n }\n\n return item;\n });\n};\n\n/**\n * Given a TinyMCE Toolbar configuration, remove the specified buttons from the named section.\n *\n * @param {object} toolbar\n * @param {string} section\n * @param {Array} buttons\n * @returns {object} The toolbar configuration\n */\n export const removeToolbarButtons = (toolbar, section, buttons) => {\n if (!toolbar) {\n return [{\n name: section,\n items: buttons,\n }];\n }\n\n const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));\n return mutatedToolbar.map((item) => {\n if (item.name === section) {\n buttons.forEach(button => item.items.splice(item.items.indexOf(button), 1));\n }\n\n return item;\n });\n};\n\n/**\n * Remove the specified sub-menu item from the named section.\n * Recreate a menu with the same sub-menu items but remove the specified item.\n *\n * @param {TinyMCE} editor\n * @param {string} section\n * @param {string} submenuitem The text of sub-menu that we want to removed\n */\nexport const removeSubmenuItem = async(editor, section, submenuitem) => {\n // Get menu items.\n const menuItems = editor.ui.registry.getAll().menuItems[section];\n\n // Because we will match between title strings,\n // we make sure no problems arise while applying multi-language.\n const submenuitemtitle = await getString(submenuitem, 'editor_tiny');\n\n // Overriding the menu items,\n // by recreating them but excluding the specified sub-menu.\n if (menuItems) {\n editor.ui.registry.addNestedMenuItem(\n section,\n {\n text: menuItems.text,\n getSubmenuItems: () => {\n let newSubmenu = [];\n menuItems.getSubmenuItems().forEach((item) => {\n // Need to trim the text because some of the sub-menus use space to replace an icon.\n if (item.text.trim() != submenuitemtitle) {\n newSubmenu.push(item);\n }\n });\n return newSubmenu;\n }\n }\n );\n }\n};\n\n/**\n * Given a TinyMCE Menubar configuration, remove the specified menu from the named section.\n *\n * @param {string} menubar\n * @param {string} section\n * @param {string} menuitem\n * @returns {object}\n */\nexport const removeMenubarItem = (menubar, section, menuitem) => {\n menubar[section].items = menubar[section].items\n .replace(menuitem, '');\n\n return menubar;\n};\n\n/**\n * Given a TinyMCE Menubar configuration, remove the specified menu from the named section.\n *\n * @param {string} menubar\n * @param {string} section\n * @param {Array} menuitems\n * @returns {object}\n */\nexport const removeMenubarItems = (menubar, section, menuitems) => {\n // Create RegExp pattern.\n const regexPattern = new RegExp(menuitems.join('|'), \"ig\");\n\n // Remove menuitems.\n menubar[section].items = menubar[section].items.replace(regexPattern, '');\n\n return menubar;\n};\n\n/**\n * Updates the state of the editor.\n *\n * @param {TinyMCE} editor\n * @param {HTMLElement} target\n */\nexport const updateEditorState = (editor, target) => {\n if (target.hasAttribute('readonly')) {\n editor.mode.set(\"readonly\");\n } else {\n editor.mode.set(\"design\");\n }\n};\n"],"names":["getImagePath","identifier","component","Promise","resolve","M","util","image_url","async","image","editor","filetype","reject","configuration","options","formcallback","core_filepicker","show","Y","toolbar","section","button","name","items","JSON","parse","stringify","map","item","push","buttons","forEach","relativeTo","append","newSection","sectionInserted","some","index","splice","unshift","menubar","menuitem","title","mutatedMenubar","Array","from","Object","entries","_ref","menu","contextmenu","contextmenuItems","split","menuitems","concat","filter","join","getDocumentationLink","pluginName","url","getMetadata","getElement","isConnected","indexOf","submenuitem","menuItems","ui","registry","getAll","submenuitemtitle","addNestedMenuItem","text","getSubmenuItems","newSubmenu","trim","replace","regexPattern","RegExp","target","hasAttribute","mode","set"],"mappings":"6qBA0BaA,aAAe,SAACC,gBAAYC,iEAAY,qBAAkBC,QAAQC,QAAQC,EAAEC,KAAKC,UAAUN,WAAYC,wEAEtFM,eAAMP,gBAAYC,iEAAY,qBAAkB,+BAAiB,6BAA8B,CACzHO,YAAaT,aAAaC,WAAYC,yCAYT,CAACQ,OAAQC,WAAa,IAAIR,SAAQ,CAACC,QAASQ,gBACnEC,eAAgB,0BAAcH,OAAQC,aACxCE,qBACMC,QAAU,IACTD,cACHE,aAAcX,SAElBC,EAAEW,gBAAgBC,KAAKC,EAAGJ,cAG9BF,kCAA2BD,wCAWC,CAACQ,QAASC,QAASC,cAC1CF,cACM,CAAC,CACJG,KAAMF,QACNG,MAAO,CAACF,iBAIOG,KAAKC,MAAMD,KAAKE,UAAUP,UAC3BQ,KAAKC,OACnBA,KAAKN,OAASF,SACdQ,KAAKL,MAAMM,KAAKR,QAGbO,oCAYkB,CAACT,QAASC,QAASU,eAC3CX,cACM,CAAC,CACJG,KAAMF,QACNG,MAAOO,iBAIQN,KAAKC,MAAMD,KAAKE,UAAUP,UAC3BQ,KAAKC,OACnBA,KAAKN,OAASF,SACdU,QAAQC,SAAQV,QAAUO,KAAKL,MAAMM,KAAKR,UAGvCO,oCAakB,SAACT,QAASG,KAAMU,gBAAYC,wEACnDC,WAAa,CACfZ,KAAAA,KACAC,MAAO,IAELY,gBAAkBhB,QAAQiB,MAAK,CAAChB,QAASiB,QACvCjB,QAAQE,OAASU,aACbC,OACAd,QAAQmB,OAAOD,MAAQ,EAAG,EAAGH,YAE7Bf,QAAQmB,OAAOD,MAAO,EAAGH,aAEtB,YAKVC,kBAEGF,OACAd,QAAQU,KAAKK,YAEbf,QAAQoB,QAAQL,aAIjBf,iCAWmB,CAACqB,QAASpB,QAASqB,gBACxCD,QAAS,EACW,IACRpB,SAAW,CACpBsB,MAAOtB,QACPG,MAAOkB,gBAITE,eAAiBnB,KAAKC,MAAMD,KAAKE,UAAUc,iBACjDI,MAAMC,KAAKC,OAAOC,QAAQJ,iBAAiBZ,SAAQiB,WAAE1B,KAAM2B,WACnD3B,OAASF,UACT6B,KAAK1B,gBAAW0B,KAAK1B,kBAASkB,cAI/BE,4CAUuB,SAACO,mBACzBC,kBAAoBD,MAAAA,YAAAA,YAAe,IAAIE,MAAM,mCADJC,6DAAAA,yCAGxCF,iBACFG,OAAOD,WACPE,QAAQ3B,MAAkB,KAATA,OACjB4B,KAAK,uCAWyB,SAACrC,gBAG7BA,eASEsC,qBAAwBC,6DAAyDA,0FAW7DlD,eAAMN,UAAWwD,gBAAYC,2DAAM,WAC1DrC,WAAa,mBAAU,eAAgBpB,iBACtC,CACH0D,YAAa,MACTtC,KAAAA,KACAqC,IAAKA,MAAAA,IAAAA,IAAOF,qBAAqBC,6CAWThD,QAM3BA,OAAOmD,aAAaC,YAIlBpD,OAHI,kCAcqB,CAACS,QAASC,QAASC,cAC9CF,cACM,CAAC,CACJG,KAAMF,QACNG,MAAO,CAACF,iBAIOG,KAAKC,MAAMD,KAAKE,UAAUP,UAC3BQ,KAAKC,OACnBA,KAAKN,OAASF,SACdQ,KAAKL,MAAMe,OAAOV,KAAKL,MAAMwC,QAAQ1C,QAAS,GAG3CO,uCAYsB,CAACT,QAASC,QAASU,eAC/CX,cACM,CAAC,CACJG,KAAMF,QACNG,MAAOO,iBAIQN,KAAKC,MAAMD,KAAKE,UAAUP,UAC3BQ,KAAKC,OACnBA,KAAKN,OAASF,SACdU,QAAQC,SAAQV,QAAUO,KAAKL,MAAMe,OAAOV,KAAKL,MAAMwC,QAAQ1C,QAAS,KAGrEO,oCAYkBpB,MAAME,OAAQU,QAAS4C,qBAE9CC,UAAYvD,OAAOwD,GAAGC,SAASC,SAASH,UAAU7C,SAIlDiD,uBAAyB,mBAAUL,YAAa,eAIlDC,WACAvD,OAAOwD,GAAGC,SAASG,kBACflD,QACA,CACImD,KAAMN,UAAUM,KAChBC,gBAAiB,SACTC,WAAa,UACjBR,UAAUO,kBAAkBzC,SAASH,OAE7BA,KAAK2C,KAAKG,QAAUL,kBACpBI,WAAW5C,KAAKD,SAGjB6C,0CAeM,CAACjC,QAASpB,QAASqB,YAChDD,QAAQpB,SAASG,MAAQiB,QAAQpB,SAASG,MACrCoD,QAAQlC,SAAU,IAEhBD,qCAWuB,CAACA,QAASpB,QAASiC,mBAE3CuB,aAAe,IAAIC,OAAOxB,UAAUG,KAAK,KAAM,aAGrDhB,QAAQpB,SAASG,MAAQiB,QAAQpB,SAASG,MAAMoD,QAAQC,aAAc,IAE/DpC,oCASsB,CAAC9B,OAAQoE,UAClCA,OAAOC,aAAa,YACpBrE,OAAOsE,KAAKC,IAAI,YAEhBvE,OAAOsE,KAAKC,IAAI"} \ No newline at end of file +{"version":3,"file":"utils.min.js","sources":["../src/utils.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {renderForPromise} from 'core/templates';\nimport {getFilePicker} from './options';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Get the image path for the specified image.\n *\n * @param {string} identifier The name of the image\n * @param {string} component The component name\n * @return {string} The image URL path\n */\nexport const getImagePath = (identifier, component = 'editor_tiny') => Promise.resolve(M.util.image_url(identifier, component));\n\nexport const getButtonImage = async(identifier, component = 'editor_tiny') => renderForPromise('editor_tiny/toolbar_button', {\n image: await getImagePath(identifier, component),\n});\n\n/**\n * Helper to display a filepicker and return a Promise.\n *\n * The Promise will resolve when a file is selected, or reject if the file type is not found.\n *\n * @param {TinyMCE} editor\n * @param {string} filetype\n * @returns {Promise} The file object returned by the filepicker\n */\nexport const displayFilepicker = (editor, filetype) => new Promise((resolve, reject) => {\n const configuration = getFilePicker(editor, filetype);\n if (configuration) {\n const options = {\n ...configuration,\n formcallback: resolve,\n };\n M.core_filepicker.show(Y, options);\n return;\n }\n reject(`Unknown filetype ${filetype}`);\n});\n\n/**\n * Given a TinyMCE Toolbar configuration, add the specified button to the named section.\n *\n * @param {object} toolbar\n * @param {string} section\n * @param {string} button\n * @returns {object} The toolbar configuration\n */\nexport const addToolbarButton = (toolbar, section, button) => {\n if (!toolbar) {\n return [{\n name: section,\n items: [button],\n }];\n }\n\n const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));\n return mutatedToolbar.map((item) => {\n if (item.name === section) {\n item.items.push(button);\n }\n\n return item;\n });\n};\n\n/**\n * Given a TinyMCE Toolbar configuration, add the specified buttons to the named section.\n *\n * @param {object} toolbar\n * @param {string} section\n * @param {Array} buttons\n * @returns {object} The toolbar configuration\n */\nexport const addToolbarButtons = (toolbar, section, buttons) => {\n if (!toolbar) {\n return [{\n name: section,\n items: buttons,\n }];\n }\n\n const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));\n return mutatedToolbar.map((item) => {\n if (item.name === section) {\n buttons.forEach(button => item.items.push(button));\n }\n\n return item;\n });\n};\n\n/**\n * Insert a new section into the toolbar.\n *\n * @param {array} toolbar The TinyMCE.editor.settings.toolbar configuration\n * @param {string} name The new section name to add\n * @param {string} relativeTo Insert relative to this section name\n * @param {boolean} append Append or Prepend\n * @returns {array}\n */\nexport const addToolbarSection = (toolbar, name, relativeTo, append = true) => {\n const newSection = {\n name,\n items: [],\n };\n const sectionInserted = toolbar.some((section, index) => {\n if (section.name === relativeTo) {\n if (append) {\n toolbar.splice(index + 1, 0, newSection);\n } else {\n toolbar.splice(index, 0, newSection);\n }\n return true;\n }\n return false;\n });\n\n if (!sectionInserted) {\n // Relative section not found.\n if (append) {\n toolbar.push(newSection);\n } else {\n toolbar.unshift(newSection);\n }\n }\n\n return toolbar;\n};\n\n/**\n * Given a TinyMCE Menubar configuration, add the specified button to the named section.\n *\n * @param {object} menubar\n * @param {string} section\n * @param {string} menuitem\n * @returns {object}\n */\nexport const addMenubarItem = (menubar, section, menuitem) => {\n if (!menubar) {\n const emptyMenubar = {};\n emptyMenubar[section] = {\n title: section,\n items: menuitem,\n };\n }\n\n const mutatedMenubar = JSON.parse(JSON.stringify(menubar));\n Array.from(Object.entries(mutatedMenubar)).forEach(([name, menu]) => {\n if (name === section) {\n menu.items = `${menu.items} ${menuitem}`;\n }\n });\n\n return mutatedMenubar;\n};\n\n/**\n * Given a TinyMCE contextmenu configuration, add the specified button to the end.\n *\n * @param {string} contextmenu\n * @param {string[]} menuitems\n * @returns {string}\n */\nexport const addContextmenuItem = (contextmenu, ...menuitems) => {\n const contextmenuItems = (contextmenu ?? '').split(' ');\n\n return contextmenuItems\n .concat(menuitems)\n .filter((item) => item !== '')\n .join(' ');\n};\n\n/**\n * Given a TinyMCE quickbars configuration, add items to the menu.\n *\n * @param {string} quicktoolbar\n * @param {string[]} toolbaritems\n * @returns {string}\n */\nexport const addQuickbarsToolbarItem = (quicktoolbar, ...toolbaritems) => {\n const quicktoolbarItems = (quicktoolbar ?? '').split(' ');\n\n return quicktoolbarItems\n .concat(toolbaritems)\n .filter((item) => item !== '')\n .join(' ');\n};\n\n/**\n * Get the link to the user documentation for the named plugin.\n *\n * @param {string} pluginName\n * @returns {string}\n */\nexport const getDocumentationLink = (pluginName) => `https://docs.moodle.org/en/editor_tiny/${pluginName}`;\n\n/**\n * Get the default plugin metadata for the named plugin.\n * If no URL is provided, then a URL is generated pointing to the standard Moodle Documentation.\n *\n * @param {string} component The component name\n * @param {string} pluginName The plugin name\n * @param {string|null} [url=null] An optional URL to the plugin documentation\n * @returns {object}\n */\nexport const getPluginMetadata = async(component, pluginName, url = null) => {\n const name = await getString('helplinktext', component);\n return {\n getMetadata: () => ({\n name,\n url: url ?? getDocumentationLink(pluginName),\n }),\n };\n};\n\n/**\n * Ensure that the editor is still in the DOM, removing it if it is not.\n *\n * @param {TinyMCE} editor\n * @returns {TinyMCE|null}\n */\nexport const ensureEditorIsValid = (editor) => {\n // TinyMCE uses the element ID as a map key internally, even if the target has changed.\n // In cases such as where an editor is in a modal form which has been detached from the DOM, but the editor not removed,\n // we need to manually destroy the editor.\n // We could theoretically do this with a Mutation Observer, but in some cases the Node may be moved,\n // or added back elsewhere in the DOM.\n if (!editor.getElement().isConnected) {\n return null;\n }\n\n return editor;\n};\n\n/**\n * Given a TinyMCE Toolbar configuration, remove the specified button from the named section.\n *\n * @param {object} toolbar\n * @param {string} section\n * @param {string} button\n * @returns {object} The toolbar configuration\n */\n export const removeToolbarButton = (toolbar, section, button) => {\n if (!toolbar) {\n return [{\n name: section,\n items: [button],\n }];\n }\n\n const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));\n return mutatedToolbar.map((item) => {\n if (item.name === section) {\n item.items.splice(item.items.indexOf(button), 1);\n }\n\n return item;\n });\n};\n\n/**\n * Given a TinyMCE Toolbar configuration, remove the specified buttons from the named section.\n *\n * @param {object} toolbar\n * @param {string} section\n * @param {Array} buttons\n * @returns {object} The toolbar configuration\n */\n export const removeToolbarButtons = (toolbar, section, buttons) => {\n if (!toolbar) {\n return [{\n name: section,\n items: buttons,\n }];\n }\n\n const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));\n return mutatedToolbar.map((item) => {\n if (item.name === section) {\n buttons.forEach(button => item.items.splice(item.items.indexOf(button), 1));\n }\n\n return item;\n });\n};\n\n/**\n * Remove the specified sub-menu item from the named section.\n * Recreate a menu with the same sub-menu items but remove the specified item.\n *\n * @param {TinyMCE} editor\n * @param {string} section\n * @param {string} submenuitem The text of sub-menu that we want to removed\n */\nexport const removeSubmenuItem = async(editor, section, submenuitem) => {\n // Get menu items.\n const menuItems = editor.ui.registry.getAll().menuItems[section];\n\n // Because we will match between title strings,\n // we make sure no problems arise while applying multi-language.\n const submenuitemtitle = await getString(submenuitem, 'editor_tiny');\n\n // Overriding the menu items,\n // by recreating them but excluding the specified sub-menu.\n if (menuItems) {\n editor.ui.registry.addNestedMenuItem(\n section,\n {\n text: menuItems.text,\n getSubmenuItems: () => {\n let newSubmenu = [];\n menuItems.getSubmenuItems().forEach((item) => {\n // Need to trim the text because some of the sub-menus use space to replace an icon.\n if (item.text.trim() != submenuitemtitle) {\n newSubmenu.push(item);\n }\n });\n return newSubmenu;\n }\n }\n );\n }\n};\n\n/**\n * Given a TinyMCE Menubar configuration, remove the specified menu from the named section.\n *\n * @param {string} menubar\n * @param {string} section\n * @param {string} menuitem\n * @returns {object}\n */\nexport const removeMenubarItem = (menubar, section, menuitem) => {\n menubar[section].items = menubar[section].items\n .replace(menuitem, '');\n\n return menubar;\n};\n\n/**\n * Given a TinyMCE Menubar configuration, remove the specified menu from the named section.\n *\n * @param {string} menubar\n * @param {string} section\n * @param {Array} menuitems\n * @returns {object}\n */\nexport const removeMenubarItems = (menubar, section, menuitems) => {\n // Create RegExp pattern.\n const regexPattern = new RegExp(menuitems.join('|'), \"ig\");\n\n // Remove menuitems.\n menubar[section].items = menubar[section].items.replace(regexPattern, '');\n\n return menubar;\n};\n\n/**\n * Updates the state of the editor.\n *\n * @param {TinyMCE} editor\n * @param {HTMLElement} target\n */\nexport const updateEditorState = (editor, target) => {\n if (target.hasAttribute('readonly')) {\n editor.mode.set(\"readonly\");\n } else {\n editor.mode.set(\"design\");\n }\n};\n"],"names":["getImagePath","identifier","component","Promise","resolve","M","util","image_url","async","image","editor","filetype","reject","configuration","options","formcallback","core_filepicker","show","Y","toolbar","section","button","name","items","JSON","parse","stringify","map","item","push","buttons","forEach","relativeTo","append","newSection","sectionInserted","some","index","splice","unshift","menubar","menuitem","title","mutatedMenubar","Array","from","Object","entries","_ref","menu","contextmenu","contextmenuItems","split","menuitems","concat","filter","join","quicktoolbar","quicktoolbarItems","toolbaritems","getDocumentationLink","pluginName","url","getMetadata","getElement","isConnected","indexOf","submenuitem","menuItems","ui","registry","getAll","submenuitemtitle","addNestedMenuItem","text","getSubmenuItems","newSubmenu","trim","replace","regexPattern","RegExp","target","hasAttribute","mode","set"],"mappings":"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"} \ No newline at end of file diff --git a/lib/editor/tiny/amd/src/utils.js b/lib/editor/tiny/amd/src/utils.js index ebaeb78be4a..e7e55eaf75f 100644 --- a/lib/editor/tiny/amd/src/utils.js +++ b/lib/editor/tiny/amd/src/utils.js @@ -186,17 +186,19 @@ export const addContextmenuItem = (contextmenu, ...menuitems) => { }; /** - * Given a TinyMCE quickbars configuration, add items to the meun. + * Given a TinyMCE quickbars configuration, add items to the menu. * - * @param {string} toolbar - * @param {string[]} menuitems + * @param {string} quicktoolbar + * @param {string[]} toolbaritems * @returns {string} */ -// eslint-disable-next-line no-unused-vars -export const addQuickbarsToolbarItem = (toolbar, ...menuitems) => { - // For the moment we have disabled use of this menu. - // The configuration is left in place to allow plugins to declare that they would like to support it in the future. - return toolbar; +export const addQuickbarsToolbarItem = (quicktoolbar, ...toolbaritems) => { + const quicktoolbarItems = (quicktoolbar ?? '').split(' '); + + return quicktoolbarItems + .concat(toolbaritems) + .filter((item) => item !== '') + .join(' '); }; /** diff --git a/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js b/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js index 93a8920f149..b1dc177929e 100644 --- a/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js +++ b/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js @@ -1,3 +1,3 @@ -define("tiny_noautolink/configuration",["exports","./common","editor_tiny/utils"],(function(_exports,_common,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>({toolbar:(0,_utils.addToolbarButton)(instanceConfig.toolbar,"content",_common.component),menu:(0,_utils.addMenubarItem)(instanceConfig.menu,"format"," | "+_common.component)})})); +define("tiny_noautolink/configuration",["exports","./common","editor_tiny/utils"],(function(_exports,_common,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>({quickbars_selection_toolbar:(0,_utils.addQuickbarsToolbarItem)(instanceConfig.quickbars_selection_toolbar,"|",_common.component),toolbar:(0,_utils.addToolbarButton)(instanceConfig.toolbar,"content",_common.component),menu:(0,_utils.addMenubarItem)(instanceConfig.menu,"format"," | "+_common.component)})})); //# sourceMappingURL=configuration.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js.map b/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js.map index badaa6a316f..432f30166d7 100644 --- a/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js.map +++ b/lib/editor/tiny/plugins/noautolink/amd/build/configuration.min.js.map @@ -1 +1 @@ -{"version":3,"file":"configuration.min.js","sources":["../src/configuration.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny noautolink configuration.\n *\n * @module tiny_noautolink/configuration\n * @copyright 2023 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport {component as buttonName} from './common';\nimport {addToolbarButton, addMenubarItem} from 'editor_tiny/utils';\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the noautolink menu option to the menus and toolbars.\n return {\n toolbar: addToolbarButton(instanceConfig.toolbar, 'content', buttonName),\n menu: addMenubarItem(instanceConfig.menu, 'format', ' | ' + buttonName),\n };\n};\n"],"names":["instanceConfig","toolbar","buttonName","menu"],"mappings":"0NA2B0BA,iBAEf,CACHC,SAAS,2BAAiBD,eAAeC,QAAS,UAAWC,mBAC7DC,MAAM,yBAAeH,eAAeG,KAAM,SAAU,MAAQD"} \ No newline at end of file +{"version":3,"file":"configuration.min.js","sources":["../src/configuration.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny noautolink configuration.\n *\n * @module tiny_noautolink/configuration\n * @copyright 2023 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {component as buttonName} from './common';\nimport {addQuickbarsToolbarItem, addToolbarButton, addMenubarItem} from 'editor_tiny/utils';\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the noautolink menu option to the menus and toolbars.\n return {\n // eslint-disable-next-line camelcase\n quickbars_selection_toolbar: addQuickbarsToolbarItem(instanceConfig.quickbars_selection_toolbar, '|', buttonName),\n toolbar: addToolbarButton(instanceConfig.toolbar, 'content', buttonName),\n menu: addMenubarItem(instanceConfig.menu, 'format', ' | ' + buttonName),\n };\n};\n"],"names":["instanceConfig","quickbars_selection_toolbar","buttonName","toolbar","menu"],"mappings":"0NA0B0BA,iBAEf,CAEHC,6BAA6B,kCAAwBD,eAAeC,4BAA6B,IAAKC,mBACtGC,SAAS,2BAAiBH,eAAeG,QAAS,UAAWD,mBAC7DE,MAAM,yBAAeJ,eAAeI,KAAM,SAAU,MAAQF"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/noautolink/amd/src/configuration.js b/lib/editor/tiny/plugins/noautolink/amd/src/configuration.js index c48fd2fb4f0..8f531e1909c 100644 --- a/lib/editor/tiny/plugins/noautolink/amd/src/configuration.js +++ b/lib/editor/tiny/plugins/noautolink/amd/src/configuration.js @@ -21,13 +21,14 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - import {component as buttonName} from './common'; -import {addToolbarButton, addMenubarItem} from 'editor_tiny/utils'; +import {addQuickbarsToolbarItem, addToolbarButton, addMenubarItem} from 'editor_tiny/utils'; export const configure = (instanceConfig) => { // Update the instance configuration to add the noautolink menu option to the menus and toolbars. return { + // eslint-disable-next-line camelcase + quickbars_selection_toolbar: addQuickbarsToolbarItem(instanceConfig.quickbars_selection_toolbar, '|', buttonName), toolbar: addToolbarButton(instanceConfig.toolbar, 'content', buttonName), menu: addMenubarItem(instanceConfig.menu, 'format', ' | ' + buttonName), };