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":"opEA2BqBA,CAAAA,C,+HAKR,CAEL,KAAKC,IAAL,CAAY,aAAZ,CAEA,KAAKC,SAAL,CAAiB,CACbC,OAAO,uBADM,CAEbC,YAAY,4BAFC,CAGbC,aAAa,6BAHA,CAIbC,cAAc,sBAJD,CAKbC,EAAE,kBALW,CAMbC,OAAO,uBANM,CAObC,OAAO,6CAPM,CAQbC,QAAQ,6BARK,CAAjB,CAWA,KAAKC,OAAL,CAAe,CACXC,aAAa,CAAE,QADJ,CAEXC,QAAQ,CAAE,QAFC,CAGXC,cAAc,CAAE,SAHL,CAAf,CAMA,KAAKC,QAAL,CAAgB,EAAhB,CACA,KAAKC,GAAL,CAAW,EACd,C,+CAoBY,YAET,KAAKC,gBAAL,CAAsB,KAAKC,OAA3B,CAAoC,OAApC,CAA6C,KAAKC,qBAAlD,EAGA,GAAMJ,CAAAA,CAAQ,CAAG,KAAKK,WAAL,CAAiB,KAAKlB,SAAL,CAAeC,OAAhC,CAAjB,CACAY,CAAQ,CAACM,OAAT,CAAiB,SAACC,CAAD,CAAa,CAC1B,CAAI,CAACP,QAAL,CAAcO,CAAO,CAACC,OAAR,CAAgBC,EAA9B,EAAoCF,CACvC,CAFD,EAGA,GAAMN,CAAAA,CAAG,CAAG,KAAKI,WAAL,CAAiB,KAAKlB,SAAL,CAAeK,EAAhC,CAAZ,CACAS,CAAG,CAACK,OAAJ,CAAY,SAACI,CAAD,CAAQ,CAChB,CAAI,CAACT,GAAL,CAASS,CAAE,CAACF,OAAH,CAAWC,EAApB,EAA0BC,CAC7B,CAFD,CAGH,C,iDAEa,CACV,MAAO,CACH,CAACC,KAAK,kBAAN,CAA2BC,OAAO,CAAE,KAAKC,eAAzC,CADG,CAEH,CAACF,KAAK,aAAN,CAAsBC,OAAO,CAAE,KAAKE,UAApC,CAFG,CAGH,CAACH,KAAK,aAAN,CAAsBC,OAAO,CAAE,KAAKG,SAApC,CAHG,CAIH,CAACJ,KAAK,aAAN,CAAsBC,OAAO,CAAE,KAAKI,SAApC,CAJG,CAMH,CAACL,KAAK,6BAAN,CAAsCC,OAAO,CAAE,KAAKK,yBAApD,CANG,CAOH,CAACN,KAAK,yBAAN,CAAkCC,OAAO,CAAE,KAAKM,qBAAhD,CAPG,CASV,C,oEAUqBC,C,CAAO,CACzB,GAAMC,CAAAA,CAAW,CAAGD,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqB,KAAKnC,SAAL,CAAeO,OAApC,CAApB,CACA,GAAI0B,CAAJ,CAAiB,CACbD,CAAK,CAACI,cAAN,GACAH,CAAW,CAACI,UAAZ,CAAuBC,aAAvB,CAAqC,KAAKtC,SAAL,CAAeQ,QAApD,EAA8D+B,KAA9D,EACH,CACJ,C,0DAO0B,IAAVvB,CAAAA,CAAU,GAAVA,OAAU,CAEjBkB,CAAM,CAAG,KAAKrB,QAAL,CAAcG,CAAO,CAACM,EAAtB,CAFQ,CAGvB,GAAI,CAACY,CAAL,CAAa,CACT,KAAM,IAAIM,CAAAA,KAAJ,kCAAoCxB,CAAO,CAACM,EAA5C,EACT,CAED,GAAMmB,CAAAA,CAAW,CAAGP,CAAM,CAACI,aAAP,CAAqB,KAAKtC,SAAL,CAAeE,YAApC,CAApB,CACAuC,CAAW,CAACC,SAAZ,CAAsBC,MAAtB,CAA6B,KAAKlC,OAAL,CAAaC,aAA1C,CAAyD,CAACM,CAAO,CAAC4B,OAAlE,EACAV,CAAM,CAACQ,SAAP,CAAiBC,MAAjB,CAAwB,KAAKlC,OAAL,CAAaG,cAArC,CAAqDI,CAAO,CAAC6B,OAA7D,EAEAX,CAAM,CAACI,aAAP,CAAqB,KAAKtC,SAAL,CAAeG,aAApC,EAAmD2C,SAAnD,CAA+D9B,CAAO,CAAC+B,KAC1E,C,gDAOqB,IAAV/B,CAAAA,CAAU,GAAVA,OAAU,CAEZkB,CAAM,CAAG,KAAKpB,GAAL,CAASE,CAAO,CAACM,EAAjB,CAFG,CAGlB,GAAI,CAACY,CAAL,CAAa,CACT,KAAM,IAAIM,CAAAA,KAAJ,wCAA0CxB,CAAO,CAACM,EAAlD,EACT,CAEDY,CAAM,CAACQ,SAAP,CAAiBC,MAAjB,CAAwB,KAAKlC,OAAL,CAAaE,QAArC,CAA+C,CAACK,CAAO,CAAC4B,OAAxD,EACAV,CAAM,CAACI,aAAP,CAAqB,KAAKtC,SAAL,CAAeM,OAApC,EAA6CwC,SAA7C,CAAyD9B,CAAO,CAACjB,IACpE,C,qLAOgBiD,C,GAAAA,K,CAAOhC,C,GAAAA,O,CAEdiC,C,CAAcC,QAAQ,CAACC,aAAT,CAAuB,IAAvB,C,CACpBF,CAAW,CAACP,SAAZ,CAAsBU,GAAtB,CAA0B,eAA1B,CAA2C,OAA3C,EACAH,CAAW,CAACH,SAAZ,CAAwB,QAAxB,CACA,KAAKhC,GAAL,CAASE,CAAO,CAACM,EAAjB,EAAuB2B,CAAvB,CAEA,KAAKlB,qBAAL,CAA2B,CACvBiB,KAAK,CAALA,CADuB,CAEvBhC,OAAO,CAAEgC,CAAK,CAAC5B,OAAN,CAAciC,GAAd,CAAkBrC,CAAO,CAACsC,SAA1B,CAFc,CAA3B,EAKMC,C,CAAW,KAAKC,QAAL,CAAcC,WAAd,E,CACXC,C,CAAOH,CAAQ,CAAChC,EAAT,CAAYyB,CAAZ,CAAmBhC,CAAnB,C,iBAEc,MAAK2C,eAAL,CAAqBV,CAArB,CAAkC,wCAAlC,CAA4ES,CAA5E,C,SAArBE,C,QAEAC,C,CAAaD,CAAY,CAACE,UAAb,E,CACnB,KAAKhD,GAAL,CAASE,CAAO,CAACM,EAAjB,EAAuBuC,CAAvB,CACAZ,CAAW,CAACZ,UAAZ,CAAuB0B,YAAvB,CAAoCF,CAApC,CAAgDZ,CAAhD,E,qLAQ6B,OAAVjC,CAAU,GAAVA,OAAU,CACvBgD,CAAM,WAAGhD,CAAO,CAACgD,MAAX,gBAAqB,EADJ,CAEvBC,CAAU,CAAG,KAAKH,UAAL,CAAgB,KAAK9D,SAAL,CAAeI,cAA/B,CAA+CY,CAAO,CAACM,EAAvD,CAFU,CAG7B,KAAK4C,SAAL,CAAeD,CAAf,CAA2BD,CAA3B,CAAmC,KAAKlD,GAAxC,CACH,C,8EAOoC,OAAVE,CAAU,GAAVA,OAAU,CAC3BmD,CAAW,WAAGnD,CAAO,CAACmD,WAAX,gBAA0B,EADV,CAEjC,KAAKD,SAAL,CAAe,KAAKlD,OAApB,CAA6BmD,CAA7B,CAA0C,KAAKtD,QAA/C,CACH,C,4CASSuD,C,CAAWC,C,CAAUC,C,CAAU,CAGrC,GAAI,CAACD,CAAQ,CAACE,MAAd,CAAsB,CAClBH,CAAS,CAAC1B,SAAV,CAAoBU,GAApB,CAAwB,QAAxB,EACAgB,CAAS,CAACtB,SAAV,CAAsB,EAAtB,CACA,MACH,CAGDsB,CAAS,CAAC1B,SAAV,CAAoB8B,MAApB,CAA2B,QAA3B,EAGAH,CAAQ,CAAClD,OAAT,CAAiB,SAACsD,CAAD,CAASC,CAAT,CAAmB,IAC1BC,CAAAA,CAAI,CAAGL,CAAQ,CAACG,CAAD,CADW,CAG1BG,CAAW,CAAGR,CAAS,CAACS,QAAV,CAAmBH,CAAnB,CAHY,CAIhC,GAAIE,CAAW,SAAf,CAA+B,CAC3BR,CAAS,CAACU,MAAV,CAAiBH,CAAjB,EACA,MACH,CACD,GAAIC,CAAW,GAAKD,CAApB,CAA0B,CACtBP,CAAS,CAACW,YAAV,CAAuBJ,CAAvB,CAA6BC,CAA7B,CACH,CACJ,CAXD,EAaA,MAAOR,CAAS,CAACS,QAAV,CAAmBN,MAAnB,CAA4BF,CAAQ,CAACE,MAA5C,CAAoD,CAChDH,CAAS,CAACY,WAAV,CAAsBZ,CAAS,CAACa,SAAhC,CACH,CACJ,C,8CASoB,IAAVjE,CAAAA,CAAU,GAAVA,OAAU,CACjB,MAAO,MAAKF,GAAL,CAASE,CAAO,CAACM,EAAjB,CACV,C,oCAxLWY,C,CAAQlC,C,CAAW,CAC3B,MAAO,IAAIF,CAAAA,CAAJ,CAAc,CACjBkB,OAAO,CAAEkC,QAAQ,CAACgC,cAAT,CAAwBhD,CAAxB,CADQ,CAEjBsB,QAAQ,CAAE,8BAFO,CAGjBxD,SAAS,CAATA,CAHiB,CAAd,CAKV,C,cA3CkCmF,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 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, + } +}} +
  • + {{#url}} + {{#uservisible}} + + {{{name}}} + + {{/uservisible}} + {{^uservisible}} + + {{{name}}} + + {{/uservisible}} + {{/url}} + {{^url}} + + {{{name}}} + + {{/url}} +
  • +{{#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 + } + ] + } +}} + diff --git a/course/format/tests/external/get_state_test.php b/course/format/tests/external/get_state_test.php index 9c5bcae3ca3..bcedd162f7f 100644 --- a/course/format/tests/external/get_state_test.php +++ b/course/format/tests/external/get_state_test.php @@ -143,7 +143,7 @@ class get_state_test extends \externallib_advanced_testcase { // Check sections information. foreach ($result->section as $section) { - if (in_array($section->section, $hiddensections)) { + if (in_array($section->number, $hiddensections)) { $this->assertFalse($section->visible); } else { $this->assertTrue($section->visible); @@ -151,8 +151,8 @@ class get_state_test extends \externallib_advanced_testcase { // Check section is defined in course->sectionlist. $this->assertContains($section->id, $result->course->sectionlist); // Check course modules list for this section is the expected. - if (array_key_exists($section->section, $this->sections)) { - $this->assertEquals($this->sections[$section->section], $section->cmlist); + if (array_key_exists($section->number, $this->sections)) { + $this->assertEquals($this->sections[$section->number], $section->cmlist); } } // Check course modules information. diff --git a/course/format/tests/external/update_course_test.php b/course/format/tests/external/update_course_test.php index fa5cd815c27..355e14923ba 100644 --- a/course/format/tests/external/update_course_test.php +++ b/course/format/tests/external/update_course_test.php @@ -111,7 +111,7 @@ class update_course_test extends \externallib_advanced_testcase { 'action' => 'cm_state', 'expected' => [ 'count' => 2, - 'action' => 'update', + 'action' => 'put', 'visible' => 1, ], 'expectexception' => false, @@ -223,14 +223,14 @@ class update_course_test extends \externallib_advanced_testcase { $results = json_decode(update_course::execute('targetsection_test', $course->id, [], $section->id)); $this->assertDebuggingCalled(); $this->assertCount(1, $results); - $update = $this->find_update($results, 'update', 'section', $section->id); + $update = $this->find_update($results, 'put', 'section', $section->id); $this->assertNotEmpty($update); // Execute action with targetcmid. $results = json_decode(update_course::execute('targetcm_test', $course->id, [], null, $activity->cmid)); $this->assertDebuggingCalled(); $this->assertCount(1, $results); - $update = $this->find_update($results, 'update', 'cm', $activity->cmid); + $update = $this->find_update($results, 'put', 'cm', $activity->cmid); $this->assertNotEmpty($update); } } diff --git a/course/format/tests/fixtures/format_theunittest_stateactions.php b/course/format/tests/fixtures/format_theunittest_stateactions.php index f1ad0847bc1..207f83acfb0 100644 --- a/course/format/tests/fixtures/format_theunittest_stateactions.php +++ b/course/format/tests/fixtures/format_theunittest_stateactions.php @@ -85,7 +85,7 @@ class stateactions extends core_actions { ?int $targetcmid = null ): void { - $updates->add_section_update($targetsectionid); + $updates->add_section_put($targetsectionid); } /** @@ -105,7 +105,7 @@ class stateactions extends core_actions { ?int $targetcmid = null ): void { - $updates->add_cm_update($targetcmid); + $updates->add_cm_put($targetcmid); } } diff --git a/course/format/tests/stateactions_test.php b/course/format/tests/stateactions_test.php index ff32dafbe4f..56c1f14c057 100644 --- a/course/format/tests/stateactions_test.php +++ b/course/format/tests/stateactions_test.php @@ -137,7 +137,7 @@ class stateactions_test extends \advanced_testcase { 'cm' => [], 'count' => 0, ], - 'update' => [ + 'put' => [ 'course' => [], 'section' => [], 'cm' => [], @@ -243,12 +243,12 @@ class stateactions_test extends \advanced_testcase { // Validate we have all the expected entries. $expectedtotal = count($expectedresults['course']) + count($expectedresults['section']) + count($expectedresults['cm']); - $this->assertEquals($expectedtotal, $results['update']['count']); + $this->assertEquals($expectedtotal, $results['put']['count']); // Validate course, section and cm. foreach ($expectedresults as $name => $referencekeys) { foreach ($referencekeys as $referencekey) { - $this->assertArrayHasKey($references[$referencekey], $results['update'][$name]); + $this->assertArrayHasKey($references[$referencekey], $results['put'][$name]); } } } diff --git a/course/format/tests/stateupdates_test.php b/course/format/tests/stateupdates_test.php index ebda0542b1a..d3eca691e60 100644 --- a/course/format/tests/stateupdates_test.php +++ b/course/format/tests/stateupdates_test.php @@ -30,14 +30,14 @@ use stdClass; class stateupdates_test extends \advanced_testcase { /** - * Test for add_course_update. + * Test for add_course_put. * - * @dataProvider add_course_update_provider - * @covers ::add_course_update + * @dataProvider add_course_put_provider + * @covers ::add_course_put * * @param string $role the user role in the course */ - public function test_add_course_update(string $role): void { + public function test_add_course_put(string $role): void { global $PAGE; $this->resetAfterTest(); @@ -65,23 +65,23 @@ class stateupdates_test extends \advanced_testcase { $currentstate = new $stateclass($format); $expected = $currentstate->export_for_template($renderer); - $updates->add_course_update(); + $updates->add_course_put(); $updatelist = $updates->jsonSerialize(); $this->assertCount(1, $updatelist); $update = array_pop($updatelist); - $this->assertEquals('update', $update->action); + $this->assertEquals('put', $update->action); $this->assertEquals('course', $update->name); $this->assertEquals($expected, $update->fields); } /** - * Data provider for test_add_course_update. + * Data provider for test_add_course_put. * * @return array testing scenarios */ - public function add_course_update_provider() { + public function add_course_put_provider() { return [ 'Admin role' => [ 'admin', @@ -128,7 +128,7 @@ class stateupdates_test extends \advanced_testcase { * Add track about a section state update. * * @dataProvider add_section_provider - * @covers ::add_course_update + * @covers ::add_course_put * @covers ::add_course_create * @covers ::add_course_delete * @@ -201,7 +201,7 @@ class stateupdates_test extends \advanced_testcase { */ public function add_section_provider(): array { return array_merge( - $this->add_section_provider_helper('update'), + $this->add_section_provider_helper('put'), $this->add_section_provider_helper('create'), $this->add_section_provider_helper('delete'), ); @@ -345,7 +345,7 @@ class stateupdates_test extends \advanced_testcase { */ public function add_cm_provider(): array { return array_merge( - $this->add_cm_provider_helper('update'), + $this->add_cm_provider_helper('put'), $this->add_cm_provider_helper('create'), $this->add_cm_provider_helper('delete'), ); diff --git a/course/format/topics/lib.php b/course/format/topics/lib.php index 3cc40e20755..b0e84ef9659 100644 --- a/course/format/topics/lib.php +++ b/course/format/topics/lib.php @@ -46,6 +46,10 @@ class format_topics extends core_courseformat\base { return true; } + public function uses_course_index() { + return true; + } + /** * Returns the display name of the given section that the course prefers. * diff --git a/course/format/upgrade.txt b/course/format/upgrade.txt index 948497e61e8..bf173ba590a 100644 --- a/course/format/upgrade.txt +++ b/course/format/upgrade.txt @@ -2,6 +2,9 @@ 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. + === 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 ce9b3265018..5526b01f7b0 100644 --- a/course/format/weeks/lib.php +++ b/course/format/weeks/lib.php @@ -45,6 +45,10 @@ class format_weeks extends core_courseformat\base { return true; } + public function uses_course_index() { + return true; + } + /** * Generate the title for this section page * @return string the page title diff --git a/course/lib.php b/course/lib.php index fbbf4a8d22a..b04d05c959a 100644 --- a/course/lib.php +++ b/course/lib.php @@ -24,6 +24,8 @@ defined('MOODLE_INTERNAL') || die; +use core_courseformat\base as course_format; + require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->libdir.'/filelib.php'); require_once($CFG->libdir.'/datalib.php'); @@ -3197,13 +3199,6 @@ function course_ajax_enabled($course) { function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) { global $CFG, $PAGE, $SITE; - // All the new editor elements will be loaded after the course is presented and - // the initial course state will be generated using core_courseformat_get_state webservice. - if ($SITE->id !== $course->id) { - $PAGE->requires->js_call_amd('core_courseformat/courseeditor', 'getCourseEditor', [$course->id]); - } - // TODO: as part of MDL-70907, add a way to indicate the plugin needs the legacy libraries (and get a deprecation message). - // Ensure that ajax should be included if (!course_ajax_enabled($course)) { return false; @@ -3295,6 +3290,29 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules = return true; } +/** + * Include and configure the course editor modules. + * + * @param course_format $format the course format instance. + */ +function include_course_editor(course_format $format) { + global $PAGE, $SITE; + + $course = $format->get_course(); + + if ($SITE->id === $course->id) { + return; + } + + // Edition mode and some format specs must be passed to the init method. + $setup = (object)[ + 'editing' => $format->show_editor(), + ]; + // 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. + $PAGE->requires->js_call_amd('core_courseformat/courseeditor', 'setViewFormat', [$course->id, $setup]); +} + /** * Returns the sorted list of available course formats, filtered by enabled if necessary * @@ -3835,6 +3853,24 @@ function core_course_core_calendar_get_valid_event_timestart_range(\calendar_eve return [$mindate, $maxdate]; } +/** + * Render the main course drawer to be included in the left part of the page. + * + * @return string HTML + */ +function core_course_drawer(): string { + global $PAGE; + // Only course and mod pages are able to render course index. + if (!preg_match('/^(mod|course).*/', $PAGE->pagetype)) { + return ''; + } + + $format = course_get_format($PAGE->course); + $renderer = $format->get_renderer($PAGE); + $placeholder = $renderer->course_index_drawer($format); + return $placeholder; +} + /** * Returns course modules tagged with a specified tag ready for output on tag/index.php page * diff --git a/course/renderer.php b/course/renderer.php index 705e59a37c1..8f325e3d91c 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -560,12 +560,10 @@ class core_course_renderer extends plugin_renderer_base { * Checks if course module has any conditions that may make it unavailable for * all or some of the students * - * This function is internal and is only used to create CSS classes for the module name/text - * * @param cm_info $mod * @return bool */ - protected function is_cm_conditionally_hidden(cm_info $mod) { + public function is_cm_conditionally_hidden(cm_info $mod) { global $CFG; $conditionalhidden = false; if (!empty($CFG->enableavailability)) { diff --git a/course/upgrade.txt b/course/upgrade.txt index 836bf135f86..ca377ada423 100644 --- a/course/upgrade.txt +++ b/course/upgrade.txt @@ -52,6 +52,8 @@ course formats don't have their own renderer. * New external function core_course_update_course runs given action to edit course status. * The `\core\event\course_category_deleted` event is now created with a snapshot of the category record being deleted, available inside event observers via `$event->get_record_snapshot` +* New include_course_editor() function to include and configure course editor modules. +* New core_course_drawer() function to render the message drawer in the top of the body of each page. === 3.11 === * A new callback xxx_coursemodule_definition_after_data that allows plugins to extend activity forms after the data is set. diff --git a/lang/en/courseformat.php b/lang/en/courseformat.php index ea5725d49e5..7d5d9bfaa6b 100644 --- a/lang/en/courseformat.php +++ b/lang/en/courseformat.php @@ -22,4 +22,5 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['courseindex'] = 'Course index'; $string['privacy:metadata'] = 'The course format subsystem does not store any personal data.'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 318803f28d5..bc1d1955d50 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -356,7 +356,6 @@ $string['coursehelpnewsitemsnumber'] = 'Number of recent announcements appearing $string['coursehelpnumberweeks'] = 'Number of sections in the course (applies to certain course formats only).'; $string['coursehelpshowgrades'] = 'Enable the display of the gradebook. It does not prevent grades from being displayed within the individual activities.'; $string['coursehidden'] = 'This course is currently unavailable to students'; -$string['courseindex'] = 'Course index'; $string['courseoverviewfiles'] = 'Course image'; $string['courseoverviewfilesext'] = 'Course image file extensions'; $string['courseoverviewfileslimit'] = 'Course image files limit'; diff --git a/theme/boost/layout/drawers.php b/theme/boost/layout/drawers.php index 16607635b78..e2d08c68ab9 100644 --- a/theme/boost/layout/drawers.php +++ b/theme/boost/layout/drawers.php @@ -25,6 +25,7 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/behat/lib.php'); +require_once($CFG->dirroot . '/course/lib.php'); user_preference_allow_ajax_update('drawer-open-nav', PARAM_ALPHA); user_preference_allow_ajax_update('drawer-open-index', PARAM_BOOL); @@ -57,7 +58,7 @@ $hasblocks = strpos($blockshtml, 'data-block=') !== false; if (!$hasblocks) { $blockdraweropen = false; } -$courseindex = false; +$courseindex = core_course_drawer(); if (!$courseindex) { $courseindexopen = false; } diff --git a/theme/boost/scss/moodle.scss b/theme/boost/scss/moodle.scss index 9eb49651a86..38a65025af4 100644 --- a/theme/boost/scss/moodle.scss +++ b/theme/boost/scss/moodle.scss @@ -44,3 +44,4 @@ $breadcrumb-divider-rtl: "◀" !default; @import "moodle/toasts"; @import "moodle/navbar"; @import "moodle/reportbuilder"; +@import "moodle/courseindex"; diff --git a/theme/boost/scss/moodle/courseindex.scss b/theme/boost/scss/moodle/courseindex.scss new file mode 100644 index 00000000000..33e32e764c8 --- /dev/null +++ b/theme/boost/scss/moodle/courseindex.scss @@ -0,0 +1,84 @@ +$courseindex-link-color: $list-group-action-color !default; +$courseindex-link-hover-color: $list-group-action-hover-color !default; +$courseindex-item-hover-bg: theme-color-level('info', -11) !default; +$courseindex-item-hover-border: theme-color-level('info', -9) !default; +$courseindex-item-active-border: $gray-300 !default; +$courseindex-item-active-bg: $gray-100 !default; +$courseindex-item-padding-y: 0.5rem; +$courseindex-item-padding-x: 1rem; +$courseindex-item-radius: $border-radius !default; +$courseindex-item-current: $primary !default; + +.courseindex { + .courseindex-heading { + padding: $courseindex-item-padding-y $courseindex-item-padding-x; + } + .courseindex-item { + padding: $courseindex-item-padding-y $courseindex-item-padding-x; + border: $border-width solid transparent; + @include border-radius($courseindex-item-radius); + + .courseindex-link { + color: $courseindex-link-color; + @include hover-focus() { + color: $courseindex-link-hover-color; + text-decoration: none; + } + } + + .courseindex-name { + font-style: italic; + } + + @include hover-focus() { + background-color: $courseindex-item-hover-bg; + border-color: $courseindex-item-hover-border; + } + + &.active { + background-color: $courseindex-item-active-bg; + border-color: $courseindex-item-active-border; + @include hover-focus() { + background-color: $courseindex-item-hover-bg; + border-color: $courseindex-item-hover-border; + } + } + + &.dimmed { + opacity: .7; + font-style: italic; + } + } + + .courseindex-sectioncontent { + .courseindex-item { + padding-left: calc(#{$courseindex-item-padding-x} * 2 + 0.5rem); + } + } + + .courseindex-section.current { + border-left: solid 3px $courseindex-item-current; + } + + .d-flex-noedit { + display: none; + } + + &.editing { + .d-flex-noedit { + display: flex; + } + } + + .media-list { + .rounded-circle { + height: 1rem; + width: 1rem; + } + + .w-100 { + height: 1rem; + margin: 0.5rem 0; + } + } +} diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index d4d8668592d..ccea513da42 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -20036,6 +20036,52 @@ div.editor_atto_toolbar button .icon { text-overflow: clip; word-break: break-all; } +.courseindex .courseindex-heading { + padding: 0.5rem 1rem; } + +.courseindex .courseindex-item { + padding: 0.5rem 1rem; + border: 1px solid transparent; } + .courseindex .courseindex-item .courseindex-link { + color: #495057; } + .courseindex .courseindex-item .courseindex-link:hover, .courseindex .courseindex-item .courseindex-link:focus { + color: #495057; + text-decoration: none; } + .courseindex .courseindex-item .courseindex-name { + font-style: italic; } + .courseindex .courseindex-item:hover, .courseindex .courseindex-item:focus { + background-color: #e0f0f2; + border-color: #b8dce2; } + .courseindex .courseindex-item.active { + background-color: #f8f9fa; + border-color: #dee2e6; } + .courseindex .courseindex-item.active:hover, .courseindex .courseindex-item.active:focus { + background-color: #e0f0f2; + border-color: #b8dce2; } + .courseindex .courseindex-item.dimmed { + opacity: .7; + font-style: italic; } + +.courseindex .courseindex-sectioncontent .courseindex-item { + padding-left: calc(1rem * 2 + 0.5rem); } + +.courseindex .courseindex-section.current { + border-left: solid 3px #0f6fc5; } + +.courseindex .d-flex-noedit { + display: none; } + +.courseindex.editing .d-flex-noedit { + display: flex; } + +.courseindex .media-list .rounded-circle { + height: 1rem; + width: 1rem; } + +.courseindex .media-list .w-100 { + height: 1rem; + margin: 0.5rem 0; } + body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } diff --git a/theme/boost/templates/drawers.mustache b/theme/boost/templates/drawers.mustache index 2da234ed00f..63bad60ad06 100644 --- a/theme/boost/templates/drawers.mustache +++ b/theme/boost/templates/drawers.mustache @@ -63,7 +63,7 @@ {{$id}}theme_boost-drawers-courseindex{{/id}} {{$drawerclasses}}drawer drawer-left {{#courseindexopen}}show{{/courseindexopen}}{{/drawerclasses}} {{$drawerheading}} - {{#str}} courseindex {{/str}} + {{#str}}courseindex, core_courseformat{{/str}} {{/drawerheading}} {{$drawercontent}} {{{courseindex}}} diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 958e79e1b87..3ce4c37ad0b 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -20227,6 +20227,53 @@ div.editor_atto_toolbar button .icon { text-overflow: clip; word-break: break-all; } +.courseindex .courseindex-heading { + padding: 0.5rem 1rem; } + +.courseindex .courseindex-item { + padding: 0.5rem 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; } + .courseindex .courseindex-item .courseindex-link { + color: #495057; } + .courseindex .courseindex-item .courseindex-link:hover, .courseindex .courseindex-item .courseindex-link:focus { + color: #495057; + text-decoration: none; } + .courseindex .courseindex-item .courseindex-name { + font-style: italic; } + .courseindex .courseindex-item:hover, .courseindex .courseindex-item:focus { + background-color: #e0f0f2; + border-color: #b8dce2; } + .courseindex .courseindex-item.active { + background-color: #f8f9fa; + border-color: #dee2e6; } + .courseindex .courseindex-item.active:hover, .courseindex .courseindex-item.active:focus { + background-color: #e0f0f2; + border-color: #b8dce2; } + .courseindex .courseindex-item.dimmed { + opacity: .7; + font-style: italic; } + +.courseindex .courseindex-sectioncontent .courseindex-item { + padding-left: calc(1rem * 2 + 0.5rem); } + +.courseindex .courseindex-section.current { + border-left: solid 3px #0f6fc5; } + +.courseindex .d-flex-noedit { + display: none; } + +.courseindex.editing .d-flex-noedit { + display: flex; } + +.courseindex .media-list .rounded-circle { + height: 1rem; + width: 1rem; } + +.courseindex .media-list .w-100 { + height: 1rem; + margin: 0.5rem 0; } + body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }