diff --git a/course/format/amd/build/courseeditor.min.js.map b/course/format/amd/build/courseeditor.min.js.map
index fd66829a1d4..8ad88346fcd 100644
--- a/course/format/amd/build/courseeditor.min.js.map
+++ b/course/format/amd/build/courseeditor.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/courseeditor.js"],"names":["courseEditorMap","Map","dispatchStateChangedEvent","detail","target","document","dispatchEvent","CustomEvent","events","stateChanged","bubbles","setViewFormat","courseId","setup","editor","getCourseEditor","parseInt","has","set","CourseEditor","name","eventName","eventDispatch","mutations","DefaultMutations","get","loadCourse","getCurrentCourseEditor","M","cfg"],"mappings":"iUAuBA,OACA,OACA,O,mDAGA,GAAMA,CAAAA,CAAe,CAAG,GAAIC,CAAAA,GAA5B,CAYA,QAASC,CAAAA,CAAT,CAAmCC,CAAnC,CAA2CC,CAA3C,CAAmD,CAC/C,GAAIA,CAAM,SAAV,CAA0B,CACtBA,CAAM,CAAGC,QACZ,CACDD,CAAM,CAACE,aAAP,CAAqB,GAAIC,CAAAA,WAAJ,CAAgBC,UAAOC,YAAvB,CAAqC,CACtDC,OAAO,GAD+C,CAEtDP,MAAM,CAAEA,CAF8C,CAArC,CAArB,CAIH,C,gBAS4B,QAAhBQ,CAAAA,aAAgB,CAACC,CAAD,CAAWC,CAAX,CAAqB,CAC9C,GAAMC,CAAAA,CAAM,CAAGC,CAAe,CAACH,CAAD,CAA9B,CACAE,CAAM,CAACH,aAAP,CAAqBE,CAArB,CACH,C,CAQM,GAAME,CAAAA,CAAe,CAAG,SAACH,CAAD,CAAc,CACzCA,CAAQ,CAAGI,QAAQ,CAACJ,CAAD,CAAnB,CAEA,GAAI,CAACZ,CAAe,CAACiB,GAAhB,CAAoBL,CAApB,CAAL,CAAoC,CAChCZ,CAAe,CAACkB,GAAhB,CACIN,CADJ,CAEI,GAAIO,UAAJ,CAAiB,CACbC,IAAI,uBAAiBR,CAAjB,CADS,CAEbS,SAAS,CAAEb,UAAOC,YAFL,CAGba,aAAa,CAAEpB,CAHF,CAMbqB,SAAS,CAAE,GAAIC,UANF,CAAjB,CAFJ,EAWAxB,CAAe,CAACyB,GAAhB,CAAoBb,CAApB,EAA8Bc,UAA9B,CAAyCd,CAAzC,CACH,CACD,MAAOZ,CAAAA,CAAe,CAACyB,GAAhB,CAAoBb,CAApB,CACV,CAlBM,C,6CAyB+B,QAAzBe,CAAAA,sBAAyB,SAAMZ,CAAAA,CAAe,CAACa,CAAC,CAACC,GAAF,CAAMjB,QAAP,CAArB,C","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 * Generic reactive module used in the course editor.\n *\n * @module core_courseformat/courseeditor\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DefaultMutations from 'core_courseformat/local/courseeditor/mutations';\nimport CourseEditor from 'core_courseformat/local/courseeditor/courseeditor';\nimport events from 'core_course/events';\n\n// A map with all the course editor instances.\nconst courseEditorMap = new Map();\n\n/**\n * Trigger a state changed event.\n *\n * This function will be moved to core_course/events module\n * when the file is migrated to the new JS events structure proposed in MDL-70990.\n *\n * @method dispatchStateChangedEvent\n * @param {object} detail the full state\n * @param {object} target the custom event target (document if none provided)\n */\nfunction dispatchStateChangedEvent(detail, target) {\n if (target === undefined) {\n target = document;\n }\n target.dispatchEvent(new CustomEvent(events.stateChanged, {\n bubbles: true,\n detail: detail,\n }));\n}\n\n/**\n * Setup the current view settings\n *\n * @param {number} courseId the course id\n * @param {setup} setup format, page and course settings\n * @property {boolean} setup.editing if the page is in edit mode\n */\nexport const setViewFormat = (courseId, setup) => {\n const editor = getCourseEditor(courseId);\n editor.setViewFormat(setup);\n};\n\n/**\n * Get a specific course editor reactive instance.\n *\n * @param {number} courseId the course id\n * @returns {CourseEditor}\n */\nexport const getCourseEditor = (courseId) => {\n courseId = parseInt(courseId);\n\n if (!courseEditorMap.has(courseId)) {\n courseEditorMap.set(\n courseId,\n new CourseEditor({\n name: `CourseEditor${courseId}`,\n eventName: events.stateChanged,\n eventDispatch: dispatchStateChangedEvent,\n // Mutations can be overridden by the format plugin using setMutations\n // but we need the default one at least.\n mutations: new DefaultMutations(),\n })\n );\n courseEditorMap.get(courseId).loadCourse(courseId);\n }\n return courseEditorMap.get(courseId);\n};\n\n/**\n * Get the current course reactive instance.\n *\n * @returns {CourseEditor}\n */\nexport const getCurrentCourseEditor = () => getCourseEditor(M.cfg.courseId);\n"],"file":"courseeditor.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../src/courseeditor.js"],"names":["courseEditorMap","Map","dispatchStateChangedEvent","detail","target","document","dispatchEvent","CustomEvent","events","stateChanged","bubbles","setViewFormat","courseId","setup","editor","getCourseEditor","parseInt","has","set","CourseEditor","name","eventName","eventDispatch","mutations","DefaultMutations","get","loadCourse","getCurrentCourseEditor","M","cfg"],"mappings":"iUAuBA,OACA,OACA,O,mDAGA,GAAMA,CAAAA,CAAe,CAAG,GAAIC,CAAAA,GAA5B,CAYA,QAASC,CAAAA,CAAT,CAAmCC,CAAnC,CAA2CC,CAA3C,CAAmD,CAC/C,GAAIA,CAAM,SAAV,CAA0B,CACtBA,CAAM,CAAGC,QACZ,CACDD,CAAM,CAACE,aAAP,CAAqB,GAAIC,CAAAA,WAAJ,CAAgBC,UAAOC,YAAvB,CAAqC,CACtDC,OAAO,GAD+C,CAEtDP,MAAM,CAAEA,CAF8C,CAArC,CAArB,CAIH,C,gBAU4B,QAAhBQ,CAAAA,aAAgB,CAACC,CAAD,CAAWC,CAAX,CAAqB,CAC9C,GAAMC,CAAAA,CAAM,CAAGC,CAAe,CAACH,CAAD,CAA9B,CACAE,CAAM,CAACH,aAAP,CAAqBE,CAArB,CACH,C,CAQM,GAAME,CAAAA,CAAe,CAAG,SAACH,CAAD,CAAc,CACzCA,CAAQ,CAAGI,QAAQ,CAACJ,CAAD,CAAnB,CAEA,GAAI,CAACZ,CAAe,CAACiB,GAAhB,CAAoBL,CAApB,CAAL,CAAoC,CAChCZ,CAAe,CAACkB,GAAhB,CACIN,CADJ,CAEI,GAAIO,UAAJ,CAAiB,CACbC,IAAI,uBAAiBR,CAAjB,CADS,CAEbS,SAAS,CAAEb,UAAOC,YAFL,CAGba,aAAa,CAAEpB,CAHF,CAMbqB,SAAS,CAAE,GAAIC,UANF,CAAjB,CAFJ,EAWAxB,CAAe,CAACyB,GAAhB,CAAoBb,CAApB,EAA8Bc,UAA9B,CAAyCd,CAAzC,CACH,CACD,MAAOZ,CAAAA,CAAe,CAACyB,GAAhB,CAAoBb,CAApB,CACV,CAlBM,C,6CAyB+B,QAAzBe,CAAAA,sBAAyB,SAAMZ,CAAAA,CAAe,CAACa,CAAC,CAACC,GAAF,CAAMjB,QAAP,CAArB,C","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 * Generic reactive module used in the course editor.\n *\n * @module core_courseformat/courseeditor\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DefaultMutations from 'core_courseformat/local/courseeditor/mutations';\nimport CourseEditor from 'core_courseformat/local/courseeditor/courseeditor';\nimport events from 'core_course/events';\n\n// A map with all the course editor instances.\nconst courseEditorMap = new Map();\n\n/**\n * Trigger a state changed event.\n *\n * This function will be moved to core_course/events module\n * when the file is migrated to the new JS events structure proposed in MDL-70990.\n *\n * @method dispatchStateChangedEvent\n * @param {object} detail the full state\n * @param {object} target the custom event target (document if none provided)\n */\nfunction dispatchStateChangedEvent(detail, target) {\n if (target === undefined) {\n target = document;\n }\n target.dispatchEvent(new CustomEvent(events.stateChanged, {\n bubbles: true,\n detail: detail,\n }));\n}\n\n/**\n * Setup the current view settings\n *\n * @param {number} courseId the course id\n * @param {setup} setup format, page and course settings\n * @property {boolean} setup.editing if the page is in edit mode\n * @property {boolean} setup.supportscomponents if the format supports components for content\n */\nexport const setViewFormat = (courseId, setup) => {\n const editor = getCourseEditor(courseId);\n editor.setViewFormat(setup);\n};\n\n/**\n * Get a specific course editor reactive instance.\n *\n * @param {number} courseId the course id\n * @returns {CourseEditor}\n */\nexport const getCourseEditor = (courseId) => {\n courseId = parseInt(courseId);\n\n if (!courseEditorMap.has(courseId)) {\n courseEditorMap.set(\n courseId,\n new CourseEditor({\n name: `CourseEditor${courseId}`,\n eventName: events.stateChanged,\n eventDispatch: dispatchStateChangedEvent,\n // Mutations can be overridden by the format plugin using setMutations\n // but we need the default one at least.\n mutations: new DefaultMutations(),\n })\n );\n courseEditorMap.get(courseId).loadCourse(courseId);\n }\n return courseEditorMap.get(courseId);\n};\n\n/**\n * Get the current course reactive instance.\n *\n * @returns {CourseEditor}\n */\nexport const getCurrentCourseEditor = () => getCourseEditor(M.cfg.courseId);\n"],"file":"courseeditor.min.js"}
\ No newline at end of file
diff --git a/course/format/amd/build/local/courseeditor/courseeditor.min.js b/course/format/amd/build/local/courseeditor/courseeditor.min.js
index 21e7feb77ba..7940e7be3e2 100644
--- a/course/format/amd/build/local/courseeditor/courseeditor.min.js
+++ b/course/format/amd/build/local/courseeditor/courseeditor.min.js
@@ -1,2 +1,2 @@
-define ("core_courseformat/local/courseeditor/courseeditor",["exports","core/reactive","core/notification","core_courseformat/local/courseeditor/exporter","core/log","core/ajax"],function(a,b,c,d,e,f){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;c=g(c);d=g(d);e=g(e);f=g(f);function g(a){return a&&a.__esModule?a:{default:a}}function h(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){h=function(a){return typeof a}}else{h=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return h(a)}function i(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function j(a){for(var b=1,c;b.\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';\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 * 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 * @param {number} courseId course id\n */\n async loadCourse(courseId) {\n\n if (this.courseId) {\n throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);\n }\n\n // Default view format setup.\n this._editing = false;\n\n this.courseId = courseId;\n\n let stateData;\n\n try {\n stateData = await this.getServerCourseState();\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\n /**\n * Setup the current view settings\n *\n * @param {Object} setup format, page and course settings\n * @property {boolean} setup.editing if the page is in edit mode\n */\n setViewFormat(setup) {\n this._editing = setup.editing ?? 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 * 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 {string} actionName the action name (usually the mutation name)\n * @param {*} param any number of params the mutation needs.\n */\n async dispatch(...args) {\n try {\n await super.dispatch(...args);\n } catch (error) {\n notification.exception(error);\n }\n }\n}\n"],"file":"courseeditor.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../../../src/local/courseeditor/courseeditor.js"],"names":["courseId","Error","_editing","_supportscomponents","getServerCourseState","stateData","log","error","setInitialState","setup","editing","supportscomponents","ajax","call","methodname","args","courseid","courseState","JSON","parse","course","section","cm","Exporter","notification","exception","Reactive"],"mappings":"wRAgBA,OACA,OACA,OACA,O,qrGAsBqBA,C,6FAET,KAAKA,Q,sBACC,IAAIC,CAAAA,KAAJ,uBAAyBD,CAAzB,4CAAoE,KAAKA,QAAzE,E,QAIV,KAAKE,QAAL,IACA,KAAKC,mBAAL,IAEA,KAAKH,QAAL,CAAgBA,CAAhB,C,wBAKsB,MAAKI,oBAAL,E,QAAlBC,C,6DAEAC,UAAIC,KAAJ,CAAU,2CAAV,EACAD,UAAIC,KAAJ,O,kCAIJ,KAAKC,eAAL,CAAqBH,CAArB,E,6KAUUI,C,CAAO,SACjB,KAAKP,QAAL,WAAgBO,CAAK,CAACC,OAAtB,mBACA,KAAKP,mBAAL,WAA2BM,CAAK,CAACE,kBAAjC,kBACH,C,oMAQ6BC,WAAKC,IAAL,CAAU,CAAC,CACjCC,UAAU,CAAE,6BADqB,CAEjCC,IAAI,CAAE,CACFC,QAAQ,CAAE,KAAKhB,QADb,CAF2B,CAAD,CAAV,EAKtB,CALsB,C,QAApBiB,C,QAOAZ,C,CAAYa,IAAI,CAACC,KAAL,CAAWF,CAAX,C,6BAGdG,MAAM,CAAE,E,CACRC,OAAO,CAAE,E,CACTC,EAAE,CAAE,E,EACDjB,C,6KAoBG,CACV,MAAO,IAAIkB,UAAJ,CAAa,IAAb,CACV,C,iNAqBiBR,C,uBAAAA,C,yFAEcA,C,4DAExBS,UAAaC,SAAb,O,0JApCQ,OACZ,iBAAO,KAAKvB,QAAZ,kBACH,C,6CAgBuB,OACpB,iBAAO,KAAKC,mBAAZ,kBACH,C,cAhGwBuB,U","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';\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 * 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 * @param {number} courseId course id\n */\n async loadCourse(courseId) {\n\n if (this.courseId) {\n throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);\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 try {\n stateData = await this.getServerCourseState();\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\n /**\n * Setup the current view settings\n *\n * @param {Object} setup format, page and course settings\n * @property {boolean} setup.editing if the page is in edit mode\n * @property {boolean} setup.supportscomponents if the format supports components for content\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 * 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 {string} actionName the action name (usually the mutation name)\n * @param {*} param any number of params the mutation needs.\n */\n async dispatch(...args) {\n try {\n await super.dispatch(...args);\n } catch (error) {\n notification.exception(error);\n }\n }\n}\n"],"file":"courseeditor.min.js"}
\ No newline at end of file
diff --git a/course/format/amd/build/local/courseeditor/dndcmitem.min.js b/course/format/amd/build/local/courseeditor/dndcmitem.min.js
new file mode 100644
index 00000000000..7bd1d49c60f
--- /dev/null
+++ b/course/format/amd/build/local/courseeditor/dndcmitem.min.js
@@ -0,0 +1,2 @@
+define ("core_courseformat/local/courseeditor/dndcmitem",["exports","core/reactive"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function c(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){c=function(a){return typeof a}}else{c=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return c(a)}function d(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function e(a,b){for(var c=0,d;c.\n\n/**\n * Course index cm component.\n *\n * This component is used to control specific course modules interactions like drag and drop\n * in both course index and course content.\n *\n * @module core_courseformat/local/courseeditor/dndcmitem\n * @class core_courseformat/local/courseeditor/dndcmitem\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 * Configure the component drag and drop.\n *\n * @param {number} cmid course module id\n */\n configDragDrop(cmid) {\n\n this.id = cmid;\n\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init element drag and drop.\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.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n // Drag and drop methods.\n\n /**\n * Get the draggable data of this component.\n *\n * @returns {Object} exported course module drop data\n */\n getDraggableData() {\n const exporter = this.reactive.getExporter();\n return exporter.cmDraggableData(this.reactive.state, this.id);\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 return dropdata?.type === 'cm';\n }\n\n /**\n * Display the component dropzone.\n *\n * @param {Object} dropdata the accepted drop data\n */\n showDropZone(dropdata) {\n // If we are the next cmid of the dragged element we accept the drop because otherwise it\n // will get captured by the section. However, we won't trigger any mutation.\n if (dropdata.nextcmid != this.id && dropdata.id != this.id) {\n this.element.classList.add(this.classes.DROPUP);\n }\n }\n\n /**\n * Hide the component dropzone.\n */\n hideDropZone() {\n this.element.classList.remove(this.classes.DROPUP);\n }\n\n /**\n * Drop event handler.\n *\n * @param {Object} dropdata the accepted drop data\n */\n drop(dropdata) {\n // Call the move mutation if necessary.\n if (dropdata.id != this.id && dropdata.nextcmid != this.id) {\n this.reactive.dispatch('cmMove', [dropdata.id], null, this.id);\n }\n }\n\n}\n"],"file":"dndcmitem.min.js"}
\ 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
new file mode 100644
index 00000000000..df8e2755369
--- /dev/null
+++ b/course/format/amd/build/local/courseeditor/dndsection.min.js
@@ -0,0 +1,2 @@
+define ("core_courseformat/local/courseeditor/dndsection",["exports","core/reactive"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function c(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){c=function(a){return typeof a}}else{c=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return c(a)}function d(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function e(a,b){for(var c=0,d;ca.number){this.element.classList.remove(this.classes.DROPUP);this.element.classList.add(this.classes.DROPDOWN)}else{this.element.classList.add(this.classes.DROPUP);this.element.classList.remove(this.classes.DROPDOWN)}}}},{key:"hideDropZone",value:function hideDropZone(){var a;null===(a=this.getLastCm())||void 0===a?void 0:a.classList.remove(this.classes.DROPDOWN);this.element.classList.remove(this.classes.DROPUP);this.element.classList.remove(this.classes.DROPDOWN)}},{key:"drop",value:function drop(a){if("cm"==a.type){this.reactive.dispatch("cmMove",[a.id],this.id)}if("section"==a.type){this.reactive.dispatch("sectionMove",[a.id],this.id)}}}]);return c}(b.BaseComponent);a.default=n;return a.default});
+//# sourceMappingURL=dndsection.min.js.map
diff --git a/course/format/amd/build/local/courseeditor/dndsection.min.js.map b/course/format/amd/build/local/courseeditor/dndsection.min.js.map
new file mode 100644
index 00000000000..0e1b3ff474e
--- /dev/null
+++ b/course/format/amd/build/local/courseeditor/dndsection.min.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../../../src/local/courseeditor/dndsection.js"],"names":["state","id","element","dataset","section","get","course","sectionitem","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","unregister","dropdata","type","sectionzeroid","sectionlist","getLastCm","classList","add","DROPDOWN","number","remove","DROPUP","dispatch","BaseComponent"],"mappings":"47DAoCgBA,C,CAAO,CACf,KAAKC,EAAL,CAAU,KAAKC,OAAL,CAAaC,OAAb,CAAqBF,EAA/B,CACA,KAAKG,OAAL,CAAeJ,CAAK,CAACI,OAAN,CAAcC,GAAd,CAAkB,KAAKJ,EAAvB,CAAf,CACA,KAAKK,MAAL,CAAcN,CAAK,CAACM,MACvB,C,sDAOcC,C,CAAa,CAExB,GAAI,KAAKC,QAAL,CAAcC,SAAd,EAA2B,KAAKD,QAAL,CAAcE,iBAA7C,CAAgE,CAE5D,KAAKH,WAAL,CAAmBA,CAAnB,CAEA,KAAKI,QAAL,CAAgB,GAAIC,WAAJ,CAAa,IAAb,CAAhB,CAEA,KAAKC,OAAL,CAAe,KAAKF,QAAL,CAAcG,UAAd,EAClB,CACJ,C,yCAKS,CACN,GAAI,KAAKP,WAAL,SAAJ,CAAoC,CAChC,KAAKA,WAAL,CAAiBQ,UAAjB,EACH,CACD,GAAI,KAAKJ,QAAL,SAAJ,CAAiC,CAC7B,KAAKA,QAAL,CAAcI,UAAd,EACH,CACJ,C,6CAOW,CACR,MAAO,KACV,C,0DAUgBC,C,CAAU,CAEvB,GAAuB,IAAnB,WAAAA,CAAQ,WAARA,SAAAA,CAAQ,CAAEC,IAAV,CAAJ,CAA6B,CACzB,QACH,CAED,GAAuB,SAAnB,WAAAD,CAAQ,WAARA,SAAAA,CAAQ,CAAEC,IAAV,CAAJ,CAAkC,CAC9B,GAAMC,CAAAA,CAAa,CAAG,KAAKZ,MAAL,CAAYa,WAAZ,CAAwB,CAAxB,CAAtB,CACA,MAAO,QAAAH,CAAQ,WAARA,SAAAA,CAAQ,CAAEf,EAAV,GAAgB,KAAKA,EAArB,EAA2B,QAAAe,CAAQ,WAARA,SAAAA,CAAQ,CAAEf,EAAV,GAAgBiB,CAA3C,EAA4D,KAAKjB,EAAL,EAAWiB,CACjF,CACD,QACH,C,kDAOYF,C,CAAU,CACnB,GAAqB,IAAjB,EAAAA,CAAQ,CAACC,IAAb,CAA2B,OACvB,eAAKG,SAAL,yBAAkBC,SAAlB,CAA4BC,GAA5B,CAAgC,KAAKT,OAAL,CAAaU,QAA7C,CACH,CACD,GAAqB,SAAjB,EAAAP,CAAQ,CAACC,IAAb,CAAgC,CAE5B,GAAI,KAAKb,OAAL,CAAaoB,MAAb,CAAsBR,CAAQ,CAACQ,MAAnC,CAA2C,CACvC,KAAKtB,OAAL,CAAamB,SAAb,CAAuBI,MAAvB,CAA8B,KAAKZ,OAAL,CAAaa,MAA3C,EACA,KAAKxB,OAAL,CAAamB,SAAb,CAAuBC,GAAvB,CAA2B,KAAKT,OAAL,CAAaU,QAAxC,CACH,CAHD,IAGO,CACH,KAAKrB,OAAL,CAAamB,SAAb,CAAuBC,GAAvB,CAA2B,KAAKT,OAAL,CAAaa,MAAxC,EACA,KAAKxB,OAAL,CAAamB,SAAb,CAAuBI,MAAvB,CAA8B,KAAKZ,OAAL,CAAaU,QAA3C,CACH,CACJ,CACJ,C,mDAKc,OACX,eAAKH,SAAL,yBAAkBC,SAAlB,CAA4BI,MAA5B,CAAmC,KAAKZ,OAAL,CAAaU,QAAhD,EACA,KAAKrB,OAAL,CAAamB,SAAb,CAAuBI,MAAvB,CAA8B,KAAKZ,OAAL,CAAaa,MAA3C,EACA,KAAKxB,OAAL,CAAamB,SAAb,CAAuBI,MAAvB,CAA8B,KAAKZ,OAAL,CAAaU,QAA3C,CACH,C,kCAOIP,C,CAAU,CAEX,GAAqB,IAAjB,EAAAA,CAAQ,CAACC,IAAb,CAA2B,CACvB,KAAKT,QAAL,CAAcmB,QAAd,CAAuB,QAAvB,CAAiC,CAACX,CAAQ,CAACf,EAAV,CAAjC,CAAgD,KAAKA,EAArD,CACH,CACD,GAAqB,SAAjB,EAAAe,CAAQ,CAACC,IAAb,CAAgC,CAC5B,KAAKT,QAAL,CAAcmB,QAAd,CAAuB,aAAvB,CAAsC,CAACX,CAAQ,CAACf,EAAV,CAAtC,CAAqD,KAAKA,EAA1D,CACH,CACJ,C,cAnHwB2B,e","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 * 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 bu 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 */\n drop(dropdata) {\n // Call the move mutation.\n if (dropdata.type == 'cm') {\n this.reactive.dispatch('cmMove', [dropdata.id], this.id);\n }\n if (dropdata.type == 'section') {\n this.reactive.dispatch('sectionMove', [dropdata.id], this.id);\n }\n }\n}\n"],"file":"dndsection.min.js"}
\ No newline at end of file
diff --git a/course/format/amd/build/local/courseeditor/dndsectionitem.min.js b/course/format/amd/build/local/courseeditor/dndsectionitem.min.js
new file mode 100644
index 00000000000..b8638579b77
--- /dev/null
+++ b/course/format/amd/build/local/courseeditor/dndsectionitem.min.js
@@ -0,0 +1,2 @@
+define ("core_courseformat/local/courseeditor/dndsectionitem",["exports","core/reactive"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function c(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){c=function(a){return typeof a}}else{c=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return c(a)}function d(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function e(a,b){for(var c=0,d;c.\n\n/**\n * Course index section title draggable 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/dndsectionitem\n * @class core_courseformat/local/courseeditor/dndsectionitem\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 * Initial state ready method.\n *\n * @param {number} sectionid the section id\n * @param {Object} state the initial state\n * @param {Element} fullregion the complete section region to mark as dragged\n */\n configDragDrop(sectionid, state, fullregion) {\n\n this.id = sectionid;\n if (this.section === undefined) {\n this.section = state.section.get(this.id);\n }\n if (this.course === undefined) {\n this.course = state.course;\n }\n\n // Prevent topic zero from being draggable.\n if (this.section.number > 0) {\n this.getDraggableData = this._getDraggableData;\n }\n\n this.fullregion = fullregion;\n\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\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.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n // Drag and drop methods.\n\n /**\n * Get the draggable data of this component.\n *\n * @returns {Object} exported course module drop data\n */\n _getDraggableData() {\n const exporter = this.reactive.getExporter();\n return exporter.sectionDraggableData(this.reactive.state, this.id);\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 // Course module validation.\n if (dropdata?.type === 'cm') {\n // The first section element is already there so we can ignore it.\n const firstcmid = this.section?.cmlist[0];\n return dropdata.id !== firstcmid;\n }\n return false;\n }\n\n /**\n * Display the component dropzone.\n *\n * @param {Object} dropdata the accepted drop data\n */\n showDropZone() {\n this.element.classList.add(this.classes.DROPZONE);\n }\n\n /**\n * Hide the component dropzone.\n */\n hideDropZone() {\n this.element.classList.remove(this.classes.DROPZONE);\n }\n\n /**\n * Drop event handler.\n *\n * @param {Object} dropdata the accepted drop data\n */\n drop(dropdata) {\n // Call the move mutation.\n if (dropdata.type == 'cm') {\n this.reactive.dispatch('cmMove', [dropdata.id], this.id, this.section?.cmlist[0]);\n }\n }\n}\n"],"file":"dndsectionitem.min.js"}
\ 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 7f492602506..09973029b2f 100644
--- a/course/format/amd/build/local/courseeditor/exporter.min.js
+++ b/course/format/amd/build/local/courseeditor/exporter.min.js
@@ -1,2 +1,2 @@
-define ("core_courseformat/local/courseeditor/exporter",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function b(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function c(a){for(var c=1,e;c.\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\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 };\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 cms: [],\n isactive: false,\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"],"file":"exporter.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../../../src/local/courseeditor/exporter.js"],"names":["reactive","state","data","sections","editmode","isEditing","sectionlist","course","forEach","sectionid","sectioninfo","section","get","push","hassections","length","cms","isactive","cmlist","cmid","cminfo","cm","hascms","nextcmid","currentindex","indexOf","id","type","name","number"],"mappings":"4rCA+BI,WAAYA,CAAZ,CAAsB,WAClB,KAAKA,QAAL,CAAgBA,CACnB,C,yCAQMC,C,CAAO,cAEJC,CAAI,CAAG,CACTC,QAAQ,CAAE,EADD,CAETC,QAAQ,CAAE,KAAKJ,QAAL,CAAcK,SAFf,CAFH,CAMJC,CAAW,WAAGL,CAAK,CAACM,MAAN,CAAaD,WAAhB,gBAA+B,EANtC,CAOVA,CAAW,CAACE,OAAZ,CAAoB,SAAAC,CAAS,CAAI,OACvBC,CAAW,WAAGT,CAAK,CAACU,OAAN,CAAcC,GAAd,CAAkBH,CAAlB,CAAH,gBAAmC,EADvB,CAEvBE,CAAO,CAAG,CAAI,CAACA,OAAL,CAAaV,CAAb,CAAoBS,CAApB,CAFa,CAG7BR,CAAI,CAACC,QAAL,CAAcU,IAAd,CAAmBF,CAAnB,CACH,CAJD,EAKAT,CAAI,CAACY,WAAL,CAA4C,CAAxB,EAAAZ,CAAI,CAACC,QAAL,CAAcY,MAAlC,CAEA,MAAOb,CAAAA,CACV,C,gCASOD,C,CAAOS,C,CAAa,cAClBC,CAAO,MACND,CADM,EAETM,GAAG,CAAE,EAFI,CAGTC,QAAQ,GAHC,EADW,CAMlBC,CAAM,WAAGR,CAAW,CAACQ,MAAf,gBAAyB,EANb,CAOxBA,CAAM,CAACV,OAAP,CAAe,SAAAW,CAAI,CAAI,IACbC,CAAAA,CAAM,CAAGnB,CAAK,CAACoB,EAAN,CAAST,GAAT,CAAaO,CAAb,CADI,CAEbE,CAAE,CAAG,CAAI,CAACA,EAAL,CAAQpB,CAAR,CAAemB,CAAf,CAFQ,CAGnBT,CAAO,CAACK,GAAR,CAAYH,IAAZ,CAAiBQ,CAAjB,CACH,CAJD,EAKAV,CAAO,CAACW,MAAR,CAAwC,CAAtB,EAAAX,CAAO,CAACK,GAAR,CAAYD,MAA9B,CAEA,MAAOJ,CAAAA,CACV,C,2BASEV,C,CAAOmB,C,CAAQ,CACd,GAAMC,CAAAA,CAAE,MACDD,CADC,EAEJH,QAAQ,GAFJ,EAAR,CAIA,MAAOI,CAAAA,CACV,C,wDAYepB,C,CAAOkB,C,CAAM,CACzB,GAAMC,CAAAA,CAAM,CAAGnB,CAAK,CAACoB,EAAN,CAAST,GAAT,CAAaO,CAAb,CAAf,CACA,GAAI,CAACC,CAAL,CAAa,CACT,MAAO,KACV,CAJwB,GAOrBG,CAAAA,CAPqB,CAQnBZ,CAAO,CAAGV,CAAK,CAACU,OAAN,CAAcC,GAAd,CAAkBQ,CAAM,CAACX,SAAzB,CARS,CASnBe,CAAY,QAAGb,CAAH,WAAGA,CAAH,QAAGA,CAAO,CAAEO,MAAT,CAAgBO,OAAhB,CAAwBL,CAAM,CAACM,EAA/B,CATI,CAUzB,GAAIF,CAAY,SAAhB,CAAgC,CAC5BD,CAAQ,QAAGZ,CAAH,WAAGA,CAAH,QAAGA,CAAO,CAAEO,MAAT,CAAgBM,CAAY,CAAG,CAA/B,CACd,CAED,MAAO,CACHG,IAAI,CAAE,IADH,CAEHD,EAAE,CAAEN,CAAM,CAACM,EAFR,CAGHE,IAAI,CAAER,CAAM,CAACQ,IAHV,CAIHL,QAAQ,CAARA,CAJG,CAMV,C,kEAYoBtB,C,CAAOQ,C,CAAW,CACnC,GAAMC,CAAAA,CAAW,CAAGT,CAAK,CAACU,OAAN,CAAcC,GAAd,CAAkBH,CAAlB,CAApB,CACA,GAAI,CAACC,CAAL,CAAkB,CACd,MAAO,KACV,CACD,MAAO,CACHiB,IAAI,CAAE,SADH,CAEHD,EAAE,CAAEhB,CAAW,CAACgB,EAFb,CAGHE,IAAI,CAAElB,CAAW,CAACkB,IAHf,CAIHC,MAAM,CAAEnB,CAAW,CAACmB,MAJjB,CAMV,C","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\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 };\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 cms: [],\n isactive: false,\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 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"],"file":"exporter.min.js"}
\ No newline at end of file
diff --git a/course/format/amd/build/local/courseeditor/mutations.min.js b/course/format/amd/build/local/courseeditor/mutations.min.js
index c247ae2c35b..bafbd33d1a2 100644
--- a/course/format/amd/build/local/courseeditor/mutations.min.js
+++ b/course/format/amd/build/local/courseeditor/mutations.min.js
@@ -1,2 +1,2 @@
-define ("core_courseformat/local/courseeditor/mutations",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);function c(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function d(a){return function(){var b=this,d=arguments;return new Promise(function(e,f){var i=a.apply(b,d);function g(a){c(i,e,f,g,h,"next",a)}function h(a){c(i,e,f,g,h,"throw",a)}g(void 0)})}}function e(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function f(a,b){for(var c=0,d;c.\n\nimport ajax from 'core/ajax';\n\n/**\n * Default mutation manager\n *\n * @module core_courseformat/local/courseeditor/mutations\n * @class core_courseformat/local/courseeditor/mutations\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 // All course editor mutations for Moodle 4.0 will be located in this file.\n\n /**\n * Private method to call core_courseformat_update_course webservice.\n *\n * @method _callEditWebservice\n * @param {string} action\n * @param {number} courseId\n * @param {array} ids\n */\n async _callEditWebservice(action, courseId, ids) {\n let ajaxresult = await ajax.call([{\n methodname: 'core_courseformat_update_course',\n args: {\n action,\n courseid: courseId,\n ids,\n }\n }])[0];\n return JSON.parse(ajaxresult);\n }\n\n /**\n * Get updated state data related to some cm ids.\n *\n * @method cmState\n * @param {StateManager} stateManager the current state\n * @param {array} cmids the list of cm ids to update\n */\n async cmState(stateManager, cmids) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('cm_state', course.id, cmids);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Get updated state data related to some section ids.\n *\n * @method sectionState\n * @param {StateManager} stateManager the current state\n * @param {array} sectionIds the list of section ids to update\n */\n async sectionState(stateManager, sectionIds) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_state', course.id, sectionIds);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Get the full updated state data of the course.\n *\n * @param {StateManager} stateManager the current state\n */\n async courseState(stateManager) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('course_state', course.id);\n stateManager.processUpdates(updates);\n }\n\n}\n"],"file":"mutations.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../../../src/local/courseeditor/mutations.js"],"names":["action","courseId","ids","targetSectionId","targetCmId","args","courseid","targetsectionid","targetcmid","ajax","call","methodname","ajaxresult","JSON","parse","stateManager","cmids","Error","course","get","_callEditWebservice","id","updates","processUpdates","sectionIds"],"mappings":"8KAeA,uD,owBAwB8BA,C,CAAQC,C,CAAUC,C,CAAKC,C,CAAiBC,C,2FACxDC,C,CAAO,CACTL,MAAM,CAANA,CADS,CAETM,QAAQ,CAAEL,CAFD,CAGTC,GAAG,CAAHA,CAHS,C,CAKb,GAAIC,CAAJ,CAAqB,CACjBE,CAAI,CAACE,eAAL,CAAuBJ,CAC1B,CACD,GAAIC,CAAJ,CAAgB,CACZC,CAAI,CAACG,UAAL,CAAkBJ,CACrB,C,eACsBK,WAAKC,IAAL,CAAU,CAAC,CAC9BC,UAAU,CAAE,iCADkB,CAE9BN,IAAI,CAAJA,CAF8B,CAAD,CAAV,EAGnB,CAHmB,C,QAAnBO,C,iCAIGC,IAAI,CAACC,KAAL,CAAWF,CAAX,C,iMAkBEG,C,CAAcC,C,CAAOb,C,CAAiBC,C,gGAC3C,CAACD,CAAD,EAAoB,CAACC,C,uBACf,IAAIa,CAAAA,KAAJ,0D,QAEJC,C,CAASH,CAAY,CAACI,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,SAAzB,CAAoCF,CAAM,CAACG,EAA3C,CAA+CL,CAA/C,CAAsDb,CAAtD,CAAuEC,CAAvE,C,QAAhBkB,C,QACNP,CAAY,CAACQ,cAAb,CAA4BD,CAA5B,E,4LAUcP,C,CAAcS,C,CAAYrB,C,8FACnCA,C,sBACK,IAAIc,CAAAA,KAAJ,iD,QAEJC,C,CAASH,CAAY,CAACI,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,cAAzB,CAAyCF,CAAM,CAACG,EAAhD,CAAoDG,CAApD,CAAgErB,CAAhE,C,QAAhBmB,C,QACNP,CAAY,CAACQ,cAAb,CAA4BD,CAA5B,E,6LAUUP,C,CAAcC,C,2FAClBE,C,CAASH,CAAY,CAACI,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,UAAzB,CAAqCF,CAAM,CAACG,EAA5C,CAAgDL,CAAhD,C,QAAhBM,C,QACNP,CAAY,CAACQ,cAAb,CAA4BD,CAA5B,E,8LAUeP,C,CAAcS,C,2FACvBN,C,CAASH,CAAY,CAACI,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,eAAzB,CAA0CF,CAAM,CAACG,EAAjD,CAAqDG,CAArD,C,QAAhBF,C,QACNP,CAAY,CAACQ,cAAb,CAA4BD,CAA5B,E,kMAQcP,C,2FACRG,C,CAASH,CAAY,CAACI,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,cAAzB,CAAyCF,CAAM,CAACG,EAAhD,C,QAAhBC,C,QACNP,CAAY,CAACQ,cAAb,CAA4BD,CAA5B,E","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 ajax from 'core/ajax';\n\n/**\n * Default mutation manager\n *\n * @module core_courseformat/local/courseeditor/mutations\n * @class core_courseformat/local/courseeditor/mutations\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 // All course editor mutations for Moodle 4.0 will be located in this file.\n\n /**\n * Private method to call core_courseformat_update_course webservice.\n *\n * @method _callEditWebservice\n * @param {string} action\n * @param {number} courseId\n * @param {array} ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _callEditWebservice(action, courseId, ids, targetSectionId, targetCmId) {\n const args = {\n action,\n courseid: courseId,\n ids,\n };\n if (targetSectionId) {\n args.targetsectionid = targetSectionId;\n }\n if (targetCmId) {\n args.targetcmid = targetCmId;\n }\n let ajaxresult = await ajax.call([{\n methodname: 'core_courseformat_update_course',\n args,\n }])[0];\n return JSON.parse(ajaxresult);\n }\n\n /**\n * Move course modules to specific course location.\n *\n * Note that one of targetSectionId or targetCmId should be provided in order to identify the\n * new location:\n * - targetCmId: the activities will be located avobe the target cm. The targetSectionId\n * value will be ignored in this case.\n * - targetSectionId: the activities will be appended to the section. In this case\n * targetSectionId should not be present.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmids the list of cm ids to move\n * @param {number} targetSectionId the target section id\n * @param {number} targetCmId the target course module id\n */\n async cmMove(stateManager, cmids, targetSectionId, targetCmId) {\n if (!targetSectionId && !targetCmId) {\n throw new Error(`Mutation cmMove requires targetSectionId or targetCmId`);\n }\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('cm_move', course.id, cmids, targetSectionId, targetCmId);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Move course modules to specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids to move\n * @param {number} targetSectionId the target section id\n */\n async sectionMove(stateManager, sectionIds, targetSectionId) {\n if (!targetSectionId) {\n throw new Error(`Mutation sectionMove requires targetSectionId`);\n }\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_move', course.id, sectionIds, targetSectionId);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Get updated state data related to some cm ids.\n *\n * @method cmState\n * @param {StateManager} stateManager the current state\n * @param {array} cmids the list of cm ids to update\n */\n async cmState(stateManager, cmids) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('cm_state', course.id, cmids);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Get updated state data related to some section ids.\n *\n * @method sectionState\n * @param {StateManager} stateManager the current state\n * @param {array} sectionIds the list of section ids to update\n */\n async sectionState(stateManager, sectionIds) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_state', course.id, sectionIds);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Get the full updated state data of the course.\n *\n * @param {StateManager} stateManager the current state\n */\n async courseState(stateManager) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('course_state', course.id);\n stateManager.processUpdates(updates);\n }\n\n}\n"],"file":"mutations.min.js"}
\ No newline at end of file
diff --git a/course/format/amd/build/local/courseindex/cm.min.js b/course/format/amd/build/local/courseindex/cm.min.js
index 9c94cb35821..3e0d337a941 100644
--- a/course/format/amd/build/local/courseindex/cm.min.js
+++ b/course/format/amd/build/local/courseindex/cm.min.js
@@ -1,2 +1,2 @@
-define ("core_courseformat/local/courseindex/cm",["exports","core/reactive"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function c(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){c=function(a){return typeof a}}else{c=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return c(a)}function d(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function e(a,b){for(var c=0,d;c.\n\n/**\n * Course index cm component.\n *\n * This component is used to control specific course modules interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/cm\n * @class core_courseformat/local/courseindex/cm\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/reactive';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_cm';\n // Default query selectors.\n this.selectors = {\n };\n // We need our id to watch specific events.\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 {element|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 Component({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n // Activate drag and drop soon.\n }\n\n getWatchers() {\n return [\n {watch: `cm[${this.id}]:deleted`, handler: this.remove},\n ];\n }\n\n}\n"],"file":"cm.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../../../src/local/courseindex/cm.js"],"names":["Component","name","id","element","dataset","configDragDrop","watch","handler","remove","target","selectors","document","getElementById","DndCmItem"],"mappings":"2MA0BA,uD,+nDAEqBA,CAAAA,C,+HAKR,CAEL,KAAKC,IAAL,CAAY,gBAAZ,CAEA,KAAKC,EAAL,CAAU,KAAKC,OAAL,CAAaC,OAAb,CAAqBF,EAClC,C,+CAmBY,CACT,KAAKG,cAAL,CAAoB,KAAKH,EAAzB,CACH,C,iDAOa,CACV,MAAO,CACH,CAACI,KAAK,cAAQ,KAAKJ,EAAb,aAAN,CAAkCK,OAAO,CAAE,KAAKC,MAAhD,CADG,CAGV,C,oCAvBWC,C,CAAQC,C,CAAW,CAC3B,MAAO,IAAIV,CAAAA,CAAJ,CAAc,CACjBG,OAAO,CAAEQ,QAAQ,CAACC,cAAT,CAAwBH,CAAxB,CADQ,CAEjBC,SAAS,CAATA,CAFiB,CAAd,CAIV,C,cAxBkCG,S","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 cm component.\n *\n * This component is used to control specific course modules interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/cm\n * @class core_courseformat/local/courseindex/cm\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DndCmItem from 'core_courseformat/local/courseeditor/dndcmitem';\n\nexport default class Component extends DndCmItem {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_cm';\n // We need our id to watch specific events.\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 {element|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 Component({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n this.configDragDrop(this.id);\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `cm[${this.id}]:deleted`, handler: this.remove},\n ];\n }\n\n}\n"],"file":"cm.min.js"}
\ 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
new file mode 100644
index 00000000000..6f408af9f75
--- /dev/null
+++ b/course/format/amd/build/local/courseindex/section.min.js
@@ -0,0 +1,2 @@
+define ("core_courseformat/local/courseindex/section",["exports","core_courseformat/local/courseindex/sectiontitle","core_courseformat/local/courseeditor/dndsection"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}function e(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){e=function(a){return typeof a}}else{e=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return e(a)}function f(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function g(a){for(var b=1,c;b.\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 CM_LAST: `[data-for=\"cm\"]:last-child`,\n };\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 Component({\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 // 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: this.getElement(this.selectors.SECTION_ITEM),\n fullregion: this.element,\n });\n this.configDragDrop(titleitem);\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"],"file":"section.min.js"}
\ No newline at end of file
diff --git a/course/format/amd/build/local/courseindex/sectiontitle.min.js b/course/format/amd/build/local/courseindex/sectiontitle.min.js
new file mode 100644
index 00000000000..f63442fa0a5
--- /dev/null
+++ b/course/format/amd/build/local/courseindex/sectiontitle.min.js
@@ -0,0 +1,2 @@
+define ("core_courseformat/local/courseindex/sectiontitle",["exports","core_courseformat/local/courseeditor/dndsectionitem"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);function c(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){c=function(a){return typeof a}}else{c=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return c(a)}function d(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function e(a,b){for(var c=0,d;c.\n\n/**\n * Course index section title component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/sectiontitle\n * @class core_courseformat/local/courseindex/sectiontitle\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DndSectionItem from 'core_courseformat/local/courseeditor/dndsectionitem';\n\nexport default class Component extends DndSectionItem {\n\n /**\n * Constructor hook.\n *\n * @param {Object} descriptor\n */\n create(descriptor) {\n // Optional component name for debugging.\n this.name = 'courseindex_sectiontitle';\n\n this.id = descriptor.id;\n this.section = descriptor.section;\n this.course = descriptor.course;\n this.fullregion = descriptor.fullregion;\n\n // Prevent topic zero from being draggable.\n if (this.section.number > 0) {\n this.getDraggableData = this._getDraggableData;\n }\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {element|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 Component({\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.configDragDrop(this.id, state, this.fullregion);\n }\n}\n"],"file":"sectiontitle.min.js"}
\ No newline at end of file
diff --git a/course/format/amd/src/courseeditor.js b/course/format/amd/src/courseeditor.js
index a17a5f91a96..056434e0bb6 100644
--- a/course/format/amd/src/courseeditor.js
+++ b/course/format/amd/src/courseeditor.js
@@ -54,6 +54,7 @@ function dispatchStateChangedEvent(detail, target) {
* @param {number} courseId the course id
* @param {setup} setup format, page and course settings
* @property {boolean} setup.editing if the page is in edit mode
+ * @property {boolean} setup.supportscomponents if the format supports components for content
*/
export const setViewFormat = (courseId, setup) => {
const editor = getCourseEditor(courseId);
diff --git a/course/format/amd/src/local/courseeditor/courseeditor.js b/course/format/amd/src/local/courseeditor/courseeditor.js
index 557841715de..c8f44c6fcc1 100644
--- a/course/format/amd/src/local/courseeditor/courseeditor.js
+++ b/course/format/amd/src/local/courseeditor/courseeditor.js
@@ -47,6 +47,7 @@ export default class extends Reactive {
// Default view format setup.
this._editing = false;
+ this._supportscomponents = false;
this.courseId = courseId;
@@ -68,9 +69,11 @@ export default class extends Reactive {
*
* @param {Object} setup format, page and course settings
* @property {boolean} setup.editing if the page is in edit mode
+ * @property {boolean} setup.supportscomponents if the format supports components for content
*/
setViewFormat(setup) {
this._editing = setup.editing ?? false;
+ this._supportscomponents = setup.supportscomponents ?? false;
}
/**
@@ -116,6 +119,15 @@ export default class extends Reactive {
return new Exporter(this);
}
+ /**
+ * Return if the current course support components to refresh the content.
+ *
+ * @returns {boolean} if the current content support components
+ */
+ get supportComponents() {
+ return this._supportscomponents ?? false;
+ }
+
/**
* Dispatch a change in the state.
*
diff --git a/course/format/amd/src/local/courseeditor/dndcmitem.js b/course/format/amd/src/local/courseeditor/dndcmitem.js
new file mode 100644
index 00000000000..19e3aef0ca4
--- /dev/null
+++ b/course/format/amd/src/local/courseeditor/dndcmitem.js
@@ -0,0 +1,113 @@
+// 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 .
+
+/**
+ * Course index cm component.
+ *
+ * This component is used to control specific course modules interactions like drag and drop
+ * in both course index and course content.
+ *
+ * @module core_courseformat/local/courseeditor/dndcmitem
+ * @class core_courseformat/local/courseeditor/dndcmitem
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent, DragDrop} from 'core/reactive';
+
+export default class extends BaseComponent {
+
+ /**
+ * Configure the component drag and drop.
+ *
+ * @param {number} cmid course module id
+ */
+ configDragDrop(cmid) {
+
+ this.id = cmid;
+
+ // Drag and drop is only available for components compatible course formats.
+ if (this.reactive.isEditing && this.reactive.supportComponents) {
+ // Init element drag and drop.
+ this.dragdrop = new DragDrop(this);
+ // Save dropzone classes.
+ this.classes = this.dragdrop.getClasses();
+ }
+ }
+
+ /**
+ * Remove all subcomponents dependencies.
+ */
+ destroy() {
+ if (this.dragdrop !== undefined) {
+ this.dragdrop.unregister();
+ }
+ }
+
+ // Drag and drop methods.
+
+ /**
+ * Get the draggable data of this component.
+ *
+ * @returns {Object} exported course module drop data
+ */
+ getDraggableData() {
+ const exporter = this.reactive.getExporter();
+ return exporter.cmDraggableData(this.reactive.state, this.id);
+ }
+
+ /**
+ * Validate if the drop data can be dropped over the component.
+ *
+ * @param {Object} dropdata the exported drop data.
+ * @returns {boolean}
+ */
+ validateDropData(dropdata) {
+ return dropdata?.type === 'cm';
+ }
+
+ /**
+ * Display the component dropzone.
+ *
+ * @param {Object} dropdata the accepted drop data
+ */
+ showDropZone(dropdata) {
+ // If we are the next cmid of the dragged element we accept the drop because otherwise it
+ // will get captured by the section. However, we won't trigger any mutation.
+ if (dropdata.nextcmid != this.id && dropdata.id != this.id) {
+ this.element.classList.add(this.classes.DROPUP);
+ }
+ }
+
+ /**
+ * Hide the component dropzone.
+ */
+ hideDropZone() {
+ this.element.classList.remove(this.classes.DROPUP);
+ }
+
+ /**
+ * Drop event handler.
+ *
+ * @param {Object} dropdata the accepted drop data
+ */
+ drop(dropdata) {
+ // Call the move mutation if necessary.
+ if (dropdata.id != this.id && dropdata.nextcmid != this.id) {
+ this.reactive.dispatch('cmMove', [dropdata.id], null, this.id);
+ }
+ }
+
+}
diff --git a/course/format/amd/src/local/courseeditor/dndsection.js b/course/format/amd/src/local/courseeditor/dndsection.js
new file mode 100644
index 00000000000..9d6568f8989
--- /dev/null
+++ b/course/format/amd/src/local/courseeditor/dndsection.js
@@ -0,0 +1,146 @@
+// 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 .
+
+/**
+ * Course index section component.
+ *
+ * This component is used to control specific course section interactions like drag and drop
+ * in both course index and course content.
+ *
+ * @module core_courseformat/local/courseeditor/dndsection
+ * @class core_courseformat/local/courseeditor/dndsection
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent, DragDrop} from 'core/reactive';
+
+export default class extends BaseComponent {
+
+ /**
+ * Save some values form the state.
+ *
+ * @param {Object} state the current state
+ */
+ configState(state) {
+ this.id = this.element.dataset.id;
+ this.section = state.section.get(this.id);
+ this.course = state.course;
+ }
+
+ /**
+ * Register state values and the drag and drop subcomponent.
+ *
+ * @param {BaseComponent} sectionitem section item component
+ */
+ configDragDrop(sectionitem) {
+ // Drag and drop is only available for components compatible course formats.
+ if (this.reactive.isEditing && this.reactive.supportComponents) {
+ // Init the inner dragable element.
+ this.sectionitem = sectionitem;
+ // Init the dropzone.
+ this.dragdrop = new DragDrop(this);
+ // Save dropzone classes.
+ this.classes = this.dragdrop.getClasses();
+ }
+ }
+
+ /**
+ * Remove all subcomponents dependencies.
+ */
+ destroy() {
+ if (this.sectionitem !== undefined) {
+ this.sectionitem.unregister();
+ }
+ if (this.dragdrop !== undefined) {
+ this.dragdrop.unregister();
+ }
+ }
+
+ /**
+ * Get the last CM element of that section.
+ *
+ * @returns {element|null} the las course module element of the section.
+ */
+ getLastCm() {
+ return null;
+ }
+
+ // Drag and drop methods.
+
+ /**
+ * Validate if the drop data can be dropped over the component.
+ *
+ * @param {Object} dropdata the exported drop data.
+ * @returns {boolean}
+ */
+ validateDropData(dropdata) {
+ // We accept any course module.
+ if (dropdata?.type === 'cm') {
+ return true;
+ }
+ // We accept any section bu the section 0 or ourself
+ if (dropdata?.type === 'section') {
+ const sectionzeroid = this.course.sectionlist[0];
+ return dropdata?.id != this.id && dropdata?.id != sectionzeroid && this.id != sectionzeroid;
+ }
+ return false;
+ }
+
+ /**
+ * Display the component dropzone.
+ *
+ * @param {Object} dropdata the accepted drop data
+ */
+ showDropZone(dropdata) {
+ if (dropdata.type == 'cm') {
+ this.getLastCm()?.classList.add(this.classes.DROPDOWN);
+ }
+ if (dropdata.type == 'section') {
+ // The relative move of section depends on the section number.
+ if (this.section.number > dropdata.number) {
+ this.element.classList.remove(this.classes.DROPUP);
+ this.element.classList.add(this.classes.DROPDOWN);
+ } else {
+ this.element.classList.add(this.classes.DROPUP);
+ this.element.classList.remove(this.classes.DROPDOWN);
+ }
+ }
+ }
+
+ /**
+ * Hide the component dropzone.
+ */
+ hideDropZone() {
+ this.getLastCm()?.classList.remove(this.classes.DROPDOWN);
+ this.element.classList.remove(this.classes.DROPUP);
+ this.element.classList.remove(this.classes.DROPDOWN);
+ }
+
+ /**
+ * Drop event handler.
+ *
+ * @param {Object} dropdata the accepted drop data
+ */
+ drop(dropdata) {
+ // Call the move mutation.
+ if (dropdata.type == 'cm') {
+ this.reactive.dispatch('cmMove', [dropdata.id], this.id);
+ }
+ if (dropdata.type == 'section') {
+ this.reactive.dispatch('sectionMove', [dropdata.id], this.id);
+ }
+ }
+}
diff --git a/course/format/amd/src/local/courseeditor/dndsectionitem.js b/course/format/amd/src/local/courseeditor/dndsectionitem.js
new file mode 100644
index 00000000000..850c6e5c93a
--- /dev/null
+++ b/course/format/amd/src/local/courseeditor/dndsectionitem.js
@@ -0,0 +1,129 @@
+// 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 .
+
+/**
+ * Course index section title draggable component.
+ *
+ * This component is used to control specific course section interactions like drag and drop
+ * in both course index and course content.
+ *
+ * @module core_courseformat/local/courseeditor/dndsectionitem
+ * @class core_courseformat/local/courseeditor/dndsectionitem
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent, DragDrop} from 'core/reactive';
+
+export default class extends BaseComponent {
+
+ /**
+ * Initial state ready method.
+ *
+ * @param {number} sectionid the section id
+ * @param {Object} state the initial state
+ * @param {Element} fullregion the complete section region to mark as dragged
+ */
+ configDragDrop(sectionid, state, fullregion) {
+
+ this.id = sectionid;
+ if (this.section === undefined) {
+ this.section = state.section.get(this.id);
+ }
+ if (this.course === undefined) {
+ this.course = state.course;
+ }
+
+ // Prevent topic zero from being draggable.
+ if (this.section.number > 0) {
+ this.getDraggableData = this._getDraggableData;
+ }
+
+ this.fullregion = fullregion;
+
+ // Drag and drop is only available for components compatible course formats.
+ if (this.reactive.isEditing && this.reactive.supportComponents) {
+ // Init the dropzone.
+ this.dragdrop = new DragDrop(this);
+ // Save dropzone classes.
+ this.classes = this.dragdrop.getClasses();
+ }
+ }
+
+ /**
+ * Remove all subcomponents dependencies.
+ */
+ destroy() {
+ if (this.dragdrop !== undefined) {
+ this.dragdrop.unregister();
+ }
+ }
+
+ // Drag and drop methods.
+
+ /**
+ * Get the draggable data of this component.
+ *
+ * @returns {Object} exported course module drop data
+ */
+ _getDraggableData() {
+ const exporter = this.reactive.getExporter();
+ return exporter.sectionDraggableData(this.reactive.state, this.id);
+ }
+
+ /**
+ * Validate if the drop data can be dropped over the component.
+ *
+ * @param {Object} dropdata the exported drop data.
+ * @returns {boolean}
+ */
+ validateDropData(dropdata) {
+ // Course module validation.
+ if (dropdata?.type === 'cm') {
+ // The first section element is already there so we can ignore it.
+ const firstcmid = this.section?.cmlist[0];
+ return dropdata.id !== firstcmid;
+ }
+ return false;
+ }
+
+ /**
+ * Display the component dropzone.
+ *
+ * @param {Object} dropdata the accepted drop data
+ */
+ showDropZone() {
+ this.element.classList.add(this.classes.DROPZONE);
+ }
+
+ /**
+ * Hide the component dropzone.
+ */
+ hideDropZone() {
+ this.element.classList.remove(this.classes.DROPZONE);
+ }
+
+ /**
+ * Drop event handler.
+ *
+ * @param {Object} dropdata the accepted drop data
+ */
+ drop(dropdata) {
+ // Call the move mutation.
+ if (dropdata.type == 'cm') {
+ this.reactive.dispatch('cmMove', [dropdata.id], this.id, this.section?.cmlist[0]);
+ }
+ }
+}
diff --git a/course/format/amd/src/local/courseeditor/exporter.js b/course/format/amd/src/local/courseeditor/exporter.js
index 80b9ebf18b4..e54fe439f55 100644
--- a/course/format/amd/src/local/courseeditor/exporter.js
+++ b/course/format/amd/src/local/courseeditor/exporter.js
@@ -94,4 +94,59 @@ export default class {
};
return cm;
}
+
+ /**
+ * Generate a dragable cm data structure.
+ *
+ * This method is used by any draggable course module element to generate drop data
+ * for its reactive/dragdrop instance.
+ *
+ * @param {*} state the state object
+ * @param {*} cmid the cours emodule id
+ * @returns {Object|null}
+ */
+ cmDraggableData(state, cmid) {
+ const cminfo = state.cm.get(cmid);
+ if (!cminfo) {
+ return null;
+ }
+
+ // Drop an activity over the next activity is the same as doing anything.
+ let nextcmid;
+ const section = state.section.get(cminfo.sectionid);
+ const currentindex = section?.cmlist.indexOf(cminfo.id);
+ if (currentindex !== undefined) {
+ nextcmid = section?.cmlist[currentindex + 1];
+ }
+
+ return {
+ type: 'cm',
+ id: cminfo.id,
+ name: cminfo.name,
+ nextcmid,
+ };
+ }
+
+ /**
+ * Generate a dragable cm data structure.
+ *
+ * This method is used by any draggable section element to generate drop data
+ * for its reactive/dragdrop instance.
+ *
+ * @param {*} state the state object
+ * @param {*} sectionid the cours section id
+ * @returns {Object|null}
+ */
+ sectionDraggableData(state, sectionid) {
+ const sectioninfo = state.section.get(sectionid);
+ if (!sectioninfo) {
+ return null;
+ }
+ return {
+ type: 'section',
+ id: sectioninfo.id,
+ name: sectioninfo.name,
+ number: sectioninfo.number,
+ };
+ }
}
diff --git a/course/format/amd/src/local/courseeditor/mutations.js b/course/format/amd/src/local/courseeditor/mutations.js
index 23d3e332730..6b431d6e126 100644
--- a/course/format/amd/src/local/courseeditor/mutations.js
+++ b/course/format/amd/src/local/courseeditor/mutations.js
@@ -34,19 +34,68 @@ export default class {
* @param {string} action
* @param {number} courseId
* @param {array} ids
+ * @param {number} targetSectionId optional target section id (for moving actions)
+ * @param {number} targetCmId optional target cm id (for moving actions)
*/
- async _callEditWebservice(action, courseId, ids) {
+ async _callEditWebservice(action, courseId, ids, targetSectionId, targetCmId) {
+ const args = {
+ action,
+ courseid: courseId,
+ ids,
+ };
+ if (targetSectionId) {
+ args.targetsectionid = targetSectionId;
+ }
+ if (targetCmId) {
+ args.targetcmid = targetCmId;
+ }
let ajaxresult = await ajax.call([{
methodname: 'core_courseformat_update_course',
- args: {
- action,
- courseid: courseId,
- ids,
- }
+ args,
}])[0];
return JSON.parse(ajaxresult);
}
+ /**
+ * Move course modules to specific course location.
+ *
+ * Note that one of targetSectionId or targetCmId should be provided in order to identify the
+ * new location:
+ * - targetCmId: the activities will be located avobe the target cm. The targetSectionId
+ * value will be ignored in this case.
+ * - targetSectionId: the activities will be appended to the section. In this case
+ * targetSectionId should not be present.
+ *
+ * @param {StateManager} stateManager the current state manager
+ * @param {array} cmids the list of cm ids to move
+ * @param {number} targetSectionId the target section id
+ * @param {number} targetCmId the target course module id
+ */
+ async cmMove(stateManager, cmids, targetSectionId, targetCmId) {
+ if (!targetSectionId && !targetCmId) {
+ throw new Error(`Mutation cmMove requires targetSectionId or targetCmId`);
+ }
+ const course = stateManager.get('course');
+ const updates = await this._callEditWebservice('cm_move', course.id, cmids, targetSectionId, targetCmId);
+ stateManager.processUpdates(updates);
+ }
+
+ /**
+ * Move course modules to specific course location.
+ *
+ * @param {StateManager} stateManager the current state manager
+ * @param {array} sectionIds the list of section ids to move
+ * @param {number} targetSectionId the target section id
+ */
+ async sectionMove(stateManager, sectionIds, targetSectionId) {
+ if (!targetSectionId) {
+ throw new Error(`Mutation sectionMove requires targetSectionId`);
+ }
+ const course = stateManager.get('course');
+ const updates = await this._callEditWebservice('section_move', course.id, sectionIds, targetSectionId);
+ stateManager.processUpdates(updates);
+ }
+
/**
* Get updated state data related to some cm ids.
*
diff --git a/course/format/amd/src/local/courseindex/cm.js b/course/format/amd/src/local/courseindex/cm.js
index b264bcbeded..f0e8b8cf5fb 100644
--- a/course/format/amd/src/local/courseindex/cm.js
+++ b/course/format/amd/src/local/courseindex/cm.js
@@ -24,9 +24,9 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-import {BaseComponent} from 'core/reactive';
+import DndCmItem from 'core_courseformat/local/courseeditor/dndcmitem';
-export default class Component extends BaseComponent {
+export default class Component extends DndCmItem {
/**
* Constructor hook.
@@ -34,9 +34,6 @@ export default class Component extends BaseComponent {
create() {
// Optional component name for debugging.
this.name = 'courseindex_cm';
- // Default query selectors.
- this.selectors = {
- };
// We need our id to watch specific events.
this.id = this.element.dataset.id;
}
@@ -59,9 +56,14 @@ export default class Component extends BaseComponent {
* Initial state ready method.
*/
stateReady() {
- // Activate drag and drop soon.
+ this.configDragDrop(this.id);
}
+ /**
+ * Component watchers.
+ *
+ * @returns {Array} of watchers
+ */
getWatchers() {
return [
{watch: `cm[${this.id}]:deleted`, handler: this.remove},
diff --git a/course/format/amd/src/local/courseindex/section.js b/course/format/amd/src/local/courseindex/section.js
new file mode 100644
index 00000000000..98b1a48205f
--- /dev/null
+++ b/course/format/amd/src/local/courseindex/section.js
@@ -0,0 +1,86 @@
+// 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 .
+
+/**
+ * Course index section component.
+ *
+ * This component is used to control specific course section interactions like drag and drop.
+ *
+ * @module core_courseformat/local/courseindex/section
+ * @class core_courseformat/local/courseindex/section
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import SectionTitle from 'core_courseformat/local/courseindex/sectiontitle';
+import DndSection from 'core_courseformat/local/courseeditor/dndsection';
+
+export default class Component extends DndSection {
+
+ /**
+ * Constructor hook.
+ */
+ create() {
+ // Optional component name for debugging.
+ this.name = 'courseindex_section';
+ // Default query selectors.
+ this.selectors = {
+ SECTION_ITEM: `[data-for='section_item']`,
+ CM_LAST: `[data-for="cm"]:last-child`,
+ };
+ }
+
+ /**
+ * Static method to create a component instance form the mustahce template.
+ *
+ * @param {string} target the DOM main element or its ID
+ * @param {object} selectors optional css selector overrides
+ * @return {Component}
+ */
+ static init(target, selectors) {
+ return new Component({
+ element: document.getElementById(target),
+ selectors,
+ });
+ }
+
+ /**
+ * Initial state ready method.
+ *
+ * @param {Object} state the initial state
+ */
+ stateReady(state) {
+ this.configState(state);
+ // Drag and drop is only available for components compatible course formats.
+ if (this.reactive.isEditing && this.reactive.supportComponents) {
+ // Init the inner dragable element passing the full section as affected region.
+ const titleitem = new SectionTitle({
+ ...this,
+ element: this.getElement(this.selectors.SECTION_ITEM),
+ fullregion: this.element,
+ });
+ this.configDragDrop(titleitem);
+ }
+ }
+
+ /**
+ * Get the last CM element of that section.
+ *
+ * @returns {element|null}
+ */
+ getLastCm() {
+ return this.getElement(this.selectors.CM_LAST);
+ }
+}
diff --git a/course/format/amd/src/local/courseindex/sectiontitle.js b/course/format/amd/src/local/courseindex/sectiontitle.js
new file mode 100644
index 00000000000..12d05eeddd2
--- /dev/null
+++ b/course/format/amd/src/local/courseindex/sectiontitle.js
@@ -0,0 +1,73 @@
+// 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 .
+
+/**
+ * Course index section title component.
+ *
+ * This component is used to control specific course section interactions like drag and drop.
+ *
+ * @module core_courseformat/local/courseindex/sectiontitle
+ * @class core_courseformat/local/courseindex/sectiontitle
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import DndSectionItem from 'core_courseformat/local/courseeditor/dndsectionitem';
+
+export default class Component extends DndSectionItem {
+
+ /**
+ * Constructor hook.
+ *
+ * @param {Object} descriptor
+ */
+ create(descriptor) {
+ // Optional component name for debugging.
+ this.name = 'courseindex_sectiontitle';
+
+ this.id = descriptor.id;
+ this.section = descriptor.section;
+ this.course = descriptor.course;
+ this.fullregion = descriptor.fullregion;
+
+ // Prevent topic zero from being draggable.
+ if (this.section.number > 0) {
+ this.getDraggableData = this._getDraggableData;
+ }
+ }
+
+ /**
+ * Static method to create a component instance form the mustahce template.
+ *
+ * @param {element|string} target the DOM main element or its ID
+ * @param {object} selectors optional css selector overrides
+ * @return {Component}
+ */
+ static init(target, selectors) {
+ return new Component({
+ element: document.getElementById(target),
+ selectors,
+ });
+ }
+
+ /**
+ * Initial state ready method.
+ *
+ * @param {Object} state the initial state
+ */
+ stateReady(state) {
+ this.configDragDrop(this.id, state, this.fullregion);
+ }
+}
diff --git a/course/format/classes/base.php b/course/format/classes/base.php
index 333edd754e7..dc8f5847658 100644
--- a/course/format/classes/base.php
+++ b/course/format/classes/base.php
@@ -494,6 +494,21 @@ abstract class base {
return $ajaxsupport;
}
+ /**
+ * Returns true if this course format is compatible with content components.
+ *
+ * Using components means the content elements can watch the frontend course state and
+ * react to the changes. Formats with component compatibility can have more interactions
+ * without refreshing the page, like having drag and drop from the course index to reorder
+ * sections and activities.
+ *
+ * @return bool if the format is compatible with components.
+ */
+ public function supports_components() {
+ return false;
+ }
+
+
/**
* Custom action after section has been moved in AJAX mode
*
diff --git a/course/format/classes/output/local/state/section.php b/course/format/classes/output/local/state/section.php
index b56671aca0f..4c3f8cc9eb3 100644
--- a/course/format/classes/output/local/state/section.php
+++ b/course/format/classes/output/local/state/section.php
@@ -64,6 +64,7 @@ class section implements renderable {
'section' => $section->section,
'number' => $section->section,
'title' => $format->get_section_name($section),
+ 'rawtitle' => $section->name,
'cmlist' => [],
'visible' => !empty($section->visible),
'sectionurl' => course_get_url($course, $section->section)->out(),
diff --git a/course/format/classes/stateactions.php b/course/format/classes/stateactions.php
index f37bf700702..c42d463ac62 100644
--- a/course/format/classes/stateactions.php
+++ b/course/format/classes/stateactions.php
@@ -17,9 +17,13 @@
namespace core_courseformat;
use core_courseformat\stateupdates;
+use cm_info;
+use section_info;
use stdClass;
use course_modinfo;
use moodle_exception;
+use context_module;
+use context_course;
/**
* Contains the core course state actions.
@@ -36,6 +40,159 @@ use moodle_exception;
*/
class stateactions {
+ /**
+ * Move course modules to another location in the same course.
+ *
+ * @param stateupdates $updates the affected course elements track
+ * @param stdClass $course the course object
+ * @param int[] $ids the list of affected course module ids
+ * @param int $targetsectionid optional target section id
+ * @param int $targetcmid optional target cm id
+ */
+ public function cm_move(
+ stateupdates $updates,
+ stdClass $course,
+ array $ids,
+ ?int $targetsectionid = null,
+ ?int $targetcmid = null
+ ): void {
+ // Validate target elements.
+ if (!$targetsectionid && !$targetcmid) {
+ throw new moodle_exception("Action cm_move requires targetsectionid or targetcmid");
+ }
+
+ $this->validate_cms($course, $ids, __FUNCTION__);
+
+ // Check capabilities on every activity context.
+ foreach ($ids as $cmid) {
+ $modcontext = context_module::instance($cmid);
+ require_capability('moodle/course:manageactivities', $modcontext);
+ }
+
+ $modinfo = get_fast_modinfo($course);
+
+ // Target cm has more priority than target section.
+ if (!empty($targetcmid)) {
+ $this->validate_cms($course, [$targetcmid], __FUNCTION__);
+ $targetcm = $modinfo->get_cm($targetcmid);
+ $targetsection = $modinfo->get_section_info_by_id($targetcm->section, MUST_EXIST);
+ } else {
+ $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
+ $targetcm = null;
+ $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
+ }
+
+ // The origin sections must be updated as well.
+ $originalsections = [];
+
+ $cms = $this->get_cm_info($modinfo, $ids);
+ foreach ($cms as $cm) {
+ $currentsection = $modinfo->get_section_info_by_id($cm->section, MUST_EXIST);
+ moveto_module($cm, $targetsection, $targetcm);
+ $updates->add_cm_put($cm->id);
+ if ($currentsection->id != $targetsection->id) {
+ $originalsections[$currentsection->id] = true;
+ }
+ // If some of the original sections are also target sections, we don't need to update them.
+ if (array_key_exists($targetsection->id, $originalsections)) {
+ unset($originalsections[$targetsection->id]);
+ }
+ }
+
+ // Use section_state to return the full affected section and activities updated state.
+ $this->cm_state($updates, $course, $ids, $targetsectionid, $targetcmid);
+
+ foreach (array_keys($originalsections) as $sectionid) {
+ $updates->add_section_put($sectionid);
+ }
+ }
+
+ /**
+ * Move course sections to another location in the same course.
+ *
+ * @param stateupdates $updates the affected course elements track
+ * @param stdClass $course the course object
+ * @param int[] $ids the list of affected course module ids
+ * @param int $targetsectionid optional target section id
+ * @param int $targetcmid optional target cm id
+ */
+ public function section_move(
+ stateupdates $updates,
+ stdClass $course,
+ array $ids,
+ ?int $targetsectionid = null,
+ ?int $targetcmid = null
+ ): void {
+ // Validate target elements.
+ if (!$targetsectionid) {
+ throw new moodle_exception("Action cm_move requires targetsectionid");
+ }
+
+ $this->validate_sections($course, $ids, __FUNCTION__);
+
+ $coursecontext = context_course::instance($course->id);
+ require_capability('moodle/course:movesections', $coursecontext);
+
+ $modinfo = get_fast_modinfo($course);
+
+ // Target section.
+ $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
+ $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
+
+ $affectedsections = [$targetsection->section => true];
+
+ $sections = $this->get_section_info($modinfo, $ids);
+ foreach ($sections as $section) {
+ $affectedsections[$section->section] = true;
+ move_section_to($course, $section->section, $targetsection->section);
+ }
+
+ // Use section_state to return the section and activities updated state.
+ $this->section_state($updates, $course, $ids, $targetsectionid);
+
+ // All course sections can be renamed because of the resort.
+ $allsections = $modinfo->get_section_info_all();
+ foreach ($allsections as $section) {
+ // Ignore the affected sections because they are already in the updates.
+ if (isset($affectedsections[$section->section])) {
+ continue;
+ }
+ $updates->add_section_put($section->id);
+ }
+ // The section order is at a course level.
+ $updates->add_course_put();
+ }
+
+ /**
+ * Extract several cm_info from the course_modinfo.
+ *
+ * @param course_modinfo $modinfo the course modinfo.
+ * @param int[] $ids the course modules $ids
+ * @return cm_info[] the extracted cm_info objects
+ */
+ protected function get_cm_info (course_modinfo $modinfo, array $ids): array {
+ $cms = [];
+ foreach ($ids as $cmid) {
+ $cms[$cmid] = $modinfo->get_cm($cmid);
+ }
+ return $cms;
+ }
+
+ /**
+ * Extract several section_info from the course_modinfo.
+ *
+ * @param course_modinfo $modinfo the course modinfo.
+ * @param int[] $ids the course modules $ids
+ * @return section_info[] the extracted section_info objects
+ */
+ protected function get_section_info(course_modinfo $modinfo, array $ids): array {
+ $sections = [];
+ foreach ($ids as $sectionid) {
+ $sections[$sectionid] = $modinfo->get_section_info_by_id($sectionid);
+ }
+ return $sections;
+ }
+
/**
* Add the update messages of the updated version of any cm and section related to the cm ids.
*
diff --git a/course/format/templates/local/courseindex/cm.mustache b/course/format/templates/local/courseindex/cm.mustache
index c17b17a35e4..ef89ba58f29 100644
--- a/course/format/templates/local/courseindex/cm.mustache
+++ b/course/format/templates/local/courseindex/cm.mustache
@@ -56,6 +56,7 @@
{{{name}}}
{{/url}}
+ {{#pix}}i/dragdrop{{/pix}}
{{#js}}
require(['core_courseformat/local/courseindex/cm'], function(component) {
diff --git a/course/format/templates/local/courseindex/section.mustache b/course/format/templates/local/courseindex/section.mustache
index 91899cb16ff..96ba678a527 100644
--- a/course/format/templates/local/courseindex/section.mustache
+++ b/course/format/templates/local/courseindex/section.mustache
@@ -89,6 +89,7 @@
>
{{{title}}}
+ {{#pix}}i/dragdrop{{/pix}}
+{{#js}}
+require(['core_courseformat/local/courseindex/section'], function(component) {
+ component.init('{{uniqid}}-course-index-section-{{id}}');
+});
+{{/js}}
diff --git a/course/format/topics/lib.php b/course/format/topics/lib.php
index b0e84ef9659..d4e34e8a2c5 100644
--- a/course/format/topics/lib.php
+++ b/course/format/topics/lib.php
@@ -159,6 +159,10 @@ class format_topics extends core_courseformat\base {
return $ajaxsupport;
}
+ public function supports_components() {
+ return true;
+ }
+
/**
* Loads all of the course sections into the navigation.
*
diff --git a/course/format/upgrade.txt b/course/format/upgrade.txt
index bf173ba590a..ecf5c18a592 100644
--- a/course/format/upgrade.txt
+++ b/course/format/upgrade.txt
@@ -3,7 +3,8 @@ This files describes API changes for course formats
Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
=== 4.0 ===
-* New core_courseformat\uses_course_index() to define whether the course format uses course index or not.
+* New core_courseformat\base::uses_course_index() to define whether the course format uses course index or not.
+* New core_courseformat\base::supports_components() to specify if the format is compatible with reactive components.
=== 3.10 ===
* Added the missing callback supports_ajax() to format_social.
diff --git a/course/format/weeks/lib.php b/course/format/weeks/lib.php
index 5526b01f7b0..cdb38b355ac 100644
--- a/course/format/weeks/lib.php
+++ b/course/format/weeks/lib.php
@@ -160,6 +160,10 @@ class format_weeks extends core_courseformat\base {
return $ajaxsupport;
}
+ public function supports_components() {
+ return true;
+ }
+
/**
* Loads all of the course sections into the navigation
*
diff --git a/course/lib.php b/course/lib.php
index 1f6b8765e7f..5b9bd4f013c 100644
--- a/course/lib.php
+++ b/course/lib.php
@@ -3308,6 +3308,7 @@ function include_course_editor(course_format $format) {
// Edition mode and some format specs must be passed to the init method.
$setup = (object)[
'editing' => $format->show_editor(),
+ 'supportscomponents' => $format->supports_components(),
];
// All the new editor elements will be loaded after the course is presented and
// the initial course state will be generated using core_course_get_state webservice.
diff --git a/lib/amd/build/local/reactive/dragdrop.min.js b/lib/amd/build/local/reactive/dragdrop.min.js
new file mode 100644
index 00000000000..14fbdd1bdf1
--- /dev/null
+++ b/lib/amd/build/local/reactive/dragdrop.min.js
@@ -0,0 +1,2 @@
+define ("core/local/reactive/dragdrop",["exports","core/local/reactive/basecomponent"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);function c(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){c=function(a){return typeof a}}else{c=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return c(a)}function d(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function e(a,b){for(var c=0,d;c.\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 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._containsFiles(event)) {\n return undefined;\n }\n return activeDropData.get(this.reactive);\n }\n\n /**\n * Check if the dragged event contains files.\n *\n * Files dragging does not generate drop data because they came from outsite 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 _containsFiles(event) {\n if (event.dataTransfer.types) {\n for (var i = 0; i < event.dataTransfer.types.length; i++) {\n if (event.dataTransfer.types[i] == \"Files\") {\n return true;\n }\n }\n }\n return false;\n }\n}\n"],"file":"dragdrop.min.js"}
\ No newline at end of file
diff --git a/lib/amd/build/reactive.min.js b/lib/amd/build/reactive.min.js
index 3c8bf71f586..2ec39b1b507 100644
--- a/lib/amd/build/reactive.min.js
+++ b/lib/amd/build/reactive.min.js
@@ -1,2 +1,2 @@
-define ("core/reactive",["exports","core/local/reactive/basecomponent","core/local/reactive/reactive"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"BaseComponent",{enumerable:!0,get:function get(){return b.default}});Object.defineProperty(a,"Reactive",{enumerable:!0,get:function get(){return c.default}});b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}});
+define ("core/reactive",["exports","core/local/reactive/basecomponent","core/local/reactive/reactive","core/local/reactive/dragdrop"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"BaseComponent",{enumerable:!0,get:function get(){return b.default}});Object.defineProperty(a,"Reactive",{enumerable:!0,get:function get(){return c.default}});Object.defineProperty(a,"DragDrop",{enumerable:!0,get:function get(){return d.default}});b=e(b);c=e(c);d=e(d);function e(a){return a&&a.__esModule?a:{default:a}}});
//# sourceMappingURL=reactive.min.js.map
diff --git a/lib/amd/build/reactive.min.js.map b/lib/amd/build/reactive.min.js.map
index 9444c6216ef..3956add0c16 100644
--- a/lib/amd/build/reactive.min.js.map
+++ b/lib/amd/build/reactive.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/reactive.js"],"names":[],"mappings":"4WAuBA,OACA,O","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 * Generic reactive module used in the course editor.\n *\n * @module core/reactive\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';\nimport Reactive from 'core/local/reactive/reactive';\n\nexport {Reactive, BaseComponent};\n"],"file":"reactive.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../src/reactive.js"],"names":[],"mappings":"seAuBA,OACA,OACA,O","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 * Generic reactive module used in the course editor.\n *\n * @module core/reactive\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';\nimport Reactive from 'core/local/reactive/reactive';\nimport DragDrop from 'core/local/reactive/dragdrop';\n\nexport {Reactive, BaseComponent, DragDrop};\n"],"file":"reactive.min.js"}
\ No newline at end of file
diff --git a/lib/amd/src/local/reactive/dragdrop.js b/lib/amd/src/local/reactive/dragdrop.js
new file mode 100644
index 00000000000..e1ce8fdf449
--- /dev/null
+++ b/lib/amd/src/local/reactive/dragdrop.js
@@ -0,0 +1,465 @@
+// 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 .
+
+/**
+ * Drag and drop helper component.
+ *
+ * This component is used to delegate drag and drop handling.
+ *
+ * To delegate the logic to this particular element the component should create a new instance
+ * passing "this" as param. The component will use all the necessary callbacks and add all the
+ * necessary listeners to the component element.
+ *
+ * Component attributes used by dragdrop module:
+ * - element: the draggable or dropzone element.
+ * - (optional) classes: object with alternative CSS classes
+ * - (optional) fullregion: page element affeted by the elementy dragging. Use this attribute if
+ * the draggable element affects a bigger region (for example a draggable
+ * title).
+ * - (optional) autoconfigDraggable: by default, the component will be draggable if it has a
+ * getDraggableData method. If this value is false draggable
+ * property must be defined using setDraggable method.
+ * - (optional) relativeDrag: by default the drag image is located at point (0,0) relative to the
+ * mouse position to prevent the mouse from covering it. If this attribute
+ * is true the drag image will be located at the click offset.
+ *
+ * Methods the parent component should have for making it draggable:
+ *
+ * - getDraggableData(): Object|data
+ * Return the data that will be passed to any valid dropzone while it is dragged.
+ * If the component has this method, the dragdrop module will enable the dragging,
+ * this is the only required method for dragging.
+ * If at the dragging moment this method returns a false|null|undefined, the dragging
+ * actions won't be captured.
+ *
+ * - (optional) dragStart(Object dropdata, Event event): void
+ * - (optional) dragEnd(Object dropdata, Event event): void
+ * Callbacks dragdrop will call when the element is dragged and getDraggableData
+ * return some data.
+ *
+ * Methods the parent component should have for enabling it as a dropzone:
+ *
+ * - validateDropData(Object dropdata): boolean
+ * If that method exists, the dragdrop module will automathically configure the element as dropzone.
+ * This method will return true if the dropdata is accepted. In case it returns false, no drag and
+ * drop event will be listened for this specific dragged dropdata.
+ *
+ * - (Optional) showDropZone(Object dropdata, Event event): void
+ * - (Optional) hideDropZone(Object dropdata, Event event): void
+ * Methods called when a valid dragged data pass over the element.
+ *
+ * - (Optional) drop(Object dropdata, Event event): void
+ * Called when a valid dragged element is dropped over the element.
+ *
+ * Note that none of this methods will be called if validateDropData
+ * returns a false value.
+ *
+ * This module will also add or remove several CSS classes from both dragged elements and dropzones.
+ * See the "this.classes" in the create method for more details. In case the parent component wants
+ * to use the same classes, it can use the getClasses method. On the other hand, if the parent
+ * component has an alternative "classes" attribute, this will override the default drag and drop
+ * classes.
+ *
+ * @module core/local/reactive/dragdrop
+ * @class core/local/reactive/dragdrop
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import BaseComponent from 'core/local/reactive/basecomponent';
+
+// Map with the dragged element generate by an specific reactive applications.
+// Potentially, any component can generate a draggable element to interact with other
+// page elements. However, the dragged data is specific and could only interact with
+// components of the same reactive instance.
+let activeDropData = new Map();
+
+// Drag & Drop API provides the final drop point and incremental movements but we can
+// provide also starting points and displacements. Absolute displacements simplifies
+// moving components with aboslute position around the page.
+let dragStartPoint = {};
+
+export default class extends BaseComponent {
+
+ /**
+ * Constructor hook.
+ *
+ * @param {BaseComponent} parent the parent component.
+ */
+ create(parent) {
+ // Optional component name for debugging.
+ this.name = `${parent.name ?? 'unkown'}_dragdrop`;
+
+ // Default drag and drop classes.
+ this.classes = Object.assign(
+ {
+ // This class indicate a dragging action is active at a page level.
+ BODYDRAGGING: 'dragging',
+
+ // Added when draggable and drop are ready.
+ DRAGGABLEREADY: 'draggable',
+ DROPREADY: 'dropready',
+
+ // When a valid drag element is over the element.
+ DRAGOVER: 'dragover',
+ // When a the component is dragged.
+ DRAGGING: 'dragging',
+
+ // Dropzones classes names.
+ DROPUP: 'drop-up',
+ DROPDOWN: 'drop-down',
+ DROPZONE: 'drop-zone',
+
+ // Drag icon class.
+ DRAGICON: 'dragicon',
+ },
+ parent?.classes ?? {}
+ );
+
+ // Add the affected region if any.
+ this.fullregion = parent.fullregion;
+
+ // Keep parent to execute drap and drop handlers.
+ this.parent = parent;
+
+ // Check if parent handle draggable manually.
+ this.autoconfigDraggable = this.parent.draggable ?? true;
+
+ // Drag image relative position.
+ this.relativeDrag = this.parent.relativeDrag ?? false;
+
+ // Sub HTML elements will trigger extra dragEnter and dragOver all the time.
+ // To prevent that from affecting dropzones, we need to count the enters and leaves.
+ this.entercount = 0;
+
+ // Stores if the droparea is shown or not.
+ this.dropzonevisible = false;
+
+ }
+
+ /**
+ * Return the component drag and drop CSS classes.
+ *
+ * @returns {Object} the dragdrop css classes
+ */
+ getClasses() {
+ return this.classes;
+ }
+
+ /**
+ * Initial state ready method.
+ *
+ * This method will add all the necessary event listeners to the component depending on the
+ * parent methods.
+ * - Add drop events to the element if the parent component has validateDropData method.
+ * - Configure the elements draggable if the parent component has getDraggableData method.
+ */
+ stateReady() {
+ // Add drop events to the element if the parent component has dropable types.
+ if (typeof this.parent.validateDropData === 'function') {
+ 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);
+ }
+
+ // Configure the elements draggable if the parent component has dragable data.
+ if (this.autoconfigDraggable && typeof this.parent.getDraggableData === 'function') {
+ this.setDraggable(true);
+ }
+ }
+
+ /**
+ * Enable or disable the draggable property.
+ *
+ * @param {bool} value the new draggable value
+ */
+ setDraggable(value) {
+ if (typeof this.parent.getDraggableData !== 'function') {
+ throw new Error(`Draggable components must have a getDraggableData method`);
+ }
+ this.element.setAttribute('draggable', value);
+ if (value) {
+ this.addEventListener(this.element, 'dragstart', this._dragStart);
+ this.addEventListener(this.element, 'dragend', this._dragEnd);
+ this.element.classList.add(this.classes.DRAGGABLEREADY);
+ } else {
+ this.removeEventListener(this.element, 'dragstart', this._dragStart);
+ this.removeEventListener(this.element, 'dragend', this._dragEnd);
+ this.element.classList.remove(this.classes.DRAGGABLEREADY);
+ }
+ }
+
+ /**
+ * Drag start event handler.
+ *
+ * This method will generate the current dropable data. This data is the one used to determine
+ * if a droparea accepts the dropping or not.
+ *
+ * @param {Event} event the event.
+ */
+ _dragStart(event) {
+ const dropdata = this.parent.getDraggableData();
+ if (!dropdata) {
+ return;
+ }
+
+ // Save the starting point.
+ dragStartPoint = {
+ pageX: event.pageX,
+ pageY: event.pageY,
+ };
+
+ // If the drag event is accepted we prevent any other draggable element from interfiering.
+ event.stopPropagation();
+
+ // Save the drop data of the current reactive intance.
+ activeDropData.set(this.reactive, dropdata);
+
+ // Add some CSS classes to indicate the state.
+ document.body.classList.add(this.classes.BODYDRAGGING);
+ this.element.classList.add(this.classes.DRAGGING);
+ this.fullregion?.classList.add(this.classes.DRAGGING);
+
+ // Force the drag image. This makes the UX more consistent in case the
+ // user dragged an internal element like a link or some other element.
+ let dragImage = this.element;
+ if (this.parent.setDragImage !== undefined) {
+ const customImage = this.parent.setDragImage(dropdata, event);
+ if (customImage) {
+ dragImage = customImage;
+ }
+ }
+ // Define the image position relative to the mouse.
+ const position = {x: 0, y: 0};
+ if (this.relativeDrag) {
+ position.x = event.offsetX;
+ position.y = event.offsetY;
+ }
+ event.dataTransfer.setDragImage(dragImage, position.x, position.y);
+
+ this._callParentMethod('dragStart', dropdata, event);
+ }
+
+ /**
+ * Drag end event handler.
+ *
+ * @param {Event} event the event.
+ */
+ _dragEnd(event) {
+ const dropdata = activeDropData.get(this.reactive);
+ if (!dropdata) {
+ return;
+ }
+
+ // Remove the current dropdata.
+ activeDropData.delete(this.reactive);
+
+ // Remove the dragging classes.
+ document.body.classList.remove(this.classes.BODYDRAGGING);
+ this.element.classList.remove(this.classes.DRAGGING);
+ this.fullregion?.classList.remove(this.classes.DRAGGING);
+
+ // We add the total movement to the event in case the component
+ // wants to move its absolute position.
+ this._addEventTotalMovement(event);
+
+ this._callParentMethod('dragEnd', dropdata, event);
+ }
+
+ /**
+ * Drag enter event handler.
+ *
+ * The JS drag&drop API triggers several dragenter events on the same element because it bubbles the
+ * child events as well. To prevent this form affecting the dropzones display, this methods use
+ * "entercount" to determine if it's one extra child event or a valid one.
+ *
+ * @param {Event} event the event.
+ */
+ _dragEnter(event) {
+ const dropdata = this._processEvent(event);
+ if (dropdata) {
+ this.entercount++;
+ this.element.classList.add(this.classes.DRAGOVER);
+ if (this.entercount == 1 && !this.dropzonevisible) {
+ this.dropzonevisible = true;
+ this.element.classList.add(this.classes.DRAGOVER);
+ this._callParentMethod('showDropZone', dropdata, event);
+ }
+ }
+ }
+
+ /**
+ * Drag over event handler.
+ *
+ * We only use dragover event when a draggable action starts inside a valid dropzone. In those cases
+ * the API won't trigger any dragEnter because the dragged alement was already there. We use the
+ * dropzonevisible to determine if the component needs to display the dropzones or not.
+ *
+ * @param {Event} event the event.
+ */
+ _dragOver(event) {
+ const dropdata = this._processEvent(event);
+ if (dropdata && !this.dropzonevisible) {
+ this.dropzonevisible = true;
+ this.element.classList.add(this.classes.DRAGOVER);
+ this._callParentMethod('showDropZone', dropdata, event);
+ }
+ }
+
+ /**
+ * Drag over leave handler.
+ *
+ * The JS drag&drop API triggers several dragleave events on the same element because it bubbles the
+ * child events as well. To prevent this form affecting the dropzones display, this methods use
+ * "entercount" to determine if it's one extra child event or a valid one.
+ *
+ * @param {Event} event the event.
+ */
+ _dragLeave(event) {
+ const dropdata = this._processEvent(event);
+ if (dropdata) {
+ this.entercount--;
+ if (this.entercount == 0 && this.dropzonevisible) {
+ this.dropzonevisible = false;
+ this.element.classList.remove(this.classes.DRAGOVER);
+ this._callParentMethod('hideDropZone', dropdata, event);
+ }
+ }
+ }
+
+ /**
+ * Drop event handler.
+ *
+ * This method will call both hideDropZones and drop methods on the parent component.
+ *
+ * @param {Event} event the event.
+ */
+ _drop(event) {
+ const dropdata = this._processEvent(event);
+ if (dropdata) {
+ this.entercount = 0;
+ if (this.dropzonevisible) {
+ this.dropzonevisible = false;
+ this._callParentMethod('hideDropZone', dropdata, event);
+ }
+ this.element.classList.remove(this.classes.DRAGOVER);
+ this._callParentMethod('drop', dropdata, event);
+ // An accepted drop resets the initial position.
+ // Save the starting point.
+ dragStartPoint = {};
+ }
+ }
+
+ /**
+ * Process a drag and drop event and delegate logic to the parent component.
+ *
+ * @param {Event} event the drag and drop event
+ * @return {Object|false} the dropdata or null if the event should not be processed
+ */
+ _processEvent(event) {
+ const dropdata = this._getDropData(event);
+ if (!dropdata) {
+ return null;
+ }
+ if (this.parent.validateDropData(dropdata)) {
+ // All accepted drag&drop event must prevent bubbling and defaults, otherwise
+ // parent dragdrop instances could capture it by mistake.
+ event.preventDefault();
+ event.stopPropagation();
+ this._addEventTotalMovement(event);
+ return dropdata;
+ }
+ return null;
+ }
+
+ /**
+ * Add the total amout of movement to a mouse event.
+ *
+ * @param {MouseEvent} event
+ */
+ _addEventTotalMovement(event) {
+ if (dragStartPoint.pageX === undefined || event.pageX === undefined) {
+ return;
+ }
+ event.fixedMovementX = event.pageX - dragStartPoint.pageX;
+ event.fixedMovementY = event.pageY - dragStartPoint.pageY;
+ event.initialPageX = dragStartPoint.pageX;
+ event.initialPageY = dragStartPoint.pageY;
+ // The element possible new top.
+ const current = this.element.getBoundingClientRect();
+ // Add the new position fixed position.
+ event.newFixedTop = current.top + event.fixedMovementY;
+ event.newFixedLeft = current.left + event.fixedMovementX;
+ // The affected region possible new top.
+ if (this.fullregion !== undefined) {
+ const current = this.fullregion.getBoundingClientRect();
+ event.newRegionFixedxTop = current.top + event.fixedMovementY;
+ event.newRegionFixedxLeft = current.left + event.fixedMovementX;
+ }
+ }
+
+ /**
+ * Convenient method for calling parent component functions if present.
+ *
+ * @param {string} methodname the name of the method
+ * @param {Object} dropdata the current drop data object
+ * @param {Event} event the original event
+ */
+ _callParentMethod(methodname, dropdata, event) {
+ if (typeof this.parent[methodname] === 'function') {
+ this.parent[methodname](dropdata, event);
+ }
+ }
+
+ /**
+ * Get the current dropdata for a specific event.
+ *
+ * The browser can generate drag&drop events related to several user interactions:
+ * - Drag a page elements: this case is registered in the activeDropData map
+ * - Drag some HTML selections: ignored for now
+ * - Drag a file over the browser: file drag may appear in the future but for now they are ignored.
+ *
+ * @param {Event} event the original event.
+ * @returns {Object|undefined} with the dragged data (or undefined if none)
+ */
+ _getDropData(event) {
+ if (this._containsFiles(event)) {
+ return undefined;
+ }
+ return activeDropData.get(this.reactive);
+ }
+
+ /**
+ * Check if the dragged event contains files.
+ *
+ * Files dragging does not generate drop data because they came from outsite the page and the component
+ * must check it before validating the event.
+ *
+ * @param {Event} event the original event.
+ * @returns {boolean} if the drag dataTransfers contains files.
+ */
+ _containsFiles(event) {
+ if (event.dataTransfer.types) {
+ for (var i = 0; i < event.dataTransfer.types.length; i++) {
+ if (event.dataTransfer.types[i] == "Files") {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/lib/amd/src/reactive.js b/lib/amd/src/reactive.js
index ac621b8e841..317f3ca7601 100644
--- a/lib/amd/src/reactive.js
+++ b/lib/amd/src/reactive.js
@@ -23,5 +23,6 @@
import BaseComponent from 'core/local/reactive/basecomponent';
import Reactive from 'core/local/reactive/reactive';
+import DragDrop from 'core/local/reactive/dragdrop';
-export {Reactive, BaseComponent};
+export {Reactive, BaseComponent, DragDrop};
diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss
index fe42c6447ea..8d9705a5c81 100644
--- a/theme/boost/scss/moodle/core.scss
+++ b/theme/boost/scss/moodle/core.scss
@@ -5,6 +5,8 @@
// want white default colour.
$bg-inverse-link-color: #fff !default;
+$dropzone-border: $gray-900 !default;
+
$font-size-xs: ($font-size-base * .75) !default;
#region-main {
@@ -2784,3 +2786,32 @@ $scrollbar-track: lighten($primary, 40%);
border-right: $border-width solid $white;
}
}
+// Generic dropzones and dragging styles.
+
+body.dragging {
+
+ .drop-zone {
+ border: 2px dashed $dropzone-border;
+ }
+
+ .drop-up {
+ border-top: 2px solid $dropzone-border;
+ }
+
+ .drop-down {
+ border-bottom: 2px solid $dropzone-border;
+ }
+
+ .dragging {
+ opacity: .6;
+ }
+}
+
+.dragicon {
+ visibility: hidden;
+}
+
+.draggable:hover .dragicon {
+ visibility: visible;
+ cursor: move;
+}
diff --git a/theme/boost/scss/moodle/courseindex.scss b/theme/boost/scss/moodle/courseindex.scss
index 33e32e764c8..f14faa3b303 100644
--- a/theme/boost/scss/moodle/courseindex.scss
+++ b/theme/boost/scss/moodle/courseindex.scss
@@ -56,8 +56,15 @@ $courseindex-item-current: $primary !default;
}
}
- .courseindex-section.current {
- border-left: solid 3px $courseindex-item-current;
+ .courseindex-section {
+ &.current {
+ border-left: solid 3px $courseindex-item-current;
+ }
+
+ &.dropready .courseindex-item-content {
+ /* Extra dropzone space */
+ padding-bottom: 1em;
+ }
}
.d-flex-noedit {
diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css
index cc89d9003ce..2af2b498a48 100644
--- a/theme/boost/style/moodle.css
+++ b/theme/boost/style/moodle.css
@@ -11944,6 +11944,25 @@ input[disabled] {
background-color: #a8d2f8;
border-right: 1px solid #fff; }
+body.dragging .drop-zone {
+ border: 2px dashed #212529; }
+
+body.dragging .drop-up {
+ border-top: 2px solid #212529; }
+
+body.dragging .drop-down {
+ border-bottom: 2px solid #212529; }
+
+body.dragging .dragging {
+ opacity: .6; }
+
+.dragicon {
+ visibility: hidden; }
+
+.draggable:hover .dragicon {
+ visibility: visible;
+ cursor: move; }
+
.icon {
font-size: 16px;
width: 16px;
@@ -20124,6 +20143,10 @@ div.editor_atto_toolbar button .icon {
.courseindex .courseindex-section.current {
border-left: solid 3px #0f6fc5; }
+.courseindex .courseindex-section.dropready .courseindex-item-content {
+ /* Extra dropzone space */
+ padding-bottom: 1em; }
+
.courseindex .d-flex-noedit {
display: none; }
diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css
index 75bd52a5002..bae611db3a9 100644
--- a/theme/classic/style/moodle.css
+++ b/theme/classic/style/moodle.css
@@ -12165,6 +12165,25 @@ input[disabled] {
background-color: #a8d2f8;
border-right: 1px solid #fff; }
+body.dragging .drop-zone {
+ border: 2px dashed #212529; }
+
+body.dragging .drop-up {
+ border-top: 2px solid #212529; }
+
+body.dragging .drop-down {
+ border-bottom: 2px solid #212529; }
+
+body.dragging .dragging {
+ opacity: .6; }
+
+.dragicon {
+ visibility: hidden; }
+
+.draggable:hover .dragicon {
+ visibility: visible;
+ cursor: move; }
+
.icon {
font-size: 16px;
width: 16px;
@@ -20316,6 +20335,10 @@ div.editor_atto_toolbar button .icon {
.courseindex .courseindex-section.current {
border-left: solid 3px #0f6fc5; }
+.courseindex .courseindex-section.dropready .courseindex-item-content {
+ /* Extra dropzone space */
+ padding-bottom: 1em; }
+
.courseindex .d-flex-noedit {
display: none; }