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 a2f4a1fec18..7a1a39514d1 100644 --- a/course/format/amd/build/courseeditor.min.js +++ b/course/format/amd/build/courseeditor.min.js @@ -1,2 +1,2 @@ -define ("core_courseformat/courseeditor",["exports","core_courseformat/local/courseeditor/mutations","core_courseformat/local/courseeditor/courseeditor","core_course/events"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.getCurrentCourseEditor=a.getCourseEditor=void 0;b=e(b);c=e(c);d=e(d);function e(a){return a&&a.__esModule?a:{default:a}}var g=new Map;function f(a,b){if(b===void 0){b=document}b.dispatchEvent(new CustomEvent(d.default.stateChanged,{bubbles:!0,detail:a}))}var h=function(a){a=parseInt(a);if(!g.has(a)){g.set(a,new c.default({name:"CourseEditor".concat(a),eventName:d.default.stateChanged,eventDispatch:f,mutations:new b.default}));g.get(a).loadCourse(a)}return g.get(a)};a.getCourseEditor=h;a.getCurrentCourseEditor=function getCurrentCourseEditor(){return h(M.cfg.courseId)}}); +define ("core_courseformat/courseeditor",["exports","core_courseformat/local/courseeditor/mutations","core_courseformat/local/courseeditor/courseeditor","core_course/events"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.getCurrentCourseEditor=a.getCourseEditor=a.setViewFormat=void 0;b=e(b);c=e(c);d=e(d);function e(a){return a&&a.__esModule?a:{default:a}}var g=new Map;function f(a,b){if(b===void 0){b=document}b.dispatchEvent(new CustomEvent(d.default.stateChanged,{bubbles:!0,detail:a}))}a.setViewFormat=function setViewFormat(a,b){var c=h(a);c.setViewFormat(b)};var h=function(a){a=parseInt(a);if(!g.has(a)){g.set(a,new c.default({name:"CourseEditor".concat(a),eventName:d.default.stateChanged,eventDispatch:f,mutations:new b.default}));g.get(a).loadCourse(a)}return g.get(a)};a.getCourseEditor=h;a.getCurrentCourseEditor=function getCurrentCourseEditor(){return h(M.cfg.courseId)}}); //# sourceMappingURL=courseeditor.min.js.map diff --git a/course/format/amd/build/courseeditor.min.js.map b/course/format/amd/build/courseeditor.min.js.map index 91ff5432a5c..fd66829a1d4 100644 --- a/course/format/amd/build/courseeditor.min.js.map +++ b/course/format/amd/build/courseeditor.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/courseeditor.js"],"names":["courseEditorMap","Map","dispatchStateChangedEvent","detail","target","document","dispatchEvent","CustomEvent","events","stateChanged","bubbles","getCourseEditor","courseId","parseInt","has","set","CourseEditor","name","eventName","eventDispatch","mutations","DefaultMutations","get","loadCourse","getCurrentCourseEditor","M","cfg"],"mappings":"iTAuBA,OACA,OACA,O,mDAGA,GAAMA,CAAAA,CAAe,CAAG,GAAIC,CAAAA,GAA5B,CAYA,QAASC,CAAAA,CAAT,CAAmCC,CAAnC,CAA2CC,CAA3C,CAAmD,CAC/C,GAAIA,CAAM,SAAV,CAA0B,CACtBA,CAAM,CAAGC,QACZ,CACDD,CAAM,CAACE,aAAP,CAAqB,GAAIC,CAAAA,WAAJ,CAAgBC,UAAOC,YAAvB,CAAqC,CACtDC,OAAO,GAD+C,CAEtDP,MAAM,CAAEA,CAF8C,CAArC,CAArB,CAIH,CAQM,GAAMQ,CAAAA,CAAe,CAAG,SAACC,CAAD,CAAc,CACzCA,CAAQ,CAAGC,QAAQ,CAACD,CAAD,CAAnB,CAEA,GAAI,CAACZ,CAAe,CAACc,GAAhB,CAAoBF,CAApB,CAAL,CAAoC,CAChCZ,CAAe,CAACe,GAAhB,CACIH,CADJ,CAEI,GAAII,UAAJ,CAAiB,CACbC,IAAI,uBAAiBL,CAAjB,CADS,CAEbM,SAAS,CAAEV,UAAOC,YAFL,CAGbU,aAAa,CAAEjB,CAHF,CAMbkB,SAAS,CAAE,GAAIC,UANF,CAAjB,CAFJ,EAWArB,CAAe,CAACsB,GAAhB,CAAoBV,CAApB,EAA8BW,UAA9B,CAAyCX,CAAzC,CACH,CACD,MAAOZ,CAAAA,CAAe,CAACsB,GAAhB,CAAoBV,CAApB,CACV,CAlBM,C,6CAyB+B,QAAzBY,CAAAA,sBAAyB,SAAMb,CAAAA,CAAe,CAACc,CAAC,CAACC,GAAF,CAAMd,QAAP,CAArB,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Generic reactive module used in the course editor.\n *\n * @module core_courseformat/courseeditor\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DefaultMutations from 'core_courseformat/local/courseeditor/mutations';\nimport CourseEditor from 'core_courseformat/local/courseeditor/courseeditor';\nimport events from 'core_course/events';\n\n// A map with all the course editor instances.\nconst courseEditorMap = new Map();\n\n/**\n * Trigger a state changed event.\n *\n * This function will be moved to core_course/events module\n * when the file is migrated to the new JS events structure proposed in MDL-70990.\n *\n * @method dispatchStateChangedEvent\n * @param {object} detail the full state\n * @param {object} target the custom event target (document if none provided)\n */\nfunction dispatchStateChangedEvent(detail, target) {\n if (target === undefined) {\n target = document;\n }\n target.dispatchEvent(new CustomEvent(events.stateChanged, {\n bubbles: true,\n detail: detail,\n }));\n}\n\n/**\n * Get a specific course editor reactive instance.\n *\n * @param {number} courseId the course id\n * @returns {CourseEditor}\n */\nexport const getCourseEditor = (courseId) => {\n courseId = parseInt(courseId);\n\n if (!courseEditorMap.has(courseId)) {\n courseEditorMap.set(\n courseId,\n new CourseEditor({\n name: `CourseEditor${courseId}`,\n eventName: events.stateChanged,\n eventDispatch: dispatchStateChangedEvent,\n // Mutations can be overridden by the format plugin using setMutations\n // but we need the default one at least.\n mutations: new DefaultMutations(),\n })\n );\n courseEditorMap.get(courseId).loadCourse(courseId);\n }\n return courseEditorMap.get(courseId);\n};\n\n/**\n * Get the current course reactive instance.\n *\n * @returns {CourseEditor}\n */\nexport const getCurrentCourseEditor = () => getCourseEditor(M.cfg.courseId);\n"],"file":"courseeditor.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/courseeditor.js"],"names":["courseEditorMap","Map","dispatchStateChangedEvent","detail","target","document","dispatchEvent","CustomEvent","events","stateChanged","bubbles","setViewFormat","courseId","setup","editor","getCourseEditor","parseInt","has","set","CourseEditor","name","eventName","eventDispatch","mutations","DefaultMutations","get","loadCourse","getCurrentCourseEditor","M","cfg"],"mappings":"iUAuBA,OACA,OACA,O,mDAGA,GAAMA,CAAAA,CAAe,CAAG,GAAIC,CAAAA,GAA5B,CAYA,QAASC,CAAAA,CAAT,CAAmCC,CAAnC,CAA2CC,CAA3C,CAAmD,CAC/C,GAAIA,CAAM,SAAV,CAA0B,CACtBA,CAAM,CAAGC,QACZ,CACDD,CAAM,CAACE,aAAP,CAAqB,GAAIC,CAAAA,WAAJ,CAAgBC,UAAOC,YAAvB,CAAqC,CACtDC,OAAO,GAD+C,CAEtDP,MAAM,CAAEA,CAF8C,CAArC,CAArB,CAIH,C,gBAS4B,QAAhBQ,CAAAA,aAAgB,CAACC,CAAD,CAAWC,CAAX,CAAqB,CAC9C,GAAMC,CAAAA,CAAM,CAAGC,CAAe,CAACH,CAAD,CAA9B,CACAE,CAAM,CAACH,aAAP,CAAqBE,CAArB,CACH,C,CAQM,GAAME,CAAAA,CAAe,CAAG,SAACH,CAAD,CAAc,CACzCA,CAAQ,CAAGI,QAAQ,CAACJ,CAAD,CAAnB,CAEA,GAAI,CAACZ,CAAe,CAACiB,GAAhB,CAAoBL,CAApB,CAAL,CAAoC,CAChCZ,CAAe,CAACkB,GAAhB,CACIN,CADJ,CAEI,GAAIO,UAAJ,CAAiB,CACbC,IAAI,uBAAiBR,CAAjB,CADS,CAEbS,SAAS,CAAEb,UAAOC,YAFL,CAGba,aAAa,CAAEpB,CAHF,CAMbqB,SAAS,CAAE,GAAIC,UANF,CAAjB,CAFJ,EAWAxB,CAAe,CAACyB,GAAhB,CAAoBb,CAApB,EAA8Bc,UAA9B,CAAyCd,CAAzC,CACH,CACD,MAAOZ,CAAAA,CAAe,CAACyB,GAAhB,CAAoBb,CAApB,CACV,CAlBM,C,6CAyB+B,QAAzBe,CAAAA,sBAAyB,SAAMZ,CAAAA,CAAe,CAACa,CAAC,CAACC,GAAF,CAAMjB,QAAP,CAArB,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Generic reactive module used in the course editor.\n *\n * @module core_courseformat/courseeditor\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DefaultMutations from 'core_courseformat/local/courseeditor/mutations';\nimport CourseEditor from 'core_courseformat/local/courseeditor/courseeditor';\nimport events from 'core_course/events';\n\n// A map with all the course editor instances.\nconst courseEditorMap = new Map();\n\n/**\n * Trigger a state changed event.\n *\n * This function will be moved to core_course/events module\n * when the file is migrated to the new JS events structure proposed in MDL-70990.\n *\n * @method dispatchStateChangedEvent\n * @param {object} detail the full state\n * @param {object} target the custom event target (document if none provided)\n */\nfunction dispatchStateChangedEvent(detail, target) {\n if (target === undefined) {\n target = document;\n }\n target.dispatchEvent(new CustomEvent(events.stateChanged, {\n bubbles: true,\n detail: detail,\n }));\n}\n\n/**\n * Setup the current view settings\n *\n * @param {number} courseId the course id\n * @param {setup} setup format, page and course settings\n * @property {boolean} setup.editing if the page is in edit mode\n */\nexport const setViewFormat = (courseId, setup) => {\n const editor = getCourseEditor(courseId);\n editor.setViewFormat(setup);\n};\n\n/**\n * Get a specific course editor reactive instance.\n *\n * @param {number} courseId the course id\n * @returns {CourseEditor}\n */\nexport const getCourseEditor = (courseId) => {\n courseId = parseInt(courseId);\n\n if (!courseEditorMap.has(courseId)) {\n courseEditorMap.set(\n courseId,\n new CourseEditor({\n name: `CourseEditor${courseId}`,\n eventName: events.stateChanged,\n eventDispatch: dispatchStateChangedEvent,\n // Mutations can be overridden by the format plugin using setMutations\n // but we need the default one at least.\n mutations: new DefaultMutations(),\n })\n );\n courseEditorMap.get(courseId).loadCourse(courseId);\n }\n return courseEditorMap.get(courseId);\n};\n\n/**\n * Get the current course reactive instance.\n *\n * @returns {CourseEditor}\n */\nexport const getCurrentCourseEditor = () => getCourseEditor(M.cfg.courseId);\n"],"file":"courseeditor.min.js"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/courseeditor.min.js b/course/format/amd/build/local/courseeditor/courseeditor.min.js index bafc4211d2d..21e7feb77ba 100644 --- a/course/format/amd/build/local/courseeditor/courseeditor.min.js +++ b/course/format/amd/build/local/courseeditor/courseeditor.min.js @@ -1,2 +1,2 @@ -define ("core_courseformat/local/courseeditor/courseeditor",["exports","core/reactive","core/notification","core/log","core/ajax"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;c=f(c);d=f(d);e=f(e);function f(a){return a&&a.__esModule?a:{default:a}}function g(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){g=function(a){return typeof a}}else{g=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return g(a)}function h(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function i(a){for(var b=1,c;b.\n\nimport {Reactive} from 'core/reactive';\nimport notification from 'core/notification';\nimport log from 'core/log';\nimport ajax from 'core/ajax';\n\n/**\n * Main course editor module.\n *\n * All formats can register new components on this object to create new reactive\n * UI components that watch the current course state.\n *\n * @module core_courseformat/local/courseeditor/courseeditor\n * @class core_courseformat/local/courseeditor/courseeditor\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class extends Reactive {\n\n /**\n * Set up the course editor when the page is ready.\n *\n * The course can only be loaded once per instance. Otherwise an error is thrown.\n *\n * @param {number} courseId course id\n */\n async loadCourse(courseId) {\n\n if (this.courseId) {\n throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);\n }\n\n this.courseId = courseId;\n\n let stateData;\n\n try {\n stateData = await this.getServerCourseState();\n } catch (error) {\n log.error(\"EXCEPTION RAISED WHILE INIT COURSE EDITOR\");\n log.error(error);\n return;\n }\n\n // Edit mode is part of the state but it could change over time.\n // Components should use isEditing method to check the editing mode instead.\n this._editing = stateData.course.editmode ?? false;\n\n this.setInitialState(stateData);\n }\n\n /**\n * Load the current course state from the server.\n *\n * @returns {Object} the current course state\n */\n async getServerCourseState() {\n const courseState = await ajax.call([{\n methodname: 'core_courseformat_get_state',\n args: {\n courseid: this.courseId,\n }\n }])[0];\n\n const stateData = JSON.parse(courseState);\n\n return {\n course: {},\n section: [],\n cm: [],\n ...stateData,\n };\n }\n\n /**\n * Return the current edit mode.\n *\n * Components should use this method to check if edit mode is active.\n *\n * @return {boolean} if edit is enabled\n */\n get isEditing() {\n return this._editing ?? false;\n }\n\n /**\n * Dispatch a change in the state.\n *\n * Usually reactive modules throw an error directly to the components when something\n * goes wrong. However, course editor can directly display a notification.\n *\n * @method dispatch\n * @param {string} actionName the action name (usually the mutation name)\n * @param {*} param any number of params the mutation needs.\n */\n async dispatch(...args) {\n try {\n await super.dispatch(...args);\n } catch (error) {\n notification.exception(error);\n }\n }\n}\n"],"file":"courseeditor.min.js"} \ No newline at end of file +{"version":3,"sources":["../../../src/local/courseeditor/courseeditor.js"],"names":["courseId","Error","_editing","getServerCourseState","stateData","log","error","setInitialState","setup","editing","ajax","call","methodname","args","courseid","courseState","JSON","parse","course","section","cm","Exporter","notification","exception","Reactive"],"mappings":"wRAgBA,OACA,OACA,OACA,O,qrGAsBqBA,C,6FAET,KAAKA,Q,sBACC,IAAIC,CAAAA,KAAJ,uBAAyBD,CAAzB,4CAAoE,KAAKA,QAAzE,E,QAIV,KAAKE,QAAL,IAEA,KAAKF,QAAL,CAAgBA,CAAhB,C,wBAKsB,MAAKG,oBAAL,E,QAAlBC,C,6DAEAC,UAAIC,KAAJ,CAAU,2CAAV,EACAD,UAAIC,KAAJ,O,kCAIJ,KAAKC,eAAL,CAAqBH,CAArB,E,6KASUI,C,CAAO,OACjB,KAAKN,QAAL,WAAgBM,CAAK,CAACC,OAAtB,kBACH,C,oMAQ6BC,WAAKC,IAAL,CAAU,CAAC,CACjCC,UAAU,CAAE,6BADqB,CAEjCC,IAAI,CAAE,CACFC,QAAQ,CAAE,KAAKd,QADb,CAF2B,CAAD,CAAV,EAKtB,CALsB,C,QAApBe,C,QAOAX,C,CAAYY,IAAI,CAACC,KAAL,CAAWF,CAAX,C,6BAGdG,MAAM,CAAE,E,CACRC,OAAO,CAAE,E,CACTC,EAAE,CAAE,E,EACDhB,C,6KAoBG,CACV,MAAO,IAAIiB,UAAJ,CAAa,IAAb,CACV,C,iNAYiBR,C,uBAAAA,C,yFAEcA,C,4DAExBS,UAAaC,SAAb,O,0JA3BQ,OACZ,iBAAO,KAAKrB,QAAZ,kBACH,C,cA3EwBsB,U","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {Reactive} from 'core/reactive';\nimport notification from 'core/notification';\nimport Exporter from 'core_courseformat/local/courseeditor/exporter';\nimport log from 'core/log';\nimport ajax from 'core/ajax';\n\n/**\n * Main course editor module.\n *\n * All formats can register new components on this object to create new reactive\n * UI components that watch the current course state.\n *\n * @module core_courseformat/local/courseeditor/courseeditor\n * @class core_courseformat/local/courseeditor/courseeditor\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class extends Reactive {\n\n /**\n * Set up the course editor when the page is ready.\n *\n * The course can only be loaded once per instance. Otherwise an error is thrown.\n *\n * @param {number} courseId course id\n */\n async loadCourse(courseId) {\n\n if (this.courseId) {\n throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);\n }\n\n // Default view format setup.\n this._editing = false;\n\n this.courseId = courseId;\n\n let stateData;\n\n try {\n stateData = await this.getServerCourseState();\n } catch (error) {\n log.error(\"EXCEPTION RAISED WHILE INIT COURSE EDITOR\");\n log.error(error);\n return;\n }\n\n this.setInitialState(stateData);\n }\n\n /**\n * Setup the current view settings\n *\n * @param {Object} setup format, page and course settings\n * @property {boolean} setup.editing if the page is in edit mode\n */\n setViewFormat(setup) {\n this._editing = setup.editing ?? false;\n }\n\n /**\n * Load the current course state from the server.\n *\n * @returns {Object} the current course state\n */\n async getServerCourseState() {\n const courseState = await ajax.call([{\n methodname: 'core_courseformat_get_state',\n args: {\n courseid: this.courseId,\n }\n }])[0];\n\n const stateData = JSON.parse(courseState);\n\n return {\n course: {},\n section: [],\n cm: [],\n ...stateData,\n };\n }\n\n /**\n * Return the current edit mode.\n *\n * Components should use this method to check if edit mode is active.\n *\n * @return {boolean} if edit is enabled\n */\n get isEditing() {\n return this._editing ?? false;\n }\n\n /**\n * Return a data exporter to transform state part into mustache contexts.\n *\n * @return {Exporter} the exporter class\n */\n getExporter() {\n return new Exporter(this);\n }\n\n /**\n * Dispatch a change in the state.\n *\n * Usually reactive modules throw an error directly to the components when something\n * goes wrong. However, course editor can directly display a notification.\n *\n * @method dispatch\n * @param {string} actionName the action name (usually the mutation name)\n * @param {*} param any number of params the mutation needs.\n */\n async dispatch(...args) {\n try {\n await super.dispatch(...args);\n } catch (error) {\n notification.exception(error);\n }\n }\n}\n"],"file":"courseeditor.min.js"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/exporter.min.js b/course/format/amd/build/local/courseeditor/exporter.min.js new file mode 100644 index 00000000000..7f492602506 --- /dev/null +++ b/course/format/amd/build/local/courseeditor/exporter.min.js @@ -0,0 +1,2 @@ +define ("core_courseformat/local/courseeditor/exporter",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function b(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function c(a){for(var c=1,e;c.\n\n/**\n * Module to export parts of the state and transform them to be used in templates\n * and as draggable data.\n *\n * @module core_courseformat/local/courseeditor/exporter\n * @class core_courseformat/local/courseeditor/exporter\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n /**\n * Class constructor.\n *\n * @param {CourseEditor} reactive the course editor object\n */\n constructor(reactive) {\n this.reactive = reactive;\n }\n\n /**\n * Generate the course export data from the state.\n *\n * @param {Object} state the current state.\n * @returns {Object}\n */\n course(state) {\n // Collect section information from the state.\n const data = {\n sections: [],\n editmode: this.reactive.isEditing,\n };\n const sectionlist = state.course.sectionlist ?? [];\n sectionlist.forEach(sectionid => {\n const sectioninfo = state.section.get(sectionid) ?? {};\n const section = this.section(state, sectioninfo);\n data.sections.push(section);\n });\n data.hassections = (data.sections.length != 0);\n\n return data;\n }\n\n /**\n * Generate a section export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} sectioninfo the section state data.\n * @returns {Object}\n */\n section(state, sectioninfo) {\n const section = {\n ...sectioninfo,\n cms: [],\n isactive: false,\n };\n const cmlist = sectioninfo.cmlist ?? [];\n cmlist.forEach(cmid => {\n const cminfo = state.cm.get(cmid);\n const cm = this.cm(state, cminfo);\n section.cms.push(cm);\n });\n section.hascms = (section.cms.length != 0);\n\n return section;\n }\n\n /**\n * Generate a cm export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} cminfo the course module state data.\n * @returns {Object}\n */\n cm(state, cminfo) {\n const cm = {\n ...cminfo,\n isactive: false,\n };\n return cm;\n }\n}\n"],"file":"exporter.min.js"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/mutations.min.js b/course/format/amd/build/local/courseeditor/mutations.min.js index 8ea3e503ec7..c247ae2c35b 100644 --- a/course/format/amd/build/local/courseeditor/mutations.min.js +++ b/course/format/amd/build/local/courseeditor/mutations.min.js @@ -1,2 +1,2 @@ -define ("core_courseformat/local/courseeditor/mutations",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);function c(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function d(a){return function(){var b=this,d=arguments;return new Promise(function(e,f){var i=a.apply(b,d);function g(a){c(i,e,f,g,h,"next",a)}function h(a){c(i,e,f,g,h,"throw",a)}g(void 0)})}}function e(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function f(a,b){for(var c=0,d;c.\n\nimport ajax from 'core/ajax';\n\n/**\n * Default mutation manager\n *\n * @module core_courseformat/local/courseeditor/mutations\n * @class core_courseformat/local/courseeditor/mutations\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n // All course editor mutations for Moodle 4.0 will be located in this file.\n\n /**\n * Private method to call core_courseformat_update_course webservice.\n *\n * @method _callEditWebservice\n * @param {string} action\n * @param {number} courseId\n * @param {array} ids\n */\n async _callEditWebservice(action, courseId, ids) {\n let ajaxresult = await ajax.call([{\n methodname: 'core_courseformat_update_course',\n args: {\n action,\n courseid: courseId,\n ids,\n }\n }])[0];\n return JSON.parse(ajaxresult);\n }\n\n /**\n * Get updated state data related to some cm ids.\n *\n * @method cmState\n * @param {StateManager} statemanager the current state\n * @param {array} cmids the list of cm ids to update\n */\n async cmState(statemanager, cmids) {\n const state = statemanager.state;\n const updates = await this._callEditWebservice('cm_state', state.course.id, cmids);\n statemanager.setReadOnly(false);\n this._processUpdates(statemanager, updates);\n }\n\n /**\n * Get updated state data related to some section ids.\n *\n * @method sectionState\n * @param {StateManager} statemanager the current state\n * @param {array} sectionIds the list of section ids to update\n */\n async sectionState(statemanager, sectionIds) {\n const state = statemanager.state;\n const updates = await this._callEditWebservice('section_state', state.course.id, sectionIds);\n this._processUpdates(statemanager, updates);\n }\n\n /**\n * Helper to propcess both section_state and cm_state action results.\n *\n * @param {StateManager} statemanager the current state\n * @param {Array} updates of updates.\n */\n _processUpdates(statemanager, updates) {\n\n const state = statemanager.state;\n\n statemanager.setReadOnly(false);\n\n // The cm_state and section_state state action returns only updated states. However, most of the time we need this\n // mutation to fix discrepancies between the course content and the course state because core_course_edit_module\n // does not provide enough information to rebuild some state objects. This is the reason why we cannot use\n // the batch method processUpdates as the rest of mutations do.\n updates.forEach((update) => {\n if (update.name === undefined) {\n throw Error('Missing state update name');\n }\n // Compare the action with the current state.\n let current = state[update.name];\n if (current instanceof Map) {\n current = state[update.name].get(update.fields.id);\n }\n if (!current) {\n update.action = 'create';\n }\n\n statemanager.processUpdate(update.name, update.action, update.fields);\n });\n\n statemanager.setReadOnly(true);\n }\n}\n"],"file":"mutations.min.js"} \ No newline at end of file +{"version":3,"sources":["../../../src/local/courseeditor/mutations.js"],"names":["action","courseId","ids","ajax","call","methodname","args","courseid","ajaxresult","JSON","parse","stateManager","cmids","course","get","_callEditWebservice","id","updates","processUpdates","sectionIds"],"mappings":"8KAeA,uD,owBAsB8BA,C,CAAQC,C,CAAUC,C,wGACjBC,WAAKC,IAAL,CAAU,CAAC,CAC9BC,UAAU,CAAE,iCADkB,CAE9BC,IAAI,CAAE,CACFN,MAAM,CAANA,CADE,CAEFO,QAAQ,CAAEN,CAFR,CAGFC,GAAG,CAAHA,CAHE,CAFwB,CAAD,CAAV,EAOnB,CAPmB,C,QAAnBM,C,iCAQGC,IAAI,CAACC,KAAL,CAAWF,CAAX,C,kMAUGG,C,CAAcC,C,2FAClBC,C,CAASF,CAAY,CAACG,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,UAAzB,CAAqCF,CAAM,CAACG,EAA5C,CAAgDJ,CAAhD,C,QAAhBK,C,QACNN,CAAY,CAACO,cAAb,CAA4BD,CAA5B,E,8LAUeN,C,CAAcQ,C,2FACvBN,C,CAASF,CAAY,CAACG,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,eAAzB,CAA0CF,CAAM,CAACG,EAAjD,CAAqDG,CAArD,C,QAAhBF,C,QACNN,CAAY,CAACO,cAAb,CAA4BD,CAA5B,E,kMAQcN,C,2FACRE,C,CAASF,CAAY,CAACG,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,cAAzB,CAAyCF,CAAM,CAACG,EAAhD,C,QAAhBC,C,QACNN,CAAY,CAACO,cAAb,CAA4BD,CAA5B,E","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport ajax from 'core/ajax';\n\n/**\n * Default mutation manager\n *\n * @module core_courseformat/local/courseeditor/mutations\n * @class core_courseformat/local/courseeditor/mutations\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n // All course editor mutations for Moodle 4.0 will be located in this file.\n\n /**\n * Private method to call core_courseformat_update_course webservice.\n *\n * @method _callEditWebservice\n * @param {string} action\n * @param {number} courseId\n * @param {array} ids\n */\n async _callEditWebservice(action, courseId, ids) {\n let ajaxresult = await ajax.call([{\n methodname: 'core_courseformat_update_course',\n args: {\n action,\n courseid: courseId,\n ids,\n }\n }])[0];\n return JSON.parse(ajaxresult);\n }\n\n /**\n * Get updated state data related to some cm ids.\n *\n * @method cmState\n * @param {StateManager} stateManager the current state\n * @param {array} cmids the list of cm ids to update\n */\n async cmState(stateManager, cmids) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('cm_state', course.id, cmids);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Get updated state data related to some section ids.\n *\n * @method sectionState\n * @param {StateManager} stateManager the current state\n * @param {array} sectionIds the list of section ids to update\n */\n async sectionState(stateManager, sectionIds) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_state', course.id, sectionIds);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Get the full updated state data of the course.\n *\n * @param {StateManager} stateManager the current state\n */\n async courseState(stateManager) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('course_state', course.id);\n stateManager.processUpdates(updates);\n }\n\n}\n"],"file":"mutations.min.js"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/cm.min.js b/course/format/amd/build/local/courseindex/cm.min.js new file mode 100644 index 00000000000..9c94cb35821 --- /dev/null +++ b/course/format/amd/build/local/courseindex/cm.min.js @@ -0,0 +1,2 @@ +define ("core_courseformat/local/courseindex/cm",["exports","core/reactive"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function c(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){c=function(a){return typeof a}}else{c=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return c(a)}function d(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function e(a,b){for(var c=0,d;c.\n\n/**\n * Course index cm component.\n *\n * This component is used to control specific course modules interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/cm\n * @class core_courseformat/local/courseindex/cm\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_cm';\n // Default query selectors.\n this.selectors = {\n };\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new Component({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n // Activate drag and drop soon.\n }\n\n getWatchers() {\n return [\n {watch: `cm[${this.id}]:deleted`, handler: this.remove},\n ];\n }\n\n}\n"],"file":"cm.min.js"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/courseindex.min.js b/course/format/amd/build/local/courseindex/courseindex.min.js new file mode 100644 index 00000000000..27b49461a36 --- /dev/null +++ b/course/format/amd/build/local/courseindex/courseindex.min.js @@ -0,0 +1,2 @@ +define ("core_courseformat/local/courseindex/courseindex",["exports","core/reactive","core_courseformat/courseeditor"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function d(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){d=function(a){return typeof a}}else{d=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return d(a)}function e(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function f(a){return function(){var b=this,c=arguments;return new Promise(function(d,f){var i=a.apply(b,c);function g(a){e(i,d,f,g,h,"next",a)}function h(a){e(i,d,f,g,h,"throw",a)}g(void 0)})}}function g(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function h(a,b){for(var c=0,d;cb.length){a.removeChild(a.lastChild)}}},{key:"_deleteCm",value:function _deleteCm(a){var b=a.element;delete this.cms[b.id]}}],[{key:"init",value:function init(a,d){return new b({element:document.getElementById(a),reactive:(0,c.getCurrentCourseEditor)(),selectors:d})}}]);return b}(b.BaseComponent);a.default=q;return a.default}); +//# sourceMappingURL=courseindex.min.js.map diff --git a/course/format/amd/build/local/courseindex/courseindex.min.js.map b/course/format/amd/build/local/courseindex/courseindex.min.js.map new file mode 100644 index 00000000000..9394481ed78 --- /dev/null +++ b/course/format/amd/build/local/courseindex/courseindex.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/local/courseindex/courseindex.js"],"names":["Component","name","selectors","SECTION","SECTION_ITEM","SECTION_TITLE","SECTION_CMLIST","CM","CM_NAME","TOGGLER","COLLAPSE","classes","SECTIONHIDDEN","CMHIDDEN","SECTIONCURRENT","sections","cms","addEventListener","element","_setupSectionTogglers","getElements","forEach","section","dataset","id","cm","watch","handler","_refreshSection","_refreshCm","_createCm","_deleteCm","_refreshCourseSectionlist","_refreshSectionCmlist","event","sectionlink","target","closest","preventDefault","parentNode","querySelector","click","Error","sectionitem","classList","toggle","visible","current","innerHTML","title","state","fakeelement","document","createElement","add","get","sectionid","exporter","reactive","getExporter","data","renderComponent","newcomponent","newelement","getElement","replaceChild","cmlist","listparent","_fixOrder","sectionlist","container","neworder","allitems","length","remove","itemid","index","item","currentitem","children","append","insertBefore","removeChild","lastChild","getElementById","BaseComponent"],"mappings":"opEA2BqBA,CAAAA,C,+HAKR,CAEL,KAAKC,IAAL,CAAY,aAAZ,CAEA,KAAKC,SAAL,CAAiB,CACbC,OAAO,uBADM,CAEbC,YAAY,4BAFC,CAGbC,aAAa,6BAHA,CAIbC,cAAc,sBAJD,CAKbC,EAAE,kBALW,CAMbC,OAAO,uBANM,CAObC,OAAO,6CAPM,CAQbC,QAAQ,6BARK,CAAjB,CAWA,KAAKC,OAAL,CAAe,CACXC,aAAa,CAAE,QADJ,CAEXC,QAAQ,CAAE,QAFC,CAGXC,cAAc,CAAE,SAHL,CAAf,CAMA,KAAKC,QAAL,CAAgB,EAAhB,CACA,KAAKC,GAAL,CAAW,EACd,C,+CAoBY,YAET,KAAKC,gBAAL,CAAsB,KAAKC,OAA3B,CAAoC,OAApC,CAA6C,KAAKC,qBAAlD,EAGA,GAAMJ,CAAAA,CAAQ,CAAG,KAAKK,WAAL,CAAiB,KAAKlB,SAAL,CAAeC,OAAhC,CAAjB,CACAY,CAAQ,CAACM,OAAT,CAAiB,SAACC,CAAD,CAAa,CAC1B,CAAI,CAACP,QAAL,CAAcO,CAAO,CAACC,OAAR,CAAgBC,EAA9B,EAAoCF,CACvC,CAFD,EAGA,GAAMN,CAAAA,CAAG,CAAG,KAAKI,WAAL,CAAiB,KAAKlB,SAAL,CAAeK,EAAhC,CAAZ,CACAS,CAAG,CAACK,OAAJ,CAAY,SAACI,CAAD,CAAQ,CAChB,CAAI,CAACT,GAAL,CAASS,CAAE,CAACF,OAAH,CAAWC,EAApB,EAA0BC,CAC7B,CAFD,CAGH,C,iDAEa,CACV,MAAO,CACH,CAACC,KAAK,kBAAN,CAA2BC,OAAO,CAAE,KAAKC,eAAzC,CADG,CAEH,CAACF,KAAK,aAAN,CAAsBC,OAAO,CAAE,KAAKE,UAApC,CAFG,CAGH,CAACH,KAAK,aAAN,CAAsBC,OAAO,CAAE,KAAKG,SAApC,CAHG,CAIH,CAACJ,KAAK,aAAN,CAAsBC,OAAO,CAAE,KAAKI,SAApC,CAJG,CAMH,CAACL,KAAK,6BAAN,CAAsCC,OAAO,CAAE,KAAKK,yBAApD,CANG,CAOH,CAACN,KAAK,yBAAN,CAAkCC,OAAO,CAAE,KAAKM,qBAAhD,CAPG,CASV,C,oEAUqBC,C,CAAO,CACzB,GAAMC,CAAAA,CAAW,CAAGD,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqB,KAAKnC,SAAL,CAAeO,OAApC,CAApB,CACA,GAAI0B,CAAJ,CAAiB,CACbD,CAAK,CAACI,cAAN,GACAH,CAAW,CAACI,UAAZ,CAAuBC,aAAvB,CAAqC,KAAKtC,SAAL,CAAeQ,QAApD,EAA8D+B,KAA9D,EACH,CACJ,C,0DAO0B,IAAVvB,CAAAA,CAAU,GAAVA,OAAU,CAEjBkB,CAAM,CAAG,KAAKrB,QAAL,CAAcG,CAAO,CAACM,EAAtB,CAFQ,CAGvB,GAAI,CAACY,CAAL,CAAa,CACT,KAAM,IAAIM,CAAAA,KAAJ,kCAAoCxB,CAAO,CAACM,EAA5C,EACT,CAED,GAAMmB,CAAAA,CAAW,CAAGP,CAAM,CAACI,aAAP,CAAqB,KAAKtC,SAAL,CAAeE,YAApC,CAApB,CACAuC,CAAW,CAACC,SAAZ,CAAsBC,MAAtB,CAA6B,KAAKlC,OAAL,CAAaC,aAA1C,CAAyD,CAACM,CAAO,CAAC4B,OAAlE,EACAV,CAAM,CAACQ,SAAP,CAAiBC,MAAjB,CAAwB,KAAKlC,OAAL,CAAaG,cAArC,CAAqDI,CAAO,CAAC6B,OAA7D,EAEAX,CAAM,CAACI,aAAP,CAAqB,KAAKtC,SAAL,CAAeG,aAApC,EAAmD2C,SAAnD,CAA+D9B,CAAO,CAAC+B,KAC1E,C,gDAOqB,IAAV/B,CAAAA,CAAU,GAAVA,OAAU,CAEZkB,CAAM,CAAG,KAAKpB,GAAL,CAASE,CAAO,CAACM,EAAjB,CAFG,CAGlB,GAAI,CAACY,CAAL,CAAa,CACT,KAAM,IAAIM,CAAAA,KAAJ,wCAA0CxB,CAAO,CAACM,EAAlD,EACT,CAEDY,CAAM,CAACQ,SAAP,CAAiBC,MAAjB,CAAwB,KAAKlC,OAAL,CAAaE,QAArC,CAA+C,CAACK,CAAO,CAAC4B,OAAxD,EACAV,CAAM,CAACI,aAAP,CAAqB,KAAKtC,SAAL,CAAeM,OAApC,EAA6CwC,SAA7C,CAAyD9B,CAAO,CAACjB,IACpE,C,qLAOgBiD,C,GAAAA,K,CAAOhC,C,GAAAA,O,CAEdiC,C,CAAcC,QAAQ,CAACC,aAAT,CAAuB,IAAvB,C,CACpBF,CAAW,CAACP,SAAZ,CAAsBU,GAAtB,CAA0B,eAA1B,CAA2C,OAA3C,EACAH,CAAW,CAACH,SAAZ,CAAwB,QAAxB,CACA,KAAKhC,GAAL,CAASE,CAAO,CAACM,EAAjB,EAAuB2B,CAAvB,CAEA,KAAKlB,qBAAL,CAA2B,CACvBiB,KAAK,CAALA,CADuB,CAEvBhC,OAAO,CAAEgC,CAAK,CAAC5B,OAAN,CAAciC,GAAd,CAAkBrC,CAAO,CAACsC,SAA1B,CAFc,CAA3B,EAKMC,C,CAAW,KAAKC,QAAL,CAAcC,WAAd,E,CACXC,C,CAAOH,CAAQ,CAAChC,EAAT,CAAYyB,CAAZ,CAAmBhC,CAAnB,C,iBAEc,MAAK2C,eAAL,CAAqBV,CAArB,CAAkC,wCAAlC,CAA4ES,CAA5E,C,SAArBE,C,QAEAC,C,CAAaD,CAAY,CAACE,UAAb,E,CACnB,KAAKhD,GAAL,CAASE,CAAO,CAACM,EAAjB,EAAuBuC,CAAvB,CACAZ,CAAW,CAACZ,UAAZ,CAAuB0B,YAAvB,CAAoCF,CAApC,CAAgDZ,CAAhD,E,qLAQ6B,OAAVjC,CAAU,GAAVA,OAAU,CACvBgD,CAAM,WAAGhD,CAAO,CAACgD,MAAX,gBAAqB,EADJ,CAEvBC,CAAU,CAAG,KAAKH,UAAL,CAAgB,KAAK9D,SAAL,CAAeI,cAA/B,CAA+CY,CAAO,CAACM,EAAvD,CAFU,CAG7B,KAAK4C,SAAL,CAAeD,CAAf,CAA2BD,CAA3B,CAAmC,KAAKlD,GAAxC,CACH,C,8EAOoC,OAAVE,CAAU,GAAVA,OAAU,CAC3BmD,CAAW,WAAGnD,CAAO,CAACmD,WAAX,gBAA0B,EADV,CAEjC,KAAKD,SAAL,CAAe,KAAKlD,OAApB,CAA6BmD,CAA7B,CAA0C,KAAKtD,QAA/C,CACH,C,4CASSuD,C,CAAWC,C,CAAUC,C,CAAU,CAGrC,GAAI,CAACD,CAAQ,CAACE,MAAd,CAAsB,CAClBH,CAAS,CAAC1B,SAAV,CAAoBU,GAApB,CAAwB,QAAxB,EACAgB,CAAS,CAACtB,SAAV,CAAsB,EAAtB,CACA,MACH,CAGDsB,CAAS,CAAC1B,SAAV,CAAoB8B,MAApB,CAA2B,QAA3B,EAGAH,CAAQ,CAAClD,OAAT,CAAiB,SAACsD,CAAD,CAASC,CAAT,CAAmB,IAC1BC,CAAAA,CAAI,CAAGL,CAAQ,CAACG,CAAD,CADW,CAG1BG,CAAW,CAAGR,CAAS,CAACS,QAAV,CAAmBH,CAAnB,CAHY,CAIhC,GAAIE,CAAW,SAAf,CAA+B,CAC3BR,CAAS,CAACU,MAAV,CAAiBH,CAAjB,EACA,MACH,CACD,GAAIC,CAAW,GAAKD,CAApB,CAA0B,CACtBP,CAAS,CAACW,YAAV,CAAuBJ,CAAvB,CAA6BC,CAA7B,CACH,CACJ,CAXD,EAaA,MAAOR,CAAS,CAACS,QAAV,CAAmBN,MAAnB,CAA4BF,CAAQ,CAACE,MAA5C,CAAoD,CAChDH,CAAS,CAACY,WAAV,CAAsBZ,CAAS,CAACa,SAAhC,CACH,CACJ,C,8CASoB,IAAVjE,CAAAA,CAAU,GAAVA,OAAU,CACjB,MAAO,MAAKF,GAAL,CAASE,CAAO,CAACM,EAAjB,CACV,C,oCAxLWY,C,CAAQlC,C,CAAW,CAC3B,MAAO,IAAIF,CAAAA,CAAJ,CAAc,CACjBkB,OAAO,CAAEkC,QAAQ,CAACgC,cAAT,CAAwBhD,CAAxB,CADQ,CAEjBsB,QAAQ,CAAE,8BAFO,CAGjBxD,SAAS,CAATA,CAHiB,CAAd,CAKV,C,cA3CkCmF,e","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Course index main component.\n *\n * @module core_courseformat/local/courseindex/courseindex\n * @class core_courseformat/local/courseindex/courseindex\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex';\n // Default query selectors.\n this.selectors = {\n SECTION: `[data-for='section']`,\n SECTION_ITEM: `[data-for='section_item']`,\n SECTION_TITLE: `[data-for='section_title']`,\n SECTION_CMLIST: `[data-for='cmlist']`,\n CM: `[data-for='cm']`,\n CM_NAME: `[data-for='cm_name']`,\n TOGGLER: `[data-action=\"togglecourseindexsection\"]`,\n COLLAPSE: `[data-toggle=\"collapse\"]`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n CMHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n };\n // Arrays to keep cms and sections elements.\n this.sections = {};\n this.cms = {};\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new Component({\n element: document.getElementById(target),\n reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n // Activate section togglers.\n this.addEventListener(this.element, 'click', this._setupSectionTogglers);\n\n // Get cms and sections elements.\n const sections = this.getElements(this.selectors.SECTION);\n sections.forEach((section) => {\n this.sections[section.dataset.id] = section;\n });\n const cms = this.getElements(this.selectors.CM);\n cms.forEach((cm) => {\n this.cms[cm.dataset.id] = cm;\n });\n }\n\n getWatchers() {\n return [\n {watch: `section:updated`, handler: this._refreshSection},\n {watch: `cm:updated`, handler: this._refreshCm},\n {watch: `cm:created`, handler: this._createCm},\n {watch: `cm:deleted`, handler: this._deleteCm},\n // Sections and cm sorting.\n {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},\n {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},\n ];\n }\n\n /**\n * Setup sections toggler.\n *\n * Toggler click is delegated to the main course index element because new sections can\n * appear at any moment and this way we prevent accidental double bindings.\n *\n * @param {Event} event the triggered event\n */\n _setupSectionTogglers(event) {\n const sectionlink = event.target.closest(this.selectors.TOGGLER);\n if (sectionlink) {\n event.preventDefault();\n sectionlink.parentNode.querySelector(this.selectors.COLLAPSE).click();\n }\n }\n\n /**\n * Update a course index section using the state information.\n *\n * @param {Object} details the update details.\n */\n _refreshSection({element}) {\n // Find the element.\n const target = this.sections[element.id];\n if (!target) {\n throw new Error(`Unkown section with ID ${element.id}`);\n }\n // Update classes.\n const sectionitem = target.querySelector(this.selectors.SECTION_ITEM);\n sectionitem.classList.toggle(this.classes.SECTIONHIDDEN, !element.visible);\n target.classList.toggle(this.classes.SECTIONCURRENT, element.current);\n // Update title.\n target.querySelector(this.selectors.SECTION_TITLE).innerHTML = element.title;\n }\n\n /**\n * Update a course index cm using the state information.\n *\n * @param {Object} details the update details.\n */\n _refreshCm({element}) {\n // Find the element.\n const target = this.cms[element.id];\n if (!target) {\n throw new Error(`Unkown course module with ID ${element.id}`);\n }\n // Update classes.\n target.classList.toggle(this.classes.CMHIDDEN, !element.visible);\n target.querySelector(this.selectors.CM_NAME).innerHTML = element.name;\n }\n\n /**\n * Create a newcm instance.\n *\n * @param {Object} details the update details.\n */\n async _createCm({state, element}) {\n // Create a fake node while the component is loading.\n const fakeelement = document.createElement('li');\n fakeelement.classList.add('bg-pulse-grey', 'w-100');\n fakeelement.innerHTML = ' ';\n this.cms[element.id] = fakeelement;\n // Place the fake node on the correct position.\n this._refreshSectionCmlist({\n state,\n element: state.section.get(element.sectionid),\n });\n // Collect render data.\n const exporter = this.reactive.getExporter();\n const data = exporter.cm(state, element);\n // Create the new content.\n const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/cm', data);\n // Replace the fake node with the real content.\n const newelement = newcomponent.getElement();\n this.cms[element.id] = newelement;\n fakeelement.parentNode.replaceChild(newelement, fakeelement);\n }\n\n /**\n * Refresh a section cm list.\n *\n * @param {Object} details the update details.\n */\n _refreshSectionCmlist({element}) {\n const cmlist = element.cmlist ?? [];\n const listparent = this.getElement(this.selectors.SECTION_CMLIST, element.id);\n this._fixOrder(listparent, cmlist, this.cms);\n }\n\n /**\n * Refresh the section list.\n *\n * @param {Object} details the update details.\n */\n _refreshCourseSectionlist({element}) {\n const sectionlist = element.sectionlist ?? [];\n this._fixOrder(this.element, sectionlist, this.sections);\n }\n\n /**\n * Fix/reorder the section or cms order.\n *\n * @param {Element} container the HTML element to reorder.\n * @param {Array} neworder an array with the ids order\n * @param {Array} allitems the list of html elements that can be placed in the container\n */\n _fixOrder(container, neworder, allitems) {\n\n // Empty lists should not be visible.\n if (!neworder.length) {\n container.classList.add('hidden');\n container.innerHTML = '';\n return;\n }\n\n // Grant the list is visible (in case it was empty).\n container.classList.remove('hidden');\n\n // Move the elements in order at the beginning of the list.\n neworder.forEach((itemid, index) => {\n const item = allitems[itemid];\n // Get the current element at that position.\n const currentitem = container.children[index];\n if (currentitem === undefined) {\n container.append(item);\n return;\n }\n if (currentitem !== item) {\n container.insertBefore(item, currentitem);\n }\n });\n // Remove the remaining elements.\n while (container.children.length > neworder.length) {\n container.removeChild(container.lastChild);\n }\n }\n\n /**\n * Remove a cm from the list.\n *\n * The actual DOM element removal is delegated to the cm component.\n *\n * @param {Object} details the update details.\n */\n _deleteCm({element}) {\n delete this.cms[element.id];\n }\n}\n"],"file":"courseindex.min.js"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/drawer.min.js b/course/format/amd/build/local/courseindex/drawer.min.js new file mode 100644 index 00000000000..d468afa12f6 --- /dev/null +++ b/course/format/amd/build/local/courseindex/drawer.min.js @@ -0,0 +1,2 @@ +define ("core_courseformat/local/courseindex/drawer",["exports","core/reactive","core_courseformat/courseeditor"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;function d(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){d=function(a){return typeof a}}else{d=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return d(a)}function e(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function f(a,b){for(var c=0,d;c.\n\n/**\n * Course index drawer wrap.\n *\n * This component is mostly used to ensure all subcomponents find a parent\n * compoment with a reactive instance defined.\n *\n * @module core_courseformat/local/courseindex/drawer\n * @class core_courseformat/local/courseindex/drawer\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex-drawer';\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new Component({\n element: document.getElementById(target),\n reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n}\n"],"file":"drawer.min.js"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/placeholder.min.js b/course/format/amd/build/local/courseindex/placeholder.min.js new file mode 100644 index 00000000000..1cc9a5f28d4 --- /dev/null +++ b/course/format/amd/build/local/courseindex/placeholder.min.js @@ -0,0 +1,2 @@ +define ("core_courseformat/local/courseindex/placeholder",["exports","core/reactive","core/templates","core_courseformat/courseeditor"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;c=function(a){return a&&a.__esModule?a:{default:a}}(c);function e(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){e=function(a){return typeof a}}else{e=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return e(a)}function f(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function g(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function g(a){f(i,d,e,g,h,"next",a)}function h(a){f(i,d,e,g,h,"throw",a)}g(void 0)})}}function h(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function i(a,b){for(var c=0,d;c.\n\n/**\n * Course index placeholder replacer.\n *\n * @module core_courseformat/local/courseindex/placeholder\n * @class core_courseformat/local/courseindex/placeholder\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport Templates from 'core/templates';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new Component({\n element: document.getElementById(target),\n reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * This stateReady to be async because it loads the real courseindex.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(state);\n\n try {\n // To render an HTML into our component we just use the regular Templates module.\n const {html, js} = await Templates.renderForPromise(\n 'core_courseformat/local/courseindex/courseindex',\n data,\n );\n Templates.replaceNode(this.element, html, js);\n } catch (error) {\n throw error;\n }\n }\n}\n"],"file":"placeholder.min.js"} \ No newline at end of file diff --git a/course/format/amd/src/courseeditor.js b/course/format/amd/src/courseeditor.js index 288611be3ca..a17a5f91a96 100644 --- a/course/format/amd/src/courseeditor.js +++ b/course/format/amd/src/courseeditor.js @@ -48,6 +48,18 @@ function dispatchStateChangedEvent(detail, target) { })); } +/** + * Setup the current view settings + * + * @param {number} courseId the course id + * @param {setup} setup format, page and course settings + * @property {boolean} setup.editing if the page is in edit mode + */ +export const setViewFormat = (courseId, setup) => { + const editor = getCourseEditor(courseId); + editor.setViewFormat(setup); +}; + /** * Get a specific course editor reactive instance. * diff --git a/course/format/amd/src/local/courseeditor/courseeditor.js b/course/format/amd/src/local/courseeditor/courseeditor.js index fb4a494c101..557841715de 100644 --- a/course/format/amd/src/local/courseeditor/courseeditor.js +++ b/course/format/amd/src/local/courseeditor/courseeditor.js @@ -15,6 +15,7 @@ import {Reactive} from 'core/reactive'; import notification from 'core/notification'; +import Exporter from 'core_courseformat/local/courseeditor/exporter'; import log from 'core/log'; import ajax from 'core/ajax'; @@ -44,6 +45,9 @@ export default class extends Reactive { throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`); } + // Default view format setup. + this._editing = false; + this.courseId = courseId; let stateData; @@ -56,13 +60,19 @@ export default class extends Reactive { return; } - // Edit mode is part of the state but it could change over time. - // Components should use isEditing method to check the editing mode instead. - this._editing = stateData.course.editmode ?? false; - this.setInitialState(stateData); } + /** + * Setup the current view settings + * + * @param {Object} setup format, page and course settings + * @property {boolean} setup.editing if the page is in edit mode + */ + setViewFormat(setup) { + this._editing = setup.editing ?? false; + } + /** * Load the current course state from the server. * @@ -97,6 +107,15 @@ export default class extends Reactive { return this._editing ?? false; } + /** + * Return a data exporter to transform state part into mustache contexts. + * + * @return {Exporter} the exporter class + */ + getExporter() { + return new Exporter(this); + } + /** * Dispatch a change in the state. * diff --git a/course/format/amd/src/local/courseeditor/exporter.js b/course/format/amd/src/local/courseeditor/exporter.js new file mode 100644 index 00000000000..80b9ebf18b4 --- /dev/null +++ b/course/format/amd/src/local/courseeditor/exporter.js @@ -0,0 +1,97 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Module to export parts of the state and transform them to be used in templates + * and as draggable data. + * + * @module core_courseformat/local/courseeditor/exporter + * @class core_courseformat/local/courseeditor/exporter + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +export default class { + + /** + * Class constructor. + * + * @param {CourseEditor} reactive the course editor object + */ + constructor(reactive) { + this.reactive = reactive; + } + + /** + * Generate the course export data from the state. + * + * @param {Object} state the current state. + * @returns {Object} + */ + course(state) { + // Collect section information from the state. + const data = { + sections: [], + editmode: this.reactive.isEditing, + }; + const sectionlist = state.course.sectionlist ?? []; + sectionlist.forEach(sectionid => { + const sectioninfo = state.section.get(sectionid) ?? {}; + const section = this.section(state, sectioninfo); + data.sections.push(section); + }); + data.hassections = (data.sections.length != 0); + + return data; + } + + /** + * Generate a section export data from the state. + * + * @param {Object} state the current state. + * @param {Object} sectioninfo the section state data. + * @returns {Object} + */ + section(state, sectioninfo) { + const section = { + ...sectioninfo, + cms: [], + isactive: false, + }; + const cmlist = sectioninfo.cmlist ?? []; + cmlist.forEach(cmid => { + const cminfo = state.cm.get(cmid); + const cm = this.cm(state, cminfo); + section.cms.push(cm); + }); + section.hascms = (section.cms.length != 0); + + return section; + } + + /** + * Generate a cm export data from the state. + * + * @param {Object} state the current state. + * @param {Object} cminfo the course module state data. + * @returns {Object} + */ + cm(state, cminfo) { + const cm = { + ...cminfo, + isactive: false, + }; + return cm; + } +} diff --git a/course/format/amd/src/local/courseeditor/mutations.js b/course/format/amd/src/local/courseeditor/mutations.js index 51e77d43b53..23d3e332730 100644 --- a/course/format/amd/src/local/courseeditor/mutations.js +++ b/course/format/amd/src/local/courseeditor/mutations.js @@ -51,61 +51,37 @@ export default class { * Get updated state data related to some cm ids. * * @method cmState - * @param {StateManager} statemanager the current state + * @param {StateManager} stateManager the current state * @param {array} cmids the list of cm ids to update */ - async cmState(statemanager, cmids) { - const state = statemanager.state; - const updates = await this._callEditWebservice('cm_state', state.course.id, cmids); - statemanager.setReadOnly(false); - this._processUpdates(statemanager, updates); + async cmState(stateManager, cmids) { + const course = stateManager.get('course'); + const updates = await this._callEditWebservice('cm_state', course.id, cmids); + stateManager.processUpdates(updates); } /** * Get updated state data related to some section ids. * * @method sectionState - * @param {StateManager} statemanager the current state + * @param {StateManager} stateManager the current state * @param {array} sectionIds the list of section ids to update */ - async sectionState(statemanager, sectionIds) { - const state = statemanager.state; - const updates = await this._callEditWebservice('section_state', state.course.id, sectionIds); - this._processUpdates(statemanager, updates); + async sectionState(stateManager, sectionIds) { + const course = stateManager.get('course'); + const updates = await this._callEditWebservice('section_state', course.id, sectionIds); + stateManager.processUpdates(updates); } /** - * Helper to propcess both section_state and cm_state action results. - * - * @param {StateManager} statemanager the current state - * @param {Array} updates of updates. - */ - _processUpdates(statemanager, updates) { - - const state = statemanager.state; - - statemanager.setReadOnly(false); - - // The cm_state and section_state state action returns only updated states. However, most of the time we need this - // mutation to fix discrepancies between the course content and the course state because core_course_edit_module - // does not provide enough information to rebuild some state objects. This is the reason why we cannot use - // the batch method processUpdates as the rest of mutations do. - updates.forEach((update) => { - if (update.name === undefined) { - throw Error('Missing state update name'); - } - // Compare the action with the current state. - let current = state[update.name]; - if (current instanceof Map) { - current = state[update.name].get(update.fields.id); - } - if (!current) { - update.action = 'create'; - } - - statemanager.processUpdate(update.name, update.action, update.fields); - }); - - statemanager.setReadOnly(true); + * Get the full updated state data of the course. + * + * @param {StateManager} stateManager the current state + */ + async courseState(stateManager) { + const course = stateManager.get('course'); + const updates = await this._callEditWebservice('course_state', course.id); + stateManager.processUpdates(updates); } + } diff --git a/course/format/amd/src/local/courseindex/cm.js b/course/format/amd/src/local/courseindex/cm.js new file mode 100644 index 00000000000..b264bcbeded --- /dev/null +++ b/course/format/amd/src/local/courseindex/cm.js @@ -0,0 +1,71 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Course index cm component. + * + * This component is used to control specific course modules interactions like drag and drop. + * + * @module core_courseformat/local/courseindex/cm + * @class core_courseformat/local/courseindex/cm + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {BaseComponent} from 'core/reactive'; + +export default class Component extends BaseComponent { + + /** + * Constructor hook. + */ + create() { + // Optional component name for debugging. + this.name = 'courseindex_cm'; + // Default query selectors. + this.selectors = { + }; + // We need our id to watch specific events. + this.id = this.element.dataset.id; + } + + /** + * Static method to create a component instance form the mustache template. + * + * @param {element|string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new Component({ + element: document.getElementById(target), + selectors, + }); + } + + /** + * Initial state ready method. + */ + stateReady() { + // Activate drag and drop soon. + } + + getWatchers() { + return [ + {watch: `cm[${this.id}]:deleted`, handler: this.remove}, + ]; + } + +} diff --git a/course/format/amd/src/local/courseindex/courseindex.js b/course/format/amd/src/local/courseindex/courseindex.js new file mode 100644 index 00000000000..464f7f0878d --- /dev/null +++ b/course/format/amd/src/local/courseindex/courseindex.js @@ -0,0 +1,250 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Course index main component. + * + * @module core_courseformat/local/courseindex/courseindex + * @class core_courseformat/local/courseindex/courseindex + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {BaseComponent} from 'core/reactive'; +import {getCurrentCourseEditor} from 'core_courseformat/courseeditor'; + +export default class Component extends BaseComponent { + + /** + * Constructor hook. + */ + create() { + // Optional component name for debugging. + this.name = 'courseindex'; + // Default query selectors. + this.selectors = { + SECTION: `[data-for='section']`, + SECTION_ITEM: `[data-for='section_item']`, + SECTION_TITLE: `[data-for='section_title']`, + SECTION_CMLIST: `[data-for='cmlist']`, + CM: `[data-for='cm']`, + CM_NAME: `[data-for='cm_name']`, + TOGGLER: `[data-action="togglecourseindexsection"]`, + COLLAPSE: `[data-toggle="collapse"]`, + }; + // Default classes to toggle on refresh. + this.classes = { + SECTIONHIDDEN: 'dimmed', + CMHIDDEN: 'dimmed', + SECTIONCURRENT: 'current', + }; + // Arrays to keep cms and sections elements. + this.sections = {}; + this.cms = {}; + } + + /** + * Static method to create a component instance form the mustache template. + * + * @param {element|string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new Component({ + element: document.getElementById(target), + reactive: getCurrentCourseEditor(), + selectors, + }); + } + + /** + * Initial state ready method. + */ + stateReady() { + // Activate section togglers. + this.addEventListener(this.element, 'click', this._setupSectionTogglers); + + // Get cms and sections elements. + const sections = this.getElements(this.selectors.SECTION); + sections.forEach((section) => { + this.sections[section.dataset.id] = section; + }); + const cms = this.getElements(this.selectors.CM); + cms.forEach((cm) => { + this.cms[cm.dataset.id] = cm; + }); + } + + getWatchers() { + return [ + {watch: `section:updated`, handler: this._refreshSection}, + {watch: `cm:updated`, handler: this._refreshCm}, + {watch: `cm:created`, handler: this._createCm}, + {watch: `cm:deleted`, handler: this._deleteCm}, + // Sections and cm sorting. + {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist}, + {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist}, + ]; + } + + /** + * Setup sections toggler. + * + * Toggler click is delegated to the main course index element because new sections can + * appear at any moment and this way we prevent accidental double bindings. + * + * @param {Event} event the triggered event + */ + _setupSectionTogglers(event) { + const sectionlink = event.target.closest(this.selectors.TOGGLER); + if (sectionlink) { + event.preventDefault(); + sectionlink.parentNode.querySelector(this.selectors.COLLAPSE).click(); + } + } + + /** + * Update a course index section using the state information. + * + * @param {Object} details the update details. + */ + _refreshSection({element}) { + // Find the element. + const target = this.sections[element.id]; + if (!target) { + throw new Error(`Unkown section with ID ${element.id}`); + } + // Update classes. + const sectionitem = target.querySelector(this.selectors.SECTION_ITEM); + sectionitem.classList.toggle(this.classes.SECTIONHIDDEN, !element.visible); + target.classList.toggle(this.classes.SECTIONCURRENT, element.current); + // Update title. + target.querySelector(this.selectors.SECTION_TITLE).innerHTML = element.title; + } + + /** + * Update a course index cm using the state information. + * + * @param {Object} details the update details. + */ + _refreshCm({element}) { + // Find the element. + const target = this.cms[element.id]; + if (!target) { + throw new Error(`Unkown course module with ID ${element.id}`); + } + // Update classes. + target.classList.toggle(this.classes.CMHIDDEN, !element.visible); + target.querySelector(this.selectors.CM_NAME).innerHTML = element.name; + } + + /** + * Create a newcm instance. + * + * @param {Object} details the update details. + */ + async _createCm({state, element}) { + // Create a fake node while the component is loading. + const fakeelement = document.createElement('li'); + fakeelement.classList.add('bg-pulse-grey', 'w-100'); + fakeelement.innerHTML = ' '; + this.cms[element.id] = fakeelement; + // Place the fake node on the correct position. + this._refreshSectionCmlist({ + state, + element: state.section.get(element.sectionid), + }); + // Collect render data. + const exporter = this.reactive.getExporter(); + const data = exporter.cm(state, element); + // Create the new content. + const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/cm', data); + // Replace the fake node with the real content. + const newelement = newcomponent.getElement(); + this.cms[element.id] = newelement; + fakeelement.parentNode.replaceChild(newelement, fakeelement); + } + + /** + * Refresh a section cm list. + * + * @param {Object} details the update details. + */ + _refreshSectionCmlist({element}) { + const cmlist = element.cmlist ?? []; + const listparent = this.getElement(this.selectors.SECTION_CMLIST, element.id); + this._fixOrder(listparent, cmlist, this.cms); + } + + /** + * Refresh the section list. + * + * @param {Object} details the update details. + */ + _refreshCourseSectionlist({element}) { + const sectionlist = element.sectionlist ?? []; + this._fixOrder(this.element, sectionlist, this.sections); + } + + /** + * Fix/reorder the section or cms order. + * + * @param {Element} container the HTML element to reorder. + * @param {Array} neworder an array with the ids order + * @param {Array} allitems the list of html elements that can be placed in the container + */ + _fixOrder(container, neworder, allitems) { + + // Empty lists should not be visible. + if (!neworder.length) { + container.classList.add('hidden'); + container.innerHTML = ''; + return; + } + + // Grant the list is visible (in case it was empty). + container.classList.remove('hidden'); + + // Move the elements in order at the beginning of the list. + neworder.forEach((itemid, index) => { + const item = allitems[itemid]; + // Get the current element at that position. + const currentitem = container.children[index]; + if (currentitem === undefined) { + container.append(item); + return; + } + if (currentitem !== item) { + container.insertBefore(item, currentitem); + } + }); + // Remove the remaining elements. + while (container.children.length > neworder.length) { + container.removeChild(container.lastChild); + } + } + + /** + * Remove a cm from the list. + * + * The actual DOM element removal is delegated to the cm component. + * + * @param {Object} details the update details. + */ + _deleteCm({element}) { + delete this.cms[element.id]; + } +} diff --git a/course/format/amd/src/local/courseindex/drawer.js b/course/format/amd/src/local/courseindex/drawer.js new file mode 100644 index 00000000000..6a91b378e45 --- /dev/null +++ b/course/format/amd/src/local/courseindex/drawer.js @@ -0,0 +1,55 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Course index drawer wrap. + * + * This component is mostly used to ensure all subcomponents find a parent + * compoment with a reactive instance defined. + * + * @module core_courseformat/local/courseindex/drawer + * @class core_courseformat/local/courseindex/drawer + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {BaseComponent} from 'core/reactive'; +import {getCurrentCourseEditor} from 'core_courseformat/courseeditor'; + +export default class Component extends BaseComponent { + + /** + * Constructor hook. + */ + create() { + // Optional component name for debugging. + this.name = 'courseindex-drawer'; + } + + /** + * Static method to create a component instance form the mustache template. + * + * @param {element|string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new Component({ + element: document.getElementById(target), + reactive: getCurrentCourseEditor(), + selectors, + }); + } +} diff --git a/course/format/amd/src/local/courseindex/placeholder.js b/course/format/amd/src/local/courseindex/placeholder.js new file mode 100644 index 00000000000..a70a6004d4a --- /dev/null +++ b/course/format/amd/src/local/courseindex/placeholder.js @@ -0,0 +1,69 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Course index placeholder replacer. + * + * @module core_courseformat/local/courseindex/placeholder + * @class core_courseformat/local/courseindex/placeholder + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {BaseComponent} from 'core/reactive'; +import Templates from 'core/templates'; +import {getCurrentCourseEditor} from 'core_courseformat/courseeditor'; + +export default class Component extends BaseComponent { + + /** + * Static method to create a component instance form the mustache template. + * + * @param {element|string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new Component({ + element: document.getElementById(target), + reactive: getCurrentCourseEditor(), + selectors, + }); + } + + /** + * Initial state ready method. + * + * This stateReady to be async because it loads the real courseindex. + * + * @param {object} state the initial state + */ + async stateReady(state) { + // Collect section information from the state. + const exporter = this.reactive.getExporter(); + const data = exporter.course(state); + + try { + // To render an HTML into our component we just use the regular Templates module. + const {html, js} = await Templates.renderForPromise( + 'core_courseformat/local/courseindex/courseindex', + data, + ); + Templates.replaceNode(this.element, html, js); + } catch (error) { + throw error; + } + } +} diff --git a/course/format/classes/base.php b/course/format/classes/base.php index 26f5dfa75ef..333edd754e7 100644 --- a/course/format/classes/base.php +++ b/course/format/classes/base.php @@ -371,6 +371,18 @@ abstract class base { return false; } + /** + * Returns true if this course format uses course index + * + * This function may be called without specifying the course id + * i.e. in course_index_drawer() + * + * @return bool + */ + public function uses_course_index() { + return false; + } + /** * Returns a list of sections used in the course * diff --git a/course/format/classes/output/local/state/cm.php b/course/format/classes/output/local/state/cm.php index 2e5b29dd63c..d899acb1f26 100644 --- a/course/format/classes/output/local/state/cm.php +++ b/course/format/classes/output/local/state/cm.php @@ -74,8 +74,21 @@ class cm implements renderable { 'id' => $cm->id, 'name' => $cm->name, 'visible' => !empty($cm->visible), + 'sectionid' => $section->id, + 'sectionnumber' => $section->section, + 'uservisible' => $cm->uservisible, ]; + // Check the user access type to this cm. + $conditionalhidden = $output->is_cm_conditionally_hidden($cm); + $data->accessvisible = ($data->visible && !$conditionalhidden); + + // Add url if the activity is compatible. + $url = $cm->url; + if ($url) { + $data->url = $url->out(); + } + if ($this->exportcontent) { $data->content = $output->course_section_updated_cm_item($format, $section, $cm); } diff --git a/course/format/classes/output/local/state/section.php b/course/format/classes/output/local/state/section.php index a1e51f055f3..b56671aca0f 100644 --- a/course/format/classes/output/local/state/section.php +++ b/course/format/classes/output/local/state/section.php @@ -55,15 +55,19 @@ class section implements renderable { */ public function export_for_template(\renderer_base $output): stdClass { $format = $this->format; + $course = $format->get_course(); $section = $this->section; $modinfo = $format->get_modinfo(); $data = (object)[ 'id' => $section->id, 'section' => $section->section, + 'number' => $section->section, 'title' => $format->get_section_name($section), 'cmlist' => [], 'visible' => !empty($section->visible), + 'sectionurl' => course_get_url($course, $section->section)->out(), + 'current' => $format->is_section_current($section), ]; if (empty($modinfo->sections[$section->section])) { diff --git a/course/format/classes/output/section_renderer.php b/course/format/classes/output/section_renderer.php index 93d8a8a1381..790cd05d605 100644 --- a/course/format/classes/output/section_renderer.php +++ b/course/format/classes/output/section_renderer.php @@ -179,6 +179,25 @@ abstract class section_renderer extends core_course_renderer { return $this->render($cmitem); } + /** + * Get the course index drawer with placeholder. + * + * The default course index is loaded after the page is ready. Format plugins can override + * this method to provide an alternative course index. + * + * If the format is not compatible with the course index, this method will return an empty string. + * + * @param course_format $format the course format + * @return String the course index HTML. + */ + public function course_index_drawer(course_format $format): ?String { + if ($format->uses_course_index()) { + include_course_editor($format); + return $this->render_from_template('core_courseformat/local/courseindex/drawer', []); + } + return ''; + } + /** * Generate the edit control action menu * diff --git a/course/format/classes/stateactions.php b/course/format/classes/stateactions.php index d3e8a058e8e..f37bf700702 100644 --- a/course/format/classes/stateactions.php +++ b/course/format/classes/stateactions.php @@ -78,14 +78,14 @@ class stateactions { foreach (array_keys($cmids) as $cmid) { // Add this action to updates array. - $updates->add_cm_update($cmid); + $updates->add_cm_put($cmid); $cm = $modinfo->get_cm($cmid); $sectionids[$cm->section] = true; } foreach (array_keys($sectionids) as $sectionid) { - $updates->add_section_update($sectionid); + $updates->add_section_put($sectionid); } } @@ -129,7 +129,7 @@ class stateactions { foreach (array_keys($sectionids) as $sectionid) { $sectioninfo = $modinfo->get_section_info_by_id($sectionid); - $updates->add_section_update($sectionid); + $updates->add_section_put($sectionid); // Add cms. if (empty($modinfo->sections[$sectioninfo->section])) { continue; @@ -145,7 +145,7 @@ class stateactions { foreach (array_keys($cmids) as $cmid) { // Add this action to updates array. - $updates->add_cm_update($cmid); + $updates->add_cm_put($cmid); } } @@ -171,7 +171,7 @@ class stateactions { $modinfo = course_modinfo::instance($course); - $updates->add_course_update(); + $updates->add_course_put(); // Add sections updates. $sections = $modinfo->get_section_info_all(); diff --git a/course/format/classes/stateupdates.php b/course/format/classes/stateupdates.php index d117bff11d4..7cf2d530c82 100644 --- a/course/format/classes/stateupdates.php +++ b/course/format/classes/stateupdates.php @@ -73,19 +73,19 @@ class stateupdates implements JsonSerializable { /** * Add track about a general course state change. */ - public function add_course_update(): void { + public function add_course_put(): void { $courseclass = $this->format->get_output_classname('state\\course'); $currentstate = new $courseclass($this->format); - $this->add_update('course', 'update', $currentstate->export_for_template($this->output)); + $this->add_update('course', 'put', $currentstate->export_for_template($this->output)); } /** - * Add track about a section state update. + * Add track about a section state put. * * @param int $sectionid The affected section id. */ - public function add_section_update(int $sectionid): void { - $this->create_or_update_section($sectionid, 'update'); + public function add_section_put(int $sectionid): void { + $this->create_or_put_section($sectionid, 'put'); } /** @@ -94,19 +94,19 @@ class stateupdates implements JsonSerializable { * @param int $sectionid The affected section id. */ public function add_section_create(int $sectionid): void { - $this->create_or_update_section($sectionid, 'create'); + $this->create_or_put_section($sectionid, 'create'); } /** - * Add track about section created or updated. + * Add track about section created or put. * * @param int $sectionid The affected section id. - * @param string $action The action to track for the section ('create' or 'update). + * @param string $action The action to track for the section ('create' or 'put'). */ - protected function create_or_update_section(int $sectionid, string $action): void { - if ($action != 'create' && $action != 'update') { + protected function create_or_put_section(int $sectionid, string $action): void { + if ($action != 'create' && $action != 'put') { throw new coding_exception( - "Invalid action passed ($action) to create_or_update_section. Only 'create' and 'update' are valid." + "Invalid action passed ($action) to create_or_put_section. Only 'create' and 'put' are valid." ); } $course = $this->format->get_course(); @@ -138,8 +138,8 @@ class stateupdates implements JsonSerializable { * * @param int $cmid the affected course module id */ - public function add_cm_update(int $cmid): void { - $this->create_or_update_cm($cmid, 'update'); + public function add_cm_put(int $cmid): void { + $this->create_or_put_cm($cmid, 'put'); } /** @@ -148,16 +148,16 @@ class stateupdates implements JsonSerializable { * @param int $cmid the affected course module id */ public function add_cm_create(int $cmid): void { - $this->create_or_update_cm($cmid, 'create', true); + $this->create_or_put_cm($cmid, 'create', true); } /** - * Add track about section created or updated. + * Add track about section created or put. * * @param int $cmid The affected course module id. - * @param string $action The action to track for the section ('create' or 'update'). + * @param string $action The action to track for the section ('create' or 'put'). */ - protected function create_or_update_cm(int $cmid, string $action): void { + protected function create_or_put_cm(int $cmid, string $action): void { $modinfo = course_modinfo::instance($this->format->get_course()); $cm = $modinfo->get_cm($cmid); diff --git a/course/format/templates/local/courseindex/cm.mustache b/course/format/templates/local/courseindex/cm.mustache new file mode 100644 index 00000000000..c17b17a35e4 --- /dev/null +++ b/course/format/templates/local/courseindex/cm.mustache @@ -0,0 +1,64 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_courseformat/local/courseindex/cm + + Displays a course index course-module entry. + + Example context (json): + { + "id": "12", + "name": "Announcements", + "url": "#", + "visible": 1, + "isactive": 1, + "uniqid": "0", + "isactive": 1, + "accessvisible": 1, + } +}} +
  • + {{#url}} + {{#uservisible}} + + {{{name}}} + + {{/uservisible}} + {{^uservisible}} + + {{{name}}} + + {{/uservisible}} + {{/url}} + {{^url}} + + {{{name}}} + + {{/url}} +
  • +{{#js}} +require(['core_courseformat/local/courseindex/cm'], function(component) { + component.init('{{uniqid}}-course-index-cm-{{id}}'); +}); +{{/js}} diff --git a/course/format/templates/local/courseindex/courseindex.mustache b/course/format/templates/local/courseindex/courseindex.mustache new file mode 100644 index 00000000000..44e948c4826 --- /dev/null +++ b/course/format/templates/local/courseindex/courseindex.mustache @@ -0,0 +1,111 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_courseformat/local/courseindex/courseindex + + Displays the course index. + + Example context (json): + { + "editmode": true, + "sections": [ + { + "title": "General", + "id": 42, + "number": 1, + "sectionurl": "#", + "isactive": 1, + "cms": [ + { + "name": "Glossary of characters", + "id": "10", + "url": "#", + "visible": 1, + "isactive": 0, + "uniqid": "0", + "accessvisible": 1 + }, + { + "name": "World Cinema forum", + "id": "11", + "url": "#", + "visible": 1, + "isactive": 0, + "uniqid": "0", + "accessvisible": 1 + }, + { + "name": "Announcements", + "id": "12", + "url": "#", + "visible": 1, + "isactive": 1, + "uniqid": "0", + "accessvisible": 1 + } + ] + }, + { + "title": "City of God or Cidade de Deus", + "id": "43", + "number": "2", + "sectionurl": "#", + "isactive": 0, + "cms": [ + { + "name": "Resources", + "id": "13", + "url": "#", + "visible": 1, + "isactive": 0, + "uniqid": "0", + "accessvisible": 1 + }, + { + "name": "Studying City of God by Stephen Smith Bergman-Messerschmidt", + "id": "14", + "url": "#", + "visible": 1, + "isactive": 0, + "uniqid": "0", + "accessvisible": 1 + }, + { + "name": "Film education study guide", + "id": "15", + "url": "#", + "visible": 1, + "isactive": 0, + "uniqid": "0", + "accessvisible": 1 + } + ] + } + ] + } + +}} + +{{#js}} +require(['core_courseformat/local/courseindex/courseindex'], function(component) { + component.init('{{uniqid}}-course-index'); +}); +{{/js}} diff --git a/course/format/templates/local/courseindex/drawer.mustache b/course/format/templates/local/courseindex/drawer.mustache new file mode 100644 index 00000000000..87efa696951 --- /dev/null +++ b/course/format/templates/local/courseindex/drawer.mustache @@ -0,0 +1,37 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_courseformat/local/courseindex/drawer + + This template renders the course index drawer with the placeholder. + + The code from this file is just an stub as the final code will come from + the new layouts for Moodle 4.0. + + Example context (json): + {} +}} + +{{#js}} +require(['core_courseformat/local/courseindex/drawer'], function(component) { + component.init('courseindex'); +}); +{{/js}} diff --git a/course/format/templates/local/courseindex/placeholders.mustache b/course/format/templates/local/courseindex/placeholders.mustache new file mode 100644 index 00000000000..a2c563d2471 --- /dev/null +++ b/course/format/templates/local/courseindex/placeholders.mustache @@ -0,0 +1,57 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_courseformat/local/courseindex/placeholders + + This template renders the loading placeholders for the course index. + + Example context (json): + {} +}} + +{{#js}} +require(['core_courseformat/local/courseindex/placeholder'], function(component) { + component.init('{{uniqid}}-course-index-placeholder'); +}); +{{/js}} diff --git a/course/format/templates/local/courseindex/section.mustache b/course/format/templates/local/courseindex/section.mustache new file mode 100644 index 00000000000..91899cb16ff --- /dev/null +++ b/course/format/templates/local/courseindex/section.mustache @@ -0,0 +1,102 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_courseformat/local/courseindex/section + + Displays a course index section entry. + + Example context (json): + { + "title": "General", + "id": 23, + "uniqid": "0", + "number": 1, + "sectionurl": "#", + "isactive": 1, + "current": 1, + "visible": 1, + "cms": [ + { + "id": 10, + "name": "Glossary of characters", + "url": "#", + "visible": 1, + "isactive": 0 + }, + { + "id": 11, + "name": "World Cinema forum", + "url": "#", + "visible": 1, + "isactive": 0 + }, + { + "id": 12, + "name": "Announcements", + "url": "#", + "visible": 0, + "isactive": 1 + } + ] + } +}} + diff --git a/course/format/tests/external/get_state_test.php b/course/format/tests/external/get_state_test.php index 9c5bcae3ca3..bcedd162f7f 100644 --- a/course/format/tests/external/get_state_test.php +++ b/course/format/tests/external/get_state_test.php @@ -143,7 +143,7 @@ class get_state_test extends \externallib_advanced_testcase { // Check sections information. foreach ($result->section as $section) { - if (in_array($section->section, $hiddensections)) { + if (in_array($section->number, $hiddensections)) { $this->assertFalse($section->visible); } else { $this->assertTrue($section->visible); @@ -151,8 +151,8 @@ class get_state_test extends \externallib_advanced_testcase { // Check section is defined in course->sectionlist. $this->assertContains($section->id, $result->course->sectionlist); // Check course modules list for this section is the expected. - if (array_key_exists($section->section, $this->sections)) { - $this->assertEquals($this->sections[$section->section], $section->cmlist); + if (array_key_exists($section->number, $this->sections)) { + $this->assertEquals($this->sections[$section->number], $section->cmlist); } } // Check course modules information. diff --git a/course/format/tests/external/update_course_test.php b/course/format/tests/external/update_course_test.php index fa5cd815c27..355e14923ba 100644 --- a/course/format/tests/external/update_course_test.php +++ b/course/format/tests/external/update_course_test.php @@ -111,7 +111,7 @@ class update_course_test extends \externallib_advanced_testcase { 'action' => 'cm_state', 'expected' => [ 'count' => 2, - 'action' => 'update', + 'action' => 'put', 'visible' => 1, ], 'expectexception' => false, @@ -223,14 +223,14 @@ class update_course_test extends \externallib_advanced_testcase { $results = json_decode(update_course::execute('targetsection_test', $course->id, [], $section->id)); $this->assertDebuggingCalled(); $this->assertCount(1, $results); - $update = $this->find_update($results, 'update', 'section', $section->id); + $update = $this->find_update($results, 'put', 'section', $section->id); $this->assertNotEmpty($update); // Execute action with targetcmid. $results = json_decode(update_course::execute('targetcm_test', $course->id, [], null, $activity->cmid)); $this->assertDebuggingCalled(); $this->assertCount(1, $results); - $update = $this->find_update($results, 'update', 'cm', $activity->cmid); + $update = $this->find_update($results, 'put', 'cm', $activity->cmid); $this->assertNotEmpty($update); } } diff --git a/course/format/tests/fixtures/format_theunittest_stateactions.php b/course/format/tests/fixtures/format_theunittest_stateactions.php index f1ad0847bc1..207f83acfb0 100644 --- a/course/format/tests/fixtures/format_theunittest_stateactions.php +++ b/course/format/tests/fixtures/format_theunittest_stateactions.php @@ -85,7 +85,7 @@ class stateactions extends core_actions { ?int $targetcmid = null ): void { - $updates->add_section_update($targetsectionid); + $updates->add_section_put($targetsectionid); } /** @@ -105,7 +105,7 @@ class stateactions extends core_actions { ?int $targetcmid = null ): void { - $updates->add_cm_update($targetcmid); + $updates->add_cm_put($targetcmid); } } diff --git a/course/format/tests/stateactions_test.php b/course/format/tests/stateactions_test.php index ff32dafbe4f..56c1f14c057 100644 --- a/course/format/tests/stateactions_test.php +++ b/course/format/tests/stateactions_test.php @@ -137,7 +137,7 @@ class stateactions_test extends \advanced_testcase { 'cm' => [], 'count' => 0, ], - 'update' => [ + 'put' => [ 'course' => [], 'section' => [], 'cm' => [], @@ -243,12 +243,12 @@ class stateactions_test extends \advanced_testcase { // Validate we have all the expected entries. $expectedtotal = count($expectedresults['course']) + count($expectedresults['section']) + count($expectedresults['cm']); - $this->assertEquals($expectedtotal, $results['update']['count']); + $this->assertEquals($expectedtotal, $results['put']['count']); // Validate course, section and cm. foreach ($expectedresults as $name => $referencekeys) { foreach ($referencekeys as $referencekey) { - $this->assertArrayHasKey($references[$referencekey], $results['update'][$name]); + $this->assertArrayHasKey($references[$referencekey], $results['put'][$name]); } } } diff --git a/course/format/tests/stateupdates_test.php b/course/format/tests/stateupdates_test.php index ebda0542b1a..d3eca691e60 100644 --- a/course/format/tests/stateupdates_test.php +++ b/course/format/tests/stateupdates_test.php @@ -30,14 +30,14 @@ use stdClass; class stateupdates_test extends \advanced_testcase { /** - * Test for add_course_update. + * Test for add_course_put. * - * @dataProvider add_course_update_provider - * @covers ::add_course_update + * @dataProvider add_course_put_provider + * @covers ::add_course_put * * @param string $role the user role in the course */ - public function test_add_course_update(string $role): void { + public function test_add_course_put(string $role): void { global $PAGE; $this->resetAfterTest(); @@ -65,23 +65,23 @@ class stateupdates_test extends \advanced_testcase { $currentstate = new $stateclass($format); $expected = $currentstate->export_for_template($renderer); - $updates->add_course_update(); + $updates->add_course_put(); $updatelist = $updates->jsonSerialize(); $this->assertCount(1, $updatelist); $update = array_pop($updatelist); - $this->assertEquals('update', $update->action); + $this->assertEquals('put', $update->action); $this->assertEquals('course', $update->name); $this->assertEquals($expected, $update->fields); } /** - * Data provider for test_add_course_update. + * Data provider for test_add_course_put. * * @return array testing scenarios */ - public function add_course_update_provider() { + public function add_course_put_provider() { return [ 'Admin role' => [ 'admin', @@ -128,7 +128,7 @@ class stateupdates_test extends \advanced_testcase { * Add track about a section state update. * * @dataProvider add_section_provider - * @covers ::add_course_update + * @covers ::add_course_put * @covers ::add_course_create * @covers ::add_course_delete * @@ -201,7 +201,7 @@ class stateupdates_test extends \advanced_testcase { */ public function add_section_provider(): array { return array_merge( - $this->add_section_provider_helper('update'), + $this->add_section_provider_helper('put'), $this->add_section_provider_helper('create'), $this->add_section_provider_helper('delete'), ); @@ -345,7 +345,7 @@ class stateupdates_test extends \advanced_testcase { */ public function add_cm_provider(): array { return array_merge( - $this->add_cm_provider_helper('update'), + $this->add_cm_provider_helper('put'), $this->add_cm_provider_helper('create'), $this->add_cm_provider_helper('delete'), ); diff --git a/course/format/topics/lib.php b/course/format/topics/lib.php index 3cc40e20755..b0e84ef9659 100644 --- a/course/format/topics/lib.php +++ b/course/format/topics/lib.php @@ -46,6 +46,10 @@ class format_topics extends core_courseformat\base { return true; } + public function uses_course_index() { + return true; + } + /** * Returns the display name of the given section that the course prefers. * diff --git a/course/format/upgrade.txt b/course/format/upgrade.txt index 948497e61e8..bf173ba590a 100644 --- a/course/format/upgrade.txt +++ b/course/format/upgrade.txt @@ -2,6 +2,9 @@ This files describes API changes for course formats Overview of this plugin type at http://docs.moodle.org/dev/Course_formats +=== 4.0 === +* New core_courseformat\uses_course_index() to define whether the course format uses course index or not. + === 3.10 === * Added the missing callback supports_ajax() to format_social. diff --git a/course/format/weeks/lib.php b/course/format/weeks/lib.php index ce9b3265018..5526b01f7b0 100644 --- a/course/format/weeks/lib.php +++ b/course/format/weeks/lib.php @@ -45,6 +45,10 @@ class format_weeks extends core_courseformat\base { return true; } + public function uses_course_index() { + return true; + } + /** * Generate the title for this section page * @return string the page title diff --git a/course/lib.php b/course/lib.php index fbbf4a8d22a..b04d05c959a 100644 --- a/course/lib.php +++ b/course/lib.php @@ -24,6 +24,8 @@ defined('MOODLE_INTERNAL') || die; +use core_courseformat\base as course_format; + require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->libdir.'/filelib.php'); require_once($CFG->libdir.'/datalib.php'); @@ -3197,13 +3199,6 @@ function course_ajax_enabled($course) { function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) { global $CFG, $PAGE, $SITE; - // All the new editor elements will be loaded after the course is presented and - // the initial course state will be generated using core_courseformat_get_state webservice. - if ($SITE->id !== $course->id) { - $PAGE->requires->js_call_amd('core_courseformat/courseeditor', 'getCourseEditor', [$course->id]); - } - // TODO: as part of MDL-70907, add a way to indicate the plugin needs the legacy libraries (and get a deprecation message). - // Ensure that ajax should be included if (!course_ajax_enabled($course)) { return false; @@ -3295,6 +3290,29 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules = return true; } +/** + * Include and configure the course editor modules. + * + * @param course_format $format the course format instance. + */ +function include_course_editor(course_format $format) { + global $PAGE, $SITE; + + $course = $format->get_course(); + + if ($SITE->id === $course->id) { + return; + } + + // Edition mode and some format specs must be passed to the init method. + $setup = (object)[ + 'editing' => $format->show_editor(), + ]; + // All the new editor elements will be loaded after the course is presented and + // the initial course state will be generated using core_course_get_state webservice. + $PAGE->requires->js_call_amd('core_courseformat/courseeditor', 'setViewFormat', [$course->id, $setup]); +} + /** * Returns the sorted list of available course formats, filtered by enabled if necessary * @@ -3835,6 +3853,24 @@ function core_course_core_calendar_get_valid_event_timestart_range(\calendar_eve return [$mindate, $maxdate]; } +/** + * Render the main course drawer to be included in the left part of the page. + * + * @return string HTML + */ +function core_course_drawer(): string { + global $PAGE; + // Only course and mod pages are able to render course index. + if (!preg_match('/^(mod|course).*/', $PAGE->pagetype)) { + return ''; + } + + $format = course_get_format($PAGE->course); + $renderer = $format->get_renderer($PAGE); + $placeholder = $renderer->course_index_drawer($format); + return $placeholder; +} + /** * Returns course modules tagged with a specified tag ready for output on tag/index.php page * diff --git a/course/renderer.php b/course/renderer.php index 705e59a37c1..8f325e3d91c 100644 --- a/course/renderer.php +++ b/course/renderer.php @@ -560,12 +560,10 @@ class core_course_renderer extends plugin_renderer_base { * Checks if course module has any conditions that may make it unavailable for * all or some of the students * - * This function is internal and is only used to create CSS classes for the module name/text - * * @param cm_info $mod * @return bool */ - protected function is_cm_conditionally_hidden(cm_info $mod) { + public function is_cm_conditionally_hidden(cm_info $mod) { global $CFG; $conditionalhidden = false; if (!empty($CFG->enableavailability)) { diff --git a/course/upgrade.txt b/course/upgrade.txt index 836bf135f86..ca377ada423 100644 --- a/course/upgrade.txt +++ b/course/upgrade.txt @@ -52,6 +52,8 @@ course formats don't have their own renderer. * New external function core_course_update_course runs given action to edit course status. * The `\core\event\course_category_deleted` event is now created with a snapshot of the category record being deleted, available inside event observers via `$event->get_record_snapshot` +* New include_course_editor() function to include and configure course editor modules. +* New core_course_drawer() function to render the message drawer in the top of the body of each page. === 3.11 === * A new callback xxx_coursemodule_definition_after_data that allows plugins to extend activity forms after the data is set. diff --git a/lang/en/courseformat.php b/lang/en/courseformat.php index ea5725d49e5..7d5d9bfaa6b 100644 --- a/lang/en/courseformat.php +++ b/lang/en/courseformat.php @@ -22,4 +22,5 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['courseindex'] = 'Course index'; $string['privacy:metadata'] = 'The course format subsystem does not store any personal data.'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 318803f28d5..bc1d1955d50 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -356,7 +356,6 @@ $string['coursehelpnewsitemsnumber'] = 'Number of recent announcements appearing $string['coursehelpnumberweeks'] = 'Number of sections in the course (applies to certain course formats only).'; $string['coursehelpshowgrades'] = 'Enable the display of the gradebook. It does not prevent grades from being displayed within the individual activities.'; $string['coursehidden'] = 'This course is currently unavailable to students'; -$string['courseindex'] = 'Course index'; $string['courseoverviewfiles'] = 'Course image'; $string['courseoverviewfilesext'] = 'Course image file extensions'; $string['courseoverviewfileslimit'] = 'Course image files limit'; diff --git a/theme/boost/layout/drawers.php b/theme/boost/layout/drawers.php index 16607635b78..e2d08c68ab9 100644 --- a/theme/boost/layout/drawers.php +++ b/theme/boost/layout/drawers.php @@ -25,6 +25,7 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/behat/lib.php'); +require_once($CFG->dirroot . '/course/lib.php'); user_preference_allow_ajax_update('drawer-open-nav', PARAM_ALPHA); user_preference_allow_ajax_update('drawer-open-index', PARAM_BOOL); @@ -57,7 +58,7 @@ $hasblocks = strpos($blockshtml, 'data-block=') !== false; if (!$hasblocks) { $blockdraweropen = false; } -$courseindex = false; +$courseindex = core_course_drawer(); if (!$courseindex) { $courseindexopen = false; } diff --git a/theme/boost/scss/moodle.scss b/theme/boost/scss/moodle.scss index 9eb49651a86..38a65025af4 100644 --- a/theme/boost/scss/moodle.scss +++ b/theme/boost/scss/moodle.scss @@ -44,3 +44,4 @@ $breadcrumb-divider-rtl: "◀" !default; @import "moodle/toasts"; @import "moodle/navbar"; @import "moodle/reportbuilder"; +@import "moodle/courseindex"; diff --git a/theme/boost/scss/moodle/courseindex.scss b/theme/boost/scss/moodle/courseindex.scss new file mode 100644 index 00000000000..33e32e764c8 --- /dev/null +++ b/theme/boost/scss/moodle/courseindex.scss @@ -0,0 +1,84 @@ +$courseindex-link-color: $list-group-action-color !default; +$courseindex-link-hover-color: $list-group-action-hover-color !default; +$courseindex-item-hover-bg: theme-color-level('info', -11) !default; +$courseindex-item-hover-border: theme-color-level('info', -9) !default; +$courseindex-item-active-border: $gray-300 !default; +$courseindex-item-active-bg: $gray-100 !default; +$courseindex-item-padding-y: 0.5rem; +$courseindex-item-padding-x: 1rem; +$courseindex-item-radius: $border-radius !default; +$courseindex-item-current: $primary !default; + +.courseindex { + .courseindex-heading { + padding: $courseindex-item-padding-y $courseindex-item-padding-x; + } + .courseindex-item { + padding: $courseindex-item-padding-y $courseindex-item-padding-x; + border: $border-width solid transparent; + @include border-radius($courseindex-item-radius); + + .courseindex-link { + color: $courseindex-link-color; + @include hover-focus() { + color: $courseindex-link-hover-color; + text-decoration: none; + } + } + + .courseindex-name { + font-style: italic; + } + + @include hover-focus() { + background-color: $courseindex-item-hover-bg; + border-color: $courseindex-item-hover-border; + } + + &.active { + background-color: $courseindex-item-active-bg; + border-color: $courseindex-item-active-border; + @include hover-focus() { + background-color: $courseindex-item-hover-bg; + border-color: $courseindex-item-hover-border; + } + } + + &.dimmed { + opacity: .7; + font-style: italic; + } + } + + .courseindex-sectioncontent { + .courseindex-item { + padding-left: calc(#{$courseindex-item-padding-x} * 2 + 0.5rem); + } + } + + .courseindex-section.current { + border-left: solid 3px $courseindex-item-current; + } + + .d-flex-noedit { + display: none; + } + + &.editing { + .d-flex-noedit { + display: flex; + } + } + + .media-list { + .rounded-circle { + height: 1rem; + width: 1rem; + } + + .w-100 { + height: 1rem; + margin: 0.5rem 0; + } + } +} diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index d4d8668592d..ccea513da42 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -20036,6 +20036,52 @@ div.editor_atto_toolbar button .icon { text-overflow: clip; word-break: break-all; } +.courseindex .courseindex-heading { + padding: 0.5rem 1rem; } + +.courseindex .courseindex-item { + padding: 0.5rem 1rem; + border: 1px solid transparent; } + .courseindex .courseindex-item .courseindex-link { + color: #495057; } + .courseindex .courseindex-item .courseindex-link:hover, .courseindex .courseindex-item .courseindex-link:focus { + color: #495057; + text-decoration: none; } + .courseindex .courseindex-item .courseindex-name { + font-style: italic; } + .courseindex .courseindex-item:hover, .courseindex .courseindex-item:focus { + background-color: #e0f0f2; + border-color: #b8dce2; } + .courseindex .courseindex-item.active { + background-color: #f8f9fa; + border-color: #dee2e6; } + .courseindex .courseindex-item.active:hover, .courseindex .courseindex-item.active:focus { + background-color: #e0f0f2; + border-color: #b8dce2; } + .courseindex .courseindex-item.dimmed { + opacity: .7; + font-style: italic; } + +.courseindex .courseindex-sectioncontent .courseindex-item { + padding-left: calc(1rem * 2 + 0.5rem); } + +.courseindex .courseindex-section.current { + border-left: solid 3px #0f6fc5; } + +.courseindex .d-flex-noedit { + display: none; } + +.courseindex.editing .d-flex-noedit { + display: flex; } + +.courseindex .media-list .rounded-circle { + height: 1rem; + width: 1rem; } + +.courseindex .media-list .w-100 { + height: 1rem; + margin: 0.5rem 0; } + body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } diff --git a/theme/boost/templates/drawers.mustache b/theme/boost/templates/drawers.mustache index 2da234ed00f..63bad60ad06 100644 --- a/theme/boost/templates/drawers.mustache +++ b/theme/boost/templates/drawers.mustache @@ -63,7 +63,7 @@ {{$id}}theme_boost-drawers-courseindex{{/id}} {{$drawerclasses}}drawer drawer-left {{#courseindexopen}}show{{/courseindexopen}}{{/drawerclasses}} {{$drawerheading}} - {{#str}} courseindex {{/str}} + {{#str}}courseindex, core_courseformat{{/str}} {{/drawerheading}} {{$drawercontent}} {{{courseindex}}} diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 958e79e1b87..3ce4c37ad0b 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -20227,6 +20227,53 @@ div.editor_atto_toolbar button .icon { text-overflow: clip; word-break: break-all; } +.courseindex .courseindex-heading { + padding: 0.5rem 1rem; } + +.courseindex .courseindex-item { + padding: 0.5rem 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; } + .courseindex .courseindex-item .courseindex-link { + color: #495057; } + .courseindex .courseindex-item .courseindex-link:hover, .courseindex .courseindex-item .courseindex-link:focus { + color: #495057; + text-decoration: none; } + .courseindex .courseindex-item .courseindex-name { + font-style: italic; } + .courseindex .courseindex-item:hover, .courseindex .courseindex-item:focus { + background-color: #e0f0f2; + border-color: #b8dce2; } + .courseindex .courseindex-item.active { + background-color: #f8f9fa; + border-color: #dee2e6; } + .courseindex .courseindex-item.active:hover, .courseindex .courseindex-item.active:focus { + background-color: #e0f0f2; + border-color: #b8dce2; } + .courseindex .courseindex-item.dimmed { + opacity: .7; + font-style: italic; } + +.courseindex .courseindex-sectioncontent .courseindex-item { + padding-left: calc(1rem * 2 + 0.5rem); } + +.courseindex .courseindex-section.current { + border-left: solid 3px #0f6fc5; } + +.courseindex .d-flex-noedit { + display: none; } + +.courseindex.editing .d-flex-noedit { + display: flex; } + +.courseindex .media-list .rounded-circle { + height: 1rem; + width: 1rem; } + +.courseindex .media-list .w-100 { + height: 1rem; + margin: 0.5rem 0; } + body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }