diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php
index 29443c3fe0712..0af212945ad6c 100644
--- a/lib/classes/plugin_manager.php
+++ b/lib/classes/plugin_manager.php
@@ -2034,6 +2034,7 @@ public static function standard_plugins_list($type) {
'h5p',
'media',
'recordrtc',
+ 'link'
],
'theme' => array(
diff --git a/lib/editor/tiny/classes/manager.php b/lib/editor/tiny/classes/manager.php
index 5f7241bcec761..b6d16ced508a3 100644
--- a/lib/editor/tiny/classes/manager.php
+++ b/lib/editor/tiny/classes/manager.php
@@ -497,6 +497,9 @@ protected function get_disabled_tinymce_plugins(): array {
// Disable the preview plugin as it does not support Moodle filters.
'preview',
+
+ // Use the Moodle link plugin instead.
+ 'link',
];
}
diff --git a/lib/editor/tiny/plugins/link/amd/build/commands.min.js b/lib/editor/tiny/plugins/link/amd/build/commands.min.js
new file mode 100644
index 0000000000000..e0811d90f4c2f
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/commands.min.js
@@ -0,0 +1,3 @@
+define("tiny_link/commands",["exports","core/str","tiny_link/common","tiny_link/ui","tiny_link/link"],(function(_exports,_str,_common,_ui,_link){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0;_exports.getSetup=async()=>{const[linkButtonText,unlinkButtonText]=await Promise.all([(0,_str.get_string)("link",_common.component),(0,_str.get_string)("unlink",_common.component)]);return editor=>{editor.ui.registry.addToggleButton(_common.linkButtonShortName,{icon:"link",tooltip:linkButtonText,onAction:()=>{(0,_ui.handleAction)(editor)},onSetup:(0,_link.toggleActiveState)(editor)}),editor.ui.registry.addMenuItem(_common.linkButtonShortName,{icon:"link",shortcut:"Meta+K",text:linkButtonText,onAction:()=>{(0,_ui.handleAction)(editor)}}),editor.ui.registry.addToggleButton(_common.unlinkButtonShortName,{icon:"unlink",tooltip:unlinkButtonText,onAction:()=>{(0,_ui.handleAction)(editor,!0)},onSetup:(0,_link.toggleActiveState)(editor)}),editor.shortcuts.add("Meta+K","Shortcut for create link",(()=>{(0,_ui.handleAction)(editor)}))}}}));
+
+//# sourceMappingURL=commands.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/commands.min.js.map b/lib/editor/tiny/plugins/link/amd/build/commands.min.js.map
new file mode 100644
index 0000000000000..26c41337d229d
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/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 {get_string as getString} from 'core/str';\nimport {component, linkButtonShortName, unlinkButtonShortName} from 'tiny_link/common';\nimport {handleAction} from 'tiny_link/ui';\nimport {toggleActiveState} from 'tiny_link/link';\n\n/**\n * Tiny Link commands.\n *\n * @module tiny_link/commands\n * @copyright 2023 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport const getSetup = async() => {\n const [\n linkButtonText,\n unlinkButtonText,\n ] = await Promise.all([\n getString('link', component),\n getString('unlink', component),\n ]);\n\n return (editor) => {\n // Register Link button.\n editor.ui.registry.addToggleButton(linkButtonShortName, {\n icon: 'link',\n tooltip: linkButtonText,\n onAction: () => {\n handleAction(editor);\n },\n onSetup: toggleActiveState(editor),\n });\n\n // Register the Link menu item.\n editor.ui.registry.addMenuItem(linkButtonShortName, {\n icon: 'link',\n shortcut: 'Meta+K',\n text: linkButtonText,\n onAction: () => {\n handleAction(editor);\n },\n });\n\n // Register Unlink button.\n editor.ui.registry.addToggleButton(unlinkButtonShortName, {\n icon: 'unlink',\n tooltip: unlinkButtonText,\n onAction: () => {\n handleAction(editor, true);\n },\n onSetup: toggleActiveState(editor),\n });\n\n // Register shortcut.\n editor.shortcuts.add('Meta+K', 'Shortcut for create link', () => {\n handleAction(editor);\n });\n };\n};\n"],"names":["async","linkButtonText","unlinkButtonText","Promise","all","component","editor","ui","registry","addToggleButton","linkButtonShortName","icon","tooltip","onAction","onSetup","addMenuItem","shortcut","text","unlinkButtonShortName","shortcuts","add"],"mappings":"oPA4BwBA,gBAEhBC,eACAC,wBACMC,QAAQC,IAAI,EAClB,mBAAU,OAAQC,oBAClB,mBAAU,SAAUA,4BAGhBC,SAEJA,OAAOC,GAAGC,SAASC,gBAAgBC,4BAAqB,CACpDC,KAAM,OACNC,QAASX,eACTY,SAAU,0BACOP,SAEjBQ,SAAS,2BAAkBR,UAI/BA,OAAOC,GAAGC,SAASO,YAAYL,4BAAqB,CAChDC,KAAM,OACNK,SAAU,SACVC,KAAMhB,eACNY,SAAU,0BACOP,WAKrBA,OAAOC,GAAGC,SAASC,gBAAgBS,8BAAuB,CACtDP,KAAM,SACNC,QAASV,iBACTW,SAAU,0BACOP,QAAQ,IAEzBQ,SAAS,2BAAkBR,UAI/BA,OAAOa,UAAUC,IAAI,SAAU,4BAA4B,0BAC1Cd"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/common.min.js b/lib/editor/tiny/plugins/link/amd/build/common.min.js
new file mode 100644
index 0000000000000..68c888a302944
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/common.min.js
@@ -0,0 +1,3 @@
+define("tiny_link/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={pluginName:"tiny_link/plugin",component:"tiny_link",linkButtonName:"link",linkButtonShortName:"tiny_link_link",unlinkButtonName:"unlink",unlinkButtonShortName:"tiny_link_unlink"},_exports.default}));
+
+//# sourceMappingURL=common.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/common.min.js.map b/lib/editor/tiny/plugins/link/amd/build/common.min.js.map
new file mode 100644
index 0000000000000..8d18b959e8abd
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/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 Link common values.\n *\n * @module tiny_link/common\n * @copyright 2023 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n pluginName: 'tiny_link/plugin',\n component: 'tiny_link',\n linkButtonName: 'link',\n linkButtonShortName: 'tiny_link_link',\n unlinkButtonName: 'unlink',\n unlinkButtonShortName: 'tiny_link_unlink',\n};\n"],"names":["pluginName","component","linkButtonName","linkButtonShortName","unlinkButtonName","unlinkButtonShortName"],"mappings":"kKAuBe,CACXA,WAAY,mBACZC,UAAW,YACXC,eAAgB,OAChBC,oBAAqB,iBACrBC,iBAAkB,SAClBC,sBAAuB"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/configuration.min.js b/lib/editor/tiny/plugins/link/amd/build/configuration.min.js
new file mode 100644
index 0000000000000..4522dffcb3311
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/configuration.min.js
@@ -0,0 +1,3 @@
+define("tiny_link/configuration",["exports","tiny_link/common","editor_tiny/utils"],(function(_exports,_common,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>{return{menu:(menu=instanceConfig.menu,menu.insert.items.match(/\blink\b/)?menu.insert.items=menu.insert.items.replace(/\blink\b/,_common.linkButtonShortName):menu.insert.items="".concat(_common.linkButtonShortName," ").concat(menu.insert.items),menu),toolbar:(0,_utils.addToolbarButtons)(instanceConfig.toolbar,"content",[_common.linkButtonShortName,_common.unlinkButtonShortName])};var menu}}));
+
+//# sourceMappingURL=configuration.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/configuration.min.js.map b/lib/editor/tiny/plugins/link/amd/build/configuration.min.js.map
new file mode 100644
index 0000000000000..fff59358d9ebc
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/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 Link configuration.\n *\n * @module tiny_link/configuration\n * @copyright 2023 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {linkButtonShortName, unlinkButtonShortName} from 'tiny_link/common';\nimport {addToolbarButtons} from 'editor_tiny/utils';\n\nconst configureMenu = (menu) => {\n // Replace the standard Link plugin with the Moodle link.\n if (menu.insert.items.match(/\\blink\\b/)) {\n menu.insert.items = menu.insert.items.replace(/\\blink\\b/, linkButtonShortName);\n } else {\n menu.insert.items = `${linkButtonShortName} ${menu.insert.items}`;\n }\n\n return menu;\n};\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the Link option to the menus and toolbars.\n return {\n menu: configureMenu(instanceConfig.menu),\n toolbar: addToolbarButtons(instanceConfig.toolbar, 'content', [linkButtonShortName, unlinkButtonShortName]),\n };\n};\n"],"names":["instanceConfig","menu","insert","items","match","replace","linkButtonShortName","toolbar","unlinkButtonShortName"],"mappings":"4NAqC0BA,uBAEf,CACHC,MAdeA,KAcKD,eAAeC,KAZnCA,KAAKC,OAAOC,MAAMC,MAAM,YACxBH,KAAKC,OAAOC,MAAQF,KAAKC,OAAOC,MAAME,QAAQ,WAAYC,6BAE1DL,KAAKC,OAAOC,gBAAWG,wCAAuBL,KAAKC,OAAOC,OAGvDF,MAOHM,SAAS,4BAAkBP,eAAeO,QAAS,UAAW,CAACD,4BAAqBE,iCAfrEP,IAAAA"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/link.min.js b/lib/editor/tiny/plugins/link/amd/build/link.min.js
new file mode 100644
index 0000000000000..e364925466667
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/link.min.js
@@ -0,0 +1,10 @@
+define("tiny_link/link",["exports","core/templates","core/pending","tiny_link/selectors"],(function(_exports,_templates,_pending,_selectors){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+/**
+ * Link helper for Tiny Link plugin.
+ *
+ * @module tiny_link/link
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.unSetLink=_exports.toggleActiveState=_exports.setLink=_exports.getCurrentLinkData=void 0,_templates=_interopRequireDefault(_templates),_pending=_interopRequireDefault(_pending),_selectors=_interopRequireDefault(_selectors);_exports.setLink=(currentForm,editor)=>{let value=currentForm.querySelector(_selectors.default.elements.urlEntry).value;if(""!==value){const pendingPromise=new _pending.default("tiny_link/setLink");value=value.trim();new RegExp(/^[a-zA-Z]*\.*\/|^#|^[a-zA-Z]*:/).test(value)||(value="http://"+value),setLinkOnSelection(currentForm,editor,value).then(pendingPromise.resolve)}};_exports.unSetLink=editor=>{if(editor.hasPlugin("rtc",!0))editor.execCommand("unlink");else{const dom=editor.dom,selection=editor.selection,bookmark=selection.getBookmark(),rng=selection.getRng().cloneRange(),startAnchorElm=dom.getParent(rng.startContainer,"a[href]",editor.getBody()),endAnchorElm=dom.getParent(rng.endContainer,"a[href]",editor.getBody());startAnchorElm&&rng.setStartBefore(startAnchorElm),endAnchorElm&&rng.setEndAfter(endAnchorElm),selection.setRng(rng),editor.execCommand("unlink"),selection.moveToBookmark(bookmark)}};const setLinkOnSelection=async(currentForm,editor,url)=>{const urlText=currentForm.querySelector(_selectors.default.elements.urlText),target=currentForm.querySelector(_selectors.default.elements.openInNewWindow);let textToDisplay=urlText.value.replace(/(<([^>]+)>)/gi,"").trim();""===textToDisplay&&(textToDisplay=url);const context={url:url,newwindow:target.checked};urlText.getAttribute("data-link-on-element")?(context.title=textToDisplay,context.name=editor.selection.getNode().outerHTML):context.name=textToDisplay;const{html:html}=await _templates.default.renderForPromise("tiny_link/embed_link",context),currentLink=getSelectedLink(editor);currentLink?currentLink.outerHTML=html:editor.insertContent(html)};_exports.getCurrentLinkData=editor=>{let properties={};const link=getSelectedLink(editor);if(link){const url=link.getAttribute("href"),target=link.getAttribute("target"),textToDisplay=link.innerText,title=link.getAttribute("title");""!==url&&(properties.url=url),"_blank"===target&&(properties.newwindow=!0),title&&""!==title?properties.urltext=title.trim():""!==textToDisplay&&(properties.urltext=textToDisplay.trim())}else{const selectedNode=editor.selection.getNode();if(selectedNode){const textToDisplay=selectedNode.textContent;""!==textToDisplay?(properties.urltext=textToDisplay.trim(),properties.hasTextToDisplay=!0,properties.hasPlainTextSelected=!0):selectedNode.getAttribute("data-mce-selected")&&(properties.setLinkOnElement=!0)}}return properties};const getSelectedLink=editor=>getAnchorElement(editor),getAnchorElement=(editor,selectedElm)=>(selectedElm=selectedElm||editor.selection.getNode(),editor.dom.getParent(selectedElm,"a[href]"));_exports.toggleActiveState=editor=>api=>{const updateState=()=>api.setActive(!editor.mode.isReadOnly()&&((editor,selectedElm)=>null!==getAnchorElement(editor,selectedElm))(editor,editor.selection.getNode()));return updateState(),((editor,toggler)=>(editor.on("NodeChange",toggler),()=>editor.off("NodeChange",toggler)))(editor,updateState)}}));
+
+//# sourceMappingURL=link.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/link.min.js.map b/lib/editor/tiny/plugins/link/amd/build/link.min.js.map
new file mode 100644
index 0000000000000..7a0fc238b4a5f
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/link.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"link.min.js","sources":["../src/link.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 * Link helper for Tiny Link plugin.\n *\n * @module tiny_link/link\n * @copyright 2023 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Pending from 'core/pending';\nimport Selectors from 'tiny_link/selectors';\n\n/**\n * Handle insertion of a new link, or update of an existing one.\n *\n * @param {Element} currentForm\n * @param {TinyMCE} editor\n */\nexport const setLink = (currentForm, editor) => {\n const input = currentForm.querySelector(Selectors.elements.urlEntry);\n let value = input.value;\n\n if (value !== '') {\n const pendingPromise = new Pending('tiny_link/setLink');\n // We add a prefix if it is not already prefixed.\n value = value.trim();\n const expr = new RegExp(/^[a-zA-Z]*\\.*\\/|^#|^[a-zA-Z]*:/);\n if (!expr.test(value)) {\n value = 'http://' + value;\n }\n\n // Add the link.\n setLinkOnSelection(currentForm, editor, value).then(pendingPromise.resolve);\n }\n};\n\n/**\n * Handle unlink of a link\n *\n * @param {TinyMCE} editor\n */\nexport const unSetLink = (editor) => {\n if (editor.hasPlugin('rtc', true)) {\n editor.execCommand('unlink');\n } else {\n const dom = editor.dom;\n const selection = editor.selection;\n const bookmark = selection.getBookmark();\n const rng = selection.getRng().cloneRange();\n const startAnchorElm = dom.getParent(rng.startContainer, 'a[href]', editor.getBody());\n const endAnchorElm = dom.getParent(rng.endContainer, 'a[href]', editor.getBody());\n if (startAnchorElm) {\n rng.setStartBefore(startAnchorElm);\n }\n if (endAnchorElm) {\n rng.setEndAfter(endAnchorElm);\n }\n selection.setRng(rng);\n editor.execCommand('unlink');\n selection.moveToBookmark(bookmark);\n }\n};\n\n/**\n * Final step setting the anchor on the selection.\n *\n * @param {Element} currentForm\n * @param {TinyMCE} editor\n * @param {String} url URL the link will point to.\n */\nconst setLinkOnSelection = async(currentForm, editor, url) => {\n const urlText = currentForm.querySelector(Selectors.elements.urlText);\n const target = currentForm.querySelector(Selectors.elements.openInNewWindow);\n let textToDisplay = urlText.value.replace(/(<([^>]+)>)/gi, \"\").trim();\n\n if (textToDisplay === '') {\n textToDisplay = url;\n }\n\n const context = {\n url: url,\n newwindow: target.checked,\n };\n if (urlText.getAttribute('data-link-on-element')) {\n context.title = textToDisplay;\n context.name = editor.selection.getNode().outerHTML;\n } else {\n context.name = textToDisplay;\n }\n const {html} = await Templates.renderForPromise('tiny_link/embed_link', context);\n const currentLink = getSelectedLink(editor);\n if (currentLink) {\n currentLink.outerHTML = html;\n } else {\n editor.insertContent(html);\n }\n};\n\n/**\n * Get current link data.\n *\n * @param {TinyMCE} editor\n * @returns {{}}\n */\nexport const getCurrentLinkData = (editor) => {\n let properties = {};\n const link = getSelectedLink(editor);\n if (link) {\n const url = link.getAttribute('href');\n const target = link.getAttribute('target');\n const textToDisplay = link.innerText;\n const title = link.getAttribute('title');\n\n if (url !== '') {\n properties.url = url;\n }\n if (target === '_blank') {\n properties.newwindow = true;\n }\n if (title && title !== '') {\n properties.urltext = title.trim();\n } else if (textToDisplay !== '') {\n properties.urltext = textToDisplay.trim();\n }\n } else {\n // Check if the user is selecting some text before clicking on the Link button.\n const selectedNode = editor.selection.getNode();\n if (selectedNode) {\n const textToDisplay = selectedNode.textContent;\n if (textToDisplay !== '') {\n properties.urltext = textToDisplay.trim();\n properties.hasTextToDisplay = true;\n properties.hasPlainTextSelected = true;\n } else {\n if (selectedNode.getAttribute('data-mce-selected')) {\n properties.setLinkOnElement = true;\n }\n }\n }\n }\n\n return properties;\n};\n\n/**\n * Get selected link.\n *\n * @param {TinyMCE} editor\n * @returns {Element}\n */\nconst getSelectedLink = (editor) => {\n return getAnchorElement(editor);\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, 'a[href]');\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};\n"],"names":["currentForm","editor","value","querySelector","Selectors","elements","urlEntry","pendingPromise","Pending","trim","RegExp","test","setLinkOnSelection","then","resolve","hasPlugin","execCommand","dom","selection","bookmark","getBookmark","rng","getRng","cloneRange","startAnchorElm","getParent","startContainer","getBody","endAnchorElm","endContainer","setStartBefore","setEndAfter","setRng","moveToBookmark","async","url","urlText","target","openInNewWindow","textToDisplay","replace","context","newwindow","checked","getAttribute","title","name","getNode","outerHTML","html","Templates","renderForPromise","currentLink","getSelectedLink","insertContent","properties","link","innerText","urltext","selectedNode","textContent","hasTextToDisplay","hasPlainTextSelected","setLinkOnElement","getAnchorElement","selectedElm","api","updateState","setActive","mode","isReadOnly","isInAnchor","toggler","on","off","toggleState"],"mappings":";;;;;;;sTAiCuB,CAACA,YAAaC,cAE7BC,MADUF,YAAYG,cAAcC,mBAAUC,SAASC,UACzCJ,SAEJ,KAAVA,MAAc,OACRK,eAAiB,IAAIC,iBAAQ,qBAEnCN,MAAQA,MAAMO,OACD,IAAIC,OAAO,kCACdC,KAAKT,SACXA,MAAQ,UAAYA,OAIxBU,mBAAmBZ,YAAaC,OAAQC,OAAOW,KAAKN,eAAeO,8BASjDb,YAClBA,OAAOc,UAAU,OAAO,GACxBd,OAAOe,YAAY,cAChB,OACGC,IAAMhB,OAAOgB,IACbC,UAAYjB,OAAOiB,UACnBC,SAAWD,UAAUE,cACrBC,IAAMH,UAAUI,SAASC,aACzBC,eAAiBP,IAAIQ,UAAUJ,IAAIK,eAAgB,UAAWzB,OAAO0B,WACrEC,aAAeX,IAAIQ,UAAUJ,IAAIQ,aAAc,UAAW5B,OAAO0B,WACnEH,gBACAH,IAAIS,eAAeN,gBAEnBI,cACAP,IAAIU,YAAYH,cAEpBV,UAAUc,OAAOX,KACjBpB,OAAOe,YAAY,UACnBE,UAAUe,eAAed,kBAW3BP,mBAAqBsB,MAAMlC,YAAaC,OAAQkC,aAC5CC,QAAUpC,YAAYG,cAAcC,mBAAUC,SAAS+B,SACvDC,OAASrC,YAAYG,cAAcC,mBAAUC,SAASiC,qBACxDC,cAAgBH,QAAQlC,MAAMsC,QAAQ,gBAAiB,IAAI/B,OAEzC,KAAlB8B,gBACAA,cAAgBJ,WAGdM,QAAU,CACZN,IAAKA,IACLO,UAAWL,OAAOM,SAElBP,QAAQQ,aAAa,yBACrBH,QAAQI,MAAQN,cAChBE,QAAQK,KAAO7C,OAAOiB,UAAU6B,UAAUC,WAE1CP,QAAQK,KAAOP,oBAEbU,KAACA,YAAcC,mBAAUC,iBAAiB,uBAAwBV,SAClEW,YAAcC,gBAAgBpD,QAChCmD,YACAA,YAAYJ,UAAYC,KAExBhD,OAAOqD,cAAcL,mCAUMhD,aAC3BsD,WAAa,SACXC,KAAOH,gBAAgBpD,WACzBuD,KAAM,OACArB,IAAMqB,KAAKZ,aAAa,QACxBP,OAASmB,KAAKZ,aAAa,UAC3BL,cAAgBiB,KAAKC,UACrBZ,MAAQW,KAAKZ,aAAa,SAEpB,KAART,MACAoB,WAAWpB,IAAMA,KAEN,WAAXE,SACAkB,WAAWb,WAAY,GAEvBG,OAAmB,KAAVA,MACTU,WAAWG,QAAUb,MAAMpC,OACF,KAAlB8B,gBACPgB,WAAWG,QAAUnB,cAAc9B,YAEpC,OAEGkD,aAAe1D,OAAOiB,UAAU6B,aAClCY,aAAc,OACRpB,cAAgBoB,aAAaC,YACb,KAAlBrB,eACAgB,WAAWG,QAAUnB,cAAc9B,OACnC8C,WAAWM,kBAAmB,EAC9BN,WAAWO,sBAAuB,GAE9BH,aAAaf,aAAa,uBAC1BW,WAAWQ,kBAAmB,WAMvCR,kBASLF,gBAAmBpD,QACd+D,iBAAiB/D,QAUtB+D,iBAAmB,CAAC/D,OAAQgE,eAC9BA,YAAcA,aAAehE,OAAOiB,UAAU6B,UACvC9C,OAAOgB,IAAIQ,UAAUwC,YAAa,uCA8BXhE,QAAYiE,YACpCC,YAAc,IAAMD,IAAIE,WAAWnE,OAAOoE,KAAKC,cArBtC,EAACrE,OAAQgE,cAA0D,OAA1CD,iBAAiB/D,OAAQgE,aAqBIM,CAAWtE,OAAQA,OAAOiB,UAAU6B,mBACzGoB,cAbgB,EAAClE,OAAQuE,WACzBvE,OAAOwE,GAAG,aAAcD,SACjB,IAAMvE,OAAOyE,IAAI,aAAcF,UAY/BG,CAAY1E,OAAQkE"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/modal.min.js b/lib/editor/tiny/plugins/link/amd/build/modal.min.js
new file mode 100644
index 0000000000000..0ad22fde30bd9
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/modal.min.js
@@ -0,0 +1,3 @@
+define("tiny_link/modal",["exports","core/modal","core/modal_registry"],(function(_exports,_modal,_modal_registry){var _class;function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=_interopRequireDefault(_modal),_modal_registry=_interopRequireDefault(_modal_registry);const LinkModal=(_defineProperty(_class=class extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}},"TYPE","tiny_link/modal"),_defineProperty(_class,"TEMPLATE","tiny_link/modal"),_class);_modal_registry.default.register(LinkModal.TYPE,LinkModal,LinkModal.TEMPLATE);var _default=LinkModal;return _exports.default=_default,_exports.default}));
+
+//# sourceMappingURL=modal.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/modal.min.js.map b/lib/editor/tiny/plugins/link/amd/build/modal.min.js.map
new file mode 100644
index 0000000000000..61bea15a29104
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/modal.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"modal.min.js","sources":["../src/modal.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 * Link Modal for Tiny.\n *\n * @module tiny_link/modal\n * @copyright 2023 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport ModalRegistry from 'core/modal_registry';\n\nconst LinkModal = class extends Modal {\n static TYPE = 'tiny_link/modal';\n static TEMPLATE = 'tiny_link/modal';\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n};\n\nModalRegistry.register(LinkModal.TYPE, LinkModal, LinkModal.TEMPLATE);\n\nexport default LinkModal;\n"],"names":["LinkModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","register","TYPE","TEMPLATE"],"mappings":"wiBA0BMA,kCAAY,cAAcC,eAI5BC,+BAEUA,8BAGDC,2BACAC,iCATK,qDACI,mDAYRC,SAASL,UAAUM,KAAMN,UAAWA,UAAUO,uBAE7CP"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/options.min.js b/lib/editor/tiny/plugins/link/amd/build/options.min.js
new file mode 100644
index 0000000000000..40fac09438825
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/options.min.js
@@ -0,0 +1,11 @@
+define("tiny_link/options",["exports","editor_tiny/options","tiny_link/common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.getPermissions=void 0;
+/**
+ * Options helper for Tiny Link plugin.
+ *
+ * @module tiny_link/options
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+const dataName=(0,_options.getPluginOptionName)(_common.pluginName,"data"),permissionsName=(0,_options.getPluginOptionName)(_common.pluginName,"permissions");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(permissionsName,{processor:"object",default:{filepicker:!1}}),registerOption(dataName,{processor:"object"})};_exports.getPermissions=editor=>editor.options.get(permissionsName)}));
+
+//# sourceMappingURL=options.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/options.min.js.map b/lib/editor/tiny/plugins/link/amd/build/options.min.js.map
new file mode 100644
index 0000000000000..63eb92f51a830
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/options.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"options.min.js","sources":["../src/options.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 * Options helper for Tiny Link plugin.\n *\n * @module tiny_link/options\n * @copyright 2023 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from 'tiny_link/common';\n\nconst dataName = getPluginOptionName(pluginName, 'data');\nconst permissionsName = getPluginOptionName(pluginName, 'permissions');\n\n/**\n * Register the options for the Tiny Link plugin.\n *\n * @param {TinyMCE} editor\n */\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n\n registerOption(permissionsName, {\n processor: 'object',\n \"default\": {\n filepicker: false,\n },\n });\n\n registerOption(dataName, {\n processor: 'object',\n });\n};\n\n/**\n * Get the permissions configuration for the Tiny Link plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getPermissions = (editor) => editor.options.get(permissionsName);\n"],"names":["dataName","pluginName","permissionsName","editor","registerOption","options","register","processor","filepicker","get"],"mappings":";;;;;;;;MA0BMA,UAAW,gCAAoBC,mBAAY,QAC3CC,iBAAkB,gCAAoBD,mBAAY,iCAO/BE,eACfC,eAAiBD,OAAOE,QAAQC,SAEtCF,eAAeF,gBAAiB,CAC5BK,UAAW,iBACA,CACPC,YAAY,KAIpBJ,eAAeJ,SAAU,CACrBO,UAAW,oCAUYJ,QAAWA,OAAOE,QAAQI,IAAIP"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/plugin.min.js b/lib/editor/tiny/plugins/link/amd/build/plugin.min.js
new file mode 100644
index 0000000000000..d5b90b81dbf95
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/plugin.min.js
@@ -0,0 +1,10 @@
+define("tiny_link/plugin",["exports","editor_tiny/loader","editor_tiny/utils","tiny_link/common","tiny_link/commands","tiny_link/configuration","tiny_link/options"],(function(_exports,_loader,_utils,_common,Commands,Configuration,Options){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 Link plugin for Moodle.
+ *
+ * @module tiny_link/plugin
+ * @copyright 2023 Huong Nguyen
+ * @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),Options=_interopRequireWildcard(Options);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=>(Options.register(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/link/amd/build/plugin.min.js.map b/lib/editor/tiny/plugins/link/amd/build/plugin.min.js.map
new file mode 100644
index 0000000000000..980545c2cceb6
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/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\nimport {getTinyMCE} from 'editor_tiny/loader';\nimport {getPluginMetadata} from 'editor_tiny/utils';\n\nimport {component, pluginName} from 'tiny_link/common';\nimport * as Commands from 'tiny_link/commands';\nimport * as Configuration from 'tiny_link/configuration';\nimport * as Options from 'tiny_link/options';\n\n/**\n * Tiny Link plugin for Moodle.\n *\n * @module tiny_link/plugin\n * @copyright 2023 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\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 // Register options.\n Options.register(editor);\n // Setup the Commands (buttons, menu items, and so on).\n setupCommands(editor);\n\n return pluginMetadata;\n });\n\n // Resolve the Link 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","Options","register","resolve","Configuration"],"mappings":";;;;;;;2OA+Be,IAAIA,SAAQC,MAAAA,gBAEnBC,QACAC,cACAC,sBACMJ,QAAQK,IAAI,EAClB,wBACAC,SAASC,YACT,4BAAkBC,kBAAWC,sBAGjCP,QAAQQ,cAAcC,cAAOH,8BAAqBI,SAE9CC,QAAQC,SAASF,QAEjBT,cAAcS,QAEPR,kBAIXW,QAAQ,WAAIP,6BAAoBQ"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/selectors.min.js b/lib/editor/tiny/plugins/link/amd/build/selectors.min.js
new file mode 100644
index 0000000000000..345a71a700960
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/selectors.min.js
@@ -0,0 +1,3 @@
+define("tiny_link/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={actions:{submit:'[data-action="save"]',linkBrowser:".openlinkbrowser"},elements:{urlEntry:".tiny_link_urlentry",urlText:".tiny_link_urltext",openInNewWindow:".tiny_link_newwindow"}},_exports.default}));
+
+//# sourceMappingURL=selectors.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/selectors.min.js.map b/lib/editor/tiny/plugins/link/amd/build/selectors.min.js.map
new file mode 100644
index 0000000000000..7459f6568b99f
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/selectors.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"selectors.min.js","sources":["../src/selectors.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 Link plugin helper function to build queryable data selectors.\n *\n * @module tiny_link/selectors\n * @copyright 2023 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n actions: {\n submit: '[data-action=\"save\"]',\n linkBrowser: '.openlinkbrowser',\n },\n elements: {\n urlEntry: '.tiny_link_urlentry',\n urlText: '.tiny_link_urltext',\n openInNewWindow: '.tiny_link_newwindow',\n }\n};\n"],"names":["actions","submit","linkBrowser","elements","urlEntry","urlText","openInNewWindow"],"mappings":"qKAuBe,CACXA,QAAS,CACLC,OAAQ,uBACRC,YAAa,oBAEjBC,SAAU,CACNC,SAAU,sBACVC,QAAS,qBACTC,gBAAiB"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/ui.min.js b/lib/editor/tiny/plugins/link/amd/build/ui.min.js
new file mode 100644
index 0000000000000..7ba19ad979941
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/ui.min.js
@@ -0,0 +1,10 @@
+define("tiny_link/ui",["exports","core/modal_factory","core/modal_events","editor_tiny/utils","tiny_link/modal","tiny_link/options","tiny_link/link","tiny_link/selectors"],(function(_exports,_modal_factory,_modal_events,_utils,_modal,_options,_link,_selectors){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+/**
+ * Tiny Link UI.
+ *
+ * @module tiny_link/ui
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.handleAction=void 0,_modal_factory=_interopRequireDefault(_modal_factory),_modal_events=_interopRequireDefault(_modal_events),_modal=_interopRequireDefault(_modal),_selectors=_interopRequireDefault(_selectors);_exports.handleAction=function(editor){let unlink=arguments.length>1&&void 0!==arguments[1]&&arguments[1];unlink?(0,_link.unSetLink)(editor):displayDialogue(editor)};const displayDialogue=async editor=>{const modalPromises=await _modal_factory.default.create({type:_modal.default.TYPE,templateContext:getTemplateContext(editor),large:!1});modalPromises.show();const $root=await modalPromises.getRoot(),root=$root[0],currentForm=root.querySelector("form");$root.on(_modal_events.default.hidden,(()=>{modalPromises.destroy()})),root.addEventListener("click",(e=>{const submitAction=e.target.closest(_selectors.default.actions.submit),linkBrowserAction=e.target.closest(_selectors.default.actions.linkBrowser);submitAction&&(e.preventDefault(),(0,_link.setLink)(currentForm,editor),modalPromises.destroy()),linkBrowserAction&&(e.preventDefault(),(0,_utils.displayFilepicker)(editor,"link").then((params=>(filePickerCallback(params,currentForm,editor),modalPromises.destroy()))).catch())}));const linkTitle=root.querySelector(_selectors.default.elements.urlText),linkUrl=root.querySelector(_selectors.default.elements.urlEntry);linkTitle.addEventListener("change",(()=>{linkTitle.value.length>0?linkTitle.dataset.useLinkAsText="false":(linkTitle.dataset.useLinkAsText="true",linkTitle.value=linkUrl.value)})),linkUrl.addEventListener("keyup",(()=>{updateTextToDisplay(currentForm)}))},getTemplateContext=editor=>{const data=(0,_link.getCurrentLinkData)(editor);return Object.assign({},{elementid:editor.id,showfilepicker:(0,_options.getPermissions)(editor).filepicker,isupdating:Object.keys(data).length>0},data)},filePickerCallback=(params,currentForm,editor)=>{if(params.url){currentForm.querySelector(_selectors.default.elements.urlEntry).value=params.url,(0,_link.setLink)(currentForm,editor)}},updateTextToDisplay=currentForm=>{const urlEntry=currentForm.querySelector(_selectors.default.elements.urlEntry),urlText=currentForm.querySelector(_selectors.default.elements.urlText);"true"===urlText.dataset.useLinkAsText&&(urlText.value=urlEntry.value)}}));
+
+//# sourceMappingURL=ui.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/build/ui.min.js.map b/lib/editor/tiny/plugins/link/amd/build/ui.min.js.map
new file mode 100644
index 0000000000000..2bcf2018505b1
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/build/ui.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ui.min.js","sources":["../src/ui.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 Link UI.\n *\n * @module tiny_link/ui\n * @copyright 2023 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\nimport {displayFilepicker} from 'editor_tiny/utils';\nimport LinkModal from 'tiny_link/modal';\nimport {getPermissions} from \"tiny_link/options\";\nimport {setLink, getCurrentLinkData, unSetLink} from \"tiny_link/link\";\nimport Selectors from 'tiny_link/selectors';\n\n/**\n * Handle action.\n *\n * @param {TinyMCE} editor\n * @param {boolean} unlink\n */\nexport const handleAction = (editor, unlink = false) => {\n if (!unlink) {\n displayDialogue(editor);\n } else {\n unSetLink(editor);\n }\n};\n\n/**\n * Display the link dialogue.\n *\n * @param {TinyMCE} editor\n * @returns {Promise}\n */\nconst displayDialogue = async(editor) => {\n const modalPromises = await ModalFactory.create({\n type: LinkModal.TYPE,\n templateContext: getTemplateContext(editor),\n large: false,\n });\n\n modalPromises.show();\n const $root = await modalPromises.getRoot();\n const root = $root[0];\n const currentForm = root.querySelector('form');\n\n $root.on(ModalEvents.hidden, () => {\n modalPromises.destroy();\n });\n\n root.addEventListener('click', (e) => {\n const submitAction = e.target.closest(Selectors.actions.submit);\n const linkBrowserAction = e.target.closest(Selectors.actions.linkBrowser);\n if (submitAction) {\n e.preventDefault();\n setLink(currentForm, editor);\n modalPromises.destroy();\n }\n if (linkBrowserAction) {\n e.preventDefault();\n displayFilepicker(editor, 'link').then((params) => {\n filePickerCallback(params, currentForm, editor);\n return modalPromises.destroy();\n }).catch();\n }\n });\n\n const linkTitle = root.querySelector(Selectors.elements.urlText);\n const linkUrl = root.querySelector(Selectors.elements.urlEntry);\n linkTitle.addEventListener('change', () => {\n if (linkTitle.value.length > 0) {\n linkTitle.dataset.useLinkAsText = 'false';\n } else {\n linkTitle.dataset.useLinkAsText = 'true';\n linkTitle.value = linkUrl.value;\n }\n });\n\n linkUrl.addEventListener('keyup', () => {\n updateTextToDisplay(currentForm);\n });\n};\n\n/**\n * Get template context.\n *\n * @param {TinyMCE} editor\n * @returns {Object}\n */\nconst getTemplateContext = (editor) => {\n const data = getCurrentLinkData(editor);\n\n return Object.assign({}, {\n elementid: editor.id,\n showfilepicker: getPermissions(editor).filepicker,\n isupdating: Object.keys(data).length > 0,\n }, data);\n};\n\n/**\n * Update the dialogue after a link was selected in the File Picker.\n *\n * @param {Object} params\n * @param {Element} currentForm\n * @param {TinyMCE} editor\n */\nconst filePickerCallback = (params, currentForm, editor) => {\n if (params.url) {\n const inputUrl = currentForm.querySelector(Selectors.elements.urlEntry);\n inputUrl.value = params.url;\n setLink(currentForm, editor);\n }\n};\n\n/**\n * Update the text to display if the user does not provide the custom text.\n *\n * @param {Element} currentForm\n */\nconst updateTextToDisplay = (currentForm) => {\n const urlEntry = currentForm.querySelector(Selectors.elements.urlEntry);\n const urlText = currentForm.querySelector(Selectors.elements.urlText);\n if (urlText.dataset.useLinkAsText === 'true') {\n urlText.value = urlEntry.value;\n }\n};\n"],"names":["editor","unlink","displayDialogue","async","modalPromises","ModalFactory","create","type","LinkModal","TYPE","templateContext","getTemplateContext","large","show","$root","getRoot","root","currentForm","querySelector","on","ModalEvents","hidden","destroy","addEventListener","e","submitAction","target","closest","Selectors","actions","submit","linkBrowserAction","linkBrowser","preventDefault","then","params","filePickerCallback","catch","linkTitle","elements","urlText","linkUrl","urlEntry","value","length","dataset","useLinkAsText","updateTextToDisplay","data","Object","assign","elementid","id","showfilepicker","filepicker","isupdating","keys","url"],"mappings":";;;;;;;8SAqC4B,SAACA,YAAQC,+DAC5BA,2BAGSD,QAFVE,gBAAgBF,eAYlBE,gBAAkBC,MAAAA,eACdC,oBAAsBC,uBAAaC,OAAO,CAC5CC,KAAMC,eAAUC,KAChBC,gBAAiBC,mBAAmBX,QACpCY,OAAO,IAGXR,cAAcS,aACRC,YAAcV,cAAcW,UAC5BC,KAAOF,MAAM,GACbG,YAAcD,KAAKE,cAAc,QAEvCJ,MAAMK,GAAGC,sBAAYC,QAAQ,KACzBjB,cAAckB,aAGlBN,KAAKO,iBAAiB,SAAUC,UACtBC,aAAeD,EAAEE,OAAOC,QAAQC,mBAAUC,QAAQC,QAClDC,kBAAoBP,EAAEE,OAAOC,QAAQC,mBAAUC,QAAQG,aACzDP,eACAD,EAAES,mCACMhB,YAAajB,QACrBI,cAAckB,WAEdS,oBACAP,EAAES,8CACgBjC,OAAQ,QAAQkC,MAAMC,SACpCC,mBAAmBD,OAAQlB,YAAajB,QACjCI,cAAckB,aACtBe,kBAILC,UAAYtB,KAAKE,cAAcU,mBAAUW,SAASC,SAClDC,QAAUzB,KAAKE,cAAcU,mBAAUW,SAASG,UACtDJ,UAAUf,iBAAiB,UAAU,KAC7Be,UAAUK,MAAMC,OAAS,EACzBN,UAAUO,QAAQC,cAAgB,SAElCR,UAAUO,QAAQC,cAAgB,OAClCR,UAAUK,MAAQF,QAAQE,UAIlCF,QAAQlB,iBAAiB,SAAS,KAC9BwB,oBAAoB9B,iBAUtBN,mBAAsBX,eAClBgD,MAAO,4BAAmBhD,eAEzBiD,OAAOC,OAAO,GAAI,CACrBC,UAAWnD,OAAOoD,GAClBC,gBAAgB,2BAAerD,QAAQsD,WACvCC,WAAYN,OAAOO,KAAKR,MAAMJ,OAAS,GACxCI,OAUDZ,mBAAqB,CAACD,OAAQlB,YAAajB,aACzCmC,OAAOsB,IAAK,CACKxC,YAAYC,cAAcU,mBAAUW,SAASG,UACrDC,MAAQR,OAAOsB,sBAChBxC,YAAajB,UASvB+C,oBAAuB9B,oBACnByB,SAAWzB,YAAYC,cAAcU,mBAAUW,SAASG,UACxDF,QAAUvB,YAAYC,cAAcU,mBAAUW,SAASC,SACvB,SAAlCA,QAAQK,QAAQC,gBAChBN,QAAQG,MAAQD,SAASC"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/link/amd/src/commands.js b/lib/editor/tiny/plugins/link/amd/src/commands.js
new file mode 100644
index 0000000000000..6f3bbe1ea96c1
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/src/commands.js
@@ -0,0 +1,74 @@
+// 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 {get_string as getString} from 'core/str';
+import {component, linkButtonShortName, unlinkButtonShortName} from 'tiny_link/common';
+import {handleAction} from 'tiny_link/ui';
+import {toggleActiveState} from 'tiny_link/link';
+
+/**
+ * Tiny Link commands.
+ *
+ * @module tiny_link/commands
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+export const getSetup = async() => {
+ const [
+ linkButtonText,
+ unlinkButtonText,
+ ] = await Promise.all([
+ getString('link', component),
+ getString('unlink', component),
+ ]);
+
+ return (editor) => {
+ // Register Link button.
+ editor.ui.registry.addToggleButton(linkButtonShortName, {
+ icon: 'link',
+ tooltip: linkButtonText,
+ onAction: () => {
+ handleAction(editor);
+ },
+ onSetup: toggleActiveState(editor),
+ });
+
+ // Register the Link menu item.
+ editor.ui.registry.addMenuItem(linkButtonShortName, {
+ icon: 'link',
+ shortcut: 'Meta+K',
+ text: linkButtonText,
+ onAction: () => {
+ handleAction(editor);
+ },
+ });
+
+ // Register Unlink button.
+ editor.ui.registry.addToggleButton(unlinkButtonShortName, {
+ icon: 'unlink',
+ tooltip: unlinkButtonText,
+ onAction: () => {
+ handleAction(editor, true);
+ },
+ onSetup: toggleActiveState(editor),
+ });
+
+ // Register shortcut.
+ editor.shortcuts.add('Meta+K', 'Shortcut for create link', () => {
+ handleAction(editor);
+ });
+ };
+};
diff --git a/lib/editor/tiny/plugins/link/amd/src/common.js b/lib/editor/tiny/plugins/link/amd/src/common.js
new file mode 100644
index 0000000000000..eeadbba032574
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/src/common.js
@@ -0,0 +1,31 @@
+// 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 Link common values.
+ *
+ * @module tiny_link/common
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+export default {
+ pluginName: 'tiny_link/plugin',
+ component: 'tiny_link',
+ linkButtonName: 'link',
+ linkButtonShortName: 'tiny_link_link',
+ unlinkButtonName: 'unlink',
+ unlinkButtonShortName: 'tiny_link_unlink',
+};
diff --git a/lib/editor/tiny/plugins/link/amd/src/configuration.js b/lib/editor/tiny/plugins/link/amd/src/configuration.js
new file mode 100644
index 0000000000000..89abcd2dcb313
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/src/configuration.js
@@ -0,0 +1,44 @@
+// 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 Link configuration.
+ *
+ * @module tiny_link/configuration
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {linkButtonShortName, unlinkButtonShortName} from 'tiny_link/common';
+import {addToolbarButtons} from 'editor_tiny/utils';
+
+const configureMenu = (menu) => {
+ // Replace the standard Link plugin with the Moodle link.
+ if (menu.insert.items.match(/\blink\b/)) {
+ menu.insert.items = menu.insert.items.replace(/\blink\b/, linkButtonShortName);
+ } else {
+ menu.insert.items = `${linkButtonShortName} ${menu.insert.items}`;
+ }
+
+ return menu;
+};
+
+export const configure = (instanceConfig) => {
+ // Update the instance configuration to add the Link option to the menus and toolbars.
+ return {
+ menu: configureMenu(instanceConfig.menu),
+ toolbar: addToolbarButtons(instanceConfig.toolbar, 'content', [linkButtonShortName, unlinkButtonShortName]),
+ };
+};
diff --git a/lib/editor/tiny/plugins/link/amd/src/link.js b/lib/editor/tiny/plugins/link/amd/src/link.js
new file mode 100644
index 0000000000000..55a1150ddf69a
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/src/link.js
@@ -0,0 +1,213 @@
+// 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 .
+
+/**
+ * Link helper for Tiny Link plugin.
+ *
+ * @module tiny_link/link
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import Templates from 'core/templates';
+import Pending from 'core/pending';
+import Selectors from 'tiny_link/selectors';
+
+/**
+ * Handle insertion of a new link, or update of an existing one.
+ *
+ * @param {Element} currentForm
+ * @param {TinyMCE} editor
+ */
+export const setLink = (currentForm, editor) => {
+ const input = currentForm.querySelector(Selectors.elements.urlEntry);
+ let value = input.value;
+
+ if (value !== '') {
+ const pendingPromise = new Pending('tiny_link/setLink');
+ // We add a prefix if it is not already prefixed.
+ value = value.trim();
+ const expr = new RegExp(/^[a-zA-Z]*\.*\/|^#|^[a-zA-Z]*:/);
+ if (!expr.test(value)) {
+ value = 'http://' + value;
+ }
+
+ // Add the link.
+ setLinkOnSelection(currentForm, editor, value).then(pendingPromise.resolve);
+ }
+};
+
+/**
+ * Handle unlink of a link
+ *
+ * @param {TinyMCE} editor
+ */
+export const unSetLink = (editor) => {
+ if (editor.hasPlugin('rtc', true)) {
+ editor.execCommand('unlink');
+ } else {
+ const dom = editor.dom;
+ const selection = editor.selection;
+ const bookmark = selection.getBookmark();
+ const rng = selection.getRng().cloneRange();
+ const startAnchorElm = dom.getParent(rng.startContainer, 'a[href]', editor.getBody());
+ const endAnchorElm = dom.getParent(rng.endContainer, 'a[href]', editor.getBody());
+ if (startAnchorElm) {
+ rng.setStartBefore(startAnchorElm);
+ }
+ if (endAnchorElm) {
+ rng.setEndAfter(endAnchorElm);
+ }
+ selection.setRng(rng);
+ editor.execCommand('unlink');
+ selection.moveToBookmark(bookmark);
+ }
+};
+
+/**
+ * Final step setting the anchor on the selection.
+ *
+ * @param {Element} currentForm
+ * @param {TinyMCE} editor
+ * @param {String} url URL the link will point to.
+ */
+const setLinkOnSelection = async(currentForm, editor, url) => {
+ const urlText = currentForm.querySelector(Selectors.elements.urlText);
+ const target = currentForm.querySelector(Selectors.elements.openInNewWindow);
+ let textToDisplay = urlText.value.replace(/(<([^>]+)>)/gi, "").trim();
+
+ if (textToDisplay === '') {
+ textToDisplay = url;
+ }
+
+ const context = {
+ url: url,
+ newwindow: target.checked,
+ };
+ if (urlText.getAttribute('data-link-on-element')) {
+ context.title = textToDisplay;
+ context.name = editor.selection.getNode().outerHTML;
+ } else {
+ context.name = textToDisplay;
+ }
+ const {html} = await Templates.renderForPromise('tiny_link/embed_link', context);
+ const currentLink = getSelectedLink(editor);
+ if (currentLink) {
+ currentLink.outerHTML = html;
+ } else {
+ editor.insertContent(html);
+ }
+};
+
+/**
+ * Get current link data.
+ *
+ * @param {TinyMCE} editor
+ * @returns {{}}
+ */
+export const getCurrentLinkData = (editor) => {
+ let properties = {};
+ const link = getSelectedLink(editor);
+ if (link) {
+ const url = link.getAttribute('href');
+ const target = link.getAttribute('target');
+ const textToDisplay = link.innerText;
+ const title = link.getAttribute('title');
+
+ if (url !== '') {
+ properties.url = url;
+ }
+ if (target === '_blank') {
+ properties.newwindow = true;
+ }
+ if (title && title !== '') {
+ properties.urltext = title.trim();
+ } else if (textToDisplay !== '') {
+ properties.urltext = textToDisplay.trim();
+ }
+ } else {
+ // Check if the user is selecting some text before clicking on the Link button.
+ const selectedNode = editor.selection.getNode();
+ if (selectedNode) {
+ const textToDisplay = selectedNode.textContent;
+ if (textToDisplay !== '') {
+ properties.urltext = textToDisplay.trim();
+ properties.hasTextToDisplay = true;
+ properties.hasPlainTextSelected = true;
+ } else {
+ if (selectedNode.getAttribute('data-mce-selected')) {
+ properties.setLinkOnElement = true;
+ }
+ }
+ }
+ }
+
+ return properties;
+};
+
+/**
+ * Get selected link.
+ *
+ * @param {TinyMCE} editor
+ * @returns {Element}
+ */
+const getSelectedLink = (editor) => {
+ return getAnchorElement(editor);
+};
+
+/**
+ * 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, 'a[href]');
+};
+
+/**
+ * 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);
+};
diff --git a/lib/editor/tiny/plugins/link/amd/src/modal.js b/lib/editor/tiny/plugins/link/amd/src/modal.js
new file mode 100644
index 0000000000000..d09cbacce572e
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/src/modal.js
@@ -0,0 +1,43 @@
+// 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 .
+
+/**
+ * Link Modal for Tiny.
+ *
+ * @module tiny_link/modal
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import Modal from 'core/modal';
+import ModalRegistry from 'core/modal_registry';
+
+const LinkModal = class extends Modal {
+ static TYPE = 'tiny_link/modal';
+ static TEMPLATE = 'tiny_link/modal';
+
+ registerEventListeners() {
+ // Call the parent registration.
+ super.registerEventListeners();
+
+ // Register to close on save/cancel.
+ this.registerCloseOnSave();
+ this.registerCloseOnCancel();
+ }
+};
+
+ModalRegistry.register(LinkModal.TYPE, LinkModal, LinkModal.TEMPLATE);
+
+export default LinkModal;
diff --git a/lib/editor/tiny/plugins/link/amd/src/options.js b/lib/editor/tiny/plugins/link/amd/src/options.js
new file mode 100644
index 0000000000000..68e5d93eadff0
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/src/options.js
@@ -0,0 +1,56 @@
+// 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 .
+
+/**
+ * Options helper for Tiny Link plugin.
+ *
+ * @module tiny_link/options
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {getPluginOptionName} from 'editor_tiny/options';
+import {pluginName} from 'tiny_link/common';
+
+const dataName = getPluginOptionName(pluginName, 'data');
+const permissionsName = getPluginOptionName(pluginName, 'permissions');
+
+/**
+ * Register the options for the Tiny Link plugin.
+ *
+ * @param {TinyMCE} editor
+ */
+export const register = (editor) => {
+ const registerOption = editor.options.register;
+
+ registerOption(permissionsName, {
+ processor: 'object',
+ "default": {
+ filepicker: false,
+ },
+ });
+
+ registerOption(dataName, {
+ processor: 'object',
+ });
+};
+
+/**
+ * Get the permissions configuration for the Tiny Link plugin.
+ *
+ * @param {TinyMCE} editor
+ * @returns {object}
+ */
+export const getPermissions = (editor) => editor.options.get(permissionsName);
diff --git a/lib/editor/tiny/plugins/link/amd/src/plugin.js b/lib/editor/tiny/plugins/link/amd/src/plugin.js
new file mode 100644
index 0000000000000..abe4262e4056c
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/src/plugin.js
@@ -0,0 +1,54 @@
+// 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 {getTinyMCE} from 'editor_tiny/loader';
+import {getPluginMetadata} from 'editor_tiny/utils';
+
+import {component, pluginName} from 'tiny_link/common';
+import * as Commands from 'tiny_link/commands';
+import * as Configuration from 'tiny_link/configuration';
+import * as Options from 'tiny_link/options';
+
+/**
+ * Tiny Link plugin for Moodle.
+ *
+ * @module tiny_link/plugin
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+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) => {
+ // Register options.
+ Options.register(editor);
+ // Setup the Commands (buttons, menu items, and so on).
+ setupCommands(editor);
+
+ return pluginMetadata;
+ });
+
+ // Resolve the Link Plugin and include configuration.
+ resolve([`${component}/plugin`, Configuration]);
+});
diff --git a/lib/editor/tiny/plugins/link/amd/src/selectors.js b/lib/editor/tiny/plugins/link/amd/src/selectors.js
new file mode 100644
index 0000000000000..f7f0b7bbc666c
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/src/selectors.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 Link plugin helper function to build queryable data selectors.
+ *
+ * @module tiny_link/selectors
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+export default {
+ actions: {
+ submit: '[data-action="save"]',
+ linkBrowser: '.openlinkbrowser',
+ },
+ elements: {
+ urlEntry: '.tiny_link_urlentry',
+ urlText: '.tiny_link_urltext',
+ openInNewWindow: '.tiny_link_newwindow',
+ }
+};
diff --git a/lib/editor/tiny/plugins/link/amd/src/ui.js b/lib/editor/tiny/plugins/link/amd/src/ui.js
new file mode 100644
index 0000000000000..b7db7c40df0b9
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/amd/src/ui.js
@@ -0,0 +1,143 @@
+// 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 Link UI.
+ *
+ * @module tiny_link/ui
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import ModalFactory from 'core/modal_factory';
+import ModalEvents from 'core/modal_events';
+import {displayFilepicker} from 'editor_tiny/utils';
+import LinkModal from 'tiny_link/modal';
+import {getPermissions} from "tiny_link/options";
+import {setLink, getCurrentLinkData, unSetLink} from "tiny_link/link";
+import Selectors from 'tiny_link/selectors';
+
+/**
+ * Handle action.
+ *
+ * @param {TinyMCE} editor
+ * @param {boolean} unlink
+ */
+export const handleAction = (editor, unlink = false) => {
+ if (!unlink) {
+ displayDialogue(editor);
+ } else {
+ unSetLink(editor);
+ }
+};
+
+/**
+ * Display the link dialogue.
+ *
+ * @param {TinyMCE} editor
+ * @returns {Promise}
+ */
+const displayDialogue = async(editor) => {
+ const modalPromises = await ModalFactory.create({
+ type: LinkModal.TYPE,
+ templateContext: getTemplateContext(editor),
+ large: false,
+ });
+
+ modalPromises.show();
+ const $root = await modalPromises.getRoot();
+ const root = $root[0];
+ const currentForm = root.querySelector('form');
+
+ $root.on(ModalEvents.hidden, () => {
+ modalPromises.destroy();
+ });
+
+ root.addEventListener('click', (e) => {
+ const submitAction = e.target.closest(Selectors.actions.submit);
+ const linkBrowserAction = e.target.closest(Selectors.actions.linkBrowser);
+ if (submitAction) {
+ e.preventDefault();
+ setLink(currentForm, editor);
+ modalPromises.destroy();
+ }
+ if (linkBrowserAction) {
+ e.preventDefault();
+ displayFilepicker(editor, 'link').then((params) => {
+ filePickerCallback(params, currentForm, editor);
+ return modalPromises.destroy();
+ }).catch();
+ }
+ });
+
+ const linkTitle = root.querySelector(Selectors.elements.urlText);
+ const linkUrl = root.querySelector(Selectors.elements.urlEntry);
+ linkTitle.addEventListener('change', () => {
+ if (linkTitle.value.length > 0) {
+ linkTitle.dataset.useLinkAsText = 'false';
+ } else {
+ linkTitle.dataset.useLinkAsText = 'true';
+ linkTitle.value = linkUrl.value;
+ }
+ });
+
+ linkUrl.addEventListener('keyup', () => {
+ updateTextToDisplay(currentForm);
+ });
+};
+
+/**
+ * Get template context.
+ *
+ * @param {TinyMCE} editor
+ * @returns {Object}
+ */
+const getTemplateContext = (editor) => {
+ const data = getCurrentLinkData(editor);
+
+ return Object.assign({}, {
+ elementid: editor.id,
+ showfilepicker: getPermissions(editor).filepicker,
+ isupdating: Object.keys(data).length > 0,
+ }, data);
+};
+
+/**
+ * Update the dialogue after a link was selected in the File Picker.
+ *
+ * @param {Object} params
+ * @param {Element} currentForm
+ * @param {TinyMCE} editor
+ */
+const filePickerCallback = (params, currentForm, editor) => {
+ if (params.url) {
+ const inputUrl = currentForm.querySelector(Selectors.elements.urlEntry);
+ inputUrl.value = params.url;
+ setLink(currentForm, editor);
+ }
+};
+
+/**
+ * Update the text to display if the user does not provide the custom text.
+ *
+ * @param {Element} currentForm
+ */
+const updateTextToDisplay = (currentForm) => {
+ const urlEntry = currentForm.querySelector(Selectors.elements.urlEntry);
+ const urlText = currentForm.querySelector(Selectors.elements.urlText);
+ if (urlText.dataset.useLinkAsText === 'true') {
+ urlText.value = urlEntry.value;
+ }
+};
diff --git a/lib/editor/tiny/plugins/link/classes/plugininfo.php b/lib/editor/tiny/plugins/link/classes/plugininfo.php
new file mode 100644
index 0000000000000..65b5aca9343b6
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/classes/plugininfo.php
@@ -0,0 +1,89 @@
+.
+
+/**
+ * Tiny Link plugin.
+ *
+ * @package tiny_link
+ * @copyright 2022 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tiny_link;
+
+use context;
+use context_system;
+use editor_tiny\editor;
+use editor_tiny\plugin;
+use editor_tiny\plugin_with_buttons;
+use editor_tiny\plugin_with_configuration;
+use editor_tiny\plugin_with_menuitems;
+
+/**
+ * Tiny link plugin.
+ *
+ * @package tiny_link
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menuitems, plugin_with_configuration {
+
+ /**
+ * Get a list of the buttons provided by this plugin.
+ *
+ * @return string[]
+ */
+ public static function get_available_buttons(): array {
+ return [
+ 'tiny_link/tiny_link_link',
+ 'tiny_link/tiny_link_unlink',
+ ];
+ }
+
+ /**
+ * Get a list of the menu items provided by this plugin.
+ *
+ * @return string[]
+ */
+ public static function get_available_menuitems(): array {
+ return [
+ 'tiny_link/tiny_link_link',
+ ];
+ }
+
+ /**
+ * Get a list of the menu items provided by this plugin.
+ *
+ * @param context $context The context that the editor is used within
+ * @param array $options The options passed in when requesting the editor
+ * @param array $fpoptions The filepicker options passed in when requesting the editor
+ * @param editor $editor The editor instance in which the plugin is initialised
+ * @return array
+ */
+ public static function get_plugin_configuration_for_context(
+ context $context,
+ array $options,
+ array $fpoptions,
+ ?editor $editor = null
+ ): array {
+ // TODO Fetch the actual permissions.
+ $permissions['filepicker'] = true;
+
+ return [
+ 'permissions' => $permissions,
+ ];
+ }
+}
diff --git a/lib/editor/tiny/plugins/link/classes/privacy/provider.php b/lib/editor/tiny/plugins/link/classes/privacy/provider.php
new file mode 100644
index 0000000000000..ec0ddd331813b
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/classes/privacy/provider.php
@@ -0,0 +1,45 @@
+.
+
+/**
+ * Privacy Subsystem implementation for the Link plugin for TinyMCE.
+ *
+ * @package tiny_link
+ * @copyright 2022 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tiny_link\privacy;
+
+/**
+ * Privacy Subsystem implementation for the Link plugin for TinyMCE.
+ *
+ * @package tiny_link
+ * @copyright 2023 Huong Nguyen
+ * @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/link/lang/en/tiny_link.php b/lib/editor/tiny/plugins/link/lang/en/tiny_link.php
new file mode 100644
index 0000000000000..756b0382f85d1
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/lang/en/tiny_link.php
@@ -0,0 +1,34 @@
+.
+
+/**
+ * Strings for component 'tiny_link', language 'en'.
+ *
+ * @package tiny_link
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['browserepositories'] = 'Browse repositories...';
+$string['createlink'] = 'Create link';
+$string['enterurl'] = 'Enter a URL';
+$string['openinnewwindow'] = 'Open in new window';
+$string['pluginname'] = 'Tiny link';
+$string['link'] = 'Link';
+$string['unlink'] = 'Unlink';
+$string['updatelink'] = 'Update link';
+$string['privacy:metadata'] = 'The link plugin for TinyMCE does not store any personal data.';
+$string['texttodisplay'] = 'Text to display';
diff --git a/lib/editor/tiny/plugins/link/templates/embed_link.mustache b/lib/editor/tiny/plugins/link/templates/embed_link.mustache
new file mode 100644
index 0000000000000..3720cc6eafa1c
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/templates/embed_link.mustache
@@ -0,0 +1,33 @@
+{{!
+ 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 .
+}}
+{{!
+ @template tiny_link/embed_link
+
+ Embed media link template.
+
+ Example context (json):
+ {
+
+ }
+}}
+{{!
+ }}{{#name}}{{{.}}}{{/name}}{{!
+ }}{{^name}}{{url}}{{/name}}{{!
+}}
diff --git a/lib/editor/tiny/plugins/link/templates/modal.mustache b/lib/editor/tiny/plugins/link/templates/modal.mustache
new file mode 100644
index 0000000000000..bd27117871d39
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/templates/modal.mustache
@@ -0,0 +1,89 @@
+{{!
+ 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 .
+}}
+{{!
+ @template tiny_link/modal
+
+ Modal to manage a link within the Tiny Editor.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+
+ Example context (json):
+ {
+ "elementid": "exampleId",
+ "setLinkOnElement": false,
+ "showfilepicker": true,
+ "urltext": "Abc",
+ "url": "https://moodle.org/",
+ "newwindow": false
+ }
+}}
+{{< core/modal }}
+
+ {{$title}}
+ {{#str}} createlink, tiny_link {{/str}}
+ {{/title}}
+
+ {{$body}}
+
+ {{/body}}
+
+ {{$footer}}
+
+ {{/footer}}
+
+{{/ core/modal }}
diff --git a/lib/editor/tiny/plugins/link/tests/behat/link.feature b/lib/editor/tiny/plugins/link/tests/behat/link.feature
new file mode 100644
index 0000000000000..45109fe9e7e12
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/tests/behat/link.feature
@@ -0,0 +1,158 @@
+@editor @editor_tiny @tiny_link @_file_upload
+Feature: Add links to TinyMCE
+ To write rich text - I need to add links.
+
+ @javascript
+ Scenario: Insert a link
+ Given the following "blocks" exist:
+ | blockname | contextlevel | reference | pagetypepattern | defaultregion |
+ | private_files | System | 1 | my-index | side-post |
+ And I log in as "admin"
+ And I follow "Manage private files..."
+ And I upload "lib/editor/tiny/tests/behat/fixtures/moodle-logo.png" file to "Files" filemanager
+ And I click on "Save changes" "button"
+ And I open my profile in edit mode
+ And I set the field "Description" to "Super cool"
+ When I select the "p" element in position "0" of the "Description" TinyMCE editor
+ And I click on the "Link" button for the "Description" TinyMCE editor
+ Then the field "Text to display" matches value "Super cool"
+ And I click on "Browse repositories..." "button" in the "Create link" "dialogue"
+ And I select "Private files" repository in file picker
+ And I click on "moodle-logo.png" "link"
+ And I click on "Select this file" "button"
+ And I click on "Update profile" "button"
+ And I follow "Preferences" in the user menu
+ And I follow "Editor preferences"
+ And I set the field "Text editor" to "Plain text area"
+ And I press "Save changes"
+ And I click on "Edit profile" "link" in the "region-main" "region"
+ And I should see "Super cool"
+
+ @javascript
+ Scenario: Insert a link without providing text to display
+ Given I log in as "admin"
+ When I open my profile in edit mode
+ And I click on the "Link" button for the "Description" TinyMCE editor
+ And I set the field "URL" to "https://moodle.org/"
+ Then the field "Text to display" matches value "https://moodle.org/"
+ And I click on "Create link" "button" in the "Create link" "dialogue"
+ And the field "Description" matches value "
"
+ And I select the "a" element in position "0" of the "Description" TinyMCE editor
+ And I click on the "Link" button for the "Description" TinyMCE editor
+ And the field "Text to display" matches value "https://moodle.org/"
+ And the field "URL" matches value "https://moodle.org/"
+ And I click on "Close" "button" in the "Create link" "dialogue"
+
+ @javascript
+ Scenario: Insert a link with providing text to display
+ Given I log in as "admin"
+ When I open my profile in edit mode
+ And I click on "Link" "button"
+ And I set the field "Text to display" to "Moodle - Open-source learning platform"
+ And I set the field "Enter a URL" to "https://moodle.org/"
+ And I click on "Create link" "button" in the "Create link" "dialogue"
+ Then the field "Description" matches value "
"
+ And I select the "a" element in position "0" of the "Description" TinyMCE editor
+ And I click on the "Link" button for the "Description" TinyMCE editor
+ And the field "Text to display" matches value "Moodle - Open-source learning platform"
+ And the field "Enter a URL" matches value "https://moodle.org/"
+ And I click on "Close" "button" in the "Create link" "dialogue"
+
+ @javascript
+ Scenario: Edit a link that already had a custom text to display
+ Given I log in as "admin"
+ And I follow "Preferences" in the user menu
+ And I follow "Editor preferences"
+ And I set the field "Text editor" to "Plain text area"
+ And I press "Save changes"
+ And I click on "Edit profile" "link" in the "region-main" "region"
+ And I set the field "Description" to "Moodle - Open-source learning platform"
+ And I click on "Update profile" "button"
+ And I follow "Preferences" in the user menu
+ And I follow "Editor preferences"
+ And I set the field "Text editor" to "TinyMCE editor"
+ And I press "Save changes"
+ When I click on "Edit profile" "link" in the "region-main" "region"
+ Then the field "Description" matches value "
"
+ And I select the "a" element in position "0" of the "Description" TinyMCE editor
+ And I click on the "Link" button for the "Description" TinyMCE editor
+ And the field "Text to display" matches value "Moodle - Open-source learning platform"
+ And the field "Enter a URL" matches value "https://moodle.org/"
+
+ @javascript
+ Scenario: Insert and update link in the TinyMCE editor
+ Given I log in as "admin"
+ When I open my profile in edit mode
+ And I click on "Link" "button"
+ And I set the field "Text to display" to "Moodle - Open-source learning platform"
+ And I set the field "Enter a URL" to "https://moodle.org/"
+ And I click on "Create link" "button" in the "Create link" "dialogue"
+ Then the field "Description" matches value "
"
+ And I select the "a" element in position "0" of the "Description" TinyMCE editor
+ And I click on the "Link" button for the "Description" TinyMCE editor
+ And the field "Text to display" matches value "Moodle - Open-source learning platform"
+ And the field "Enter a URL" matches value "https://moodle.org/"
+ And I set the field "Enter a URL" to "https://moodle.com/"
+ And "Create link" "button" should not exist in the "Create link" "dialogue"
+ And "Update link" "button" should exist in the "Create link" "dialogue"
+ And I click on "Update link" "button" in the "Create link" "dialogue"
+ And the field "Description" matches value "
"
+
+ @javascript
+ Scenario: Insert a link for an image using TinyMCE editor
+ Given I log in as "admin"
+ And I follow "Private files" in the user menu
+ And I upload "lib/editor/tiny/tests/behat/fixtures/moodle-logo.png" file to "Files" filemanager
+ And I click on "Save changes" "button"
+ And I open my profile in edit mode
+ And I click on the "Image" button for the "Description" TinyMCE editor
+ And I click on "Browse repositories..." "button" in the "Image properties" "dialogue"
+ And I select "Private files" repository in file picker
+ And I click on "moodle-logo.png" "link"
+ And I click on "Select this file" "button"
+ And I set the field "Describe this image for someone who cannot see it" to "It's the Moodle"
+ And I click on "Save image" "button" in the "Image properties" "dialogue"
+ And I select the "img" element in position "0" of the "Description" TinyMCE editor
+ And I click on the "Link" button for the "Description" TinyMCE editor
+ And I set the field "Enter a URL" to "https://moodle.org/"
+ And I set the field "Text to display" to "Moodle - Open-source learning platform"
+ And I click on "Update link" "button" in the "Create link" "dialogue"
+ # TODO: Verify the HTML by the improved code plugin in MDL-75265
+ And I click on "Update profile" "button"
+ And I follow "Preferences" in the user menu
+ And I follow "Editor preferences"
+ And I set the field "Text editor" to "Plain text area"
+ And I press "Save changes"
+ When I click on "Edit profile" "link" in the "region-main" "region"
+ Then I should see "Moodle - Open-source learning platform"
+ And I click on "Update profile" "button"
+ And I follow "Preferences" in the user menu
+ And I follow "Editor preferences"
+ And I set the field "Text editor" to "TinyMCE editor"
+ And I press "Save changes"
+ And I click on "Edit profile" "link" in the "region-main" "region"
+ And I select the "a" element in position "0" of the "Description" TinyMCE editor
+ When I click on the "Unlink" button for the "Description" TinyMCE editor
+ Then the field "Description" matches value "
Moodle - Open-source learning platform
"
diff --git a/lib/editor/tiny/plugins/link/version.php b/lib/editor/tiny/plugins/link/version.php
new file mode 100644
index 0000000000000..01f37cc35cded
--- /dev/null
+++ b/lib/editor/tiny/plugins/link/version.php
@@ -0,0 +1,29 @@
+.
+
+/**
+ * Tiny Link plugin version details.
+ *
+ * @package tiny_link
+ * @copyright 2023 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version = 2022112800;
+$plugin->requires = 2022111800;
+$plugin->component = 'tiny_link';