From ff5f669cf8f63a4eb883f3c610625d0c2ceb562b Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Wed, 30 Nov 2022 14:33:22 +0100 Subject: [PATCH 1/5] MDL-76432 aria: add getFirst to nomalise Half of the times the normalise module is used is to get a single element. However, because jQuery elements can contain multiple elements the getList is always an array. Due to this in many ocasions we repeat the getList(VAR)[0] line instead of having a more readable getFirst method which only implies a couple of lines in the original code. --- lib/amd/build/normalise.min.js | 2 +- lib/amd/build/normalise.min.js.map | 2 +- lib/amd/src/normalise.js | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/amd/build/normalise.min.js b/lib/amd/build/normalise.min.js index 76e3ac70788..7e9da8c647f 100644 --- a/lib/amd/build/normalise.min.js +++ b/lib/amd/build/normalise.min.js @@ -5,6 +5,6 @@ define("core/normalise",["exports","jquery"],(function(_exports,_jquery){var obj * @module core/normalise * @copyright 2020 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getList=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.getList=nodes=>nodes instanceof HTMLElement?[nodes]:nodes instanceof Array?nodes:nodes instanceof NodeList?Array.from(nodes):nodes instanceof _jquery.default?nodes.get():Array.from(nodes)})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getList=_exports.getFirst=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};const getList=nodes=>nodes instanceof HTMLElement?[nodes]:nodes instanceof Array?nodes:nodes instanceof NodeList?Array.from(nodes):nodes instanceof _jquery.default?nodes.get():Array.from(nodes);_exports.getList=getList;_exports.getFirst=nodes=>getList(nodes)[0]})); //# sourceMappingURL=normalise.min.js.map \ No newline at end of file diff --git a/lib/amd/build/normalise.min.js.map b/lib/amd/build/normalise.min.js.map index 5a696e0ea40..1009d925686 100644 --- a/lib/amd/build/normalise.min.js.map +++ b/lib/amd/build/normalise.min.js.map @@ -1 +1 @@ -{"version":3,"file":"normalise.min.js","sources":["../src/normalise.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 * Normalisation helpers.\n *\n * @module core/normalise\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\n\n/**\n * Normalise a list of Nodes into an Array of Nodes.\n *\n * @method getList\n * @param {(Array|jQuery|NodeList|HTMLElement)} nodes\n * @returns {HTMLElement[]}\n */\nexport const getList = nodes => {\n if (nodes instanceof HTMLElement) {\n // A single record to conver to a NodeList.\n return [nodes];\n }\n\n if (nodes instanceof Array) {\n // A single record to conver to a NodeList.\n return nodes;\n }\n\n if (nodes instanceof NodeList) {\n // Already a NodeList.\n return Array.from(nodes);\n }\n\n if (nodes instanceof jQuery) {\n // A jQuery object to a NodeList.\n return nodes.get();\n }\n\n // Fallback to just having a go.\n return Array.from(nodes);\n};\n"],"names":["nodes","HTMLElement","Array","NodeList","from","jQuery","get"],"mappings":";;;;;;;8JAgCuBA,OACfA,iBAAiBC,YAEV,CAACD,OAGRA,iBAAiBE,MAEVF,MAGPA,iBAAiBG,SAEVD,MAAME,KAAKJ,OAGlBA,iBAAiBK,gBAEVL,MAAMM,MAIVJ,MAAME,KAAKJ"} \ No newline at end of file +{"version":3,"file":"normalise.min.js","sources":["../src/normalise.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 * Normalisation helpers.\n *\n * @module core/normalise\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\n\n/**\n * Normalise a list of Nodes into an Array of Nodes.\n *\n * @method getList\n * @param {(Array|jQuery|NodeList|HTMLElement)} nodes\n * @returns {HTMLElement[]}\n */\nexport const getList = nodes => {\n if (nodes instanceof HTMLElement) {\n // A single record to conver to a NodeList.\n return [nodes];\n }\n\n if (nodes instanceof Array) {\n // A single record to conver to a NodeList.\n return nodes;\n }\n\n if (nodes instanceof NodeList) {\n // Already a NodeList.\n return Array.from(nodes);\n }\n\n if (nodes instanceof jQuery) {\n // A jQuery object to a NodeList.\n return nodes.get();\n }\n\n // Fallback to just having a go.\n return Array.from(nodes);\n};\n\n/**\n * Return the first element in a list of normalised Nodes.\n *\n * @param {Array|jQuery|NodeList|HTMLElement} nodes the unmormalised list of nodes\n * @returns {HTMLElement|undefined} the first list element\n */\nexport const getFirst = nodes => {\n const list = getList(nodes);\n return list[0];\n};\n"],"names":["getList","nodes","HTMLElement","Array","NodeList","from","jQuery","get"],"mappings":";;;;;;;qKAgCaA,QAAUC,OACfA,iBAAiBC,YAEV,CAACD,OAGRA,iBAAiBE,MAEVF,MAGPA,iBAAiBG,SAEVD,MAAME,KAAKJ,OAGlBA,iBAAiBK,gBAEVL,MAAMM,MAIVJ,MAAME,KAAKJ,kDASEA,OACPD,QAAQC,OACT"} \ No newline at end of file diff --git a/lib/amd/src/normalise.js b/lib/amd/src/normalise.js index c55d82f47ef..98e8c285946 100644 --- a/lib/amd/src/normalise.js +++ b/lib/amd/src/normalise.js @@ -54,3 +54,14 @@ export const getList = nodes => { // Fallback to just having a go. return Array.from(nodes); }; + +/** + * Return the first element in a list of normalised Nodes. + * + * @param {Array|jQuery|NodeList|HTMLElement} nodes the unmormalised list of nodes + * @returns {HTMLElement|undefined} the first list element + */ +export const getFirst = nodes => { + const list = getList(nodes); + return list[0]; +}; From 2846751f2be9f6b321351dafa14f615f01963594 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Wed, 30 Nov 2022 15:54:01 +0100 Subject: [PATCH 2/5] MDL-76432 core: add process monitor UI component Create a new UI compoment to queue, execute and display errors on batch processing. The first use of this component is when the teacher drops a file into the course page. --- lang/en/moodle.php | 1 + .../build/local/process_monitor/events.min.js | 12 + .../local/process_monitor/events.min.js.map | 1 + .../process_monitor/loadingprocess.min.js | 3 + .../process_monitor/loadingprocess.min.js.map | 1 + .../local/process_monitor/manager.min.js | 14 ++ .../local/process_monitor/manager.min.js.map | 1 + .../local/process_monitor/monitor.min.js | 11 + .../local/process_monitor/monitor.min.js.map | 1 + .../local/process_monitor/process.min.js | 12 + .../local/process_monitor/process.min.js.map | 1 + .../local/process_monitor/processqueue.min.js | 14 ++ .../process_monitor/processqueue.min.js.map | 1 + lib/amd/build/process_monitor.min.js | 10 + lib/amd/build/process_monitor.min.js.map | 1 + lib/amd/src/local/process_monitor/events.js | 58 +++++ .../local/process_monitor/loadingprocess.js | 211 ++++++++++++++++++ lib/amd/src/local/process_monitor/manager.js | 182 +++++++++++++++ lib/amd/src/local/process_monitor/monitor.js | 120 ++++++++++ lib/amd/src/local/process_monitor/process.js | 115 ++++++++++ .../src/local/process_monitor/processqueue.js | 116 ++++++++++ lib/amd/src/process_monitor.js | 109 +++++++++ .../local/process_monitor/monitor.mustache | 57 +++++ .../local/process_monitor/process.mustache | 57 +++++ theme/boost/scss/moodle.scss | 1 + theme/boost/scss/moodle/process-monitor.scss | 30 +++ theme/boost/style/moodle.css | 28 +++ theme/classic/style/moodle.css | 28 +++ 28 files changed, 1196 insertions(+) create mode 100644 lib/amd/build/local/process_monitor/events.min.js create mode 100644 lib/amd/build/local/process_monitor/events.min.js.map create mode 100644 lib/amd/build/local/process_monitor/loadingprocess.min.js create mode 100644 lib/amd/build/local/process_monitor/loadingprocess.min.js.map create mode 100644 lib/amd/build/local/process_monitor/manager.min.js create mode 100644 lib/amd/build/local/process_monitor/manager.min.js.map create mode 100644 lib/amd/build/local/process_monitor/monitor.min.js create mode 100644 lib/amd/build/local/process_monitor/monitor.min.js.map create mode 100644 lib/amd/build/local/process_monitor/process.min.js create mode 100644 lib/amd/build/local/process_monitor/process.min.js.map create mode 100644 lib/amd/build/local/process_monitor/processqueue.min.js create mode 100644 lib/amd/build/local/process_monitor/processqueue.min.js.map create mode 100644 lib/amd/build/process_monitor.min.js create mode 100644 lib/amd/build/process_monitor.min.js.map create mode 100644 lib/amd/src/local/process_monitor/events.js create mode 100644 lib/amd/src/local/process_monitor/loadingprocess.js create mode 100644 lib/amd/src/local/process_monitor/manager.js create mode 100644 lib/amd/src/local/process_monitor/monitor.js create mode 100644 lib/amd/src/local/process_monitor/process.js create mode 100644 lib/amd/src/local/process_monitor/processqueue.js create mode 100644 lib/amd/src/process_monitor.js create mode 100644 lib/templates/local/process_monitor/monitor.mustache create mode 100644 lib/templates/local/process_monitor/process.mustache create mode 100644 theme/boost/scss/moodle/process-monitor.scss diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 8ba8fdc7a14..df689508a0d 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1745,6 +1745,7 @@ $string['private_files_handler_name'] = 'Email to Private files'; $string['proceed'] = 'Proceed'; $string['profile'] = 'Profile'; $string['profilenotshown'] = 'This profile description will not be shown until this person is enrolled in at least one course.'; +$string['progress'] = 'Progress'; $string['publicprofile'] = 'Public profile'; $string['publicsitefileswarning'] = 'Note: files placed here can be accessed by anyone'; $string['publicsitefileswarning2'] = 'Note: Files placed here can be accessed by anyone who knows (or can guess) the URL. For security reasons, it is recommended that any backup files are deleted immediately after restoring them.'; diff --git a/lib/amd/build/local/process_monitor/events.min.js b/lib/amd/build/local/process_monitor/events.min.js new file mode 100644 index 00000000000..dbbed6e9ee1 --- /dev/null +++ b/lib/amd/build/local/process_monitor/events.min.js @@ -0,0 +1,12 @@ +define("core/local/process_monitor/events",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.dispatchStateChangedEvent=function(detail,target){void 0===target&&(target=document);target.dispatchEvent(new CustomEvent(eventTypes.processMonitorStateChange,{bubbles:!0,detail:detail}))},_exports.eventTypes=void 0; +/** + * Javascript events for the `process_monitor` module. + * + * @module core/local/process_monitor/events + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 4.2 + */ +const eventTypes={processMonitorStateChange:"core_editor/contentRestored"};_exports.eventTypes=eventTypes})); + +//# sourceMappingURL=events.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/process_monitor/events.min.js.map b/lib/amd/build/local/process_monitor/events.min.js.map new file mode 100644 index 00000000000..c5c82c1e516 --- /dev/null +++ b/lib/amd/build/local/process_monitor/events.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"events.min.js","sources":["../../../src/local/process_monitor/events.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/ //\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 * Javascript events for the `process_monitor` module.\n *\n * @module core/local/process_monitor/events\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.2\n */\n\n/**\n * Events for the `core_editor` subsystem.\n *\n * @constant\n * @property {String} processMonitorStateChange See {@link event:processMonitorStateChange}\n */\nexport const eventTypes = {\n /**\n * An event triggered when the monitor state has changed.\n *\n * @event processMonitorStateChange\n */\n processMonitorStateChange: 'core_editor/contentRestored',\n};\n\n/**\n * Trigger a state changed event.\n *\n * @method dispatchStateChangedEvent\n * @param {Object} detail the full state\n * @param {Object} target the custom event target (document if none provided)\n * @param {Function} target.dispatchEvent the component dispatch event method.\n */\nexport function dispatchStateChangedEvent(detail, target) {\n if (target === undefined) {\n target = document;\n }\n target.dispatchEvent(new CustomEvent(\n eventTypes.processMonitorStateChange,\n {\n bubbles: true,\n detail: detail,\n }\n ));\n}\n"],"names":["detail","target","undefined","document","dispatchEvent","CustomEvent","eventTypes","processMonitorStateChange","bubbles"],"mappings":"+KA8C0CA,OAAQC,aAC/BC,IAAXD,SACAA,OAASE,UAEbF,OAAOG,cAAc,IAAIC,YACrBC,WAAWC,0BACX,CACIC,SAAS,EACTR,OAAQA;;;;;;;;;MAzBPM,WAAa,CAMtBC,0BAA2B"} \ No newline at end of file diff --git a/lib/amd/build/local/process_monitor/loadingprocess.min.js b/lib/amd/build/local/process_monitor/loadingprocess.min.js new file mode 100644 index 00000000000..286e7b27d38 --- /dev/null +++ b/lib/amd/build/local/process_monitor/loadingprocess.min.js @@ -0,0 +1,3 @@ +define("core/local/process_monitor/loadingprocess",["exports","core/log"],(function(_exports,_log){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.LoadingProcess=void 0,_log=(obj=_log)&&obj.__esModule?obj:{default:obj};_exports.LoadingProcess=class{constructor(manager,definition){_defineProperty(this,"processData",null),_defineProperty(this,"extraData",null),_defineProperty(this,"manager",null),_defineProperty(this,"finishedCallback",null),_defineProperty(this,"removedCallback",null),_defineProperty(this,"errorCallback",null),this.manager=manager,this.processData={id:manager.generateProcessId(),name:"",percentage:0,url:null,error:null,finished:!1,...definition},this._dispatch("addProcess",this.processData)}_dispatch(action,params){this.manager.getInitialStatePromise().then((()=>{this.manager.dispatch(action,params)})).catch((()=>{_log.default.error("Cannot update process monitor.")}))}onFinish(callback){this.finishedCallback=callback}onRemove(callback){this.removedCallback=callback}onError(callback){this.errorCallback=callback}setPercentage(percentage){this.processData.percentage=percentage,this._dispatch("updateProcess",this.processData)}setExtraData(extraData){this.extraData=extraData}setError(error){this.processData.error=error,null!==this.errorCallback&&this.errorCallback(this),this.processData.finished=!0,null!==this.finishedCallback&&this.finishedCallback(this),this._dispatch("updateProcess",this.processData)}setName(name){this.processData.name=name,this._dispatch("updateProcess",this.processData)}finish(){this.processData.finished=!0,null!==this.finishedCallback&&this.finishedCallback(this),this._dispatch("updateProcess",this.processData)}remove(){null!==this.removedCallback&&this.removedCallback(this),this._dispatch("removeProcess",this.processData.id)}getData(){return{...this.processData}}get name(){return this.processData.name}get id(){return this.processData.id}get data(){return this.extraData}}})); + +//# sourceMappingURL=loadingprocess.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/process_monitor/loadingprocess.min.js.map b/lib/amd/build/local/process_monitor/loadingprocess.min.js.map new file mode 100644 index 00000000000..a3ba12a3b18 --- /dev/null +++ b/lib/amd/build/local/process_monitor/loadingprocess.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"loadingprocess.min.js","sources":["../../../src/local/process_monitor/loadingprocess.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 * The process wrapper class.\n *\n * This module is used to update a process in the process monitor.\n *\n * @module core/local/process_monitor/loadingprocess\n * @class LoadingProcess\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport log from 'core/log';\n\nexport class LoadingProcess {\n\n /** @var {Map} editorUpdates the courses pending to be updated. */\n processData = null;\n\n /** @var {Object} extraData any extra process information to store. */\n extraData = null;\n\n /** @var {ProcessMonitorManager} manager the page monitor. */\n manager = null;\n\n /** @var {Function} finishedCallback the finished callback if any. */\n finishedCallback = null;\n\n /** @var {Function} removedCallback the removed callback if any. */\n removedCallback = null;\n\n /** @var {Function} errorCallback the error callback if any. */\n errorCallback = null;\n\n /**\n * Class constructor\n * @param {ProcessMonitorManager} manager the monitor manager\n * @param {Object} definition the process definition data\n */\n constructor(manager, definition) {\n this.manager = manager;\n // Add defaults.\n this.processData = {\n id: manager.generateProcessId(),\n name: '',\n percentage: 0,\n url: null,\n error: null,\n finished: false,\n ...definition,\n };\n // Create a new entry.\n this._dispatch('addProcess', this.processData);\n }\n\n /**\n * Execute a monitor manager mutation when the state is ready.\n *\n * @private\n * @param {String} action the mutation to dispatch\n * @param {*} params the mutaiton params\n */\n _dispatch(action, params) {\n this.manager.getInitialStatePromise().then(() => {\n this.manager.dispatch(action, params);\n return;\n }).catch(() => {\n log.error(`Cannot update process monitor.`);\n });\n }\n\n /**\n * Define a finished process callback function.\n * @param {Function} callback the callback function\n */\n onFinish(callback) {\n this.finishedCallback = callback;\n }\n\n /**\n * Define a removed from monitor process callback function.\n * @param {Function} callback the callback function\n */\n onRemove(callback) {\n this.removedCallback = callback;\n }\n\n /**\n * Define a error process callback function.\n * @param {Function} callback the callback function\n */\n onError(callback) {\n this.errorCallback = callback;\n }\n\n /**\n * Set the process percentage.\n * @param {Number} percentage\n */\n setPercentage(percentage) {\n this.processData.percentage = percentage;\n this._dispatch('updateProcess', this.processData);\n }\n\n /**\n * Stores extra information to the process.\n *\n * This method is used to add information like the course, the user\n * or any other needed information.\n *\n * @param {Object} extraData any extra process information to store\n */\n setExtraData(extraData) {\n this.extraData = extraData;\n }\n\n /**\n * Set the process error string.\n *\n * Note: set the error message will mark the process as finished.\n *\n * @param {String} error the string message\n */\n setError(error) {\n this.processData.error = error;\n if (this.errorCallback !== null) {\n this.errorCallback(this);\n }\n this.processData.finished = true;\n if (this.finishedCallback !== null) {\n this.finishedCallback(this);\n }\n this._dispatch('updateProcess', this.processData);\n }\n\n /**\n * Rename the process\n * @param {String} name the new process name\n */\n setName(name) {\n this.processData.name = name;\n this._dispatch('updateProcess', this.processData);\n }\n\n /**\n * Mark the process as finished.\n */\n finish() {\n this.processData.finished = true;\n if (this.finishedCallback !== null) {\n this.finishedCallback(this);\n }\n this._dispatch('updateProcess', this.processData);\n }\n\n /**\n * Remove the process from the monitor.\n */\n remove() {\n if (this.removedCallback !== null) {\n this.removedCallback(this);\n }\n this._dispatch('removeProcess', this.processData.id);\n }\n\n /**\n * Returns the current rpocess data.\n * @returns {Object} the process data\n */\n getData() {\n return {...this.processData};\n }\n\n /**\n * Return the process name\n * @return {String}\n */\n get name() {\n return this.processData.name;\n }\n\n /**\n * Return the process internal id\n * @return {Number}\n */\n get id() {\n return this.processData.id;\n }\n\n /**\n * Return the process extra data.\n * @return {*} whatever is in extra data\n */\n get data() {\n return this.extraData;\n }\n}\n"],"names":["constructor","manager","definition","processData","id","generateProcessId","name","percentage","url","error","finished","_dispatch","this","action","params","getInitialStatePromise","then","dispatch","catch","onFinish","callback","finishedCallback","onRemove","removedCallback","onError","errorCallback","setPercentage","setExtraData","extraData","setError","setName","finish","remove","getData","data"],"mappings":"sbAqDIA,YAAYC,QAASC,+CAtBP,uCAGF,qCAGF,8CAGS,6CAGD,2CAGF,WAQPD,QAAUA,aAEVE,YAAc,CACfC,GAAIH,QAAQI,oBACZC,KAAM,GACNC,WAAY,EACZC,IAAK,KACLC,MAAO,KACPC,UAAU,KACPR,iBAGFS,UAAU,aAAcC,KAAKT,aAUtCQ,UAAUE,OAAQC,aACTb,QAAQc,yBAAyBC,MAAK,UAClCf,QAAQgB,SAASJ,OAAQC,WAE/BI,OAAM,kBACDT,2CAQZU,SAASC,eACAC,iBAAmBD,SAO5BE,SAASF,eACAG,gBAAkBH,SAO3BI,QAAQJ,eACCK,cAAgBL,SAOzBM,cAAcnB,iBACLJ,YAAYI,WAAaA,gBACzBI,UAAU,gBAAiBC,KAAKT,aAWzCwB,aAAaC,gBACJA,UAAYA,UAUrBC,SAASpB,YACAN,YAAYM,MAAQA,MACE,OAAvBG,KAAKa,oBACAA,cAAcb,WAElBT,YAAYO,UAAW,EACE,OAA1BE,KAAKS,uBACAA,iBAAiBT,WAErBD,UAAU,gBAAiBC,KAAKT,aAOzC2B,QAAQxB,WACCH,YAAYG,KAAOA,UACnBK,UAAU,gBAAiBC,KAAKT,aAMzC4B,cACS5B,YAAYO,UAAW,EACE,OAA1BE,KAAKS,uBACAA,iBAAiBT,WAErBD,UAAU,gBAAiBC,KAAKT,aAMzC6B,SACiC,OAAzBpB,KAAKW,sBACAA,gBAAgBX,WAEpBD,UAAU,gBAAiBC,KAAKT,YAAYC,IAOrD6B,gBACW,IAAIrB,KAAKT,aAOhBG,kBACOM,KAAKT,YAAYG,KAOxBF,gBACOQ,KAAKT,YAAYC,GAOxB8B,kBACOtB,KAAKgB"} \ No newline at end of file diff --git a/lib/amd/build/local/process_monitor/manager.min.js b/lib/amd/build/local/process_monitor/manager.min.js new file mode 100644 index 00000000000..93a6950ff2c --- /dev/null +++ b/lib/amd/build/local/process_monitor/manager.min.js @@ -0,0 +1,14 @@ +define("core/local/process_monitor/manager",["exports","core/reactive","core/local/process_monitor/events"],(function(_exports,_reactive,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.manager=void 0; +/** + * The reactive file uploader class. + * + * As all the upload queues are reactive, any plugin can implement its own upload monitor. + * + * @module core/local/process_monitor/manager + * @class ProcessMonitorManager + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class ProcessMonitorManager extends _reactive.Reactive{constructor(){var obj,key,value;super(...arguments),value=1,(key="nextId")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}generateProcessId(){return this.nextId++}}const mutations={addProcess:function(stateManager,processData){const state=stateManager.state;stateManager.setReadOnly(!1),state.queue.add({...processData}),state.display.show=!0,stateManager.setReadOnly(!0)},removeProcess:function(stateManager,processId){const state=stateManager.state;stateManager.setReadOnly(!1),state.queue.delete(processId),0===state.queue.size&&(state.display.show=!1),stateManager.setReadOnly(!0)},updateProcess:function(stateManager,processData){if(void 0===processData.id)throw Error("Missing process ID in process data");const state=stateManager.state;stateManager.setReadOnly(!1);const queueItem=state.queue.get(processData.id);if(!queueItem)throw Error("Unkown process with id ".concat(processData.id));for(const[prop,propValue]of Object.entries(processData))queueItem[prop]=propValue;stateManager.setReadOnly(!0)},setShow:function(stateManager,show){const state=stateManager.state;stateManager.setReadOnly(!1),state.display.show=show,show||this.cleanFinishedProcesses(stateManager),stateManager.setReadOnly(!0)},removeAllProcesses:function(stateManager){const state=stateManager.state;stateManager.setReadOnly(!1),state.queue.forEach((element=>{state.queue.delete(element.id)})),state.display.show=!1,stateManager.setReadOnly(!0)},cleanFinishedProcesses:function(stateManager){const state=stateManager.state;stateManager.setReadOnly(!1),state.queue.forEach((element=>{element.finished&&!element.error&&state.queue.delete(element.id)})),0===state.queue.size&&(state.display.show=!1),stateManager.setReadOnly(!0)}},manager=new ProcessMonitorManager({name:"ProcessMonitor",eventName:_events.eventTypes.processMonitorStateChange,eventDispatch:_events.dispatchStateChangedEvent,mutations:mutations,state:{display:{show:!1},queue:[]}});_exports.manager=manager})); + +//# sourceMappingURL=manager.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/process_monitor/manager.min.js.map b/lib/amd/build/local/process_monitor/manager.min.js.map new file mode 100644 index 00000000000..40d141f5ea6 --- /dev/null +++ b/lib/amd/build/local/process_monitor/manager.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"manager.min.js","sources":["../../../src/local/process_monitor/manager.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 * The course file uploader.\n *\n * This module is used to upload files directly into the course.\n *\n * @module core/local/process_monitor/manager\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {Reactive} from 'core/reactive';\nimport {eventTypes, dispatchStateChangedEvent} from 'core/local/process_monitor/events';\n\nconst initialState = {\n display: {\n show: false,\n },\n queue: [],\n};\n\n/**\n * The reactive file uploader class.\n *\n * As all the upload queues are reactive, any plugin can implement its own upload monitor.\n *\n * @module core/local/process_monitor/manager\n * @class ProcessMonitorManager\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nclass ProcessMonitorManager extends Reactive {\n /**\n * The next process id to use.\n *\n * @attribute nextId\n * @type number\n * @default 1\n * @package\n */\n nextId = 1;\n\n /**\n * Generate a unique process id.\n * @return {number} a generated process Id\n */\n generateProcessId() {\n return this.nextId++;\n }\n}\n\n/**\n * @var {Object} mutations the monitor mutations.\n */\nconst mutations = {\n /**\n * Add a new process to the queue.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {Object} processData the upload id to finish\n */\n addProcess: function(stateManager, processData) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.queue.add({...processData});\n state.display.show = true;\n stateManager.setReadOnly(true);\n },\n\n /**\n * Remove a process from the queue.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {Number} processId the process id\n */\n removeProcess: function(stateManager, processId) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.queue.delete(processId);\n if (state.queue.size === 0) {\n state.display.show = false;\n }\n stateManager.setReadOnly(true);\n },\n\n /**\n * Update a process process to the queue.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {Object} processData the upload id to finish\n * @param {Number} processData.id the process id\n */\n updateProcess: function(stateManager, processData) {\n if (processData.id === undefined) {\n throw Error(`Missing process ID in process data`);\n }\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n const queueItem = state.queue.get(processData.id);\n if (!queueItem) {\n throw Error(`Unkown process with id ${processData.id}`);\n }\n for (const [prop, propValue] of Object.entries(processData)) {\n queueItem[prop] = propValue;\n }\n stateManager.setReadOnly(true);\n },\n\n /**\n * Set the monitor show attribute.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {Boolean} show the show value\n */\n setShow: function(stateManager, show) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.display.show = show;\n if (!show) {\n this.cleanFinishedProcesses(stateManager);\n }\n stateManager.setReadOnly(true);\n },\n\n /**\n * Remove a processes from the queue.\n *\n * @param {StateManager} stateManager the current state manager\n */\n removeAllProcesses: function(stateManager) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.queue.forEach((element) => {\n state.queue.delete(element.id);\n });\n state.display.show = false;\n stateManager.setReadOnly(true);\n },\n\n /**\n * Clean all finished processes.\n *\n * @param {StateManager} stateManager the current state manager\n */\n cleanFinishedProcesses: function(stateManager) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.queue.forEach((element) => {\n if (element.finished && !element.error) {\n state.queue.delete(element.id);\n }\n });\n if (state.queue.size === 0) {\n state.display.show = false;\n }\n stateManager.setReadOnly(true);\n },\n};\n\nconst manager = new ProcessMonitorManager({\n name: `ProcessMonitor`,\n eventName: eventTypes.processMonitorStateChange,\n eventDispatch: dispatchStateChangedEvent,\n mutations: mutations,\n state: initialState,\n});\n\nexport {manager};\n"],"names":["ProcessMonitorManager","Reactive","generateProcessId","this","nextId","mutations","addProcess","stateManager","processData","state","setReadOnly","queue","add","display","show","removeProcess","processId","delete","size","updateProcess","undefined","id","Error","queueItem","get","prop","propValue","Object","entries","setShow","cleanFinishedProcesses","removeAllProcesses","forEach","element","finished","error","manager","name","eventName","eventTypes","processMonitorStateChange","eventDispatch","dispatchStateChangedEvent"],"mappings":";;;;;;;;;;;MA6CMA,8BAA8BC,6EASvB,mIAMTC,2BACWC,KAAKC,gBAOdC,UAAY,CAOdC,WAAY,SAASC,aAAcC,mBACzBC,MAAQF,aAAaE,MAC3BF,aAAaG,aAAY,GACzBD,MAAME,MAAMC,IAAI,IAAIJ,cACpBC,MAAMI,QAAQC,MAAO,EACrBP,aAAaG,aAAY,IAS7BK,cAAe,SAASR,aAAcS,iBAC5BP,MAAQF,aAAaE,MAC3BF,aAAaG,aAAY,GACzBD,MAAME,MAAMM,OAAOD,WACM,IAArBP,MAAME,MAAMO,OACZT,MAAMI,QAAQC,MAAO,GAEzBP,aAAaG,aAAY,IAU7BS,cAAe,SAASZ,aAAcC,qBACXY,IAAnBZ,YAAYa,SACNC,kDAEJb,MAAQF,aAAaE,MAC3BF,aAAaG,aAAY,SACnBa,UAAYd,MAAME,MAAMa,IAAIhB,YAAYa,QACzCE,gBACKD,uCAAgCd,YAAYa,SAEjD,MAAOI,KAAMC,aAAcC,OAAOC,QAAQpB,aAC3Ce,UAAUE,MAAQC,UAEtBnB,aAAaG,aAAY,IAS7BmB,QAAS,SAAStB,aAAcO,YACtBL,MAAQF,aAAaE,MAC3BF,aAAaG,aAAY,GACzBD,MAAMI,QAAQC,KAAOA,KAChBA,WACIgB,uBAAuBvB,cAEhCA,aAAaG,aAAY,IAQ7BqB,mBAAoB,SAASxB,oBACnBE,MAAQF,aAAaE,MAC3BF,aAAaG,aAAY,GACzBD,MAAME,MAAMqB,SAASC,UACjBxB,MAAME,MAAMM,OAAOgB,QAAQZ,OAE/BZ,MAAMI,QAAQC,MAAO,EACrBP,aAAaG,aAAY,IAQ7BoB,uBAAwB,SAASvB,oBACvBE,MAAQF,aAAaE,MAC3BF,aAAaG,aAAY,GACzBD,MAAME,MAAMqB,SAASC,UACbA,QAAQC,WAAaD,QAAQE,OAC7B1B,MAAME,MAAMM,OAAOgB,QAAQZ,OAGV,IAArBZ,MAAME,MAAMO,OACZT,MAAMI,QAAQC,MAAO,GAEzBP,aAAaG,aAAY,KAI3B0B,QAAU,IAAIpC,sBAAsB,CACtCqC,sBACAC,UAAWC,mBAAWC,0BACtBC,cAAeC,kCACfrC,UAAWA,UACXI,MAtJiB,CACjBI,QAAS,CACLC,MAAM,GAEVH,MAAO"} \ No newline at end of file diff --git a/lib/amd/build/local/process_monitor/monitor.min.js b/lib/amd/build/local/process_monitor/monitor.min.js new file mode 100644 index 00000000000..97fca51de16 --- /dev/null +++ b/lib/amd/build/local/process_monitor/monitor.min.js @@ -0,0 +1,11 @@ +define("core/local/process_monitor/monitor",["exports","core/templates","core/reactive","core/local/process_monitor/manager"],(function(_exports,_templates,_reactive,_manager){var obj; +/** + * The file upload monitor component. + * + * @module core/local/process_monitor/monitor + * @class core/local/process_monitor/monitor + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};class _default extends _reactive.BaseComponent{create(){this.name="process_monitor",this.selectors={QUEUELIST:'[data-for="process-list"]',CLOSE:'[data-action="hide"]'},this.classes={HIDE:"d-none"}}static init(query,selectors){return new this({element:document.querySelector(query),reactive:_manager.manager,selectors:selectors})}stateReady(state){this._updateMonitor({state:state,element:state.display}),this.addEventListener(this.getElement(this.selectors.CLOSE),"click",this._closeMonitor),state.queue.forEach((element=>{this._createListItem({state:state,element:element})}))}getWatchers(){return[{watch:"queue:created",handler:this._createListItem},{watch:"display:updated",handler:this._updateMonitor}]}async _createListItem(_ref){let{element:element}=_ref;const{html:html,js:js}=await _templates.default.renderForPromise("core/local/process_monitor/process",{...element}),target=this.getElement(this.selectors.QUEUELIST);_templates.default.appendNodeContents(target,html,js)}_updateMonitor(_ref2){let{element:element}=_ref2;this.element.classList.toggle(this.classes.HIDE,!0!==element.show)}_closeMonitor(){this.reactive.dispatch("setShow",!1)}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=monitor.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/process_monitor/monitor.min.js.map b/lib/amd/build/local/process_monitor/monitor.min.js.map new file mode 100644 index 00000000000..32a60c76ff3 --- /dev/null +++ b/lib/amd/build/local/process_monitor/monitor.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"monitor.min.js","sources":["../../../src/local/process_monitor/monitor.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 * The file upload monitor component.\n *\n * @module core/local/process_monitor/monitor\n * @class core/local/process_monitor/monitor\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport {BaseComponent} from 'core/reactive';\nimport {manager} from 'core/local/process_monitor/manager';\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'process_monitor';\n // Default query selectors.\n this.selectors = {\n QUEUELIST: `[data-for=\"process-list\"]`,\n CLOSE: `[data-action=\"hide\"]`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n HIDE: `d-none`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} query the DOM main element query selector\n * @param {object} selectors optional css selector overrides\n * @return {this}\n */\n static init(query, selectors) {\n return new this({\n element: document.querySelector(query),\n reactive: manager,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the initial state\n */\n stateReady(state) {\n this._updateMonitor({state, element: state.display});\n this.addEventListener(this.getElement(this.selectors.CLOSE), 'click', this._closeMonitor);\n state.queue.forEach((element) => {\n this._createListItem({state, element});\n });\n }\n\n /**\n * Return the component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n // State changes that require to reload some course modules.\n {watch: `queue:created`, handler: this._createListItem},\n {watch: `display:updated`, handler: this._updateMonitor},\n ];\n }\n\n /**\n * Create a monitor item.\n *\n * @param {object} args the watcher arguments\n * @param {object} args.element the item state data\n */\n async _createListItem({element}) {\n const {html, js} = await Templates.renderForPromise(\n 'core/local/process_monitor/process',\n {...element}\n );\n const target = this.getElement(this.selectors.QUEUELIST);\n Templates.appendNodeContents(target, html, js);\n }\n\n /**\n * Create a monitor item.\n *\n * @param {object} args the watcher arguments\n * @param {object} args.element the display state data\n */\n _updateMonitor({element}) {\n this.element.classList.toggle(this.classes.HIDE, element.show !== true);\n }\n\n /**\n * Close the monitor.\n */\n _closeMonitor() {\n this.reactive.dispatch('setShow', false);\n }\n}\n"],"names":["BaseComponent","create","name","selectors","QUEUELIST","CLOSE","classes","HIDE","query","this","element","document","querySelector","reactive","manager","stateReady","state","_updateMonitor","display","addEventListener","getElement","_closeMonitor","queue","forEach","_createListItem","getWatchers","watch","handler","html","js","Templates","renderForPromise","target","appendNodeContents","classList","toggle","show","dispatch"],"mappings":";;;;;;;;0KA4B6BA,wBAKzBC,cAESC,KAAO,uBAEPC,UAAY,CACbC,sCACAC,mCAGCC,QAAU,CACXC,2BAWIC,MAAOL,kBACR,IAAIM,KAAK,CACZC,QAASC,SAASC,cAAcJ,OAChCK,SAAUC,iBACVX,UAAAA,YASRY,WAAWC,YACFC,eAAe,CAACD,MAAAA,MAAON,QAASM,MAAME,eACtCC,iBAAiBV,KAAKW,WAAWX,KAAKN,UAAUE,OAAQ,QAASI,KAAKY,eAC3EL,MAAMM,MAAMC,SAASb,eACZc,gBAAgB,CAACR,MAAAA,MAAON,QAAAA,aASrCe,oBACW,CAEH,CAACC,sBAAwBC,QAASlB,KAAKe,iBACvC,CAACE,wBAA0BC,QAASlB,KAAKQ,iDAU3BP,QAACA,oBACbkB,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAC/B,qCACA,IAAIrB,UAEFsB,OAASvB,KAAKW,WAAWX,KAAKN,UAAUC,8BACpC6B,mBAAmBD,OAAQJ,KAAMC,IAS/CZ,0BAAeP,QAACA,oBACPA,QAAQwB,UAAUC,OAAO1B,KAAKH,QAAQC,MAAuB,IAAjBG,QAAQ0B,MAM7Df,qBACSR,SAASwB,SAAS,WAAW"} \ No newline at end of file diff --git a/lib/amd/build/local/process_monitor/process.min.js b/lib/amd/build/local/process_monitor/process.min.js new file mode 100644 index 00000000000..70f3b40010a --- /dev/null +++ b/lib/amd/build/local/process_monitor/process.min.js @@ -0,0 +1,12 @@ +define("core/local/process_monitor/process",["exports","core/reactive","core/local/process_monitor/manager"],(function(_exports,_reactive,_manager){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; +/** + * The process motnitor's process reactive component. + * + * @module core/local/process_monitor/process + * @class core/local/process_monitor/process + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class _default extends _reactive.BaseComponent{create(){this.name="process_monitor_process",this.selectors={CLOSE:'[data-action="closeProcess"]',ERROR:'[data-for="error"]',PROGRESSBAR:"progress",NAME:'[data-for="name"]'},this.classes={HIDE:"d-none"},this.id=this.element.dataset.id}static init(query,selectors){return new this({element:document.querySelector(query),reactive:_manager.manager,selectors:selectors})}stateReady(state){this._refreshItem({state:state,element:state.queue.get(this.id)}),this.addEventListener(this.getElement(this.selectors.CLOSE),"click",this._removeProcess)}getWatchers(){return[{watch:"queue[".concat(this.id,"]:updated"),handler:this._refreshItem},{watch:"queue[".concat(this.id,"]:deleted"),handler:this.remove}]}async _refreshItem(_ref){let{element:element}=_ref;this.getElement(this.selectors.NAME).innerHTML=element.name;const progressbar=this.getElement(this.selectors.PROGRESSBAR);progressbar.classList.toggle(this.classes.HIDE,element.finished),progressbar.value=element.percentage;this.getElement(this.selectors.CLOSE).classList.toggle(this.classes.HIDE,!element.error);const error=this.getElement(this.selectors.ERROR);error.innerHTML=element.error,error.classList.toggle(this.classes.HIDE,!element.error)}_removeProcess(){this.reactive.dispatch("removeProcess",this.id)}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=process.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/process_monitor/process.min.js.map b/lib/amd/build/local/process_monitor/process.min.js.map new file mode 100644 index 00000000000..4abdce3afc9 --- /dev/null +++ b/lib/amd/build/local/process_monitor/process.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"process.min.js","sources":["../../../src/local/process_monitor/process.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 * The process motnitor's process reactive component.\n *\n * @module core/local/process_monitor/process\n * @class core/local/process_monitor/process\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {manager} from 'core/local/process_monitor/manager';\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'process_monitor_process';\n // Default query selectors.\n this.selectors = {\n CLOSE: `[data-action=\"closeProcess\"]`,\n ERROR: `[data-for=\"error\"]`,\n PROGRESSBAR: `progress`,\n NAME: `[data-for=\"name\"]`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n HIDE: `d-none`,\n };\n this.id = this.element.dataset.id;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} query the DOM main element query selector\n * @param {object} selectors optional css selector overrides\n * @return {this}\n */\n static init(query, selectors) {\n return new this({\n element: document.querySelector(query),\n reactive: manager,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the initial state\n */\n stateReady(state) {\n this._refreshItem({state, element: state.queue.get(this.id)});\n this.addEventListener(this.getElement(this.selectors.CLOSE), 'click', this._removeProcess);\n }\n\n /**\n * Return the component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `queue[${this.id}]:updated`, handler: this._refreshItem},\n {watch: `queue[${this.id}]:deleted`, handler: this.remove},\n ];\n }\n\n /**\n * Create a monitor item.\n *\n * @param {object} args the watcher arguments\n * @param {object} args.element the item state data\n */\n async _refreshItem({element}) {\n const name = this.getElement(this.selectors.NAME);\n name.innerHTML = element.name;\n\n const progressbar = this.getElement(this.selectors.PROGRESSBAR);\n progressbar.classList.toggle(this.classes.HIDE, element.finished);\n progressbar.value = element.percentage;\n\n const close = this.getElement(this.selectors.CLOSE);\n close.classList.toggle(this.classes.HIDE, !element.error);\n\n const error = this.getElement(this.selectors.ERROR);\n error.innerHTML = element.error;\n error.classList.toggle(this.classes.HIDE, !element.error);\n }\n\n /**\n * Close the process.\n */\n _removeProcess() {\n this.reactive.dispatch('removeProcess', this.id);\n }\n}\n"],"names":["BaseComponent","create","name","selectors","CLOSE","ERROR","PROGRESSBAR","NAME","classes","HIDE","id","this","element","dataset","query","document","querySelector","reactive","manager","stateReady","state","_refreshItem","queue","get","addEventListener","getElement","_removeProcess","getWatchers","watch","handler","remove","innerHTML","progressbar","classList","toggle","finished","value","percentage","error","dispatch"],"mappings":";;;;;;;;;uBA2B6BA,wBAKzBC,cAESC,KAAO,+BAEPC,UAAY,CACbC,qCACAC,2BACAC,uBACAC,+BAGCC,QAAU,CACXC,oBAECC,GAAKC,KAAKC,QAAQC,QAAQH,eAUvBI,MAAOX,kBACR,IAAIQ,KAAK,CACZC,QAASG,SAASC,cAAcF,OAChCG,SAAUC,iBACVf,UAAAA,YASRgB,WAAWC,YACFC,aAAa,CAACD,MAAAA,MAAOR,QAASQ,MAAME,MAAMC,IAAIZ,KAAKD,WACnDc,iBAAiBb,KAAKc,WAAWd,KAAKR,UAAUC,OAAQ,QAASO,KAAKe,gBAQ/EC,oBACW,CACH,CAACC,sBAAgBjB,KAAKD,gBAAemB,QAASlB,KAAKU,cACnD,CAACO,sBAAgBjB,KAAKD,gBAAemB,QAASlB,KAAKmB,sCAUxClB,QAACA,cACHD,KAAKc,WAAWd,KAAKR,UAAUI,MACvCwB,UAAYnB,QAAQV,WAEnB8B,YAAcrB,KAAKc,WAAWd,KAAKR,UAAUG,aACnD0B,YAAYC,UAAUC,OAAOvB,KAAKH,QAAQC,KAAMG,QAAQuB,UACxDH,YAAYI,MAAQxB,QAAQyB,WAEd1B,KAAKc,WAAWd,KAAKR,UAAUC,OACvC6B,UAAUC,OAAOvB,KAAKH,QAAQC,MAAOG,QAAQ0B,aAE7CA,MAAQ3B,KAAKc,WAAWd,KAAKR,UAAUE,OAC7CiC,MAAMP,UAAYnB,QAAQ0B,MAC1BA,MAAML,UAAUC,OAAOvB,KAAKH,QAAQC,MAAOG,QAAQ0B,OAMvDZ,sBACST,SAASsB,SAAS,gBAAiB5B,KAAKD"} \ No newline at end of file diff --git a/lib/amd/build/local/process_monitor/processqueue.min.js b/lib/amd/build/local/process_monitor/processqueue.min.js new file mode 100644 index 00000000000..9580f36b2ca --- /dev/null +++ b/lib/amd/build/local/process_monitor/processqueue.min.js @@ -0,0 +1,14 @@ +define("core/local/process_monitor/processqueue",["exports","core/utils","core/local/process_monitor/loadingprocess","core/log"],(function(_exports,_utils,_loadingprocess,_log){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.ProcessQueue=void 0,_log=(obj=_log)&&obj.__esModule?obj:{default:obj};_exports.ProcessQueue= +/** + * A process queue manager. + * + * Adding process to the queue will guarante process are executed in sequence. + * + * @module core/local/process_monitor/processqueue + * @class ProcessQueue + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class{constructor(manager){_defineProperty(this,"pending",[]),_defineProperty(this,"currentProcess",null),this.manager=manager,this.cleanFinishedProcesses=(0,_utils.debounce)((()=>manager.dispatch("cleanFinishedProcesses")),3e3)}addPending(processName,processor){const process=new _loadingprocess.LoadingProcess(this.manager,{name:processName});process.setExtraData({processor:processor}),process.onFinish((uploadedFile=>{var _this$currentProcess;(null===(_this$currentProcess=this.currentProcess)||void 0===_this$currentProcess?void 0:_this$currentProcess.id)===uploadedFile.id&&this._discardCurrent()})),this.pending.push(process),this._continueProcessing()}addError(processName,errorMessage){new _loadingprocess.LoadingProcess(this.manager,{name:processName}).setError(errorMessage)}_discardCurrent(){this.currentProcess&&(this.currentProcess=null),this.cleanFinishedProcesses(),this._continueProcessing()}_currentProcessor(){return this.currentProcess.data.processor}async _continueProcessing(){if(null===this.currentProcess&&0!==this.pending.length){this.currentProcess=this.pending.shift();try{const processor=this._currentProcessor();await processor(this.currentProcess)}catch(error){this.currentProcess.setError(error.message),_log.default.error(error)}}}}})); + +//# sourceMappingURL=processqueue.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/process_monitor/processqueue.min.js.map b/lib/amd/build/local/process_monitor/processqueue.min.js.map new file mode 100644 index 00000000000..e6bc81dd58a --- /dev/null +++ b/lib/amd/build/local/process_monitor/processqueue.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"processqueue.min.js","sources":["../../../src/local/process_monitor/processqueue.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 {debounce} from 'core/utils';\nimport {LoadingProcess} from 'core/local/process_monitor/loadingprocess';\nimport log from 'core/log';\n\nconst TOASTSTIMER = 3000;\n\n/**\n * A process queue manager.\n *\n * Adding process to the queue will guarante process are executed in sequence.\n *\n * @module core/local/process_monitor/processqueue\n * @class ProcessQueue\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport class ProcessQueue {\n /** @var {Array} pending the pending queue. */\n pending = [];\n\n /** @var {LoadingProcess} current the current uploading process. */\n currentProcess = null;\n\n /**\n * Class constructor.\n * @param {ProcessMonitorManager} manager the monitor manager\n */\n constructor(manager) {\n this.manager = manager;\n this.cleanFinishedProcesses = debounce(\n () => manager.dispatch('cleanFinishedProcesses'),\n TOASTSTIMER\n );\n }\n\n /**\n * Adds a new pending upload to the queue.\n * @param {String} processName the process name\n * @param {Function} processor the execution function\n */\n addPending(processName, processor) {\n const process = new LoadingProcess(this.manager, {name: processName});\n process.setExtraData({\n processor,\n });\n process.onFinish((uploadedFile) => {\n if (this.currentProcess?.id !== uploadedFile.id) {\n return;\n }\n this._discardCurrent();\n });\n this.pending.push(process);\n this._continueProcessing();\n }\n\n /**\n * Adds a new pending upload to the queue.\n * @param {String} processName the file info\n * @param {String} errorMessage the file processor\n */\n addError(processName, errorMessage) {\n const process = new LoadingProcess(this.manager, {name: processName});\n process.setError(errorMessage);\n }\n\n /**\n * Discard the current process and execute the next one if any.\n */\n _discardCurrent() {\n if (this.currentProcess) {\n this.currentProcess = null;\n }\n this.cleanFinishedProcesses();\n this._continueProcessing();\n }\n\n /**\n * Return the current file uploader.\n * @return {FileUploader}\n */\n _currentProcessor() {\n return this.currentProcess.data.processor;\n }\n\n /**\n * Continue the queue processing if no current process is defined.\n */\n async _continueProcessing() {\n if (this.currentProcess !== null || this.pending.length === 0) {\n return;\n }\n this.currentProcess = this.pending.shift();\n try {\n const processor = this._currentProcessor();\n await processor(this.currentProcess);\n } catch (error) {\n this.currentProcess.setError(error.message);\n log.error(error);\n }\n }\n}\n"],"names":["constructor","manager","cleanFinishedProcesses","dispatch","addPending","processName","processor","process","LoadingProcess","this","name","setExtraData","onFinish","uploadedFile","currentProcess","id","_discardCurrent","pending","push","_continueProcessing","addError","errorMessage","setError","_currentProcessor","data","length","shift","error","message"],"mappings":";;;;;;;;;;;MA0CIA,YAAYC,wCATF,0CAGO,WAORA,QAAUA,aACVC,wBAAyB,oBAC1B,IAAMD,QAAQE,SAAS,2BA1Bf,KAoChBC,WAAWC,YAAaC,iBACdC,QAAU,IAAIC,+BAAeC,KAAKR,QAAS,CAACS,KAAML,cACxDE,QAAQI,aAAa,CACjBL,UAAAA,YAEJC,QAAQK,UAAUC,2EACLC,2EAAgBC,MAAOF,aAAaE,SAGxCC,0BAEJC,QAAQC,KAAKX,cACbY,sBAQTC,SAASf,YAAagB,cACF,IAAIb,+BAAeC,KAAKR,QAAS,CAACS,KAAML,cAChDiB,SAASD,cAMrBL,kBACQP,KAAKK,sBACAA,eAAiB,WAErBZ,8BACAiB,sBAOTI,2BACWd,KAAKK,eAAeU,KAAKlB,yCAOJ,OAAxBG,KAAKK,gBAAmD,IAAxBL,KAAKQ,QAAQQ,aAG5CX,eAAiBL,KAAKQ,QAAQS,kBAEzBpB,UAAYG,KAAKc,0BACjBjB,UAAUG,KAAKK,gBACvB,MAAOa,YACAb,eAAeQ,SAASK,MAAMC,sBAC/BD,MAAMA"} \ No newline at end of file diff --git a/lib/amd/build/process_monitor.min.js b/lib/amd/build/process_monitor.min.js new file mode 100644 index 00000000000..db18382a311 --- /dev/null +++ b/lib/amd/build/process_monitor.min.js @@ -0,0 +1,10 @@ +define("core/process_monitor",["exports","core/log","core/local/process_monitor/manager","core/local/process_monitor/loadingprocess","core/local/process_monitor/processqueue","core/templates"],(function(_exports,_log,_manager,_loadingprocess,_processqueue,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * Process monitor includer. + * + * @module core/process_monitor + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.processMonitor=void 0,_log=_interopRequireDefault(_log),_templates=_interopRequireDefault(_templates);let initialized=!1;const processMonitor={addLoadingProcess:function(definition){this.initProcessMonitor();return new _loadingprocess.LoadingProcess(_manager.manager,definition)},removeAllProcesses:function(){_manager.manager.getInitialStatePromise().then((()=>{_manager.manager.dispatch("removeAllProcesses")})).catch((()=>{_log.default.error("Cannot update process monitor.")}))},initProcessMonitor:async function(){if(initialized)return;initialized=!0;const container=null!==(_document$querySelect=document.querySelector("#page"))&&void 0!==_document$querySelect?_document$querySelect:document.body;var _document$querySelect;if(!document.getElementById("#processMonitor"))try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/process_monitor/monitor",{});_templates.default.appendNodeContents(container,html,js)}catch(error){_log.default.error("Cannot load the process monitor")}},getInitialStatePromise:function(){return _manager.manager.getInitialStatePromise()},createProcessQueue:async function(){processMonitor.initProcessMonitor();const processQueue=new _processqueue.ProcessQueue(_manager.manager);return await processMonitor.getInitialStatePromise(),processQueue}};_exports.processMonitor=processMonitor})); + +//# sourceMappingURL=process_monitor.min.js.map \ No newline at end of file diff --git a/lib/amd/build/process_monitor.min.js.map b/lib/amd/build/process_monitor.min.js.map new file mode 100644 index 00000000000..28d4c2e0614 --- /dev/null +++ b/lib/amd/build/process_monitor.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"process_monitor.min.js","sources":["../src/process_monitor.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 * Process monitor includer.\n *\n * @module core/process_monitor\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport log from 'core/log';\nimport {manager} from 'core/local/process_monitor/manager';\nimport {LoadingProcess} from 'core/local/process_monitor/loadingprocess';\nimport {ProcessQueue} from 'core/local/process_monitor/processqueue';\nimport Templates from 'core/templates';\n\nlet initialized = false;\n\n/**\n * Get the parent container.\n * @private\n * @return {HTMLelement} the process monitor container.\n */\nconst getParentContainer = () => {\n // The footer pop over depends on the theme.\n return document.querySelector(`#page`) ?? document.body;\n};\n\nexport const processMonitor = {\n /**\n * Adds a new process to the monitor.\n * @param {Object} definition the process definition\n * @param {String} definition.name the process name\n * @param {Number} definition.percentage the current percentage (0 - 100)\n * @param {String} definition.error the error message if any\n * @param {String} definition.url possible link url if any\n * @returns {LoadingProcess} the loading process\n */\n addLoadingProcess: function(definition) {\n this.initProcessMonitor();\n const process = new LoadingProcess(manager, definition);\n return process;\n },\n\n /**\n * Remove all processes form the current monitor.\n */\n removeAllProcesses: function() {\n manager.getInitialStatePromise().then(() => {\n manager.dispatch('removeAllProcesses');\n return;\n }).catch(() => {\n log.error(`Cannot update process monitor.`);\n });\n },\n\n /**\n * Initialize the process monitor.\n */\n initProcessMonitor: async function() {\n if (initialized) {\n return;\n }\n initialized = true;\n const container = getParentContainer();\n if (document.getElementById(`#processMonitor`)) {\n return;\n }\n try {\n const {html, js} = await Templates.renderForPromise('core/local/process_monitor/monitor', {});\n Templates.appendNodeContents(container, html, js);\n } catch (error) {\n log.error(`Cannot load the process monitor`);\n }\n },\n\n /**\n * Return the process monitor initial state promise.\n * @returns {Promise} Promise of the initial state fully loaded\n */\n getInitialStatePromise: function() {\n return manager.getInitialStatePromise();\n },\n\n /**\n * Load the load queue monitor.\n *\n * @return {Promise} when the file uploader is ready to be used.\n */\n createProcessQueue: async function() {\n processMonitor.initProcessMonitor();\n const processQueue = new ProcessQueue(manager);\n await processMonitor.getInitialStatePromise();\n return processQueue;\n }\n};\n"],"names":["initialized","processMonitor","addLoadingProcess","definition","initProcessMonitor","LoadingProcess","manager","removeAllProcesses","getInitialStatePromise","then","dispatch","catch","error","async","container","document","querySelector","body","getElementById","html","js","Templates","renderForPromise","appendNodeContents","createProcessQueue","processQueue","ProcessQueue"],"mappings":";;;;;;;gLA6BIA,aAAc,QAYLC,eAAiB,CAU1BC,kBAAmB,SAASC,iBACnBC,4BACW,IAAIC,+BAAeC,iBAASH,aAOhDI,mBAAoB,4BACRC,yBAAyBC,MAAK,sBAC1BC,SAAS,yBAElBC,OAAM,kBACDC,4CAOZR,mBAAoBS,oBACZb,mBAGJA,aAAc,QACRc,wCAvCHC,SAASC,8EAA0BD,SAASE,KAF5B,8BA0CfF,SAASG,4CAIHC,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAAiB,qCAAsC,uBAChFC,mBAAmBT,UAAWK,KAAMC,IAChD,MAAOR,oBACDA,2CAQZJ,uBAAwB,kBACbF,iBAAQE,0BAQnBgB,mBAAoBX,iBAChBZ,eAAeG,2BACTqB,aAAe,IAAIC,2BAAapB,+BAChCL,eAAeO,yBACdiB"} \ No newline at end of file diff --git a/lib/amd/src/local/process_monitor/events.js b/lib/amd/src/local/process_monitor/events.js new file mode 100644 index 00000000000..04cafe8f941 --- /dev/null +++ b/lib/amd/src/local/process_monitor/events.js @@ -0,0 +1,58 @@ +// 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 . + +/** + * Javascript events for the `process_monitor` module. + * + * @module core/local/process_monitor/events + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 4.2 + */ + +/** + * Events for the `core_editor` subsystem. + * + * @constant + * @property {String} processMonitorStateChange See {@link event:processMonitorStateChange} + */ +export const eventTypes = { + /** + * An event triggered when the monitor state has changed. + * + * @event processMonitorStateChange + */ + processMonitorStateChange: 'core_editor/contentRestored', +}; + +/** + * Trigger a state changed event. + * + * @method dispatchStateChangedEvent + * @param {Object} detail the full state + * @param {Object} target the custom event target (document if none provided) + * @param {Function} target.dispatchEvent the component dispatch event method. + */ +export function dispatchStateChangedEvent(detail, target) { + if (target === undefined) { + target = document; + } + target.dispatchEvent(new CustomEvent( + eventTypes.processMonitorStateChange, + { + bubbles: true, + detail: detail, + } + )); +} diff --git a/lib/amd/src/local/process_monitor/loadingprocess.js b/lib/amd/src/local/process_monitor/loadingprocess.js new file mode 100644 index 00000000000..8b25ee96497 --- /dev/null +++ b/lib/amd/src/local/process_monitor/loadingprocess.js @@ -0,0 +1,211 @@ +// 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 . + +/** + * The process wrapper class. + * + * This module is used to update a process in the process monitor. + * + * @module core/local/process_monitor/loadingprocess + * @class LoadingProcess + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import log from 'core/log'; + +export class LoadingProcess { + + /** @var {Map} editorUpdates the courses pending to be updated. */ + processData = null; + + /** @var {Object} extraData any extra process information to store. */ + extraData = null; + + /** @var {ProcessMonitorManager} manager the page monitor. */ + manager = null; + + /** @var {Function} finishedCallback the finished callback if any. */ + finishedCallback = null; + + /** @var {Function} removedCallback the removed callback if any. */ + removedCallback = null; + + /** @var {Function} errorCallback the error callback if any. */ + errorCallback = null; + + /** + * Class constructor + * @param {ProcessMonitorManager} manager the monitor manager + * @param {Object} definition the process definition data + */ + constructor(manager, definition) { + this.manager = manager; + // Add defaults. + this.processData = { + id: manager.generateProcessId(), + name: '', + percentage: 0, + url: null, + error: null, + finished: false, + ...definition, + }; + // Create a new entry. + this._dispatch('addProcess', this.processData); + } + + /** + * Execute a monitor manager mutation when the state is ready. + * + * @private + * @param {String} action the mutation to dispatch + * @param {*} params the mutaiton params + */ + _dispatch(action, params) { + this.manager.getInitialStatePromise().then(() => { + this.manager.dispatch(action, params); + return; + }).catch(() => { + log.error(`Cannot update process monitor.`); + }); + } + + /** + * Define a finished process callback function. + * @param {Function} callback the callback function + */ + onFinish(callback) { + this.finishedCallback = callback; + } + + /** + * Define a removed from monitor process callback function. + * @param {Function} callback the callback function + */ + onRemove(callback) { + this.removedCallback = callback; + } + + /** + * Define a error process callback function. + * @param {Function} callback the callback function + */ + onError(callback) { + this.errorCallback = callback; + } + + /** + * Set the process percentage. + * @param {Number} percentage + */ + setPercentage(percentage) { + this.processData.percentage = percentage; + this._dispatch('updateProcess', this.processData); + } + + /** + * Stores extra information to the process. + * + * This method is used to add information like the course, the user + * or any other needed information. + * + * @param {Object} extraData any extra process information to store + */ + setExtraData(extraData) { + this.extraData = extraData; + } + + /** + * Set the process error string. + * + * Note: set the error message will mark the process as finished. + * + * @param {String} error the string message + */ + setError(error) { + this.processData.error = error; + if (this.errorCallback !== null) { + this.errorCallback(this); + } + this.processData.finished = true; + if (this.finishedCallback !== null) { + this.finishedCallback(this); + } + this._dispatch('updateProcess', this.processData); + } + + /** + * Rename the process + * @param {String} name the new process name + */ + setName(name) { + this.processData.name = name; + this._dispatch('updateProcess', this.processData); + } + + /** + * Mark the process as finished. + */ + finish() { + this.processData.finished = true; + if (this.finishedCallback !== null) { + this.finishedCallback(this); + } + this._dispatch('updateProcess', this.processData); + } + + /** + * Remove the process from the monitor. + */ + remove() { + if (this.removedCallback !== null) { + this.removedCallback(this); + } + this._dispatch('removeProcess', this.processData.id); + } + + /** + * Returns the current rpocess data. + * @returns {Object} the process data + */ + getData() { + return {...this.processData}; + } + + /** + * Return the process name + * @return {String} + */ + get name() { + return this.processData.name; + } + + /** + * Return the process internal id + * @return {Number} + */ + get id() { + return this.processData.id; + } + + /** + * Return the process extra data. + * @return {*} whatever is in extra data + */ + get data() { + return this.extraData; + } +} diff --git a/lib/amd/src/local/process_monitor/manager.js b/lib/amd/src/local/process_monitor/manager.js new file mode 100644 index 00000000000..b30d7008b5d --- /dev/null +++ b/lib/amd/src/local/process_monitor/manager.js @@ -0,0 +1,182 @@ +// 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 . + +/** + * The course file uploader. + * + * This module is used to upload files directly into the course. + * + * @module core/local/process_monitor/manager + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {Reactive} from 'core/reactive'; +import {eventTypes, dispatchStateChangedEvent} from 'core/local/process_monitor/events'; + +const initialState = { + display: { + show: false, + }, + queue: [], +}; + +/** + * The reactive file uploader class. + * + * As all the upload queues are reactive, any plugin can implement its own upload monitor. + * + * @module core/local/process_monitor/manager + * @class ProcessMonitorManager + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class ProcessMonitorManager extends Reactive { + /** + * The next process id to use. + * + * @attribute nextId + * @type number + * @default 1 + * @package + */ + nextId = 1; + + /** + * Generate a unique process id. + * @return {number} a generated process Id + */ + generateProcessId() { + return this.nextId++; + } +} + +/** + * @var {Object} mutations the monitor mutations. + */ +const mutations = { + /** + * Add a new process to the queue. + * + * @param {StateManager} stateManager the current state manager + * @param {Object} processData the upload id to finish + */ + addProcess: function(stateManager, processData) { + const state = stateManager.state; + stateManager.setReadOnly(false); + state.queue.add({...processData}); + state.display.show = true; + stateManager.setReadOnly(true); + }, + + /** + * Remove a process from the queue. + * + * @param {StateManager} stateManager the current state manager + * @param {Number} processId the process id + */ + removeProcess: function(stateManager, processId) { + const state = stateManager.state; + stateManager.setReadOnly(false); + state.queue.delete(processId); + if (state.queue.size === 0) { + state.display.show = false; + } + stateManager.setReadOnly(true); + }, + + /** + * Update a process process to the queue. + * + * @param {StateManager} stateManager the current state manager + * @param {Object} processData the upload id to finish + * @param {Number} processData.id the process id + */ + updateProcess: function(stateManager, processData) { + if (processData.id === undefined) { + throw Error(`Missing process ID in process data`); + } + const state = stateManager.state; + stateManager.setReadOnly(false); + const queueItem = state.queue.get(processData.id); + if (!queueItem) { + throw Error(`Unkown process with id ${processData.id}`); + } + for (const [prop, propValue] of Object.entries(processData)) { + queueItem[prop] = propValue; + } + stateManager.setReadOnly(true); + }, + + /** + * Set the monitor show attribute. + * + * @param {StateManager} stateManager the current state manager + * @param {Boolean} show the show value + */ + setShow: function(stateManager, show) { + const state = stateManager.state; + stateManager.setReadOnly(false); + state.display.show = show; + if (!show) { + this.cleanFinishedProcesses(stateManager); + } + stateManager.setReadOnly(true); + }, + + /** + * Remove a processes from the queue. + * + * @param {StateManager} stateManager the current state manager + */ + removeAllProcesses: function(stateManager) { + const state = stateManager.state; + stateManager.setReadOnly(false); + state.queue.forEach((element) => { + state.queue.delete(element.id); + }); + state.display.show = false; + stateManager.setReadOnly(true); + }, + + /** + * Clean all finished processes. + * + * @param {StateManager} stateManager the current state manager + */ + cleanFinishedProcesses: function(stateManager) { + const state = stateManager.state; + stateManager.setReadOnly(false); + state.queue.forEach((element) => { + if (element.finished && !element.error) { + state.queue.delete(element.id); + } + }); + if (state.queue.size === 0) { + state.display.show = false; + } + stateManager.setReadOnly(true); + }, +}; + +const manager = new ProcessMonitorManager({ + name: `ProcessMonitor`, + eventName: eventTypes.processMonitorStateChange, + eventDispatch: dispatchStateChangedEvent, + mutations: mutations, + state: initialState, +}); + +export {manager}; diff --git a/lib/amd/src/local/process_monitor/monitor.js b/lib/amd/src/local/process_monitor/monitor.js new file mode 100644 index 00000000000..0d44848c999 --- /dev/null +++ b/lib/amd/src/local/process_monitor/monitor.js @@ -0,0 +1,120 @@ +// 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 . + +/** + * The file upload monitor component. + * + * @module core/local/process_monitor/monitor + * @class core/local/process_monitor/monitor + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Templates from 'core/templates'; +import {BaseComponent} from 'core/reactive'; +import {manager} from 'core/local/process_monitor/manager'; + +export default class extends BaseComponent { + + /** + * Constructor hook. + */ + create() { + // Optional component name for debugging. + this.name = 'process_monitor'; + // Default query selectors. + this.selectors = { + QUEUELIST: `[data-for="process-list"]`, + CLOSE: `[data-action="hide"]`, + }; + // Default classes to toggle on refresh. + this.classes = { + HIDE: `d-none`, + }; + } + + /** + * Static method to create a component instance form the mustache template. + * + * @param {string} query the DOM main element query selector + * @param {object} selectors optional css selector overrides + * @return {this} + */ + static init(query, selectors) { + return new this({ + element: document.querySelector(query), + reactive: manager, + selectors, + }); + } + + /** + * Initial state ready method. + * + * @param {Object} state the initial state + */ + stateReady(state) { + this._updateMonitor({state, element: state.display}); + this.addEventListener(this.getElement(this.selectors.CLOSE), 'click', this._closeMonitor); + state.queue.forEach((element) => { + this._createListItem({state, element}); + }); + } + + /** + * Return the component watchers. + * + * @returns {Array} of watchers + */ + getWatchers() { + return [ + // State changes that require to reload some course modules. + {watch: `queue:created`, handler: this._createListItem}, + {watch: `display:updated`, handler: this._updateMonitor}, + ]; + } + + /** + * Create a monitor item. + * + * @param {object} args the watcher arguments + * @param {object} args.element the item state data + */ + async _createListItem({element}) { + const {html, js} = await Templates.renderForPromise( + 'core/local/process_monitor/process', + {...element} + ); + const target = this.getElement(this.selectors.QUEUELIST); + Templates.appendNodeContents(target, html, js); + } + + /** + * Create a monitor item. + * + * @param {object} args the watcher arguments + * @param {object} args.element the display state data + */ + _updateMonitor({element}) { + this.element.classList.toggle(this.classes.HIDE, element.show !== true); + } + + /** + * Close the monitor. + */ + _closeMonitor() { + this.reactive.dispatch('setShow', false); + } +} diff --git a/lib/amd/src/local/process_monitor/process.js b/lib/amd/src/local/process_monitor/process.js new file mode 100644 index 00000000000..aca58444019 --- /dev/null +++ b/lib/amd/src/local/process_monitor/process.js @@ -0,0 +1,115 @@ +// 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 . + +/** + * The process motnitor's process reactive component. + * + * @module core/local/process_monitor/process + * @class core/local/process_monitor/process + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {BaseComponent} from 'core/reactive'; +import {manager} from 'core/local/process_monitor/manager'; + +export default class extends BaseComponent { + + /** + * Constructor hook. + */ + create() { + // Optional component name for debugging. + this.name = 'process_monitor_process'; + // Default query selectors. + this.selectors = { + CLOSE: `[data-action="closeProcess"]`, + ERROR: `[data-for="error"]`, + PROGRESSBAR: `progress`, + NAME: `[data-for="name"]`, + }; + // Default classes to toggle on refresh. + this.classes = { + HIDE: `d-none`, + }; + this.id = this.element.dataset.id; + } + + /** + * Static method to create a component instance form the mustache template. + * + * @param {string} query the DOM main element query selector + * @param {object} selectors optional css selector overrides + * @return {this} + */ + static init(query, selectors) { + return new this({ + element: document.querySelector(query), + reactive: manager, + selectors, + }); + } + + /** + * Initial state ready method. + * + * @param {Object} state the initial state + */ + stateReady(state) { + this._refreshItem({state, element: state.queue.get(this.id)}); + this.addEventListener(this.getElement(this.selectors.CLOSE), 'click', this._removeProcess); + } + + /** + * Return the component watchers. + * + * @returns {Array} of watchers + */ + getWatchers() { + return [ + {watch: `queue[${this.id}]:updated`, handler: this._refreshItem}, + {watch: `queue[${this.id}]:deleted`, handler: this.remove}, + ]; + } + + /** + * Create a monitor item. + * + * @param {object} args the watcher arguments + * @param {object} args.element the item state data + */ + async _refreshItem({element}) { + const name = this.getElement(this.selectors.NAME); + name.innerHTML = element.name; + + const progressbar = this.getElement(this.selectors.PROGRESSBAR); + progressbar.classList.toggle(this.classes.HIDE, element.finished); + progressbar.value = element.percentage; + + const close = this.getElement(this.selectors.CLOSE); + close.classList.toggle(this.classes.HIDE, !element.error); + + const error = this.getElement(this.selectors.ERROR); + error.innerHTML = element.error; + error.classList.toggle(this.classes.HIDE, !element.error); + } + + /** + * Close the process. + */ + _removeProcess() { + this.reactive.dispatch('removeProcess', this.id); + } +} diff --git a/lib/amd/src/local/process_monitor/processqueue.js b/lib/amd/src/local/process_monitor/processqueue.js new file mode 100644 index 00000000000..c3290e77956 --- /dev/null +++ b/lib/amd/src/local/process_monitor/processqueue.js @@ -0,0 +1,116 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +import {debounce} from 'core/utils'; +import {LoadingProcess} from 'core/local/process_monitor/loadingprocess'; +import log from 'core/log'; + +const TOASTSTIMER = 3000; + +/** + * A process queue manager. + * + * Adding process to the queue will guarante process are executed in sequence. + * + * @module core/local/process_monitor/processqueue + * @class ProcessQueue + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +export class ProcessQueue { + /** @var {Array} pending the pending queue. */ + pending = []; + + /** @var {LoadingProcess} current the current uploading process. */ + currentProcess = null; + + /** + * Class constructor. + * @param {ProcessMonitorManager} manager the monitor manager + */ + constructor(manager) { + this.manager = manager; + this.cleanFinishedProcesses = debounce( + () => manager.dispatch('cleanFinishedProcesses'), + TOASTSTIMER + ); + } + + /** + * Adds a new pending upload to the queue. + * @param {String} processName the process name + * @param {Function} processor the execution function + */ + addPending(processName, processor) { + const process = new LoadingProcess(this.manager, {name: processName}); + process.setExtraData({ + processor, + }); + process.onFinish((uploadedFile) => { + if (this.currentProcess?.id !== uploadedFile.id) { + return; + } + this._discardCurrent(); + }); + this.pending.push(process); + this._continueProcessing(); + } + + /** + * Adds a new pending upload to the queue. + * @param {String} processName the file info + * @param {String} errorMessage the file processor + */ + addError(processName, errorMessage) { + const process = new LoadingProcess(this.manager, {name: processName}); + process.setError(errorMessage); + } + + /** + * Discard the current process and execute the next one if any. + */ + _discardCurrent() { + if (this.currentProcess) { + this.currentProcess = null; + } + this.cleanFinishedProcesses(); + this._continueProcessing(); + } + + /** + * Return the current file uploader. + * @return {FileUploader} + */ + _currentProcessor() { + return this.currentProcess.data.processor; + } + + /** + * Continue the queue processing if no current process is defined. + */ + async _continueProcessing() { + if (this.currentProcess !== null || this.pending.length === 0) { + return; + } + this.currentProcess = this.pending.shift(); + try { + const processor = this._currentProcessor(); + await processor(this.currentProcess); + } catch (error) { + this.currentProcess.setError(error.message); + log.error(error); + } + } +} diff --git a/lib/amd/src/process_monitor.js b/lib/amd/src/process_monitor.js new file mode 100644 index 00000000000..3e7c73f553e --- /dev/null +++ b/lib/amd/src/process_monitor.js @@ -0,0 +1,109 @@ +// 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 . + +/** + * Process monitor includer. + * + * @module core/process_monitor + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import log from 'core/log'; +import {manager} from 'core/local/process_monitor/manager'; +import {LoadingProcess} from 'core/local/process_monitor/loadingprocess'; +import {ProcessQueue} from 'core/local/process_monitor/processqueue'; +import Templates from 'core/templates'; + +let initialized = false; + +/** + * Get the parent container. + * @private + * @return {HTMLelement} the process monitor container. + */ +const getParentContainer = () => { + // The footer pop over depends on the theme. + return document.querySelector(`#page`) ?? document.body; +}; + +export const processMonitor = { + /** + * Adds a new process to the monitor. + * @param {Object} definition the process definition + * @param {String} definition.name the process name + * @param {Number} definition.percentage the current percentage (0 - 100) + * @param {String} definition.error the error message if any + * @param {String} definition.url possible link url if any + * @returns {LoadingProcess} the loading process + */ + addLoadingProcess: function(definition) { + this.initProcessMonitor(); + const process = new LoadingProcess(manager, definition); + return process; + }, + + /** + * Remove all processes form the current monitor. + */ + removeAllProcesses: function() { + manager.getInitialStatePromise().then(() => { + manager.dispatch('removeAllProcesses'); + return; + }).catch(() => { + log.error(`Cannot update process monitor.`); + }); + }, + + /** + * Initialize the process monitor. + */ + initProcessMonitor: async function() { + if (initialized) { + return; + } + initialized = true; + const container = getParentContainer(); + if (document.getElementById(`#processMonitor`)) { + return; + } + try { + const {html, js} = await Templates.renderForPromise('core/local/process_monitor/monitor', {}); + Templates.appendNodeContents(container, html, js); + } catch (error) { + log.error(`Cannot load the process monitor`); + } + }, + + /** + * Return the process monitor initial state promise. + * @returns {Promise} Promise of the initial state fully loaded + */ + getInitialStatePromise: function() { + return manager.getInitialStatePromise(); + }, + + /** + * Load the load queue monitor. + * + * @return {Promise} when the file uploader is ready to be used. + */ + createProcessQueue: async function() { + processMonitor.initProcessMonitor(); + const processQueue = new ProcessQueue(manager); + await processMonitor.getInitialStatePromise(); + return processQueue; + } +}; diff --git a/lib/templates/local/process_monitor/monitor.mustache b/lib/templates/local/process_monitor/monitor.mustache new file mode 100644 index 00000000000..ef0e5fd05dc --- /dev/null +++ b/lib/templates/local/process_monitor/monitor.mustache @@ -0,0 +1,57 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core/local/process_monitor/monitor + + Template to render the global reactive debug panel. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Example context (json): + { + "title": "Some title" + } +}} +
+ +
+
+{{#js}} +require(['core/local/process_monitor/monitor'], function(component) { + component.init('#process-monitor-{{uniqid}}'); +}); +{{/js}} diff --git a/lib/templates/local/process_monitor/process.mustache b/lib/templates/local/process_monitor/process.mustache new file mode 100644 index 00000000000..e40aa5636f7 --- /dev/null +++ b/lib/templates/local/process_monitor/process.mustache @@ -0,0 +1,57 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core/local/process_monitor/process + + Template to render a process inside the process monitor. + + Example context (json): + { + "id": 42, + "name": "Sample", + "percentage": 30, + "error": "Something goes wrong" + } +}} +
+
+
{{name}}
+
+ + +
+
+ +
+{{#js}} +require(['core/local/process_monitor/process'], function(component) { + component.init('[data-for="queue-process"][data-id="{{id}}"]'); +}); +{{/js}} diff --git a/theme/boost/scss/moodle.scss b/theme/boost/scss/moodle.scss index c2afe26ca39..d41118cca32 100644 --- a/theme/boost/scss/moodle.scss +++ b/theme/boost/scss/moodle.scss @@ -48,3 +48,4 @@ $breadcrumb-divider-rtl: "â—€" !default; @import "moodle/primarynavigation"; @import "moodle/secondarynavigation"; @import "moodle/tertiarynavigation"; +@import "moodle/process-monitor"; diff --git a/theme/boost/scss/moodle/process-monitor.scss b/theme/boost/scss/moodle/process-monitor.scss new file mode 100644 index 00000000000..04bd657488b --- /dev/null +++ b/theme/boost/scss/moodle/process-monitor.scss @@ -0,0 +1,30 @@ +// The popover process monitor. +$popover-process-monitor-right: 2rem !default; +$popover-process-monitor-bottom: 5rem !default; +$popover-process-monitor-max-height: 30vh !default; +$popover-process-monitor-width: 350px !default; +$popover-process-monitor-scroll-bg: $gray-100 !default; + +.popover-process-monitor { + position: fixed; + right: $popover-process-monitor-right; + bottom: $popover-process-monitor-bottom; + width: $popover-process-monitor-width; + background-color: $white; + @include border-radius(); + border: $border-width solid $border-color; + + .process-list { + max-height: $popover-process-monitor-max-height; + overflow: auto; + @include thin-scrolls($popover-process-monitor-scroll-bg); + } + + .queue-process { + border-bottom: 1px solid $gray-200; + } + + .queue-process:last-child { + border-bottom: 0; + } +} diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index e7ba43bef0c..b52c4da99e5 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -22024,6 +22024,34 @@ div.editor_atto_toolbar button .icon { .tertiary-navigation { display: none; } } +.popover-process-monitor { + position: fixed; + right: 2rem; + bottom: 5rem; + width: 350px; + background-color: #fff; + border-radius: 0.5rem; + border: 1px solid #dee2e6; } + .popover-process-monitor .process-list { + max-height: 30vh; + overflow: auto; + scrollbar-width: thin; + scrollbar-color: #6a737b #f8f9fa; } + .popover-process-monitor .process-list::-webkit-scrollbar { + width: 12px; } + .popover-process-monitor .process-list::-webkit-scrollbar-track { + background: #f8f9fa; } + .popover-process-monitor .process-list::-webkit-scrollbar-thumb { + background-color: #6a737b; + border-radius: 20px; + border: 3px solid #f8f9fa; } + .popover-process-monitor .process-list::-webkit-scrollbar-thumb:hover { + background-color: #495057; } + .popover-process-monitor .queue-process { + border-bottom: 1px solid #e9ecef; } + .popover-process-monitor .queue-process:last-child { + border-bottom: 0; } + body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 11278534562..76bf20a459d 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -21970,6 +21970,34 @@ div.editor_atto_toolbar button .icon { .tertiary-navigation { display: none; } } +.popover-process-monitor { + position: fixed; + right: 2rem; + bottom: 5rem; + width: 350px; + background-color: #fff; + border-radius: 0.25rem; + border: 1px solid #dee2e6; } + .popover-process-monitor .process-list { + max-height: 30vh; + overflow: auto; + scrollbar-width: thin; + scrollbar-color: #6a737b #f8f9fa; } + .popover-process-monitor .process-list::-webkit-scrollbar { + width: 12px; } + .popover-process-monitor .process-list::-webkit-scrollbar-track { + background: #f8f9fa; } + .popover-process-monitor .process-list::-webkit-scrollbar-thumb { + background-color: #6a737b; + border-radius: 20px; + border: 3px solid #f8f9fa; } + .popover-process-monitor .process-list::-webkit-scrollbar-thumb:hover { + background-color: #495057; } + .popover-process-monitor .queue-process { + border-bottom: 1px solid #e9ecef; } + .popover-process-monitor .queue-process:last-child { + border-bottom: 0; } + body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } From e359b9889abfc081d150a9b5571da6c056d39657 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Wed, 30 Nov 2022 15:59:31 +0100 Subject: [PATCH 3/5] MDL-76432 core: reactive drapdrop file support This commit adds all the necessary CSS and logic to handle file dropping into a reactive compoment. From now on, a reactive application can handle both element drag&drop and file drop easily. --- .../build/local/reactive/basecomponent.min.js | 4 +- .../local/reactive/basecomponent.min.js.map | 2 +- lib/amd/build/local/reactive/dragdrop.min.js | 2 +- .../build/local/reactive/dragdrop.min.js.map | 2 +- lib/amd/build/local/reactive/overlay.min.js | 13 ++ .../build/local/reactive/overlay.min.js.map | 1 + lib/amd/src/local/reactive/basecomponent.js | 40 ++++ lib/amd/src/local/reactive/dragdrop.js | 54 +++++- lib/amd/src/local/reactive/overlay.js | 171 ++++++++++++++++++ lib/templates/local/reactive/overlay.mustache | 36 ++++ theme/boost/scss/moodle/core.scss | 35 ++++ theme/boost/style/moodle.css | 29 +++ theme/classic/style/moodle.css | 29 +++ 13 files changed, 406 insertions(+), 12 deletions(-) create mode 100644 lib/amd/build/local/reactive/overlay.min.js create mode 100644 lib/amd/build/local/reactive/overlay.min.js.map create mode 100644 lib/amd/src/local/reactive/overlay.js create mode 100644 lib/templates/local/reactive/overlay.mustache diff --git a/lib/amd/build/local/reactive/basecomponent.min.js b/lib/amd/build/local/reactive/basecomponent.min.js index 4a39f08f774..748ec629372 100644 --- a/lib/amd/build/local/reactive/basecomponent.min.js +++ b/lib/amd/build/local/reactive/basecomponent.min.js @@ -1,4 +1,4 @@ -define("core/local/reactive/basecomponent",["exports","core/templates"],(function(_exports,_templates){var obj; +define("core/local/reactive/basecomponent",["exports","core/templates","core/local/reactive/overlay"],(function(_exports,_templates,_overlay){var obj; /** * Reactive UI component base class. * @@ -8,6 +8,6 @@ define("core/local/reactive/basecomponent",["exports","core/templates"],(functio * @class core/local/reactive/basecomponent * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};return _exports.default=class{constructor(descriptor){if(void 0===descriptor.element||!(descriptor.element instanceof HTMLElement))throw Error("Reactive components needs a main DOM element to dispatch events");this.element=descriptor.element,this.eventHandlers=new Map([]),this.eventListeners=[],this.selectors={},this.events=this.constructor.getEvents(),this.create(descriptor),void 0!==descriptor.selectors&&this.addSelectors(descriptor.selectors),void 0===descriptor.reactive?this.element.dispatchEvent(new CustomEvent("core/reactive:requestRegistration",{bubbles:!0,detail:{component:this}})):(this.reactive=descriptor.reactive,this.reactive.registerComponent(this),this.addEventListener(this.element,"core/reactive:requestRegistration",(event=>{var _event$detail;null!=event&&null!==(_event$detail=event.detail)&&void 0!==_event$detail&&_event$detail.component&&(event.stopPropagation(),this.registerChildComponent(event.detail.component))})))}static getEvents(){return{}}create(descriptor){}destroy(){}getWatchers(){return[]}stateReady(){}getElement(query,dataId){if(void 0===query&&void 0===dataId)return this.element;const dataSelector=dataId?"[data-id='".concat(dataId,"']"):"",selector="".concat(null!=query?query:"").concat(dataSelector);return this.element.querySelector(selector)}getElements(query,dataId){const dataSelector=dataId?"[data-id='".concat(dataId,"']"):"",selector="".concat(null!=query?query:"").concat(dataSelector);return this.element.querySelectorAll(selector)}addSelectors(newSelectors){for(const[selectorName,selector]of Object.entries(newSelectors))this.selectors[selectorName]=selector}getSelector(selectorName){return this.selectors[selectorName]}dispatchEvent(eventName,detail){this.element.dispatchEvent(new CustomEvent(eventName,{bubbles:!0,detail:detail}))}renderComponent(target,file,data){return new Promise(((resolve,reject)=>{target.addEventListener("ComponentRegistration:Success",(_ref=>{let{detail:detail}=_ref;resolve(detail.component)})),target.addEventListener("ComponentRegistration:Fail",(()=>{reject("Registration of ".concat(file," fails."))})),_templates.default.renderForPromise(file,data).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.replaceNodeContents(target,html,js),!0})).catch((error=>{throw reject("Rendering of ".concat(file," throws an error.")),error}))}))}addEventListener(target,type,listener){let bindListener=this.eventHandlers.get(listener);void 0===bindListener&&(bindListener=listener.bind(this),this.eventHandlers.set(listener,bindListener)),target.addEventListener(type,bindListener),this.eventListeners.push({target:target,type:type,bindListener:bindListener})}removeEventListener(target,type,listener){let bindListener=this.eventHandlers.get(listener);void 0!==bindListener&&target.removeEventListener(type,bindListener)}removeAllEventListeners(){this.eventListeners.forEach((_ref3=>{let{target:target,type:type,bindListener:bindListener}=_ref3;target.removeEventListener(type,bindListener)})),this.eventListeners=[]}remove(){this.unregister(),this.element.remove()}unregister(){this.reactive.unregisterComponent(this),this.removeAllEventListeners(),this.destroy()}dispatchRegistrationSuccess(){void 0!==this.element.parentNode&&this.element.parentNode.dispatchEvent(new CustomEvent("ComponentRegistration:Success",{bubbles:!1,detail:{component:this}}))}dispatchRegistrationFail(){void 0!==this.element.parentNode&&this.element.parentNode.dispatchEvent(new CustomEvent("ComponentRegistration:Fail",{bubbles:!1,detail:{component:this}}))}registerChildComponent(component){component.reactive=this.reactive,this.reactive.registerComponent(component)}set locked(locked){this.setElementLocked(this.element,locked)}get locked(){return this.getElementLocked(this.element)}setElementLocked(target,locked){target.dataset.locked=null!=locked&&locked,locked?(target.style.pointerEvents="none",target.style.userSelect="none",target.hasAttribute("draggable")&&target.setAttribute("draggable",!1),target.setAttribute("aria-busy",!0)):(target.style.pointerEvents=null,target.style.userSelect=null,target.hasAttribute("draggable")&&target.setAttribute("draggable",!0),target.setAttribute("aria-busy",!1))}getElementLocked(target){var _target$dataset$locke;return null!==(_target$dataset$locke=target.dataset.locked)&&void 0!==_target$dataset$locke&&_target$dataset$locke}},_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};return _exports.default=class{constructor(descriptor){if(void 0===descriptor.element||!(descriptor.element instanceof HTMLElement))throw Error("Reactive components needs a main DOM element to dispatch events");this.element=descriptor.element,this.eventHandlers=new Map([]),this.eventListeners=[],this.selectors={},this.events=this.constructor.getEvents(),this.create(descriptor),void 0!==descriptor.selectors&&this.addSelectors(descriptor.selectors),void 0===descriptor.reactive?this.element.dispatchEvent(new CustomEvent("core/reactive:requestRegistration",{bubbles:!0,detail:{component:this}})):(this.reactive=descriptor.reactive,this.reactive.registerComponent(this),this.addEventListener(this.element,"core/reactive:requestRegistration",(event=>{var _event$detail;null!=event&&null!==(_event$detail=event.detail)&&void 0!==_event$detail&&_event$detail.component&&(event.stopPropagation(),this.registerChildComponent(event.detail.component))})))}static getEvents(){return{}}create(descriptor){}destroy(){}getWatchers(){return[]}stateReady(){}getElement(query,dataId){if(void 0===query&&void 0===dataId)return this.element;const dataSelector=dataId?"[data-id='".concat(dataId,"']"):"",selector="".concat(null!=query?query:"").concat(dataSelector);return this.element.querySelector(selector)}getElements(query,dataId){const dataSelector=dataId?"[data-id='".concat(dataId,"']"):"",selector="".concat(null!=query?query:"").concat(dataSelector);return this.element.querySelectorAll(selector)}addSelectors(newSelectors){for(const[selectorName,selector]of Object.entries(newSelectors))this.selectors[selectorName]=selector}getSelector(selectorName){return this.selectors[selectorName]}dispatchEvent(eventName,detail){this.element.dispatchEvent(new CustomEvent(eventName,{bubbles:!0,detail:detail}))}renderComponent(target,file,data){return new Promise(((resolve,reject)=>{target.addEventListener("ComponentRegistration:Success",(_ref=>{let{detail:detail}=_ref;resolve(detail.component)})),target.addEventListener("ComponentRegistration:Fail",(()=>{reject("Registration of ".concat(file," fails."))})),_templates.default.renderForPromise(file,data).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.replaceNodeContents(target,html,js),!0})).catch((error=>{throw reject("Rendering of ".concat(file," throws an error.")),error}))}))}addEventListener(target,type,listener){let bindListener=this.eventHandlers.get(listener);void 0===bindListener&&(bindListener=listener.bind(this),this.eventHandlers.set(listener,bindListener)),target.addEventListener(type,bindListener),this.eventListeners.push({target:target,type:type,bindListener:bindListener})}removeEventListener(target,type,listener){let bindListener=this.eventHandlers.get(listener);void 0!==bindListener&&target.removeEventListener(type,bindListener)}removeAllEventListeners(){this.eventListeners.forEach((_ref3=>{let{target:target,type:type,bindListener:bindListener}=_ref3;target.removeEventListener(type,bindListener)})),this.eventListeners=[]}remove(){this.unregister(),this.element.remove()}unregister(){this.reactive.unregisterComponent(this),this.removeAllEventListeners(),this.destroy()}dispatchRegistrationSuccess(){void 0!==this.element.parentNode&&this.element.parentNode.dispatchEvent(new CustomEvent("ComponentRegistration:Success",{bubbles:!1,detail:{component:this}}))}dispatchRegistrationFail(){void 0!==this.element.parentNode&&this.element.parentNode.dispatchEvent(new CustomEvent("ComponentRegistration:Fail",{bubbles:!1,detail:{component:this}}))}registerChildComponent(component){component.reactive=this.reactive,this.reactive.registerComponent(component)}set locked(locked){this.setElementLocked(this.element,locked)}get locked(){return this.getElementLocked(this.element)}setElementLocked(target,locked){target.dataset.locked=null!=locked&&locked,locked?(target.style.pointerEvents="none",target.style.userSelect="none",target.hasAttribute("draggable")&&target.setAttribute("draggable",!1),target.setAttribute("aria-busy",!0)):(target.style.pointerEvents=null,target.style.userSelect=null,target.hasAttribute("draggable")&&target.setAttribute("draggable",!0),target.setAttribute("aria-busy",!1))}getElementLocked(target){var _target$dataset$locke;return null!==(_target$dataset$locke=target.dataset.locked)&&void 0!==_target$dataset$locke&&_target$dataset$locke}async addOverlay(definition,target){var _definition$classes;this._overlay&&this.removeOverlay(),this._overlay=await(0,_overlay.addOverlay)({content:definition.content,css:null!==(_definition$classes=definition.classes)&&void 0!==_definition$classes?_definition$classes:"file-drop-zone"},null!=target?target:this.element)}removeOverlay(){this._overlay&&((0,_overlay.removeOverlay)(this._overlay),this._overlay=null)}removeAllOverlays(){(0,_overlay.removeAllOverlays)()}},_exports.default})); //# sourceMappingURL=basecomponent.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/reactive/basecomponent.min.js.map b/lib/amd/build/local/reactive/basecomponent.min.js.map index fbc59e31c87..c1afacfd487 100644 --- a/lib/amd/build/local/reactive/basecomponent.min.js.map +++ b/lib/amd/build/local/reactive/basecomponent.min.js.map @@ -1 +1 @@ -{"version":3,"file":"basecomponent.min.js","sources":["../../../src/local/reactive/basecomponent.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 Templates from 'core/templates';\n\n/**\n * Reactive UI component base class.\n *\n * Each UI reactive component should extend this class to interact with a reactive state.\n *\n * @module core/local/reactive/basecomponent\n * @class core/local/reactive/basecomponent\n * @copyright 2020 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n /**\n * The component descriptor data structure.\n *\n * This structure is used by any component and init method to define the way the component will interact\n * with the interface and whith reactive instance operates. The logic behind this object is to avoid\n * unnecessary dependancies between the final interface and the state logic.\n *\n * Any component interacts with a single main DOM element (description.element) but it can use internal\n * selector to select elements within this main element (descriptor.selectors). By default each component\n * will provide it's own default selectors, but those can be overridden by the \"descriptor.selectors\"\n * property in case the mustache wants to reuse the same component logic but with a different interface.\n *\n * @typedef {object} descriptor\n * @property {Reactive} reactive an optional reactive module to register in\n * @property {DOMElement} element all components needs an element to anchor events\n * @property {object} [selectors] an optional object to override query selectors\n */\n\n /**\n * The class constructor.\n *\n * The only param this method gets is a constructor with all the mandatory\n * and optional component data. Component will receive the same descriptor\n * as create method param.\n *\n * This method will call the \"create\" method before registering the component into\n * the reactive module. This way any component can add default selectors and events.\n *\n * @param {descriptor} descriptor data to create the object.\n */\n constructor(descriptor) {\n\n if (descriptor.element === undefined || !(descriptor.element instanceof HTMLElement)) {\n throw Error(`Reactive components needs a main DOM element to dispatch events`);\n }\n\n this.element = descriptor.element;\n\n // Variable to track event listeners.\n this.eventHandlers = new Map([]);\n this.eventListeners = [];\n\n // Empty default component selectors.\n this.selectors = {};\n\n // Empty default event list from the static method.\n this.events = this.constructor.getEvents();\n\n // Call create function to get the component defaults.\n this.create(descriptor);\n\n // Overwrite the components selectors if necessary.\n if (descriptor.selectors !== undefined) {\n this.addSelectors(descriptor.selectors);\n }\n\n // Register into a reactive instance.\n if (descriptor.reactive === undefined) {\n // Ask parent components for registration.\n this.element.dispatchEvent(new CustomEvent(\n 'core/reactive:requestRegistration',\n {\n bubbles: true,\n detail: {component: this},\n }\n ));\n } else {\n this.reactive = descriptor.reactive;\n this.reactive.registerComponent(this);\n // Add a listener to register child components.\n this.addEventListener(\n this.element,\n 'core/reactive:requestRegistration',\n (event) => {\n if (event?.detail?.component) {\n event.stopPropagation();\n this.registerChildComponent(event.detail.component);\n }\n }\n );\n }\n }\n\n /**\n * Return the component custom event names.\n *\n * Components may override this method to provide their own events.\n *\n * Component custom events is an important part of component reusability. This function\n * is static because is part of the component definition and should be accessible from\n * outsite the instances. However, values will be available at instance level in the\n * this.events object.\n *\n * @returns {Object} the component events.\n */\n static getEvents() {\n return {};\n }\n\n /**\n * Component create function.\n *\n * Default init method will call \"create\" when all internal attributes are set\n * but before the component is not yet registered in the reactive module.\n *\n * In this method any component can define its own defaults such as:\n * - this.selectors {object} the default query selectors of this component.\n * - this.events {object} a list of event names this component dispatch\n * - extract any data from the main dom element (this.element)\n * - set any other data the component uses\n *\n * @param {descriptor} descriptor the component descriptor\n */\n // eslint-disable-next-line no-unused-vars\n create(descriptor) {\n // Components may override this method to initialize selects, events or other data.\n }\n\n /**\n * Component destroy hook.\n *\n * BaseComponent call this method when a component is unregistered or removed.\n *\n * Components may override this method to clean the HTML or do some action when the\n * component is unregistered or removed.\n */\n destroy() {\n // Components can override this method.\n }\n\n /**\n * Return the list of watchers that component has.\n *\n * Each watcher is represented by an object with two attributes:\n * - watch (string) the specific state event to watch. Example 'section.visible:updated'\n * - handler (function) the function to call when the watching state change happens\n *\n * Any component shoudl override this method to define their state watchers.\n *\n * @returns {array} array of watchers.\n */\n getWatchers() {\n return [];\n }\n\n /**\n * Reactive module will call this method when the state is ready.\n *\n * Component can override this method to update/load the component HTML or to bind\n * listeners to HTML entities.\n */\n stateReady() {\n // Components can override this method.\n }\n\n /**\n * Get the main DOM element of this component or a subelement.\n *\n * @param {string|undefined} query optional subelement query\n * @param {string|undefined} dataId optional data-id value\n * @returns {element|undefined} the DOM element (if any)\n */\n getElement(query, dataId) {\n if (query === undefined && dataId === undefined) {\n return this.element;\n }\n const dataSelector = (dataId) ? `[data-id='${dataId}']` : '';\n const selector = `${query ?? ''}${dataSelector}`;\n return this.element.querySelector(selector);\n }\n\n /**\n * Get the all subelement that match a query selector.\n *\n * @param {string|undefined} query optional subelement query\n * @param {string|undefined} dataId optional data-id value\n * @returns {NodeList} the DOM elements\n */\n getElements(query, dataId) {\n const dataSelector = (dataId) ? `[data-id='${dataId}']` : '';\n const selector = `${query ?? ''}${dataSelector}`;\n return this.element.querySelectorAll(selector);\n }\n\n /**\n * Add or update the component selectors.\n *\n * @param {Object} newSelectors an object of new selectors.\n */\n addSelectors(newSelectors) {\n for (const [selectorName, selector] of Object.entries(newSelectors)) {\n this.selectors[selectorName] = selector;\n }\n }\n\n /**\n * Return a component selector.\n *\n * @param {string} selectorName the selector name\n * @return {string|undefined} the query selector\n */\n getSelector(selectorName) {\n return this.selectors[selectorName];\n }\n\n /**\n * Dispatch a custom event on this.element.\n *\n * This is just a convenient method to dispatch custom events from within a component.\n * Components are free to use an alternative function to dispatch custom\n * events. The only restriction is that it should be dispatched on this.element\n * and specify \"bubbles:true\" to alert any component listeners.\n *\n * @param {string} eventName the event name\n * @param {*} detail event detail data\n */\n dispatchEvent(eventName, detail) {\n this.element.dispatchEvent(new CustomEvent(eventName, {\n bubbles: true,\n detail: detail,\n }));\n }\n\n /**\n * Render a new Component using a mustache file.\n *\n * It is important to note that this method should NOT be used for loading regular mustache files\n * as it returns a Promise that will only be resolved if the mustache registers a component instance.\n *\n * @param {element} target the DOM element that contains the component\n * @param {string} file the component mustache file to render\n * @param {*} data the mustache data\n * @return {Promise} a promise of the resulting component instance\n */\n renderComponent(target, file, data) {\n return new Promise((resolve, reject) => {\n target.addEventListener('ComponentRegistration:Success', ({detail}) => {\n resolve(detail.component);\n });\n target.addEventListener('ComponentRegistration:Fail', () => {\n reject(`Registration of ${file} fails.`);\n });\n Templates.renderForPromise(\n file,\n data\n ).then(({html, js}) => {\n Templates.replaceNodeContents(target, html, js);\n return true;\n }).catch(error => {\n reject(`Rendering of ${file} throws an error.`);\n throw error;\n });\n });\n }\n\n /**\n * Add and bind an event listener to a target and keep track of all event listeners.\n *\n * The native element.addEventListener method is not object oriented friently as the\n * \"this\" represents the element that triggers the event and not the listener class.\n * As components can be unregister and removed at any time, the BaseComponent provides\n * this method to keep track of all component listeners and do all of the bind stuff.\n *\n * @param {Element} target the event target\n * @param {string} type the event name\n * @param {function} listener the class method that recieve the event\n */\n addEventListener(target, type, listener) {\n\n // Check if we have the bind version of that listener.\n let bindListener = this.eventHandlers.get(listener);\n\n if (bindListener === undefined) {\n bindListener = listener.bind(this);\n this.eventHandlers.set(listener, bindListener);\n }\n\n target.addEventListener(type, bindListener);\n\n // Keep track of all component event listeners in case we need to remove them.\n this.eventListeners.push({\n target,\n type,\n bindListener,\n });\n\n }\n\n /**\n * Remove an event listener from a component.\n *\n * This method allows components to remove listeners without keeping track of the\n * listeners bind versions of the method. Both addEventListener and removeEventListener\n * keeps internally the relation between the original class method and the bind one.\n *\n * @param {Element} target the event target\n * @param {string} type the event name\n * @param {function} listener the class method that recieve the event\n */\n removeEventListener(target, type, listener) {\n // Check if we have the bind version of that listener.\n let bindListener = this.eventHandlers.get(listener);\n\n if (bindListener === undefined) {\n // This listener has not been added.\n return;\n }\n\n target.removeEventListener(type, bindListener);\n }\n\n /**\n * Remove all event listeners from this component.\n *\n * This method is called also when the component is unregistered or removed.\n *\n * Note that only listeners registered with the addEventListener method\n * will be removed. Other manual listeners will keep active.\n */\n removeAllEventListeners() {\n this.eventListeners.forEach(({target, type, bindListener}) => {\n target.removeEventListener(type, bindListener);\n });\n this.eventListeners = [];\n }\n\n /**\n * Remove a previously rendered component instance.\n *\n * This method will remove the component HTML and unregister it from the\n * reactive module.\n */\n remove() {\n this.unregister();\n this.element.remove();\n }\n\n /**\n * Unregister the component from the reactive module.\n *\n * This method will disable the component logic, event listeners and watchers\n * but it won't remove any HTML created by the component. However, it will trigger\n * the destroy hook to allow the component to clean parts of the interface.\n */\n unregister() {\n this.reactive.unregisterComponent(this);\n this.removeAllEventListeners();\n this.destroy();\n }\n\n /**\n * Dispatch a component registration event to inform the parent node.\n *\n * The registration event is different from the rest of the component events because\n * is the only way in which components can communicate its existence to a possible parent.\n * Most components will be created by including a mustache file, child components\n * must emit a registration event to the parent DOM element to alert about the registration.\n */\n dispatchRegistrationSuccess() {\n // The registration event does not bubble because we just want to comunicate with the parentNode.\n // Otherwise, any component can get multiple registrations events and could not differentiate\n // between child components and grand child components.\n if (this.element.parentNode === undefined) {\n return;\n }\n // This custom element is captured by renderComponent method.\n this.element.parentNode.dispatchEvent(new CustomEvent(\n 'ComponentRegistration:Success',\n {\n bubbles: false,\n detail: {component: this},\n }\n ));\n }\n\n /**\n * Dispatch a component registration fail event to inform the parent node.\n *\n * As dispatchRegistrationSuccess, this method will communicate the registration fail to the\n * parent node to inform the possible parent component.\n */\n dispatchRegistrationFail() {\n if (this.element.parentNode === undefined) {\n return;\n }\n // This custom element is captured only by renderComponent method.\n this.element.parentNode.dispatchEvent(new CustomEvent(\n 'ComponentRegistration:Fail',\n {\n bubbles: false,\n detail: {component: this},\n }\n ));\n }\n\n /**\n * Register a child component into the reactive instance.\n *\n * @param {self} component the component to register.\n */\n registerChildComponent(component) {\n component.reactive = this.reactive;\n this.reactive.registerComponent(component);\n }\n\n /**\n * Set the lock value and locks or unlocks the element.\n *\n * @param {boolean} locked the new locked value\n */\n set locked(locked) {\n this.setElementLocked(this.element, locked);\n }\n\n /**\n * Get the current locked value from the element.\n *\n * @return {boolean}\n */\n get locked() {\n return this.getElementLocked(this.element);\n }\n\n /**\n * Lock/unlock an element.\n *\n * @param {Element} target the event target\n * @param {boolean} locked the new locked value\n */\n setElementLocked(target, locked) {\n target.dataset.locked = locked ?? false;\n if (locked) {\n // Disable interactions.\n target.style.pointerEvents = 'none';\n target.style.userSelect = 'none';\n // Check if it is draggable.\n if (target.hasAttribute('draggable')) {\n target.setAttribute('draggable', false);\n }\n target.setAttribute('aria-busy', true);\n } else {\n // Enable interactions.\n target.style.pointerEvents = null;\n target.style.userSelect = null;\n // Check if it was draggable.\n if (target.hasAttribute('draggable')) {\n target.setAttribute('draggable', true);\n }\n target.setAttribute('aria-busy', false);\n }\n }\n\n /**\n * Get the current locked value from the element.\n *\n * @param {Element} target the event target\n * @return {boolean}\n */\n getElementLocked(target) {\n return target.dataset.locked ?? false;\n }\n}\n"],"names":["constructor","descriptor","undefined","element","HTMLElement","Error","eventHandlers","Map","eventListeners","selectors","events","this","getEvents","create","addSelectors","reactive","dispatchEvent","CustomEvent","bubbles","detail","component","registerComponent","addEventListener","event","_event$detail","stopPropagation","registerChildComponent","destroy","getWatchers","stateReady","getElement","query","dataId","dataSelector","selector","querySelector","getElements","querySelectorAll","newSelectors","selectorName","Object","entries","getSelector","eventName","renderComponent","target","file","data","Promise","resolve","reject","_ref","renderForPromise","then","_ref2","html","js","replaceNodeContents","catch","error","type","listener","bindListener","get","bind","set","push","removeEventListener","removeAllEventListeners","forEach","_ref3","remove","unregister","unregisterComponent","dispatchRegistrationSuccess","parentNode","dispatchRegistrationFail","locked","setElementLocked","getElementLocked","dataset","style","pointerEvents","userSelect","hasAttribute","setAttribute"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"basecomponent.min.js","sources":["../../../src/local/reactive/basecomponent.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 Templates from 'core/templates';\nimport {addOverlay, removeOverlay, removeAllOverlays} from 'core/local/reactive/overlay';\n\n/**\n * Reactive UI component base class.\n *\n * Each UI reactive component should extend this class to interact with a reactive state.\n *\n * @module core/local/reactive/basecomponent\n * @class core/local/reactive/basecomponent\n * @copyright 2020 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n /**\n * The component descriptor data structure.\n *\n * This structure is used by any component and init method to define the way the component will interact\n * with the interface and whith reactive instance operates. The logic behind this object is to avoid\n * unnecessary dependancies between the final interface and the state logic.\n *\n * Any component interacts with a single main DOM element (description.element) but it can use internal\n * selector to select elements within this main element (descriptor.selectors). By default each component\n * will provide it's own default selectors, but those can be overridden by the \"descriptor.selectors\"\n * property in case the mustache wants to reuse the same component logic but with a different interface.\n *\n * @typedef {object} descriptor\n * @property {Reactive} reactive an optional reactive module to register in\n * @property {DOMElement} element all components needs an element to anchor events\n * @property {object} [selectors] an optional object to override query selectors\n */\n\n /**\n * The class constructor.\n *\n * The only param this method gets is a constructor with all the mandatory\n * and optional component data. Component will receive the same descriptor\n * as create method param.\n *\n * This method will call the \"create\" method before registering the component into\n * the reactive module. This way any component can add default selectors and events.\n *\n * @param {descriptor} descriptor data to create the object.\n */\n constructor(descriptor) {\n\n if (descriptor.element === undefined || !(descriptor.element instanceof HTMLElement)) {\n throw Error(`Reactive components needs a main DOM element to dispatch events`);\n }\n\n this.element = descriptor.element;\n\n // Variable to track event listeners.\n this.eventHandlers = new Map([]);\n this.eventListeners = [];\n\n // Empty default component selectors.\n this.selectors = {};\n\n // Empty default event list from the static method.\n this.events = this.constructor.getEvents();\n\n // Call create function to get the component defaults.\n this.create(descriptor);\n\n // Overwrite the components selectors if necessary.\n if (descriptor.selectors !== undefined) {\n this.addSelectors(descriptor.selectors);\n }\n\n // Register into a reactive instance.\n if (descriptor.reactive === undefined) {\n // Ask parent components for registration.\n this.element.dispatchEvent(new CustomEvent(\n 'core/reactive:requestRegistration',\n {\n bubbles: true,\n detail: {component: this},\n }\n ));\n } else {\n this.reactive = descriptor.reactive;\n this.reactive.registerComponent(this);\n // Add a listener to register child components.\n this.addEventListener(\n this.element,\n 'core/reactive:requestRegistration',\n (event) => {\n if (event?.detail?.component) {\n event.stopPropagation();\n this.registerChildComponent(event.detail.component);\n }\n }\n );\n }\n }\n\n /**\n * Return the component custom event names.\n *\n * Components may override this method to provide their own events.\n *\n * Component custom events is an important part of component reusability. This function\n * is static because is part of the component definition and should be accessible from\n * outsite the instances. However, values will be available at instance level in the\n * this.events object.\n *\n * @returns {Object} the component events.\n */\n static getEvents() {\n return {};\n }\n\n /**\n * Component create function.\n *\n * Default init method will call \"create\" when all internal attributes are set\n * but before the component is not yet registered in the reactive module.\n *\n * In this method any component can define its own defaults such as:\n * - this.selectors {object} the default query selectors of this component.\n * - this.events {object} a list of event names this component dispatch\n * - extract any data from the main dom element (this.element)\n * - set any other data the component uses\n *\n * @param {descriptor} descriptor the component descriptor\n */\n // eslint-disable-next-line no-unused-vars\n create(descriptor) {\n // Components may override this method to initialize selects, events or other data.\n }\n\n /**\n * Component destroy hook.\n *\n * BaseComponent call this method when a component is unregistered or removed.\n *\n * Components may override this method to clean the HTML or do some action when the\n * component is unregistered or removed.\n */\n destroy() {\n // Components can override this method.\n }\n\n /**\n * Return the list of watchers that component has.\n *\n * Each watcher is represented by an object with two attributes:\n * - watch (string) the specific state event to watch. Example 'section.visible:updated'\n * - handler (function) the function to call when the watching state change happens\n *\n * Any component shoudl override this method to define their state watchers.\n *\n * @returns {array} array of watchers.\n */\n getWatchers() {\n return [];\n }\n\n /**\n * Reactive module will call this method when the state is ready.\n *\n * Component can override this method to update/load the component HTML or to bind\n * listeners to HTML entities.\n */\n stateReady() {\n // Components can override this method.\n }\n\n /**\n * Get the main DOM element of this component or a subelement.\n *\n * @param {string|undefined} query optional subelement query\n * @param {string|undefined} dataId optional data-id value\n * @returns {element|undefined} the DOM element (if any)\n */\n getElement(query, dataId) {\n if (query === undefined && dataId === undefined) {\n return this.element;\n }\n const dataSelector = (dataId) ? `[data-id='${dataId}']` : '';\n const selector = `${query ?? ''}${dataSelector}`;\n return this.element.querySelector(selector);\n }\n\n /**\n * Get the all subelement that match a query selector.\n *\n * @param {string|undefined} query optional subelement query\n * @param {string|undefined} dataId optional data-id value\n * @returns {NodeList} the DOM elements\n */\n getElements(query, dataId) {\n const dataSelector = (dataId) ? `[data-id='${dataId}']` : '';\n const selector = `${query ?? ''}${dataSelector}`;\n return this.element.querySelectorAll(selector);\n }\n\n /**\n * Add or update the component selectors.\n *\n * @param {Object} newSelectors an object of new selectors.\n */\n addSelectors(newSelectors) {\n for (const [selectorName, selector] of Object.entries(newSelectors)) {\n this.selectors[selectorName] = selector;\n }\n }\n\n /**\n * Return a component selector.\n *\n * @param {string} selectorName the selector name\n * @return {string|undefined} the query selector\n */\n getSelector(selectorName) {\n return this.selectors[selectorName];\n }\n\n /**\n * Dispatch a custom event on this.element.\n *\n * This is just a convenient method to dispatch custom events from within a component.\n * Components are free to use an alternative function to dispatch custom\n * events. The only restriction is that it should be dispatched on this.element\n * and specify \"bubbles:true\" to alert any component listeners.\n *\n * @param {string} eventName the event name\n * @param {*} detail event detail data\n */\n dispatchEvent(eventName, detail) {\n this.element.dispatchEvent(new CustomEvent(eventName, {\n bubbles: true,\n detail: detail,\n }));\n }\n\n /**\n * Render a new Component using a mustache file.\n *\n * It is important to note that this method should NOT be used for loading regular mustache files\n * as it returns a Promise that will only be resolved if the mustache registers a component instance.\n *\n * @param {element} target the DOM element that contains the component\n * @param {string} file the component mustache file to render\n * @param {*} data the mustache data\n * @return {Promise} a promise of the resulting component instance\n */\n renderComponent(target, file, data) {\n return new Promise((resolve, reject) => {\n target.addEventListener('ComponentRegistration:Success', ({detail}) => {\n resolve(detail.component);\n });\n target.addEventListener('ComponentRegistration:Fail', () => {\n reject(`Registration of ${file} fails.`);\n });\n Templates.renderForPromise(\n file,\n data\n ).then(({html, js}) => {\n Templates.replaceNodeContents(target, html, js);\n return true;\n }).catch(error => {\n reject(`Rendering of ${file} throws an error.`);\n throw error;\n });\n });\n }\n\n /**\n * Add and bind an event listener to a target and keep track of all event listeners.\n *\n * The native element.addEventListener method is not object oriented friently as the\n * \"this\" represents the element that triggers the event and not the listener class.\n * As components can be unregister and removed at any time, the BaseComponent provides\n * this method to keep track of all component listeners and do all of the bind stuff.\n *\n * @param {Element} target the event target\n * @param {string} type the event name\n * @param {function} listener the class method that recieve the event\n */\n addEventListener(target, type, listener) {\n\n // Check if we have the bind version of that listener.\n let bindListener = this.eventHandlers.get(listener);\n\n if (bindListener === undefined) {\n bindListener = listener.bind(this);\n this.eventHandlers.set(listener, bindListener);\n }\n\n target.addEventListener(type, bindListener);\n\n // Keep track of all component event listeners in case we need to remove them.\n this.eventListeners.push({\n target,\n type,\n bindListener,\n });\n\n }\n\n /**\n * Remove an event listener from a component.\n *\n * This method allows components to remove listeners without keeping track of the\n * listeners bind versions of the method. Both addEventListener and removeEventListener\n * keeps internally the relation between the original class method and the bind one.\n *\n * @param {Element} target the event target\n * @param {string} type the event name\n * @param {function} listener the class method that recieve the event\n */\n removeEventListener(target, type, listener) {\n // Check if we have the bind version of that listener.\n let bindListener = this.eventHandlers.get(listener);\n\n if (bindListener === undefined) {\n // This listener has not been added.\n return;\n }\n\n target.removeEventListener(type, bindListener);\n }\n\n /**\n * Remove all event listeners from this component.\n *\n * This method is called also when the component is unregistered or removed.\n *\n * Note that only listeners registered with the addEventListener method\n * will be removed. Other manual listeners will keep active.\n */\n removeAllEventListeners() {\n this.eventListeners.forEach(({target, type, bindListener}) => {\n target.removeEventListener(type, bindListener);\n });\n this.eventListeners = [];\n }\n\n /**\n * Remove a previously rendered component instance.\n *\n * This method will remove the component HTML and unregister it from the\n * reactive module.\n */\n remove() {\n this.unregister();\n this.element.remove();\n }\n\n /**\n * Unregister the component from the reactive module.\n *\n * This method will disable the component logic, event listeners and watchers\n * but it won't remove any HTML created by the component. However, it will trigger\n * the destroy hook to allow the component to clean parts of the interface.\n */\n unregister() {\n this.reactive.unregisterComponent(this);\n this.removeAllEventListeners();\n this.destroy();\n }\n\n /**\n * Dispatch a component registration event to inform the parent node.\n *\n * The registration event is different from the rest of the component events because\n * is the only way in which components can communicate its existence to a possible parent.\n * Most components will be created by including a mustache file, child components\n * must emit a registration event to the parent DOM element to alert about the registration.\n */\n dispatchRegistrationSuccess() {\n // The registration event does not bubble because we just want to comunicate with the parentNode.\n // Otherwise, any component can get multiple registrations events and could not differentiate\n // between child components and grand child components.\n if (this.element.parentNode === undefined) {\n return;\n }\n // This custom element is captured by renderComponent method.\n this.element.parentNode.dispatchEvent(new CustomEvent(\n 'ComponentRegistration:Success',\n {\n bubbles: false,\n detail: {component: this},\n }\n ));\n }\n\n /**\n * Dispatch a component registration fail event to inform the parent node.\n *\n * As dispatchRegistrationSuccess, this method will communicate the registration fail to the\n * parent node to inform the possible parent component.\n */\n dispatchRegistrationFail() {\n if (this.element.parentNode === undefined) {\n return;\n }\n // This custom element is captured only by renderComponent method.\n this.element.parentNode.dispatchEvent(new CustomEvent(\n 'ComponentRegistration:Fail',\n {\n bubbles: false,\n detail: {component: this},\n }\n ));\n }\n\n /**\n * Register a child component into the reactive instance.\n *\n * @param {self} component the component to register.\n */\n registerChildComponent(component) {\n component.reactive = this.reactive;\n this.reactive.registerComponent(component);\n }\n\n /**\n * Set the lock value and locks or unlocks the element.\n *\n * @param {boolean} locked the new locked value\n */\n set locked(locked) {\n this.setElementLocked(this.element, locked);\n }\n\n /**\n * Get the current locked value from the element.\n *\n * @return {boolean}\n */\n get locked() {\n return this.getElementLocked(this.element);\n }\n\n /**\n * Lock/unlock an element.\n *\n * @param {Element} target the event target\n * @param {boolean} locked the new locked value\n */\n setElementLocked(target, locked) {\n target.dataset.locked = locked ?? false;\n if (locked) {\n // Disable interactions.\n target.style.pointerEvents = 'none';\n target.style.userSelect = 'none';\n // Check if it is draggable.\n if (target.hasAttribute('draggable')) {\n target.setAttribute('draggable', false);\n }\n target.setAttribute('aria-busy', true);\n } else {\n // Enable interactions.\n target.style.pointerEvents = null;\n target.style.userSelect = null;\n // Check if it was draggable.\n if (target.hasAttribute('draggable')) {\n target.setAttribute('draggable', true);\n }\n target.setAttribute('aria-busy', false);\n }\n }\n\n /**\n * Get the current locked value from the element.\n *\n * @param {Element} target the event target\n * @return {boolean}\n */\n getElementLocked(target) {\n return target.dataset.locked ?? false;\n }\n\n /**\n * Adds an overlay to a specific page element.\n *\n * @param {Object} definition the overlay definition.\n * @param {String} definition.content an optional overlay content.\n * @param {String} definition.classes an optional CSS classes\n * @param {Element} target optional parent object (this.element will be used if none provided)\n */\n async addOverlay(definition, target) {\n if (this._overlay) {\n this.removeOverlay();\n }\n this._overlay = await addOverlay(\n {\n content: definition.content,\n css: definition.classes ?? 'file-drop-zone',\n },\n target ?? this.element\n );\n }\n\n /**\n * Remove the current overlay.\n */\n removeOverlay() {\n if (!this._overlay) {\n return;\n }\n removeOverlay(this._overlay);\n this._overlay = null;\n }\n\n /**\n * Remove all page overlais.\n */\n removeAllOverlays() {\n removeAllOverlays();\n }\n}\n"],"names":["constructor","descriptor","undefined","element","HTMLElement","Error","eventHandlers","Map","eventListeners","selectors","events","this","getEvents","create","addSelectors","reactive","dispatchEvent","CustomEvent","bubbles","detail","component","registerComponent","addEventListener","event","_event$detail","stopPropagation","registerChildComponent","destroy","getWatchers","stateReady","getElement","query","dataId","dataSelector","selector","querySelector","getElements","querySelectorAll","newSelectors","selectorName","Object","entries","getSelector","eventName","renderComponent","target","file","data","Promise","resolve","reject","_ref","renderForPromise","then","_ref2","html","js","replaceNodeContents","catch","error","type","listener","bindListener","get","bind","set","push","removeEventListener","removeAllEventListeners","forEach","_ref3","remove","unregister","unregisterComponent","dispatchRegistrationSuccess","parentNode","dispatchRegistrationFail","locked","setElementLocked","getElementLocked","dataset","style","pointerEvents","userSelect","hasAttribute","setAttribute","definition","_overlay","removeOverlay","content","css","classes","removeAllOverlays"],"mappings":"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"} \ No newline at end of file diff --git a/lib/amd/build/local/reactive/dragdrop.min.js b/lib/amd/build/local/reactive/dragdrop.min.js index a79f946e554..168c501e59f 100644 --- a/lib/amd/build/local/reactive/dragdrop.min.js +++ b/lib/amd/build/local/reactive/dragdrop.min.js @@ -62,6 +62,6 @@ define("core/local/reactive/dragdrop",["exports","core/local/reactive/basecompon * @class core/local/reactive/dragdrop * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_basecomponent=(obj=_basecomponent)&&obj.__esModule?obj:{default:obj};let activeDropData=new Map,dragStartPoint={};class _default extends _basecomponent.default{create(parent){var _parent$name,_parent$classes,_this$parent$draggabl,_this$parent$relative;this.name="".concat(null!==(_parent$name=parent.name)&&void 0!==_parent$name?_parent$name:"unkown","_dragdrop"),this.classes=Object.assign({BODYDRAGGING:"dragging",DRAGGABLEREADY:"draggable",DROPREADY:"dropready",DRAGOVER:"dragover",DRAGGING:"dragging",DROPUP:"drop-up",DROPDOWN:"drop-down",DROPZONE:"drop-zone",DRAGICON:"dragicon"},null!==(_parent$classes=null==parent?void 0:parent.classes)&&void 0!==_parent$classes?_parent$classes:{}),this.fullregion=parent.fullregion,this.parent=parent,this.autoconfigDraggable=null===(_this$parent$draggabl=this.parent.draggable)||void 0===_this$parent$draggabl||_this$parent$draggabl,this.relativeDrag=null!==(_this$parent$relative=this.parent.relativeDrag)&&void 0!==_this$parent$relative&&_this$parent$relative,this.entercount=0,this.dropzonevisible=!1}getClasses(){return this.classes}stateReady(){"function"==typeof this.parent.validateDropData&&(this.element.classList.add(this.classes.DROPREADY),this.addEventListener(this.element,"dragenter",this._dragEnter),this.addEventListener(this.element,"dragleave",this._dragLeave),this.addEventListener(this.element,"dragover",this._dragOver),this.addEventListener(this.element,"drop",this._drop)),this.autoconfigDraggable&&"function"==typeof this.parent.getDraggableData&&this.setDraggable(!0)}setDraggable(value){if("function"!=typeof this.parent.getDraggableData)throw new Error("Draggable components must have a getDraggableData method");this.element.setAttribute("draggable",value),value?(this.addEventListener(this.element,"dragstart",this._dragStart),this.addEventListener(this.element,"dragend",this._dragEnd),this.element.classList.add(this.classes.DRAGGABLEREADY)):(this.removeEventListener(this.element,"dragstart",this._dragStart),this.removeEventListener(this.element,"dragend",this._dragEnd),this.element.classList.remove(this.classes.DRAGGABLEREADY))}_dragStart(event){var _this$fullregion;if(document.activeElement.matches("textarea, input"))return void event.preventDefault();const dropdata=this.parent.getDraggableData();if(!dropdata)return;dragStartPoint={pageX:event.pageX,pageY:event.pageY},event.stopPropagation(),activeDropData.set(this.reactive,dropdata),document.body.classList.add(this.classes.BODYDRAGGING),this.element.classList.add(this.classes.DRAGGING),null===(_this$fullregion=this.fullregion)||void 0===_this$fullregion||_this$fullregion.classList.add(this.classes.DRAGGING);let dragImage=this.element;if(void 0!==this.parent.setDragImage){const customImage=this.parent.setDragImage(dropdata,event);customImage&&(dragImage=customImage)}const position={x:0,y:0};this.relativeDrag&&(position.x=event.offsetX,position.y=event.offsetY),event.dataTransfer.setDragImage(dragImage,position.x,position.y),this._callParentMethod("dragStart",dropdata,event)}_dragEnd(event){var _this$fullregion2;const dropdata=activeDropData.get(this.reactive);dropdata&&(activeDropData.delete(this.reactive),document.body.classList.remove(this.classes.BODYDRAGGING),this.element.classList.remove(this.classes.DRAGGING),null===(_this$fullregion2=this.fullregion)||void 0===_this$fullregion2||_this$fullregion2.classList.remove(this.classes.DRAGGING),this._addEventTotalMovement(event),this._callParentMethod("dragEnd",dropdata,event))}_dragEnter(event){const dropdata=this._processEvent(event);dropdata&&(this.entercount++,this.element.classList.add(this.classes.DRAGOVER),1!=this.entercount||this.dropzonevisible||(this.dropzonevisible=!0,this.element.classList.add(this.classes.DRAGOVER),this._callParentMethod("showDropZone",dropdata,event)))}_dragOver(event){const dropdata=this._processEvent(event);dropdata&&!this.dropzonevisible&&(this.dropzonevisible=!0,this.element.classList.add(this.classes.DRAGOVER),this._callParentMethod("showDropZone",dropdata,event))}_dragLeave(event){const dropdata=this._processEvent(event);dropdata&&(this.entercount--,0==this.entercount&&this.dropzonevisible&&(this.dropzonevisible=!1,this.element.classList.remove(this.classes.DRAGOVER),this._callParentMethod("hideDropZone",dropdata,event)))}_drop(event){const dropdata=this._processEvent(event);dropdata&&(this.entercount=0,this.dropzonevisible&&(this.dropzonevisible=!1,this._callParentMethod("hideDropZone",dropdata,event)),this.element.classList.remove(this.classes.DRAGOVER),this._callParentMethod("drop",dropdata,event),dragStartPoint={})}_processEvent(event){const dropdata=this._getDropData(event);return dropdata&&this.parent.validateDropData(dropdata)?(event.preventDefault(),event.stopPropagation(),this._addEventTotalMovement(event),dropdata):null}_addEventTotalMovement(event){if(void 0===dragStartPoint.pageX||void 0===event.pageX)return;event.fixedMovementX=event.pageX-dragStartPoint.pageX,event.fixedMovementY=event.pageY-dragStartPoint.pageY,event.initialPageX=dragStartPoint.pageX,event.initialPageY=dragStartPoint.pageY;const current=this.element.getBoundingClientRect();if(event.newFixedTop=current.top+event.fixedMovementY,event.newFixedLeft=current.left+event.fixedMovementX,void 0!==this.fullregion){const current=this.fullregion.getBoundingClientRect();event.newRegionFixedxTop=current.top+event.fixedMovementY,event.newRegionFixedxLeft=current.left+event.fixedMovementX}}_callParentMethod(methodname,dropdata,event){"function"==typeof this.parent[methodname]&&this.parent[methodname](dropdata,event)}_getDropData(event){if(!this._containsOnlyFiles(event))return activeDropData.get(this.reactive)}_containsOnlyFiles(event){return!!(event.dataTransfer.types&&event.dataTransfer.types.length>0)&&event.dataTransfer.types.every((type=>"Files"===type))}}return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_basecomponent=(obj=_basecomponent)&&obj.__esModule?obj:{default:obj};let activeDropData=new Map,dragStartPoint={};class _default extends _basecomponent.default{create(parent){var _parent$name,_parent$classes,_this$parent$draggabl,_this$parent$relative;this.name="".concat(null!==(_parent$name=parent.name)&&void 0!==_parent$name?_parent$name:"unkown","_dragdrop"),this.classes=Object.assign({BODYDRAGGING:"dragging",DRAGGABLEREADY:"draggable",DROPREADY:"dropready",DRAGOVER:"dragover",DRAGGING:"dragging",DROPUP:"drop-up",DROPDOWN:"drop-down",DROPZONE:"drop-zone",DRAGICON:"dragicon"},null!==(_parent$classes=null==parent?void 0:parent.classes)&&void 0!==_parent$classes?_parent$classes:{}),this.fullregion=parent.fullregion,this.parent=parent,this.autoconfigDraggable=null===(_this$parent$draggabl=this.parent.draggable)||void 0===_this$parent$draggabl||_this$parent$draggabl,this.relativeDrag=null!==(_this$parent$relative=this.parent.relativeDrag)&&void 0!==_this$parent$relative&&_this$parent$relative,this.entercount=0,this.dropzonevisible=!1,this.ismouseover=!1}getClasses(){return this.classes}isDropzoneVisible(){return this.dropzonevisible}stateReady(){"function"==typeof this.parent.validateDropData&&(this.element.classList.add(this.classes.DROPREADY),this.addEventListener(this.element,"dragenter",this._dragEnter),this.addEventListener(this.element,"dragleave",this._dragLeave),this.addEventListener(this.element,"dragover",this._dragOver),this.addEventListener(this.element,"drop",this._drop),this.addEventListener(this.element,"mouseover",this._mouseOver),this.addEventListener(this.element,"mouseleave",this._mouseLeave)),this.autoconfigDraggable&&"function"==typeof this.parent.getDraggableData&&this.setDraggable(!0)}setDraggable(value){if("function"!=typeof this.parent.getDraggableData)throw new Error("Draggable components must have a getDraggableData method");this.element.setAttribute("draggable",value),value?(this.addEventListener(this.element,"dragstart",this._dragStart),this.addEventListener(this.element,"dragend",this._dragEnd),this.element.classList.add(this.classes.DRAGGABLEREADY)):(this.removeEventListener(this.element,"dragstart",this._dragStart),this.removeEventListener(this.element,"dragend",this._dragEnd),this.element.classList.remove(this.classes.DRAGGABLEREADY))}_mouseOver(){this.ismouseover=!0}_mouseLeave(){this.ismouseover=!1}_dragStart(event){var _this$fullregion;if(document.activeElement.matches("textarea, input"))return void event.preventDefault();const dropdata=this.parent.getDraggableData();if(!dropdata)return;dragStartPoint={pageX:event.pageX,pageY:event.pageY},event.stopPropagation(),activeDropData.set(this.reactive,dropdata),document.body.classList.add(this.classes.BODYDRAGGING),this.element.classList.add(this.classes.DRAGGING),null===(_this$fullregion=this.fullregion)||void 0===_this$fullregion||_this$fullregion.classList.add(this.classes.DRAGGING);let dragImage=this.element;if(void 0!==this.parent.setDragImage){const customImage=this.parent.setDragImage(dropdata,event);customImage&&(dragImage=customImage)}const position={x:0,y:0};this.relativeDrag&&(position.x=event.offsetX,position.y=event.offsetY),event.dataTransfer.setDragImage(dragImage,position.x,position.y),this._callParentMethod("dragStart",dropdata,event)}_dragEnd(event){var _this$fullregion2;const dropdata=activeDropData.get(this.reactive);dropdata&&(activeDropData.delete(this.reactive),document.body.classList.remove(this.classes.BODYDRAGGING),this.element.classList.remove(this.classes.DRAGGING),null===(_this$fullregion2=this.fullregion)||void 0===_this$fullregion2||_this$fullregion2.classList.remove(this.classes.DRAGGING),this.removeAllOverlays(),this._addEventTotalMovement(event),this._callParentMethod("dragEnd",dropdata,event))}_dragEnter(event){const dropdata=this._processEvent(event);dropdata&&(this.entercount++,this.element.classList.add(this.classes.DRAGOVER),1!=this.entercount||this.dropzonevisible||(this.dropzonevisible=!0,this.element.classList.add(this.classes.DRAGOVER),this._callParentMethod("showDropZone",dropdata,event)))}_dragOver(event){const dropdata=this._processEvent(event);dropdata&&!this.dropzonevisible&&(this.dropzonevisible=!0,this.element.classList.add(this.classes.DRAGOVER),this._callParentMethod("showDropZone",dropdata,event))}_dragLeave(event){const dropdata=this._processEvent(event);dropdata&&(this.entercount--,this.entercount<=0&&this.dropzonevisible&&(this.dropzonevisible=!1,this.element.classList.remove(this.classes.DRAGOVER),this._callParentMethod("hideDropZone",dropdata,event)))}_drop(event){const dropdata=this._processEvent(event);dropdata&&(this.entercount=0,this.dropzonevisible&&(this.dropzonevisible=!1,this._callParentMethod("hideDropZone",dropdata,event)),this.element.classList.remove(this.classes.DRAGOVER),this.removeAllOverlays(),this._callParentMethod("drop",dropdata,event),dragStartPoint={})}_processEvent(event){const dropdata=this._getDropData(event);return dropdata&&this.parent.validateDropData(dropdata)?(event.preventDefault(),event.stopPropagation(),this._addEventTotalMovement(event),dropdata):null}_addEventTotalMovement(event){if(void 0===dragStartPoint.pageX||void 0===event.pageX)return;event.fixedMovementX=event.pageX-dragStartPoint.pageX,event.fixedMovementY=event.pageY-dragStartPoint.pageY,event.initialPageX=dragStartPoint.pageX,event.initialPageY=dragStartPoint.pageY;const current=this.element.getBoundingClientRect();if(event.newFixedTop=current.top+event.fixedMovementY,event.newFixedLeft=current.left+event.fixedMovementX,void 0!==this.fullregion){const current=this.fullregion.getBoundingClientRect();event.newRegionFixedxTop=current.top+event.fixedMovementY,event.newRegionFixedxLeft=current.left+event.fixedMovementX}}_callParentMethod(methodname,dropdata,event){"function"==typeof this.parent[methodname]&&this.parent[methodname](dropdata,event)}_getDropData(event){return this._isOnlyFilesDragging=this._containsOnlyFiles(event),this._isOnlyFilesDragging?void 0!==this.reactive.getFilesDraggableData&&"function"==typeof this.reactive.getFilesDraggableData?this.reactive.getFilesDraggableData(event.dataTransfer):void 0:activeDropData.get(this.reactive)}_containsOnlyFiles(event){return!!event.dataTransfer.types.includes("Files")&&event.dataTransfer.types.every((type=>"text/uri-list"!=type.toLowerCase()&&"text/html"!=type.toLowerCase()&&"text/plain"!=type.toLowerCase()))}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=dragdrop.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/reactive/dragdrop.min.js.map b/lib/amd/build/local/reactive/dragdrop.min.js.map index fbb6f7f8414..0a814846d68 100644 --- a/lib/amd/build/local/reactive/dragdrop.min.js.map +++ b/lib/amd/build/local/reactive/dragdrop.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dragdrop.min.js","sources":["../../../src/local/reactive/dragdrop.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 * Drag and drop helper component.\n *\n * This component is used to delegate drag and drop handling.\n *\n * To delegate the logic to this particular element the component should create a new instance\n * passing \"this\" as param. The component will use all the necessary callbacks and add all the\n * necessary listeners to the component element.\n *\n * Component attributes used by dragdrop module:\n * - element: the draggable or dropzone element.\n * - (optional) classes: object with alternative CSS classes\n * - (optional) fullregion: page element affeted by the elementy dragging. Use this attribute if\n * the draggable element affects a bigger region (for example a draggable\n * title).\n * - (optional) autoconfigDraggable: by default, the component will be draggable if it has a\n * getDraggableData method. If this value is false draggable\n * property must be defined using setDraggable method.\n * - (optional) relativeDrag: by default the drag image is located at point (0,0) relative to the\n * mouse position to prevent the mouse from covering it. If this attribute\n * is true the drag image will be located at the click offset.\n *\n * Methods the parent component should have for making it draggable:\n *\n * - getDraggableData(): Object|data\n * Return the data that will be passed to any valid dropzone while it is dragged.\n * If the component has this method, the dragdrop module will enable the dragging,\n * this is the only required method for dragging.\n * If at the dragging moment this method returns a false|null|undefined, the dragging\n * actions won't be captured.\n *\n * - (optional) dragStart(Object dropdata, Event event): void\n * - (optional) dragEnd(Object dropdata, Event event): void\n * Callbacks dragdrop will call when the element is dragged and getDraggableData\n * return some data.\n *\n * Methods the parent component should have for enabling it as a dropzone:\n *\n * - validateDropData(Object dropdata): boolean\n * If that method exists, the dragdrop module will automathically configure the element as dropzone.\n * This method will return true if the dropdata is accepted. In case it returns false, no drag and\n * drop event will be listened for this specific dragged dropdata.\n *\n * - (Optional) showDropZone(Object dropdata, Event event): void\n * - (Optional) hideDropZone(Object dropdata, Event event): void\n * Methods called when a valid dragged data pass over the element.\n *\n * - (Optional) drop(Object dropdata, Event event): void\n * Called when a valid dragged element is dropped over the element.\n *\n * Note that none of this methods will be called if validateDropData\n * returns a false value.\n *\n * This module will also add or remove several CSS classes from both dragged elements and dropzones.\n * See the \"this.classes\" in the create method for more details. In case the parent component wants\n * to use the same classes, it can use the getClasses method. On the other hand, if the parent\n * component has an alternative \"classes\" attribute, this will override the default drag and drop\n * classes.\n *\n * @module core/local/reactive/dragdrop\n * @class core/local/reactive/dragdrop\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BaseComponent from 'core/local/reactive/basecomponent';\n\n// Map with the dragged element generate by an specific reactive applications.\n// Potentially, any component can generate a draggable element to interact with other\n// page elements. However, the dragged data is specific and could only interact with\n// components of the same reactive instance.\nlet activeDropData = new Map();\n\n// Drag & Drop API provides the final drop point and incremental movements but we can\n// provide also starting points and displacements. Absolute displacements simplifies\n// moving components with aboslute position around the page.\nlet dragStartPoint = {};\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n *\n * @param {BaseComponent} parent the parent component.\n */\n create(parent) {\n // Optional component name for debugging.\n this.name = `${parent.name ?? 'unkown'}_dragdrop`;\n\n // Default drag and drop classes.\n this.classes = Object.assign(\n {\n // This class indicate a dragging action is active at a page level.\n BODYDRAGGING: 'dragging',\n\n // Added when draggable and drop are ready.\n DRAGGABLEREADY: 'draggable',\n DROPREADY: 'dropready',\n\n // When a valid drag element is over the element.\n DRAGOVER: 'dragover',\n // When a the component is dragged.\n DRAGGING: 'dragging',\n\n // Dropzones classes names.\n DROPUP: 'drop-up',\n DROPDOWN: 'drop-down',\n DROPZONE: 'drop-zone',\n\n // Drag icon class.\n DRAGICON: 'dragicon',\n },\n parent?.classes ?? {}\n );\n\n // Add the affected region if any.\n this.fullregion = parent.fullregion;\n\n // Keep parent to execute drap and drop handlers.\n this.parent = parent;\n\n // Check if parent handle draggable manually.\n this.autoconfigDraggable = this.parent.draggable ?? true;\n\n // Drag image relative position.\n this.relativeDrag = this.parent.relativeDrag ?? false;\n\n // Sub HTML elements will trigger extra dragEnter and dragOver all the time.\n // To prevent that from affecting dropzones, we need to count the enters and leaves.\n this.entercount = 0;\n\n // Stores if the droparea is shown or not.\n this.dropzonevisible = false;\n\n }\n\n /**\n * Return the component drag and drop CSS classes.\n *\n * @returns {Object} the dragdrop css classes\n */\n getClasses() {\n return this.classes;\n }\n\n /**\n * Initial state ready method.\n *\n * This method will add all the necessary event listeners to the component depending on the\n * parent methods.\n * - Add drop events to the element if the parent component has validateDropData method.\n * - Configure the elements draggable if the parent component has getDraggableData method.\n */\n stateReady() {\n // Add drop events to the element if the parent component has dropable types.\n if (typeof this.parent.validateDropData === 'function') {\n this.element.classList.add(this.classes.DROPREADY);\n this.addEventListener(this.element, 'dragenter', this._dragEnter);\n this.addEventListener(this.element, 'dragleave', this._dragLeave);\n this.addEventListener(this.element, 'dragover', this._dragOver);\n this.addEventListener(this.element, 'drop', this._drop);\n }\n\n // Configure the elements draggable if the parent component has dragable data.\n if (this.autoconfigDraggable && typeof this.parent.getDraggableData === 'function') {\n this.setDraggable(true);\n }\n }\n\n /**\n * Enable or disable the draggable property.\n *\n * @param {bool} value the new draggable value\n */\n setDraggable(value) {\n if (typeof this.parent.getDraggableData !== 'function') {\n throw new Error(`Draggable components must have a getDraggableData method`);\n }\n this.element.setAttribute('draggable', value);\n if (value) {\n this.addEventListener(this.element, 'dragstart', this._dragStart);\n this.addEventListener(this.element, 'dragend', this._dragEnd);\n this.element.classList.add(this.classes.DRAGGABLEREADY);\n } else {\n this.removeEventListener(this.element, 'dragstart', this._dragStart);\n this.removeEventListener(this.element, 'dragend', this._dragEnd);\n this.element.classList.remove(this.classes.DRAGGABLEREADY);\n }\n }\n\n /**\n * Drag start event handler.\n *\n * This method will generate the current dropable data. This data is the one used to determine\n * if a droparea accepts the dropping or not.\n *\n * @param {Event} event the event.\n */\n _dragStart(event) {\n // Cancel dragging if any editable form element is focussed.\n if (document.activeElement.matches(`textarea, input`)) {\n event.preventDefault();\n return;\n }\n\n const dropdata = this.parent.getDraggableData();\n if (!dropdata) {\n return;\n }\n\n // Save the starting point.\n dragStartPoint = {\n pageX: event.pageX,\n pageY: event.pageY,\n };\n\n // If the drag event is accepted we prevent any other draggable element from interfiering.\n event.stopPropagation();\n\n // Save the drop data of the current reactive intance.\n activeDropData.set(this.reactive, dropdata);\n\n // Add some CSS classes to indicate the state.\n document.body.classList.add(this.classes.BODYDRAGGING);\n this.element.classList.add(this.classes.DRAGGING);\n this.fullregion?.classList.add(this.classes.DRAGGING);\n\n // Force the drag image. This makes the UX more consistent in case the\n // user dragged an internal element like a link or some other element.\n let dragImage = this.element;\n if (this.parent.setDragImage !== undefined) {\n const customImage = this.parent.setDragImage(dropdata, event);\n if (customImage) {\n dragImage = customImage;\n }\n }\n // Define the image position relative to the mouse.\n const position = {x: 0, y: 0};\n if (this.relativeDrag) {\n position.x = event.offsetX;\n position.y = event.offsetY;\n }\n event.dataTransfer.setDragImage(dragImage, position.x, position.y);\n\n this._callParentMethod('dragStart', dropdata, event);\n }\n\n /**\n * Drag end event handler.\n *\n * @param {Event} event the event.\n */\n _dragEnd(event) {\n const dropdata = activeDropData.get(this.reactive);\n if (!dropdata) {\n return;\n }\n\n // Remove the current dropdata.\n activeDropData.delete(this.reactive);\n\n // Remove the dragging classes.\n document.body.classList.remove(this.classes.BODYDRAGGING);\n this.element.classList.remove(this.classes.DRAGGING);\n this.fullregion?.classList.remove(this.classes.DRAGGING);\n\n // We add the total movement to the event in case the component\n // wants to move its absolute position.\n this._addEventTotalMovement(event);\n\n this._callParentMethod('dragEnd', dropdata, event);\n }\n\n /**\n * Drag enter event handler.\n *\n * The JS drag&drop API triggers several dragenter events on the same element because it bubbles the\n * child events as well. To prevent this form affecting the dropzones display, this methods use\n * \"entercount\" to determine if it's one extra child event or a valid one.\n *\n * @param {Event} event the event.\n */\n _dragEnter(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount++;\n this.element.classList.add(this.classes.DRAGOVER);\n if (this.entercount == 1 && !this.dropzonevisible) {\n this.dropzonevisible = true;\n this.element.classList.add(this.classes.DRAGOVER);\n this._callParentMethod('showDropZone', dropdata, event);\n }\n }\n }\n\n /**\n * Drag over event handler.\n *\n * We only use dragover event when a draggable action starts inside a valid dropzone. In those cases\n * the API won't trigger any dragEnter because the dragged alement was already there. We use the\n * dropzonevisible to determine if the component needs to display the dropzones or not.\n *\n * @param {Event} event the event.\n */\n _dragOver(event) {\n const dropdata = this._processEvent(event);\n if (dropdata && !this.dropzonevisible) {\n this.dropzonevisible = true;\n this.element.classList.add(this.classes.DRAGOVER);\n this._callParentMethod('showDropZone', dropdata, event);\n }\n }\n\n /**\n * Drag over leave handler.\n *\n * The JS drag&drop API triggers several dragleave events on the same element because it bubbles the\n * child events as well. To prevent this form affecting the dropzones display, this methods use\n * \"entercount\" to determine if it's one extra child event or a valid one.\n *\n * @param {Event} event the event.\n */\n _dragLeave(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount--;\n if (this.entercount == 0 && this.dropzonevisible) {\n this.dropzonevisible = false;\n this.element.classList.remove(this.classes.DRAGOVER);\n this._callParentMethod('hideDropZone', dropdata, event);\n }\n }\n }\n\n /**\n * Drop event handler.\n *\n * This method will call both hideDropZones and drop methods on the parent component.\n *\n * @param {Event} event the event.\n */\n _drop(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount = 0;\n if (this.dropzonevisible) {\n this.dropzonevisible = false;\n this._callParentMethod('hideDropZone', dropdata, event);\n }\n this.element.classList.remove(this.classes.DRAGOVER);\n this._callParentMethod('drop', dropdata, event);\n // An accepted drop resets the initial position.\n // Save the starting point.\n dragStartPoint = {};\n }\n }\n\n /**\n * Process a drag and drop event and delegate logic to the parent component.\n *\n * @param {Event} event the drag and drop event\n * @return {Object|false} the dropdata or null if the event should not be processed\n */\n _processEvent(event) {\n const dropdata = this._getDropData(event);\n if (!dropdata) {\n return null;\n }\n if (this.parent.validateDropData(dropdata)) {\n // All accepted drag&drop event must prevent bubbling and defaults, otherwise\n // parent dragdrop instances could capture it by mistake.\n event.preventDefault();\n event.stopPropagation();\n this._addEventTotalMovement(event);\n return dropdata;\n }\n return null;\n }\n\n /**\n * Add the total amout of movement to a mouse event.\n *\n * @param {MouseEvent} event\n */\n _addEventTotalMovement(event) {\n if (dragStartPoint.pageX === undefined || event.pageX === undefined) {\n return;\n }\n event.fixedMovementX = event.pageX - dragStartPoint.pageX;\n event.fixedMovementY = event.pageY - dragStartPoint.pageY;\n event.initialPageX = dragStartPoint.pageX;\n event.initialPageY = dragStartPoint.pageY;\n // The element possible new top.\n const current = this.element.getBoundingClientRect();\n // Add the new position fixed position.\n event.newFixedTop = current.top + event.fixedMovementY;\n event.newFixedLeft = current.left + event.fixedMovementX;\n // The affected region possible new top.\n if (this.fullregion !== undefined) {\n const current = this.fullregion.getBoundingClientRect();\n event.newRegionFixedxTop = current.top + event.fixedMovementY;\n event.newRegionFixedxLeft = current.left + event.fixedMovementX;\n }\n }\n\n /**\n * Convenient method for calling parent component functions if present.\n *\n * @param {string} methodname the name of the method\n * @param {Object} dropdata the current drop data object\n * @param {Event} event the original event\n */\n _callParentMethod(methodname, dropdata, event) {\n if (typeof this.parent[methodname] === 'function') {\n this.parent[methodname](dropdata, event);\n }\n }\n\n /**\n * Get the current dropdata for a specific event.\n *\n * The browser can generate drag&drop events related to several user interactions:\n * - Drag a page elements: this case is registered in the activeDropData map\n * - Drag some HTML selections: ignored for now\n * - Drag a file over the browser: file drag may appear in the future but for now they are ignored.\n *\n * @param {Event} event the original event.\n * @returns {Object|undefined} with the dragged data (or undefined if none)\n */\n _getDropData(event) {\n if (this._containsOnlyFiles(event)) {\n return undefined;\n }\n return activeDropData.get(this.reactive);\n }\n\n /**\n * Check if the dragged event contains only files.\n *\n * Files dragging does not generate drop data because they came from outside the page and the component\n * must check it before validating the event.\n *\n * @param {Event} event the original event.\n * @returns {boolean} if the drag dataTransfers contains files.\n */\n _containsOnlyFiles(event) {\n if (event.dataTransfer.types && event.dataTransfer.types.length > 0) {\n // Chrome drag page images as files. To differentiate a real file from a page\n // image we need to check if all the dataTransfers types are files.\n return event.dataTransfer.types.every(type => type === 'Files');\n }\n return false;\n }\n}\n"],"names":["activeDropData","Map","dragStartPoint","BaseComponent","create","parent","name","classes","Object","assign","BODYDRAGGING","DRAGGABLEREADY","DROPREADY","DRAGOVER","DRAGGING","DROPUP","DROPDOWN","DROPZONE","DRAGICON","fullregion","autoconfigDraggable","this","draggable","relativeDrag","entercount","dropzonevisible","getClasses","stateReady","validateDropData","element","classList","add","addEventListener","_dragEnter","_dragLeave","_dragOver","_drop","getDraggableData","setDraggable","value","Error","setAttribute","_dragStart","_dragEnd","removeEventListener","remove","event","document","activeElement","matches","preventDefault","dropdata","pageX","pageY","stopPropagation","set","reactive","body","dragImage","undefined","setDragImage","customImage","position","x","y","offsetX","offsetY","dataTransfer","_callParentMethod","get","delete","_addEventTotalMovement","_processEvent","_getDropData","fixedMovementX","fixedMovementY","initialPageX","initialPageY","current","getBoundingClientRect","newFixedTop","top","newFixedLeft","left","newRegionFixedxTop","newRegionFixedxLeft","methodname","_containsOnlyFiles","types","length","every","type"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"dragdrop.min.js","sources":["../../../src/local/reactive/dragdrop.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 * Drag and drop helper component.\n *\n * This component is used to delegate drag and drop handling.\n *\n * To delegate the logic to this particular element the component should create a new instance\n * passing \"this\" as param. The component will use all the necessary callbacks and add all the\n * necessary listeners to the component element.\n *\n * Component attributes used by dragdrop module:\n * - element: the draggable or dropzone element.\n * - (optional) classes: object with alternative CSS classes\n * - (optional) fullregion: page element affeted by the elementy dragging. Use this attribute if\n * the draggable element affects a bigger region (for example a draggable\n * title).\n * - (optional) autoconfigDraggable: by default, the component will be draggable if it has a\n * getDraggableData method. If this value is false draggable\n * property must be defined using setDraggable method.\n * - (optional) relativeDrag: by default the drag image is located at point (0,0) relative to the\n * mouse position to prevent the mouse from covering it. If this attribute\n * is true the drag image will be located at the click offset.\n *\n * Methods the parent component should have for making it draggable:\n *\n * - getDraggableData(): Object|data\n * Return the data that will be passed to any valid dropzone while it is dragged.\n * If the component has this method, the dragdrop module will enable the dragging,\n * this is the only required method for dragging.\n * If at the dragging moment this method returns a false|null|undefined, the dragging\n * actions won't be captured.\n *\n * - (optional) dragStart(Object dropdata, Event event): void\n * - (optional) dragEnd(Object dropdata, Event event): void\n * Callbacks dragdrop will call when the element is dragged and getDraggableData\n * return some data.\n *\n * Methods the parent component should have for enabling it as a dropzone:\n *\n * - validateDropData(Object dropdata): boolean\n * If that method exists, the dragdrop module will automathically configure the element as dropzone.\n * This method will return true if the dropdata is accepted. In case it returns false, no drag and\n * drop event will be listened for this specific dragged dropdata.\n *\n * - (Optional) showDropZone(Object dropdata, Event event): void\n * - (Optional) hideDropZone(Object dropdata, Event event): void\n * Methods called when a valid dragged data pass over the element.\n *\n * - (Optional) drop(Object dropdata, Event event): void\n * Called when a valid dragged element is dropped over the element.\n *\n * Note that none of this methods will be called if validateDropData\n * returns a false value.\n *\n * This module will also add or remove several CSS classes from both dragged elements and dropzones.\n * See the \"this.classes\" in the create method for more details. In case the parent component wants\n * to use the same classes, it can use the getClasses method. On the other hand, if the parent\n * component has an alternative \"classes\" attribute, this will override the default drag and drop\n * classes.\n *\n * @module core/local/reactive/dragdrop\n * @class core/local/reactive/dragdrop\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BaseComponent from 'core/local/reactive/basecomponent';\n\n// Map with the dragged element generate by an specific reactive applications.\n// Potentially, any component can generate a draggable element to interact with other\n// page elements. However, the dragged data is specific and could only interact with\n// components of the same reactive instance.\nlet activeDropData = new Map();\n\n// Drag & Drop API provides the final drop point and incremental movements but we can\n// provide also starting points and displacements. Absolute displacements simplifies\n// moving components with aboslute position around the page.\nlet dragStartPoint = {};\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n *\n * @param {BaseComponent} parent the parent component.\n */\n create(parent) {\n // Optional component name for debugging.\n this.name = `${parent.name ?? 'unkown'}_dragdrop`;\n\n // Default drag and drop classes.\n this.classes = Object.assign(\n {\n // This class indicate a dragging action is active at a page level.\n BODYDRAGGING: 'dragging',\n\n // Added when draggable and drop are ready.\n DRAGGABLEREADY: 'draggable',\n DROPREADY: 'dropready',\n\n // When a valid drag element is over the element.\n DRAGOVER: 'dragover',\n // When a the component is dragged.\n DRAGGING: 'dragging',\n\n // Dropzones classes names.\n DROPUP: 'drop-up',\n DROPDOWN: 'drop-down',\n DROPZONE: 'drop-zone',\n\n // Drag icon class.\n DRAGICON: 'dragicon',\n },\n parent?.classes ?? {}\n );\n\n // Add the affected region if any.\n this.fullregion = parent.fullregion;\n\n // Keep parent to execute drap and drop handlers.\n this.parent = parent;\n\n // Check if parent handle draggable manually.\n this.autoconfigDraggable = this.parent.draggable ?? true;\n\n // Drag image relative position.\n this.relativeDrag = this.parent.relativeDrag ?? false;\n\n // Sub HTML elements will trigger extra dragEnter and dragOver all the time.\n // To prevent that from affecting dropzones, we need to count the enters and leaves.\n this.entercount = 0;\n\n // Stores if the droparea is shown or not.\n this.dropzonevisible = false;\n\n // Stores if the mouse is over the element or not.\n this.ismouseover = false;\n }\n\n /**\n * Return the component drag and drop CSS classes.\n *\n * @returns {Object} the dragdrop css classes\n */\n getClasses() {\n return this.classes;\n }\n\n /**\n * Return the current drop-zone visible of the element.\n *\n * @returns {boolean} if the dropzone should be visible or not\n */\n isDropzoneVisible() {\n return this.dropzonevisible;\n }\n\n /**\n * Initial state ready method.\n *\n * This method will add all the necessary event listeners to the component depending on the\n * parent methods.\n * - Add drop events to the element if the parent component has validateDropData method.\n * - Configure the elements draggable if the parent component has getDraggableData method.\n */\n stateReady() {\n // Add drop events to the element if the parent component has dropable types.\n if (typeof this.parent.validateDropData === 'function') {\n this.element.classList.add(this.classes.DROPREADY);\n this.addEventListener(this.element, 'dragenter', this._dragEnter);\n this.addEventListener(this.element, 'dragleave', this._dragLeave);\n this.addEventListener(this.element, 'dragover', this._dragOver);\n this.addEventListener(this.element, 'drop', this._drop);\n this.addEventListener(this.element, 'mouseover', this._mouseOver);\n this.addEventListener(this.element, 'mouseleave', this._mouseLeave);\n }\n\n // Configure the elements draggable if the parent component has dragable data.\n if (this.autoconfigDraggable && typeof this.parent.getDraggableData === 'function') {\n this.setDraggable(true);\n }\n }\n\n /**\n * Enable or disable the draggable property.\n *\n * @param {bool} value the new draggable value\n */\n setDraggable(value) {\n if (typeof this.parent.getDraggableData !== 'function') {\n throw new Error(`Draggable components must have a getDraggableData method`);\n }\n this.element.setAttribute('draggable', value);\n if (value) {\n this.addEventListener(this.element, 'dragstart', this._dragStart);\n this.addEventListener(this.element, 'dragend', this._dragEnd);\n this.element.classList.add(this.classes.DRAGGABLEREADY);\n } else {\n this.removeEventListener(this.element, 'dragstart', this._dragStart);\n this.removeEventListener(this.element, 'dragend', this._dragEnd);\n this.element.classList.remove(this.classes.DRAGGABLEREADY);\n }\n }\n\n /**\n * Mouse over handle.\n */\n _mouseOver() {\n this.ismouseover = true;\n }\n\n /**\n * Mouse leave handler.\n */\n _mouseLeave() {\n this.ismouseover = false;\n }\n\n /**\n * Drag start event handler.\n *\n * This method will generate the current dropable data. This data is the one used to determine\n * if a droparea accepts the dropping or not.\n *\n * @param {Event} event the event.\n */\n _dragStart(event) {\n // Cancel dragging if any editable form element is focussed.\n if (document.activeElement.matches(`textarea, input`)) {\n event.preventDefault();\n return;\n }\n\n const dropdata = this.parent.getDraggableData();\n if (!dropdata) {\n return;\n }\n\n // Save the starting point.\n dragStartPoint = {\n pageX: event.pageX,\n pageY: event.pageY,\n };\n\n // If the drag event is accepted we prevent any other draggable element from interfiering.\n event.stopPropagation();\n\n // Save the drop data of the current reactive intance.\n activeDropData.set(this.reactive, dropdata);\n\n // Add some CSS classes to indicate the state.\n document.body.classList.add(this.classes.BODYDRAGGING);\n this.element.classList.add(this.classes.DRAGGING);\n this.fullregion?.classList.add(this.classes.DRAGGING);\n\n // Force the drag image. This makes the UX more consistent in case the\n // user dragged an internal element like a link or some other element.\n let dragImage = this.element;\n if (this.parent.setDragImage !== undefined) {\n const customImage = this.parent.setDragImage(dropdata, event);\n if (customImage) {\n dragImage = customImage;\n }\n }\n // Define the image position relative to the mouse.\n const position = {x: 0, y: 0};\n if (this.relativeDrag) {\n position.x = event.offsetX;\n position.y = event.offsetY;\n }\n event.dataTransfer.setDragImage(dragImage, position.x, position.y);\n\n this._callParentMethod('dragStart', dropdata, event);\n }\n\n /**\n * Drag end event handler.\n *\n * @param {Event} event the event.\n */\n _dragEnd(event) {\n const dropdata = activeDropData.get(this.reactive);\n if (!dropdata) {\n return;\n }\n\n // Remove the current dropdata.\n activeDropData.delete(this.reactive);\n\n // Remove the dragging classes.\n document.body.classList.remove(this.classes.BODYDRAGGING);\n this.element.classList.remove(this.classes.DRAGGING);\n this.fullregion?.classList.remove(this.classes.DRAGGING);\n this.removeAllOverlays();\n\n // We add the total movement to the event in case the component\n // wants to move its absolute position.\n this._addEventTotalMovement(event);\n\n this._callParentMethod('dragEnd', dropdata, event);\n }\n\n /**\n * Drag enter event handler.\n *\n * The JS drag&drop API triggers several dragenter events on the same element because it bubbles the\n * child events as well. To prevent this form affecting the dropzones display, this methods use\n * \"entercount\" to determine if it's one extra child event or a valid one.\n *\n * @param {Event} event the event.\n */\n _dragEnter(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount++;\n this.element.classList.add(this.classes.DRAGOVER);\n if (this.entercount == 1 && !this.dropzonevisible) {\n this.dropzonevisible = true;\n this.element.classList.add(this.classes.DRAGOVER);\n this._callParentMethod('showDropZone', dropdata, event);\n }\n }\n }\n\n /**\n * Drag over event handler.\n *\n * We only use dragover event when a draggable action starts inside a valid dropzone. In those cases\n * the API won't trigger any dragEnter because the dragged alement was already there. We use the\n * dropzonevisible to determine if the component needs to display the dropzones or not.\n *\n * @param {Event} event the event.\n */\n _dragOver(event) {\n const dropdata = this._processEvent(event);\n if (dropdata && !this.dropzonevisible) {\n this.dropzonevisible = true;\n this.element.classList.add(this.classes.DRAGOVER);\n this._callParentMethod('showDropZone', dropdata, event);\n }\n }\n\n /**\n * Drag over leave handler.\n *\n * The JS drag&drop API triggers several dragleave events on the same element because it bubbles the\n * child events as well. To prevent this form affecting the dropzones display, this methods use\n * \"entercount\" to determine if it's one extra child event or a valid one.\n *\n * @param {Event} event the event.\n */\n _dragLeave(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount--;\n if (this.entercount <= 0 && this.dropzonevisible) {\n this.dropzonevisible = false;\n this.element.classList.remove(this.classes.DRAGOVER);\n this._callParentMethod('hideDropZone', dropdata, event);\n }\n }\n }\n\n /**\n * Drop event handler.\n *\n * This method will call both hideDropZones and drop methods on the parent component.\n *\n * @param {Event} event the event.\n */\n _drop(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount = 0;\n if (this.dropzonevisible) {\n this.dropzonevisible = false;\n this._callParentMethod('hideDropZone', dropdata, event);\n }\n this.element.classList.remove(this.classes.DRAGOVER);\n this.removeAllOverlays();\n this._callParentMethod('drop', dropdata, event);\n // An accepted drop resets the initial position.\n // Save the starting point.\n dragStartPoint = {};\n }\n }\n\n /**\n * Process a drag and drop event and delegate logic to the parent component.\n *\n * @param {Event} event the drag and drop event\n * @return {Object|false} the dropdata or null if the event should not be processed\n */\n _processEvent(event) {\n const dropdata = this._getDropData(event);\n if (!dropdata) {\n return null;\n }\n if (this.parent.validateDropData(dropdata)) {\n // All accepted drag&drop event must prevent bubbling and defaults, otherwise\n // parent dragdrop instances could capture it by mistake.\n event.preventDefault();\n event.stopPropagation();\n this._addEventTotalMovement(event);\n return dropdata;\n }\n return null;\n }\n\n /**\n * Add the total amout of movement to a mouse event.\n *\n * @param {MouseEvent} event\n */\n _addEventTotalMovement(event) {\n if (dragStartPoint.pageX === undefined || event.pageX === undefined) {\n return;\n }\n event.fixedMovementX = event.pageX - dragStartPoint.pageX;\n event.fixedMovementY = event.pageY - dragStartPoint.pageY;\n event.initialPageX = dragStartPoint.pageX;\n event.initialPageY = dragStartPoint.pageY;\n // The element possible new top.\n const current = this.element.getBoundingClientRect();\n // Add the new position fixed position.\n event.newFixedTop = current.top + event.fixedMovementY;\n event.newFixedLeft = current.left + event.fixedMovementX;\n // The affected region possible new top.\n if (this.fullregion !== undefined) {\n const current = this.fullregion.getBoundingClientRect();\n event.newRegionFixedxTop = current.top + event.fixedMovementY;\n event.newRegionFixedxLeft = current.left + event.fixedMovementX;\n }\n }\n\n /**\n * Convenient method for calling parent component functions if present.\n *\n * @param {string} methodname the name of the method\n * @param {Object} dropdata the current drop data object\n * @param {Event} event the original event\n */\n _callParentMethod(methodname, dropdata, event) {\n if (typeof this.parent[methodname] === 'function') {\n this.parent[methodname](dropdata, event);\n }\n }\n\n /**\n * Get the current dropdata for a specific event.\n *\n * The browser can generate drag&drop events related to several user interactions:\n * - Drag a page elements: this case is registered in the activeDropData map\n * - Drag some HTML selections: ignored for now\n * - Drag a file over the browser: file drag may appear in the future but for now they are ignored.\n *\n * @param {Event} event the original event.\n * @returns {Object|undefined} with the dragged data (or undefined if none)\n */\n _getDropData(event) {\n this._isOnlyFilesDragging = this._containsOnlyFiles(event);\n if (this._isOnlyFilesDragging) {\n // Check if the reactive instance can provide a files draggable data.\n if (this.reactive.getFilesDraggableData !== undefined && typeof this.reactive.getFilesDraggableData === 'function') {\n return this.reactive.getFilesDraggableData(event.dataTransfer);\n }\n return undefined;\n }\n return activeDropData.get(this.reactive);\n }\n\n /**\n * Check if the dragged event contains only files.\n *\n * Files dragging does not generate drop data because they came from outside the page and the component\n * must check it before validating the event.\n *\n * Some browsers like Firefox add extra types to file dragging. To discard the false positives\n * a double check is necessary.\n *\n * @param {Event} event the original event.\n * @returns {boolean} if the drag dataTransfers contains files.\n */\n _containsOnlyFiles(event) {\n if (!event.dataTransfer.types.includes('Files')) {\n return false;\n }\n return event.dataTransfer.types.every((type) => {\n return (type.toLowerCase() != 'text/uri-list'\n && type.toLowerCase() != 'text/html'\n && type.toLowerCase() != 'text/plain'\n );\n });\n }\n}\n"],"names":["activeDropData","Map","dragStartPoint","BaseComponent","create","parent","name","classes","Object","assign","BODYDRAGGING","DRAGGABLEREADY","DROPREADY","DRAGOVER","DRAGGING","DROPUP","DROPDOWN","DROPZONE","DRAGICON","fullregion","autoconfigDraggable","this","draggable","relativeDrag","entercount","dropzonevisible","ismouseover","getClasses","isDropzoneVisible","stateReady","validateDropData","element","classList","add","addEventListener","_dragEnter","_dragLeave","_dragOver","_drop","_mouseOver","_mouseLeave","getDraggableData","setDraggable","value","Error","setAttribute","_dragStart","_dragEnd","removeEventListener","remove","event","document","activeElement","matches","preventDefault","dropdata","pageX","pageY","stopPropagation","set","reactive","body","dragImage","undefined","setDragImage","customImage","position","x","y","offsetX","offsetY","dataTransfer","_callParentMethod","get","delete","removeAllOverlays","_addEventTotalMovement","_processEvent","_getDropData","fixedMovementX","fixedMovementY","initialPageX","initialPageY","current","getBoundingClientRect","newFixedTop","top","newFixedLeft","left","newRegionFixedxTop","newRegionFixedxLeft","methodname","_isOnlyFilesDragging","_containsOnlyFiles","getFilesDraggableData","types","includes","every","type","toLowerCase"],"mappings":"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"} \ No newline at end of file diff --git a/lib/amd/build/local/reactive/overlay.min.js b/lib/amd/build/local/reactive/overlay.min.js new file mode 100644 index 00000000000..2b2d0ea5eae --- /dev/null +++ b/lib/amd/build/local/reactive/overlay.min.js @@ -0,0 +1,13 @@ +define("core/local/reactive/overlay",["exports","core/templates","core/prefetch"],(function(_exports,_templates,_prefetch){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * Element overlay methods. + * + * This module is used to create overlay information on components. For example + * to generate or destroy file drop-zones. + * + * @module core/local/reactive/overlay + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.removeOverlay=_exports.removeAllOverlays=_exports.addOverlay=void 0,_templates=_interopRequireDefault(_templates),_prefetch=_interopRequireDefault(_prefetch);_prefetch.default.prefetchTemplate("core/local/reactive/overlay");const selectors_OVERLAY="[data-overlay]",selectors_REPOSITION="[data-overlay-dynamic]",selectors_NAVBAR="nav.navbar.fixed-top";_exports.addOverlay=async(definition,parent)=>{var _definition$classes;definition.content&&"string"!=typeof definition.content&&(definition.content=await definition.content),definition.icon&&"string"!=typeof definition.icon&&(definition.icon=await definition.icon);const data={content:definition.content,css:null!==(_definition$classes=definition.classes)&&void 0!==_definition$classes?_definition$classes:"file-drop-zone"};let overlay;try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/reactive/overlay",data);_templates.default.appendNodeContents(parent,html,js),overlay=parent.querySelector(selectors_OVERLAY),rePositionPreviewInfoElement(overlay),init()}catch(error){throw error}return overlay};const removeOverlay=overlay=>{var _overlay$dataset;overlay&&overlay.parentNode&&(null!==(_overlay$dataset=overlay.dataset)&&void 0!==_overlay$dataset&&_overlay$dataset.overlayPosition&&delete overlay.parentNode.style.position,overlay.parentNode.removeChild(overlay))};_exports.removeOverlay=removeOverlay;_exports.removeAllOverlays=()=>{document.querySelectorAll(selectors_OVERLAY).forEach((overlay=>{removeOverlay(overlay)}))};const rePositionPreviewInfoElement=function(overlay){var _overlay$parentNode,_overlay$parentNode$s;if(!overlay)throw new Error("Inexistent overlay element");null!==(_overlay$parentNode=overlay.parentNode)&&void 0!==_overlay$parentNode&&null!==(_overlay$parentNode$s=_overlay$parentNode.style)&&void 0!==_overlay$parentNode$s&&_overlay$parentNode$s.position||(overlay.parentNode.style.position="relative",overlay.dataset.overlayPosition="true");const target=overlay.querySelector(selectors_REPOSITION);if(!target)return;const rect=overlay.getBoundingClientRect(),sectionHeight=parseInt(window.getComputedStyle(overlay).height,10),sectionOffset=rect.top,previewHeight=parseInt(window.getComputedStyle(target).height,10)+2*parseInt(window.getComputedStyle(target).padding,10);let top,bottom;if(sectionOffset<0)if(sectionHeight+sectionOffset>=previewHeight){let offSetTop=0-sectionOffset;const navBar=document.querySelector(selectors_NAVBAR);navBar&&(offSetTop+=navBar.offsetHeight),top=offSetTop+"px",bottom="unset"}else top="unset",bottom=0;else top=0,bottom="unset";target.style.top=top,target.style.bottom=bottom},init=()=>{document.addEventListener("scroll",(()=>{document.querySelectorAll(selectors_OVERLAY).forEach((overlay=>{rePositionPreviewInfoElement(overlay)}))}),!0)}})); + +//# sourceMappingURL=overlay.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/reactive/overlay.min.js.map b/lib/amd/build/local/reactive/overlay.min.js.map new file mode 100644 index 00000000000..76623879487 --- /dev/null +++ b/lib/amd/build/local/reactive/overlay.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"overlay.min.js","sources":["../../../src/local/reactive/overlay.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 * Element overlay methods.\n *\n * This module is used to create overlay information on components. For example\n * to generate or destroy file drop-zones.\n *\n * @module core/local/reactive/overlay\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Prefetch from 'core/prefetch';\n\n// Prefetch the overlay html.\nconst overlayTemplate = 'core/local/reactive/overlay';\nPrefetch.prefetchTemplate(overlayTemplate);\n\n/**\n * @var {boolean} isInitialized if the module is capturing the proper page events.\n */\nlet isInitialized = false;\n\n/**\n * @var {Object} isInitialized if the module is capturing the proper page events.\n */\nconst selectors = {\n OVERLAY: \"[data-overlay]\",\n REPOSITION: \"[data-overlay-dynamic]\",\n NAVBAR: \"nav.navbar.fixed-top\",\n};\n\n/**\n * Adds an overlay to a specific page element.\n *\n * @param {Object} definition the overlay definition.\n * @param {String|Promise} definition.content an optional overlay content.\n * @param {String|Promise} definition.icon an optional icon content.\n * @param {String} definition.classes an optional CSS classes\n * @param {HTMLElement} parent the parent object\n * @return {HTMLElement|undefined} the new page element.\n */\nexport const addOverlay = async(definition, parent) => {\n // Validate non of the passed params is a promise.\n if (definition.content && typeof definition.content !== 'string') {\n definition.content = await definition.content;\n }\n if (definition.icon && typeof definition.icon !== 'string') {\n definition.icon = await definition.icon;\n }\n const data = {\n content: definition.content,\n css: definition.classes ?? 'file-drop-zone',\n };\n let overlay;\n try {\n const {html, js} = await Templates.renderForPromise(overlayTemplate, data);\n Templates.appendNodeContents(parent, html, js);\n overlay = parent.querySelector(selectors.OVERLAY);\n rePositionPreviewInfoElement(overlay);\n init();\n } catch (error) {\n throw error;\n }\n return overlay;\n};\n\n/**\n * Adds an overlay to a specific page element.\n *\n * @param {HTMLElement} overlay the parent object\n */\nexport const removeOverlay = (overlay) => {\n if (!overlay || !overlay.parentNode) {\n return;\n }\n // Remove any forced parentNode position.\n if (overlay.dataset?.overlayPosition) {\n delete overlay.parentNode.style.position;\n }\n overlay.parentNode.removeChild(overlay);\n};\n\nexport const removeAllOverlays = () => {\n document.querySelectorAll(selectors.OVERLAY).forEach(\n (overlay) => {\n removeOverlay(overlay);\n }\n );\n};\n\n/**\n * Re-position the preview information element by calculating the section position.\n *\n * @param {Object} overlay the overlay element.\n */\nconst rePositionPreviewInfoElement = function(overlay) {\n if (!overlay) {\n throw new Error('Inexistent overlay element');\n }\n // Add relative position to the parent object.\n if (!overlay.parentNode?.style?.position) {\n overlay.parentNode.style.position = 'relative';\n overlay.dataset.overlayPosition = \"true\";\n }\n // Get the element to reposition.\n const target = overlay.querySelector(selectors.REPOSITION);\n if (!target) {\n return;\n }\n // Get the new bounds.\n const rect = overlay.getBoundingClientRect();\n const sectionHeight = parseInt(window.getComputedStyle(overlay).height, 10);\n const sectionOffset = rect.top;\n const previewHeight = parseInt(window.getComputedStyle(target).height, 10) +\n (2 * parseInt(window.getComputedStyle(target).padding, 10));\n // Calculate the new target position.\n let top, bottom;\n if (sectionOffset < 0) {\n if (sectionHeight + sectionOffset >= previewHeight) {\n // We have enough space here, just stick the preview to the top.\n let offSetTop = 0 - sectionOffset;\n const navBar = document.querySelector(selectors.NAVBAR);\n if (navBar) {\n offSetTop = offSetTop + navBar.offsetHeight;\n }\n top = offSetTop + 'px';\n bottom = 'unset';\n } else {\n // We do not have enough space here, just stick the preview to the bottom.\n top = 'unset';\n bottom = 0;\n }\n } else {\n top = 0;\n bottom = 'unset';\n }\n\n target.style.top = top;\n target.style.bottom = bottom;\n};\n\n// Update overlays when the page scrolls.\nconst init = () => {\n if (isInitialized) {\n return;\n }\n // Add scroll events.\n document.addEventListener('scroll', () => {\n document.querySelectorAll(selectors.OVERLAY).forEach(\n (overlay) => {\n rePositionPreviewInfoElement(overlay);\n }\n );\n }, true);\n};\n"],"names":["prefetchTemplate","selectors","async","definition","parent","content","icon","data","css","classes","overlay","html","js","Templates","renderForPromise","appendNodeContents","querySelector","rePositionPreviewInfoElement","init","error","removeOverlay","parentNode","dataset","_overlay$dataset","overlayPosition","style","position","removeChild","document","querySelectorAll","forEach","Error","_overlay$parentNode","_overlay$parentNode$s","target","rect","getBoundingClientRect","sectionHeight","parseInt","window","getComputedStyle","height","sectionOffset","top","previewHeight","padding","bottom","offSetTop","navBar","offsetHeight","addEventListener"],"mappings":";;;;;;;;;;sPA+BSA,iBADe,qCAWlBC,kBACO,iBADPA,qBAEU,yBAFVA,iBAGM,2CAacC,MAAMC,WAAYC,kCAEpCD,WAAWE,SAAyC,iBAAvBF,WAAWE,UACxCF,WAAWE,cAAgBF,WAAWE,SAEtCF,WAAWG,MAAmC,iBAApBH,WAAWG,OACrCH,WAAWG,WAAaH,WAAWG,YAEjCC,KAAO,CACTF,QAASF,WAAWE,QACpBG,gCAAKL,WAAWM,2DAAW,sBAE3BC,kBAEMC,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAzCnB,8BAyCqDP,yBAC3DQ,mBAAmBX,OAAQO,KAAMC,IAC3CF,QAAUN,OAAOY,cAAcf,mBAC/BgB,6BAA6BP,SAC7BQ,OACF,MAAOC,aACCA,aAEHT,eAQEU,cAAiBV,+BACrBA,SAAYA,QAAQW,sCAIrBX,QAAQY,qCAARC,iBAAiBC,wBACVd,QAAQW,WAAWI,MAAMC,SAEpChB,QAAQW,WAAWM,YAAYjB,2EAGF,KAC7BkB,SAASC,iBAAiB5B,mBAAmB6B,SACxCpB,UACGU,cAAcV,mBAUpBO,6BAA+B,SAASP,2DACrCA,cACK,IAAIqB,MAAM,0DAGfrB,QAAQW,yEAARW,oBAAoBP,wCAApBQ,sBAA2BP,WAC5BhB,QAAQW,WAAWI,MAAMC,SAAW,WACpChB,QAAQY,QAAQE,gBAAkB,cAGhCU,OAASxB,QAAQM,cAAcf,0BAChCiC,oBAICC,KAAOzB,QAAQ0B,wBACfC,cAAgBC,SAASC,OAAOC,iBAAiB9B,SAAS+B,OAAQ,IAClEC,cAAgBP,KAAKQ,IACrBC,cAAgBN,SAASC,OAAOC,iBAAiBN,QAAQO,OAAQ,IAClE,EAAIH,SAASC,OAAOC,iBAAiBN,QAAQW,QAAS,QAEvDF,IAAKG,UACLJ,cAAgB,KACZL,cAAgBK,eAAiBE,cAAe,KAE5CG,UAAY,EAAIL,oBACdM,OAASpB,SAASZ,cAAcf,kBAClC+C,SACAD,WAAwBC,OAAOC,cAEnCN,IAAMI,UAAY,KAClBD,OAAS,aAGTH,IAAM,QACNG,OAAS,OAGbH,IAAM,EACNG,OAAS,QAGbZ,OAAOT,MAAMkB,IAAMA,IACnBT,OAAOT,MAAMqB,OAASA,QAIpB5B,KAAO,KAKTU,SAASsB,iBAAiB,UAAU,KAChCtB,SAASC,iBAAiB5B,mBAAmB6B,SACxCpB,UACGO,6BAA6BP,eAGtC"} \ No newline at end of file diff --git a/lib/amd/src/local/reactive/basecomponent.js b/lib/amd/src/local/reactive/basecomponent.js index f276df1c5be..84be5c99730 100644 --- a/lib/amd/src/local/reactive/basecomponent.js +++ b/lib/amd/src/local/reactive/basecomponent.js @@ -14,6 +14,7 @@ // along with Moodle. If not, see . import Templates from 'core/templates'; +import {addOverlay, removeOverlay, removeAllOverlays} from 'core/local/reactive/overlay'; /** * Reactive UI component base class. @@ -488,4 +489,43 @@ export default class { getElementLocked(target) { return target.dataset.locked ?? false; } + + /** + * Adds an overlay to a specific page element. + * + * @param {Object} definition the overlay definition. + * @param {String} definition.content an optional overlay content. + * @param {String} definition.classes an optional CSS classes + * @param {Element} target optional parent object (this.element will be used if none provided) + */ + async addOverlay(definition, target) { + if (this._overlay) { + this.removeOverlay(); + } + this._overlay = await addOverlay( + { + content: definition.content, + css: definition.classes ?? 'file-drop-zone', + }, + target ?? this.element + ); + } + + /** + * Remove the current overlay. + */ + removeOverlay() { + if (!this._overlay) { + return; + } + removeOverlay(this._overlay); + this._overlay = null; + } + + /** + * Remove all page overlais. + */ + removeAllOverlays() { + removeAllOverlays(); + } } diff --git a/lib/amd/src/local/reactive/dragdrop.js b/lib/amd/src/local/reactive/dragdrop.js index 1e433242e66..44c8ef5b0d4 100644 --- a/lib/amd/src/local/reactive/dragdrop.js +++ b/lib/amd/src/local/reactive/dragdrop.js @@ -147,6 +147,8 @@ export default class extends BaseComponent { // Stores if the droparea is shown or not. this.dropzonevisible = false; + // Stores if the mouse is over the element or not. + this.ismouseover = false; } /** @@ -158,6 +160,15 @@ export default class extends BaseComponent { return this.classes; } + /** + * Return the current drop-zone visible of the element. + * + * @returns {boolean} if the dropzone should be visible or not + */ + isDropzoneVisible() { + return this.dropzonevisible; + } + /** * Initial state ready method. * @@ -174,6 +185,8 @@ export default class extends BaseComponent { this.addEventListener(this.element, 'dragleave', this._dragLeave); this.addEventListener(this.element, 'dragover', this._dragOver); this.addEventListener(this.element, 'drop', this._drop); + this.addEventListener(this.element, 'mouseover', this._mouseOver); + this.addEventListener(this.element, 'mouseleave', this._mouseLeave); } // Configure the elements draggable if the parent component has dragable data. @@ -203,6 +216,20 @@ export default class extends BaseComponent { } } + /** + * Mouse over handle. + */ + _mouseOver() { + this.ismouseover = true; + } + + /** + * Mouse leave handler. + */ + _mouseLeave() { + this.ismouseover = false; + } + /** * Drag start event handler. * @@ -278,6 +305,7 @@ export default class extends BaseComponent { document.body.classList.remove(this.classes.BODYDRAGGING); this.element.classList.remove(this.classes.DRAGGING); this.fullregion?.classList.remove(this.classes.DRAGGING); + this.removeAllOverlays(); // We add the total movement to the event in case the component // wants to move its absolute position. @@ -339,7 +367,7 @@ export default class extends BaseComponent { const dropdata = this._processEvent(event); if (dropdata) { this.entercount--; - if (this.entercount == 0 && this.dropzonevisible) { + if (this.entercount <= 0 && this.dropzonevisible) { this.dropzonevisible = false; this.element.classList.remove(this.classes.DRAGOVER); this._callParentMethod('hideDropZone', dropdata, event); @@ -363,6 +391,7 @@ export default class extends BaseComponent { this._callParentMethod('hideDropZone', dropdata, event); } this.element.classList.remove(this.classes.DRAGOVER); + this.removeAllOverlays(); this._callParentMethod('drop', dropdata, event); // An accepted drop resets the initial position. // Save the starting point. @@ -443,7 +472,12 @@ export default class extends BaseComponent { * @returns {Object|undefined} with the dragged data (or undefined if none) */ _getDropData(event) { - if (this._containsOnlyFiles(event)) { + this._isOnlyFilesDragging = this._containsOnlyFiles(event); + if (this._isOnlyFilesDragging) { + // Check if the reactive instance can provide a files draggable data. + if (this.reactive.getFilesDraggableData !== undefined && typeof this.reactive.getFilesDraggableData === 'function') { + return this.reactive.getFilesDraggableData(event.dataTransfer); + } return undefined; } return activeDropData.get(this.reactive); @@ -455,15 +489,21 @@ export default class extends BaseComponent { * Files dragging does not generate drop data because they came from outside the page and the component * must check it before validating the event. * + * Some browsers like Firefox add extra types to file dragging. To discard the false positives + * a double check is necessary. + * * @param {Event} event the original event. * @returns {boolean} if the drag dataTransfers contains files. */ _containsOnlyFiles(event) { - if (event.dataTransfer.types && event.dataTransfer.types.length > 0) { - // Chrome drag page images as files. To differentiate a real file from a page - // image we need to check if all the dataTransfers types are files. - return event.dataTransfer.types.every(type => type === 'Files'); + if (!event.dataTransfer.types.includes('Files')) { + return false; } - return false; + return event.dataTransfer.types.every((type) => { + return (type.toLowerCase() != 'text/uri-list' + && type.toLowerCase() != 'text/html' + && type.toLowerCase() != 'text/plain' + ); + }); } } diff --git a/lib/amd/src/local/reactive/overlay.js b/lib/amd/src/local/reactive/overlay.js new file mode 100644 index 00000000000..2c189e637f4 --- /dev/null +++ b/lib/amd/src/local/reactive/overlay.js @@ -0,0 +1,171 @@ +// 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 . + +/** + * Element overlay methods. + * + * This module is used to create overlay information on components. For example + * to generate or destroy file drop-zones. + * + * @module core/local/reactive/overlay + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Templates from 'core/templates'; +import Prefetch from 'core/prefetch'; + +// Prefetch the overlay html. +const overlayTemplate = 'core/local/reactive/overlay'; +Prefetch.prefetchTemplate(overlayTemplate); + +/** + * @var {boolean} isInitialized if the module is capturing the proper page events. + */ +let isInitialized = false; + +/** + * @var {Object} isInitialized if the module is capturing the proper page events. + */ +const selectors = { + OVERLAY: "[data-overlay]", + REPOSITION: "[data-overlay-dynamic]", + NAVBAR: "nav.navbar.fixed-top", +}; + +/** + * Adds an overlay to a specific page element. + * + * @param {Object} definition the overlay definition. + * @param {String|Promise} definition.content an optional overlay content. + * @param {String|Promise} definition.icon an optional icon content. + * @param {String} definition.classes an optional CSS classes + * @param {HTMLElement} parent the parent object + * @return {HTMLElement|undefined} the new page element. + */ +export const addOverlay = async(definition, parent) => { + // Validate non of the passed params is a promise. + if (definition.content && typeof definition.content !== 'string') { + definition.content = await definition.content; + } + if (definition.icon && typeof definition.icon !== 'string') { + definition.icon = await definition.icon; + } + const data = { + content: definition.content, + css: definition.classes ?? 'file-drop-zone', + }; + let overlay; + try { + const {html, js} = await Templates.renderForPromise(overlayTemplate, data); + Templates.appendNodeContents(parent, html, js); + overlay = parent.querySelector(selectors.OVERLAY); + rePositionPreviewInfoElement(overlay); + init(); + } catch (error) { + throw error; + } + return overlay; +}; + +/** + * Adds an overlay to a specific page element. + * + * @param {HTMLElement} overlay the parent object + */ +export const removeOverlay = (overlay) => { + if (!overlay || !overlay.parentNode) { + return; + } + // Remove any forced parentNode position. + if (overlay.dataset?.overlayPosition) { + delete overlay.parentNode.style.position; + } + overlay.parentNode.removeChild(overlay); +}; + +export const removeAllOverlays = () => { + document.querySelectorAll(selectors.OVERLAY).forEach( + (overlay) => { + removeOverlay(overlay); + } + ); +}; + +/** + * Re-position the preview information element by calculating the section position. + * + * @param {Object} overlay the overlay element. + */ +const rePositionPreviewInfoElement = function(overlay) { + if (!overlay) { + throw new Error('Inexistent overlay element'); + } + // Add relative position to the parent object. + if (!overlay.parentNode?.style?.position) { + overlay.parentNode.style.position = 'relative'; + overlay.dataset.overlayPosition = "true"; + } + // Get the element to reposition. + const target = overlay.querySelector(selectors.REPOSITION); + if (!target) { + return; + } + // Get the new bounds. + const rect = overlay.getBoundingClientRect(); + const sectionHeight = parseInt(window.getComputedStyle(overlay).height, 10); + const sectionOffset = rect.top; + const previewHeight = parseInt(window.getComputedStyle(target).height, 10) + + (2 * parseInt(window.getComputedStyle(target).padding, 10)); + // Calculate the new target position. + let top, bottom; + if (sectionOffset < 0) { + if (sectionHeight + sectionOffset >= previewHeight) { + // We have enough space here, just stick the preview to the top. + let offSetTop = 0 - sectionOffset; + const navBar = document.querySelector(selectors.NAVBAR); + if (navBar) { + offSetTop = offSetTop + navBar.offsetHeight; + } + top = offSetTop + 'px'; + bottom = 'unset'; + } else { + // We do not have enough space here, just stick the preview to the bottom. + top = 'unset'; + bottom = 0; + } + } else { + top = 0; + bottom = 'unset'; + } + + target.style.top = top; + target.style.bottom = bottom; +}; + +// Update overlays when the page scrolls. +const init = () => { + if (isInitialized) { + return; + } + // Add scroll events. + document.addEventListener('scroll', () => { + document.querySelectorAll(selectors.OVERLAY).forEach( + (overlay) => { + rePositionPreviewInfoElement(overlay); + } + ); + }, true); +}; diff --git a/lib/templates/local/reactive/overlay.mustache b/lib/templates/local/reactive/overlay.mustache new file mode 100644 index 00000000000..6cba2f3ff86 --- /dev/null +++ b/lib/templates/local/reactive/overlay.mustache @@ -0,0 +1,36 @@ +{{! + This file is part of Moodle - http://moodle.org/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core/local/reactive/overlay + Template to render the global reactive debug panel. + Classes required for JS: + * none + Data attributes required for JS: + * none + Example context (json): + { + "content": "Drop here!", + "icon": "" + } +}} +
+ {{#content}} +
+
+ {{{icon}}} + {{content}} +
+
+ {{/content}} +
diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index 69311602aa6..2d44a2d1b0b 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -2909,6 +2909,41 @@ body.dragging { // Generic classes reactive components can use. +.overlay-preview { + background-color: rgba($white, .8); + border: 2px dashed $primary; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + .overlay-preview-wrapper { + position: absolute; + top: 0; + padding: 2rem; + width: 100%; + } + + .overlay-preview-content { + position: relative; + top: 0; + padding: $modal-inner-padding; + margin: 0 auto; + width: 100%; + max-width: 600px; + background-color: $primary; + color: $white; + text-align: center; + font-size: $font-size-lg; + @include border-radius(); + } +} + +.overlay-preview-borders { + outline: 2px dashed $primary; +} + .waitstate { display: none; } diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index b52c4da99e5..fe5318785a0 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -12228,6 +12228,35 @@ body.dragging .dragging { visibility: visible; cursor: move; } +.overlay-preview { + background-color: rgba(255, 255, 255, 0.8); + border: 2px dashed #0f6cbf; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } + .overlay-preview .overlay-preview-wrapper { + position: absolute; + top: 0; + padding: 2rem; + width: 100%; } + .overlay-preview .overlay-preview-content { + position: relative; + top: 0; + padding: 1rem; + margin: 0 auto; + width: 100%; + max-width: 600px; + background-color: #0f6cbf; + color: #fff; + text-align: center; + font-size: 1.171875rem; + border-radius: 0.5rem; } + +.overlay-preview-borders { + outline: 2px dashed #0f6cbf; } + .waitstate { display: none; } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 76bf20a459d..e960a90c86f 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -12228,6 +12228,35 @@ body.dragging .dragging { visibility: visible; cursor: move; } +.overlay-preview { + background-color: rgba(255, 255, 255, 0.8); + border: 2px dashed #0f6cbf; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } + .overlay-preview .overlay-preview-wrapper { + position: absolute; + top: 0; + padding: 2rem; + width: 100%; } + .overlay-preview .overlay-preview-content { + position: relative; + top: 0; + padding: 1rem; + margin: 0 auto; + width: 100%; + max-width: 600px; + background-color: #0f6cbf; + color: #fff; + text-align: center; + font-size: 1.171875rem; + border-radius: 0.25rem; } + +.overlay-preview-borders { + outline: 2px dashed #0f6cbf; } + .waitstate { display: none; } From a3f116367e229519bd67c50ddae4e0240ac6f4d4 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Thu, 1 Dec 2022 15:44:09 +0100 Subject: [PATCH 4/5] MDL-76432 core_courseformat: add file_handlers webservice In 4.0- version each time the course page is loaded the file handlers are calculate din the backend and injected directly into JS using a json encapsulation. With this new webservice the handlers can be obtained directly from the frontend when needed. --- .../format/classes/external/file_handlers.php | 95 +++++++++++++++++++ .../tests/external/file_handlers_test.php | 84 ++++++++++++++++ lib/db/services.php | 6 ++ version.php | 2 +- 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 course/format/classes/external/file_handlers.php create mode 100644 course/format/tests/external/file_handlers_test.php diff --git a/course/format/classes/external/file_handlers.php b/course/format/classes/external/file_handlers.php new file mode 100644 index 00000000000..2d8721ff271 --- /dev/null +++ b/course/format/classes/external/file_handlers.php @@ -0,0 +1,95 @@ +. + +namespace core_courseformat\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/course/dnduploadlib.php'); + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_multiple_structure; +use core_external\external_single_structure; +use core_external\external_value; +use dndupload_handler; + +/** + * Class for exporting a course file handlers. + * + * @package core_courseformat + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.2 + */ +class file_handlers extends external_api { + + /** + * Webservice parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'courseid' => new external_value(PARAM_INT, 'course id', VALUE_REQUIRED), + ] + ); + } + + /** + * Return the list of available file handlers. + * + * @param int $courseid the course id + * @return array of file hanlders. + */ + public static function execute(int $courseid): array { + global $CFG; + + require_once($CFG->dirroot . '/course/lib.php'); + + $params = external_api::validate_parameters(self::execute_parameters(), [ + 'courseid' => $courseid, + ]); + $courseid = $params['courseid']; + + self::validate_context(\context_course::instance($courseid)); + + $format = course_get_format($courseid); + $course = $format->get_course(); + + $handler = new dndupload_handler($course, null); + + $data = $handler->get_js_data(); + return $data->filehandlers ?? []; + } + + /** + * Webservice returns. + * + * @return external_multiple_structure + */ + public static function execute_returns(): external_multiple_structure { + return new external_multiple_structure( + new external_single_structure([ + 'extension' => new external_value(PARAM_TEXT, 'File extension'), + 'module' => new external_value(PARAM_TEXT, 'Target module'), + 'message' => new external_value(PARAM_TEXT, 'Output message'), + ]) + ); + } +} diff --git a/course/format/tests/external/file_handlers_test.php b/course/format/tests/external/file_handlers_test.php new file mode 100644 index 00000000000..ef639e3c95d --- /dev/null +++ b/course/format/tests/external/file_handlers_test.php @@ -0,0 +1,84 @@ +. + +namespace core_courseformat\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +use external_api; +use dndupload_handler; + +/** + * Tests for the file_hanlders class. + * + * @package core_course + * @category test + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core_courseformat\external\file_handlers + */ +class file_handlers_test extends \externallib_advanced_testcase { + + /** + * Setup to ensure that fixtures are loaded. + */ + public static function setupBeforeClass(): void { // phpcs:ignore + global $CFG; + require_once($CFG->dirroot . '/course/lib.php'); + require_once($CFG->dirroot . '/course/dnduploadlib.php'); + } + + /** + * Test the behaviour of get_state::execute(). + * + * @covers ::execute + */ + public function test_execute(): void { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(['numsections' => 3, 'format' => 'topics']); + $this->setAdminUser(); + + $result = file_handlers::execute($course->id); + $result = external_api::clean_returnvalue(file_handlers::execute_returns(), $result); + + $handlers = new dndupload_handler($course, null); + $expected = $handlers->get_js_data(); + + $this->assertCount(count($expected->filehandlers), $result); + foreach ($expected->filehandlers as $key => $handler) { + $tocompare = $result[$key]; + $this->assertEquals($handler->extension, $tocompare['extension']); + } + } + + /** + * Test the behaviour of get_state::execute() in a wrong course. + * + * @covers ::execute + */ + public function test_execute_wrong_course(): void { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(['numsections' => 3, 'format' => 'topics']); + $this->setAdminUser(); + + $this->expectException('dml_missing_record_exception'); + $result = file_handlers::execute(-1); + $result = external_api::clean_returnvalue(file_handlers::execute_returns(), $result); + } +} diff --git a/lib/db/services.php b/lib/db/services.php index 7bb55cae44e..106ea4321ff 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -515,6 +515,12 @@ $functions = array( 'type' => 'read', 'ajax' => true, ), + 'core_courseformat_file_handlers' => [ + 'classname' => 'core_courseformat\external\file_handlers', + 'description' => 'Get the current course file hanlders.', + 'type' => 'read', + 'ajax' => true, + ], 'core_courseformat_get_state' => [ 'classname' => 'core_courseformat\external\get_state', 'description' => 'Get the current course state.', diff --git a/version.php b/version.php index f15eb913deb..86e57bfed0f 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2023020300.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2023020300.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.2dev (Build: 20230203)'; // Human-friendly version name From 1113f8328e2d53feff79021f730fbae0111673f8 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Thu, 1 Dec 2022 15:44:38 +0100 Subject: [PATCH 5/5] MDL-76432 core_courseformat: migrate file drag and drop Replace the old course/dragdrop.js file (which was not even minimised) to AMD modules and integrate them to the new reactive course editor. From now on, a file can be drop over any course section, no matter if it is in the course content or in the course index. It will also start using the new process monitor to show the uploading state to the user. --- .../local/courseeditor/courseeditor.min.js | 4 +- .../courseeditor/courseeditor.min.js.map | 2 +- .../local/courseeditor/dndsection.min.js | 5 +- .../local/courseeditor/dndsection.min.js.map | 2 +- .../build/local/courseeditor/exporter.min.js | 2 +- .../local/courseeditor/exporter.min.js.map | 2 +- .../local/courseeditor/fileuploader.min.js | 3 + .../courseeditor/fileuploader.min.js.map | 1 + .../build/local/courseindex/section.min.js | 2 +- .../local/courseindex/section.min.js.map | 2 +- .../src/local/courseeditor/courseeditor.js | 77 +++ .../amd/src/local/courseeditor/dndsection.js | 34 ++ .../amd/src/local/courseeditor/exporter.js | 25 +- .../src/local/courseeditor/fileuploader.js | 558 ++++++++++++++++++ .../amd/src/local/courseindex/section.js | 19 + .../classes/output/local/state/course.php | 7 + course/format/templates/fileuploader.mustache | 54 ++ course/lib.php | 34 +- lang/en/error.php | 4 + theme/boost/scss/moodle/course.scss | 1 + 20 files changed, 809 insertions(+), 29 deletions(-) create mode 100644 course/format/amd/build/local/courseeditor/fileuploader.min.js create mode 100644 course/format/amd/build/local/courseeditor/fileuploader.min.js.map create mode 100644 course/format/amd/src/local/courseeditor/fileuploader.js create mode 100644 course/format/templates/fileuploader.mustache diff --git a/course/format/amd/build/local/courseeditor/courseeditor.min.js b/course/format/amd/build/local/courseeditor/courseeditor.min.js index f92da286093..375f0b21835 100644 --- a/course/format/amd/build/local/courseeditor/courseeditor.min.js +++ b/course/format/amd/build/local/courseeditor/courseeditor.min.js @@ -1,4 +1,4 @@ -define("core_courseformat/local/courseeditor/courseeditor",["exports","core/reactive","core/notification","core_courseformat/local/courseeditor/exporter","core/log","core/ajax","core/sessionstorage"],(function(_exports,_reactive,_notification,_exporter,_log,_ajax,Storage){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 _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj} +define("core_courseformat/local/courseeditor/courseeditor",["exports","core/reactive","core/notification","core_courseformat/local/courseeditor/exporter","core/log","core/ajax","core/sessionstorage","core_courseformat/local/courseeditor/fileuploader"],(function(_exports,_reactive,_notification,_exporter,_log,_ajax,Storage,_fileuploader){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 _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj} /** * Main course editor module. * @@ -9,6 +9,6 @@ define("core_courseformat/local/courseeditor/courseeditor",["exports","core/reac * @class core_courseformat/local/courseeditor/courseeditor * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=_interopRequireDefault(_notification),_exporter=_interopRequireDefault(_exporter),_log=_interopRequireDefault(_log),_ajax=_interopRequireDefault(_ajax),Storage=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);class _default extends _reactive.Reactive{constructor(){super(...arguments),_defineProperty(this,"stateKey",1),_defineProperty(this,"sectionReturn",0)}async loadCourse(courseId,serverStateKey){if(this.courseId)throw new Error("Cannot load ".concat(courseId,", course already loaded with id ").concat(this.courseId));let stateData;serverStateKey||(serverStateKey="invalidStateKey_".concat(Date.now())),this._editing=!1,this._supportscomponents=!1,this.courseId=courseId;const storeStateKey=Storage.get("course/".concat(courseId,"/stateKey"));try{this.isEditing||serverStateKey!=storeStateKey||(stateData=JSON.parse(Storage.get("course/".concat(courseId,"/staticState")))),stateData||(stateData=await this.getServerCourseState())}catch(error){return _log.default.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR"),void _log.default.error(error)}if(this.setInitialState(stateData),this.isEditing)this.stateKey=null;else{const newState=JSON.stringify(stateData);var _stateData$course$sta,_stateData,_stateData$course;if(Storage.get("course/".concat(courseId,"/staticState"))!==newState||storeStateKey!==serverStateKey)Storage.set("course/".concat(courseId,"/staticState"),newState),Storage.set("course/".concat(courseId,"/stateKey"),null!==(_stateData$course$sta=null===(_stateData=stateData)||void 0===_stateData||null===(_stateData$course=_stateData.course)||void 0===_stateData$course?void 0:_stateData$course.statekey)&&void 0!==_stateData$course$sta?_stateData$course$sta:serverStateKey);this.stateKey=Storage.get("course/".concat(courseId,"/stateKey"))}}setViewFormat(setup){var _setup$editing,_setup$supportscompon;this._editing=null!==(_setup$editing=setup.editing)&&void 0!==_setup$editing&&_setup$editing,this._supportscomponents=null!==(_setup$supportscompon=setup.supportscomponents)&&void 0!==_setup$supportscompon&&_setup$supportscompon}async getServerCourseState(){const courseState=await _ajax.default.call([{methodname:"core_courseformat_get_state",args:{courseid:this.courseId}}])[0];return{course:{},section:[],cm:[],...JSON.parse(courseState)}}get isEditing(){var _this$_editing;return null!==(_this$_editing=this._editing)&&void 0!==_this$_editing&&_this$_editing}getExporter(){return new _exporter.default(this)}get supportComponents(){var _this$_supportscompon;return null!==(_this$_supportscompon=this._supportscomponents)&&void 0!==_this$_supportscompon&&_this$_supportscompon}getStorageValue(key){if(this.isEditing||!this.stateKey)return!1;const dataJson=Storage.get("course/".concat(this.courseId,"/").concat(key));if(!dataJson)return!1;try{const data=JSON.parse(dataJson);return(null==data?void 0:data.stateKey)===this.stateKey&&data.value}catch(error){return!1}}setStorageValue(key,value){if(this.isEditing)return!1;const data={stateKey:this.stateKey,value:value};return Storage.set("course/".concat(this.courseId,"/").concat(key),JSON.stringify(data))}async dispatch(){try{await super.dispatch(...arguments)}catch(error){_notification.default.exception(error),super.dispatch("unlockAll")}}}return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=_interopRequireDefault(_notification),_exporter=_interopRequireDefault(_exporter),_log=_interopRequireDefault(_log),_ajax=_interopRequireDefault(_ajax),Storage=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);class _default extends _reactive.Reactive{constructor(){super(...arguments),_defineProperty(this,"stateKey",1),_defineProperty(this,"sectionReturn",0)}async loadCourse(courseId,serverStateKey){if(this.courseId)throw new Error("Cannot load ".concat(courseId,", course already loaded with id ").concat(this.courseId));let stateData;serverStateKey||(serverStateKey="invalidStateKey_".concat(Date.now())),this._editing=!1,this._supportscomponents=!1,this._fileHandlers=null,this.courseId=courseId;const storeStateKey=Storage.get("course/".concat(courseId,"/stateKey"));try{this.isEditing||serverStateKey!=storeStateKey||(stateData=JSON.parse(Storage.get("course/".concat(courseId,"/staticState")))),stateData||(stateData=await this.getServerCourseState())}catch(error){return _log.default.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR"),void _log.default.error(error)}if(this.setInitialState(stateData),this.isEditing)this.stateKey=null;else{const newState=JSON.stringify(stateData);var _stateData$course$sta,_stateData,_stateData$course;if(Storage.get("course/".concat(courseId,"/staticState"))!==newState||storeStateKey!==serverStateKey)Storage.set("course/".concat(courseId,"/staticState"),newState),Storage.set("course/".concat(courseId,"/stateKey"),null!==(_stateData$course$sta=null===(_stateData=stateData)||void 0===_stateData||null===(_stateData$course=_stateData.course)||void 0===_stateData$course?void 0:_stateData$course.statekey)&&void 0!==_stateData$course$sta?_stateData$course$sta:serverStateKey);this.stateKey=Storage.get("course/".concat(courseId,"/stateKey"))}this._loadFileHandlers()}_loadFileHandlers(){this._fileHandlersPromise=new Promise((resolve=>{if(!this.isEditing)return void resolve([]);const handlersCacheKey="course/".concat(this.courseId,"/fileHandlers"),cacheValue=Storage.get(handlersCacheKey);if(cacheValue)try{const cachedHandlers=JSON.parse(cacheValue);return void resolve(cachedHandlers)}catch(error){_log.default.error("ERROR PARSING CACHED FILE HANDLERS")}_ajax.default.call([{methodname:"core_courseformat_file_handlers",args:{courseid:this.courseId}}])[0].then((handlers=>{Storage.set(handlersCacheKey,JSON.stringify(handlers)),resolve(handlers)})).catch((error=>{_log.default.error(error),resolve([])}))}))}setViewFormat(setup){var _setup$editing,_setup$supportscompon;this._editing=null!==(_setup$editing=setup.editing)&&void 0!==_setup$editing&&_setup$editing,this._supportscomponents=null!==(_setup$supportscompon=setup.supportscomponents)&&void 0!==_setup$supportscompon&&_setup$supportscompon}async getServerCourseState(){const courseState=await _ajax.default.call([{methodname:"core_courseformat_get_state",args:{courseid:this.courseId}}])[0];return{course:{},section:[],cm:[],...JSON.parse(courseState)}}get isEditing(){var _this$_editing;return null!==(_this$_editing=this._editing)&&void 0!==_this$_editing&&_this$_editing}getExporter(){return new _exporter.default(this)}get supportComponents(){var _this$_supportscompon;return null!==(_this$_supportscompon=this._supportscomponents)&&void 0!==_this$_supportscompon&&_this$_supportscompon}async getFileHandlersPromise(){var _this$_fileHandlersPr;return null!==(_this$_fileHandlersPr=this._fileHandlersPromise)&&void 0!==_this$_fileHandlersPr?_this$_fileHandlersPr:[]}uploadFiles(sectionId,sectionNum,files){return(0,_fileuploader.uploadFilesToCourse)(this.courseId,sectionId,sectionNum,files)}getStorageValue(key){if(this.isEditing||!this.stateKey)return!1;const dataJson=Storage.get("course/".concat(this.courseId,"/").concat(key));if(!dataJson)return!1;try{const data=JSON.parse(dataJson);return(null==data?void 0:data.stateKey)===this.stateKey&&data.value}catch(error){return!1}}setStorageValue(key,value){if(this.isEditing)return!1;const data={stateKey:this.stateKey,value:value};return Storage.set("course/".concat(this.courseId,"/").concat(key),JSON.stringify(data))}getFilesDraggableData(dataTransfer){return this.getExporter().fileDraggableData(this.state,dataTransfer)}async dispatch(){try{await super.dispatch(...arguments)}catch(error){_notification.default.exception(error),super.dispatch("unlockAll")}}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=courseeditor.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/courseeditor.min.js.map b/course/format/amd/build/local/courseeditor/courseeditor.min.js.map index 6e44c926d69..51f2b847872 100644 --- a/course/format/amd/build/local/courseeditor/courseeditor.min.js.map +++ b/course/format/amd/build/local/courseeditor/courseeditor.min.js.map @@ -1 +1 @@ -{"version":3,"file":"courseeditor.min.js","sources":["../../../src/local/courseeditor/courseeditor.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 {Reactive} from 'core/reactive';\nimport notification from 'core/notification';\nimport Exporter from 'core_courseformat/local/courseeditor/exporter';\nimport log from 'core/log';\nimport ajax from 'core/ajax';\nimport * as Storage from 'core/sessionstorage';\n\n/**\n * Main course editor module.\n *\n * All formats can register new components on this object to create new reactive\n * UI components that watch the current course state.\n *\n * @module core_courseformat/local/courseeditor/courseeditor\n * @class core_courseformat/local/courseeditor/courseeditor\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class extends Reactive {\n\n /**\n * The current state cache key\n *\n * The state cache is considered dirty if the state changes from the last page or\n * if the page has editing mode on.\n *\n * @attribute stateKey\n * @type number|null\n * @default 1\n * @package\n */\n stateKey = 1;\n\n /**\n * The current page section return\n * @attribute sectionReturn\n * @type number\n * @default 0\n */\n sectionReturn = 0;\n\n /**\n * Set up the course editor when the page is ready.\n *\n * The course can only be loaded once per instance. Otherwise an error is thrown.\n *\n * The backend can inform the module of the current state key. This key changes every time some\n * update in the course affect the current user state. Some examples are:\n * - The course content has been edited\n * - The user marks some activity as completed\n * - The user collapses or uncollapses a section (it is stored as a user preference)\n *\n * @param {number} courseId course id\n * @param {string} serverStateKey the current backend course cache reference\n */\n async loadCourse(courseId, serverStateKey) {\n\n if (this.courseId) {\n throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);\n }\n\n if (!serverStateKey) {\n // The server state key is not provided, we use a invalid statekey to force reloading.\n serverStateKey = `invalidStateKey_${Date.now()}`;\n }\n\n // Default view format setup.\n this._editing = false;\n this._supportscomponents = false;\n\n this.courseId = courseId;\n\n let stateData;\n\n const storeStateKey = Storage.get(`course/${courseId}/stateKey`);\n try {\n // Check if the backend state key is the same we have in our session storage.\n if (!this.isEditing && serverStateKey == storeStateKey) {\n stateData = JSON.parse(Storage.get(`course/${courseId}/staticState`));\n }\n if (!stateData) {\n stateData = await this.getServerCourseState();\n }\n\n } catch (error) {\n log.error(\"EXCEPTION RAISED WHILE INIT COURSE EDITOR\");\n log.error(error);\n return;\n }\n\n this.setInitialState(stateData);\n\n // In editing mode, the session cache is considered dirty always.\n if (this.isEditing) {\n this.stateKey = null;\n } else {\n // Check if the last state is the same as the cached one.\n const newState = JSON.stringify(stateData);\n const previousState = Storage.get(`course/${courseId}/staticState`);\n if (previousState !== newState || storeStateKey !== serverStateKey) {\n Storage.set(`course/${courseId}/staticState`, newState);\n Storage.set(`course/${courseId}/stateKey`, stateData?.course?.statekey ?? serverStateKey);\n }\n this.stateKey = Storage.get(`course/${courseId}/stateKey`);\n }\n }\n\n /**\n * Setup the current view settings\n *\n * @param {Object} setup format, page and course settings\n * @param {boolean} setup.editing if the page is in edit mode\n * @param {boolean} setup.supportscomponents if the format supports components for content\n * @param {string} setup.cacherev the backend cached state revision\n */\n setViewFormat(setup) {\n this._editing = setup.editing ?? false;\n this._supportscomponents = setup.supportscomponents ?? false;\n }\n\n /**\n * Load the current course state from the server.\n *\n * @returns {Object} the current course state\n */\n async getServerCourseState() {\n const courseState = await ajax.call([{\n methodname: 'core_courseformat_get_state',\n args: {\n courseid: this.courseId,\n }\n }])[0];\n\n const stateData = JSON.parse(courseState);\n\n return {\n course: {},\n section: [],\n cm: [],\n ...stateData,\n };\n }\n\n /**\n * Return the current edit mode.\n *\n * Components should use this method to check if edit mode is active.\n *\n * @return {boolean} if edit is enabled\n */\n get isEditing() {\n return this._editing ?? false;\n }\n\n /**\n * Return a data exporter to transform state part into mustache contexts.\n *\n * @return {Exporter} the exporter class\n */\n getExporter() {\n return new Exporter(this);\n }\n\n /**\n * Return if the current course support components to refresh the content.\n *\n * @returns {boolean} if the current content support components\n */\n get supportComponents() {\n return this._supportscomponents ?? false;\n }\n\n /**\n * Get a value from the course editor static storage if any.\n *\n * The course editor static storage uses the sessionStorage to store values from the\n * components. This is used to prevent unnecesary template loadings on every page. However,\n * the storage does not work if no sessionStorage can be used (in debug mode for example),\n * if the page is in editing mode or if the initial state change from the last page.\n *\n * @param {string} key the key to get\n * @return {boolean|string} the storage value or false if cannot be loaded\n */\n getStorageValue(key) {\n if (this.isEditing || !this.stateKey) {\n return false;\n }\n const dataJson = Storage.get(`course/${this.courseId}/${key}`);\n if (!dataJson) {\n return false;\n }\n // Check the stateKey.\n try {\n const data = JSON.parse(dataJson);\n if (data?.stateKey !== this.stateKey) {\n return false;\n }\n return data.value;\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Stores a value into the course editor static storage if available\n *\n * @param {String} key the key to store\n * @param {*} value the value to store (must be compatible with JSON,stringify)\n * @returns {boolean} true if the value is stored\n */\n setStorageValue(key, value) {\n // Values cannot be stored on edit mode.\n if (this.isEditing) {\n return false;\n }\n const data = {\n stateKey: this.stateKey,\n value,\n };\n return Storage.set(`course/${this.courseId}/${key}`, JSON.stringify(data));\n }\n\n /**\n * Dispatch a change in the state.\n *\n * Usually reactive modules throw an error directly to the components when something\n * goes wrong. However, course editor can directly display a notification.\n *\n * @method dispatch\n * @param {mixed} args any number of params the mutation needs.\n */\n async dispatch(...args) {\n try {\n await super.dispatch(...args);\n } catch (error) {\n // Display error modal.\n notification.exception(error);\n // Force unlock all elements.\n super.dispatch('unlockAll');\n }\n }\n}\n"],"names":["Reactive","courseId","serverStateKey","this","Error","stateData","Date","now","_editing","_supportscomponents","storeStateKey","Storage","get","isEditing","JSON","parse","getServerCourseState","error","setInitialState","stateKey","newState","stringify","set","_stateData","course","_stateData$course","statekey","setViewFormat","setup","editing","supportscomponents","courseState","ajax","call","methodname","args","courseid","section","cm","getExporter","Exporter","supportComponents","getStorageValue","key","dataJson","data","value","setStorageValue","super","dispatch","exception"],"mappings":";;;;;;;;;;;g7BAiC6BA,qFAad,wCAQK,oBAgBCC,SAAUC,mBAEnBC,KAAKF,eACC,IAAIG,4BAAqBH,oDAA2CE,KAAKF,eAc/EI,UAXCH,iBAEDA,yCAAoCI,KAAKC,aAIxCC,UAAW,OACXC,qBAAsB,OAEtBR,SAAWA,eAIVS,cAAgBC,QAAQC,qBAAcX,2BAGnCE,KAAKU,WAAaX,gBAAkBQ,gBACrCL,UAAYS,KAAKC,MAAMJ,QAAQC,qBAAcX,4BAE5CI,YACDA,gBAAkBF,KAAKa,wBAG7B,MAAOC,2BACDA,MAAM,+DACNA,MAAMA,eAITC,gBAAgBb,WAGjBF,KAAKU,eACAM,SAAW,SACb,OAEGC,SAAWN,KAAKO,UAAUhB,qEACVM,QAAQC,qBAAcX,4BACtBmB,UAAYV,gBAAkBR,eAChDS,QAAQW,qBAAcrB,yBAAwBmB,UAC9CT,QAAQW,qBAAcrB,uEAAqBI,2DAAAkB,WAAWC,2CAAXC,kBAAmBC,gEAAYxB,qBAEzEiB,SAAWR,QAAQC,qBAAcX,wBAY9C0B,cAAcC,qDACLpB,gCAAWoB,MAAMC,uDACjBpB,kDAAsBmB,MAAME,8GAS3BC,kBAAoBC,cAAKC,KAAK,CAAC,CACjCC,WAAY,8BACZC,KAAM,CACFC,SAAUjC,KAAKF,aAEnB,SAIG,CACHuB,OAAQ,GACRa,QAAS,GACTC,GAAI,MALUxB,KAAKC,MAAMgB,cAiB7BlB,iEACOV,KAAKK,mDAQhB+B,qBACW,IAAIC,kBAASrC,MAQpBsC,uFACOtC,KAAKM,4EAchBiC,gBAAgBC,QACRxC,KAAKU,YAAcV,KAAKgB,gBACjB,QAELyB,SAAWjC,QAAQC,qBAAcT,KAAKF,qBAAY0C,UACnDC,gBACM,YAIDC,KAAO/B,KAAKC,MAAM6B,iBACpBC,MAAAA,YAAAA,KAAM1B,YAAahB,KAAKgB,UAGrB0B,KAAKC,MACd,MAAO7B,cACE,GAWf8B,gBAAgBJ,IAAKG,UAEb3C,KAAKU,iBACE,QAELgC,KAAO,CACT1B,SAAUhB,KAAKgB,SACf2B,MAAAA,cAEGnC,QAAQW,qBAAcnB,KAAKF,qBAAY0C,KAAO7B,KAAKO,UAAUwB,kCAc1DG,MAAMC,uBACd,MAAOhC,6BAEQiC,UAAUjC,aAEjBgC,SAAS"} \ No newline at end of file +{"version":3,"file":"courseeditor.min.js","sources":["../../../src/local/courseeditor/courseeditor.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 {Reactive} from 'core/reactive';\nimport notification from 'core/notification';\nimport Exporter from 'core_courseformat/local/courseeditor/exporter';\nimport log from 'core/log';\nimport ajax from 'core/ajax';\nimport * as Storage from 'core/sessionstorage';\nimport {uploadFilesToCourse} from 'core_courseformat/local/courseeditor/fileuploader';\n\n/**\n * Main course editor module.\n *\n * All formats can register new components on this object to create new reactive\n * UI components that watch the current course state.\n *\n * @module core_courseformat/local/courseeditor/courseeditor\n * @class core_courseformat/local/courseeditor/courseeditor\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class extends Reactive {\n\n /**\n * The current state cache key\n *\n * The state cache is considered dirty if the state changes from the last page or\n * if the page has editing mode on.\n *\n * @attribute stateKey\n * @type number|null\n * @default 1\n * @package\n */\n stateKey = 1;\n\n /**\n * The current page section return\n * @attribute sectionReturn\n * @type number\n * @default 0\n */\n sectionReturn = 0;\n\n /**\n * Set up the course editor when the page is ready.\n *\n * The course can only be loaded once per instance. Otherwise an error is thrown.\n *\n * The backend can inform the module of the current state key. This key changes every time some\n * update in the course affect the current user state. Some examples are:\n * - The course content has been edited\n * - The user marks some activity as completed\n * - The user collapses or uncollapses a section (it is stored as a user preference)\n *\n * @param {number} courseId course id\n * @param {string} serverStateKey the current backend course cache reference\n */\n async loadCourse(courseId, serverStateKey) {\n\n if (this.courseId) {\n throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);\n }\n\n if (!serverStateKey) {\n // The server state key is not provided, we use a invalid statekey to force reloading.\n serverStateKey = `invalidStateKey_${Date.now()}`;\n }\n\n // Default view format setup.\n this._editing = false;\n this._supportscomponents = false;\n this._fileHandlers = null;\n\n this.courseId = courseId;\n\n let stateData;\n\n const storeStateKey = Storage.get(`course/${courseId}/stateKey`);\n try {\n // Check if the backend state key is the same we have in our session storage.\n if (!this.isEditing && serverStateKey == storeStateKey) {\n stateData = JSON.parse(Storage.get(`course/${courseId}/staticState`));\n }\n if (!stateData) {\n stateData = await this.getServerCourseState();\n }\n\n } catch (error) {\n log.error(\"EXCEPTION RAISED WHILE INIT COURSE EDITOR\");\n log.error(error);\n return;\n }\n\n this.setInitialState(stateData);\n\n // In editing mode, the session cache is considered dirty always.\n if (this.isEditing) {\n this.stateKey = null;\n } else {\n // Check if the last state is the same as the cached one.\n const newState = JSON.stringify(stateData);\n const previousState = Storage.get(`course/${courseId}/staticState`);\n if (previousState !== newState || storeStateKey !== serverStateKey) {\n Storage.set(`course/${courseId}/staticState`, newState);\n Storage.set(`course/${courseId}/stateKey`, stateData?.course?.statekey ?? serverStateKey);\n }\n this.stateKey = Storage.get(`course/${courseId}/stateKey`);\n }\n\n this._loadFileHandlers();\n }\n\n /**\n * Load the file hanlders promise.\n */\n _loadFileHandlers() {\n // Load the course file extensions.\n this._fileHandlersPromise = new Promise((resolve) => {\n if (!this.isEditing) {\n resolve([]);\n return;\n }\n // Check the cache.\n const handlersCacheKey = `course/${this.courseId}/fileHandlers`;\n\n const cacheValue = Storage.get(handlersCacheKey);\n if (cacheValue) {\n try {\n const cachedHandlers = JSON.parse(cacheValue);\n resolve(cachedHandlers);\n return;\n } catch (error) {\n log.error(\"ERROR PARSING CACHED FILE HANDLERS\");\n }\n }\n // Call file handlers webservice.\n ajax.call([{\n methodname: 'core_courseformat_file_handlers',\n args: {\n courseid: this.courseId,\n }\n }])[0].then((handlers) => {\n Storage.set(handlersCacheKey, JSON.stringify(handlers));\n resolve(handlers);\n return;\n }).catch(error => {\n log.error(error);\n resolve([]);\n return;\n });\n });\n }\n\n /**\n * Setup the current view settings\n *\n * @param {Object} setup format, page and course settings\n * @param {boolean} setup.editing if the page is in edit mode\n * @param {boolean} setup.supportscomponents if the format supports components for content\n * @param {string} setup.cacherev the backend cached state revision\n */\n setViewFormat(setup) {\n this._editing = setup.editing ?? false;\n this._supportscomponents = setup.supportscomponents ?? false;\n }\n\n /**\n * Load the current course state from the server.\n *\n * @returns {Object} the current course state\n */\n async getServerCourseState() {\n const courseState = await ajax.call([{\n methodname: 'core_courseformat_get_state',\n args: {\n courseid: this.courseId,\n }\n }])[0];\n\n const stateData = JSON.parse(courseState);\n\n return {\n course: {},\n section: [],\n cm: [],\n ...stateData,\n };\n }\n\n /**\n * Return the current edit mode.\n *\n * Components should use this method to check if edit mode is active.\n *\n * @return {boolean} if edit is enabled\n */\n get isEditing() {\n return this._editing ?? false;\n }\n\n /**\n * Return a data exporter to transform state part into mustache contexts.\n *\n * @return {Exporter} the exporter class\n */\n getExporter() {\n return new Exporter(this);\n }\n\n /**\n * Return if the current course support components to refresh the content.\n *\n * @returns {boolean} if the current content support components\n */\n get supportComponents() {\n return this._supportscomponents ?? false;\n }\n\n /**\n * Return the course file handlers promise.\n * @returns {Promise} the promise for file handlers.\n */\n async getFileHandlersPromise() {\n return this._fileHandlersPromise ?? [];\n }\n\n /**\n * Upload a file list to the course.\n *\n * This method is a wrapper to the course file uploader.\n *\n * @param {number} sectionId the section id\n * @param {number} sectionNum the section number\n * @param {Array} files and array of files\n * @return {Promise} the file queue promise\n */\n uploadFiles(sectionId, sectionNum, files) {\n return uploadFilesToCourse(this.courseId, sectionId, sectionNum, files);\n }\n\n /**\n * Get a value from the course editor static storage if any.\n *\n * The course editor static storage uses the sessionStorage to store values from the\n * components. This is used to prevent unnecesary template loadings on every page. However,\n * the storage does not work if no sessionStorage can be used (in debug mode for example),\n * if the page is in editing mode or if the initial state change from the last page.\n *\n * @param {string} key the key to get\n * @return {boolean|string} the storage value or false if cannot be loaded\n */\n getStorageValue(key) {\n if (this.isEditing || !this.stateKey) {\n return false;\n }\n const dataJson = Storage.get(`course/${this.courseId}/${key}`);\n if (!dataJson) {\n return false;\n }\n // Check the stateKey.\n try {\n const data = JSON.parse(dataJson);\n if (data?.stateKey !== this.stateKey) {\n return false;\n }\n return data.value;\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Stores a value into the course editor static storage if available\n *\n * @param {String} key the key to store\n * @param {*} value the value to store (must be compatible with JSON,stringify)\n * @returns {boolean} true if the value is stored\n */\n setStorageValue(key, value) {\n // Values cannot be stored on edit mode.\n if (this.isEditing) {\n return false;\n }\n const data = {\n stateKey: this.stateKey,\n value,\n };\n return Storage.set(`course/${this.courseId}/${key}`, JSON.stringify(data));\n }\n\n /**\n * Convert a file dragging event into a proper dragging file list.\n * @param {DataTransfer} dataTransfer the event to convert\n * @return {Array} of file list info.\n */\n getFilesDraggableData(dataTransfer) {\n const exporter = this.getExporter();\n return exporter.fileDraggableData(this.state, dataTransfer);\n }\n\n /**\n * Dispatch a change in the state.\n *\n * Usually reactive modules throw an error directly to the components when something\n * goes wrong. However, course editor can directly display a notification.\n *\n * @method dispatch\n * @param {mixed} args any number of params the mutation needs.\n */\n async dispatch(...args) {\n try {\n await super.dispatch(...args);\n } catch (error) {\n // Display error modal.\n notification.exception(error);\n // Force unlock all elements.\n super.dispatch('unlockAll');\n }\n }\n}\n"],"names":["Reactive","courseId","serverStateKey","this","Error","stateData","Date","now","_editing","_supportscomponents","_fileHandlers","storeStateKey","Storage","get","isEditing","JSON","parse","getServerCourseState","error","setInitialState","stateKey","newState","stringify","set","_stateData","course","_stateData$course","statekey","_loadFileHandlers","_fileHandlersPromise","Promise","resolve","handlersCacheKey","cacheValue","cachedHandlers","call","methodname","args","courseid","then","handlers","catch","setViewFormat","setup","editing","supportscomponents","courseState","ajax","section","cm","getExporter","Exporter","supportComponents","uploadFiles","sectionId","sectionNum","files","getStorageValue","key","dataJson","data","value","setStorageValue","getFilesDraggableData","dataTransfer","fileDraggableData","state","super","dispatch","exception"],"mappings":";;;;;;;;;;;g7BAkC6BA,qFAad,wCAQK,oBAgBCC,SAAUC,mBAEnBC,KAAKF,eACC,IAAIG,4BAAqBH,oDAA2CE,KAAKF,eAe/EI,UAZCH,iBAEDA,yCAAoCI,KAAKC,aAIxCC,UAAW,OACXC,qBAAsB,OACtBC,cAAgB,UAEhBT,SAAWA,eAIVU,cAAgBC,QAAQC,qBAAcZ,2BAGnCE,KAAKW,WAAaZ,gBAAkBS,gBACrCN,UAAYU,KAAKC,MAAMJ,QAAQC,qBAAcZ,4BAE5CI,YACDA,gBAAkBF,KAAKc,wBAG7B,MAAOC,2BACDA,MAAM,+DACNA,MAAMA,eAITC,gBAAgBd,WAGjBF,KAAKW,eACAM,SAAW,SACb,OAEGC,SAAWN,KAAKO,UAAUjB,qEACVO,QAAQC,qBAAcZ,4BACtBoB,UAAYV,gBAAkBT,eAChDU,QAAQW,qBAActB,yBAAwBoB,UAC9CT,QAAQW,qBAActB,uEAAqBI,2DAAAmB,WAAWC,2CAAXC,kBAAmBC,gEAAYzB,qBAEzEkB,SAAWR,QAAQC,qBAAcZ,4BAGrC2B,oBAMTA,yBAESC,qBAAuB,IAAIC,SAASC,cAChC5B,KAAKW,sBACNiB,QAAQ,UAINC,kCAA6B7B,KAAKF,0BAElCgC,WAAarB,QAAQC,IAAImB,qBAC3BC,qBAEUC,eAAiBnB,KAAKC,MAAMiB,wBAClCF,QAAQG,gBAEV,MAAOhB,oBACDA,MAAM,oDAIbiB,KAAK,CAAC,CACPC,WAAY,kCACZC,KAAM,CACFC,SAAUnC,KAAKF,aAEnB,GAAGsC,MAAMC,WACT5B,QAAQW,IAAIS,iBAAkBjB,KAAKO,UAAUkB,WAC7CT,QAAQS,aAETC,OAAMvB,qBACDA,MAAMA,OACVa,QAAQ,UAcpBW,cAAcC,qDACLnC,gCAAWmC,MAAMC,uDACjBnC,kDAAsBkC,MAAME,8GAS3BC,kBAAoBC,cAAKZ,KAAK,CAAC,CACjCC,WAAY,8BACZC,KAAM,CACFC,SAAUnC,KAAKF,aAEnB,SAIG,CACHwB,OAAQ,GACRuB,QAAS,GACTC,GAAI,MALUlC,KAAKC,MAAM8B,cAiB7BhC,iEACOX,KAAKK,mDAQhB0C,qBACW,IAAIC,kBAAShD,MAQpBiD,uFACOjD,KAAKM,0KAQLN,KAAK0B,4EAAwB,GAaxCwB,YAAYC,UAAWC,WAAYC,cACxB,qCAAoBrD,KAAKF,SAAUqD,UAAWC,WAAYC,OAcrEC,gBAAgBC,QACRvD,KAAKW,YAAcX,KAAKiB,gBACjB,QAELuC,SAAW/C,QAAQC,qBAAcV,KAAKF,qBAAYyD,UACnDC,gBACM,YAIDC,KAAO7C,KAAKC,MAAM2C,iBACpBC,MAAAA,YAAAA,KAAMxC,YAAajB,KAAKiB,UAGrBwC,KAAKC,MACd,MAAO3C,cACE,GAWf4C,gBAAgBJ,IAAKG,UAEb1D,KAAKW,iBACE,QAEL8C,KAAO,CACTxC,SAAUjB,KAAKiB,SACfyC,MAAAA,cAEGjD,QAAQW,qBAAcpB,KAAKF,qBAAYyD,KAAO3C,KAAKO,UAAUsC,OAQxEG,sBAAsBC,qBACD7D,KAAK+C,cACNe,kBAAkB9D,KAAK+D,MAAOF,yCAcpCG,MAAMC,uBACd,MAAOlD,6BAEQmD,UAAUnD,aAEjBkD,SAAS"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/dndsection.min.js b/course/format/amd/build/local/courseeditor/dndsection.min.js index a074ee75e74..ddc81925316 100644 --- a/course/format/amd/build/local/courseeditor/dndsection.min.js +++ b/course/format/amd/build/local/courseeditor/dndsection.min.js @@ -1,4 +1,4 @@ -define("core_courseformat/local/courseeditor/dndsection",["exports","core/reactive"],(function(_exports,_reactive){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; +define("core_courseformat/local/courseeditor/dndsection",["exports","core/reactive","core/str","core/prefetch","core/templates"],(function(_exports,_reactive,_str,_prefetch,_templates){var obj; /** * Course index section component. * @@ -9,7 +9,6 @@ define("core_courseformat/local/courseeditor/dndsection",["exports","core/reacti * @class core_courseformat/local/courseeditor/dndsection * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class _default extends _reactive.BaseComponent{configState(state){this.id=this.element.dataset.id,this.section=state.section.get(this.id),this.course=state.course}configDragDrop(sectionitem){this.reactive.isEditing&&this.reactive.supportComponents&&(this.sectionitem=sectionitem,this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.sectionitem&&this.sectionitem.unregister(),void 0!==this.dragdrop&&this.dragdrop.unregister()}getLastCm(){return null}dragStart(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!1)}validateDropData(dropdata){if("cm"===(null==dropdata?void 0:dropdata.type))return!0;if("section"===(null==dropdata?void 0:dropdata.type)){const sectionzeroid=this.course.sectionlist[0];return(null==dropdata?void 0:dropdata.id)!=this.id&&(null==dropdata?void 0:dropdata.id)!=sectionzeroid&&this.id!=sectionzeroid}return!1}showDropZone(dropdata){var _this$getLastCm;"cm"==dropdata.type&&(null===(_this$getLastCm=this.getLastCm())||void 0===_this$getLastCm||_this$getLastCm.classList.add(this.classes.DROPDOWN));"section"==dropdata.type&&(this.section.number>dropdata.number?(this.element.classList.remove(this.classes.DROPUP),this.element.classList.add(this.classes.DROPDOWN)):(this.element.classList.add(this.classes.DROPUP),this.element.classList.remove(this.classes.DROPDOWN)))}hideDropZone(){var _this$getLastCm2;null===(_this$getLastCm2=this.getLastCm())||void 0===_this$getLastCm2||_this$getLastCm2.classList.remove(this.classes.DROPDOWN),this.element.classList.remove(this.classes.DROPUP),this.element.classList.remove(this.classes.DROPDOWN)}drop(dropdata,event){if("cm"==dropdata.type){const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[dropdata.id],this.id)}"section"==dropdata.type&&this.reactive.dispatch("sectionMove",[dropdata.id],this.id)}}return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj},(0,_prefetch.prefetchStrings)("core",["addfilehere"]);class _default extends _reactive.BaseComponent{configState(state){this.id=this.element.dataset.id,this.section=state.section.get(this.id),this.course=state.course}configDragDrop(sectionitem){this.reactive.isEditing&&this.reactive.supportComponents&&(this.sectionitem=sectionitem,this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.sectionitem&&this.sectionitem.unregister(),void 0!==this.dragdrop&&this.dragdrop.unregister()}getLastCm(){return null}dragStart(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!1)}validateDropData(dropdata){if("files"===(null==dropdata?void 0:dropdata.type))return!0;if("cm"===(null==dropdata?void 0:dropdata.type))return!0;if("section"===(null==dropdata?void 0:dropdata.type)){const sectionzeroid=this.course.sectionlist[0];return(null==dropdata?void 0:dropdata.id)!=this.id&&(null==dropdata?void 0:dropdata.id)!=sectionzeroid&&this.id!=sectionzeroid}return!1}showDropZone(dropdata){var _this$getLastCm;("files"==dropdata.type&&this.addOverlay({content:(0,_str.get_string)("addfilehere","core"),icon:_templates.default.renderPix("t/download","core")}).then((()=>{var _this$dragdrop;null!==(_this$dragdrop=this.dragdrop)&&void 0!==_this$dragdrop&&_this$dragdrop.isDropzoneVisible()||this.removeOverlay()})).catch((error=>{throw error})),"cm"==dropdata.type)&&(null===(_this$getLastCm=this.getLastCm())||void 0===_this$getLastCm||_this$getLastCm.classList.add(this.classes.DROPDOWN));"section"==dropdata.type&&(this.section.number>dropdata.number?(this.element.classList.remove(this.classes.DROPUP),this.element.classList.add(this.classes.DROPDOWN)):(this.element.classList.add(this.classes.DROPUP),this.element.classList.remove(this.classes.DROPDOWN)))}hideDropZone(){var _this$getLastCm2;null===(_this$getLastCm2=this.getLastCm())||void 0===_this$getLastCm2||_this$getLastCm2.classList.remove(this.classes.DROPDOWN),this.element.classList.remove(this.classes.DROPUP),this.element.classList.remove(this.classes.DROPDOWN),this.removeOverlay()}drop(dropdata,event){if("files"!=dropdata.type){if("cm"==dropdata.type){const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[dropdata.id],this.id)}"section"==dropdata.type&&this.reactive.dispatch("sectionMove",[dropdata.id],this.id)}else this.reactive.uploadFiles(this.section.id,this.section.number,dropdata.files)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=dndsection.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/dndsection.min.js.map b/course/format/amd/build/local/courseeditor/dndsection.min.js.map index 44bcb9eb2b6..c1a8e81d4e3 100644 --- a/course/format/amd/build/local/courseeditor/dndsection.min.js.map +++ b/course/format/amd/build/local/courseeditor/dndsection.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dndsection.min.js","sources":["../../../src/local/courseeditor/dndsection.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 * Course index section component.\n *\n * This component is used to control specific course section interactions like drag and drop\n * in both course index and course content.\n *\n * @module core_courseformat/local/courseeditor/dndsection\n * @class core_courseformat/local/courseeditor/dndsection\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\n\nexport default class extends BaseComponent {\n\n /**\n * Save some values form the state.\n *\n * @param {Object} state the current state\n */\n configState(state) {\n this.id = this.element.dataset.id;\n this.section = state.section.get(this.id);\n this.course = state.course;\n }\n\n /**\n * Register state values and the drag and drop subcomponent.\n *\n * @param {BaseComponent} sectionitem section item component\n */\n configDragDrop(sectionitem) {\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init the inner dragable element.\n this.sectionitem = sectionitem;\n // Init the dropzone.\n this.dragdrop = new DragDrop(this);\n // Save dropzone classes.\n this.classes = this.dragdrop.getClasses();\n }\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.sectionitem !== undefined) {\n this.sectionitem.unregister();\n }\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null} the las course module element of the section.\n */\n getLastCm() {\n return null;\n }\n\n // Drag and drop methods.\n\n /**\n * The element drop start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragStart(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], true);\n }\n\n /**\n * The element drop end hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragEnd(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], false);\n }\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n // We accept any course module.\n if (dropdata?.type === 'cm') {\n return true;\n }\n // We accept any section but the section 0 or ourself\n if (dropdata?.type === 'section') {\n const sectionzeroid = this.course.sectionlist[0];\n return dropdata?.id != this.id && dropdata?.id != sectionzeroid && this.id != sectionzeroid;\n }\n return false;\n }\n\n /**\n * Display the component dropzone.\n *\n * @param {Object} dropdata the accepted drop data\n */\n showDropZone(dropdata) {\n if (dropdata.type == 'cm') {\n this.getLastCm()?.classList.add(this.classes.DROPDOWN);\n }\n if (dropdata.type == 'section') {\n // The relative move of section depends on the section number.\n if (this.section.number > dropdata.number) {\n this.element.classList.remove(this.classes.DROPUP);\n this.element.classList.add(this.classes.DROPDOWN);\n } else {\n this.element.classList.add(this.classes.DROPUP);\n this.element.classList.remove(this.classes.DROPDOWN);\n }\n }\n }\n\n /**\n * Hide the component dropzone.\n */\n hideDropZone() {\n this.getLastCm()?.classList.remove(this.classes.DROPDOWN);\n this.element.classList.remove(this.classes.DROPUP);\n this.element.classList.remove(this.classes.DROPDOWN);\n }\n\n /**\n * Drop event handler.\n *\n * @param {Object} dropdata the accepted drop data\n * @param {Event} event the drop event\n */\n drop(dropdata, event) {\n // Call the move mutation.\n if (dropdata.type == 'cm') {\n const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove';\n this.reactive.dispatch(mutation, [dropdata.id], this.id);\n }\n if (dropdata.type == 'section') {\n this.reactive.dispatch('sectionMove', [dropdata.id], this.id);\n }\n }\n}\n"],"names":["BaseComponent","configState","state","id","this","element","dataset","section","get","course","configDragDrop","sectionitem","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","destroy","undefined","unregister","getLastCm","dragStart","dropdata","dispatch","dragEnd","validateDropData","type","sectionzeroid","sectionlist","showDropZone","classList","add","DROPDOWN","number","remove","DROPUP","hideDropZone","drop","event","mutation","altKey"],"mappings":";;;;;;;;;;;;uBA6B6BA,wBAOzBC,YAAYC,YACHC,GAAKC,KAAKC,QAAQC,QAAQH,QAC1BI,QAAUL,MAAMK,QAAQC,IAAIJ,KAAKD,SACjCM,OAASP,MAAMO,OAQxBC,eAAeC,aAEPP,KAAKQ,SAASC,WAAaT,KAAKQ,SAASE,yBAEpCH,YAAcA,iBAEdI,SAAW,IAAIC,mBAASZ,WAExBa,QAAUb,KAAKW,SAASG,cAOrCC,eAC6BC,IAArBhB,KAAKO,kBACAA,YAAYU,kBAECD,IAAlBhB,KAAKW,eACAA,SAASM,aAStBC,mBACW,KAUXC,UAAUC,eACDZ,SAASa,SAAS,cAAe,CAACD,SAASrB,KAAK,GAQzDuB,QAAQF,eACCZ,SAASa,SAAS,cAAe,CAACD,SAASrB,KAAK,GASzDwB,iBAAiBH,aAEU,QAAnBA,MAAAA,gBAAAA,SAAUI,aACH,KAGY,aAAnBJ,MAAAA,gBAAAA,SAAUI,MAAoB,OACxBC,cAAgBzB,KAAKK,OAAOqB,YAAY,UACvCN,MAAAA,gBAAAA,SAAUrB,KAAMC,KAAKD,KAAMqB,MAAAA,gBAAAA,SAAUrB,KAAM0B,eAAiBzB,KAAKD,IAAM0B,qBAE3E,EAQXE,aAAaP,8BACY,MAAjBA,SAASI,oCACJN,wDAAaU,UAAUC,IAAI7B,KAAKa,QAAQiB,WAE5B,WAAjBV,SAASI,OAELxB,KAAKG,QAAQ4B,OAASX,SAASW,aAC1B9B,QAAQ2B,UAAUI,OAAOhC,KAAKa,QAAQoB,aACtChC,QAAQ2B,UAAUC,IAAI7B,KAAKa,QAAQiB,iBAEnC7B,QAAQ2B,UAAUC,IAAI7B,KAAKa,QAAQoB,aACnChC,QAAQ2B,UAAUI,OAAOhC,KAAKa,QAAQiB,YAQvDI,kEACShB,0DAAaU,UAAUI,OAAOhC,KAAKa,QAAQiB,eAC3C7B,QAAQ2B,UAAUI,OAAOhC,KAAKa,QAAQoB,aACtChC,QAAQ2B,UAAUI,OAAOhC,KAAKa,QAAQiB,UAS/CK,KAAKf,SAAUgB,UAEU,MAAjBhB,SAASI,KAAc,OACjBa,SAAYD,MAAME,OAAU,cAAgB,cAC7C9B,SAASa,SAASgB,SAAU,CAACjB,SAASrB,IAAKC,KAAKD,IAEpC,WAAjBqB,SAASI,WACJhB,SAASa,SAAS,cAAe,CAACD,SAASrB,IAAKC,KAAKD"} \ No newline at end of file +{"version":3,"file":"dndsection.min.js","sources":["../../../src/local/courseeditor/dndsection.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 * Course index section component.\n *\n * This component is used to control specific course section interactions like drag and drop\n * in both course index and course content.\n *\n * @module core_courseformat/local/courseeditor/dndsection\n * @class core_courseformat/local/courseeditor/dndsection\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\nimport {get_string as getString} from 'core/str';\nimport {prefetchStrings} from 'core/prefetch';\nimport Templates from 'core/templates';\n\n// Load global strings.\nprefetchStrings('core', ['addfilehere']);\n\nexport default class extends BaseComponent {\n\n /**\n * Save some values form the state.\n *\n * @param {Object} state the current state\n */\n configState(state) {\n this.id = this.element.dataset.id;\n this.section = state.section.get(this.id);\n this.course = state.course;\n }\n\n /**\n * Register state values and the drag and drop subcomponent.\n *\n * @param {BaseComponent} sectionitem section item component\n */\n configDragDrop(sectionitem) {\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init the inner dragable element.\n this.sectionitem = sectionitem;\n // Init the dropzone.\n this.dragdrop = new DragDrop(this);\n // Save dropzone classes.\n this.classes = this.dragdrop.getClasses();\n }\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.sectionitem !== undefined) {\n this.sectionitem.unregister();\n }\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null} the las course module element of the section.\n */\n getLastCm() {\n return null;\n }\n\n // Drag and drop methods.\n\n /**\n * The element drop start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragStart(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], true);\n }\n\n /**\n * The element drop end hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragEnd(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], false);\n }\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n // We accept files.\n if (dropdata?.type === 'files') {\n return true;\n }\n // We accept any course module.\n if (dropdata?.type === 'cm') {\n return true;\n }\n // We accept any section but the section 0 or ourself\n if (dropdata?.type === 'section') {\n const sectionzeroid = this.course.sectionlist[0];\n return dropdata?.id != this.id && dropdata?.id != sectionzeroid && this.id != sectionzeroid;\n }\n return false;\n }\n\n /**\n * Display the component dropzone.\n *\n * @param {Object} dropdata the accepted drop data\n */\n showDropZone(dropdata) {\n if (dropdata.type == 'files') {\n this.addOverlay({\n content: getString('addfilehere', 'core'),\n icon: Templates.renderPix('t/download', 'core'),\n }).then(() => {\n // Check if we still need the file dropzone.\n if (!this.dragdrop?.isDropzoneVisible()) {\n this.removeOverlay();\n }\n return;\n }).catch((error) => {\n throw error;\n });\n }\n if (dropdata.type == 'cm') {\n this.getLastCm()?.classList.add(this.classes.DROPDOWN);\n }\n if (dropdata.type == 'section') {\n // The relative move of section depends on the section number.\n if (this.section.number > dropdata.number) {\n this.element.classList.remove(this.classes.DROPUP);\n this.element.classList.add(this.classes.DROPDOWN);\n } else {\n this.element.classList.add(this.classes.DROPUP);\n this.element.classList.remove(this.classes.DROPDOWN);\n }\n }\n }\n\n /**\n * Hide the component dropzone.\n */\n hideDropZone() {\n this.getLastCm()?.classList.remove(this.classes.DROPDOWN);\n this.element.classList.remove(this.classes.DROPUP);\n this.element.classList.remove(this.classes.DROPDOWN);\n this.removeOverlay();\n }\n\n /**\n * Drop event handler.\n *\n * @param {Object} dropdata the accepted drop data\n * @param {Event} event the drop event\n */\n drop(dropdata, event) {\n // File handling.\n if (dropdata.type == 'files') {\n this.reactive.uploadFiles(\n this.section.id,\n this.section.number,\n dropdata.files\n );\n return;\n }\n // Call the move mutation.\n if (dropdata.type == 'cm') {\n const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove';\n this.reactive.dispatch(mutation, [dropdata.id], this.id);\n }\n if (dropdata.type == 'section') {\n this.reactive.dispatch('sectionMove', [dropdata.id], this.id);\n }\n }\n}\n"],"names":["BaseComponent","configState","state","id","this","element","dataset","section","get","course","configDragDrop","sectionitem","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","destroy","undefined","unregister","getLastCm","dragStart","dropdata","dispatch","dragEnd","validateDropData","type","sectionzeroid","sectionlist","showDropZone","addOverlay","content","icon","Templates","renderPix","then","_this$dragdrop","isDropzoneVisible","removeOverlay","catch","error","classList","add","DROPDOWN","number","remove","DROPUP","hideDropZone","drop","event","mutation","altKey","uploadFiles","files"],"mappings":";;;;;;;;;;;iLAiCgB,OAAQ,CAAC,uCAEIA,wBAOzBC,YAAYC,YACHC,GAAKC,KAAKC,QAAQC,QAAQH,QAC1BI,QAAUL,MAAMK,QAAQC,IAAIJ,KAAKD,SACjCM,OAASP,MAAMO,OAQxBC,eAAeC,aAEPP,KAAKQ,SAASC,WAAaT,KAAKQ,SAASE,yBAEpCH,YAAcA,iBAEdI,SAAW,IAAIC,mBAASZ,WAExBa,QAAUb,KAAKW,SAASG,cAOrCC,eAC6BC,IAArBhB,KAAKO,kBACAA,YAAYU,kBAECD,IAAlBhB,KAAKW,eACAA,SAASM,aAStBC,mBACW,KAUXC,UAAUC,eACDZ,SAASa,SAAS,cAAe,CAACD,SAASrB,KAAK,GAQzDuB,QAAQF,eACCZ,SAASa,SAAS,cAAe,CAACD,SAASrB,KAAK,GASzDwB,iBAAiBH,aAEU,WAAnBA,MAAAA,gBAAAA,SAAUI,aACH,KAGY,QAAnBJ,MAAAA,gBAAAA,SAAUI,aACH,KAGY,aAAnBJ,MAAAA,gBAAAA,SAAUI,MAAoB,OACxBC,cAAgBzB,KAAKK,OAAOqB,YAAY,UACvCN,MAAAA,gBAAAA,SAAUrB,KAAMC,KAAKD,KAAMqB,MAAAA,gBAAAA,SAAUrB,KAAM0B,eAAiBzB,KAAKD,IAAM0B,qBAE3E,EAQXE,aAAaP,+BACY,SAAjBA,SAASI,WACJI,WAAW,CACZC,SAAS,mBAAU,cAAe,QAClCC,KAAMC,mBAAUC,UAAU,aAAc,UACzCC,MAAK,+CAECjC,KAAKW,oCAALuB,eAAeC,0BACXC,mBAGVC,OAAOC,cACAA,SAGO,MAAjBlB,SAASI,qCACJN,wDAAaqB,UAAUC,IAAIxC,KAAKa,QAAQ4B,WAE5B,WAAjBrB,SAASI,OAELxB,KAAKG,QAAQuC,OAAStB,SAASsB,aAC1BzC,QAAQsC,UAAUI,OAAO3C,KAAKa,QAAQ+B,aACtC3C,QAAQsC,UAAUC,IAAIxC,KAAKa,QAAQ4B,iBAEnCxC,QAAQsC,UAAUC,IAAIxC,KAAKa,QAAQ+B,aACnC3C,QAAQsC,UAAUI,OAAO3C,KAAKa,QAAQ4B,YAQvDI,kEACS3B,0DAAaqB,UAAUI,OAAO3C,KAAKa,QAAQ4B,eAC3CxC,QAAQsC,UAAUI,OAAO3C,KAAKa,QAAQ+B,aACtC3C,QAAQsC,UAAUI,OAAO3C,KAAKa,QAAQ4B,eACtCL,gBASTU,KAAK1B,SAAU2B,UAEU,SAAjB3B,SAASI,SASQ,MAAjBJ,SAASI,KAAc,OACjBwB,SAAYD,MAAME,OAAU,cAAgB,cAC7CzC,SAASa,SAAS2B,SAAU,CAAC5B,SAASrB,IAAKC,KAAKD,IAEpC,WAAjBqB,SAASI,WACJhB,SAASa,SAAS,cAAe,CAACD,SAASrB,IAAKC,KAAKD,cAbrDS,SAAS0C,YACVlD,KAAKG,QAAQJ,GACbC,KAAKG,QAAQuC,OACbtB,SAAS+B"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/exporter.min.js b/course/format/amd/build/local/courseeditor/exporter.min.js index b1fba982b55..f6aa2488e5b 100644 --- a/course/format/amd/build/local/courseeditor/exporter.min.js +++ b/course/format/amd/build/local/courseeditor/exporter.min.js @@ -8,6 +8,6 @@ define("core_courseformat/local/courseeditor/exporter",["exports"],(function(_ex * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class{constructor(reactive){this.reactive=reactive,this.COMPLETIONS=["incomplete","complete","complete","fail"]}course(state){var _state$course$highlig,_state$course$section;const data={sections:[],editmode:this.reactive.isEditing,highlighted:null!==(_state$course$highlig=state.course.highlighted)&&void 0!==_state$course$highlig?_state$course$highlig:""};return(null!==(_state$course$section=state.course.sectionlist)&&void 0!==_state$course$section?_state$course$section:[]).forEach((sectionid=>{var _state$section$get;const sectioninfo=null!==(_state$section$get=state.section.get(sectionid))&&void 0!==_state$section$get?_state$section$get:{},section=this.section(state,sectioninfo);data.sections.push(section)})),data.hassections=0!=data.sections.length,data}section(state,sectioninfo){var _state$course$highlig2,_sectioninfo$cmlist;const section={...sectioninfo,highlighted:null!==(_state$course$highlig2=state.course.highlighted)&&void 0!==_state$course$highlig2?_state$course$highlig2:"",cms:[]};return(null!==(_sectioninfo$cmlist=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist?_sectioninfo$cmlist:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid),cm=this.cm(state,cminfo);section.cms.push(cm)})),section.hascms=0!=section.cms.length,section}cm(state,cminfo){return{...cminfo,isactive:!1}}cmDraggableData(state,cmid){const cminfo=state.cm.get(cmid);if(!cminfo)return null;let nextcmid;const section=state.section.get(cminfo.sectionid),currentindex=null==section?void 0:section.cmlist.indexOf(cminfo.id);return void 0!==currentindex&&(nextcmid=null==section?void 0:section.cmlist[currentindex+1]),{type:"cm",id:cminfo.id,name:cminfo.name,sectionid:cminfo.sectionid,nextcmid:nextcmid}}sectionDraggableData(state,sectionid){const sectioninfo=state.section.get(sectionid);return sectioninfo?{type:"section",id:sectioninfo.id,name:sectioninfo.name,number:sectioninfo.number}:null}cmCompletion(state,cminfo){const data={statename:"",state:"NaN"};if(void 0!==cminfo.completionstate){var _this$COMPLETIONS$cmi;data.state=cminfo.completionstate,data.hasstate=!0;const statename=null!==(_this$COMPLETIONS$cmi=this.COMPLETIONS[cminfo.completionstate])&&void 0!==_this$COMPLETIONS$cmi?_this$COMPLETIONS$cmi:"NaN";data["is".concat(statename)]=!0}return data}allItemsArray(state){var _state$course$section2;const items=[];return(null!==(_state$course$section2=state.course.sectionlist)&&void 0!==_state$course$section2?_state$course$section2:[]).forEach((sectionid=>{var _sectioninfo$cmlist2;const sectioninfo=state.section.get(sectionid);items.push({type:"section",id:sectioninfo.id,url:sectioninfo.sectionurl});(null!==(_sectioninfo$cmlist2=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist2?_sectioninfo$cmlist2:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid);items.push({type:"cm",id:cminfo.id,url:cminfo.url})}))})),items}},_exports.default})); +class{constructor(reactive){this.reactive=reactive,this.COMPLETIONS=["incomplete","complete","complete","fail"]}course(state){var _state$course$highlig,_state$course$section;const data={sections:[],editmode:this.reactive.isEditing,highlighted:null!==(_state$course$highlig=state.course.highlighted)&&void 0!==_state$course$highlig?_state$course$highlig:""};return(null!==(_state$course$section=state.course.sectionlist)&&void 0!==_state$course$section?_state$course$section:[]).forEach((sectionid=>{var _state$section$get;const sectioninfo=null!==(_state$section$get=state.section.get(sectionid))&&void 0!==_state$section$get?_state$section$get:{},section=this.section(state,sectioninfo);data.sections.push(section)})),data.hassections=0!=data.sections.length,data}section(state,sectioninfo){var _state$course$highlig2,_sectioninfo$cmlist;const section={...sectioninfo,highlighted:null!==(_state$course$highlig2=state.course.highlighted)&&void 0!==_state$course$highlig2?_state$course$highlig2:"",cms:[]};return(null!==(_sectioninfo$cmlist=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist?_sectioninfo$cmlist:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid),cm=this.cm(state,cminfo);section.cms.push(cm)})),section.hascms=0!=section.cms.length,section}cm(state,cminfo){return{...cminfo,isactive:!1}}cmDraggableData(state,cmid){const cminfo=state.cm.get(cmid);if(!cminfo)return null;let nextcmid;const section=state.section.get(cminfo.sectionid),currentindex=null==section?void 0:section.cmlist.indexOf(cminfo.id);return void 0!==currentindex&&(nextcmid=null==section?void 0:section.cmlist[currentindex+1]),{type:"cm",id:cminfo.id,name:cminfo.name,sectionid:cminfo.sectionid,nextcmid:nextcmid}}sectionDraggableData(state,sectionid){const sectioninfo=state.section.get(sectionid);return sectioninfo?{type:"section",id:sectioninfo.id,name:sectioninfo.name,number:sectioninfo.number}:null}fileDraggableData(state,dataTransfer){var _dataTransfer$files;const files=[];return(null===(_dataTransfer$files=dataTransfer.files)||void 0===_dataTransfer$files?void 0:_dataTransfer$files.length)>0&&dataTransfer.files.forEach((file=>{files.push(file)})),{type:"files",files:files}}cmCompletion(state,cminfo){const data={statename:"",state:"NaN"};if(void 0!==cminfo.completionstate){var _this$COMPLETIONS$cmi;data.state=cminfo.completionstate,data.hasstate=!0;const statename=null!==(_this$COMPLETIONS$cmi=this.COMPLETIONS[cminfo.completionstate])&&void 0!==_this$COMPLETIONS$cmi?_this$COMPLETIONS$cmi:"NaN";data["is".concat(statename)]=!0}return data}allItemsArray(state){var _state$course$section2;const items=[];return(null!==(_state$course$section2=state.course.sectionlist)&&void 0!==_state$course$section2?_state$course$section2:[]).forEach((sectionid=>{var _sectioninfo$cmlist2;const sectioninfo=state.section.get(sectionid);items.push({type:"section",id:sectioninfo.id,url:sectioninfo.sectionurl});(null!==(_sectioninfo$cmlist2=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist2?_sectioninfo$cmlist2:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid);items.push({type:"cm",id:cminfo.id,url:cminfo.url})}))})),items}},_exports.default})); //# sourceMappingURL=exporter.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/exporter.min.js.map b/course/format/amd/build/local/courseeditor/exporter.min.js.map index 5099a14ad18..c38cb0f2d96 100644 --- a/course/format/amd/build/local/courseeditor/exporter.min.js.map +++ b/course/format/amd/build/local/courseeditor/exporter.min.js.map @@ -1 +1 @@ -{"version":3,"file":"exporter.min.js","sources":["../../../src/local/courseeditor/exporter.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 * Module to export parts of the state and transform them to be used in templates\n * and as draggable data.\n *\n * @module core_courseformat/local/courseeditor/exporter\n * @class core_courseformat/local/courseeditor/exporter\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n /**\n * Class constructor.\n *\n * @param {CourseEditor} reactive the course editor object\n */\n constructor(reactive) {\n this.reactive = reactive;\n\n // Completions states are defined in lib/completionlib.php. There are 4 different completion\n // state values, however, the course index uses the same state for complete and complete_pass.\n // This is the reason why completed appears twice in the array.\n this.COMPLETIONS = ['incomplete', 'complete', 'complete', 'fail'];\n }\n\n /**\n * Generate the course export data from the state.\n *\n * @param {Object} state the current state.\n * @returns {Object}\n */\n course(state) {\n // Collect section information from the state.\n const data = {\n sections: [],\n editmode: this.reactive.isEditing,\n highlighted: state.course.highlighted ?? '',\n };\n const sectionlist = state.course.sectionlist ?? [];\n sectionlist.forEach(sectionid => {\n const sectioninfo = state.section.get(sectionid) ?? {};\n const section = this.section(state, sectioninfo);\n data.sections.push(section);\n });\n data.hassections = (data.sections.length != 0);\n\n return data;\n }\n\n /**\n * Generate a section export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} sectioninfo the section state data.\n * @returns {Object}\n */\n section(state, sectioninfo) {\n const section = {\n ...sectioninfo,\n highlighted: state.course.highlighted ?? '',\n cms: [],\n };\n const cmlist = sectioninfo.cmlist ?? [];\n cmlist.forEach(cmid => {\n const cminfo = state.cm.get(cmid);\n const cm = this.cm(state, cminfo);\n section.cms.push(cm);\n });\n section.hascms = (section.cms.length != 0);\n\n return section;\n }\n\n /**\n * Generate a cm export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} cminfo the course module state data.\n * @returns {Object}\n */\n cm(state, cminfo) {\n const cm = {\n ...cminfo,\n isactive: false,\n };\n return cm;\n }\n\n /**\n * Generate a dragable cm data structure.\n *\n * This method is used by any draggable course module element to generate drop data\n * for its reactive/dragdrop instance.\n *\n * @param {*} state the state object\n * @param {*} cmid the cours emodule id\n * @returns {Object|null}\n */\n cmDraggableData(state, cmid) {\n const cminfo = state.cm.get(cmid);\n if (!cminfo) {\n return null;\n }\n\n // Drop an activity over the next activity is the same as doing anything.\n let nextcmid;\n const section = state.section.get(cminfo.sectionid);\n const currentindex = section?.cmlist.indexOf(cminfo.id);\n if (currentindex !== undefined) {\n nextcmid = section?.cmlist[currentindex + 1];\n }\n\n return {\n type: 'cm',\n id: cminfo.id,\n name: cminfo.name,\n sectionid: cminfo.sectionid,\n nextcmid,\n };\n }\n\n /**\n * Generate a dragable cm data structure.\n *\n * This method is used by any draggable section element to generate drop data\n * for its reactive/dragdrop instance.\n *\n * @param {*} state the state object\n * @param {*} sectionid the cours section id\n * @returns {Object|null}\n */\n sectionDraggableData(state, sectionid) {\n const sectioninfo = state.section.get(sectionid);\n if (!sectioninfo) {\n return null;\n }\n return {\n type: 'section',\n id: sectioninfo.id,\n name: sectioninfo.name,\n number: sectioninfo.number,\n };\n }\n\n /**\n * Generate a compoetion export data from the cm element.\n *\n * @param {Object} state the current state.\n * @param {Object} cminfo the course module state data.\n * @returns {Object}\n */\n cmCompletion(state, cminfo) {\n const data = {\n statename: '',\n state: 'NaN',\n };\n if (cminfo.completionstate !== undefined) {\n data.state = cminfo.completionstate;\n data.hasstate = true;\n const statename = this.COMPLETIONS[cminfo.completionstate] ?? 'NaN';\n data[`is${statename}`] = true;\n }\n return data;\n }\n\n /**\n * Return a sorted list of all sections and cms items in the state.\n *\n * @param {Object} state the current state.\n * @returns {Array} all sections and cms items in the state.\n */\n allItemsArray(state) {\n const items = [];\n const sectionlist = state.course.sectionlist ?? [];\n // Add sections.\n sectionlist.forEach(sectionid => {\n const sectioninfo = state.section.get(sectionid);\n items.push({type: 'section', id: sectioninfo.id, url: sectioninfo.sectionurl});\n // Add cms.\n const cmlist = sectioninfo.cmlist ?? [];\n cmlist.forEach(cmid => {\n const cminfo = state.cm.get(cmid);\n items.push({type: 'cm', id: cminfo.id, url: cminfo.url});\n });\n });\n return items;\n }\n}\n"],"names":["constructor","reactive","COMPLETIONS","course","state","data","sections","editmode","this","isEditing","highlighted","sectionlist","forEach","sectionid","sectioninfo","section","get","push","hassections","length","cms","cmlist","cmid","cminfo","cm","hascms","isactive","cmDraggableData","nextcmid","currentindex","indexOf","id","undefined","type","name","sectionDraggableData","number","cmCompletion","statename","completionstate","hasstate","allItemsArray","items","url","sectionurl"],"mappings":";;;;;;;;;;MA+BIA,YAAYC,eACHA,SAAWA,cAKXC,YAAc,CAAC,aAAc,WAAY,WAAY,QAS9DC,OAAOC,6DAEGC,KAAO,CACTC,SAAU,GACVC,SAAUC,KAAKP,SAASQ,UACxBC,0CAAaN,MAAMD,OAAOO,mEAAe,yCAEzBN,MAAMD,OAAOQ,mEAAe,IACpCC,SAAQC,yCACVC,uCAAcV,MAAMW,QAAQC,IAAIH,4DAAc,GAC9CE,QAAUP,KAAKO,QAAQX,MAAOU,aACpCT,KAAKC,SAASW,KAAKF,YAEvBV,KAAKa,YAAuC,GAAxBb,KAAKC,SAASa,OAE3Bd,KAUXU,QAAQX,MAAOU,kEACLC,QAAU,IACTD,YACHJ,2CAAaN,MAAMD,OAAOO,qEAAe,GACzCU,IAAK,uCAEMN,YAAYO,0DAAU,IAC9BT,SAAQU,aACLC,OAASnB,MAAMoB,GAAGR,IAAIM,MACtBE,GAAKhB,KAAKgB,GAAGpB,MAAOmB,QAC1BR,QAAQK,IAAIH,KAAKO,OAErBT,QAAQU,OAAgC,GAAtBV,QAAQK,IAAID,OAEvBJ,QAUXS,GAAGpB,MAAOmB,cACK,IACJA,OACHG,UAAU,GAelBC,gBAAgBvB,MAAOkB,YACbC,OAASnB,MAAMoB,GAAGR,IAAIM,UACvBC,cACM,SAIPK,eACEb,QAAUX,MAAMW,QAAQC,IAAIO,OAAOV,WACnCgB,aAAed,MAAAA,eAAAA,QAASM,OAAOS,QAAQP,OAAOQ,gBAC/BC,IAAjBH,eACAD,SAAWb,MAAAA,eAAAA,QAASM,OAAOQ,aAAe,IAGvC,CACHI,KAAM,KACNF,GAAIR,OAAOQ,GACXG,KAAMX,OAAOW,KACbrB,UAAWU,OAAOV,UAClBe,SAAAA,UAcRO,qBAAqB/B,MAAOS,iBAClBC,YAAcV,MAAMW,QAAQC,IAAIH,kBACjCC,YAGE,CACHmB,KAAM,UACNF,GAAIjB,YAAYiB,GAChBG,KAAMpB,YAAYoB,KAClBE,OAAQtB,YAAYsB,QANb,KAiBfC,aAAajC,MAAOmB,cACVlB,KAAO,CACTiC,UAAW,GACXlC,MAAO,eAEoB4B,IAA3BT,OAAOgB,gBAA+B,2BACtClC,KAAKD,MAAQmB,OAAOgB,gBACpBlC,KAAKmC,UAAW,QACVF,wCAAY9B,KAAKN,YAAYqB,OAAOgB,wEAAoB,MAC9DlC,iBAAUiC,aAAe,SAEtBjC,KASXoC,cAAcrC,wCACJsC,MAAQ,yCACMtC,MAAMD,OAAOQ,qEAAe,IAEpCC,SAAQC,2CACVC,YAAcV,MAAMW,QAAQC,IAAIH,WACtC6B,MAAMzB,KAAK,CAACgB,KAAM,UAAWF,GAAIjB,YAAYiB,GAAIY,IAAK7B,YAAY8B,2CAEnD9B,YAAYO,4DAAU,IAC9BT,SAAQU,aACLC,OAASnB,MAAMoB,GAAGR,IAAIM,MAC5BoB,MAAMzB,KAAK,CAACgB,KAAM,KAAMF,GAAIR,OAAOQ,GAAIY,IAAKpB,OAAOoB,YAGpDD"} \ No newline at end of file +{"version":3,"file":"exporter.min.js","sources":["../../../src/local/courseeditor/exporter.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 * Module to export parts of the state and transform them to be used in templates\n * and as draggable data.\n *\n * @module core_courseformat/local/courseeditor/exporter\n * @class core_courseformat/local/courseeditor/exporter\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n /**\n * Class constructor.\n *\n * @param {CourseEditor} reactive the course editor object\n */\n constructor(reactive) {\n this.reactive = reactive;\n\n // Completions states are defined in lib/completionlib.php. There are 4 different completion\n // state values, however, the course index uses the same state for complete and complete_pass.\n // This is the reason why completed appears twice in the array.\n this.COMPLETIONS = ['incomplete', 'complete', 'complete', 'fail'];\n }\n\n /**\n * Generate the course export data from the state.\n *\n * @param {Object} state the current state.\n * @returns {Object}\n */\n course(state) {\n // Collect section information from the state.\n const data = {\n sections: [],\n editmode: this.reactive.isEditing,\n highlighted: state.course.highlighted ?? '',\n };\n const sectionlist = state.course.sectionlist ?? [];\n sectionlist.forEach(sectionid => {\n const sectioninfo = state.section.get(sectionid) ?? {};\n const section = this.section(state, sectioninfo);\n data.sections.push(section);\n });\n data.hassections = (data.sections.length != 0);\n\n return data;\n }\n\n /**\n * Generate a section export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} sectioninfo the section state data.\n * @returns {Object}\n */\n section(state, sectioninfo) {\n const section = {\n ...sectioninfo,\n highlighted: state.course.highlighted ?? '',\n cms: [],\n };\n const cmlist = sectioninfo.cmlist ?? [];\n cmlist.forEach(cmid => {\n const cminfo = state.cm.get(cmid);\n const cm = this.cm(state, cminfo);\n section.cms.push(cm);\n });\n section.hascms = (section.cms.length != 0);\n\n return section;\n }\n\n /**\n * Generate a cm export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} cminfo the course module state data.\n * @returns {Object}\n */\n cm(state, cminfo) {\n const cm = {\n ...cminfo,\n isactive: false,\n };\n return cm;\n }\n\n /**\n * Generate a dragable cm data structure.\n *\n * This method is used by any draggable course module element to generate drop data\n * for its reactive/dragdrop instance.\n *\n * @param {*} state the state object\n * @param {*} cmid the cours emodule id\n * @returns {Object|null}\n */\n cmDraggableData(state, cmid) {\n const cminfo = state.cm.get(cmid);\n if (!cminfo) {\n return null;\n }\n\n // Drop an activity over the next activity is the same as doing anything.\n let nextcmid;\n const section = state.section.get(cminfo.sectionid);\n const currentindex = section?.cmlist.indexOf(cminfo.id);\n if (currentindex !== undefined) {\n nextcmid = section?.cmlist[currentindex + 1];\n }\n\n return {\n type: 'cm',\n id: cminfo.id,\n name: cminfo.name,\n sectionid: cminfo.sectionid,\n nextcmid,\n };\n }\n\n /**\n * Generate a dragable cm data structure.\n *\n * This method is used by any draggable section element to generate drop data\n * for its reactive/dragdrop instance.\n *\n * @param {*} state the state object\n * @param {*} sectionid the cours section id\n * @returns {Object|null}\n */\n sectionDraggableData(state, sectionid) {\n const sectioninfo = state.section.get(sectionid);\n if (!sectioninfo) {\n return null;\n }\n return {\n type: 'section',\n id: sectioninfo.id,\n name: sectioninfo.name,\n number: sectioninfo.number,\n };\n }\n\n /**\n * Generate a file draggable structure.\n *\n * This method is used when files are dragged on the browser.\n *\n * @param {*} state the state object\n * @param {*} dataTransfer the current data tranfer data\n * @returns {Object|null}\n */\n fileDraggableData(state, dataTransfer) {\n const files = [];\n // Browsers do not provide the file list until the drop event.\n if (dataTransfer.files?.length > 0) {\n dataTransfer.files.forEach(file => {\n files.push(file);\n });\n }\n return {\n type: 'files',\n files,\n };\n }\n\n /**\n * Generate a completion export data from the cm element.\n *\n * @param {Object} state the current state.\n * @param {Object} cminfo the course module state data.\n * @returns {Object}\n */\n cmCompletion(state, cminfo) {\n const data = {\n statename: '',\n state: 'NaN',\n };\n if (cminfo.completionstate !== undefined) {\n data.state = cminfo.completionstate;\n data.hasstate = true;\n const statename = this.COMPLETIONS[cminfo.completionstate] ?? 'NaN';\n data[`is${statename}`] = true;\n }\n return data;\n }\n\n /**\n * Return a sorted list of all sections and cms items in the state.\n *\n * @param {Object} state the current state.\n * @returns {Array} all sections and cms items in the state.\n */\n allItemsArray(state) {\n const items = [];\n const sectionlist = state.course.sectionlist ?? [];\n // Add sections.\n sectionlist.forEach(sectionid => {\n const sectioninfo = state.section.get(sectionid);\n items.push({type: 'section', id: sectioninfo.id, url: sectioninfo.sectionurl});\n // Add cms.\n const cmlist = sectioninfo.cmlist ?? [];\n cmlist.forEach(cmid => {\n const cminfo = state.cm.get(cmid);\n items.push({type: 'cm', id: cminfo.id, url: cminfo.url});\n });\n });\n return items;\n }\n}\n"],"names":["constructor","reactive","COMPLETIONS","course","state","data","sections","editmode","this","isEditing","highlighted","sectionlist","forEach","sectionid","sectioninfo","section","get","push","hassections","length","cms","cmlist","cmid","cminfo","cm","hascms","isactive","cmDraggableData","nextcmid","currentindex","indexOf","id","undefined","type","name","sectionDraggableData","number","fileDraggableData","dataTransfer","files","file","cmCompletion","statename","completionstate","hasstate","allItemsArray","items","url","sectionurl"],"mappings":";;;;;;;;;;MA+BIA,YAAYC,eACHA,SAAWA,cAKXC,YAAc,CAAC,aAAc,WAAY,WAAY,QAS9DC,OAAOC,6DAEGC,KAAO,CACTC,SAAU,GACVC,SAAUC,KAAKP,SAASQ,UACxBC,0CAAaN,MAAMD,OAAOO,mEAAe,yCAEzBN,MAAMD,OAAOQ,mEAAe,IACpCC,SAAQC,yCACVC,uCAAcV,MAAMW,QAAQC,IAAIH,4DAAc,GAC9CE,QAAUP,KAAKO,QAAQX,MAAOU,aACpCT,KAAKC,SAASW,KAAKF,YAEvBV,KAAKa,YAAuC,GAAxBb,KAAKC,SAASa,OAE3Bd,KAUXU,QAAQX,MAAOU,kEACLC,QAAU,IACTD,YACHJ,2CAAaN,MAAMD,OAAOO,qEAAe,GACzCU,IAAK,uCAEMN,YAAYO,0DAAU,IAC9BT,SAAQU,aACLC,OAASnB,MAAMoB,GAAGR,IAAIM,MACtBE,GAAKhB,KAAKgB,GAAGpB,MAAOmB,QAC1BR,QAAQK,IAAIH,KAAKO,OAErBT,QAAQU,OAAgC,GAAtBV,QAAQK,IAAID,OAEvBJ,QAUXS,GAAGpB,MAAOmB,cACK,IACJA,OACHG,UAAU,GAelBC,gBAAgBvB,MAAOkB,YACbC,OAASnB,MAAMoB,GAAGR,IAAIM,UACvBC,cACM,SAIPK,eACEb,QAAUX,MAAMW,QAAQC,IAAIO,OAAOV,WACnCgB,aAAed,MAAAA,eAAAA,QAASM,OAAOS,QAAQP,OAAOQ,gBAC/BC,IAAjBH,eACAD,SAAWb,MAAAA,eAAAA,QAASM,OAAOQ,aAAe,IAGvC,CACHI,KAAM,KACNF,GAAIR,OAAOQ,GACXG,KAAMX,OAAOW,KACbrB,UAAWU,OAAOV,UAClBe,SAAAA,UAcRO,qBAAqB/B,MAAOS,iBAClBC,YAAcV,MAAMW,QAAQC,IAAIH,kBACjCC,YAGE,CACHmB,KAAM,UACNF,GAAIjB,YAAYiB,GAChBG,KAAMpB,YAAYoB,KAClBE,OAAQtB,YAAYsB,QANb,KAmBfC,kBAAkBjC,MAAOkC,4CACfC,MAAQ,sCAEVD,aAAaC,gEAAOpB,QAAS,GAC7BmB,aAAaC,MAAM3B,SAAQ4B,OACvBD,MAAMtB,KAAKuB,SAGZ,CACHP,KAAM,QACNM,MAAAA,OAWRE,aAAarC,MAAOmB,cACVlB,KAAO,CACTqC,UAAW,GACXtC,MAAO,eAEoB4B,IAA3BT,OAAOoB,gBAA+B,2BACtCtC,KAAKD,MAAQmB,OAAOoB,gBACpBtC,KAAKuC,UAAW,QACVF,wCAAYlC,KAAKN,YAAYqB,OAAOoB,wEAAoB,MAC9DtC,iBAAUqC,aAAe,SAEtBrC,KASXwC,cAAczC,wCACJ0C,MAAQ,yCACM1C,MAAMD,OAAOQ,qEAAe,IAEpCC,SAAQC,2CACVC,YAAcV,MAAMW,QAAQC,IAAIH,WACtCiC,MAAM7B,KAAK,CAACgB,KAAM,UAAWF,GAAIjB,YAAYiB,GAAIgB,IAAKjC,YAAYkC,2CAEnDlC,YAAYO,4DAAU,IAC9BT,SAAQU,aACLC,OAASnB,MAAMoB,GAAGR,IAAIM,MAC5BwB,MAAM7B,KAAK,CAACgB,KAAM,KAAMF,GAAIR,OAAOQ,GAAIgB,IAAKxB,OAAOwB,YAGpDD"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/fileuploader.min.js b/course/format/amd/build/local/courseeditor/fileuploader.min.js new file mode 100644 index 00000000000..e74631786a9 --- /dev/null +++ b/course/format/amd/build/local/courseeditor/fileuploader.min.js @@ -0,0 +1,3 @@ +define("core_courseformat/local/courseeditor/fileuploader",["exports","core/config","core/modal_factory","core/modal_events","core/templates","core/normalise","core/prefetch","core/str","core_courseformat/courseeditor","core/process_monitor","core/utils"],(function(_exports,_config,_modal_factory,_modal_events,_templates,_normalise,_prefetch,_str,_courseeditor,_process_monitor,_utils){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.uploadFilesToCourse=void 0,_config=_interopRequireDefault(_config),_modal_factory=_interopRequireDefault(_modal_factory),_modal_events=_interopRequireDefault(_modal_events),_templates=_interopRequireDefault(_templates);const UPLOADURL=_config.default.wwwroot+"/course/dndupload.php";let uploadQueue=null,handlerManagers={},courseUpdates=new Map,errors=null;(0,_prefetch.prefetchStrings)("moodle",["addresourceoractivity","upload"]),(0,_prefetch.prefetchStrings)("core_error",["dndmaxbytes","dndread","dndupload","dndunkownfile"]);class FileUploader{constructor(courseId,sectionId,sectionNum,fileInfo,handler){this.courseId=courseId,this.sectionId=sectionId,this.sectionNum=sectionNum,this.fileInfo=fileInfo,this.handler=handler}execute(process){const fileInfo=this.fileInfo,xhr=this._createXhrRequest(process),formData=this._createUploadFormData(),reader=new FileReader;reader.onload=function(){xhr.open("POST",UPLOADURL,!0),xhr.send(formData)},reader.onerror=function(){process.setError(errors.dndread)},fileInfo.size>0?reader.readAsText(fileInfo.slice(0,5)):reader.readAsText(fileInfo)}getExecutionFunction(){return this.execute.bind(this)}_createXhrRequest(process){const xhr=new XMLHttpRequest;return xhr.upload.addEventListener("progress",(event=>{if(event.lengthComputable){const percent=Math.round(100*event.loaded/event.total);process.setPercentage(percent)}}),!1),xhr.onreadystatechange=()=>{if(1==xhr.readyState&&process.setPercentage(1),4==xhr.readyState)if(200==xhr.status){var result=JSON.parse(xhr.responseText);result&&0==result.error?this._finishProcess(process):process.setError(result.error)}else process.setError(errors.dndupload)},xhr}_createUploadFormData(){const formData=new FormData;try{formData.append("repo_upload_file",this.fileInfo)}catch(error){throw Error(error.dndread)}return formData.append("sesskey",_config.default.sesskey),formData.append("course",this.courseId),formData.append("section",this.sectionNum),formData.append("module",this.handler.module),formData.append("type","Files"),formData}_finishProcess(process){!function(courseId,sectionId){let refresh=courseUpdates.get(courseId);refresh||(refresh=new Set);refresh.add(sectionId),courseUpdates.set(courseId,refresh),refreshCourseEditors()}(this.courseId,this.sectionId),process.setPercentage(100),process.finish()}}class HandlerManager{constructor(courseId){var _this$courseEditor$ge,_this$courseEditor$ge2;if(_defineProperty(this,"lastHandlers",{}),_defineProperty(this,"allHandlers",null),this.courseId=courseId,this.lastUploadId=0,this.courseEditor=(0,_courseeditor.getCourseEditor)(courseId),!this.courseEditor)throw Error("Unkown course editor");this.maxbytes=null!==(_this$courseEditor$ge=null===(_this$courseEditor$ge2=this.courseEditor.get("course"))||void 0===_this$courseEditor$ge2?void 0:_this$courseEditor$ge2.maxbytes)&&void 0!==_this$courseEditor$ge?_this$courseEditor$ge:0}async loadHandlers(){this.allHandlers=await this.courseEditor.getFileHandlersPromise()}getFileExtension(fileInfo){let extension="";const dotpos=fileInfo.name.lastIndexOf(".");return-1!=dotpos&&(extension=fileInfo.name.substring(dotpos+1,fileInfo.name.length).toLowerCase()),extension}validateFile(fileInfo){if(-1!==this.maxbytes&&fileInfo.size>this.maxbytes)throw Error(errors.dndmaxbytes)}filterHandlers(fileInfo){const extension=this.getFileExtension(fileInfo);return this.allHandlers.filter((handler=>"*"==handler.extension||handler.extension==extension))}async getFileHandler(fileInfo){const fileHandlers=this.filterHandlers(fileInfo);if(0==fileHandlers.length)throw Error(errors.dndunkownfile);let fileHandler=null;return fileHandler=1==fileHandlers.length?fileHandlers[0]:await this.askHandlerToUser(fileHandlers,fileInfo),fileHandler}async askHandlerToUser(fileHandlers,fileInfo){var _this$lastHandlers$ex;const extension=this.getFileExtension(fileInfo),modalParams={title:(0,_str.get_string)("addresourceoractivity","moodle"),body:_templates.default.render("core_courseformat/fileuploader",this.getModalData(fileHandlers,fileInfo,null!==(_this$lastHandlers$ex=this.lastHandlers[extension])&&void 0!==_this$lastHandlers$ex?_this$lastHandlers$ex:null)),type:_modal_factory.default.types.SAVE_CANCEL,saveButtonText:(0,_str.get_string)("upload","moodle")},modal=await this.modalBodyRenderedPromise(modalParams),selectedHandler=await this.modalUserAnswerPromise(modal,fileHandlers);return null===selectedHandler?null:(this.lastHandlers[extension]=selectedHandler.module,selectedHandler)}getModalData(fileHandlers,fileInfo,defaultModule){const data={filename:fileInfo.name,uploadid:++this.lastUploadId,handlers:[]};let hasDefault=!1;if(fileHandlers.forEach(((handler,index)=>{const isDefault=defaultModule==handler.module;data.handlers.push({...handler,selected:isDefault,labelid:"fileuploader_".concat(data.uploadid),value:index}),hasDefault=hasDefault||isDefault})),!hasDefault&&data.handlers.length>0){const lastHandler=data.handlers.pop();lastHandler.selected=!0,data.handlers.push(lastHandler)}return data}modalUserAnswerPromise(modal,fileHandlers){const modalBody=(0,_normalise.getFirst)(modal.getBody());return new Promise(((resolve,reject)=>{modal.getRoot().on(_modal_events.default.save,(event=>{const index=modalBody.querySelector("input:checked").value;event.preventDefault(),modal.destroy(),fileHandlers[index]||reject("Invalid handler selected"),resolve(fileHandlers[index])})),modal.getRoot().on(_modal_events.default.cancel,(()=>{resolve(null)}))}))}modalBodyRenderedPromise(modalParams){return new Promise(((resolve,reject)=>{_modal_factory.default.create(modalParams).then((modal=>{modal.setRemoveOnClose(!0),modal.getRoot().on(_modal_events.default.bodyRendered,(()=>{resolve(modal)})),void 0!==modalParams.saveButtonText&&modal.setSaveButtonText(modalParams.saveButtonText),modal.show()})).catch((()=>{reject("Cannot load modal content")}))}))}}const refreshCourseEditors=(0,_utils.debounce)((()=>{const refreshes=courseUpdates;courseUpdates=new Map,refreshes.forEach(((sectionIds,courseId)=>{const courseEditor=(0,_courseeditor.getCourseEditor)(courseId);courseEditor&&courseEditor.dispatch("sectionState",[...sectionIds])}))}),500);const queueFileUpload=async function(courseId,sectionId,sectionNum,fileInfo,handlerManager){let handler;uploadQueue=await _process_monitor.processMonitor.createProcessQueue();try{handlerManager.validateFile(fileInfo),handler=await handlerManager.getFileHandler(fileInfo)}catch(error){return void uploadQueue.addError(fileInfo.name,error.message)}if(!handler)return;const fileProcessor=new FileUploader(courseId,sectionId,sectionNum,fileInfo,handler);uploadQueue.addPending(fileInfo.name,fileProcessor.getExecutionFunction())};_exports.uploadFilesToCourse=async function(courseId,sectionId,sectionNum,files){let handlerManager;try{handlerManager=await async function(courseId){if(void 0!==handlerManagers[courseId])return handlerManagers[courseId];try{const handlerManager=new HandlerManager(courseId);await handlerManager.loadHandlers(),handlerManagers[courseId]=handlerManager}catch(error){throw error}return handlerManagers[courseId]}(courseId),await async function(courseId){var _courseEditor$get$max,_courseEditor$get;if(null!==errors)return;const maxbytestext=null!==(_courseEditor$get$max=null===(_courseEditor$get=(0,_courseeditor.getCourseEditor)(courseId).get("course"))||void 0===_courseEditor$get?void 0:_courseEditor$get.maxbytestext)&&void 0!==_courseEditor$get$max?_courseEditor$get$max:"0";errors={};const allStrings=[{key:"dndmaxbytes",component:"core_error",param:{size:maxbytestext}},{key:"dndread",component:"core_error"},{key:"dndupload",component:"core_error"},{key:"dndunkownfile",component:"core_error"}];window.console.log(allStrings);const loadedStrings=await(0,_str.get_strings)(allStrings);allStrings.forEach(((_ref,index)=>{let{key:key}=_ref;errors[key]=loadedStrings[index]}))}(courseId)}catch(error){throw error}for(let index=0;index.\n\n/**\n * The course file uploader.\n *\n * This module is used to upload files directly into the course.\n *\n * @module core_courseformat/local/courseeditor/fileuploader\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * @typedef {Object} Handler\n * @property {String} extension the handled extension or * for any\n * @property {String} message the handler message\n * @property {String} module the module name\n */\n\nimport Config from 'core/config';\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\nimport Templates from 'core/templates';\nimport {getFirst} from 'core/normalise';\nimport {prefetchStrings} from 'core/prefetch';\nimport {get_string as getString, get_strings as getStrings} from 'core/str';\nimport {getCourseEditor} from 'core_courseformat/courseeditor';\nimport {processMonitor} from 'core/process_monitor';\nimport {debounce} from 'core/utils';\n\n// Uploading url.\nconst UPLOADURL = Config.wwwroot + '/course/dndupload.php';\nconst DEBOUNCETIMER = 500;\nconst USERCANIGNOREFILESIZELIMITS = -1;\n\n/** @var {ProcessQueue} uploadQueue the internal uploadQueue instance. */\nlet uploadQueue = null;\n/** @var {Object} handlerManagers the courseId indexed loaded handler managers. */\nlet handlerManagers = {};\n/** @var {Map} courseUpdates the pending course sections updates. */\nlet courseUpdates = new Map();\n/** @var {Object} errors the error messages. */\nlet errors = null;\n\n// Load global strings.\nprefetchStrings('moodle', ['addresourceoractivity', 'upload']);\nprefetchStrings('core_error', ['dndmaxbytes', 'dndread', 'dndupload', 'dndunkownfile']);\n\n/**\n * Class to upload a file into the course.\n * @private\n */\nclass FileUploader {\n /**\n * Class constructor.\n *\n * @param {number} courseId the course id\n * @param {number} sectionId the section id\n * @param {number} sectionNum the section number\n * @param {File} fileInfo the file information object\n * @param {Handler} handler the file selected file handler\n */\n constructor(courseId, sectionId, sectionNum, fileInfo, handler) {\n this.courseId = courseId;\n this.sectionId = sectionId;\n this.sectionNum = sectionNum;\n this.fileInfo = fileInfo;\n this.handler = handler;\n }\n\n /**\n * Execute the file upload and update the state in the given process.\n *\n * @param {LoadingProcess} process the process to store the upload result\n */\n execute(process) {\n const fileInfo = this.fileInfo;\n const xhr = this._createXhrRequest(process);\n const formData = this._createUploadFormData();\n\n // Try reading the file to check it is not a folder, before sending it to the server.\n const reader = new FileReader();\n reader.onload = function() {\n // File was read OK - send it to the server.\n xhr.open(\"POST\", UPLOADURL, true);\n xhr.send(formData);\n };\n reader.onerror = function() {\n // Unable to read the file (it is probably a folder) - display an error message.\n process.setError(errors.dndread);\n };\n if (fileInfo.size > 0) {\n // If this is a non-empty file, try reading the first few bytes.\n // This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files.\n reader.readAsText(fileInfo.slice(0, 5));\n } else {\n // If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(),\n // instead of reader.onerror().\n // So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected).\n reader.readAsText(fileInfo);\n }\n }\n\n /**\n * Returns the bind version of execute function.\n *\n * This method is used to queue the process into a ProcessQueue instance.\n *\n * @returns {Function} the bind function to execute the process\n */\n getExecutionFunction() {\n return this.execute.bind(this);\n }\n\n /**\n * Generate a upload XHR file request.\n *\n * @param {LoadingProcess} process the current process\n * @return {XMLHttpRequest} the XHR request\n */\n _createXhrRequest(process) {\n const xhr = new XMLHttpRequest();\n // Update the progress bar as the file is uploaded.\n xhr.upload.addEventListener(\n 'progress',\n (event) => {\n if (event.lengthComputable) {\n const percent = Math.round((event.loaded * 100) / event.total);\n process.setPercentage(percent);\n }\n },\n false\n );\n // Wait for the AJAX call to complete.\n xhr.onreadystatechange = () => {\n if (xhr.readyState == 1) {\n // Add a 1% just to indicate that it is uploading.\n process.setPercentage(1);\n }\n // State 4 is DONE. Otherwise the connection is still ongoing.\n if (xhr.readyState != 4) {\n return;\n }\n if (xhr.status == 200) {\n var result = JSON.parse(xhr.responseText);\n if (result && result.error == 0) {\n // All OK.\n this._finishProcess(process);\n } else {\n process.setError(result.error);\n }\n } else {\n process.setError(errors.dndupload);\n }\n };\n return xhr;\n }\n\n /**\n * Upload a file into the course.\n *\n * @return {FormData|null} the new form data object\n */\n _createUploadFormData() {\n const formData = new FormData();\n try {\n formData.append('repo_upload_file', this.fileInfo);\n } catch (error) {\n throw Error(error.dndread);\n }\n formData.append('sesskey', Config.sesskey);\n formData.append('course', this.courseId);\n formData.append('section', this.sectionNum);\n formData.append('module', this.handler.module);\n formData.append('type', 'Files');\n return formData;\n }\n\n /**\n * Finishes the current process.\n * @param {LoadingProcess} process the process\n */\n _finishProcess(process) {\n addRefreshSection(this.courseId, this.sectionId);\n process.setPercentage(100);\n process.finish();\n }\n}\n\n/**\n * The file handler manager class.\n *\n * @private\n */\nclass HandlerManager {\n\n /** @var {Object} lastHandlers the last handlers selected per each file extension. */\n lastHandlers = {};\n\n /** @var {Handler[]|null} allHandlers all the available handlers. */\n allHandlers = null;\n\n /**\n * Class constructor.\n *\n * @param {Number} courseId\n */\n constructor(courseId) {\n this.courseId = courseId;\n this.lastUploadId = 0;\n this.courseEditor = getCourseEditor(courseId);\n if (!this.courseEditor) {\n throw Error('Unkown course editor');\n }\n this.maxbytes = this.courseEditor.get('course')?.maxbytes ?? 0;\n }\n\n /**\n * Load the course file handlers.\n */\n async loadHandlers() {\n this.allHandlers = await this.courseEditor.getFileHandlersPromise();\n }\n\n /**\n * Extract the file extension from a fileInfo.\n *\n * @param {File} fileInfo\n * @returns {String} the file extension or an empty string.\n */\n getFileExtension(fileInfo) {\n let extension = '';\n const dotpos = fileInfo.name.lastIndexOf('.');\n if (dotpos != -1) {\n extension = fileInfo.name.substring(dotpos + 1, fileInfo.name.length).toLowerCase();\n }\n return extension;\n }\n\n /**\n * Check if the file is valid.\n *\n * @param {File} fileInfo the file info\n */\n validateFile(fileInfo) {\n if (this.maxbytes !== USERCANIGNOREFILESIZELIMITS && fileInfo.size > this.maxbytes) {\n throw Error(errors.dndmaxbytes);\n }\n }\n\n /**\n * Get the file handlers of an specific file.\n *\n * @param {File} fileInfo the file indo\n * @return {Array} Array of handlers\n */\n filterHandlers(fileInfo) {\n const extension = this.getFileExtension(fileInfo);\n return this.allHandlers.filter(handler => handler.extension == '*' || handler.extension == extension);\n }\n\n /**\n * Get the Handler to upload a specific file.\n *\n * It will ask the used if more than one handler is available.\n *\n * @param {File} fileInfo the file info\n * @returns {Promise} the selected handler or null if the user cancel\n */\n async getFileHandler(fileInfo) {\n const fileHandlers = this.filterHandlers(fileInfo);\n if (fileHandlers.length == 0) {\n throw Error(errors.dndunkownfile);\n }\n let fileHandler = null;\n if (fileHandlers.length == 1) {\n fileHandler = fileHandlers[0];\n } else {\n fileHandler = await this.askHandlerToUser(fileHandlers, fileInfo);\n }\n return fileHandler;\n }\n\n /**\n * Ask the user to select a specific handler.\n *\n * @param {Handler[]} fileHandlers\n * @param {File} fileInfo the file info\n * @return {Promise} the selected handler\n */\n async askHandlerToUser(fileHandlers, fileInfo) {\n const extension = this.getFileExtension(fileInfo);\n // Build the modal parameters from the event data.\n const modalParams = {\n title: getString('addresourceoractivity', 'moodle'),\n body: Templates.render(\n 'core_courseformat/fileuploader',\n this.getModalData(\n fileHandlers,\n fileInfo,\n this.lastHandlers[extension] ?? null\n )\n ),\n type: ModalFactory.types.SAVE_CANCEL,\n saveButtonText: getString('upload', 'moodle'),\n };\n // Create the modal.\n const modal = await this.modalBodyRenderedPromise(modalParams);\n const selectedHandler = await this.modalUserAnswerPromise(modal, fileHandlers);\n // Cancel action.\n if (selectedHandler === null) {\n return null;\n }\n // Save last selected handler.\n this.lastHandlers[extension] = selectedHandler.module;\n return selectedHandler;\n }\n\n /**\n * Generated the modal template data.\n *\n * @param {Handler[]} fileHandlers\n * @param {File} fileInfo the file info\n * @param {String|null} defaultModule the default module if any\n * @return {Object} the modal template data.\n */\n getModalData(fileHandlers, fileInfo, defaultModule) {\n const data = {\n filename: fileInfo.name,\n uploadid: ++this.lastUploadId,\n handlers: [],\n };\n let hasDefault = false;\n fileHandlers.forEach((handler, index) => {\n const isDefault = (defaultModule == handler.module);\n data.handlers.push({\n ...handler,\n selected: isDefault,\n labelid: `fileuploader_${data.uploadid}`,\n value: index,\n });\n hasDefault = hasDefault || isDefault;\n });\n if (!hasDefault && data.handlers.length > 0) {\n const lastHandler = data.handlers.pop();\n lastHandler.selected = true;\n data.handlers.push(lastHandler);\n }\n return data;\n }\n\n /**\n * Get the user handler choice.\n *\n * Wait for the user answer in the modal and resolve with the selected index.\n *\n * @param {Modal} modal the modal instance\n * @param {Handler[]} fileHandlers the availabvle file handlers\n * @return {Promise} with the option selected by the user.\n */\n modalUserAnswerPromise(modal, fileHandlers) {\n const modalBody = getFirst(modal.getBody());\n return new Promise((resolve, reject) => {\n modal.getRoot().on(\n ModalEvents.save,\n event => {\n // Get the selected option.\n const index = modalBody.querySelector('input:checked').value;\n event.preventDefault();\n modal.destroy();\n if (!fileHandlers[index]) {\n reject('Invalid handler selected');\n }\n resolve(fileHandlers[index]);\n\n }\n );\n modal.getRoot().on(\n ModalEvents.cancel,\n () => {\n resolve(null);\n }\n );\n });\n }\n\n /**\n * Create a new modal and return a Promise to the body rendered.\n *\n * @param {Object} modalParams the modal params\n * @returns {Promise} the modal body rendered promise\n */\n modalBodyRenderedPromise(modalParams) {\n return new Promise((resolve, reject) => {\n ModalFactory.create(modalParams).then((modal) => {\n modal.setRemoveOnClose(true);\n // Handle body loading event.\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n resolve(modal);\n });\n // Configure some extra modal params.\n if (modalParams.saveButtonText !== undefined) {\n modal.setSaveButtonText(modalParams.saveButtonText);\n }\n modal.show();\n return;\n }).catch(() => {\n reject(`Cannot load modal content`);\n });\n });\n }\n}\n\n/**\n * Add a section to refresh.\n *\n * @param {number} courseId the course id\n * @param {number} sectionId the seciton id\n */\nfunction addRefreshSection(courseId, sectionId) {\n let refresh = courseUpdates.get(courseId);\n if (!refresh) {\n refresh = new Set();\n }\n refresh.add(sectionId);\n courseUpdates.set(courseId, refresh);\n refreshCourseEditors();\n}\n\n/**\n * Debounced processing all pending course refreshes.\n * @private\n */\nconst refreshCourseEditors = debounce(\n () => {\n const refreshes = courseUpdates;\n courseUpdates = new Map();\n refreshes.forEach((sectionIds, courseId) => {\n const courseEditor = getCourseEditor(courseId);\n if (!courseEditor) {\n return;\n }\n courseEditor.dispatch('sectionState', [...sectionIds]);\n });\n },\n DEBOUNCETIMER\n);\n\n/**\n * Load and return the course handler manager instance.\n *\n * @param {Number} courseId the course Id to load\n * @returns {Promise} promise of the the loaded handleManager\n */\nasync function loadCourseHandlerManager(courseId) {\n if (handlerManagers[courseId] !== undefined) {\n return handlerManagers[courseId];\n }\n try {\n const handlerManager = new HandlerManager(courseId);\n await handlerManager.loadHandlers();\n handlerManagers[courseId] = handlerManager;\n } catch (error) {\n throw error;\n }\n return handlerManagers[courseId];\n}\n\n/**\n * Load all the erros messages at once in the module \"errors\" variable.\n * @param {Number} courseId the course id\n */\nasync function loadErrorStrings(courseId) {\n if (errors !== null) {\n return;\n }\n const courseEditor = getCourseEditor(courseId);\n const maxbytestext = courseEditor.get('course')?.maxbytestext ?? '0';\n\n errors = {};\n const allStrings = [\n {key: 'dndmaxbytes', component: 'core_error', param: {size: maxbytestext}},\n {key: 'dndread', component: 'core_error'},\n {key: 'dndupload', component: 'core_error'},\n {key: 'dndunkownfile', component: 'core_error'},\n ];\n window.console.log(allStrings);\n const loadedStrings = await getStrings(allStrings);\n allStrings.forEach(({key}, index) => {\n errors[key] = loadedStrings[index];\n });\n}\n\n/**\n * Start a batch file uploading into the course.\n *\n * @private\n * @param {number} courseId the course id.\n * @param {number} sectionId the section id.\n * @param {number} sectionNum the section number.\n * @param {File} fileInfo the file information object\n * @param {HandlerManager} handlerManager the course handler manager\n */\nconst queueFileUpload = async function(courseId, sectionId, sectionNum, fileInfo, handlerManager) {\n let handler;\n uploadQueue = await processMonitor.createProcessQueue();\n try {\n handlerManager.validateFile(fileInfo);\n handler = await handlerManager.getFileHandler(fileInfo);\n } catch (error) {\n uploadQueue.addError(fileInfo.name, error.message);\n return;\n }\n // If we don't have a handler means the user cancel the upload.\n if (!handler) {\n return;\n }\n const fileProcessor = new FileUploader(courseId, sectionId, sectionNum, fileInfo, handler);\n uploadQueue.addPending(fileInfo.name, fileProcessor.getExecutionFunction());\n};\n\n/**\n * Upload a file to the course.\n *\n * This method will show any necesary modal to handle the request.\n *\n * @param {number} courseId the course id\n * @param {number} sectionId the section id\n * @param {number} sectionNum the section number\n * @param {Array} files and array of files\n */\nexport const uploadFilesToCourse = async function(courseId, sectionId, sectionNum, files) {\n // Get the course handlers.\n let handlerManager;\n try {\n handlerManager = await loadCourseHandlerManager(courseId);\n await loadErrorStrings(courseId);\n } catch (error) {\n throw error;\n }\n for (let index = 0; index < files.length; index++) {\n const fileInfo = files[index];\n await queueFileUpload(courseId, sectionId, sectionNum, fileInfo, handlerManager);\n }\n};\n"],"names":["UPLOADURL","Config","wwwroot","uploadQueue","handlerManagers","courseUpdates","Map","errors","FileUploader","constructor","courseId","sectionId","sectionNum","fileInfo","handler","execute","process","this","xhr","_createXhrRequest","formData","_createUploadFormData","reader","FileReader","onload","open","send","onerror","setError","dndread","size","readAsText","slice","getExecutionFunction","bind","XMLHttpRequest","upload","addEventListener","event","lengthComputable","percent","Math","round","loaded","total","setPercentage","onreadystatechange","readyState","status","result","JSON","parse","responseText","error","_finishProcess","dndupload","FormData","append","Error","sesskey","module","refresh","get","Set","add","set","refreshCourseEditors","addRefreshSection","finish","HandlerManager","lastUploadId","courseEditor","maxbytes","_this$courseEditor$ge2","allHandlers","getFileHandlersPromise","getFileExtension","extension","dotpos","name","lastIndexOf","substring","length","toLowerCase","validateFile","dndmaxbytes","filterHandlers","filter","fileHandlers","dndunkownfile","fileHandler","askHandlerToUser","modalParams","title","body","Templates","render","getModalData","lastHandlers","type","ModalFactory","types","SAVE_CANCEL","saveButtonText","modal","modalBodyRenderedPromise","selectedHandler","modalUserAnswerPromise","defaultModule","data","filename","uploadid","handlers","hasDefault","forEach","index","isDefault","push","selected","labelid","value","lastHandler","pop","modalBody","getBody","Promise","resolve","reject","getRoot","on","ModalEvents","save","querySelector","preventDefault","destroy","cancel","create","then","setRemoveOnClose","bodyRendered","undefined","setSaveButtonText","show","catch","refreshes","sectionIds","dispatch","queueFileUpload","async","handlerManager","processMonitor","createProcessQueue","getFileHandler","addError","message","fileProcessor","addPending","files","loadHandlers","loadCourseHandlerManager","maxbytestext","_courseEditor$get","allStrings","key","component","param","window","console","log","loadedStrings","loadErrorStrings"],"mappings":"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"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/section.min.js b/course/format/amd/build/local/courseindex/section.min.js index e939d192d68..7c8581fd735 100644 --- a/course/format/amd/build/local/courseindex/section.min.js +++ b/course/format/amd/build/local/courseindex/section.min.js @@ -8,6 +8,6 @@ define("core_courseformat/local/courseindex/section",["exports","core_courseform * @class core_courseformat/local/courseindex/section * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_sectiontitle=_interopRequireDefault(_sectiontitle),_dndsection=_interopRequireDefault(_dndsection);class Component extends _dndsection.default{create(){this.name="courseindex_section",this.selectors={SECTION_ITEM:"[data-for='section_item']",SECTION_TITLE:"[data-for='section_title']",CM_LAST:'[data-for="cm"]:last-child'},this.classes={SECTIONHIDDEN:"dimmed",SECTIONCURRENT:"current",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem"},this.id=this.element.dataset.id,this.isPageItem=!1}static init(target,selectors){return new this({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configState(state);const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(this.reactive.isEditing&&this.reactive.supportComponents){const titleitem=new _sectiontitle.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(titleitem)}const section=state.section.get(this.id);window.location.href==section.sectionurl.replace(/&/g,"&")&&(this.reactive.dispatch("setPageItem","section",this.id),sectionItem.scrollIntoView())}getWatchers(){return[{watch:"section[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}getLastCm(){return this.getElement(this.selectors.CM_LAST)}_refreshSection(_ref){var _element$hasrestricti,_element$dragging,_element$locked;let{element:element}=_ref;const sectionItem=this.getElement(this.selectors.SECTION_ITEM);sectionItem.classList.toggle(this.classes.SECTIONHIDDEN,!element.visible),sectionItem.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hasrestricti=element.hasrestrictions)&&void 0!==_element$hasrestricti&&_element$hasrestricti),this.element.classList.toggle(this.classes.SECTIONCURRENT,element.current),this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked,this.getElement(this.selectors.SECTION_TITLE).innerHTML=element.title}_refreshPageItem(_ref2){var _element$pageItem,_this$pageItem;let{element:element,state:state}=_ref2;if(!element.pageItem)return;if(element.pageItem.sectionId!==this.id&&this.isPageItem)return this.pageItem=!1,void this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);var _element$pageItem2;!state.section.get(this.id).indexcollapsed||null!==(_element$pageItem=element.pageItem)&&void 0!==_element$pageItem&&_element$pageItem.isStatic?this.pageItem="section"==element.pageItem.type&&element.pageItem.id==this.id:this.pageItem=(null===(_element$pageItem2=element.pageItem)||void 0===_element$pageItem2?void 0:_element$pageItem2.sectionId)==this.id;this.getElement(this.selectors.SECTION_ITEM).classList.toggle(this.classes.PAGEITEM,null!==(_this$pageItem=this.pageItem)&&void 0!==_this$pageItem&&_this$pageItem),this.pageItem&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}}return _exports.default=Component,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_sectiontitle=_interopRequireDefault(_sectiontitle),_dndsection=_interopRequireDefault(_dndsection);class Component extends _dndsection.default{create(){this.name="courseindex_section",this.selectors={SECTION_ITEM:"[data-for='section_item']",SECTION_TITLE:"[data-for='section_title']",CM_LAST:'[data-for="cm"]:last-child'},this.classes={SECTIONHIDDEN:"dimmed",SECTIONCURRENT:"current",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",OVERLAYBORDERS:"overlay-preview-borders"},this.id=this.element.dataset.id,this.isPageItem=!1}static init(target,selectors){return new this({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configState(state);const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(this.reactive.isEditing&&this.reactive.supportComponents){const titleitem=new _sectiontitle.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(titleitem)}const section=state.section.get(this.id);window.location.href==section.sectionurl.replace(/&/g,"&")&&(this.reactive.dispatch("setPageItem","section",this.id),sectionItem.scrollIntoView())}getWatchers(){return[{watch:"section[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}getLastCm(){return this.getElement(this.selectors.CM_LAST)}_refreshSection(_ref){var _element$hasrestricti,_element$dragging,_element$locked;let{element:element}=_ref;const sectionItem=this.getElement(this.selectors.SECTION_ITEM);sectionItem.classList.toggle(this.classes.SECTIONHIDDEN,!element.visible),sectionItem.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hasrestricti=element.hasrestrictions)&&void 0!==_element$hasrestricti&&_element$hasrestricti),this.element.classList.toggle(this.classes.SECTIONCURRENT,element.current),this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked,this.getElement(this.selectors.SECTION_TITLE).innerHTML=element.title}_refreshPageItem(_ref2){var _element$pageItem,_this$pageItem;let{element:element,state:state}=_ref2;if(!element.pageItem)return;if(element.pageItem.sectionId!==this.id&&this.isPageItem)return this.pageItem=!1,void this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);var _element$pageItem2;!state.section.get(this.id).indexcollapsed||null!==(_element$pageItem=element.pageItem)&&void 0!==_element$pageItem&&_element$pageItem.isStatic?this.pageItem="section"==element.pageItem.type&&element.pageItem.id==this.id:this.pageItem=(null===(_element$pageItem2=element.pageItem)||void 0===_element$pageItem2?void 0:_element$pageItem2.sectionId)==this.id;this.getElement(this.selectors.SECTION_ITEM).classList.toggle(this.classes.PAGEITEM,null!==(_this$pageItem=this.pageItem)&&void 0!==_this$pageItem&&_this$pageItem),this.pageItem&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async addOverlay(){this.element.classList.add(this.classes.OVERLAYBORDERS)}removeOverlay(){this.element.classList.remove(this.classes.OVERLAYBORDERS)}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=section.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/section.min.js.map b/course/format/amd/build/local/courseindex/section.min.js.map index 97f5aa2ac3c..906309fe3d0 100644 --- a/course/format/amd/build/local/courseindex/section.min.js.map +++ b/course/format/amd/build/local/courseindex/section.min.js.map @@ -1 +1 @@ -{"version":3,"file":"section.min.js","sources":["../../../src/local/courseindex/section.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 * Course index section component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/section\n * @class core_courseformat/local/courseindex/section\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport SectionTitle from 'core_courseformat/local/courseindex/sectiontitle';\nimport DndSection from 'core_courseformat/local/courseeditor/dndsection';\n\nexport default class Component extends DndSection {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_section';\n // Default query selectors.\n this.selectors = {\n SECTION_ITEM: `[data-for='section_item']`,\n SECTION_TITLE: `[data-for='section_title']`,\n CM_LAST: `[data-for=\"cm\"]:last-child`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n LOCKED: 'editinprogress',\n RESTRICTIONS: 'restrictions',\n PAGEITEM: 'pageitem',\n };\n\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n this.isPageItem = false;\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the initial state\n */\n stateReady(state) {\n this.configState(state);\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init the inner dragable element passing the full section as affected region.\n const titleitem = new SectionTitle({\n ...this,\n element: sectionItem,\n fullregion: this.element,\n });\n this.configDragDrop(titleitem);\n }\n // Check if the current url is the section url.\n const section = state.section.get(this.id);\n if (window.location.href == section.sectionurl.replace(/&/g, \"&\")) {\n this.reactive.dispatch('setPageItem', 'section', this.id);\n sectionItem.scrollIntoView();\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `section[${this.id}]:deleted`, handler: this.remove},\n {watch: `section[${this.id}]:updated`, handler: this._refreshSection},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n ];\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null}\n */\n getLastCm() {\n return this.getElement(this.selectors.CM_LAST);\n }\n\n /**\n * Update a course index section using the state information.\n *\n * @param {Object} param details the update details.\n * @param {Object} param.element the section element\n */\n _refreshSection({element}) {\n // Update classes.\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.SECTIONHIDDEN, !element.visible);\n sectionItem.classList.toggle(this.classes.RESTRICTIONS, element.hasrestrictions ?? false);\n this.element.classList.toggle(this.classes.SECTIONCURRENT, element.current);\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.locked = element.locked;\n // Update title.\n this.getElement(this.selectors.SECTION_TITLE).innerHTML = element.title;\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data.\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element, state}) {\n if (!element.pageItem) {\n return;\n }\n if (element.pageItem.sectionId !== this.id && this.isPageItem) {\n this.pageItem = false;\n this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);\n return;\n }\n const section = state.section.get(this.id);\n if (section.indexcollapsed && !element.pageItem?.isStatic) {\n this.pageItem = (element.pageItem?.sectionId == this.id);\n } else {\n this.pageItem = (element.pageItem.type == 'section' && element.pageItem.id == this.id);\n }\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.PAGEITEM, this.pageItem ?? false);\n if (this.pageItem && !this.reactive.isEditing) {\n this.element.scrollIntoView({block: \"nearest\"});\n }\n }\n}\n"],"names":["Component","DndSection","create","name","selectors","SECTION_ITEM","SECTION_TITLE","CM_LAST","classes","SECTIONHIDDEN","SECTIONCURRENT","LOCKED","RESTRICTIONS","PAGEITEM","id","this","element","dataset","isPageItem","target","document","getElementById","stateReady","state","configState","sectionItem","getElement","reactive","isEditing","supportComponents","titleitem","SectionTitle","fullregion","configDragDrop","section","get","window","location","href","sectionurl","replace","dispatch","scrollIntoView","getWatchers","watch","handler","remove","_refreshSection","_refreshPageItem","getLastCm","classList","toggle","visible","hasrestrictions","current","DRAGGING","dragging","locked","innerHTML","title","pageItem","sectionId","indexcollapsed","_element$pageItem","isStatic","type","block"],"mappings":";;;;;;;;;;+LA6BqBA,kBAAkBC,oBAKnCC,cAESC,KAAO,2BAEPC,UAAY,CACbC,yCACAC,2CACAC,2CAGCC,QAAU,CACXC,cAAe,SACfC,eAAgB,UAChBC,OAAQ,iBACRC,aAAc,eACdC,SAAU,iBAITC,GAAKC,KAAKC,QAAQC,QAAQH,QAC1BI,YAAa,cAUVC,OAAQf,kBACT,IAAIW,KAAK,CACZC,QAASI,SAASC,eAAeF,QACjCf,UAAAA,YASRkB,WAAWC,YACFC,YAAYD,aACXE,YAAcV,KAAKW,WAAWX,KAAKX,UAAUC,iBAE/CU,KAAKY,SAASC,WAAab,KAAKY,SAASE,kBAAmB,OAEtDC,UAAY,IAAIC,sBAAa,IAC5BhB,KACHC,QAASS,YACTO,WAAYjB,KAAKC,eAEhBiB,eAAeH,iBAGlBI,QAAUX,MAAMW,QAAQC,IAAIpB,KAAKD,IACnCsB,OAAOC,SAASC,MAAQJ,QAAQK,WAAWC,QAAQ,SAAU,YACxDb,SAASc,SAAS,cAAe,UAAW1B,KAAKD,IACtDW,YAAYiB,kBASpBC,oBACW,CACH,CAACC,wBAAkB7B,KAAKD,gBAAe+B,QAAS9B,KAAK+B,QACrD,CAACF,wBAAkB7B,KAAKD,gBAAe+B,QAAS9B,KAAKgC,iBACrD,CAACH,gCAAkCC,QAAS9B,KAAKiC,mBASzDC,mBACWlC,KAAKW,WAAWX,KAAKX,UAAUG,SAS1CwC,sFAAgB/B,QAACA,oBAEPS,YAAcV,KAAKW,WAAWX,KAAKX,UAAUC,cACnDoB,YAAYyB,UAAUC,OAAOpC,KAAKP,QAAQC,eAAgBO,QAAQoC,SAClE3B,YAAYyB,UAAUC,OAAOpC,KAAKP,QAAQI,2CAAcI,QAAQqC,8EAC3DrC,QAAQkC,UAAUC,OAAOpC,KAAKP,QAAQE,eAAgBM,QAAQsC,cAC9DtC,QAAQkC,UAAUC,OAAOpC,KAAKP,QAAQ+C,mCAAUvC,QAAQwC,+DACxDxC,QAAQkC,UAAUC,OAAOpC,KAAKP,QAAQG,+BAAQK,QAAQyC,yDACtDA,OAASzC,QAAQyC,YAEjB/B,WAAWX,KAAKX,UAAUE,eAAeoD,UAAY1C,QAAQ2C,MAUtEX,iEAAiBhC,QAACA,QAADO,MAAUA,iBAClBP,QAAQ4C,mBAGT5C,QAAQ4C,SAASC,YAAc9C,KAAKD,IAAMC,KAAKG,uBAC1C0C,UAAW,YACXlC,WAAWX,KAAKX,UAAUC,cAAc6C,UAAUJ,OAAO/B,KAAKP,QAAQK,kCAG/DU,MAAMW,QAAQC,IAAIpB,KAAKD,IAC3BgD,0CAAmB9C,QAAQ4C,uCAARG,kBAAkBC,cAGxCJ,SAAqC,WAAzB5C,QAAQ4C,SAASK,MAAqBjD,QAAQ4C,SAAS9C,IAAMC,KAAKD,QAF9E8C,qCAAY5C,QAAQ4C,iEAAUC,YAAa9C,KAAKD,GAIrCC,KAAKW,WAAWX,KAAKX,UAAUC,cACvC6C,UAAUC,OAAOpC,KAAKP,QAAQK,gCAAUE,KAAK6C,oDACrD7C,KAAK6C,WAAa7C,KAAKY,SAASC,gBAC3BZ,QAAQ0B,eAAe,CAACwB,MAAO"} \ No newline at end of file +{"version":3,"file":"section.min.js","sources":["../../../src/local/courseindex/section.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 * Course index section component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/section\n * @class core_courseformat/local/courseindex/section\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport SectionTitle from 'core_courseformat/local/courseindex/sectiontitle';\nimport DndSection from 'core_courseformat/local/courseeditor/dndsection';\n\nexport default class Component extends DndSection {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_section';\n // Default query selectors.\n this.selectors = {\n SECTION_ITEM: `[data-for='section_item']`,\n SECTION_TITLE: `[data-for='section_title']`,\n CM_LAST: `[data-for=\"cm\"]:last-child`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n LOCKED: 'editinprogress',\n RESTRICTIONS: 'restrictions',\n PAGEITEM: 'pageitem',\n OVERLAYBORDERS: 'overlay-preview-borders',\n };\n\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n this.isPageItem = false;\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the initial state\n */\n stateReady(state) {\n this.configState(state);\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init the inner dragable element passing the full section as affected region.\n const titleitem = new SectionTitle({\n ...this,\n element: sectionItem,\n fullregion: this.element,\n });\n this.configDragDrop(titleitem);\n }\n // Check if the current url is the section url.\n const section = state.section.get(this.id);\n if (window.location.href == section.sectionurl.replace(/&/g, \"&\")) {\n this.reactive.dispatch('setPageItem', 'section', this.id);\n sectionItem.scrollIntoView();\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `section[${this.id}]:deleted`, handler: this.remove},\n {watch: `section[${this.id}]:updated`, handler: this._refreshSection},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n ];\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null}\n */\n getLastCm() {\n return this.getElement(this.selectors.CM_LAST);\n }\n\n /**\n * Update a course index section using the state information.\n *\n * @param {Object} param details the update details.\n * @param {Object} param.element the section element\n */\n _refreshSection({element}) {\n // Update classes.\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.SECTIONHIDDEN, !element.visible);\n sectionItem.classList.toggle(this.classes.RESTRICTIONS, element.hasrestrictions ?? false);\n this.element.classList.toggle(this.classes.SECTIONCURRENT, element.current);\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.locked = element.locked;\n // Update title.\n this.getElement(this.selectors.SECTION_TITLE).innerHTML = element.title;\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data.\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element, state}) {\n if (!element.pageItem) {\n return;\n }\n if (element.pageItem.sectionId !== this.id && this.isPageItem) {\n this.pageItem = false;\n this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);\n return;\n }\n const section = state.section.get(this.id);\n if (section.indexcollapsed && !element.pageItem?.isStatic) {\n this.pageItem = (element.pageItem?.sectionId == this.id);\n } else {\n this.pageItem = (element.pageItem.type == 'section' && element.pageItem.id == this.id);\n }\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.PAGEITEM, this.pageItem ?? false);\n if (this.pageItem && !this.reactive.isEditing) {\n this.element.scrollIntoView({block: \"nearest\"});\n }\n }\n\n /**\n * Overridden version of the component addOverlay async method.\n *\n * The course index is not compatible with overlay elements.\n */\n async addOverlay() {\n this.element.classList.add(this.classes.OVERLAYBORDERS);\n }\n\n /**\n * Overridden version of the component removeOverlay.\n *\n * The course index is not compatible with overlay elements.\n */\n removeOverlay() {\n this.element.classList.remove(this.classes.OVERLAYBORDERS);\n }\n}\n"],"names":["Component","DndSection","create","name","selectors","SECTION_ITEM","SECTION_TITLE","CM_LAST","classes","SECTIONHIDDEN","SECTIONCURRENT","LOCKED","RESTRICTIONS","PAGEITEM","OVERLAYBORDERS","id","this","element","dataset","isPageItem","target","document","getElementById","stateReady","state","configState","sectionItem","getElement","reactive","isEditing","supportComponents","titleitem","SectionTitle","fullregion","configDragDrop","section","get","window","location","href","sectionurl","replace","dispatch","scrollIntoView","getWatchers","watch","handler","remove","_refreshSection","_refreshPageItem","getLastCm","classList","toggle","visible","hasrestrictions","current","DRAGGING","dragging","locked","innerHTML","title","pageItem","sectionId","indexcollapsed","_element$pageItem","isStatic","type","block","add","removeOverlay"],"mappings":";;;;;;;;;;+LA6BqBA,kBAAkBC,oBAKnCC,cAESC,KAAO,2BAEPC,UAAY,CACbC,yCACAC,2CACAC,2CAGCC,QAAU,CACXC,cAAe,SACfC,eAAgB,UAChBC,OAAQ,iBACRC,aAAc,eACdC,SAAU,WACVC,eAAgB,gCAIfC,GAAKC,KAAKC,QAAQC,QAAQH,QAC1BI,YAAa,cAUVC,OAAQhB,kBACT,IAAIY,KAAK,CACZC,QAASI,SAASC,eAAeF,QACjChB,UAAAA,YASRmB,WAAWC,YACFC,YAAYD,aACXE,YAAcV,KAAKW,WAAWX,KAAKZ,UAAUC,iBAE/CW,KAAKY,SAASC,WAAab,KAAKY,SAASE,kBAAmB,OAEtDC,UAAY,IAAIC,sBAAa,IAC5BhB,KACHC,QAASS,YACTO,WAAYjB,KAAKC,eAEhBiB,eAAeH,iBAGlBI,QAAUX,MAAMW,QAAQC,IAAIpB,KAAKD,IACnCsB,OAAOC,SAASC,MAAQJ,QAAQK,WAAWC,QAAQ,SAAU,YACxDb,SAASc,SAAS,cAAe,UAAW1B,KAAKD,IACtDW,YAAYiB,kBASpBC,oBACW,CACH,CAACC,wBAAkB7B,KAAKD,gBAAe+B,QAAS9B,KAAK+B,QACrD,CAACF,wBAAkB7B,KAAKD,gBAAe+B,QAAS9B,KAAKgC,iBACrD,CAACH,gCAAkCC,QAAS9B,KAAKiC,mBASzDC,mBACWlC,KAAKW,WAAWX,KAAKZ,UAAUG,SAS1CyC,sFAAgB/B,QAACA,oBAEPS,YAAcV,KAAKW,WAAWX,KAAKZ,UAAUC,cACnDqB,YAAYyB,UAAUC,OAAOpC,KAAKR,QAAQC,eAAgBQ,QAAQoC,SAClE3B,YAAYyB,UAAUC,OAAOpC,KAAKR,QAAQI,2CAAcK,QAAQqC,8EAC3DrC,QAAQkC,UAAUC,OAAOpC,KAAKR,QAAQE,eAAgBO,QAAQsC,cAC9DtC,QAAQkC,UAAUC,OAAOpC,KAAKR,QAAQgD,mCAAUvC,QAAQwC,+DACxDxC,QAAQkC,UAAUC,OAAOpC,KAAKR,QAAQG,+BAAQM,QAAQyC,yDACtDA,OAASzC,QAAQyC,YAEjB/B,WAAWX,KAAKZ,UAAUE,eAAeqD,UAAY1C,QAAQ2C,MAUtEX,iEAAiBhC,QAACA,QAADO,MAAUA,iBAClBP,QAAQ4C,mBAGT5C,QAAQ4C,SAASC,YAAc9C,KAAKD,IAAMC,KAAKG,uBAC1C0C,UAAW,YACXlC,WAAWX,KAAKZ,UAAUC,cAAc8C,UAAUJ,OAAO/B,KAAKR,QAAQK,kCAG/DW,MAAMW,QAAQC,IAAIpB,KAAKD,IAC3BgD,0CAAmB9C,QAAQ4C,uCAARG,kBAAkBC,cAGxCJ,SAAqC,WAAzB5C,QAAQ4C,SAASK,MAAqBjD,QAAQ4C,SAAS9C,IAAMC,KAAKD,QAF9E8C,qCAAY5C,QAAQ4C,iEAAUC,YAAa9C,KAAKD,GAIrCC,KAAKW,WAAWX,KAAKZ,UAAUC,cACvC8C,UAAUC,OAAOpC,KAAKR,QAAQK,gCAAUG,KAAK6C,oDACrD7C,KAAK6C,WAAa7C,KAAKY,SAASC,gBAC3BZ,QAAQ0B,eAAe,CAACwB,MAAO,oCAUnClD,QAAQkC,UAAUiB,IAAIpD,KAAKR,QAAQM,gBAQ5CuD,qBACSpD,QAAQkC,UAAUJ,OAAO/B,KAAKR,QAAQM"} \ No newline at end of file diff --git a/course/format/amd/src/local/courseeditor/courseeditor.js b/course/format/amd/src/local/courseeditor/courseeditor.js index 778f3c8285c..ae464fcb953 100644 --- a/course/format/amd/src/local/courseeditor/courseeditor.js +++ b/course/format/amd/src/local/courseeditor/courseeditor.js @@ -19,6 +19,7 @@ import Exporter from 'core_courseformat/local/courseeditor/exporter'; import log from 'core/log'; import ajax from 'core/ajax'; import * as Storage from 'core/sessionstorage'; +import {uploadFilesToCourse} from 'core_courseformat/local/courseeditor/fileuploader'; /** * Main course editor module. @@ -82,6 +83,7 @@ export default class extends Reactive { // Default view format setup. this._editing = false; this._supportscomponents = false; + this._fileHandlers = null; this.courseId = courseId; @@ -118,6 +120,49 @@ export default class extends Reactive { } this.stateKey = Storage.get(`course/${courseId}/stateKey`); } + + this._loadFileHandlers(); + } + + /** + * Load the file hanlders promise. + */ + _loadFileHandlers() { + // Load the course file extensions. + this._fileHandlersPromise = new Promise((resolve) => { + if (!this.isEditing) { + resolve([]); + return; + } + // Check the cache. + const handlersCacheKey = `course/${this.courseId}/fileHandlers`; + + const cacheValue = Storage.get(handlersCacheKey); + if (cacheValue) { + try { + const cachedHandlers = JSON.parse(cacheValue); + resolve(cachedHandlers); + return; + } catch (error) { + log.error("ERROR PARSING CACHED FILE HANDLERS"); + } + } + // Call file handlers webservice. + ajax.call([{ + methodname: 'core_courseformat_file_handlers', + args: { + courseid: this.courseId, + } + }])[0].then((handlers) => { + Storage.set(handlersCacheKey, JSON.stringify(handlers)); + resolve(handlers); + return; + }).catch(error => { + log.error(error); + resolve([]); + return; + }); + }); } /** @@ -185,6 +230,28 @@ export default class extends Reactive { return this._supportscomponents ?? false; } + /** + * Return the course file handlers promise. + * @returns {Promise} the promise for file handlers. + */ + async getFileHandlersPromise() { + return this._fileHandlersPromise ?? []; + } + + /** + * Upload a file list to the course. + * + * This method is a wrapper to the course file uploader. + * + * @param {number} sectionId the section id + * @param {number} sectionNum the section number + * @param {Array} files and array of files + * @return {Promise} the file queue promise + */ + uploadFiles(sectionId, sectionNum, files) { + return uploadFilesToCourse(this.courseId, sectionId, sectionNum, files); + } + /** * Get a value from the course editor static storage if any. * @@ -235,6 +302,16 @@ export default class extends Reactive { return Storage.set(`course/${this.courseId}/${key}`, JSON.stringify(data)); } + /** + * Convert a file dragging event into a proper dragging file list. + * @param {DataTransfer} dataTransfer the event to convert + * @return {Array} of file list info. + */ + getFilesDraggableData(dataTransfer) { + const exporter = this.getExporter(); + return exporter.fileDraggableData(this.state, dataTransfer); + } + /** * Dispatch a change in the state. * diff --git a/course/format/amd/src/local/courseeditor/dndsection.js b/course/format/amd/src/local/courseeditor/dndsection.js index 546e6474c6a..fec0ad3443f 100644 --- a/course/format/amd/src/local/courseeditor/dndsection.js +++ b/course/format/amd/src/local/courseeditor/dndsection.js @@ -26,6 +26,12 @@ */ import {BaseComponent, DragDrop} from 'core/reactive'; +import {get_string as getString} from 'core/str'; +import {prefetchStrings} from 'core/prefetch'; +import Templates from 'core/templates'; + +// Load global strings. +prefetchStrings('core', ['addfilehere']); export default class extends BaseComponent { @@ -105,6 +111,10 @@ export default class extends BaseComponent { * @returns {boolean} */ validateDropData(dropdata) { + // We accept files. + if (dropdata?.type === 'files') { + return true; + } // We accept any course module. if (dropdata?.type === 'cm') { return true; @@ -123,6 +133,20 @@ export default class extends BaseComponent { * @param {Object} dropdata the accepted drop data */ showDropZone(dropdata) { + if (dropdata.type == 'files') { + this.addOverlay({ + content: getString('addfilehere', 'core'), + icon: Templates.renderPix('t/download', 'core'), + }).then(() => { + // Check if we still need the file dropzone. + if (!this.dragdrop?.isDropzoneVisible()) { + this.removeOverlay(); + } + return; + }).catch((error) => { + throw error; + }); + } if (dropdata.type == 'cm') { this.getLastCm()?.classList.add(this.classes.DROPDOWN); } @@ -145,6 +169,7 @@ export default class extends BaseComponent { this.getLastCm()?.classList.remove(this.classes.DROPDOWN); this.element.classList.remove(this.classes.DROPUP); this.element.classList.remove(this.classes.DROPDOWN); + this.removeOverlay(); } /** @@ -154,6 +179,15 @@ export default class extends BaseComponent { * @param {Event} event the drop event */ drop(dropdata, event) { + // File handling. + if (dropdata.type == 'files') { + this.reactive.uploadFiles( + this.section.id, + this.section.number, + dropdata.files + ); + return; + } // Call the move mutation. if (dropdata.type == 'cm') { const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove'; diff --git a/course/format/amd/src/local/courseeditor/exporter.js b/course/format/amd/src/local/courseeditor/exporter.js index 9a23fe50046..1dc64af12df 100644 --- a/course/format/amd/src/local/courseeditor/exporter.js +++ b/course/format/amd/src/local/courseeditor/exporter.js @@ -158,7 +158,30 @@ export default class { } /** - * Generate a compoetion export data from the cm element. + * Generate a file draggable structure. + * + * This method is used when files are dragged on the browser. + * + * @param {*} state the state object + * @param {*} dataTransfer the current data tranfer data + * @returns {Object|null} + */ + fileDraggableData(state, dataTransfer) { + const files = []; + // Browsers do not provide the file list until the drop event. + if (dataTransfer.files?.length > 0) { + dataTransfer.files.forEach(file => { + files.push(file); + }); + } + return { + type: 'files', + files, + }; + } + + /** + * Generate a completion export data from the cm element. * * @param {Object} state the current state. * @param {Object} cminfo the course module state data. diff --git a/course/format/amd/src/local/courseeditor/fileuploader.js b/course/format/amd/src/local/courseeditor/fileuploader.js new file mode 100644 index 00000000000..2c1e6c30bd1 --- /dev/null +++ b/course/format/amd/src/local/courseeditor/fileuploader.js @@ -0,0 +1,558 @@ +// 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 . + +/** + * The course file uploader. + * + * This module is used to upload files directly into the course. + * + * @module core_courseformat/local/courseeditor/fileuploader + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * @typedef {Object} Handler + * @property {String} extension the handled extension or * for any + * @property {String} message the handler message + * @property {String} module the module name + */ + +import Config from 'core/config'; +import ModalFactory from 'core/modal_factory'; +import ModalEvents from 'core/modal_events'; +import Templates from 'core/templates'; +import {getFirst} from 'core/normalise'; +import {prefetchStrings} from 'core/prefetch'; +import {get_string as getString, get_strings as getStrings} from 'core/str'; +import {getCourseEditor} from 'core_courseformat/courseeditor'; +import {processMonitor} from 'core/process_monitor'; +import {debounce} from 'core/utils'; + +// Uploading url. +const UPLOADURL = Config.wwwroot + '/course/dndupload.php'; +const DEBOUNCETIMER = 500; +const USERCANIGNOREFILESIZELIMITS = -1; + +/** @var {ProcessQueue} uploadQueue the internal uploadQueue instance. */ +let uploadQueue = null; +/** @var {Object} handlerManagers the courseId indexed loaded handler managers. */ +let handlerManagers = {}; +/** @var {Map} courseUpdates the pending course sections updates. */ +let courseUpdates = new Map(); +/** @var {Object} errors the error messages. */ +let errors = null; + +// Load global strings. +prefetchStrings('moodle', ['addresourceoractivity', 'upload']); +prefetchStrings('core_error', ['dndmaxbytes', 'dndread', 'dndupload', 'dndunkownfile']); + +/** + * Class to upload a file into the course. + * @private + */ +class FileUploader { + /** + * Class constructor. + * + * @param {number} courseId the course id + * @param {number} sectionId the section id + * @param {number} sectionNum the section number + * @param {File} fileInfo the file information object + * @param {Handler} handler the file selected file handler + */ + constructor(courseId, sectionId, sectionNum, fileInfo, handler) { + this.courseId = courseId; + this.sectionId = sectionId; + this.sectionNum = sectionNum; + this.fileInfo = fileInfo; + this.handler = handler; + } + + /** + * Execute the file upload and update the state in the given process. + * + * @param {LoadingProcess} process the process to store the upload result + */ + execute(process) { + const fileInfo = this.fileInfo; + const xhr = this._createXhrRequest(process); + const formData = this._createUploadFormData(); + + // Try reading the file to check it is not a folder, before sending it to the server. + const reader = new FileReader(); + reader.onload = function() { + // File was read OK - send it to the server. + xhr.open("POST", UPLOADURL, true); + xhr.send(formData); + }; + reader.onerror = function() { + // Unable to read the file (it is probably a folder) - display an error message. + process.setError(errors.dndread); + }; + if (fileInfo.size > 0) { + // If this is a non-empty file, try reading the first few bytes. + // This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files. + reader.readAsText(fileInfo.slice(0, 5)); + } else { + // If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(), + // instead of reader.onerror(). + // So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected). + reader.readAsText(fileInfo); + } + } + + /** + * Returns the bind version of execute function. + * + * This method is used to queue the process into a ProcessQueue instance. + * + * @returns {Function} the bind function to execute the process + */ + getExecutionFunction() { + return this.execute.bind(this); + } + + /** + * Generate a upload XHR file request. + * + * @param {LoadingProcess} process the current process + * @return {XMLHttpRequest} the XHR request + */ + _createXhrRequest(process) { + const xhr = new XMLHttpRequest(); + // Update the progress bar as the file is uploaded. + xhr.upload.addEventListener( + 'progress', + (event) => { + if (event.lengthComputable) { + const percent = Math.round((event.loaded * 100) / event.total); + process.setPercentage(percent); + } + }, + false + ); + // Wait for the AJAX call to complete. + xhr.onreadystatechange = () => { + if (xhr.readyState == 1) { + // Add a 1% just to indicate that it is uploading. + process.setPercentage(1); + } + // State 4 is DONE. Otherwise the connection is still ongoing. + if (xhr.readyState != 4) { + return; + } + if (xhr.status == 200) { + var result = JSON.parse(xhr.responseText); + if (result && result.error == 0) { + // All OK. + this._finishProcess(process); + } else { + process.setError(result.error); + } + } else { + process.setError(errors.dndupload); + } + }; + return xhr; + } + + /** + * Upload a file into the course. + * + * @return {FormData|null} the new form data object + */ + _createUploadFormData() { + const formData = new FormData(); + try { + formData.append('repo_upload_file', this.fileInfo); + } catch (error) { + throw Error(error.dndread); + } + formData.append('sesskey', Config.sesskey); + formData.append('course', this.courseId); + formData.append('section', this.sectionNum); + formData.append('module', this.handler.module); + formData.append('type', 'Files'); + return formData; + } + + /** + * Finishes the current process. + * @param {LoadingProcess} process the process + */ + _finishProcess(process) { + addRefreshSection(this.courseId, this.sectionId); + process.setPercentage(100); + process.finish(); + } +} + +/** + * The file handler manager class. + * + * @private + */ +class HandlerManager { + + /** @var {Object} lastHandlers the last handlers selected per each file extension. */ + lastHandlers = {}; + + /** @var {Handler[]|null} allHandlers all the available handlers. */ + allHandlers = null; + + /** + * Class constructor. + * + * @param {Number} courseId + */ + constructor(courseId) { + this.courseId = courseId; + this.lastUploadId = 0; + this.courseEditor = getCourseEditor(courseId); + if (!this.courseEditor) { + throw Error('Unkown course editor'); + } + this.maxbytes = this.courseEditor.get('course')?.maxbytes ?? 0; + } + + /** + * Load the course file handlers. + */ + async loadHandlers() { + this.allHandlers = await this.courseEditor.getFileHandlersPromise(); + } + + /** + * Extract the file extension from a fileInfo. + * + * @param {File} fileInfo + * @returns {String} the file extension or an empty string. + */ + getFileExtension(fileInfo) { + let extension = ''; + const dotpos = fileInfo.name.lastIndexOf('.'); + if (dotpos != -1) { + extension = fileInfo.name.substring(dotpos + 1, fileInfo.name.length).toLowerCase(); + } + return extension; + } + + /** + * Check if the file is valid. + * + * @param {File} fileInfo the file info + */ + validateFile(fileInfo) { + if (this.maxbytes !== USERCANIGNOREFILESIZELIMITS && fileInfo.size > this.maxbytes) { + throw Error(errors.dndmaxbytes); + } + } + + /** + * Get the file handlers of an specific file. + * + * @param {File} fileInfo the file indo + * @return {Array} Array of handlers + */ + filterHandlers(fileInfo) { + const extension = this.getFileExtension(fileInfo); + return this.allHandlers.filter(handler => handler.extension == '*' || handler.extension == extension); + } + + /** + * Get the Handler to upload a specific file. + * + * It will ask the used if more than one handler is available. + * + * @param {File} fileInfo the file info + * @returns {Promise} the selected handler or null if the user cancel + */ + async getFileHandler(fileInfo) { + const fileHandlers = this.filterHandlers(fileInfo); + if (fileHandlers.length == 0) { + throw Error(errors.dndunkownfile); + } + let fileHandler = null; + if (fileHandlers.length == 1) { + fileHandler = fileHandlers[0]; + } else { + fileHandler = await this.askHandlerToUser(fileHandlers, fileInfo); + } + return fileHandler; + } + + /** + * Ask the user to select a specific handler. + * + * @param {Handler[]} fileHandlers + * @param {File} fileInfo the file info + * @return {Promise} the selected handler + */ + async askHandlerToUser(fileHandlers, fileInfo) { + const extension = this.getFileExtension(fileInfo); + // Build the modal parameters from the event data. + const modalParams = { + title: getString('addresourceoractivity', 'moodle'), + body: Templates.render( + 'core_courseformat/fileuploader', + this.getModalData( + fileHandlers, + fileInfo, + this.lastHandlers[extension] ?? null + ) + ), + type: ModalFactory.types.SAVE_CANCEL, + saveButtonText: getString('upload', 'moodle'), + }; + // Create the modal. + const modal = await this.modalBodyRenderedPromise(modalParams); + const selectedHandler = await this.modalUserAnswerPromise(modal, fileHandlers); + // Cancel action. + if (selectedHandler === null) { + return null; + } + // Save last selected handler. + this.lastHandlers[extension] = selectedHandler.module; + return selectedHandler; + } + + /** + * Generated the modal template data. + * + * @param {Handler[]} fileHandlers + * @param {File} fileInfo the file info + * @param {String|null} defaultModule the default module if any + * @return {Object} the modal template data. + */ + getModalData(fileHandlers, fileInfo, defaultModule) { + const data = { + filename: fileInfo.name, + uploadid: ++this.lastUploadId, + handlers: [], + }; + let hasDefault = false; + fileHandlers.forEach((handler, index) => { + const isDefault = (defaultModule == handler.module); + data.handlers.push({ + ...handler, + selected: isDefault, + labelid: `fileuploader_${data.uploadid}`, + value: index, + }); + hasDefault = hasDefault || isDefault; + }); + if (!hasDefault && data.handlers.length > 0) { + const lastHandler = data.handlers.pop(); + lastHandler.selected = true; + data.handlers.push(lastHandler); + } + return data; + } + + /** + * Get the user handler choice. + * + * Wait for the user answer in the modal and resolve with the selected index. + * + * @param {Modal} modal the modal instance + * @param {Handler[]} fileHandlers the availabvle file handlers + * @return {Promise} with the option selected by the user. + */ + modalUserAnswerPromise(modal, fileHandlers) { + const modalBody = getFirst(modal.getBody()); + return new Promise((resolve, reject) => { + modal.getRoot().on( + ModalEvents.save, + event => { + // Get the selected option. + const index = modalBody.querySelector('input:checked').value; + event.preventDefault(); + modal.destroy(); + if (!fileHandlers[index]) { + reject('Invalid handler selected'); + } + resolve(fileHandlers[index]); + + } + ); + modal.getRoot().on( + ModalEvents.cancel, + () => { + resolve(null); + } + ); + }); + } + + /** + * Create a new modal and return a Promise to the body rendered. + * + * @param {Object} modalParams the modal params + * @returns {Promise} the modal body rendered promise + */ + modalBodyRenderedPromise(modalParams) { + return new Promise((resolve, reject) => { + ModalFactory.create(modalParams).then((modal) => { + modal.setRemoveOnClose(true); + // Handle body loading event. + modal.getRoot().on(ModalEvents.bodyRendered, () => { + resolve(modal); + }); + // Configure some extra modal params. + if (modalParams.saveButtonText !== undefined) { + modal.setSaveButtonText(modalParams.saveButtonText); + } + modal.show(); + return; + }).catch(() => { + reject(`Cannot load modal content`); + }); + }); + } +} + +/** + * Add a section to refresh. + * + * @param {number} courseId the course id + * @param {number} sectionId the seciton id + */ +function addRefreshSection(courseId, sectionId) { + let refresh = courseUpdates.get(courseId); + if (!refresh) { + refresh = new Set(); + } + refresh.add(sectionId); + courseUpdates.set(courseId, refresh); + refreshCourseEditors(); +} + +/** + * Debounced processing all pending course refreshes. + * @private + */ +const refreshCourseEditors = debounce( + () => { + const refreshes = courseUpdates; + courseUpdates = new Map(); + refreshes.forEach((sectionIds, courseId) => { + const courseEditor = getCourseEditor(courseId); + if (!courseEditor) { + return; + } + courseEditor.dispatch('sectionState', [...sectionIds]); + }); + }, + DEBOUNCETIMER +); + +/** + * Load and return the course handler manager instance. + * + * @param {Number} courseId the course Id to load + * @returns {Promise} promise of the the loaded handleManager + */ +async function loadCourseHandlerManager(courseId) { + if (handlerManagers[courseId] !== undefined) { + return handlerManagers[courseId]; + } + try { + const handlerManager = new HandlerManager(courseId); + await handlerManager.loadHandlers(); + handlerManagers[courseId] = handlerManager; + } catch (error) { + throw error; + } + return handlerManagers[courseId]; +} + +/** + * Load all the erros messages at once in the module "errors" variable. + * @param {Number} courseId the course id + */ +async function loadErrorStrings(courseId) { + if (errors !== null) { + return; + } + const courseEditor = getCourseEditor(courseId); + const maxbytestext = courseEditor.get('course')?.maxbytestext ?? '0'; + + errors = {}; + const allStrings = [ + {key: 'dndmaxbytes', component: 'core_error', param: {size: maxbytestext}}, + {key: 'dndread', component: 'core_error'}, + {key: 'dndupload', component: 'core_error'}, + {key: 'dndunkownfile', component: 'core_error'}, + ]; + window.console.log(allStrings); + const loadedStrings = await getStrings(allStrings); + allStrings.forEach(({key}, index) => { + errors[key] = loadedStrings[index]; + }); +} + +/** + * Start a batch file uploading into the course. + * + * @private + * @param {number} courseId the course id. + * @param {number} sectionId the section id. + * @param {number} sectionNum the section number. + * @param {File} fileInfo the file information object + * @param {HandlerManager} handlerManager the course handler manager + */ +const queueFileUpload = async function(courseId, sectionId, sectionNum, fileInfo, handlerManager) { + let handler; + uploadQueue = await processMonitor.createProcessQueue(); + try { + handlerManager.validateFile(fileInfo); + handler = await handlerManager.getFileHandler(fileInfo); + } catch (error) { + uploadQueue.addError(fileInfo.name, error.message); + return; + } + // If we don't have a handler means the user cancel the upload. + if (!handler) { + return; + } + const fileProcessor = new FileUploader(courseId, sectionId, sectionNum, fileInfo, handler); + uploadQueue.addPending(fileInfo.name, fileProcessor.getExecutionFunction()); +}; + +/** + * Upload a file to the course. + * + * This method will show any necesary modal to handle the request. + * + * @param {number} courseId the course id + * @param {number} sectionId the section id + * @param {number} sectionNum the section number + * @param {Array} files and array of files + */ +export const uploadFilesToCourse = async function(courseId, sectionId, sectionNum, files) { + // Get the course handlers. + let handlerManager; + try { + handlerManager = await loadCourseHandlerManager(courseId); + await loadErrorStrings(courseId); + } catch (error) { + throw error; + } + for (let index = 0; index < files.length; index++) { + const fileInfo = files[index]; + await queueFileUpload(courseId, sectionId, sectionNum, fileInfo, handlerManager); + } +}; diff --git a/course/format/amd/src/local/courseindex/section.js b/course/format/amd/src/local/courseindex/section.js index a8d4be1acf9..927b0e2ed71 100644 --- a/course/format/amd/src/local/courseindex/section.js +++ b/course/format/amd/src/local/courseindex/section.js @@ -48,6 +48,7 @@ export default class Component extends DndSection { LOCKED: 'editinprogress', RESTRICTIONS: 'restrictions', PAGEITEM: 'pageitem', + OVERLAYBORDERS: 'overlay-preview-borders', }; // We need our id to watch specific events. @@ -164,4 +165,22 @@ export default class Component extends DndSection { this.element.scrollIntoView({block: "nearest"}); } } + + /** + * Overridden version of the component addOverlay async method. + * + * The course index is not compatible with overlay elements. + */ + async addOverlay() { + this.element.classList.add(this.classes.OVERLAYBORDERS); + } + + /** + * Overridden version of the component removeOverlay. + * + * The course index is not compatible with overlay elements. + */ + removeOverlay() { + this.element.classList.remove(this.classes.OVERLAYBORDERS); + } } diff --git a/course/format/classes/output/local/state/course.php b/course/format/classes/output/local/state/course.php index 70c9f9ea5f6..cd2059d3fe7 100644 --- a/course/format/classes/output/local/state/course.php +++ b/course/format/classes/output/local/state/course.php @@ -50,12 +50,16 @@ class course implements renderable { * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { + global $CFG; + $format = $this->format; $course = $format->get_course(); + $context = $format->get_context(); // State must represent always the most updated version of the course. $modinfo = course_modinfo::instance($course); $url = new moodle_url('/course/view.php', ['id' => $course->id]); + $maxbytes = get_user_max_upload_file_size($context, $CFG->maxbytes, $course->maxbytes); $data = (object)[ 'id' => $course->id, @@ -66,8 +70,11 @@ class course implements renderable { 'maxsections' => $format->get_max_sections(), 'baseurl' => $url->out(), 'statekey' => course_format::session_cache($course), + 'maxbytes' => $maxbytes, + 'maxbytestext' => display_size($maxbytes), ]; + $sections = $modinfo->get_section_info_all(); foreach ($sections as $section) { if ($format->is_section_visible($section)) { diff --git a/course/format/templates/fileuploader.mustache b/course/format/templates/fileuploader.mustache new file mode 100644 index 00000000000..c13922c7350 --- /dev/null +++ b/course/format/templates/fileuploader.mustache @@ -0,0 +1,54 @@ +{{! + This file is part of Moodle - http://moodle.org/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_courseformat/fileuploader + Displays the complete course format. + Example context (json): + { + "filename": "course-format.html", + "uploadid": "example", + "handlers": [ + { + "module": "resource", + "labelid": "resource_file1", + "message": "Create a ressource", + "selected": true, + "value": 1 + }, + { + "module": "h5pactivity", + "labelid": "resource_file2", + "message": "Create an H5P activity", + "selected": false, + "value": 2 + } + ] + } +}} +

+ {{#str}} actionchoice, moodle, {{filename}}{{/str}} +

+
+{{#handlers}} + + +
+{{/handlers}} +
diff --git a/course/lib.php b/course/lib.php index 2fab5631418..9608ce599ce 100644 --- a/course/lib.php +++ b/course/lib.php @@ -3116,10 +3116,9 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules = 'ajaxurl' => $config->resourceurl, 'config' => $config, )), null, true); - } - // Require various strings for the command toolbox - $PAGE->requires->strings_for_js(array( + // Require various strings for the command toolbox. + $PAGE->requires->strings_for_js(array( 'moveleft', 'deletechecktype', 'deletechecktypename', @@ -3146,22 +3145,23 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules = 'totopofsection', ), 'moodle'); - // Include section-specific strings for formats which support sections. - if (course_format_uses_sections($course->format)) { - $PAGE->requires->strings_for_js(array( - 'showfromothers', - 'hidefromothers', - ), 'format_' . $course->format); - } + // Include section-specific strings for formats which support sections. + if (course_format_uses_sections($course->format)) { + $PAGE->requires->strings_for_js(array( + 'showfromothers', + 'hidefromothers', + ), 'format_' . $course->format); + } - // For confirming resource deletion we need the name of the module in question - foreach ($usedmodules as $module => $modname) { - $PAGE->requires->string_for_js('pluginname', $module); - } + // For confirming resource deletion we need the name of the module in question. + foreach ($usedmodules as $module => $modname) { + $PAGE->requires->string_for_js('pluginname', $module); + } - // Load drag and drop upload AJAX. - require_once($CFG->dirroot.'/course/dnduploadlib.php'); - dndupload_add_to_course($course, $enabledmodules); + // Load drag and drop upload AJAX. + require_once($CFG->dirroot.'/course/dnduploadlib.php'); + dndupload_add_to_course($course, $enabledmodules); + } $PAGE->requires->js_call_amd('core_course/actions', 'initCoursePage', array($course->format)); diff --git a/lang/en/error.php b/lang/en/error.php index e33ae2d6e96..c99db202203 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -228,6 +228,10 @@ $string['dmlparseexception'] = 'Error parsing SQL query'; $string['dmlreadexception'] = 'Error reading from database'; $string['dmltransactionexception'] = 'Database transaction error'; $string['dmlwriteexception'] = 'Error writing to database'; +$string['dndmaxbytes'] = 'The file is too large. The maximum size allowed is {$a->size}'; +$string['dndread'] = 'Error reading the file'; +$string['dndupload'] = 'An unknown error ocurred while uploading the file'; +$string['dndunkownfile'] = 'This file type is not supported'; $string['downgradedcore'] = 'ERROR!!! The code you are using is OLDER than the version that made these databases!'; $string['downloadedfilecheckfailed'] = 'Downloaded file check failed'; $string['duplicatefieldname'] = 'Duplicate field name "{$a}" detected'; diff --git a/theme/boost/scss/moodle/course.scss b/theme/boost/scss/moodle/course.scss index 8cbb2ca2724..f8681102ee2 100644 --- a/theme/boost/scss/moodle/course.scss +++ b/theme/boost/scss/moodle/course.scss @@ -1333,6 +1333,7 @@ $activity-add-hover: theme-color-level('primary', -10) !default; display: inline-block; } + // Legacy dndupload classes. Can be removed in 4.4 as part of MDL-77124. &.dndupload-dropzone { border: 2px dashed $primary; padding-left: 2px;