From cd064a40eb0ca45199ef4cf73f1ddebd52236e56 Mon Sep 17 00:00:00 2001 From: David Woloszyn Date: Fri, 11 Aug 2023 12:17:32 +1000 Subject: [PATCH 1/2] MDL-75268 editor_tiny: Add items after specified node --- 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 | 29 +++++++++++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/editor/tiny/amd/build/utils.min.js b/lib/editor/tiny/amd/build/utils.min.js index a672847db29..461f62c1209 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=function(toolbar,section,button){let after=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;if(!toolbar)return[{name:section,items:[button]}];const mutatedToolbar=JSON.parse(JSON.stringify(toolbar));return mutatedToolbar.map((item=>{if(item.name===section)if(after){let index=item.items.findIndex((value=>value==after));-1!==index&&item.items.splice(index+1,0,button)}else item.items.push(button);return 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=function(menubar,section,menuitem){let after=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;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;if(name===section)if(after){let index=menu.items.indexOf(after);-1!==index&&(index+=after.length,menu.items=menu.items.slice(0,index)+" ".concat(menuitem)+menu.items.slice(index))}else 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")}})); //# 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..5898ad319ac 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":"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"} \ 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 * @param {string|null} [after=null]\n * @returns {object} The toolbar configuration\n */\nexport const addToolbarButton = (toolbar, section, button, after = null) => {\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 if (after) {\n // Insert new button after the specified button.\n let index = item.items.findIndex(value => value == after);\n if (index !== -1) {\n item.items.splice(index + 1, 0, button);\n }\n } else {\n // Append button to end of button section.\n item.items.push(button);\n }\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 * @param {string|null} [after=null]\n * @returns {object}\n */\nexport const addMenubarItem = (menubar, section, menuitem, after = null) => {\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 if (after) {\n // Insert new item after the specified menu item.\n let index = menu.items.indexOf(after);\n if (index !== -1) {\n index += after.length;\n menu.items = menu.items.slice(0, index) + ` ${menuitem}` + menu.items.slice(index);\n }\n } else {\n // Append item to end of the menu section.\n menu.items = `${menu.items} ${menuitem}`;\n }\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","after","name","items","mutatedToolbar","JSON","parse","stringify","map","item","index","findIndex","value","splice","push","buttons","forEach","relativeTo","append","newSection","sectionInserted","some","unshift","menubar","menuitem","title","mutatedMenubar","Array","from","Object","entries","_ref","menu","indexOf","length","slice","contextmenu","contextmenuItems","split","menuitems","concat","filter","join","getDocumentationLink","pluginName","url","getMetadata","getElement","isConnected","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..f444b3eab0a 100644 --- a/lib/editor/tiny/amd/src/utils.js +++ b/lib/editor/tiny/amd/src/utils.js @@ -58,9 +58,10 @@ export const displayFilepicker = (editor, filetype) => new Promise((resolve, rej * @param {object} toolbar * @param {string} section * @param {string} button + * @param {string|null} [after=null] * @returns {object} The toolbar configuration */ -export const addToolbarButton = (toolbar, section, button) => { +export const addToolbarButton = (toolbar, section, button, after = null) => { if (!toolbar) { return [{ name: section, @@ -71,7 +72,16 @@ export const addToolbarButton = (toolbar, section, button) => { const mutatedToolbar = JSON.parse(JSON.stringify(toolbar)); return mutatedToolbar.map((item) => { if (item.name === section) { - item.items.push(button); + if (after) { + // Insert new button after the specified button. + let index = item.items.findIndex(value => value == after); + if (index !== -1) { + item.items.splice(index + 1, 0, button); + } + } else { + // Append button to end of button section. + item.items.push(button); + } } return item; @@ -148,9 +158,10 @@ export const addToolbarSection = (toolbar, name, relativeTo, append = true) => { * @param {object} menubar * @param {string} section * @param {string} menuitem + * @param {string|null} [after=null] * @returns {object} */ -export const addMenubarItem = (menubar, section, menuitem) => { +export const addMenubarItem = (menubar, section, menuitem, after = null) => { if (!menubar) { const emptyMenubar = {}; emptyMenubar[section] = { @@ -162,7 +173,17 @@ export const addMenubarItem = (menubar, section, menuitem) => { const mutatedMenubar = JSON.parse(JSON.stringify(menubar)); Array.from(Object.entries(mutatedMenubar)).forEach(([name, menu]) => { if (name === section) { - menu.items = `${menu.items} ${menuitem}`; + if (after) { + // Insert new item after the specified menu item. + let index = menu.items.indexOf(after); + if (index !== -1) { + index += after.length; + menu.items = menu.items.slice(0, index) + ` ${menuitem}` + menu.items.slice(index); + } + } else { + // Append item to end of the menu section. + menu.items = `${menu.items} ${menuitem}`; + } } }); From 585fd545ae84366af4cd6f31c2b6743223b599e4 Mon Sep 17 00:00:00 2001 From: David Woloszyn Date: Wed, 16 Aug 2023 14:50:20 +1000 Subject: [PATCH 2/2] MDL-75268 tiny_premium: Add Tiny Premium plugin As a first round of support for Tiny Premium, plugins with minimal config have been included for sites with a valid Tiny Cloud API key. --- .../plugins/premium/amd/build/common.min.js | 3 + .../premium/amd/build/common.min.js.map | 1 + .../premium/amd/build/configuration.min.js | 3 + .../amd/build/configuration.min.js.map | 1 + .../plugins/premium/amd/build/external.min.js | 10 ++ .../premium/amd/build/external.min.js.map | 1 + .../plugins/premium/amd/build/plugin.min.js | 10 ++ .../premium/amd/build/plugin.min.js.map | 1 + .../tiny/plugins/premium/amd/src/common.js | 27 ++++ .../plugins/premium/amd/src/configuration.js | 121 ++++++++++++++++++ .../tiny/plugins/premium/amd/src/external.js | 41 ++++++ .../tiny/plugins/premium/amd/src/plugin.js | 87 +++++++++++++ .../premium/classes/external/get_api_key.php | 78 +++++++++++ .../plugins/premium/classes/plugininfo.php | 49 +++++++ .../premium/classes/privacy/provider.php | 37 ++++++ .../tiny/plugins/premium/db/services.php | 36 ++++++ .../plugins/premium/lang/en/tiny_premium.php | 31 +++++ .../tiny/plugins/premium/readme_moodle.txt | 12 ++ lib/editor/tiny/plugins/premium/settings.php | 39 ++++++ lib/editor/tiny/plugins/premium/version.php | 29 +++++ 20 files changed, 617 insertions(+) create mode 100644 lib/editor/tiny/plugins/premium/amd/build/common.min.js create mode 100644 lib/editor/tiny/plugins/premium/amd/build/common.min.js.map create mode 100644 lib/editor/tiny/plugins/premium/amd/build/configuration.min.js create mode 100644 lib/editor/tiny/plugins/premium/amd/build/configuration.min.js.map create mode 100644 lib/editor/tiny/plugins/premium/amd/build/external.min.js create mode 100644 lib/editor/tiny/plugins/premium/amd/build/external.min.js.map create mode 100644 lib/editor/tiny/plugins/premium/amd/build/plugin.min.js create mode 100644 lib/editor/tiny/plugins/premium/amd/build/plugin.min.js.map create mode 100644 lib/editor/tiny/plugins/premium/amd/src/common.js create mode 100644 lib/editor/tiny/plugins/premium/amd/src/configuration.js create mode 100644 lib/editor/tiny/plugins/premium/amd/src/external.js create mode 100644 lib/editor/tiny/plugins/premium/amd/src/plugin.js create mode 100644 lib/editor/tiny/plugins/premium/classes/external/get_api_key.php create mode 100644 lib/editor/tiny/plugins/premium/classes/plugininfo.php create mode 100644 lib/editor/tiny/plugins/premium/classes/privacy/provider.php create mode 100644 lib/editor/tiny/plugins/premium/db/services.php create mode 100644 lib/editor/tiny/plugins/premium/lang/en/tiny_premium.php create mode 100644 lib/editor/tiny/plugins/premium/readme_moodle.txt create mode 100644 lib/editor/tiny/plugins/premium/settings.php create mode 100644 lib/editor/tiny/plugins/premium/version.php diff --git a/lib/editor/tiny/plugins/premium/amd/build/common.min.js b/lib/editor/tiny/plugins/premium/amd/build/common.min.js new file mode 100644 index 00000000000..959a0b8c8e4 --- /dev/null +++ b/lib/editor/tiny/plugins/premium/amd/build/common.min.js @@ -0,0 +1,3 @@ +define("tiny_premium/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={pluginName:"tiny_premium/plugin",component:"tiny_premium"},_exports.default})); + +//# sourceMappingURL=common.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/premium/amd/build/common.min.js.map b/lib/editor/tiny/plugins/premium/amd/build/common.min.js.map new file mode 100644 index 00000000000..8ad877fb412 --- /dev/null +++ b/lib/editor/tiny/plugins/premium/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 Premium common values.\n *\n * @module tiny_premium/common\n * @copyright 2023 David Woloszyn \n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n pluginName: 'tiny_premium/plugin',\n component: 'tiny_premium',\n};\n"],"names":["pluginName","component"],"mappings":"qKAuBe,CACXA,WAAY,sBACZC,UAAW"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js b/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js new file mode 100644 index 00000000000..621fad27a73 --- /dev/null +++ b/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js @@ -0,0 +1,3 @@ +define("tiny_premium/configuration",["exports","editor_tiny/utils"],(function(_exports,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>{let plugins=instanceConfig.plugins,menu=instanceConfig.menu,toolbar=(toolbar=>(toolbar=(0,_utils.addToolbarSection)(toolbar,"premium_a","advanced",!0),(0,_utils.addToolbarSection)(toolbar,"premium_b","formatting",!0)))(instanceConfig.toolbar),contextmenu=instanceConfig.contextmenu;return plugins+=" advtable",menu=(0,_utils.addMenubarItem)(menu,"table","| advtablerownumbering","advtablesort"),plugins+=" editimage",toolbar=(0,_utils.addToolbarButton)(toolbar,"content","editimage","tiny_media_image"),instanceConfig.editimage_toolbar="rotateleft rotateright flipv fliph editimage",plugins+=" export",menu=(0,_utils.addMenubarItem)(menu,"tools","| export"),plugins+=" pageembed",toolbar=(0,_utils.addToolbarButton)(toolbar,"content","pageembed","tiny_media_video"),plugins+=" typography",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_b","typography"),plugins+=" casechange",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","casechange"),plugins+=" checklist",toolbar=(0,_utils.addToolbarButton)(toolbar,"lists","checklist"),plugins+=" tinymcespellchecker",menu=(0,_utils.addMenubarItem)(menu,"tools","spellcheckdialog","spellcheckerlanguage"),contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"spellchecker"),toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","spellcheckdialog"),plugins+=" autocorrect",menu=(0,_utils.addMenubarItem)(menu,"tools","| autocorrect capitalization","spellcheckdialog"),plugins+=" permanentpen",menu=(0,_utils.addMenubarItem)(menu,"format","| permanentpen configurepermanentpen"),toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","permanentpen"),contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"configurepermanentpen"),plugins+=" formatpainter",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","formatpainter"),plugins+=" linkchecker",contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"linkchecker"),plugins+=" tableofcontents",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","tableofcontents"),plugins+=" footnotes",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","footnotes"),menu=(0,_utils.addMenubarItem)(menu,"insert","footnotes","tableofcontents"),plugins+=" powerpaste",{plugins:plugins,toolbar:toolbar,menu:menu,contextmenu:contextmenu}}})); + +//# sourceMappingURL=configuration.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js.map b/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js.map new file mode 100644 index 00000000000..ca798b5a1b9 --- /dev/null +++ b/lib/editor/tiny/plugins/premium/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 Premium configuration.\n *\n * @module tiny_premium/configuration\n * @copyright 2023 David Woloszyn \n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {\n addToolbarButton,\n addMenubarItem,\n addToolbarSection,\n addContextmenuItem\n} from 'editor_tiny/utils';\n\nconst configureToolbar = (toolbar) => {\n // Add premium toolbar sections to house all the plugins with no natural home.\n toolbar = addToolbarSection(toolbar, 'premium_a', 'advanced', true);\n toolbar = addToolbarSection(toolbar, 'premium_b', 'formatting', true);\n return toolbar;\n};\n\nexport const configure = (instanceConfig) => {\n // There is some manipulating of the plugin menu, toolbar, context and quickbar items.\n // This was necessary to enhance user experience and closer align to the Tiny demo site.\n let plugins = instanceConfig.plugins;\n let menu = instanceConfig.menu;\n let toolbar = configureToolbar(instanceConfig.toolbar);\n let contextmenu = instanceConfig.contextmenu;\n let pluginsettings = {};\n\n // Advanced Table.\n plugins += ` advtable`;\n menu = addMenubarItem(menu, 'table', '| advtablerownumbering', 'advtablesort');\n\n // Enhanced Image Editing.\n plugins += ` editimage`;\n toolbar = addToolbarButton(toolbar, 'content', 'editimage', 'tiny_media_image');\n // Remove the duplicate image button from the quickbar toolbar by redefining the values without 'imageoptions'.\n // eslint-disable-next-line camelcase\n instanceConfig.editimage_toolbar = 'rotateleft rotateright flipv fliph editimage';\n\n // Export.\n plugins += ` export`;\n menu = addMenubarItem(menu, 'tools', '| export');\n\n // Page Embed.\n plugins += ` pageembed`;\n toolbar = addToolbarButton(toolbar, 'content', 'pageembed', 'tiny_media_video');\n\n // Advanced Typography.\n plugins += ` typography`;\n toolbar = addToolbarButton(toolbar, 'premium_b', 'typography');\n\n // Case Change.\n plugins += ` casechange`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'casechange');\n\n // Checklist.\n plugins += ` checklist`;\n toolbar = addToolbarButton(toolbar, 'lists', 'checklist');\n\n // Spell Checker Pro.\n plugins += ` tinymcespellchecker`;\n menu = addMenubarItem(menu, 'tools', 'spellcheckdialog', 'spellcheckerlanguage');\n contextmenu = addContextmenuItem(contextmenu, 'spellchecker');\n toolbar = addToolbarButton(toolbar, 'premium_a', 'spellcheckdialog');\n\n // Spelling Autocorrect.\n plugins += ` autocorrect`;\n menu = addMenubarItem(menu, 'tools', '| autocorrect capitalization', 'spellcheckdialog');\n\n // Permanent Pen.\n plugins += ` permanentpen`;\n menu = addMenubarItem(menu, 'format', '| permanentpen configurepermanentpen');\n toolbar = addToolbarButton(toolbar, 'premium_a', 'permanentpen');\n contextmenu = addContextmenuItem(contextmenu, 'configurepermanentpen');\n\n // Format Painter.\n plugins += ` formatpainter`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'formatpainter');\n\n // Link Checker.\n plugins += ` linkchecker`;\n contextmenu = addContextmenuItem(contextmenu, 'linkchecker');\n\n // Table of Contents.\n plugins += ` tableofcontents`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'tableofcontents');\n\n // Footnotes.\n plugins += ` footnotes`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'footnotes');\n menu = addMenubarItem(menu, 'insert', 'footnotes', 'tableofcontents');\n\n // Powerpaste.\n plugins += ` powerpaste`;\n\n return {\n plugins,\n toolbar,\n menu,\n contextmenu,\n ...pluginsettings\n };\n};\n"],"names":["instanceConfig","plugins","menu","toolbar","configureToolbar","contextmenu","editimage_toolbar"],"mappings":"oMAqC0BA,qBAGlBC,QAAUD,eAAeC,QACzBC,KAAOF,eAAeE,KACtBC,QAZkBA,CAAAA,UAEtBA,SAAU,4BAAkBA,QAAS,YAAa,YAAY,IACpD,4BAAkBA,QAAS,YAAa,cAAc,IASlDC,CAAiBJ,eAAeG,SAC1CE,YAAcL,eAAeK,mBAIjCJ,qBACAC,MAAO,yBAAeA,KAAM,QAAS,yBAA0B,gBAG/DD,sBACAE,SAAU,2BAAiBA,QAAS,UAAW,YAAa,oBAG5DH,eAAeM,kBAAoB,+CAGnCL,mBACAC,MAAO,yBAAeA,KAAM,QAAS,YAGrCD,sBACAE,SAAU,2BAAiBA,QAAS,UAAW,YAAa,oBAG5DF,uBACAE,SAAU,2BAAiBA,QAAS,YAAa,cAGjDF,uBACAE,SAAU,2BAAiBA,QAAS,YAAa,cAGjDF,sBACAE,SAAU,2BAAiBA,QAAS,QAAS,aAG7CF,gCACAC,MAAO,yBAAeA,KAAM,QAAS,mBAAoB,wBACzDG,aAAc,6BAAmBA,YAAa,gBAC9CF,SAAU,2BAAiBA,QAAS,YAAa,oBAGjDF,wBACAC,MAAO,yBAAeA,KAAM,QAAS,+BAAgC,oBAGrED,yBACAC,MAAO,yBAAeA,KAAM,SAAU,wCACtCC,SAAU,2BAAiBA,QAAS,YAAa,gBACjDE,aAAc,6BAAmBA,YAAa,yBAG9CJ,0BACAE,SAAU,2BAAiBA,QAAS,YAAa,iBAGjDF,wBACAI,aAAc,6BAAmBA,YAAa,eAG9CJ,4BACAE,SAAU,2BAAiBA,QAAS,YAAa,mBAGjDF,sBACAE,SAAU,2BAAiBA,QAAS,YAAa,aACjDD,MAAO,yBAAeA,KAAM,SAAU,YAAa,mBAGnDD,uBAEO,CACHA,QAAAA,QACAE,QAAAA,QACAD,KAAAA,KACAG,YAAAA"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/premium/amd/build/external.min.js b/lib/editor/tiny/plugins/premium/amd/build/external.min.js new file mode 100644 index 00000000000..f23d5c14110 --- /dev/null +++ b/lib/editor/tiny/plugins/premium/amd/build/external.min.js @@ -0,0 +1,10 @@ +define("tiny_premium/external",["exports","core/ajax"],(function(_exports,_ajax){var obj; +/** + * Helper to get external content for Tiny Premium plugin. + * + * @module tiny_premium/external + * @copyright 2023 David Woloszyn + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getApiKey=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.getApiKey=contextId=>{const request={methodname:"tiny_premium_get_api_key",args:{contextid:contextId}};return _ajax.default.call([request])[0]}})); + +//# sourceMappingURL=external.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/premium/amd/build/external.min.js.map b/lib/editor/tiny/plugins/premium/amd/build/external.min.js.map new file mode 100644 index 00000000000..057c665ccb7 --- /dev/null +++ b/lib/editor/tiny/plugins/premium/amd/build/external.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"external.min.js","sources":["../src/external.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 to get external content for Tiny Premium plugin.\n *\n * @module tiny_premium/external\n * @copyright 2023 David Woloszyn \n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Due to the order items are initialise, options.js will not work in getting the API key.\n * This external method is used to get the API key in time.\n *\n * @param {Number} contextId The context id\n * @return {Promise}\n */\nexport const getApiKey = (contextId) => {\n const request = {\n methodname: 'tiny_premium_get_api_key',\n args: {\n contextid: contextId\n }\n };\n return Ajax.call([request])[0];\n};\n"],"names":["contextId","request","methodname","args","contextid","Ajax","call"],"mappings":";;;;;;;8JAgC0BA,kBAChBC,QAAU,CACZC,WAAY,2BACZC,KAAM,CACFC,UAAWJ,mBAGZK,cAAKC,KAAK,CAACL,UAAU"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js b/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js new file mode 100644 index 00000000000..cb93b76838a --- /dev/null +++ b/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js @@ -0,0 +1,10 @@ +define("tiny_premium/plugin",["exports","editor_tiny/loader","editor_tiny/utils","tiny_premium/common","tiny_premium/configuration","tiny_premium/external","core/config"],(function(_exports,_loader,_utils,_common,Configuration,_external,_config){var obj;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)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Configuration=function(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]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj} +/** + * Tiny Premium plugin for Moodle. + * + * @module tiny_premium/plugin + * @copyright 2023 David Woloszyn + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */(Configuration);const currentContextId=(_config=(obj=_config)&&obj.__esModule?obj:{default:obj}).default.contextid;var _default=new Promise((async resolve=>{const[tinyMCE,pluginMetadata,externalData]=await Promise.all([(0,_loader.getTinyMCE)(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName),(0,_external.getApiKey)(currentContextId)]);tinyMCE.PluginManager.add("".concat(_common.component,"/plugin"),(()=>pluginMetadata)),await getTinyPremium(externalData.apikey),resolve(["".concat(_common.component,"/plugin"),Configuration])}));let tinyPremiumPromise;_exports.default=_default;const getTinyPremium=apikey=>tinyPremiumPromise||(tinyPremiumPromise=new Promise(((resolve,reject)=>{const head=document.querySelector("head"),script=document.createElement("script");script.dataset.tinymce="premium",script.src="https://cdn.tiny.cloud/1/".concat(apikey,"/tinymce/6/plugins.min.js"),script.referrerpolicy="origin",script.addEventListener("load",(()=>{resolve()}),!1),script.addEventListener("error",(err=>{reject(err)}),!1),head.append(script)})),tinyPremiumPromise);return _exports.default})); + +//# sourceMappingURL=plugin.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js.map b/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js.map new file mode 100644 index 00000000000..f0fa0b24920 --- /dev/null +++ b/lib/editor/tiny/plugins/premium/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 Premium plugin for Moodle.\n *\n * @module tiny_premium/plugin\n * @copyright 2023 David Woloszyn \n * @license https://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';\nimport {component, pluginName} from 'tiny_premium/common';\nimport * as Configuration from 'tiny_premium/configuration';\nimport {getApiKey} from 'tiny_premium/external';\nimport Config from 'core/config';\n\nconst currentContextId = Config.contextid;\n\n// eslint-disable-next-line no-async-promise-executor\nexport default new Promise(async(resolve) => {\n const [\n tinyMCE,\n pluginMetadata,\n externalData\n ] = await Promise.all([\n getTinyMCE(),\n getPluginMetadata(component, pluginName),\n getApiKey(currentContextId)\n ]);\n\n tinyMCE.PluginManager.add(`${component}/plugin`, () => {\n return pluginMetadata;\n });\n\n // Load the Tiny Premium script using the provided API key.\n await getTinyPremium(externalData.apikey);\n\n resolve([`${component}/plugin`, Configuration]);\n});\n\nlet tinyPremiumPromise;\n\n/**\n * Promise for Tiny Premium plugins script.\n *\n * @param {string} apikey\n * @return {Promise}\n */\nconst getTinyPremium = (apikey) => {\n if (tinyPremiumPromise) {\n return tinyPremiumPromise;\n }\n\n tinyPremiumPromise = new Promise((resolve, reject) => {\n const head = document.querySelector('head');\n const script = document.createElement('script');\n script.dataset.tinymce = 'premium';\n script.src = `https://cdn.tiny.cloud/1/${apikey}/tinymce/6/plugins.min.js`;\n script.referrerpolicy = \"origin\";\n\n script.addEventListener('load', () => {\n resolve();\n }, false);\n\n script.addEventListener('error', (err) => {\n reject(err);\n }, false);\n\n head.append(script);\n });\n\n return tinyPremiumPromise;\n};\n"],"names":["currentContextId","contextid","Promise","async","tinyMCE","pluginMetadata","externalData","all","component","pluginName","PluginManager","add","getTinyPremium","apikey","resolve","Configuration","tinyPremiumPromise","reject","head","document","querySelector","script","createElement","dataset","tinymce","src","referrerpolicy","addEventListener","err","append"],"mappings":";;;;;;;2BA8BMA,mFAA0BC,uBAGjB,IAAIC,SAAQC,MAAAA,gBAEnBC,QACAC,eACAC,oBACMJ,QAAQK,IAAI,EAClB,yBACA,4BAAkBC,kBAAWC,qBAC7B,uBAAUT,oBAGdI,QAAQM,cAAcC,cAAOH,8BAAoB,IACtCH,uBAILO,eAAeN,aAAaO,QAElCC,QAAQ,WAAIN,6BAAoBO,uBAGhCC,mDAQEJ,eAAkBC,QAChBG,qBAIJA,mBAAqB,IAAId,SAAQ,CAACY,QAASG,gBACjCC,KAAOC,SAASC,cAAc,QAC9BC,OAASF,SAASG,cAAc,UACtCD,OAAOE,QAAQC,QAAU,UACzBH,OAAOI,uCAAkCZ,oCACzCQ,OAAOK,eAAiB,SAExBL,OAAOM,iBAAiB,QAAQ,KAC5Bb,aACD,GAEHO,OAAOM,iBAAiB,SAAUC,MAC9BX,OAAOW,QACR,GAEHV,KAAKW,OAAOR,WAGTL"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/premium/amd/src/common.js b/lib/editor/tiny/plugins/premium/amd/src/common.js new file mode 100644 index 00000000000..3ee35d20c5f --- /dev/null +++ b/lib/editor/tiny/plugins/premium/amd/src/common.js @@ -0,0 +1,27 @@ +// 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 Premium common values. + * + * @module tiny_premium/common + * @copyright 2023 David Woloszyn + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +export default { + pluginName: 'tiny_premium/plugin', + component: 'tiny_premium', +}; diff --git a/lib/editor/tiny/plugins/premium/amd/src/configuration.js b/lib/editor/tiny/plugins/premium/amd/src/configuration.js new file mode 100644 index 00000000000..1b2cfa7e14c --- /dev/null +++ b/lib/editor/tiny/plugins/premium/amd/src/configuration.js @@ -0,0 +1,121 @@ +// 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 Premium configuration. + * + * @module tiny_premium/configuration + * @copyright 2023 David Woloszyn + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import { + addToolbarButton, + addMenubarItem, + addToolbarSection, + addContextmenuItem +} from 'editor_tiny/utils'; + +const configureToolbar = (toolbar) => { + // Add premium toolbar sections to house all the plugins with no natural home. + toolbar = addToolbarSection(toolbar, 'premium_a', 'advanced', true); + toolbar = addToolbarSection(toolbar, 'premium_b', 'formatting', true); + return toolbar; +}; + +export const configure = (instanceConfig) => { + // There is some manipulating of the plugin menu, toolbar, context and quickbar items. + // This was necessary to enhance user experience and closer align to the Tiny demo site. + let plugins = instanceConfig.plugins; + let menu = instanceConfig.menu; + let toolbar = configureToolbar(instanceConfig.toolbar); + let contextmenu = instanceConfig.contextmenu; + let pluginsettings = {}; + + // Advanced Table. + plugins += ` advtable`; + menu = addMenubarItem(menu, 'table', '| advtablerownumbering', 'advtablesort'); + + // Enhanced Image Editing. + plugins += ` editimage`; + toolbar = addToolbarButton(toolbar, 'content', 'editimage', 'tiny_media_image'); + // Remove the duplicate image button from the quickbar toolbar by redefining the values without 'imageoptions'. + // eslint-disable-next-line camelcase + instanceConfig.editimage_toolbar = 'rotateleft rotateright flipv fliph editimage'; + + // Export. + plugins += ` export`; + menu = addMenubarItem(menu, 'tools', '| export'); + + // Page Embed. + plugins += ` pageembed`; + toolbar = addToolbarButton(toolbar, 'content', 'pageembed', 'tiny_media_video'); + + // Advanced Typography. + plugins += ` typography`; + toolbar = addToolbarButton(toolbar, 'premium_b', 'typography'); + + // Case Change. + plugins += ` casechange`; + toolbar = addToolbarButton(toolbar, 'premium_a', 'casechange'); + + // Checklist. + plugins += ` checklist`; + toolbar = addToolbarButton(toolbar, 'lists', 'checklist'); + + // Spell Checker Pro. + plugins += ` tinymcespellchecker`; + menu = addMenubarItem(menu, 'tools', 'spellcheckdialog', 'spellcheckerlanguage'); + contextmenu = addContextmenuItem(contextmenu, 'spellchecker'); + toolbar = addToolbarButton(toolbar, 'premium_a', 'spellcheckdialog'); + + // Spelling Autocorrect. + plugins += ` autocorrect`; + menu = addMenubarItem(menu, 'tools', '| autocorrect capitalization', 'spellcheckdialog'); + + // Permanent Pen. + plugins += ` permanentpen`; + menu = addMenubarItem(menu, 'format', '| permanentpen configurepermanentpen'); + toolbar = addToolbarButton(toolbar, 'premium_a', 'permanentpen'); + contextmenu = addContextmenuItem(contextmenu, 'configurepermanentpen'); + + // Format Painter. + plugins += ` formatpainter`; + toolbar = addToolbarButton(toolbar, 'premium_a', 'formatpainter'); + + // Link Checker. + plugins += ` linkchecker`; + contextmenu = addContextmenuItem(contextmenu, 'linkchecker'); + + // Table of Contents. + plugins += ` tableofcontents`; + toolbar = addToolbarButton(toolbar, 'premium_a', 'tableofcontents'); + + // Footnotes. + plugins += ` footnotes`; + toolbar = addToolbarButton(toolbar, 'premium_a', 'footnotes'); + menu = addMenubarItem(menu, 'insert', 'footnotes', 'tableofcontents'); + + // Powerpaste. + plugins += ` powerpaste`; + + return { + plugins, + toolbar, + menu, + contextmenu, + ...pluginsettings + }; +}; diff --git a/lib/editor/tiny/plugins/premium/amd/src/external.js b/lib/editor/tiny/plugins/premium/amd/src/external.js new file mode 100644 index 00000000000..da1946c3ce2 --- /dev/null +++ b/lib/editor/tiny/plugins/premium/amd/src/external.js @@ -0,0 +1,41 @@ +// 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 to get external content for Tiny Premium plugin. + * + * @module tiny_premium/external + * @copyright 2023 David Woloszyn + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from 'core/ajax'; + +/** + * Due to the order items are initialise, options.js will not work in getting the API key. + * This external method is used to get the API key in time. + * + * @param {Number} contextId The context id + * @return {Promise} + */ +export const getApiKey = (contextId) => { + const request = { + methodname: 'tiny_premium_get_api_key', + args: { + contextid: contextId + } + }; + return Ajax.call([request])[0]; +}; diff --git a/lib/editor/tiny/plugins/premium/amd/src/plugin.js b/lib/editor/tiny/plugins/premium/amd/src/plugin.js new file mode 100644 index 00000000000..db9da575e85 --- /dev/null +++ b/lib/editor/tiny/plugins/premium/amd/src/plugin.js @@ -0,0 +1,87 @@ +// 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 Premium plugin for Moodle. + * + * @module tiny_premium/plugin + * @copyright 2023 David Woloszyn + * @license https://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 'tiny_premium/common'; +import * as Configuration from 'tiny_premium/configuration'; +import {getApiKey} from 'tiny_premium/external'; +import Config from 'core/config'; + +const currentContextId = Config.contextid; + +// eslint-disable-next-line no-async-promise-executor +export default new Promise(async(resolve) => { + const [ + tinyMCE, + pluginMetadata, + externalData + ] = await Promise.all([ + getTinyMCE(), + getPluginMetadata(component, pluginName), + getApiKey(currentContextId) + ]); + + tinyMCE.PluginManager.add(`${component}/plugin`, () => { + return pluginMetadata; + }); + + // Load the Tiny Premium script using the provided API key. + await getTinyPremium(externalData.apikey); + + resolve([`${component}/plugin`, Configuration]); +}); + +let tinyPremiumPromise; + +/** + * Promise for Tiny Premium plugins script. + * + * @param {string} apikey + * @return {Promise} + */ +const getTinyPremium = (apikey) => { + if (tinyPremiumPromise) { + return tinyPremiumPromise; + } + + tinyPremiumPromise = new Promise((resolve, reject) => { + const head = document.querySelector('head'); + const script = document.createElement('script'); + script.dataset.tinymce = 'premium'; + script.src = `https://cdn.tiny.cloud/1/${apikey}/tinymce/6/plugins.min.js`; + script.referrerpolicy = "origin"; + + script.addEventListener('load', () => { + resolve(); + }, false); + + script.addEventListener('error', (err) => { + reject(err); + }, false); + + head.append(script); + }); + + return tinyPremiumPromise; +}; diff --git a/lib/editor/tiny/plugins/premium/classes/external/get_api_key.php b/lib/editor/tiny/plugins/premium/classes/external/get_api_key.php new file mode 100644 index 00000000000..fc3e9a65987 --- /dev/null +++ b/lib/editor/tiny/plugins/premium/classes/external/get_api_key.php @@ -0,0 +1,78 @@ +. + +namespace tiny_premium\external; + +use context; +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_value; + +/** + * External API for Tiny Premium. + * + * @package tiny_premium + * @copyright 2023 David Woloszyn + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_api_key extends external_api { + + /** + * Describes the parameters for Tiny Premium API key. + * + * @return external_function_parameters + * @since Moodle 4.3 + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'contextid' => new external_value(PARAM_INT, 'The current context ID.', VALUE_REQUIRED), + ]); + } + + /** + * External function to get the Tiny Premium API key. + * + * @param int $contextid + * @return array + * @since Moodle 4.3 + */ + public static function execute(int $contextid): array { + [ + 'contextid' => $contextid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'contextid' => $contextid, + ]); + + $context = context::instance_by_id($contextid); + self::validate_context($context); + return [ + 'apikey' => get_config('tiny_premium', 'apikey'), + ]; + } + + /** + * Describes the data returned from the external function. + * + * @return external_single_structure + * @since Moodle 4.3 + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'apikey' => new external_value(PARAM_ALPHANUM, 'The API key for Tiny Premium'), + ]); + } +} diff --git a/lib/editor/tiny/plugins/premium/classes/plugininfo.php b/lib/editor/tiny/plugins/premium/classes/plugininfo.php new file mode 100644 index 00000000000..09b11df295f --- /dev/null +++ b/lib/editor/tiny/plugins/premium/classes/plugininfo.php @@ -0,0 +1,49 @@ +. + +namespace tiny_premium; + +use context; +use editor_tiny\editor; +use editor_tiny\plugin; + +/** + * Tiny Premium plugin. + * + * @package tiny_premium + * @copyright 2023 David Woloszyn + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugininfo extends plugin { + + /** + * Determine if the plugin should be enabled by checking if the Tiny Premium API key is set. + * + * @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 bool + */ + public static function is_enabled( + context $context, + array $options, + array $fpoptions, + ?editor $editor = null + ): bool { + return get_config('tiny_premium', 'apikey') != false; + } +} diff --git a/lib/editor/tiny/plugins/premium/classes/privacy/provider.php b/lib/editor/tiny/plugins/premium/classes/privacy/provider.php new file mode 100644 index 00000000000..984e7adc0c9 --- /dev/null +++ b/lib/editor/tiny/plugins/premium/classes/privacy/provider.php @@ -0,0 +1,37 @@ +. + +namespace tiny_premium\privacy; + +/** + * Privacy Subsystem implementation for the Premium plugin for TinyMCE. + * + * @package tiny_premium + * @copyright 2023 David Woloszyn + * @license https://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/premium/db/services.php b/lib/editor/tiny/plugins/premium/db/services.php new file mode 100644 index 00000000000..b3e9e3ee62f --- /dev/null +++ b/lib/editor/tiny/plugins/premium/db/services.php @@ -0,0 +1,36 @@ +. + +/** + * Tiny Premium external functions and service definitions. + * + * @package tiny_premium + * @copyright 2023 David Woloszyn + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = [ + 'tiny_premium_get_api_key' => [ + 'classname' => 'tiny_premium\external\get_api_key', + 'methodname' => 'execute', + 'description' => 'Get the Tiny Premium API key from Moodle', + 'type' => 'read', + 'capabilities' => '', + 'ajax' => true, + ], +]; diff --git a/lib/editor/tiny/plugins/premium/lang/en/tiny_premium.php b/lib/editor/tiny/plugins/premium/lang/en/tiny_premium.php new file mode 100644 index 00000000000..45291f449fe --- /dev/null +++ b/lib/editor/tiny/plugins/premium/lang/en/tiny_premium.php @@ -0,0 +1,31 @@ +. + +/** + * Strings for component 'tiny_premium', language 'en'. + * + * @package tiny_premium + * @category string + * @copyright 2023 David Woloszyn + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['pluginname'] = 'Tiny Premium'; +$string['apikey'] = 'API Key'; +$string['apikey_desc'] = 'Your Tiny Premium API Key requires a paid subscription. You can obtain your key by accessing your Tiny Cloud account page.'; +$string['privacy:metadata'] = 'The Tiny Premium plugin for TinyMCE does not store any personal data.'; diff --git a/lib/editor/tiny/plugins/premium/readme_moodle.txt b/lib/editor/tiny/plugins/premium/readme_moodle.txt new file mode 100644 index 00000000000..d0afd906ebb --- /dev/null +++ b/lib/editor/tiny/plugins/premium/readme_moodle.txt @@ -0,0 +1,12 @@ +Instructions for updating the Tiny Premium plugin for Moodle. + +A request to Tiny Cloud is made in the plugin.js file of this plugin. +This request passes the Tiny Premium API key as part of a URL. +The URL also contains the major version of Tiny and may need to be updated. + +The URL looks like this: https://cdn.tiny.cloud/1/YOUR_API_KEY/tinymce/6/plugins.min.js + +Notice that the version (6) is baked into the URL and may need revision. + +When upgrading, check Tiny Cloud's documentation regarding the correct API URL +to use. Go to https://www.tiny.cloud/docs/tinymce diff --git a/lib/editor/tiny/plugins/premium/settings.php b/lib/editor/tiny/plugins/premium/settings.php new file mode 100644 index 00000000000..737ed7ceeaf --- /dev/null +++ b/lib/editor/tiny/plugins/premium/settings.php @@ -0,0 +1,39 @@ +. + +/** + * Settings for the Tiny Premium plugin. + * + * @package tiny_premium + * @category admin + * @copyright 2023 David Woloszyn + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($hassiteconfig) { + // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf + if ($ADMIN->fulltree) { + $setting = new admin_setting_configpasswordunmask( + 'tiny_premium/apikey', + get_string('apikey', 'tiny_premium'), + get_string('apikey_desc', 'tiny_premium'), + '', + ); + $settings->add($setting); + } +} diff --git a/lib/editor/tiny/plugins/premium/version.php b/lib/editor/tiny/plugins/premium/version.php new file mode 100644 index 00000000000..de44ec5e9ee --- /dev/null +++ b/lib/editor/tiny/plugins/premium/version.php @@ -0,0 +1,29 @@ +. + +/** + * Plugin version and other meta-data are defined here. + * + * @package tiny_premium + * @copyright 2023 David Woloszyn + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2023082200; +$plugin->requires = 2021051700; +$plugin->component = 'tiny_premium';