From 830c3eb907642369b60b287ff42147e420f82ec5 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Mon, 21 Jun 2021 11:59:53 +0200 Subject: [PATCH] MDL-71209 courseformat: add course index modules The course index is the first UI component that implements the new drawers and the reactive components. The course index uses the course state to present the current course structure and changes whenever that structure change. --- course/format/amd/build/courseeditor.min.js | 2 +- .../format/amd/build/courseeditor.min.js.map | 2 +- .../local/courseeditor/courseeditor.min.js | 2 +- .../courseeditor/courseeditor.min.js.map | 2 +- .../build/local/courseeditor/exporter.min.js | 2 + .../local/courseeditor/exporter.min.js.map | 1 + .../build/local/courseeditor/mutations.min.js | 2 +- .../local/courseeditor/mutations.min.js.map | 2 +- .../amd/build/local/courseindex/cm.min.js | 2 + .../amd/build/local/courseindex/cm.min.js.map | 1 + .../local/courseindex/courseindex.min.js | 2 + .../local/courseindex/courseindex.min.js.map | 1 + .../amd/build/local/courseindex/drawer.min.js | 2 + .../build/local/courseindex/drawer.min.js.map | 1 + .../local/courseindex/placeholder.min.js | 2 + .../local/courseindex/placeholder.min.js.map | 1 + course/format/amd/src/courseeditor.js | 12 + .../src/local/courseeditor/courseeditor.js | 27 +- .../amd/src/local/courseeditor/exporter.js | 97 +++++++ .../amd/src/local/courseeditor/mutations.js | 62 ++--- course/format/amd/src/local/courseindex/cm.js | 71 +++++ .../amd/src/local/courseindex/courseindex.js | 250 ++++++++++++++++++ .../amd/src/local/courseindex/drawer.js | 55 ++++ .../amd/src/local/courseindex/placeholder.js | 69 +++++ course/format/classes/base.php | 12 + .../format/classes/output/local/state/cm.php | 13 + .../classes/output/local/state/section.php | 4 + .../classes/output/section_renderer.php | 19 ++ course/format/classes/stateactions.php | 10 +- course/format/classes/stateupdates.php | 34 +-- .../templates/local/courseindex/cm.mustache | 64 +++++ .../local/courseindex/courseindex.mustache | 111 ++++++++ .../local/courseindex/drawer.mustache | 37 +++ .../local/courseindex/placeholders.mustache | 57 ++++ .../local/courseindex/section.mustache | 102 +++++++ .../format/tests/external/get_state_test.php | 6 +- .../tests/external/update_course_test.php | 6 +- .../format_theunittest_stateactions.php | 4 +- course/format/tests/stateactions_test.php | 6 +- course/format/tests/stateupdates_test.php | 22 +- course/format/topics/lib.php | 4 + course/format/upgrade.txt | 3 + course/format/weeks/lib.php | 4 + course/lib.php | 50 +++- course/renderer.php | 4 +- course/upgrade.txt | 2 + lang/en/courseformat.php | 1 + lang/en/moodle.php | 1 - theme/boost/layout/drawers.php | 3 +- theme/boost/scss/moodle.scss | 1 + theme/boost/scss/moodle/courseindex.scss | 84 ++++++ theme/boost/style/moodle.css | 46 ++++ theme/boost/templates/drawers.mustache | 2 +- theme/classic/style/moodle.css | 47 ++++ 54 files changed, 1319 insertions(+), 110 deletions(-) create mode 100644 course/format/amd/build/local/courseeditor/exporter.min.js create mode 100644 course/format/amd/build/local/courseeditor/exporter.min.js.map create mode 100644 course/format/amd/build/local/courseindex/cm.min.js create mode 100644 course/format/amd/build/local/courseindex/cm.min.js.map create mode 100644 course/format/amd/build/local/courseindex/courseindex.min.js create mode 100644 course/format/amd/build/local/courseindex/courseindex.min.js.map create mode 100644 course/format/amd/build/local/courseindex/drawer.min.js create mode 100644 course/format/amd/build/local/courseindex/drawer.min.js.map create mode 100644 course/format/amd/build/local/courseindex/placeholder.min.js create mode 100644 course/format/amd/build/local/courseindex/placeholder.min.js.map create mode 100644 course/format/amd/src/local/courseeditor/exporter.js create mode 100644 course/format/amd/src/local/courseindex/cm.js create mode 100644 course/format/amd/src/local/courseindex/courseindex.js create mode 100644 course/format/amd/src/local/courseindex/drawer.js create mode 100644 course/format/amd/src/local/courseindex/placeholder.js create mode 100644 course/format/templates/local/courseindex/cm.mustache create mode 100644 course/format/templates/local/courseindex/courseindex.mustache create mode 100644 course/format/templates/local/courseindex/drawer.mustache create mode 100644 course/format/templates/local/courseindex/placeholders.mustache create mode 100644 course/format/templates/local/courseindex/section.mustache create mode 100644 theme/boost/scss/moodle/courseindex.scss diff --git a/course/format/amd/build/courseeditor.min.js b/course/format/amd/build/courseeditor.min.js index a2f4a1fec18a5..7a1a39514d1a7 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 91ff5432a5c98..fd66829a1d4ca 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 bafc4211d2de8..21e7feb77bafa 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 0000000000000..7f492602506b2 --- /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 8ea3e503ec794..c247ae2c35b2c 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 0000000000000..9c94cb3582159 --- /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 0000000000000..27b49461a36c0 --- /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 0000000000000..9394481ed78f4 --- /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 0000000000000..d468afa12f6a1 --- /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 0000000000000..1cc9a5f28d412 --- /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 288611be3ca2f..a17a5f91a9601 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 fb4a494c10193..557841715de47 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 0000000000000..80b9ebf18b445 --- /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 51e77d43b53d7..23d3e33273096 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 0000000000000..b264bcbededa7 --- /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 0000000000000..464f7f0878d50 --- /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 0000000000000..6a91b378e45e4 --- /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 0000000000000..a70a6004d4abe --- /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 26f5dfa75ef1f..333edd754e7dc 100644 --- a/course/format/classes/base.php +++ b/course/format/classes/base.php @@ -371,6 +371,18 @@ public function uses_sections() { 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 2e5b29dd63cc4..d899acb1f262d 100644 --- a/course/format/classes/output/local/state/cm.php +++ b/course/format/classes/output/local/state/cm.php @@ -74,8 +74,21 @@ public function export_for_template(\renderer_base $output): stdClass { '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 a1e51f055f3df..b56671aca0fe7 100644 --- a/course/format/classes/output/local/state/section.php +++ b/course/format/classes/output/local/state/section.php @@ -55,15 +55,19 @@ public function __construct(course_format $format, section_info $section) { */ 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 93d8a8a1381f1..790cd05d60501 100644 --- a/course/format/classes/output/section_renderer.php +++ b/course/format/classes/output/section_renderer.php @@ -179,6 +179,25 @@ public function course_section_updated_cm_item( 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 d3e8a058e8eb8..f37bf70070259 100644 --- a/course/format/classes/stateactions.php +++ b/course/format/classes/stateactions.php @@ -78,14 +78,14 @@ public function cm_state( 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 @@ public function section_state( 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 @@ public function section_state( 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 @@ public function course_state( $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 d117bff11d474..7cf2d530c8222 100644 --- a/course/format/classes/stateupdates.php +++ b/course/format/classes/stateupdates.php @@ -73,19 +73,19 @@ public function jsonSerialize(): array { /** * 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 @@ public function add_section_update(int $sectionid): void { * @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 @@ public function add_section_delete(int $sectionid): void { * * @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 @@ public function add_cm_update(int $cmid): void { * @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 0000000000000..c17b17a35e462 --- /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 0000000000000..44e948c4826b1 --- /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 0000000000000..87efa69695137 --- /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 0000000000000..a2c563d247118 --- /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 0000000000000..91899cb16ffce --- /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 9c5bcae3ca3b4..bcedd162f7f84 100644 --- a/course/format/tests/external/get_state_test.php +++ b/course/format/tests/external/get_state_test.php @@ -143,7 +143,7 @@ public function test_get_state(string $role, string $format = 'topics', ?string // 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 @@ public function test_get_state(string $role, string $format = 'topics', ?string // 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 fa5cd815c2738..355e14923ba3d 100644 --- a/course/format/tests/external/update_course_test.php +++ b/course/format/tests/external/update_course_test.php @@ -111,7 +111,7 @@ public function execute_course_state_provider(): array { 'action' => 'cm_state', 'expected' => [ 'count' => 2, - 'action' => 'update', + 'action' => 'put', 'visible' => 1, ], 'expectexception' => false, @@ -223,14 +223,14 @@ public function test_execute_target_params() { $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 f1ad0847bc164..207f83acfb048 100644 --- a/course/format/tests/fixtures/format_theunittest_stateactions.php +++ b/course/format/tests/fixtures/format_theunittest_stateactions.php @@ -85,7 +85,7 @@ public function targetsection_test( ?int $targetcmid = null ): void { - $updates->add_section_update($targetsectionid); + $updates->add_section_put($targetsectionid); } /** @@ -105,7 +105,7 @@ public function targetcm_test( ?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 ff32dafbe4f0f..56c1f14c0578b 100644 --- a/course/format/tests/stateactions_test.php +++ b/course/format/tests/stateactions_test.php @@ -137,7 +137,7 @@ private function summarize_updates(stateupdates $updateobj): array { 'cm' => [], 'count' => 0, ], - 'update' => [ + 'put' => [ 'course' => [], 'section' => [], 'cm' => [], @@ -243,12 +243,12 @@ public function test_get_state( // 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 ebda0542b1a49..d3eca691e6000 100644 --- a/course/format/tests/stateupdates_test.php +++ b/course/format/tests/stateupdates_test.php @@ -30,14 +30,14 @@ 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 @@ public function test_add_course_update(string $role): void { $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 @@ private function find_update( * 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 @@ public function test_add_section(string $action, string $role, array $expected): */ 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 @@ public function test_add_cm(string $action, string $role, array $expected): void */ 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 3cc40e20755ce..b0e84ef965968 100644 --- a/course/format/topics/lib.php +++ b/course/format/topics/lib.php @@ -46,6 +46,10 @@ public function uses_sections() { 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 948497e61e8f8..bf173ba590ae9 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 ce9b326501850..5526b01f7b07c 100644 --- a/course/format/weeks/lib.php +++ b/course/format/weeks/lib.php @@ -45,6 +45,10 @@ public function uses_sections() { 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 fbbf4a8d22a46..b04d05c959a48 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 705e59a37c1d9..8f325e3d91cc7 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -560,12 +560,10 @@ public function course_section_cm_completion($course, &$completioninfo, cm_info * 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 836bf135f86ce..ca377ada42302 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 ea5725d49e5c5..7d5d9bfaa6b7c 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 318803f28d5ef..bc1d1955d50d5 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -356,7 +356,6 @@ $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 16607635b7867..e2d08c68ab98c 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 @@ 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 9eb49651a8691..38a65025af41b 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 0000000000000..33e32e764c8a9 --- /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 d4d8668592df2..ccea513da42a1 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 2da234ed00f71..63bad60ad06eb 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 958e79e1b875b..3ce4c37ad0b91 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; }