diff --git a/course/format/amd/build/courseeditor.min.js b/course/format/amd/build/courseeditor.min.js
index a2f4a1fec18..7a1a39514d1 100644
--- a/course/format/amd/build/courseeditor.min.js
+++ b/course/format/amd/build/courseeditor.min.js
@@ -1,2 +1,2 @@
-define ("core_courseformat/courseeditor",["exports","core_courseformat/local/courseeditor/mutations","core_courseformat/local/courseeditor/courseeditor","core_course/events"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.getCurrentCourseEditor=a.getCourseEditor=void 0;b=e(b);c=e(c);d=e(d);function e(a){return a&&a.__esModule?a:{default:a}}var g=new Map;function f(a,b){if(b===void 0){b=document}b.dispatchEvent(new CustomEvent(d.default.stateChanged,{bubbles:!0,detail:a}))}var h=function(a){a=parseInt(a);if(!g.has(a)){g.set(a,new c.default({name:"CourseEditor".concat(a),eventName:d.default.stateChanged,eventDispatch:f,mutations:new b.default}));g.get(a).loadCourse(a)}return g.get(a)};a.getCourseEditor=h;a.getCurrentCourseEditor=function getCurrentCourseEditor(){return h(M.cfg.courseId)}});
+define ("core_courseformat/courseeditor",["exports","core_courseformat/local/courseeditor/mutations","core_courseformat/local/courseeditor/courseeditor","core_course/events"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.getCurrentCourseEditor=a.getCourseEditor=a.setViewFormat=void 0;b=e(b);c=e(c);d=e(d);function e(a){return a&&a.__esModule?a:{default:a}}var g=new Map;function f(a,b){if(b===void 0){b=document}b.dispatchEvent(new CustomEvent(d.default.stateChanged,{bubbles:!0,detail:a}))}a.setViewFormat=function setViewFormat(a,b){var c=h(a);c.setViewFormat(b)};var h=function(a){a=parseInt(a);if(!g.has(a)){g.set(a,new c.default({name:"CourseEditor".concat(a),eventName:d.default.stateChanged,eventDispatch:f,mutations:new b.default}));g.get(a).loadCourse(a)}return g.get(a)};a.getCourseEditor=h;a.getCurrentCourseEditor=function getCurrentCourseEditor(){return h(M.cfg.courseId)}});
//# sourceMappingURL=courseeditor.min.js.map
diff --git a/course/format/amd/build/courseeditor.min.js.map b/course/format/amd/build/courseeditor.min.js.map
index 91ff5432a5c..fd66829a1d4 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","getCourseEditor","courseId","parseInt","has","set","CourseEditor","name","eventName","eventDispatch","mutations","DefaultMutations","get","loadCourse","getCurrentCourseEditor","M","cfg"],"mappings":"iTAuBA,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,CAQM,GAAMQ,CAAAA,CAAe,CAAG,SAACC,CAAD,CAAc,CACzCA,CAAQ,CAAGC,QAAQ,CAACD,CAAD,CAAnB,CAEA,GAAI,CAACZ,CAAe,CAACc,GAAhB,CAAoBF,CAApB,CAAL,CAAoC,CAChCZ,CAAe,CAACe,GAAhB,CACIH,CADJ,CAEI,GAAII,UAAJ,CAAiB,CACbC,IAAI,uBAAiBL,CAAjB,CADS,CAEbM,SAAS,CAAEV,UAAOC,YAFL,CAGbU,aAAa,CAAEjB,CAHF,CAMbkB,SAAS,CAAE,GAAIC,UANF,CAAjB,CAFJ,EAWArB,CAAe,CAACsB,GAAhB,CAAoBV,CAApB,EAA8BW,UAA9B,CAAyCX,CAAzC,CACH,CACD,MAAOZ,CAAAA,CAAe,CAACsB,GAAhB,CAAoBV,CAApB,CACV,CAlBM,C,6CAyB+B,QAAzBY,CAAAA,sBAAyB,SAAMb,CAAAA,CAAe,CAACc,CAAC,CAACC,GAAF,CAAMd,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 * 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,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
diff --git a/course/format/amd/build/local/courseeditor/courseeditor.min.js b/course/format/amd/build/local/courseeditor/courseeditor.min.js
index bafc4211d2d..21e7feb77ba 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/log","core/ajax"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;c=f(c);d=f(d);e=f(e);function f(a){return a&&a.__esModule?a:{default:a}}function g(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){g=function(a){return typeof a}}else{g=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return g(a)}function h(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 i(a){for(var b=1,c;b.\n\nimport {Reactive} from 'core/reactive';\nimport notification from 'core/notification';\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 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 // Edit mode is part of the state but it could change over time.\n // Components should use isEditing method to check the editing mode instead.\n this._editing = stateData.course.editmode ?? false;\n\n this.setInitialState(stateData);\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 * 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","getServerCourseState","stateData","log","error","setInitialState","setup","editing","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,IAEA,KAAKF,QAAL,CAAgBA,CAAhB,C,wBAKsB,MAAKG,oBAAL,E,QAAlBC,C,6DAEAC,UAAIC,KAAJ,CAAU,2CAAV,EACAD,UAAIC,KAAJ,O,kCAIJ,KAAKC,eAAL,CAAqBH,CAArB,E,6KASUI,C,CAAO,OACjB,KAAKN,QAAL,WAAgBM,CAAK,CAACC,OAAtB,kBACH,C,oMAQ6BC,WAAKC,IAAL,CAAU,CAAC,CACjCC,UAAU,CAAE,6BADqB,CAEjCC,IAAI,CAAE,CACFC,QAAQ,CAAE,KAAKd,QADb,CAF2B,CAAD,CAAV,EAKtB,CALsB,C,QAApBe,C,QAOAX,C,CAAYY,IAAI,CAACC,KAAL,CAAWF,CAAX,C,6BAGdG,MAAM,CAAE,E,CACRC,OAAO,CAAE,E,CACTC,EAAE,CAAE,E,EACDhB,C,6KAoBG,CACV,MAAO,IAAIiB,UAAJ,CAAa,IAAb,CACV,C,iNAYiBR,C,uBAAAA,C,yFAEcA,C,4DAExBS,UAAaC,SAAb,O,0JA3BQ,OACZ,iBAAO,KAAKrB,QAAZ,kBACH,C,cA3EwBsB,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\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
diff --git a/course/format/amd/build/local/courseeditor/exporter.min.js b/course/format/amd/build/local/courseeditor/exporter.min.js
new file mode 100644
index 00000000000..7f492602506
--- /dev/null
+++ b/course/format/amd/build/local/courseeditor/exporter.min.js
@@ -0,0 +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
diff --git a/course/format/amd/build/local/courseeditor/mutations.min.js b/course/format/amd/build/local/courseeditor/mutations.min.js
index 8ea3e503ec7..c247ae2c35b 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 state = statemanager.state;\n const updates = await this._callEditWebservice('cm_state', state.course.id, cmids);\n statemanager.setReadOnly(false);\n this._processUpdates(statemanager, 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 state = statemanager.state;\n const updates = await this._callEditWebservice('section_state', state.course.id, sectionIds);\n this._processUpdates(statemanager, updates);\n }\n\n /**\n * Helper to propcess both section_state and cm_state action results.\n *\n * @param {StateManager} statemanager the current state\n * @param {Array} updates of updates.\n */\n _processUpdates(statemanager, updates) {\n\n const state = statemanager.state;\n\n statemanager.setReadOnly(false);\n\n // The cm_state and section_state state action returns only updated states. However, most of the time we need this\n // mutation to fix discrepancies between the course content and the course state because core_course_edit_module\n // does not provide enough information to rebuild some state objects. This is the reason why we cannot use\n // the batch method processUpdates as the rest of mutations do.\n updates.forEach((update) => {\n if (update.name === undefined) {\n throw Error('Missing state update name');\n }\n // Compare the action with the current state.\n let current = state[update.name];\n if (current instanceof Map) {\n current = state[update.name].get(update.fields.id);\n }\n if (!current) {\n update.action = 'create';\n }\n\n statemanager.processUpdate(update.name, update.action, update.fields);\n });\n\n statemanager.setReadOnly(true);\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","ajax","call","methodname","args","courseid","ajaxresult","JSON","parse","stateManager","cmids","course","get","_callEditWebservice","id","updates","processUpdates","sectionIds"],"mappings":"8KAeA,uD,owBAsB8BA,C,CAAQC,C,CAAUC,C,wGACjBC,WAAKC,IAAL,CAAU,CAAC,CAC9BC,UAAU,CAAE,iCADkB,CAE9BC,IAAI,CAAE,CACFN,MAAM,CAANA,CADE,CAEFO,QAAQ,CAAEN,CAFR,CAGFC,GAAG,CAAHA,CAHE,CAFwB,CAAD,CAAV,EAOnB,CAPmB,C,QAAnBM,C,iCAQGC,IAAI,CAACC,KAAL,CAAWF,CAAX,C,kMAUGG,C,CAAcC,C,2FAClBC,C,CAASF,CAAY,CAACG,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,UAAzB,CAAqCF,CAAM,CAACG,EAA5C,CAAgDJ,CAAhD,C,QAAhBK,C,QACNN,CAAY,CAACO,cAAb,CAA4BD,CAA5B,E,8LAUeN,C,CAAcQ,C,2FACvBN,C,CAASF,CAAY,CAACG,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,eAAzB,CAA0CF,CAAM,CAACG,EAAjD,CAAqDG,CAArD,C,QAAhBF,C,QACNN,CAAY,CAACO,cAAb,CAA4BD,CAA5B,E,kMAQcN,C,2FACRE,C,CAASF,CAAY,CAACG,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,cAAzB,CAAyCF,CAAM,CAACG,EAAhD,C,QAAhBC,C,QACNN,CAAY,CAACO,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 */\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
diff --git a/course/format/amd/build/local/courseindex/cm.min.js b/course/format/amd/build/local/courseindex/cm.min.js
new file mode 100644
index 00000000000..9c94cb35821
--- /dev/null
+++ b/course/format/amd/build/local/courseindex/cm.min.js
@@ -0,0 +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
diff --git a/course/format/amd/build/local/courseindex/courseindex.min.js b/course/format/amd/build/local/courseindex/courseindex.min.js
new file mode 100644
index 00000000000..27b49461a36
--- /dev/null
+++ b/course/format/amd/build/local/courseindex/courseindex.min.js
@@ -0,0 +1,2 @@
+define ("core_courseformat/local/courseindex/courseindex",["exports","core/reactive","core_courseformat/courseeditor"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function d(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){d=function(a){return typeof a}}else{d=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return d(a)}function e(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 f(a){return function(){var b=this,c=arguments;return new Promise(function(d,f){var i=a.apply(b,c);function g(a){e(i,d,f,g,h,"next",a)}function h(a){e(i,d,f,g,h,"throw",a)}g(void 0)})}}function g(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function h(a,b){for(var c=0,d;cb.length){a.removeChild(a.lastChild)}}},{key:"_deleteCm",value:function _deleteCm(a){var b=a.element;delete this.cms[b.id]}}],[{key:"init",value:function init(a,d){return new b({element:document.getElementById(a),reactive:(0,c.getCurrentCourseEditor)(),selectors:d})}}]);return b}(b.BaseComponent);a.default=q;return a.default});
+//# sourceMappingURL=courseindex.min.js.map
diff --git a/course/format/amd/build/local/courseindex/courseindex.min.js.map b/course/format/amd/build/local/courseindex/courseindex.min.js.map
new file mode 100644
index 00000000000..9394481ed78
--- /dev/null
+++ b/course/format/amd/build/local/courseindex/courseindex.min.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../../../src/local/courseindex/courseindex.js"],"names":["Component","name","selectors","SECTION","SECTION_ITEM","SECTION_TITLE","SECTION_CMLIST","CM","CM_NAME","TOGGLER","COLLAPSE","classes","SECTIONHIDDEN","CMHIDDEN","SECTIONCURRENT","sections","cms","addEventListener","element","_setupSectionTogglers","getElements","forEach","section","dataset","id","cm","watch","handler","_refreshSection","_refreshCm","_createCm","_deleteCm","_refreshCourseSectionlist","_refreshSectionCmlist","event","sectionlink","target","closest","preventDefault","parentNode","querySelector","click","Error","sectionitem","classList","toggle","visible","current","innerHTML","title","state","fakeelement","document","createElement","add","get","sectionid","exporter","reactive","getExporter","data","renderComponent","newcomponent","newelement","getElement","replaceChild","cmlist","listparent","_fixOrder","sectionlist","container","neworder","allitems","length","remove","itemid","index","item","currentitem","children","append","insertBefore","removeChild","lastChild","getElementById","BaseComponent"],"mappings":"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","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 main component.\n *\n * @module core_courseformat/local/courseindex/courseindex\n * @class core_courseformat/local/courseindex/courseindex\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';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\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';\n // Default query selectors.\n this.selectors = {\n SECTION: `[data-for='section']`,\n SECTION_ITEM: `[data-for='section_item']`,\n SECTION_TITLE: `[data-for='section_title']`,\n SECTION_CMLIST: `[data-for='cmlist']`,\n CM: `[data-for='cm']`,\n CM_NAME: `[data-for='cm_name']`,\n TOGGLER: `[data-action=\"togglecourseindexsection\"]`,\n COLLAPSE: `[data-toggle=\"collapse\"]`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n CMHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n };\n // Arrays to keep cms and sections elements.\n this.sections = {};\n this.cms = {};\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 reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n // Activate section togglers.\n this.addEventListener(this.element, 'click', this._setupSectionTogglers);\n\n // Get cms and sections elements.\n const sections = this.getElements(this.selectors.SECTION);\n sections.forEach((section) => {\n this.sections[section.dataset.id] = section;\n });\n const cms = this.getElements(this.selectors.CM);\n cms.forEach((cm) => {\n this.cms[cm.dataset.id] = cm;\n });\n }\n\n getWatchers() {\n return [\n {watch: `section:updated`, handler: this._refreshSection},\n {watch: `cm:updated`, handler: this._refreshCm},\n {watch: `cm:created`, handler: this._createCm},\n {watch: `cm:deleted`, handler: this._deleteCm},\n // Sections and cm sorting.\n {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},\n {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},\n ];\n }\n\n /**\n * Setup sections toggler.\n *\n * Toggler click is delegated to the main course index element because new sections can\n * appear at any moment and this way we prevent accidental double bindings.\n *\n * @param {Event} event the triggered event\n */\n _setupSectionTogglers(event) {\n const sectionlink = event.target.closest(this.selectors.TOGGLER);\n if (sectionlink) {\n event.preventDefault();\n sectionlink.parentNode.querySelector(this.selectors.COLLAPSE).click();\n }\n }\n\n /**\n * Update a course index section using the state information.\n *\n * @param {Object} details the update details.\n */\n _refreshSection({element}) {\n // Find the element.\n const target = this.sections[element.id];\n if (!target) {\n throw new Error(`Unkown section with ID ${element.id}`);\n }\n // Update classes.\n const sectionitem = target.querySelector(this.selectors.SECTION_ITEM);\n sectionitem.classList.toggle(this.classes.SECTIONHIDDEN, !element.visible);\n target.classList.toggle(this.classes.SECTIONCURRENT, element.current);\n // Update title.\n target.querySelector(this.selectors.SECTION_TITLE).innerHTML = element.title;\n }\n\n /**\n * Update a course index cm using the state information.\n *\n * @param {Object} details the update details.\n */\n _refreshCm({element}) {\n // Find the element.\n const target = this.cms[element.id];\n if (!target) {\n throw new Error(`Unkown course module with ID ${element.id}`);\n }\n // Update classes.\n target.classList.toggle(this.classes.CMHIDDEN, !element.visible);\n target.querySelector(this.selectors.CM_NAME).innerHTML = element.name;\n }\n\n /**\n * Create a newcm instance.\n *\n * @param {Object} details the update details.\n */\n async _createCm({state, element}) {\n // Create a fake node while the component is loading.\n const fakeelement = document.createElement('li');\n fakeelement.classList.add('bg-pulse-grey', 'w-100');\n fakeelement.innerHTML = ' ';\n this.cms[element.id] = fakeelement;\n // Place the fake node on the correct position.\n this._refreshSectionCmlist({\n state,\n element: state.section.get(element.sectionid),\n });\n // Collect render data.\n const exporter = this.reactive.getExporter();\n const data = exporter.cm(state, element);\n // Create the new content.\n const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/cm', data);\n // Replace the fake node with the real content.\n const newelement = newcomponent.getElement();\n this.cms[element.id] = newelement;\n fakeelement.parentNode.replaceChild(newelement, fakeelement);\n }\n\n /**\n * Refresh a section cm list.\n *\n * @param {Object} details the update details.\n */\n _refreshSectionCmlist({element}) {\n const cmlist = element.cmlist ?? [];\n const listparent = this.getElement(this.selectors.SECTION_CMLIST, element.id);\n this._fixOrder(listparent, cmlist, this.cms);\n }\n\n /**\n * Refresh the section list.\n *\n * @param {Object} details the update details.\n */\n _refreshCourseSectionlist({element}) {\n const sectionlist = element.sectionlist ?? [];\n this._fixOrder(this.element, sectionlist, this.sections);\n }\n\n /**\n * Fix/reorder the section or cms order.\n *\n * @param {Element} container the HTML element to reorder.\n * @param {Array} neworder an array with the ids order\n * @param {Array} allitems the list of html elements that can be placed in the container\n */\n _fixOrder(container, neworder, allitems) {\n\n // Empty lists should not be visible.\n if (!neworder.length) {\n container.classList.add('hidden');\n container.innerHTML = '';\n return;\n }\n\n // Grant the list is visible (in case it was empty).\n container.classList.remove('hidden');\n\n // Move the elements in order at the beginning of the list.\n neworder.forEach((itemid, index) => {\n const item = allitems[itemid];\n // Get the current element at that position.\n const currentitem = container.children[index];\n if (currentitem === undefined) {\n container.append(item);\n return;\n }\n if (currentitem !== item) {\n container.insertBefore(item, currentitem);\n }\n });\n // Remove the remaining elements.\n while (container.children.length > neworder.length) {\n container.removeChild(container.lastChild);\n }\n }\n\n /**\n * Remove a cm from the list.\n *\n * The actual DOM element removal is delegated to the cm component.\n *\n * @param {Object} details the update details.\n */\n _deleteCm({element}) {\n delete this.cms[element.id];\n }\n}\n"],"file":"courseindex.min.js"}
\ No newline at end of file
diff --git a/course/format/amd/build/local/courseindex/drawer.min.js b/course/format/amd/build/local/courseindex/drawer.min.js
new file mode 100644
index 00000000000..d468afa12f6
--- /dev/null
+++ b/course/format/amd/build/local/courseindex/drawer.min.js
@@ -0,0 +1,2 @@
+define ("core_courseformat/local/courseindex/drawer",["exports","core/reactive","core_courseformat/courseeditor"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function d(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){d=function(a){return typeof a}}else{d=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return d(a)}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\n/**\n * Course index drawer wrap.\n *\n * This component is mostly used to ensure all subcomponents find a parent\n * compoment with a reactive instance defined.\n *\n * @module core_courseformat/local/courseindex/drawer\n * @class core_courseformat/local/courseindex/drawer\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';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\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-drawer';\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 reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n}\n"],"file":"drawer.min.js"}
\ No newline at end of file
diff --git a/course/format/amd/build/local/courseindex/placeholder.min.js b/course/format/amd/build/local/courseindex/placeholder.min.js
new file mode 100644
index 00000000000..1cc9a5f28d4
--- /dev/null
+++ b/course/format/amd/build/local/courseindex/placeholder.min.js
@@ -0,0 +1,2 @@
+define ("core_courseformat/local/courseindex/placeholder",["exports","core/reactive","core/templates","core_courseformat/courseeditor"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;c=function(a){return a&&a.__esModule?a:{default:a}}(c);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,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 g(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function g(a){f(i,d,e,g,h,"next",a)}function h(a){f(i,d,e,g,h,"throw",a)}g(void 0)})}}function h(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function i(a,b){for(var c=0,d;c.\n\n/**\n * Course index placeholder replacer.\n *\n * @module core_courseformat/local/courseindex/placeholder\n * @class core_courseformat/local/courseindex/placeholder\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';\nimport Templates from 'core/templates';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\n\nexport default class Component extends BaseComponent {\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 reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * This stateReady to be async because it loads the real courseindex.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(state);\n\n try {\n // To render an HTML into our component we just use the regular Templates module.\n const {html, js} = await Templates.renderForPromise(\n 'core_courseformat/local/courseindex/courseindex',\n data,\n );\n Templates.replaceNode(this.element, html, js);\n } catch (error) {\n throw error;\n }\n }\n}\n"],"file":"placeholder.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 288611be3ca..a17a5f91a96 100644
--- a/course/format/amd/src/courseeditor.js
+++ b/course/format/amd/src/courseeditor.js
@@ -48,6 +48,18 @@ function dispatchStateChangedEvent(detail, target) {
}));
}
+/**
+ * Setup the current view settings
+ *
+ * @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
+ */
+export const setViewFormat = (courseId, setup) => {
+ const editor = getCourseEditor(courseId);
+ editor.setViewFormat(setup);
+};
+
/**
* Get a specific course editor reactive instance.
*
diff --git a/course/format/amd/src/local/courseeditor/courseeditor.js b/course/format/amd/src/local/courseeditor/courseeditor.js
index fb4a494c101..557841715de 100644
--- a/course/format/amd/src/local/courseeditor/courseeditor.js
+++ b/course/format/amd/src/local/courseeditor/courseeditor.js
@@ -15,6 +15,7 @@
import {Reactive} from 'core/reactive';
import notification from 'core/notification';
+import Exporter from 'core_courseformat/local/courseeditor/exporter';
import log from 'core/log';
import ajax from 'core/ajax';
@@ -44,6 +45,9 @@ export default class extends Reactive {
throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);
}
+ // Default view format setup.
+ this._editing = false;
+
this.courseId = courseId;
let stateData;
@@ -56,13 +60,19 @@ export default class extends Reactive {
return;
}
- // Edit mode is part of the state but it could change over time.
- // Components should use isEditing method to check the editing mode instead.
- this._editing = stateData.course.editmode ?? false;
-
this.setInitialState(stateData);
}
+ /**
+ * Setup the current view settings
+ *
+ * @param {Object} setup format, page and course settings
+ * @property {boolean} setup.editing if the page is in edit mode
+ */
+ setViewFormat(setup) {
+ this._editing = setup.editing ?? false;
+ }
+
/**
* Load the current course state from the server.
*
@@ -97,6 +107,15 @@ export default class extends Reactive {
return this._editing ?? false;
}
+ /**
+ * Return a data exporter to transform state part into mustache contexts.
+ *
+ * @return {Exporter} the exporter class
+ */
+ getExporter() {
+ return new Exporter(this);
+ }
+
/**
* Dispatch a change in the state.
*
diff --git a/course/format/amd/src/local/courseeditor/exporter.js b/course/format/amd/src/local/courseeditor/exporter.js
new file mode 100644
index 00000000000..80b9ebf18b4
--- /dev/null
+++ b/course/format/amd/src/local/courseeditor/exporter.js
@@ -0,0 +1,97 @@
+// 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 .
+
+/**
+ * Module to export parts of the state and transform them to be used in templates
+ * and as draggable data.
+ *
+ * @module core_courseformat/local/courseeditor/exporter
+ * @class core_courseformat/local/courseeditor/exporter
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+export default class {
+
+ /**
+ * Class constructor.
+ *
+ * @param {CourseEditor} reactive the course editor object
+ */
+ constructor(reactive) {
+ this.reactive = reactive;
+ }
+
+ /**
+ * Generate the course export data from the state.
+ *
+ * @param {Object} state the current state.
+ * @returns {Object}
+ */
+ course(state) {
+ // Collect section information from the state.
+ const data = {
+ sections: [],
+ editmode: this.reactive.isEditing,
+ };
+ const sectionlist = state.course.sectionlist ?? [];
+ sectionlist.forEach(sectionid => {
+ const sectioninfo = state.section.get(sectionid) ?? {};
+ const section = this.section(state, sectioninfo);
+ data.sections.push(section);
+ });
+ data.hassections = (data.sections.length != 0);
+
+ return data;
+ }
+
+ /**
+ * Generate a section export data from the state.
+ *
+ * @param {Object} state the current state.
+ * @param {Object} sectioninfo the section state data.
+ * @returns {Object}
+ */
+ section(state, sectioninfo) {
+ const section = {
+ ...sectioninfo,
+ cms: [],
+ isactive: false,
+ };
+ const cmlist = sectioninfo.cmlist ?? [];
+ cmlist.forEach(cmid => {
+ const cminfo = state.cm.get(cmid);
+ const cm = this.cm(state, cminfo);
+ section.cms.push(cm);
+ });
+ section.hascms = (section.cms.length != 0);
+
+ return section;
+ }
+
+ /**
+ * Generate a cm export data from the state.
+ *
+ * @param {Object} state the current state.
+ * @param {Object} cminfo the course module state data.
+ * @returns {Object}
+ */
+ cm(state, cminfo) {
+ const cm = {
+ ...cminfo,
+ isactive: false,
+ };
+ return cm;
+ }
+}
diff --git a/course/format/amd/src/local/courseeditor/mutations.js b/course/format/amd/src/local/courseeditor/mutations.js
index 51e77d43b53..23d3e332730 100644
--- a/course/format/amd/src/local/courseeditor/mutations.js
+++ b/course/format/amd/src/local/courseeditor/mutations.js
@@ -51,61 +51,37 @@ export default class {
* Get updated state data related to some cm ids.
*
* @method cmState
- * @param {StateManager} statemanager the current state
+ * @param {StateManager} stateManager the current state
* @param {array} cmids the list of cm ids to update
*/
- async cmState(statemanager, cmids) {
- const state = statemanager.state;
- const updates = await this._callEditWebservice('cm_state', state.course.id, cmids);
- statemanager.setReadOnly(false);
- this._processUpdates(statemanager, updates);
+ async cmState(stateManager, cmids) {
+ const course = stateManager.get('course');
+ const updates = await this._callEditWebservice('cm_state', course.id, cmids);
+ stateManager.processUpdates(updates);
}
/**
* Get updated state data related to some section ids.
*
* @method sectionState
- * @param {StateManager} statemanager the current state
+ * @param {StateManager} stateManager the current state
* @param {array} sectionIds the list of section ids to update
*/
- async sectionState(statemanager, sectionIds) {
- const state = statemanager.state;
- const updates = await this._callEditWebservice('section_state', state.course.id, sectionIds);
- this._processUpdates(statemanager, updates);
+ async sectionState(stateManager, sectionIds) {
+ const course = stateManager.get('course');
+ const updates = await this._callEditWebservice('section_state', course.id, sectionIds);
+ stateManager.processUpdates(updates);
}
/**
- * Helper to propcess both section_state and cm_state action results.
- *
- * @param {StateManager} statemanager the current state
- * @param {Array} updates of updates.
- */
- _processUpdates(statemanager, updates) {
-
- const state = statemanager.state;
-
- statemanager.setReadOnly(false);
-
- // The cm_state and section_state state action returns only updated states. However, most of the time we need this
- // mutation to fix discrepancies between the course content and the course state because core_course_edit_module
- // does not provide enough information to rebuild some state objects. This is the reason why we cannot use
- // the batch method processUpdates as the rest of mutations do.
- updates.forEach((update) => {
- if (update.name === undefined) {
- throw Error('Missing state update name');
- }
- // Compare the action with the current state.
- let current = state[update.name];
- if (current instanceof Map) {
- current = state[update.name].get(update.fields.id);
- }
- if (!current) {
- update.action = 'create';
- }
-
- statemanager.processUpdate(update.name, update.action, update.fields);
- });
-
- statemanager.setReadOnly(true);
+ * Get the full updated state data of the course.
+ *
+ * @param {StateManager} stateManager the current state
+ */
+ async courseState(stateManager) {
+ const course = stateManager.get('course');
+ const updates = await this._callEditWebservice('course_state', course.id);
+ stateManager.processUpdates(updates);
}
+
}
diff --git a/course/format/amd/src/local/courseindex/cm.js b/course/format/amd/src/local/courseindex/cm.js
new file mode 100644
index 00000000000..b264bcbeded
--- /dev/null
+++ b/course/format/amd/src/local/courseindex/cm.js
@@ -0,0 +1,71 @@
+// 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.
+ *
+ * @module core_courseformat/local/courseindex/cm
+ * @class core_courseformat/local/courseindex/cm
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent} from 'core/reactive';
+
+export default class Component extends BaseComponent {
+
+ /**
+ * Constructor hook.
+ */
+ 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;
+ }
+
+ /**
+ * Static method to create a component instance form the mustache 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.
+ */
+ stateReady() {
+ // Activate drag and drop soon.
+ }
+
+ getWatchers() {
+ return [
+ {watch: `cm[${this.id}]:deleted`, handler: this.remove},
+ ];
+ }
+
+}
diff --git a/course/format/amd/src/local/courseindex/courseindex.js b/course/format/amd/src/local/courseindex/courseindex.js
new file mode 100644
index 00000000000..464f7f0878d
--- /dev/null
+++ b/course/format/amd/src/local/courseindex/courseindex.js
@@ -0,0 +1,250 @@
+// 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 main component.
+ *
+ * @module core_courseformat/local/courseindex/courseindex
+ * @class core_courseformat/local/courseindex/courseindex
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent} from 'core/reactive';
+import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
+
+export default class Component extends BaseComponent {
+
+ /**
+ * Constructor hook.
+ */
+ create() {
+ // Optional component name for debugging.
+ this.name = 'courseindex';
+ // Default query selectors.
+ this.selectors = {
+ SECTION: `[data-for='section']`,
+ SECTION_ITEM: `[data-for='section_item']`,
+ SECTION_TITLE: `[data-for='section_title']`,
+ SECTION_CMLIST: `[data-for='cmlist']`,
+ CM: `[data-for='cm']`,
+ CM_NAME: `[data-for='cm_name']`,
+ TOGGLER: `[data-action="togglecourseindexsection"]`,
+ COLLAPSE: `[data-toggle="collapse"]`,
+ };
+ // Default classes to toggle on refresh.
+ this.classes = {
+ SECTIONHIDDEN: 'dimmed',
+ CMHIDDEN: 'dimmed',
+ SECTIONCURRENT: 'current',
+ };
+ // Arrays to keep cms and sections elements.
+ this.sections = {};
+ this.cms = {};
+ }
+
+ /**
+ * Static method to create a component instance form the mustache 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),
+ reactive: getCurrentCourseEditor(),
+ selectors,
+ });
+ }
+
+ /**
+ * Initial state ready method.
+ */
+ stateReady() {
+ // Activate section togglers.
+ this.addEventListener(this.element, 'click', this._setupSectionTogglers);
+
+ // Get cms and sections elements.
+ const sections = this.getElements(this.selectors.SECTION);
+ sections.forEach((section) => {
+ this.sections[section.dataset.id] = section;
+ });
+ const cms = this.getElements(this.selectors.CM);
+ cms.forEach((cm) => {
+ this.cms[cm.dataset.id] = cm;
+ });
+ }
+
+ getWatchers() {
+ return [
+ {watch: `section:updated`, handler: this._refreshSection},
+ {watch: `cm:updated`, handler: this._refreshCm},
+ {watch: `cm:created`, handler: this._createCm},
+ {watch: `cm:deleted`, handler: this._deleteCm},
+ // Sections and cm sorting.
+ {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},
+ {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},
+ ];
+ }
+
+ /**
+ * Setup sections toggler.
+ *
+ * Toggler click is delegated to the main course index element because new sections can
+ * appear at any moment and this way we prevent accidental double bindings.
+ *
+ * @param {Event} event the triggered event
+ */
+ _setupSectionTogglers(event) {
+ const sectionlink = event.target.closest(this.selectors.TOGGLER);
+ if (sectionlink) {
+ event.preventDefault();
+ sectionlink.parentNode.querySelector(this.selectors.COLLAPSE).click();
+ }
+ }
+
+ /**
+ * Update a course index section using the state information.
+ *
+ * @param {Object} details the update details.
+ */
+ _refreshSection({element}) {
+ // Find the element.
+ const target = this.sections[element.id];
+ if (!target) {
+ throw new Error(`Unkown section with ID ${element.id}`);
+ }
+ // Update classes.
+ const sectionitem = target.querySelector(this.selectors.SECTION_ITEM);
+ sectionitem.classList.toggle(this.classes.SECTIONHIDDEN, !element.visible);
+ target.classList.toggle(this.classes.SECTIONCURRENT, element.current);
+ // Update title.
+ target.querySelector(this.selectors.SECTION_TITLE).innerHTML = element.title;
+ }
+
+ /**
+ * Update a course index cm using the state information.
+ *
+ * @param {Object} details the update details.
+ */
+ _refreshCm({element}) {
+ // Find the element.
+ const target = this.cms[element.id];
+ if (!target) {
+ throw new Error(`Unkown course module with ID ${element.id}`);
+ }
+ // Update classes.
+ target.classList.toggle(this.classes.CMHIDDEN, !element.visible);
+ target.querySelector(this.selectors.CM_NAME).innerHTML = element.name;
+ }
+
+ /**
+ * Create a newcm instance.
+ *
+ * @param {Object} details the update details.
+ */
+ async _createCm({state, element}) {
+ // Create a fake node while the component is loading.
+ const fakeelement = document.createElement('li');
+ fakeelement.classList.add('bg-pulse-grey', 'w-100');
+ fakeelement.innerHTML = ' ';
+ this.cms[element.id] = fakeelement;
+ // Place the fake node on the correct position.
+ this._refreshSectionCmlist({
+ state,
+ element: state.section.get(element.sectionid),
+ });
+ // Collect render data.
+ const exporter = this.reactive.getExporter();
+ const data = exporter.cm(state, element);
+ // Create the new content.
+ const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/cm', data);
+ // Replace the fake node with the real content.
+ const newelement = newcomponent.getElement();
+ this.cms[element.id] = newelement;
+ fakeelement.parentNode.replaceChild(newelement, fakeelement);
+ }
+
+ /**
+ * Refresh a section cm list.
+ *
+ * @param {Object} details the update details.
+ */
+ _refreshSectionCmlist({element}) {
+ const cmlist = element.cmlist ?? [];
+ const listparent = this.getElement(this.selectors.SECTION_CMLIST, element.id);
+ this._fixOrder(listparent, cmlist, this.cms);
+ }
+
+ /**
+ * Refresh the section list.
+ *
+ * @param {Object} details the update details.
+ */
+ _refreshCourseSectionlist({element}) {
+ const sectionlist = element.sectionlist ?? [];
+ this._fixOrder(this.element, sectionlist, this.sections);
+ }
+
+ /**
+ * Fix/reorder the section or cms order.
+ *
+ * @param {Element} container the HTML element to reorder.
+ * @param {Array} neworder an array with the ids order
+ * @param {Array} allitems the list of html elements that can be placed in the container
+ */
+ _fixOrder(container, neworder, allitems) {
+
+ // Empty lists should not be visible.
+ if (!neworder.length) {
+ container.classList.add('hidden');
+ container.innerHTML = '';
+ return;
+ }
+
+ // Grant the list is visible (in case it was empty).
+ container.classList.remove('hidden');
+
+ // Move the elements in order at the beginning of the list.
+ neworder.forEach((itemid, index) => {
+ const item = allitems[itemid];
+ // Get the current element at that position.
+ const currentitem = container.children[index];
+ if (currentitem === undefined) {
+ container.append(item);
+ return;
+ }
+ if (currentitem !== item) {
+ container.insertBefore(item, currentitem);
+ }
+ });
+ // Remove the remaining elements.
+ while (container.children.length > neworder.length) {
+ container.removeChild(container.lastChild);
+ }
+ }
+
+ /**
+ * Remove a cm from the list.
+ *
+ * The actual DOM element removal is delegated to the cm component.
+ *
+ * @param {Object} details the update details.
+ */
+ _deleteCm({element}) {
+ delete this.cms[element.id];
+ }
+}
diff --git a/course/format/amd/src/local/courseindex/drawer.js b/course/format/amd/src/local/courseindex/drawer.js
new file mode 100644
index 00000000000..6a91b378e45
--- /dev/null
+++ b/course/format/amd/src/local/courseindex/drawer.js
@@ -0,0 +1,55 @@
+// 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 drawer wrap.
+ *
+ * This component is mostly used to ensure all subcomponents find a parent
+ * compoment with a reactive instance defined.
+ *
+ * @module core_courseformat/local/courseindex/drawer
+ * @class core_courseformat/local/courseindex/drawer
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent} from 'core/reactive';
+import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
+
+export default class Component extends BaseComponent {
+
+ /**
+ * Constructor hook.
+ */
+ create() {
+ // Optional component name for debugging.
+ this.name = 'courseindex-drawer';
+ }
+
+ /**
+ * Static method to create a component instance form the mustache 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),
+ reactive: getCurrentCourseEditor(),
+ selectors,
+ });
+ }
+}
diff --git a/course/format/amd/src/local/courseindex/placeholder.js b/course/format/amd/src/local/courseindex/placeholder.js
new file mode 100644
index 00000000000..a70a6004d4a
--- /dev/null
+++ b/course/format/amd/src/local/courseindex/placeholder.js
@@ -0,0 +1,69 @@
+// 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 placeholder replacer.
+ *
+ * @module core_courseformat/local/courseindex/placeholder
+ * @class core_courseformat/local/courseindex/placeholder
+ * @copyright 2021 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {BaseComponent} from 'core/reactive';
+import Templates from 'core/templates';
+import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
+
+export default class Component extends BaseComponent {
+
+ /**
+ * Static method to create a component instance form the mustache 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),
+ reactive: getCurrentCourseEditor(),
+ selectors,
+ });
+ }
+
+ /**
+ * Initial state ready method.
+ *
+ * This stateReady to be async because it loads the real courseindex.
+ *
+ * @param {object} state the initial state
+ */
+ async stateReady(state) {
+ // Collect section information from the state.
+ const exporter = this.reactive.getExporter();
+ const data = exporter.course(state);
+
+ try {
+ // To render an HTML into our component we just use the regular Templates module.
+ const {html, js} = await Templates.renderForPromise(
+ 'core_courseformat/local/courseindex/courseindex',
+ data,
+ );
+ Templates.replaceNode(this.element, html, js);
+ } catch (error) {
+ throw error;
+ }
+ }
+}
diff --git a/course/format/classes/base.php b/course/format/classes/base.php
index 26f5dfa75ef..333edd754e7 100644
--- a/course/format/classes/base.php
+++ b/course/format/classes/base.php
@@ -371,6 +371,18 @@ abstract class base {
return false;
}
+ /**
+ * Returns true if this course format uses course index
+ *
+ * This function may be called without specifying the course id
+ * i.e. in course_index_drawer()
+ *
+ * @return bool
+ */
+ public function uses_course_index() {
+ return false;
+ }
+
/**
* Returns a list of sections used in the course
*
diff --git a/course/format/classes/output/local/state/cm.php b/course/format/classes/output/local/state/cm.php
index 2e5b29dd63c..d899acb1f26 100644
--- a/course/format/classes/output/local/state/cm.php
+++ b/course/format/classes/output/local/state/cm.php
@@ -74,8 +74,21 @@ class cm implements renderable {
'id' => $cm->id,
'name' => $cm->name,
'visible' => !empty($cm->visible),
+ 'sectionid' => $section->id,
+ 'sectionnumber' => $section->section,
+ 'uservisible' => $cm->uservisible,
];
+ // Check the user access type to this cm.
+ $conditionalhidden = $output->is_cm_conditionally_hidden($cm);
+ $data->accessvisible = ($data->visible && !$conditionalhidden);
+
+ // Add url if the activity is compatible.
+ $url = $cm->url;
+ if ($url) {
+ $data->url = $url->out();
+ }
+
if ($this->exportcontent) {
$data->content = $output->course_section_updated_cm_item($format, $section, $cm);
}
diff --git a/course/format/classes/output/local/state/section.php b/course/format/classes/output/local/state/section.php
index a1e51f055f3..b56671aca0f 100644
--- a/course/format/classes/output/local/state/section.php
+++ b/course/format/classes/output/local/state/section.php
@@ -55,15 +55,19 @@ class section implements renderable {
*/
public function export_for_template(\renderer_base $output): stdClass {
$format = $this->format;
+ $course = $format->get_course();
$section = $this->section;
$modinfo = $format->get_modinfo();
$data = (object)[
'id' => $section->id,
'section' => $section->section,
+ 'number' => $section->section,
'title' => $format->get_section_name($section),
'cmlist' => [],
'visible' => !empty($section->visible),
+ 'sectionurl' => course_get_url($course, $section->section)->out(),
+ 'current' => $format->is_section_current($section),
];
if (empty($modinfo->sections[$section->section])) {
diff --git a/course/format/classes/output/section_renderer.php b/course/format/classes/output/section_renderer.php
index 93d8a8a1381..790cd05d605 100644
--- a/course/format/classes/output/section_renderer.php
+++ b/course/format/classes/output/section_renderer.php
@@ -179,6 +179,25 @@ abstract class section_renderer extends core_course_renderer {
return $this->render($cmitem);
}
+ /**
+ * Get the course index drawer with placeholder.
+ *
+ * The default course index is loaded after the page is ready. Format plugins can override
+ * this method to provide an alternative course index.
+ *
+ * If the format is not compatible with the course index, this method will return an empty string.
+ *
+ * @param course_format $format the course format
+ * @return String the course index HTML.
+ */
+ public function course_index_drawer(course_format $format): ?String {
+ if ($format->uses_course_index()) {
+ include_course_editor($format);
+ return $this->render_from_template('core_courseformat/local/courseindex/drawer', []);
+ }
+ return '';
+ }
+
/**
* Generate the edit control action menu
*
diff --git a/course/format/classes/stateactions.php b/course/format/classes/stateactions.php
index d3e8a058e8e..f37bf700702 100644
--- a/course/format/classes/stateactions.php
+++ b/course/format/classes/stateactions.php
@@ -78,14 +78,14 @@ class stateactions {
foreach (array_keys($cmids) as $cmid) {
// Add this action to updates array.
- $updates->add_cm_update($cmid);
+ $updates->add_cm_put($cmid);
$cm = $modinfo->get_cm($cmid);
$sectionids[$cm->section] = true;
}
foreach (array_keys($sectionids) as $sectionid) {
- $updates->add_section_update($sectionid);
+ $updates->add_section_put($sectionid);
}
}
@@ -129,7 +129,7 @@ class stateactions {
foreach (array_keys($sectionids) as $sectionid) {
$sectioninfo = $modinfo->get_section_info_by_id($sectionid);
- $updates->add_section_update($sectionid);
+ $updates->add_section_put($sectionid);
// Add cms.
if (empty($modinfo->sections[$sectioninfo->section])) {
continue;
@@ -145,7 +145,7 @@ class stateactions {
foreach (array_keys($cmids) as $cmid) {
// Add this action to updates array.
- $updates->add_cm_update($cmid);
+ $updates->add_cm_put($cmid);
}
}
@@ -171,7 +171,7 @@ class stateactions {
$modinfo = course_modinfo::instance($course);
- $updates->add_course_update();
+ $updates->add_course_put();
// Add sections updates.
$sections = $modinfo->get_section_info_all();
diff --git a/course/format/classes/stateupdates.php b/course/format/classes/stateupdates.php
index d117bff11d4..7cf2d530c82 100644
--- a/course/format/classes/stateupdates.php
+++ b/course/format/classes/stateupdates.php
@@ -73,19 +73,19 @@ class stateupdates implements JsonSerializable {
/**
* Add track about a general course state change.
*/
- public function add_course_update(): void {
+ public function add_course_put(): void {
$courseclass = $this->format->get_output_classname('state\\course');
$currentstate = new $courseclass($this->format);
- $this->add_update('course', 'update', $currentstate->export_for_template($this->output));
+ $this->add_update('course', 'put', $currentstate->export_for_template($this->output));
}
/**
- * Add track about a section state update.
+ * Add track about a section state put.
*
* @param int $sectionid The affected section id.
*/
- public function add_section_update(int $sectionid): void {
- $this->create_or_update_section($sectionid, 'update');
+ public function add_section_put(int $sectionid): void {
+ $this->create_or_put_section($sectionid, 'put');
}
/**
@@ -94,19 +94,19 @@ class stateupdates implements JsonSerializable {
* @param int $sectionid The affected section id.
*/
public function add_section_create(int $sectionid): void {
- $this->create_or_update_section($sectionid, 'create');
+ $this->create_or_put_section($sectionid, 'create');
}
/**
- * Add track about section created or updated.
+ * Add track about section created or put.
*
* @param int $sectionid The affected section id.
- * @param string $action The action to track for the section ('create' or 'update).
+ * @param string $action The action to track for the section ('create' or 'put').
*/
- protected function create_or_update_section(int $sectionid, string $action): void {
- if ($action != 'create' && $action != 'update') {
+ protected function create_or_put_section(int $sectionid, string $action): void {
+ if ($action != 'create' && $action != 'put') {
throw new coding_exception(
- "Invalid action passed ($action) to create_or_update_section. Only 'create' and 'update' are valid."
+ "Invalid action passed ($action) to create_or_put_section. Only 'create' and 'put' are valid."
);
}
$course = $this->format->get_course();
@@ -138,8 +138,8 @@ class stateupdates implements JsonSerializable {
*
* @param int $cmid the affected course module id
*/
- public function add_cm_update(int $cmid): void {
- $this->create_or_update_cm($cmid, 'update');
+ public function add_cm_put(int $cmid): void {
+ $this->create_or_put_cm($cmid, 'put');
}
/**
@@ -148,16 +148,16 @@ class stateupdates implements JsonSerializable {
* @param int $cmid the affected course module id
*/
public function add_cm_create(int $cmid): void {
- $this->create_or_update_cm($cmid, 'create', true);
+ $this->create_or_put_cm($cmid, 'create', true);
}
/**
- * Add track about section created or updated.
+ * Add track about section created or put.
*
* @param int $cmid The affected course module id.
- * @param string $action The action to track for the section ('create' or 'update').
+ * @param string $action The action to track for the section ('create' or 'put').
*/
- protected function create_or_update_cm(int $cmid, string $action): void {
+ protected function create_or_put_cm(int $cmid, string $action): void {
$modinfo = course_modinfo::instance($this->format->get_course());
$cm = $modinfo->get_cm($cmid);
diff --git a/course/format/templates/local/courseindex/cm.mustache b/course/format/templates/local/courseindex/cm.mustache
new file mode 100644
index 00000000000..c17b17a35e4
--- /dev/null
+++ b/course/format/templates/local/courseindex/cm.mustache
@@ -0,0 +1,64 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core_courseformat/local/courseindex/cm
+
+ Displays a course index course-module entry.
+
+ Example context (json):
+ {
+ "id": "12",
+ "name": "Announcements",
+ "url": "#",
+ "visible": 1,
+ "isactive": 1,
+ "uniqid": "0",
+ "isactive": 1,
+ "accessvisible": 1,
+ }
+}}
+
+{{#js}}
+require(['core_courseformat/local/courseindex/cm'], function(component) {
+ component.init('{{uniqid}}-course-index-cm-{{id}}');
+});
+{{/js}}
diff --git a/course/format/templates/local/courseindex/courseindex.mustache b/course/format/templates/local/courseindex/courseindex.mustache
new file mode 100644
index 00000000000..44e948c4826
--- /dev/null
+++ b/course/format/templates/local/courseindex/courseindex.mustache
@@ -0,0 +1,111 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core_courseformat/local/courseindex/courseindex
+
+ Displays the course index.
+
+ Example context (json):
+ {
+ "editmode": true,
+ "sections": [
+ {
+ "title": "General",
+ "id": 42,
+ "number": 1,
+ "sectionurl": "#",
+ "isactive": 1,
+ "cms": [
+ {
+ "name": "Glossary of characters",
+ "id": "10",
+ "url": "#",
+ "visible": 1,
+ "isactive": 0,
+ "uniqid": "0",
+ "accessvisible": 1
+ },
+ {
+ "name": "World Cinema forum",
+ "id": "11",
+ "url": "#",
+ "visible": 1,
+ "isactive": 0,
+ "uniqid": "0",
+ "accessvisible": 1
+ },
+ {
+ "name": "Announcements",
+ "id": "12",
+ "url": "#",
+ "visible": 1,
+ "isactive": 1,
+ "uniqid": "0",
+ "accessvisible": 1
+ }
+ ]
+ },
+ {
+ "title": "City of God or Cidade de Deus",
+ "id": "43",
+ "number": "2",
+ "sectionurl": "#",
+ "isactive": 0,
+ "cms": [
+ {
+ "name": "Resources",
+ "id": "13",
+ "url": "#",
+ "visible": 1,
+ "isactive": 0,
+ "uniqid": "0",
+ "accessvisible": 1
+ },
+ {
+ "name": "Studying City of God by Stephen Smith Bergman-Messerschmidt",
+ "id": "14",
+ "url": "#",
+ "visible": 1,
+ "isactive": 0,
+ "uniqid": "0",
+ "accessvisible": 1
+ },
+ {
+ "name": "Film education study guide",
+ "id": "15",
+ "url": "#",
+ "visible": 1,
+ "isactive": 0,
+ "uniqid": "0",
+ "accessvisible": 1
+ }
+ ]
+ }
+ ]
+ }
+
+}}
+
+{{#js}}
+require(['core_courseformat/local/courseindex/courseindex'], function(component) {
+ component.init('{{uniqid}}-course-index');
+});
+{{/js}}
diff --git a/course/format/templates/local/courseindex/drawer.mustache b/course/format/templates/local/courseindex/drawer.mustache
new file mode 100644
index 00000000000..87efa696951
--- /dev/null
+++ b/course/format/templates/local/courseindex/drawer.mustache
@@ -0,0 +1,37 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core_courseformat/local/courseindex/drawer
+
+ This template renders the course index drawer with the placeholder.
+
+ The code from this file is just an stub as the final code will come from
+ the new layouts for Moodle 4.0.
+
+ Example context (json):
+ {}
+}}
+
+{{#js}}
+require(['core_courseformat/local/courseindex/drawer'], function(component) {
+ component.init('courseindex');
+});
+{{/js}}
diff --git a/course/format/templates/local/courseindex/placeholders.mustache b/course/format/templates/local/courseindex/placeholders.mustache
new file mode 100644
index 00000000000..a2c563d2471
--- /dev/null
+++ b/course/format/templates/local/courseindex/placeholders.mustache
@@ -0,0 +1,57 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core_courseformat/local/courseindex/placeholders
+
+ This template renders the loading placeholders for the course index.
+
+ Example context (json):
+ {}
+}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{#js}}
+require(['core_courseformat/local/courseindex/placeholder'], function(component) {
+ component.init('{{uniqid}}-course-index-placeholder');
+});
+{{/js}}
diff --git a/course/format/templates/local/courseindex/section.mustache b/course/format/templates/local/courseindex/section.mustache
new file mode 100644
index 00000000000..91899cb16ff
--- /dev/null
+++ b/course/format/templates/local/courseindex/section.mustache
@@ -0,0 +1,102 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core_courseformat/local/courseindex/section
+
+ Displays a course index section entry.
+
+ Example context (json):
+ {
+ "title": "General",
+ "id": 23,
+ "uniqid": "0",
+ "number": 1,
+ "sectionurl": "#",
+ "isactive": 1,
+ "current": 1,
+ "visible": 1,
+ "cms": [
+ {
+ "id": 10,
+ "name": "Glossary of characters",
+ "url": "#",
+ "visible": 1,
+ "isactive": 0
+ },
+ {
+ "id": 11,
+ "name": "World Cinema forum",
+ "url": "#",
+ "visible": 1,
+ "isactive": 0
+ },
+ {
+ "id": 12,
+ "name": "Announcements",
+ "url": "#",
+ "visible": 0,
+ "isactive": 1
+ }
+ ]
+ }
+}}
+