From cdbabe8ef895b5280a55645ce6b6e9d458202c64 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 10 Aug 2022 16:22:21 +0800 Subject: [PATCH] MDL-75263 tiny_autosave: Implement Autosave for TinyMCE Part of MDL-75966 --- lib/classes/plugin_manager.php | 1 + 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 | 20 ++ lib/editor/tiny/classes/manager.php | 3 + lib/editor/tiny/classes/privacy/provider.php | 2 +- .../autosave/amd/build/autosaver.min.js | 10 + .../autosave/amd/build/autosaver.min.js.map | 1 + .../plugins/autosave/amd/build/common.min.js | 3 + .../autosave/amd/build/common.min.js.map | 1 + .../plugins/autosave/amd/build/options.min.js | 11 + .../autosave/amd/build/options.min.js.map | 1 + .../plugins/autosave/amd/build/plugin.min.js | 10 + .../autosave/amd/build/plugin.min.js.map | 1 + .../autosave/amd/build/repository.min.js | 10 + .../autosave/amd/build/repository.min.js.map | 1 + .../plugins/autosave/amd/build/storage.min.js | 10 + .../autosave/amd/build/storage.min.js.map | 1 + .../plugins/autosave/amd/src/autosaver.js | 70 +++++ .../tiny/plugins/autosave/amd/src/common.js | 28 ++ .../tiny/plugins/autosave/amd/src/options.js | 77 +++++ .../tiny/plugins/autosave/amd/src/plugin.js | 50 ++++ .../plugins/autosave/amd/src/repository.js | 101 +++++++ .../tiny/plugins/autosave/amd/src/storage.js | 83 ++++++ .../autosave/classes/autosave_manager.php | 267 ++++++++++++++++++ .../external/reset_autosave_session.php | 98 +++++++ .../external/resume_autosave_session.php | 111 ++++++++ .../update_autosave_session_content.php | 102 +++++++ .../plugins/autosave/classes/plugininfo.php | 52 ++++ .../autosave/classes/privacy/provider.php | 237 ++++++++++++++++ .../tiny/plugins/autosave/db/install.xml | 25 ++ .../tiny/plugins/autosave/db/services.php | 49 ++++ .../autosave/lang/en/tiny_autosave.php | 30 ++ lib/editor/tiny/plugins/autosave/version.php | 29 ++ 34 files changed, 1496 insertions(+), 3 deletions(-) create mode 100644 lib/editor/tiny/plugins/autosave/amd/build/autosaver.min.js create mode 100644 lib/editor/tiny/plugins/autosave/amd/build/autosaver.min.js.map create mode 100644 lib/editor/tiny/plugins/autosave/amd/build/common.min.js create mode 100644 lib/editor/tiny/plugins/autosave/amd/build/common.min.js.map create mode 100644 lib/editor/tiny/plugins/autosave/amd/build/options.min.js create mode 100644 lib/editor/tiny/plugins/autosave/amd/build/options.min.js.map create mode 100644 lib/editor/tiny/plugins/autosave/amd/build/plugin.min.js create mode 100644 lib/editor/tiny/plugins/autosave/amd/build/plugin.min.js.map create mode 100644 lib/editor/tiny/plugins/autosave/amd/build/repository.min.js create mode 100644 lib/editor/tiny/plugins/autosave/amd/build/repository.min.js.map create mode 100644 lib/editor/tiny/plugins/autosave/amd/build/storage.min.js create mode 100644 lib/editor/tiny/plugins/autosave/amd/build/storage.min.js.map create mode 100644 lib/editor/tiny/plugins/autosave/amd/src/autosaver.js create mode 100644 lib/editor/tiny/plugins/autosave/amd/src/common.js create mode 100644 lib/editor/tiny/plugins/autosave/amd/src/options.js create mode 100644 lib/editor/tiny/plugins/autosave/amd/src/plugin.js create mode 100644 lib/editor/tiny/plugins/autosave/amd/src/repository.js create mode 100644 lib/editor/tiny/plugins/autosave/amd/src/storage.js create mode 100644 lib/editor/tiny/plugins/autosave/classes/autosave_manager.php create mode 100644 lib/editor/tiny/plugins/autosave/classes/external/reset_autosave_session.php create mode 100644 lib/editor/tiny/plugins/autosave/classes/external/resume_autosave_session.php create mode 100644 lib/editor/tiny/plugins/autosave/classes/external/update_autosave_session_content.php create mode 100644 lib/editor/tiny/plugins/autosave/classes/plugininfo.php create mode 100644 lib/editor/tiny/plugins/autosave/classes/privacy/provider.php create mode 100644 lib/editor/tiny/plugins/autosave/db/install.xml create mode 100644 lib/editor/tiny/plugins/autosave/db/services.php create mode 100644 lib/editor/tiny/plugins/autosave/lang/en/tiny_autosave.php create mode 100644 lib/editor/tiny/plugins/autosave/version.php diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 92079d2fe8b..94d86de5964 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -2029,6 +2029,7 @@ class core_plugin_manager { ), 'tiny' => [ + 'autosave', 'h5p', ], diff --git a/lib/editor/tiny/amd/build/utils.min.js b/lib/editor/tiny/amd/build/utils.min.js index 15d59922064..fb9fb96ac7a 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.getPluginMetadata=_exports.getImagePath=_exports.getDocumentationLink=_exports.getButtonImage=_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)})}}})); +define("editor_tiny/utils",["exports","core/templates","./options","core/str"],(function(_exports,_templates,_options,_str){Object.defineProperty(_exports,"__esModule",{value:!0}),_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=>{var _editor$targetElm;return null!=editor&&null!==(_editor$targetElm=editor.targetElm)&&void 0!==_editor$targetElm&&_editor$targetElm.closest("body")?editor:(editor.destroy(),null)}})); //# 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 a1ad4052c75..89fa8816f7c 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"],"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"],"mappings":"weA0BaA,aAAe,SAACC,gBAAYC,iEAAY,qBAAkBC,QAAQC,QAAQC,EAAEC,KAAKC,UAAUN,WAAYC,wEAEtFM,eAAMP,gBAAYC,iEAAY,qBAAkB,+BAAiB,6BAA8B,CACzHO,YAAaT,aAAaC,WAAYC,yCAYT,CAACQ,OAAQC,WAAa,IAAIR,SAAQ,CAACC,QAASQ,gBACnEC,eAAgB,0BAAcH,OAAQC,aACxCE,qBACMC,QAAU,IACTD,cACHE,aAAcX,SAElBC,EAAEW,gBAAgBC,KAAKC,EAAGJ,cAG9BF,kCAA2BD,wCAWC,CAACQ,QAASC,QAASC,cAC1CF,cACM,CAAC,CACJG,KAAMF,QACNG,MAAO,CAACF,iBAIOG,KAAKC,MAAMD,KAAKE,UAAUP,UAC3BQ,KAAKC,OACnBA,KAAKN,OAASF,SACdQ,KAAKL,MAAMM,KAAKR,QAGbO,oCAYkB,CAACT,QAASC,QAASU,eAC3CX,cACM,CAAC,CACJG,KAAMF,QACNG,MAAOO,iBAIQN,KAAKC,MAAMD,KAAKE,UAAUP,UAC3BQ,KAAKC,OACnBA,KAAKN,OAASF,SACdU,QAAQC,SAAQV,QAAUO,KAAKL,MAAMM,KAAKR,UAGvCO,oCAakB,SAACT,QAASG,KAAMU,gBAAYC,wEACnDC,WAAa,CACfZ,KAAAA,KACAC,MAAO,IAELY,gBAAkBhB,QAAQiB,MAAK,CAAChB,QAASiB,QACvCjB,QAAQE,OAASU,aACbC,OACAd,QAAQmB,OAAOD,MAAQ,EAAG,EAAGH,YAE7Bf,QAAQmB,OAAOD,MAAO,EAAGH,aAEtB,YAKVC,kBAEGF,OACAd,QAAQU,KAAKK,YAEbf,QAAQoB,QAAQL,aAIjBf,iCAWmB,CAACqB,QAASpB,QAASqB,gBACxCD,QAAS,EACW,IACRpB,SAAW,CACpBsB,MAAOtB,QACPG,MAAOkB,gBAITE,eAAiBnB,KAAKC,MAAMD,KAAKE,UAAUc,iBACjDI,MAAMC,KAAKC,OAAOC,QAAQJ,iBAAiBZ,SAAQiB,WAAE1B,KAAM2B,WACnD3B,OAASF,UACT6B,KAAK1B,gBAAW0B,KAAK1B,kBAASkB,cAI/BE,4CAUuB,SAACO,mBACzBC,kBAAoBD,MAAAA,YAAAA,YAAe,IAAIE,MAAM,mCADJC,6DAAAA,yCAGxCF,iBACFG,OAAOD,WACPE,QAAQ3B,MAAkB,KAATA,OACjB4B,KAAK,uCAWyB,SAACrC,gBAG7BA,eASEsC,qBAAwBC,6DAAyDA,0FAW7DlD,eAAMN,UAAWwD,gBAAYC,2DAAM,WAC1DrC,WAAa,mBAAU,eAAgBpB,iBACtC,CACH0D,YAAa,MACTtC,KAAAA,KACAqC,IAAKA,MAAAA,IAAAA,IAAOF,qBAAqBC"} \ No newline at end of file +{"version":3,"file":"utils.min.js","sources":["../src/utils.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {renderForPromise} from 'core/templates';\nimport {getFilePicker} from './options';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Get the image path for the specified image.\n *\n * @param {string} identifier The name of the image\n * @param {string} component The component name\n * @return {string} The image URL path\n */\nexport const getImagePath = (identifier, component = 'editor_tiny') => Promise.resolve(M.util.image_url(identifier, component));\n\nexport const getButtonImage = async(identifier, component = 'editor_tiny') => renderForPromise('editor_tiny/toolbar_button', {\n image: await getImagePath(identifier, component),\n});\n\n/**\n * Helper to display a filepicker and return a Promise.\n *\n * The Promise will resolve when a file is selected, or reject if the file type is not found.\n *\n * @param {TinyMCE} editor\n * @param {string} filetype\n * @returns {Promise} The file object returned by the filepicker\n */\nexport const displayFilepicker = (editor, filetype) => new Promise((resolve, reject) => {\n const configuration = getFilePicker(editor, filetype);\n if (configuration) {\n const options = {\n ...configuration,\n formcallback: resolve,\n };\n M.core_filepicker.show(Y, options);\n return;\n }\n reject(`Unknown filetype ${filetype}`);\n});\n\n/**\n * Given a TinyMCE Toolbar configuration, add the specified button to the named section.\n *\n * @param {object} toolbar\n * @param {string} section\n * @param {string} button\n * @returns {object} The toolbar configuration\n */\nexport const addToolbarButton = (toolbar, section, button) => {\n if (!toolbar) {\n return [{\n name: section,\n items: [button],\n }];\n }\n\n const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));\n return mutatedToolbar.map((item) => {\n if (item.name === section) {\n item.items.push(button);\n }\n\n return item;\n });\n};\n\n/**\n * Given a TinyMCE Toolbar configuration, add the specified buttons to the named section.\n *\n * @param {object} toolbar\n * @param {string} section\n * @param {Array} buttons\n * @returns {object} The toolbar configuration\n */\nexport const addToolbarButtons = (toolbar, section, buttons) => {\n if (!toolbar) {\n return [{\n name: section,\n items: buttons,\n }];\n }\n\n const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));\n return mutatedToolbar.map((item) => {\n if (item.name === section) {\n buttons.forEach(button => item.items.push(button));\n }\n\n return item;\n });\n};\n\n/**\n * Insert a new section into the toolbar.\n *\n * @param {array} toolbar The TinyMCE.editor.settings.toolbar configuration\n * @param {string} name The new section name to add\n * @param {string} relativeTo Insert relative to this section name\n * @param {boolean} append Append or Prepend\n * @returns {array}\n */\nexport const addToolbarSection = (toolbar, name, relativeTo, append = true) => {\n const newSection = {\n name,\n items: [],\n };\n const sectionInserted = toolbar.some((section, index) => {\n if (section.name === relativeTo) {\n if (append) {\n toolbar.splice(index + 1, 0, newSection);\n } else {\n toolbar.splice(index, 0, newSection);\n }\n return true;\n }\n return false;\n });\n\n if (!sectionInserted) {\n // Relative section not found.\n if (append) {\n toolbar.push(newSection);\n } else {\n toolbar.unshift(newSection);\n }\n }\n\n return toolbar;\n};\n\n/**\n * Given a TinyMCE Menubar configuration, add the specified button to the named section.\n *\n * @param {object} menubar\n * @param {string} section\n * @param {string} menuitem\n * @returns {object}\n */\nexport const addMenubarItem = (menubar, section, menuitem) => {\n if (!menubar) {\n const emptyMenubar = {};\n emptyMenubar[section] = {\n title: section,\n items: menuitem,\n };\n }\n\n const mutatedMenubar = JSON.parse(JSON.stringify(menubar));\n Array.from(Object.entries(mutatedMenubar)).forEach(([name, menu]) => {\n if (name === section) {\n menu.items = `${menu.items} ${menuitem}`;\n }\n });\n\n return mutatedMenubar;\n};\n\n/**\n * Given a TinyMCE contextmenu configuration, add the specified button to the end.\n *\n * @param {string} contextmenu\n * @param {string[]} menuitems\n * @returns {string}\n */\nexport const addContextmenuItem = (contextmenu, ...menuitems) => {\n const contextmenuItems = (contextmenu ?? '').split(' ');\n\n return contextmenuItems\n .concat(menuitems)\n .filter((item) => item !== '')\n .join(' ');\n};\n\n/**\n * Given a TinyMCE quickbars configuration, add items to the 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?.targetElm?.closest('body')) {\n editor.destroy();\n return null;\n }\n\n return editor;\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","targetElm","_editor$targetElm","closest","destroy"],"mappings":"qgBA0BaA,aAAe,SAACC,gBAAYC,iEAAY,qBAAkBC,QAAQC,QAAQC,EAAEC,KAAKC,UAAUN,WAAYC,wEAEtFM,eAAMP,gBAAYC,iEAAY,qBAAkB,+BAAiB,6BAA8B,CACzHO,YAAaT,aAAaC,WAAYC,yCAYT,CAACQ,OAAQC,WAAa,IAAIR,SAAQ,CAACC,QAASQ,gBACnEC,eAAgB,0BAAcH,OAAQC,aACxCE,qBACMC,QAAU,IACTD,cACHE,aAAcX,SAElBC,EAAEW,gBAAgBC,KAAKC,EAAGJ,cAG9BF,kCAA2BD,wCAWC,CAACQ,QAASC,QAASC,cAC1CF,cACM,CAAC,CACJG,KAAMF,QACNG,MAAO,CAACF,iBAIOG,KAAKC,MAAMD,KAAKE,UAAUP,UAC3BQ,KAAKC,OACnBA,KAAKN,OAASF,SACdQ,KAAKL,MAAMM,KAAKR,QAGbO,oCAYkB,CAACT,QAASC,QAASU,eAC3CX,cACM,CAAC,CACJG,KAAMF,QACNG,MAAOO,iBAIQN,KAAKC,MAAMD,KAAKE,UAAUP,UAC3BQ,KAAKC,OACnBA,KAAKN,OAASF,SACdU,QAAQC,SAAQV,QAAUO,KAAKL,MAAMM,KAAKR,UAGvCO,oCAakB,SAACT,QAASG,KAAMU,gBAAYC,wEACnDC,WAAa,CACfZ,KAAAA,KACAC,MAAO,IAELY,gBAAkBhB,QAAQiB,MAAK,CAAChB,QAASiB,QACvCjB,QAAQE,OAASU,aACbC,OACAd,QAAQmB,OAAOD,MAAQ,EAAG,EAAGH,YAE7Bf,QAAQmB,OAAOD,MAAO,EAAGH,aAEtB,YAKVC,kBAEGF,OACAd,QAAQU,KAAKK,YAEbf,QAAQoB,QAAQL,aAIjBf,iCAWmB,CAACqB,QAASpB,QAASqB,gBACxCD,QAAS,EACW,IACRpB,SAAW,CACpBsB,MAAOtB,QACPG,MAAOkB,gBAITE,eAAiBnB,KAAKC,MAAMD,KAAKE,UAAUc,iBACjDI,MAAMC,KAAKC,OAAOC,QAAQJ,iBAAiBZ,SAAQiB,WAAE1B,KAAM2B,WACnD3B,OAASF,UACT6B,KAAK1B,gBAAW0B,KAAK1B,kBAASkB,cAI/BE,4CAUuB,SAACO,mBACzBC,kBAAoBD,MAAAA,YAAAA,YAAe,IAAIE,MAAM,mCADJC,6DAAAA,yCAGxCF,iBACFG,OAAOD,WACPE,QAAQ3B,MAAkB,KAATA,OACjB4B,KAAK,uCAWyB,SAACrC,gBAG7BA,eASEsC,qBAAwBC,6DAAyDA,0FAW7DlD,eAAMN,UAAWwD,gBAAYC,2DAAM,WAC1DrC,WAAa,mBAAU,eAAgBpB,iBACtC,CACH0D,YAAa,MACTtC,KAAAA,KACAqC,IAAKA,MAAAA,IAAAA,IAAOF,qBAAqBC,6CAWThD,sCAM3BA,MAAAA,kCAAAA,OAAQmD,wCAARC,kBAAmBC,QAAQ,QAKzBrD,QAJHA,OAAOsD,UACA"} \ 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 8f3df1d1ce3..5b2e20d3e96 100644 --- a/lib/editor/tiny/amd/src/utils.js +++ b/lib/editor/tiny/amd/src/utils.js @@ -225,3 +225,23 @@ export const getPluginMetadata = async(component, pluginName, url = null) => { }), }; }; + +/** + * Ensure that the editor is still in the DOM, removing it if it is not. + * + * @param {TinyMCE} editor + * @returns {TinyMCE|null} + */ +export const ensureEditorIsValid = (editor) => { + // TinyMCE uses the element ID as a map key internally, even if the target has changed. + // In cases such as where an editor is in a modal form which has been detached from the DOM, but the editor not removed, + // we need to manually destroy the editor. + // We could theoretically do this with a Mutation Observer, but in some cases the Node may be moved, + // or added back elsewhere in the DOM. + if (!editor?.targetElm?.closest('body')) { + editor.destroy(); + return null; + } + + return editor; +}; diff --git a/lib/editor/tiny/classes/manager.php b/lib/editor/tiny/classes/manager.php index 8f733001581..f2690a78d89 100644 --- a/lib/editor/tiny/classes/manager.php +++ b/lib/editor/tiny/classes/manager.php @@ -491,6 +491,9 @@ class manager { 'image', 'media', + // Use the Moodle autosave plugin instead. + 'autosave', + // Disable the Template plugin for now. 'template', ]; diff --git a/lib/editor/tiny/classes/privacy/provider.php b/lib/editor/tiny/classes/privacy/provider.php index 0e06cdf9374..65d62f45b40 100644 --- a/lib/editor/tiny/classes/privacy/provider.php +++ b/lib/editor/tiny/classes/privacy/provider.php @@ -31,6 +31,6 @@ class provider implements \core_privacy\local\metadata\null_provider { * @return string */ public static function get_reason(): string { - return 'privacy:metadata'; + return 'privacy:reason'; } } diff --git a/lib/editor/tiny/plugins/autosave/amd/build/autosaver.min.js b/lib/editor/tiny/plugins/autosave/amd/build/autosaver.min.js new file mode 100644 index 00000000000..2cdfc6ab0b3 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/build/autosaver.min.js @@ -0,0 +1,10 @@ +define("tiny_autosave/autosaver",["exports","./options","./storage","core/log","./common"],(function(_exports,Options,Storage,_log,_common){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)}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} +/** + * Storage helper for the Moodle Tiny Autosave plugin. + * + * @module tiny_autosave/plugin + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=void 0,Options=_interopRequireWildcard(Options),Storage=_interopRequireWildcard(Storage),_log=(obj=_log)&&obj.__esModule?obj:{default:obj};_exports.register=editor=>{const undoHandler=()=>{editor.undoManager.hasUndo()?Storage.saveDraft(editor):_log.default.debug("Ignoring undo event as there is no undo history",(0,_common.getLogSource)(editor))},visibilityChangedHandler=()=>{"hidden"===document.visibilityState&&Options.isInitialised(editor)&&Storage.saveDraft(editor)};document.addEventListener("visibilitychange",visibilityChangedHandler),editor.on("submit",(()=>{document.removeEventListener("visibilitychange",visibilityChangedHandler),Storage.removeAutosaveSession(editor)})),editor.on("init",(()=>{editor.on("AddUndo",undoHandler),editor.dom.isEmpty(editor.getBody())?(_log.default.info("Attempting to restore draft",(0,_common.getLogSource)(editor)),Storage.restoreDraft(editor)):(_log.default.warn("Skipping draft restoration. The editor is not empty.",(0,_common.getLogSource)(editor)),Options.markInitialised(editor))}))}})); + +//# sourceMappingURL=autosaver.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/autosave/amd/build/autosaver.min.js.map b/lib/editor/tiny/plugins/autosave/amd/build/autosaver.min.js.map new file mode 100644 index 00000000000..2dfcd385103 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/build/autosaver.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"autosaver.min.js","sources":["../src/autosaver.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 * Storage helper for the Moodle Tiny Autosave plugin.\n *\n * @module tiny_autosave/plugin\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Options from './options';\nimport * as Storage from './storage';\nimport Log from 'core/log';\nimport {getLogSource} from './common';\n\nexport const register = (editor) => {\n const undoHandler = () => {\n if (!editor.undoManager.hasUndo()) {\n Log.debug(`Ignoring undo event as there is no undo history`, getLogSource(editor));\n return;\n }\n Storage.saveDraft(editor);\n };\n\n const visibilityChangedHandler = () => {\n if (document.visibilityState === 'hidden') {\n if (Options.isInitialised(editor)) {\n Storage.saveDraft(editor);\n }\n }\n };\n\n // Attempt to store the draft one final time before the page unloads.\n // Note: This may need to be sent as a beacon instead.\n document.addEventListener('visibilitychange', visibilityChangedHandler);\n\n // When the page is submitted as a form, remove the draft.\n editor.on('submit', () => {\n document.removeEventListener('visibilitychange', visibilityChangedHandler);\n Storage.removeAutosaveSession(editor);\n });\n\n\n editor.on('init', () => {\n // Setup the Undo handler.\n editor.on('AddUndo', undoHandler);\n\n if (editor.dom.isEmpty(editor.getBody())) {\n Log.info(`Attempting to restore draft`, getLogSource(editor));\n Storage.restoreDraft(editor);\n } else {\n // There was nothing to restore, so we can mark the editor as initialised.\n Log.warn(`Skipping draft restoration. The editor is not empty.`, getLogSource(editor));\n Options.markInitialised(editor);\n }\n });\n};\n"],"names":["editor","undoHandler","undoManager","hasUndo","Storage","saveDraft","debug","visibilityChangedHandler","document","visibilityState","Options","isInitialised","addEventListener","on","removeEventListener","removeAutosaveSession","dom","isEmpty","getBody","info","restoreDraft","warn","markInitialised"],"mappings":";;;;;;;4OA4ByBA,eACfC,YAAc,KACXD,OAAOE,YAAYC,UAIxBC,QAAQC,UAAUL,qBAHVM,yDAAyD,wBAAaN,UAM5EO,yBAA2B,KACI,WAA7BC,SAASC,iBACLC,QAAQC,cAAcX,SACtBI,QAAQC,UAAUL,SAO9BQ,SAASI,iBAAiB,mBAAoBL,0BAG9CP,OAAOa,GAAG,UAAU,KAChBL,SAASM,oBAAoB,mBAAoBP,0BACjDH,QAAQW,sBAAsBf,WAIlCA,OAAOa,GAAG,QAAQ,KAEdb,OAAOa,GAAG,UAAWZ,aAEjBD,OAAOgB,IAAIC,QAAQjB,OAAOkB,yBACtBC,oCAAoC,wBAAanB,SACrDI,QAAQgB,aAAapB,uBAGjBqB,6DAA6D,wBAAarB,SAC9EU,QAAQY,gBAAgBtB"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/autosave/amd/build/common.min.js b/lib/editor/tiny/plugins/autosave/amd/build/common.min.js new file mode 100644 index 00000000000..f481cbf2c06 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/build/common.min.js @@ -0,0 +1,3 @@ +define("tiny_autosave/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={component:"tiny_autosave",pluginName:"tiny_autosave/plugin",getLogSource:editor=>"tiny_autosave/".concat(editor.id)},_exports.default})); + +//# sourceMappingURL=common.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/autosave/amd/build/common.min.js.map b/lib/editor/tiny/plugins/autosave/amd/build/common.min.js.map new file mode 100644 index 00000000000..4d851b9913f --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/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 Autosave plugin for Moodle.\n *\n * @module tiny_autosave/plugin\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n component: 'tiny_autosave',\n pluginName: 'tiny_autosave/plugin',\n getLogSource: (editor) => `tiny_autosave/${editor.id}`,\n};\n"],"names":["component","pluginName","getLogSource","editor","id"],"mappings":"sKAuBe,CACXA,UAAW,gBACXC,WAAY,uBACZC,aAAeC,gCAA4BA,OAAOC"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/autosave/amd/build/options.min.js b/lib/editor/tiny/plugins/autosave/amd/build/options.min.js new file mode 100644 index 00000000000..d2a1a503775 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/build/options.min.js @@ -0,0 +1,11 @@ +define("tiny_autosave/options",["exports","./common","editor_tiny/options","editor_tiny/utils"],(function(_exports,_common,_options,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getBackoffTime=void 0,Object.defineProperty(_exports,"getContextId",{enumerable:!0,get:function(){return _options.getContextId}}),Object.defineProperty(_exports,"getDraftItemId",{enumerable:!0,get:function(){return _options.getDraftItemId}}),_exports.register=_exports.markInitialised=_exports.isInitialised=_exports.getPageInstance=_exports.getPageHash=void 0; +/** + * Options helper for the Moodle Tiny Autosave plugin. + * + * @module tiny_autosave/plugin + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const initialisedOptionName=(0,_options.getPluginOptionName)(_common.pluginName,"initialised"),pageHashName=(0,_options.getPluginOptionName)(_common.pluginName,"pagehash"),pageInstanceName=(0,_options.getPluginOptionName)(_common.pluginName,"pageinstance"),backoffTime=(0,_options.getPluginOptionName)(_common.pluginName,"backoffTime");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(initialisedOptionName,{processor:"boolean",default:!1}),registerOption(pageHashName,{processor:"string",default:""}),registerOption(pageInstanceName,{processor:"string",default:""}),registerOption(pageInstanceName,{processor:"string",default:""}),registerOption(backoffTime,{processor:"number",default:500})};_exports.isInitialised=editor=>!!(0,_utils.ensureEditorIsValid)(editor)&&editor.options.get(initialisedOptionName);_exports.markInitialised=editor=>editor.options.set(initialisedOptionName,!0);_exports.getPageHash=editor=>editor.options.get(pageHashName);_exports.getPageInstance=editor=>editor.options.get(pageInstanceName);_exports.getBackoffTime=editor=>editor.options.get(backoffTime)})); + +//# sourceMappingURL=options.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/autosave/amd/build/options.min.js.map b/lib/editor/tiny/plugins/autosave/amd/build/options.min.js.map new file mode 100644 index 00000000000..d4dcde6e985 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/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 the Moodle Tiny Autosave plugin.\n *\n * @module tiny_autosave/plugin\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {pluginName} from './common';\nimport {\n getContextId,\n getDraftItemId,\n getPluginOptionName,\n} from 'editor_tiny/options';\nimport {ensureEditorIsValid} from 'editor_tiny/utils';\n\nconst initialisedOptionName = getPluginOptionName(pluginName, 'initialised');\nconst pageHashName = getPluginOptionName(pluginName, 'pagehash');\nconst pageInstanceName = getPluginOptionName(pluginName, 'pageinstance');\nconst backoffTime = getPluginOptionName(pluginName, 'backoffTime');\n\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n registerOption(initialisedOptionName, {\n processor: 'boolean',\n \"default\": false,\n });\n\n registerOption(pageHashName, {\n processor: 'string',\n \"default\": '',\n });\n\n registerOption(pageInstanceName, {\n processor: 'string',\n \"default\": '',\n });\n registerOption(pageInstanceName, {\n processor: 'string',\n \"default\": '',\n });\n registerOption(backoffTime, {\n processor: 'number',\n \"default\": 500,\n });\n};\n\nexport const isInitialised = (editor) => {\n if (!ensureEditorIsValid(editor)) {\n return false;\n }\n\n return editor.options.get(initialisedOptionName);\n};\nexport const markInitialised = (editor) => editor.options.set(initialisedOptionName, true);\nexport const getPageHash = (editor) => editor.options.get(pageHashName);\nexport const getPageInstance = (editor) => editor.options.get(pageInstanceName);\nexport const getBackoffTime = (editor) => editor.options.get(backoffTime);\nexport {\n getContextId,\n getDraftItemId,\n};\n"],"names":["initialisedOptionName","pluginName","pageHashName","pageInstanceName","backoffTime","editor","registerOption","options","register","processor","get","set"],"mappings":";;;;;;;;MA+BMA,uBAAwB,gCAAoBC,mBAAY,eACxDC,cAAe,gCAAoBD,mBAAY,YAC/CE,kBAAmB,gCAAoBF,mBAAY,gBACnDG,aAAc,gCAAoBH,mBAAY,iCAE3BI,eACfC,eAAiBD,OAAOE,QAAQC,SACtCF,eAAeN,sBAAuB,CAClCS,UAAW,mBACA,IAGfH,eAAeJ,aAAc,CACzBO,UAAW,iBACA,KAGfH,eAAeH,iBAAkB,CAC7BM,UAAW,iBACA,KAEfH,eAAeH,iBAAkB,CAC7BM,UAAW,iBACA,KAEfH,eAAeF,YAAa,CACxBK,UAAW,iBACA,8BAIWJ,WACrB,8BAAoBA,SAIlBA,OAAOE,QAAQG,IAAIV,gDAEEK,QAAWA,OAAOE,QAAQI,IAAIX,uBAAuB,wBACzDK,QAAWA,OAAOE,QAAQG,IAAIR,uCAC1BG,QAAWA,OAAOE,QAAQG,IAAIP,0CAC/BE,QAAWA,OAAOE,QAAQG,IAAIN"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/autosave/amd/build/plugin.min.js b/lib/editor/tiny/plugins/autosave/amd/build/plugin.min.js new file mode 100644 index 00000000000..cab4f99fe92 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/build/plugin.min.js @@ -0,0 +1,10 @@ +define("tiny_autosave/plugin",["exports","editor_tiny/loader","editor_tiny/utils","./common","./options","./autosaver"],(function(_exports,_loader,_utils,_common,Options,Autosaver){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 Autosave plugin for Moodle. + * + * @module tiny_autosave/plugin + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Options=_interopRequireWildcard(Options),Autosaver=_interopRequireWildcard(Autosaver);var _default=new Promise((async resolve=>{const[tinyMCE,pluginMetadata]=await Promise.all([(0,_loader.getTinyMCE)(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName)]);tinyMCE.PluginManager.add(_common.pluginName,(editor=>(Options.register(editor),Autosaver.register(editor),pluginMetadata))),resolve(_common.pluginName)}));return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=plugin.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/autosave/amd/build/plugin.min.js.map b/lib/editor/tiny/plugins/autosave/amd/build/plugin.min.js.map new file mode 100644 index 00000000000..6b9d9e5335c --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/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 Autosave plugin for Moodle.\n *\n * @module tiny_autosave/plugin\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {getTinyMCE} from 'editor_tiny/loader';\nimport {getPluginMetadata} from 'editor_tiny/utils';\n\nimport {component, pluginName} from './common';\nimport * as Options from './options';\nimport * as Autosaver from './autosaver';\n\n// Setup the autosave Plugin.\nexport default new Promise(async(resolve) => {\n const [tinyMCE, pluginMetadata] = await Promise.all([\n getTinyMCE(),\n getPluginMetadata(component, pluginName),\n ]);\n\n // Note: The PluginManager.add function does not accept a Promise.\n // Any asynchronous code must be run before this point.\n tinyMCE.PluginManager.add(pluginName, (editor) => {\n // Register options.\n Options.register(editor);\n\n // Register the Autosaver.\n Autosaver.register(editor);\n\n return pluginMetadata;\n });\n\n resolve(pluginName);\n});\n"],"names":["Promise","async","tinyMCE","pluginMetadata","all","component","pluginName","PluginManager","add","editor","Options","register","Autosaver","resolve"],"mappings":";;;;;;;wLA8Be,IAAIA,SAAQC,MAAAA,gBAChBC,QAASC,sBAAwBH,QAAQI,IAAI,EAChD,yBACA,4BAAkBC,kBAAWC,sBAKjCJ,QAAQK,cAAcC,IAAIF,oBAAaG,SAEnCC,QAAQC,SAASF,QAGjBG,UAAUD,SAASF,QAEZN,kBAGXU,QAAQP"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/autosave/amd/build/repository.min.js b/lib/editor/tiny/plugins/autosave/amd/build/repository.min.js new file mode 100644 index 00000000000..7a6c3cc9e52 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/build/repository.min.js @@ -0,0 +1,10 @@ +define("tiny_autosave/repository",["exports","core/ajax","./options","core/pending","editor_tiny/utils"],(function(_exports,_ajax,Options,_pending,_utils){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.updateAutosaveSession=_exports.resumeAutosaveSession=_exports.removeAutosaveSession=void 0,Options=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} +/** + * Repository helper for the Moodle Tiny Autosave plugin. + * + * @module tiny_autosave/repository + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */(Options),_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};const fetchOne=(methodname,args)=>(0,_ajax.call)([{methodname:methodname,args:args}])[0];_exports.resumeAutosaveSession=editor=>{if(!(0,_utils.ensureEditorIsValid)(editor))return Promise.reject("Invalid editor");const pendingPromise=new _pending.default("tiny_autosave/repository:resumeAutosaveSession");return fetchOne("tiny_autosave_resume_session",{contextid:Options.getContextId(editor),pagehash:Options.getPageHash(editor),pageinstance:Options.getPageInstance(editor),elementid:editor.targetElm.id,draftid:Options.getDraftItemId(editor)}).then((result=>(pendingPromise.resolve(),result)))};_exports.updateAutosaveSession=editor=>{if(!(0,_utils.ensureEditorIsValid)(editor))return Promise.reject("Invalid editor");const pendingPromise=new _pending.default("tiny_autosave/repository:updateAutosaveSession");return fetchOne("tiny_autosave_update_session",{contextid:Options.getContextId(editor),pagehash:Options.getPageHash(editor),pageinstance:Options.getPageInstance(editor),elementid:editor.targetElm.id,drafttext:editor.getContent()}).then((result=>(pendingPromise.resolve(),result)))};_exports.removeAutosaveSession=editor=>(0,_utils.ensureEditorIsValid)(editor)?fetchOne("tiny_autosave_reset_session",{contextid:Options.getContextId(editor),pagehash:Options.getPageHash(editor),pageinstance:Options.getPageInstance(editor),elementid:editor.targetElm.id}):Promise.reject("Invalid editor")})); + +//# sourceMappingURL=repository.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/autosave/amd/build/repository.min.js.map b/lib/editor/tiny/plugins/autosave/amd/build/repository.min.js.map new file mode 100644 index 00000000000..ad77d6d6786 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/build/repository.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"repository.min.js","sources":["../src/repository.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 * Repository helper for the Moodle Tiny Autosave plugin.\n *\n * @module tiny_autosave/repository\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call} from 'core/ajax';\nimport * as Options from './options';\nimport Pending from 'core/pending';\nimport {ensureEditorIsValid} from 'editor_tiny/utils';\n\nconst fetchOne = (methodname, args) => call([{\n methodname,\n args,\n}])[0];\n\n/**\n * Resume an Autosave session.\n *\n * @param {TinyMCE} editor The TinyMCE editor instance\n * @returns {Promise} The Autosave session\n */\nexport const resumeAutosaveSession = (editor) => {\n if (!ensureEditorIsValid(editor)) {\n return Promise.reject('Invalid editor');\n }\n const pendingPromise = new Pending('tiny_autosave/repository:resumeAutosaveSession');\n return fetchOne('tiny_autosave_resume_session', {\n contextid: Options.getContextId(editor),\n pagehash: Options.getPageHash(editor),\n pageinstance: Options.getPageInstance(editor),\n elementid: editor.targetElm.id,\n draftid: Options.getDraftItemId(editor),\n })\n .then((result) => {\n pendingPromise.resolve();\n return result;\n });\n};\n\n/**\n * Update the content of the Autosave session.\n *\n * @param {TinyMCE} editor The TinyMCE editor instance\n * @returns {Promise} The Autosave session\n */\nexport const updateAutosaveSession = (editor) => {\n if (!ensureEditorIsValid(editor)) {\n return Promise.reject('Invalid editor');\n }\n\n const pendingPromise = new Pending('tiny_autosave/repository:updateAutosaveSession');\n\n return fetchOne('tiny_autosave_update_session', {\n contextid: Options.getContextId(editor),\n pagehash: Options.getPageHash(editor),\n pageinstance: Options.getPageInstance(editor),\n elementid: editor.targetElm.id,\n drafttext: editor.getContent(),\n })\n .then((result) => {\n pendingPromise.resolve();\n return result;\n });\n};\n\n/**\n * Remove the Autosave session.\n *\n * @param {TinyMCE} editor The TinyMCE editor instance\n * @returns {Promise} The Autosave session\n */\nexport const removeAutosaveSession = (editor) => {\n if (!ensureEditorIsValid(editor)) {\n return Promise.reject('Invalid editor');\n }\n\n return fetchOne('tiny_autosave_reset_session', {\n contextid: Options.getContextId(editor),\n pagehash: Options.getPageHash(editor),\n pageinstance: Options.getPageInstance(editor),\n elementid: editor.targetElm.id,\n });\n};\n"],"names":["fetchOne","methodname","args","editor","Promise","reject","pendingPromise","Pending","contextid","Options","getContextId","pagehash","getPageHash","pageinstance","getPageInstance","elementid","targetElm","id","draftid","getDraftItemId","then","result","resolve","drafttext","getContent"],"mappings":";;;;;;;+EA4BMA,SAAW,CAACC,WAAYC,QAAS,cAAK,CAAC,CACzCD,WAAAA,WACAC,KAAAA,QACA,kCAQkCC,cAC7B,8BAAoBA,eACdC,QAAQC,OAAO,wBAEpBC,eAAiB,IAAIC,iBAAQ,yDAC5BP,SAAS,+BAAgC,CAC5CQ,UAAWC,QAAQC,aAAaP,QAChCQ,SAAUF,QAAQG,YAAYT,QAC9BU,aAAcJ,QAAQK,gBAAgBX,QACtCY,UAAWZ,OAAOa,UAAUC,GAC5BC,QAAST,QAAQU,eAAehB,UAEnCiB,MAAMC,SACHf,eAAegB,UACRD,0CAUuBlB,cAC7B,8BAAoBA,eACdC,QAAQC,OAAO,wBAGpBC,eAAiB,IAAIC,iBAAQ,yDAE5BP,SAAS,+BAAgC,CAC5CQ,UAAWC,QAAQC,aAAaP,QAChCQ,SAAUF,QAAQG,YAAYT,QAC9BU,aAAcJ,QAAQK,gBAAgBX,QACtCY,UAAWZ,OAAOa,UAAUC,GAC5BM,UAAWpB,OAAOqB,eAErBJ,MAAMC,SACHf,eAAegB,UACRD,0CAUuBlB,SAC7B,8BAAoBA,QAIlBH,SAAS,8BAA+B,CAC3CQ,UAAWC,QAAQC,aAAaP,QAChCQ,SAAUF,QAAQG,YAAYT,QAC9BU,aAAcJ,QAAQK,gBAAgBX,QACtCY,UAAWZ,OAAOa,UAAUC,KAPrBb,QAAQC,OAAO"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/autosave/amd/build/storage.min.js b/lib/editor/tiny/plugins/autosave/amd/build/storage.min.js new file mode 100644 index 00000000000..17856f30625 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/build/storage.min.js @@ -0,0 +1,10 @@ +define("tiny_autosave/storage",["exports","./repository","core/pending","./options","core/log","./common"],(function(_exports,Repository,_pending,_options,_log,_common){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default: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.saveDraft=_exports.restoreDraft=_exports.removeAutosaveSession=void 0,Repository=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} +/** + * Storage helper for the Moodle Tiny Autosave plugin. + * + * @module tiny_autosave/plugin + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */(Repository),_pending=_interopRequireDefault(_pending),_log=_interopRequireDefault(_log);const saveDebounceMap=new Map;_exports.restoreDraft=async editor=>{const pendingPromise=new _pending.default("tiny_autosave/restoreDraft");try{const session=await Repository.resumeAutosaveSession(editor);session&&session.drafttext&&editor.undoManager.ignore((()=>{editor.setContent(session.drafttext),editor.save()}))}catch(error){_log.default.warn("Failed to restore draft: ".concat(error),(0,_common.getLogSource)(editor))}(0,_options.markInitialised)(editor),pendingPromise.resolve()};_exports.saveDraft=editor=>{const timerId=saveDebounceMap.get(editor);timerId&&clearTimeout(timerId),saveDebounceMap.set(editor,setTimeout((()=>{_log.default.debug("Saving draft",(0,_common.getLogSource)(editor)),Repository.updateAutosaveSession(editor)}),(0,_options.getBackoffTime)(editor)))};_exports.removeAutosaveSession=editor=>{_log.default.debug("Removing Autosave session",(0,_common.getLogSource)(editor)),Repository.removeAutosaveSession(editor)}})); + +//# sourceMappingURL=storage.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/autosave/amd/build/storage.min.js.map b/lib/editor/tiny/plugins/autosave/amd/build/storage.min.js.map new file mode 100644 index 00000000000..b6454708020 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/build/storage.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"storage.min.js","sources":["../src/storage.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 * Storage helper for the Moodle Tiny Autosave plugin.\n *\n * @module tiny_autosave/plugin\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Repository from \"./repository\";\nimport Pending from 'core/pending';\nimport {\n markInitialised,\n getBackoffTime,\n} from \"./options\";\nimport Log from 'core/log';\nimport {getLogSource} from './common';\n\n/** @property {Map} A map of debounced draft saves */\nconst saveDebounceMap = new Map();\n\n/**\n * Attempt to restore a draft into the editor\n *\n * @param {TinyMCE} editor The Editor to restore a draft for\n */\nexport const restoreDraft = async(editor) => {\n const pendingPromise = new Pending('tiny_autosave/restoreDraft');\n try {\n const session = await Repository.resumeAutosaveSession(editor);\n if (session && session.drafttext) {\n editor.undoManager.ignore(() => {\n editor.setContent(session.drafttext);\n editor.save();\n });\n }\n } catch (error) {\n // Ignore any errors as drafts are optional.\n Log.warn(`Failed to restore draft: ${error}`, getLogSource(editor));\n }\n markInitialised(editor);\n pendingPromise.resolve();\n};\n\n/**\n * Save the current content of the editor as a draft.\n *\n * @param {TinyMCE} editor\n */\nexport const saveDraft = (editor) => {\n const timerId = saveDebounceMap.get(editor);\n if (timerId) {\n clearTimeout(timerId);\n }\n saveDebounceMap.set(editor, setTimeout(() => {\n Log.debug(`Saving draft`, getLogSource(editor));\n Repository.updateAutosaveSession(editor);\n }, getBackoffTime(editor)));\n};\n\n/**\n * Delete the draft for the current editor.\n *\n * @param {TinyMCE} editor\n */\nexport const removeAutosaveSession = (editor) => {\n Log.debug(`Removing Autosave session`, getLogSource(editor));\n Repository.removeAutosaveSession(editor);\n};\n"],"names":["saveDebounceMap","Map","async","pendingPromise","Pending","session","Repository","resumeAutosaveSession","editor","drafttext","undoManager","ignore","setContent","save","error","warn","resolve","timerId","get","clearTimeout","set","setTimeout","debug","updateAutosaveSession","removeAutosaveSession"],"mappings":";;;;;;;oGAiCMA,gBAAkB,IAAIC,0BAOAC,MAAAA,eAClBC,eAAiB,IAAIC,iBAAQ,wCAEzBC,cAAgBC,WAAWC,sBAAsBC,QACnDH,SAAWA,QAAQI,WACnBD,OAAOE,YAAYC,QAAO,KACtBH,OAAOI,WAAWP,QAAQI,WAC1BD,OAAOK,UAGjB,MAAOC,oBAEDC,wCAAiCD,QAAS,wBAAaN,sCAE/CA,QAChBL,eAAea,8BAQOR,eAChBS,QAAUjB,gBAAgBkB,IAAIV,QAChCS,SACAE,aAAaF,SAEjBjB,gBAAgBoB,IAAIZ,OAAQa,YAAW,kBAC/BC,sBAAsB,wBAAad,SACvCF,WAAWiB,sBAAsBf,WAClC,2BAAeA,0CAQgBA,sBAC9Bc,mCAAmC,wBAAad,SACpDF,WAAWkB,sBAAsBhB"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/autosave/amd/src/autosaver.js b/lib/editor/tiny/plugins/autosave/amd/src/autosaver.js new file mode 100644 index 00000000000..daeb9cc66be --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/src/autosaver.js @@ -0,0 +1,70 @@ +// 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 . + +/** + * Storage helper for the Moodle Tiny Autosave plugin. + * + * @module tiny_autosave/plugin + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import * as Options from './options'; +import * as Storage from './storage'; +import Log from 'core/log'; +import {getLogSource} from './common'; + +export const register = (editor) => { + const undoHandler = () => { + if (!editor.undoManager.hasUndo()) { + Log.debug(`Ignoring undo event as there is no undo history`, getLogSource(editor)); + return; + } + Storage.saveDraft(editor); + }; + + const visibilityChangedHandler = () => { + if (document.visibilityState === 'hidden') { + if (Options.isInitialised(editor)) { + Storage.saveDraft(editor); + } + } + }; + + // Attempt to store the draft one final time before the page unloads. + // Note: This may need to be sent as a beacon instead. + document.addEventListener('visibilitychange', visibilityChangedHandler); + + // When the page is submitted as a form, remove the draft. + editor.on('submit', () => { + document.removeEventListener('visibilitychange', visibilityChangedHandler); + Storage.removeAutosaveSession(editor); + }); + + + editor.on('init', () => { + // Setup the Undo handler. + editor.on('AddUndo', undoHandler); + + if (editor.dom.isEmpty(editor.getBody())) { + Log.info(`Attempting to restore draft`, getLogSource(editor)); + Storage.restoreDraft(editor); + } else { + // There was nothing to restore, so we can mark the editor as initialised. + Log.warn(`Skipping draft restoration. The editor is not empty.`, getLogSource(editor)); + Options.markInitialised(editor); + } + }); +}; diff --git a/lib/editor/tiny/plugins/autosave/amd/src/common.js b/lib/editor/tiny/plugins/autosave/amd/src/common.js new file mode 100644 index 00000000000..0c90e4c7ef1 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/src/common.js @@ -0,0 +1,28 @@ +// 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 Autosave plugin for Moodle. + * + * @module tiny_autosave/plugin + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +export default { + component: 'tiny_autosave', + pluginName: 'tiny_autosave/plugin', + getLogSource: (editor) => `tiny_autosave/${editor.id}`, +}; diff --git a/lib/editor/tiny/plugins/autosave/amd/src/options.js b/lib/editor/tiny/plugins/autosave/amd/src/options.js new file mode 100644 index 00000000000..054784c9f2a --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/src/options.js @@ -0,0 +1,77 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Options helper for the Moodle Tiny Autosave plugin. + * + * @module tiny_autosave/plugin + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {pluginName} from './common'; +import { + getContextId, + getDraftItemId, + getPluginOptionName, +} from 'editor_tiny/options'; +import {ensureEditorIsValid} from 'editor_tiny/utils'; + +const initialisedOptionName = getPluginOptionName(pluginName, 'initialised'); +const pageHashName = getPluginOptionName(pluginName, 'pagehash'); +const pageInstanceName = getPluginOptionName(pluginName, 'pageinstance'); +const backoffTime = getPluginOptionName(pluginName, 'backoffTime'); + +export const register = (editor) => { + const registerOption = editor.options.register; + registerOption(initialisedOptionName, { + processor: 'boolean', + "default": false, + }); + + registerOption(pageHashName, { + processor: 'string', + "default": '', + }); + + registerOption(pageInstanceName, { + processor: 'string', + "default": '', + }); + registerOption(pageInstanceName, { + processor: 'string', + "default": '', + }); + registerOption(backoffTime, { + processor: 'number', + "default": 500, + }); +}; + +export const isInitialised = (editor) => { + if (!ensureEditorIsValid(editor)) { + return false; + } + + return editor.options.get(initialisedOptionName); +}; +export const markInitialised = (editor) => editor.options.set(initialisedOptionName, true); +export const getPageHash = (editor) => editor.options.get(pageHashName); +export const getPageInstance = (editor) => editor.options.get(pageInstanceName); +export const getBackoffTime = (editor) => editor.options.get(backoffTime); +export { + getContextId, + getDraftItemId, +}; diff --git a/lib/editor/tiny/plugins/autosave/amd/src/plugin.js b/lib/editor/tiny/plugins/autosave/amd/src/plugin.js new file mode 100644 index 00000000000..868201d1b5b --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/src/plugin.js @@ -0,0 +1,50 @@ +// 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 Autosave plugin for Moodle. + * + * @module tiny_autosave/plugin + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import {getTinyMCE} from 'editor_tiny/loader'; +import {getPluginMetadata} from 'editor_tiny/utils'; + +import {component, pluginName} from './common'; +import * as Options from './options'; +import * as Autosaver from './autosaver'; + +// Setup the autosave Plugin. +export default new Promise(async(resolve) => { + const [tinyMCE, pluginMetadata] = await Promise.all([ + getTinyMCE(), + getPluginMetadata(component, pluginName), + ]); + + // Note: The PluginManager.add function does not accept a Promise. + // Any asynchronous code must be run before this point. + tinyMCE.PluginManager.add(pluginName, (editor) => { + // Register options. + Options.register(editor); + + // Register the Autosaver. + Autosaver.register(editor); + + return pluginMetadata; + }); + + resolve(pluginName); +}); diff --git a/lib/editor/tiny/plugins/autosave/amd/src/repository.js b/lib/editor/tiny/plugins/autosave/amd/src/repository.js new file mode 100644 index 00000000000..dd9dc68fc6a --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/src/repository.js @@ -0,0 +1,101 @@ +// 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 . + +/** + * Repository helper for the Moodle Tiny Autosave plugin. + * + * @module tiny_autosave/repository + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {call} from 'core/ajax'; +import * as Options from './options'; +import Pending from 'core/pending'; +import {ensureEditorIsValid} from 'editor_tiny/utils'; + +const fetchOne = (methodname, args) => call([{ + methodname, + args, +}])[0]; + +/** + * Resume an Autosave session. + * + * @param {TinyMCE} editor The TinyMCE editor instance + * @returns {Promise} The Autosave session + */ +export const resumeAutosaveSession = (editor) => { + if (!ensureEditorIsValid(editor)) { + return Promise.reject('Invalid editor'); + } + const pendingPromise = new Pending('tiny_autosave/repository:resumeAutosaveSession'); + return fetchOne('tiny_autosave_resume_session', { + contextid: Options.getContextId(editor), + pagehash: Options.getPageHash(editor), + pageinstance: Options.getPageInstance(editor), + elementid: editor.targetElm.id, + draftid: Options.getDraftItemId(editor), + }) + .then((result) => { + pendingPromise.resolve(); + return result; + }); +}; + +/** + * Update the content of the Autosave session. + * + * @param {TinyMCE} editor The TinyMCE editor instance + * @returns {Promise} The Autosave session + */ +export const updateAutosaveSession = (editor) => { + if (!ensureEditorIsValid(editor)) { + return Promise.reject('Invalid editor'); + } + + const pendingPromise = new Pending('tiny_autosave/repository:updateAutosaveSession'); + + return fetchOne('tiny_autosave_update_session', { + contextid: Options.getContextId(editor), + pagehash: Options.getPageHash(editor), + pageinstance: Options.getPageInstance(editor), + elementid: editor.targetElm.id, + drafttext: editor.getContent(), + }) + .then((result) => { + pendingPromise.resolve(); + return result; + }); +}; + +/** + * Remove the Autosave session. + * + * @param {TinyMCE} editor The TinyMCE editor instance + * @returns {Promise} The Autosave session + */ +export const removeAutosaveSession = (editor) => { + if (!ensureEditorIsValid(editor)) { + return Promise.reject('Invalid editor'); + } + + return fetchOne('tiny_autosave_reset_session', { + contextid: Options.getContextId(editor), + pagehash: Options.getPageHash(editor), + pageinstance: Options.getPageInstance(editor), + elementid: editor.targetElm.id, + }); +}; diff --git a/lib/editor/tiny/plugins/autosave/amd/src/storage.js b/lib/editor/tiny/plugins/autosave/amd/src/storage.js new file mode 100644 index 00000000000..9de48e3d4ba --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/amd/src/storage.js @@ -0,0 +1,83 @@ +// 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 . + +/** + * Storage helper for the Moodle Tiny Autosave plugin. + * + * @module tiny_autosave/plugin + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import * as Repository from "./repository"; +import Pending from 'core/pending'; +import { + markInitialised, + getBackoffTime, +} from "./options"; +import Log from 'core/log'; +import {getLogSource} from './common'; + +/** @property {Map} A map of debounced draft saves */ +const saveDebounceMap = new Map(); + +/** + * Attempt to restore a draft into the editor + * + * @param {TinyMCE} editor The Editor to restore a draft for + */ +export const restoreDraft = async(editor) => { + const pendingPromise = new Pending('tiny_autosave/restoreDraft'); + try { + const session = await Repository.resumeAutosaveSession(editor); + if (session && session.drafttext) { + editor.undoManager.ignore(() => { + editor.setContent(session.drafttext); + editor.save(); + }); + } + } catch (error) { + // Ignore any errors as drafts are optional. + Log.warn(`Failed to restore draft: ${error}`, getLogSource(editor)); + } + markInitialised(editor); + pendingPromise.resolve(); +}; + +/** + * Save the current content of the editor as a draft. + * + * @param {TinyMCE} editor + */ +export const saveDraft = (editor) => { + const timerId = saveDebounceMap.get(editor); + if (timerId) { + clearTimeout(timerId); + } + saveDebounceMap.set(editor, setTimeout(() => { + Log.debug(`Saving draft`, getLogSource(editor)); + Repository.updateAutosaveSession(editor); + }, getBackoffTime(editor))); +}; + +/** + * Delete the draft for the current editor. + * + * @param {TinyMCE} editor + */ +export const removeAutosaveSession = (editor) => { + Log.debug(`Removing Autosave session`, getLogSource(editor)); + Repository.removeAutosaveSession(editor); +}; diff --git a/lib/editor/tiny/plugins/autosave/classes/autosave_manager.php b/lib/editor/tiny/plugins/autosave/classes/autosave_manager.php new file mode 100644 index 00000000000..91fabda9bf9 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/classes/autosave_manager.php @@ -0,0 +1,267 @@ +. + +namespace tiny_autosave; + +use stdClass; + +/** + * Autosave Manager. + * + * @package tiny_autosave + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class autosave_manager { + + /** @var int The contextid */ + protected $contextid; + + /** @var string The page hash reference */ + protected $pagehash; + + /** @var string The page instance reference */ + protected $pageinstance; + + /** @var string The elementid for this editor */ + protected $elementid; + + /** @var stdClass The user record */ + protected $user; + + /** + * Constructor for the autosave manager. + * + * @param int $contextid The contextid of the session + * @param string $pagehash The page hash + * @param string $pageinstance The page instance + * @param string $elementid The element id + * @param null|stdClass $user The user object for the owner of the autosave + */ + public function __construct( + int $contextid, + string $pagehash, + string $pageinstance, + string $elementid, + ?stdClass $user = null + ) { + global $USER; + + $this->contextid = $contextid; + $this->pagehash = $pagehash; + $this->pageinstance = $pageinstance; + $this->elementid = $elementid; + $this->user = $user ?? $USER; + } + + /** + * Get the autosave record for this session. + * + * @return stdClass|null + */ + public function get_autosave_record(): ?stdClass { + global $DB; + + $record = $DB->get_record('tiny_autosave', [ + 'contextid' => $this->contextid, + 'userid' => $this->user->id, + 'pagehash' => $this->pagehash, + 'elementid' => $this->elementid, + ]); + + if (empty($record)) { + return null; + } + + return $record; + } + + /** + * Create an autosave record for the session. + * + * @param string $drafttext The draft text to save + * @param null|int $draftid The draft file area if one is used + * @return stdClass The autosave record + */ + public function create_autosave_record(string $drafttext, ?int $draftid = null): stdClass { + global $DB; + $record = (object) [ + 'userid' => $this->user->id, + 'contextid' => $this->contextid, + 'pagehash' => $this->pagehash, + 'pageinstance' => $this->pageinstance, + 'elementid' => $this->elementid, + 'drafttext' => $drafttext, + 'timemodified' => time(), + ]; + + if ($draftid) { + $record->draftid = $draftid; + } + + $record->id = $DB->insert_record('tiny_autosave', $record); + + return $record; + } + + /** + * Update the text of the autosave session. + * + * @param string $drafttext The text to save + * @return stdClass The updated record + */ + public function update_autosave_record(string $drafttext): stdClass { + global $DB; + + $record = $this->get_autosave_record(); + if ($record) { + $record->drafttext = $drafttext; + $record->timemodified = time(); + $DB->update_record('tiny_autosave', $record); + + return $record; + } else { + return $this->create_autosave_record($drafttext); + } + } + + /** + * Resume an autosave session, updating the draft file area if relevant. + * + * @param null|int $draftid The draft file area to update + * @return stdClass The updated autosave record + */ + public function resume_autosave_session(?int $draftid = null): stdClass { + $record = $this->get_autosave_record(); + if (!$record) { + return $this->create_autosave_record('', $draftid); + } + + if ($this->is_autosave_stale($record)) { + // If the autosave record it stale, remove it and create a new, blank, record. + $this->remove_autosave_record(); + + return $this->create_autosave_record('', $draftid); + } + + if (empty($draftid)) { + // There is no file area to handle, so just return the record without any further changes. + return $record; + } + + // This autosave is not stale, so update the draftid and move any files over to the new draft file area. + return $this->update_draftid_for_record($record, $draftid); + } + + /** + * Check whether the autosave data is stale. + * + * Records are considered stale if either of the following conditions are true: + * - The record is older than the stale period + * - Any of the files in the draft area are newer than the autosave data itself + * + * @param stdClass $record The autosave record + * @return bool Whether the record is stale + */ + protected function is_autosave_stale(stdClass $record): bool { + $timemodified = $record->timemodified; + // TODO Create the UI for the stale period. + $staleperiod = get_config('tiny_autosave', 'staleperiod'); + if (empty($staleperiod)) { + $staleperiod = (4 * DAYSECS); + } + + $stale = $timemodified < (time() - $staleperiod); + + if (empty($record->draftid)) { + return $stale; + } + + $fs = get_file_storage(); + $files = $fs->get_directory_files($record->contextid, 'user', 'draft', $record->draftid, '/', true, true); + + $lastfilemodified = 0; + foreach ($files as $file) { + if ($record->timemodified < $file->get_timemodified()) { + $stale = true; + break; + } + } + + return $stale; + } + + /** + * Move the files relating to the autosave session to a new draft file area. + * + * @param stdClass $record The autosave record + * @param int $newdraftid The new draftid to move files to + * @return stdClass The updated autosave record + */ + protected function update_draftid_for_record(stdClass $record, int $newdraftid): stdClass { + global $CFG, $DB; + + require_once("{$CFG->libdir}/filelib.php"); + + // Copy all draft files from the old draft area. + $usercontext = \context_user::instance($this->user->id); + + // This function copies all the files in one draft area, to another area (in this case it's + // another draft area). It also rewrites the text to @@PLUGINFILE@@ links. + $record->drafttext = file_save_draft_area_files( + $record->draftid, + $usercontext->id, + 'user', + 'draft', + $newdraftid, + [], + $record->drafttext + ); + + // Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again). + $record->drafttext = file_rewrite_pluginfile_urls( + $record->drafttext, + 'draftfile.php', + $usercontext->id, + 'user', + 'draft', + $newdraftid + ); + + $record->draftid = $newdraftid; + $record->pageinstance = $this->pageinstance; + $record->timemodified = time(); + + $DB->update_record('tiny_autosave', $record); + + return $record; + } + + /** + * Remove the autosave record. + */ + public function remove_autosave_record(): void { + global $DB; + + $DB->delete_records('tiny_autosave', [ + 'contextid' => $this->contextid, + 'userid' => $this->user->id, + 'pagehash' => $this->pagehash, + 'elementid' => $this->elementid, + ]); + } + +} diff --git a/lib/editor/tiny/plugins/autosave/classes/external/reset_autosave_session.php b/lib/editor/tiny/plugins/autosave/classes/external/reset_autosave_session.php new file mode 100644 index 00000000000..308b0a76c06 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/classes/external/reset_autosave_session.php @@ -0,0 +1,98 @@ +. + +namespace tiny_autosave\external; + +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; + +defined('MOODLE_INTERNAL') || die(); + +require_once("{$CFG->libdir}/externallib.php"); + +/** + * Web Service to reset the autosave session. + * + * @package tiny_autosave + * @category external + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reset_autosave_session extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'contextid' => new external_value(PARAM_INT, 'The context id that owns the editor', VALUE_REQUIRED), + 'pagehash' => new external_value(PARAM_ALPHANUMEXT, 'The page hash', VALUE_REQUIRED), + 'pageinstance' => new external_value(PARAM_ALPHANUMEXT, 'The page instance', VALUE_REQUIRED), + 'elementid' => new external_value(PARAM_RAW, 'The ID of the element', VALUE_REQUIRED), + ]); + } + + /** + * Reset the autosave entry for this autosave instance. + * + * If not matching autosave area could be found, the function will + * silently return and this is not treated as an error condition. + * + * @param int $contextid The context id of the owner + * @param string $pagehash The hash of the page + * @param string $pageinstance The instance id of the page + * @param string $elementid The id of the element + * @return null + */ + public static function execute( + int $contextid, + string $pagehash, + string $pageinstance, + string $elementid + ): array { + global $DB, $USER; + + [ + 'contextid' => $contextid, + 'pagehash' => $pagehash, + 'elementid' => $elementid, + 'pageinstance' => $pageinstance, + ] = self::validate_parameters(self::execute_parameters(), [ + 'contextid' => $contextid, + 'pagehash' => $pagehash, + 'elementid' => $elementid, + 'pageinstance' => $pageinstance, + + ]); + + $manager = new \tiny_autosave\autosave_manager($contextid, $pagehash, $pageinstance, $elementid); + $manager->remove_autosave_record(); + + return []; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([]); + } +} diff --git a/lib/editor/tiny/plugins/autosave/classes/external/resume_autosave_session.php b/lib/editor/tiny/plugins/autosave/classes/external/resume_autosave_session.php new file mode 100644 index 00000000000..51a86b84d01 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/classes/external/resume_autosave_session.php @@ -0,0 +1,111 @@ +. + +namespace tiny_autosave\external; + +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; + +defined('MOODLE_INTERNAL') || die(); + +require_once("{$CFG->libdir}/externallib.php"); + +/** + * Web Service to resume an autosave session. + * + * @package tiny_autosave + * @category external + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class resume_autosave_session extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'contextid' => new external_value(PARAM_INT, 'The context id that owns the editor', VALUE_REQUIRED), + 'pagehash' => new external_value(PARAM_ALPHANUMEXT, 'The page hash', VALUE_REQUIRED), + 'pageinstance' => new external_value(PARAM_ALPHANUMEXT, 'The page instance', VALUE_REQUIRED), + 'elementid' => new external_value(PARAM_RAW, 'The ID of the element', VALUE_REQUIRED), + 'draftid' => new external_value( + PARAM_INT, + 'The new draft item id to resume files to', + VALUE_REQUIRED, + null, + NULL_ALLOWED + ), + ]); + } + + /** + * Reset the autosave entry for this autosave instance. + * + * If not matching autosave area could be found, the function will + * silently return and this is not treated as an error condition. + * + * @param int $contextid The context id of the owner + * @param string $pagehash The hash of the page + * @param string $pageinstance The instance id of the page + * @param string $elementid The id of the element + * @param int $draftid The id of the draftid to resume to + * @return null + */ + public static function execute( + int $contextid, + string $pagehash, + string $pageinstance, + string $elementid, + ?int $draftid + ): array { + global $DB, $USER; + + [ + 'contextid' => $contextid, + 'pagehash' => $pagehash, + 'pageinstance' => $pageinstance, + 'elementid' => $elementid, + 'draftid' => $draftid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'contextid' => $contextid, + 'pagehash' => $pagehash, + 'pageinstance' => $pageinstance, + 'elementid' => $elementid, + 'draftid' => $draftid, + ]); + + $manager = new \tiny_autosave\autosave_manager($contextid, $pagehash, $pageinstance, $elementid); + $sessiondata = $manager->resume_autosave_session($draftid); + return [ + 'drafttext' => $sessiondata->drafttext, + ]; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'drafttext' => new external_value(PARAM_RAW, 'The draft text'), + ]); + } +} diff --git a/lib/editor/tiny/plugins/autosave/classes/external/update_autosave_session_content.php b/lib/editor/tiny/plugins/autosave/classes/external/update_autosave_session_content.php new file mode 100644 index 00000000000..615b0f3a3f2 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/classes/external/update_autosave_session_content.php @@ -0,0 +1,102 @@ +. + +namespace tiny_autosave\external; + +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; + +defined('MOODLE_INTERNAL') || die(); + +require_once("{$CFG->libdir}/externallib.php"); + +/** + * Web Service to update an autosave session's content. + * + * @package tiny_autosave + * @category external + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class update_autosave_session_content extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'contextid' => new external_value(PARAM_INT, 'The context id that owns the editor', VALUE_REQUIRED), + 'pagehash' => new external_value(PARAM_ALPHANUMEXT, 'The page hash', VALUE_REQUIRED), + 'pageinstance' => new external_value(PARAM_ALPHANUMEXT, 'The page instance', VALUE_REQUIRED), + 'elementid' => new external_value(PARAM_RAW, 'The ID of the element', VALUE_REQUIRED), + 'drafttext' => new external_value(PARAM_RAW, 'The draft text', VALUE_REQUIRED), + ]); + } + + /** + * Reset the autosave entry for this autosave instance. + * + * If not matching autosave area could be found, the function will + * silently return and this is not treated as an error condition. + * + * @param int $contextid The context id of the owner + * @param string $pagehash The hash of the page + * @param string $pageinstance The instance id of the page + * @param string $elementid The id of the element + * @param string $drafttext The text to store + * @return null + */ + public static function execute( + int $contextid, + string $pagehash, + string $pageinstance, + string $elementid, + string $drafttext + ): array { + global $DB, $USER; + + [ + 'contextid' => $contextid, + 'pagehash' => $pagehash, + 'pageinstance' => $pageinstance, + 'elementid' => $elementid, + 'drafttext' => $drafttext, + ] = self::validate_parameters(self::execute_parameters(), [ + 'contextid' => $contextid, + 'pagehash' => $pagehash, + 'pageinstance' => $pageinstance, + 'elementid' => $elementid, + 'drafttext' => $drafttext, + ]); + + $manager = new \tiny_autosave\autosave_manager($contextid, $pagehash, $pageinstance, $elementid); + $manager->update_autosave_record($drafttext); + + return []; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([]); + } +} diff --git a/lib/editor/tiny/plugins/autosave/classes/plugininfo.php b/lib/editor/tiny/plugins/autosave/classes/plugininfo.php new file mode 100644 index 00000000000..38aca833f86 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/classes/plugininfo.php @@ -0,0 +1,52 @@ +. + +namespace tiny_autosave; + +use context; +use editor_tiny\plugin; +use editor_tiny\plugin_with_configuration; + +/** + * Tiny autosave plugin for Moodle. + * + * @package tiny_autosave + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugininfo extends plugin implements plugin_with_configuration { + + public static function get_plugin_configuration_for_context( + context $context, + array $options, + array $fpoptions, + ?\editor_tiny\editor $editor = null + ): array { + global $PAGE; + + if (empty($editor) || empty($options['autosave'])) { + return [ + 'autosave' => null, + ]; + } + + return [ + 'pagehash' => sha1($PAGE->url . '<>' . s($editor->get_text())), + 'pageinstance' => bin2hex(random_bytes(16)), + 'backoffTime' => (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) ? 0 : 500, + ]; + } +} diff --git a/lib/editor/tiny/plugins/autosave/classes/privacy/provider.php b/lib/editor/tiny/plugins/autosave/classes/privacy/provider.php new file mode 100644 index 00000000000..1543871a94d --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/classes/privacy/provider.php @@ -0,0 +1,237 @@ +. + +namespace tiny_autosave\privacy; + +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\writer; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\userlist; +use core_privacy\local\request\approved_userlist; +use stdClass; + +/** + * Privacy Subsystem implementation for tiny_autosave. + * + * @package tiny_autosave + * @copyright 2022 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // The tiny editor stores user provided data. + \core_privacy\local\metadata\provider, + + // The tiny editor provides data directly to core. + \core_privacy\local\request\plugin\provider, + + // The tiny editor is capable of determining which users have data within it. + \core_privacy\local\request\core_userlist_provider { + + /** + * Returns information about how tiny_autosave stores its data. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection): collection { + // There isn't much point giving details about the pageid, etc. + $collection->add_database_table('tiny_autosave', [ + 'userid' => 'privacy:metadata:database:tiny_autosave:userid', + 'drafttext' => 'privacy:metadata:database:tiny_autosave:drafttext', + 'timemodified' => 'privacy:metadata:database:tiny_autosave:timemodified', + ], 'privacy:metadata:database:tiny_autosave'); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist { + $contextlist = new \core_privacy\local\request\contextlist(); + + // Data may be saved in the user context. + $sql = "SELECT + c.id + FROM {tiny_autosave} eas + JOIN {context} c ON c.id = eas.contextid + WHERE contextlevel = :contextuser AND c.instanceid = :userid"; + $contextlist->add_from_sql($sql, ['contextuser' => CONTEXT_USER, 'userid' => $userid]); + + // Data may be saved against the userid. + $sql = "SELECT contextid FROM {tiny_autosave} WHERE userid = :userid"; + $contextlist->add_from_sql($sql, ['userid' => $userid]); + + return $contextlist; + } + + /** + * Get the list of users within a specific context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist) { + $context = $userlist->get_context(); + + $params = [ + 'contextid' => $context->id + ]; + + $sql = "SELECT userid + FROM {tiny_autosave} + WHERE contextid = :contextid"; + + $userlist->add_from_sql('userid', $sql, $params); + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + $user = $contextlist->get_user(); + + // Firstly export all autosave records from all contexts in the list owned by the given user. + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $contextparams['userid'] = $user->id; + + $sql = "SELECT * + FROM {tiny_autosave} + WHERE userid = :userid AND contextid {$contextsql}"; + + $autosaves = $DB->get_recordset_sql($sql, $contextparams); + self::export_autosaves($user, $autosaves); + + // Additionally export all eventual records in the given user's context regardless the actual owner. + // We still consider them to be the user's personal data even when edited by someone else. + [$contextsql, $contextparams] = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $contextparams['userid'] = $user->id; + $contextparams['contextuser'] = CONTEXT_USER; + + $sql = "SELECT eas.* + FROM {tiny_autosave} eas + JOIN {context} c ON c.id = eas.contextid + WHERE c.id {$contextsql} AND c.contextlevel = :contextuser AND c.instanceid = :userid"; + + $autosaves = $DB->get_recordset_sql($sql, $contextparams); + self::export_autosaves($user, $autosaves); + } + + /** + * Export all autosave records in the recordset, and close the recordset when finished. + * + * @param stdClass $user The user whose data is to be exported + * @param \moodle_recordset $autosaves The recordset containing the data to export + */ + protected static function export_autosaves(stdClass $user, \moodle_recordset $autosaves) { + foreach ($autosaves as $autosave) { + $context = \context::instance_by_id($autosave->contextid); + $subcontext = [ + get_string('autosaves', 'tiny_autosave'), + $autosave->id, + ]; + + $html = writer::with_context($context) + ->rewrite_pluginfile_urls($subcontext, 'user', 'draft', $autosave->draftid, $autosave->drafttext); + + $data = (object) [ + 'drafttext' => format_text($html, FORMAT_HTML, static::get_filter_options()), + 'timemodified' => \core_privacy\local\request\transform::datetime($autosave->timemodified), + ]; + + if ($autosave->userid != $user->id) { + $data->author = \core_privacy\local\request\transform::user($autosave->userid); + } + + writer::with_context($context) + ->export_data($subcontext, $data) + ->export_area_files($subcontext, 'user', 'draft', $autosave->draftid); + } + $autosaves->close(); + } + + /** + * Delete all data for all users in the specified context. + * + * @param context $context The specific context to delete data for. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + $DB->delete_records('tiny_autosave', [ + 'contextid' => $context->id, + ]); + } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + global $DB; + + $context = $userlist->get_context(); + $userids = $userlist->get_userids(); + + [$useridsql, $useridsqlparams] = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); + $params = ['contextid' => $context->id] + $useridsqlparams; + + $DB->delete_records_select('tiny_autosave', "contextid = :contextid AND userid {$useridsql}", + $params); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + $user = $contextlist->get_user(); + + [$contextsql, $contextparams] = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $contextparams['userid'] = $user->id; + + $sql = "SELECT * FROM {tiny_autosave} WHERE contextid {$contextsql}"; + $autosaves = $DB->delete_records_select( + 'tiny_autosave', + "userid = :userid AND contextid {$contextsql}", + $contextparams + ); + } + + /** + * Get the filter options. + * + * This is shared to allow unit testing too. + * + * @return stdClass + */ + public static function get_filter_options() { + return (object) [ + 'overflowdiv' => true, + 'noclean' => true, + ]; + } +} diff --git a/lib/editor/tiny/plugins/autosave/db/install.xml b/lib/editor/tiny/plugins/autosave/db/install.xml new file mode 100644 index 00000000000..19aa5ec6095 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/db/install.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/lib/editor/tiny/plugins/autosave/db/services.php b/lib/editor/tiny/plugins/autosave/db/services.php new file mode 100644 index 00000000000..8f62857f83e --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/db/services.php @@ -0,0 +1,49 @@ +. + +/** + * External service definitions for tiny_autosavse. + * + * @package tiny_autosave + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$functions = [ + 'tiny_autosave_resume_session' => array( + 'classname' => 'tiny_autosave\external\resume_autosave_session', + 'methodname' => 'execute', + 'description' => 'Resume an autosave session', + 'type' => 'write', + 'ajax' => true, + ), + 'tiny_autosave_reset_session' => array( + 'classname' => 'tiny_autosave\external\reset_autosave_session', + 'methodname' => 'execute', + 'description' => 'Reset an autosave session', + 'type' => 'write', + 'ajax' => true, + ), + 'tiny_autosave_update_session' => array( + 'classname' => 'tiny_autosave\external\update_autosave_session_content', + 'methodname' => 'execute', + 'description' => 'Update an autosave session', + 'type' => 'write', + 'ajax' => true, + ), +]; diff --git a/lib/editor/tiny/plugins/autosave/lang/en/tiny_autosave.php b/lib/editor/tiny/plugins/autosave/lang/en/tiny_autosave.php new file mode 100644 index 00000000000..1a5908d86c6 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/lang/en/tiny_autosave.php @@ -0,0 +1,30 @@ +. + +/** + * Strings for component 'tiny_autosave', language 'en'. + * + * @package tiny_autosave + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Tiny Autosave'; +$string['helplinktext'] = 'Moodle Autosave'; +$string['privacy:metadata:database:tiny_autosave:userid'] = 'The user ID of the user who created the autosave session'; +$string['privacy:metadata:database:tiny_autosave:drafttext'] = 'The text content of the autosave session'; +$string['privacy:metadata:database:tiny_autosave:timemodified'] = 'The time that the autosave session was last modified'; +$string['privacy:metadata:database:tiny_autosave'] = 'A table storing autosave session data for the TinyMCE editor'; diff --git a/lib/editor/tiny/plugins/autosave/version.php b/lib/editor/tiny/plugins/autosave/version.php new file mode 100644 index 00000000000..7cb7cfc5571 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/version.php @@ -0,0 +1,29 @@ +. + +/** + * Tiny text editor Autosave Plugin version file. + * + * @package tiny_autosave + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2022080900; +$plugin->requires = 2020061500; +$plugin->component = 'tiny_autosave';