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.
This commit is contained in:
Ferran Recio
2021-08-20 16:49:06 +02:00
committed by Amaia Anabitarte
parent 804e138cb2
commit 830c3eb907
54 changed files with 1319 additions and 110 deletions
+1 -1
View File
@@ -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
@@ -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 <http://www.gnu.org/licenses/>.\n\n/**\n * Generic reactive module used in the course editor.\n *\n * @module core_courseformat/courseeditor\n * @copyright 2021 Ferran Recio <[email protected]>\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"}
{"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 <http://www.gnu.org/licenses/>.\n\n/**\n * Generic reactive module used in the course editor.\n *\n * @module core_courseformat/courseeditor\n * @copyright 2021 Ferran Recio <[email protected]>\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"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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<arguments.length;c++){e=null!=arguments[c]?arguments[c]:{};if(c%2){b(Object(e),!0).forEach(function(b){d(a,b,e[b])})}else if(Object.getOwnPropertyDescriptors){Object.defineProperties(a,Object.getOwnPropertyDescriptors(e))}else{b(Object(e)).forEach(function(b){Object.defineProperty(a,b,Object.getOwnPropertyDescriptor(e,b))})}}return a}function d(a,b,c){if(b in a){Object.defineProperty(a,b,{value:c,enumerable:!0,configurable:!0,writable:!0})}else{a[b]=c}return 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<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function g(a,b,c){if(b)f(a.prototype,b);if(c)f(a,c);return a}var h=function(){function a(b){e(this,a);this.reactive=b}g(a,[{key:"course",value:function course(a){var b,c=this,d={sections:[],editmode:this.reactive.isEditing},e=null!==(b=a.course.sectionlist)&&void 0!==b?b:[];e.forEach(function(b){var e,f=null!==(e=a.section.get(b))&&void 0!==e?e:{},g=c.section(a,f);d.sections.push(g)});d.hassections=0!=d.sections.length;return d}},{key:"section",value:function(a,b){var d,e=this,f=c({},b,{cms:[],isactive:!1}),g=null!==(d=b.cmlist)&&void 0!==d?d:[];g.forEach(function(b){var c=a.cm.get(b),d=e.cm(a,c);f.cms.push(d)});f.hascms=0!=f.cms.length;return f}},{key:"cm",value:function(a,b){var d=c({},b,{isactive:!1});return d}}]);return a}();a.default=h;return a.default});
//# sourceMappingURL=exporter.min.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/local/courseeditor/exporter.js"],"names":["reactive","state","data","sections","editmode","isEditing","sectionlist","course","forEach","sectionid","sectioninfo","section","get","push","hassections","length","cms","isactive","cmlist","cmid","cminfo","cm","hascms"],"mappings":"4rCA+BI,WAAYA,CAAZ,CAAsB,WAClB,KAAKA,QAAL,CAAgBA,CACnB,C,yCAQMC,C,CAAO,cAEJC,CAAI,CAAG,CACTC,QAAQ,CAAE,EADD,CAETC,QAAQ,CAAE,KAAKJ,QAAL,CAAcK,SAFf,CAFH,CAMJC,CAAW,WAAGL,CAAK,CAACM,MAAN,CAAaD,WAAhB,gBAA+B,EANtC,CAOVA,CAAW,CAACE,OAAZ,CAAoB,SAAAC,CAAS,CAAI,OACvBC,CAAW,WAAGT,CAAK,CAACU,OAAN,CAAcC,GAAd,CAAkBH,CAAlB,CAAH,gBAAmC,EADvB,CAEvBE,CAAO,CAAG,CAAI,CAACA,OAAL,CAAaV,CAAb,CAAoBS,CAApB,CAFa,CAG7BR,CAAI,CAACC,QAAL,CAAcU,IAAd,CAAmBF,CAAnB,CACH,CAJD,EAKAT,CAAI,CAACY,WAAL,CAA4C,CAAxB,EAAAZ,CAAI,CAACC,QAAL,CAAcY,MAAlC,CAEA,MAAOb,CAAAA,CACV,C,gCASOD,C,CAAOS,C,CAAa,cAClBC,CAAO,MACND,CADM,EAETM,GAAG,CAAE,EAFI,CAGTC,QAAQ,GAHC,EADW,CAMlBC,CAAM,WAAGR,CAAW,CAACQ,MAAf,gBAAyB,EANb,CAOxBA,CAAM,CAACV,OAAP,CAAe,SAAAW,CAAI,CAAI,IACbC,CAAAA,CAAM,CAAGnB,CAAK,CAACoB,EAAN,CAAST,GAAT,CAAaO,CAAb,CADI,CAEbE,CAAE,CAAG,CAAI,CAACA,EAAL,CAAQpB,CAAR,CAAemB,CAAf,CAFQ,CAGnBT,CAAO,CAACK,GAAR,CAAYH,IAAZ,CAAiBQ,CAAjB,CACH,CAJD,EAKAV,CAAO,CAACW,MAAR,CAAwC,CAAtB,EAAAX,CAAO,CAACK,GAAR,CAAYD,MAA9B,CAEA,MAAOJ,CAAAA,CACV,C,2BASEV,C,CAAOmB,C,CAAQ,CACd,GAAMC,CAAAA,CAAE,MACDD,CADC,EAEJH,QAAQ,GAFJ,EAAR,CAIA,MAAOI,CAAAA,CACV,C","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 <http://www.gnu.org/licenses/>.\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 <[email protected]>\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"}
@@ -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<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function g(a,b,c){if(b)f(a.prototype,b);if(c)f(a,c);return a}var h=function(){function a(){e(this,a)}g(a,[{key:"_callEditWebservice",value:function(){var a=d(regeneratorRuntime.mark(function a(c,d,e){var f;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:a.next=2;return b.default.call([{methodname:"core_courseformat_update_course",args:{action:c,courseid:d,ids:e}}])[0];case 2:f=a.sent;return a.abrupt("return",JSON.parse(f));case 4:case"end":return a.stop();}}},a)}));return function _callEditWebservice(){return a.apply(this,arguments)}}()},{key:"cmState",value:function(){var a=d(regeneratorRuntime.mark(function a(b,c){var d,e;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=b.state;a.next=3;return this._callEditWebservice("cm_state",d.course.id,c);case 3:e=a.sent;b.setReadOnly(!1);this._processUpdates(b,e);case 6:case"end":return a.stop();}}},a,this)}));return function cmState(){return a.apply(this,arguments)}}()},{key:"sectionState",value:function(){var a=d(regeneratorRuntime.mark(function a(b,c){var d,e;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=b.state;a.next=3;return this._callEditWebservice("section_state",d.course.id,c);case 3:e=a.sent;this._processUpdates(b,e);case 5:case"end":return a.stop();}}},a,this)}));return function sectionState(){return a.apply(this,arguments)}}()},{key:"_processUpdates",value:function _processUpdates(a,b){var c=a.state;a.setReadOnly(!1);b.forEach(function(b){if(b.name===void 0){throw Error("Missing state update name")}var d=c[b.name];if(d instanceof Map){d=c[b.name].get(b.fields.id)}if(!d){b.action="create"}a.processUpdate(b.name,b.action,b.fields)});a.setReadOnly(!0)}}]);return a}();a.default=h;return a.default});
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<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function g(a,b,c){if(b)f(a.prototype,b);if(c)f(a,c);return a}var h=function(){function a(){e(this,a)}g(a,[{key:"_callEditWebservice",value:function(){var a=d(regeneratorRuntime.mark(function a(c,d,e){var f;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:a.next=2;return b.default.call([{methodname:"core_courseformat_update_course",args:{action:c,courseid:d,ids:e}}])[0];case 2:f=a.sent;return a.abrupt("return",JSON.parse(f));case 4:case"end":return a.stop();}}},a)}));return function _callEditWebservice(){return a.apply(this,arguments)}}()},{key:"cmState",value:function(){var a=d(regeneratorRuntime.mark(function a(b,c){var d,e;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=b.get("course");a.next=3;return this._callEditWebservice("cm_state",d.id,c);case 3:e=a.sent;b.processUpdates(e);case 5:case"end":return a.stop();}}},a,this)}));return function cmState(){return a.apply(this,arguments)}}()},{key:"sectionState",value:function(){var a=d(regeneratorRuntime.mark(function a(b,c){var d,e;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=b.get("course");a.next=3;return this._callEditWebservice("section_state",d.id,c);case 3:e=a.sent;b.processUpdates(e);case 5:case"end":return a.stop();}}},a,this)}));return function sectionState(){return a.apply(this,arguments)}}()},{key:"courseState",value:function(){var a=d(regeneratorRuntime.mark(function a(b){var c,d;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:c=b.get("course");a.next=3;return this._callEditWebservice("course_state",c.id);case 3:d=a.sent;b.processUpdates(d);case 5:case"end":return a.stop();}}},a,this)}));return function courseState(){return a.apply(this,arguments)}}()}]);return a}();a.default=h;return a.default});
//# sourceMappingURL=mutations.min.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -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<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function f(a,b,c){if(b)e(a.prototype,b);if(c)e(a,c);return a}function g(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)h(a,b)}function h(a,b){h=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return h(a,b)}function i(a){return function(){var b=m(a),c;if(l()){var d=m(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return j(this,c)}}function j(a,b){if(b&&("object"===c(b)||"function"==typeof b)){return b}return k(a)}function k(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function l(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function m(a){m=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return m(a)}var n=function(a){g(b,a);var c=i(b);function b(){d(this,b);return c.apply(this,arguments)}f(b,[{key:"create",value:function create(){this.name="courseindex_cm";this.selectors={};this.id=this.element.dataset.id}},{key:"stateReady",value:function stateReady(){}},{key:"getWatchers",value:function getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.remove}]}}],[{key:"init",value:function init(a,c){return new b({element:document.getElementById(a),selectors:c})}}]);return b}(b.BaseComponent);a.default=n;return a.default});
//# sourceMappingURL=cm.min.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/local/courseindex/cm.js"],"names":["Component","name","selectors","id","element","dataset","watch","handler","remove","target","document","getElementById","BaseComponent"],"mappings":"yyDA4BqBA,CAAAA,C,+HAKR,CAEL,KAAKC,IAAL,CAAY,gBAAZ,CAEA,KAAKC,SAAL,CAAiB,EAAjB,CAGA,KAAKC,EAAL,CAAU,KAAKC,OAAL,CAAaC,OAAb,CAAqBF,EAClC,C,+CAmBY,CAEZ,C,iDAEa,CACV,MAAO,CACH,CAACG,KAAK,cAAQ,KAAKH,EAAb,aAAN,CAAkCI,OAAO,CAAE,KAAKC,MAAhD,CADG,CAGV,C,oCAlBWC,C,CAAQP,C,CAAW,CAC3B,MAAO,IAAIF,CAAAA,CAAJ,CAAc,CACjBI,OAAO,CAAEM,QAAQ,CAACC,cAAT,CAAwBF,CAAxB,CADQ,CAEjBP,SAAS,CAATA,CAFiB,CAAd,CAIV,C,cA3BkCU,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 <http://www.gnu.org/licenses/>.\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 <[email protected]>\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"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function g(a,b,c){if(b)f(a.prototype,b);if(c)f(a,c);return a}function h(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)i(a,b)}function i(a,b){i=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return i(a,b)}function j(a){return function(){var b=n(a),c;if(m()){var d=n(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return k(this,c)}}function k(a,b){if(b&&("object"===d(b)||"function"==typeof b)){return b}return l(a)}function l(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function m(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function n(a){n=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return n(a)}var o=function(a){h(b,a);var d=j(b);function b(){e(this,b);return d.apply(this,arguments)}g(b,[{key:"create",value:function create(){this.name="courseindex-drawer"}}],[{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=o;return a.default});
//# sourceMappingURL=drawer.min.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/local/courseindex/drawer.js"],"names":["Component","name","target","selectors","element","document","getElementById","reactive","BaseComponent"],"mappings":"g1DA8BqBA,CAAAA,C,+HAKR,CAEL,KAAKC,IAAL,CAAY,oBACf,C,oCASWC,C,CAAQC,C,CAAW,CAC3B,MAAO,IAAIH,CAAAA,CAAJ,CAAc,CACjBI,OAAO,CAAEC,QAAQ,CAACC,cAAT,CAAwBJ,CAAxB,CADQ,CAEjBK,QAAQ,CAAE,8BAFO,CAGjBJ,SAAS,CAATA,CAHiB,CAAd,CAKV,C,cAvBkCK,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 <http://www.gnu.org/licenses/>.\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 <[email protected]>\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"}
@@ -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<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function j(a,b,c){if(b)i(a.prototype,b);if(c)i(a,c);return a}function k(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)l(a,b)}function l(a,b){l=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return l(a,b)}function m(a){return function(){var b=q(a),c;if(p()){var d=q(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return n(this,c)}}function n(a,b){if(b&&("object"===e(b)||"function"==typeof b)){return b}return o(a)}function o(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function p(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function q(a){q=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return q(a)}var r=function(a){k(b,a);var e=m(b);function b(){h(this,b);return e.apply(this,arguments)}j(b,[{key:"stateReady",value:function(){var a=g(regeneratorRuntime.mark(function a(b){var d,e,f,g,h;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=this.reactive.getExporter();e=d.course(b);a.prev=2;a.next=5;return c.default.renderForPromise("core_courseformat/local/courseindex/courseindex",e);case 5:f=a.sent;g=f.html;h=f.js;c.default.replaceNode(this.element,g,h);a.next=14;break;case 11:a.prev=11;a.t0=a["catch"](2);throw a.t0;case 14:case"end":return a.stop();}}},a,this,[[2,11]])}));return function stateReady(){return a.apply(this,arguments)}}()}],[{key:"init",value:function init(a,c){return new b({element:document.getElementById(a),reactive:(0,d.getCurrentCourseEditor)(),selectors:c})}}]);return b}(b.BaseComponent);a.default=r;return a.default});
//# sourceMappingURL=placeholder.min.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/local/courseindex/placeholder.js"],"names":["Component","state","exporter","reactive","getExporter","data","course","Templates","renderForPromise","html","js","replaceNode","element","target","selectors","document","getElementById","BaseComponent"],"mappings":"yOAyBA,uD,87DAGqBA,CAAAA,C,wKAwBAC,C,iGAEPC,C,CAAW,KAAKC,QAAL,CAAcC,WAAd,E,CACXC,C,CAAOH,CAAQ,CAACI,MAAT,CAAgBL,CAAhB,C,yBAIgBM,WAAUC,gBAAV,CACrB,iDADqB,CAErBH,CAFqB,C,iBAAlBI,C,GAAAA,I,CAAMC,C,GAAAA,E,CAIbH,UAAUI,WAAV,CAAsB,KAAKC,OAA3B,CAAoCH,CAApC,CAA0CC,CAA1C,E,6NA1BIG,C,CAAQC,C,CAAW,CAC3B,MAAO,IAAId,CAAAA,CAAJ,CAAc,CACjBY,OAAO,CAAEG,QAAQ,CAACC,cAAT,CAAwBH,CAAxB,CADQ,CAEjBV,QAAQ,CAAE,8BAFO,CAGjBW,SAAS,CAATA,CAHiB,CAAd,CAKV,C,cAfkCG,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 <http://www.gnu.org/licenses/>.\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 <[email protected]>\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"}
+12
View File
@@ -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.
*
@@ -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.
*
@@ -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 <http://www.gnu.org/licenses/>.
/**
* 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 <[email protected]>
* @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;
}
}
@@ -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);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
/**
* 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 <[email protected]>
* @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},
];
}
}
@@ -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 <http://www.gnu.org/licenses/>.
/**
* Course index main component.
*
* @module core_courseformat/local/courseindex/courseindex
* @class core_courseformat/local/courseindex/courseindex
* @copyright 2021 Ferran Recio <[email protected]>
* @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 = '&nbsp;';
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];
}
}
@@ -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 <http://www.gnu.org/licenses/>.
/**
* 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 <[email protected]>
* @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,
});
}
}
@@ -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 <http://www.gnu.org/licenses/>.
/**
* Course index placeholder replacer.
*
* @module core_courseformat/local/courseindex/placeholder
* @class core_courseformat/local/courseindex/placeholder
* @copyright 2021 Ferran Recio <[email protected]>
* @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;
}
}
}
+12
View File
@@ -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
*
@@ -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);
}
@@ -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])) {
@@ -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
*
+5 -5
View File
@@ -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();
+17 -17
View File
@@ -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);
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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,
}
}}
<li class="courseindex-item
{{#isactive}}active{{/isactive}}
{{^accessvisible}}dimmed{{/accessvisible}}
{{#url}} d-flex {{/url}} {{^url}} d-flex-noedit {{/url}}"
id="{{uniqid}}-course-index-cm-{{id}}"
data-for="cm"
data-id="{{id}}"
>
{{#url}}
{{#uservisible}}
<a class="courseindex-link text-truncate" href="{{{url}}}" data-for="cm_name">
{{{name}}}
</a>
{{/uservisible}}
{{^uservisible}}
<span class="courseindex-name text-truncate" data-for="cm_name">
{{{name}}}
</span>
{{/uservisible}}
{{/url}}
{{^url}}
<span class="courseindex-name text-truncate" data-for="cm_name">
{{{name}}}
</span>
{{/url}}
</li>
{{#js}}
require(['core_courseformat/local/courseindex/cm'], function(component) {
component.init('{{uniqid}}-course-index-cm-{{id}}');
});
{{/js}}
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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
}
]
}
]
}
}}
<nav id="{{uniqid}}-course-index" class="courseindex {{#editmode}} editing {{/editmode}}">
{{#sections}}
{{> core_courseformat/local/courseindex/section }}
{{/sections}}
</nav>
{{#js}}
require(['core_courseformat/local/courseindex/courseindex'], function(component) {
component.init('{{uniqid}}-course-index');
});
{{/js}}
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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):
{}
}}
<nav id="courseindex" class="courseindex">
<div id="courseindex-content">
{{> core_courseformat/local/courseindex/placeholders }}
</div>
</nav>
{{#js}}
require(['core_courseformat/local/courseindex/drawer'], function(component) {
component.init('courseindex');
});
{{/js}}
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_courseformat/local/courseindex/placeholders
This template renders the loading placeholders for the course index.
Example context (json):
{}
}}
<div data-region="loading-placeholder-content" aria-hidden="true" id="{{uniqid}}-course-index-placeholder">
<ul class="media-list">
<li class="media">
<div class="media-body col-md-6 p-0 d-flex align-items-center">
<div class="bg-pulse-grey rounded-circle mr-2"></div>
<div class="bg-pulse-grey w-100"></div>
</div>
</li>
<li class="media">
<div class="media-body col-md-6 p-0 d-flex align-items-center">
<div class="bg-pulse-grey rounded-circle mr-2"></div>
<div class="bg-pulse-grey w-100"></div>
</div>
</li>
<li class="media">
<div class="media-body col-md-6 p-0 d-flex align-items-center">
<div class="bg-pulse-grey rounded-circle mr-2"></div>
<div class="bg-pulse-grey w-100"></div>
</div>
</li>
<li class="media">
<div class="media-body col-md-6 p-0 d-flex align-items-center">
<div class="bg-pulse-grey rounded-circle mr-2"></div>
<div class="bg-pulse-grey w-100"></div>
</div>
</li>
</ul>
</div>
{{#js}}
require(['core_courseformat/local/courseindex/placeholder'], function(component) {
component.init('{{uniqid}}-course-index-placeholder');
});
{{/js}}
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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
}
]
}
}}
<div
class="courseindex-section {{#current}}current{{/current}}"
id="{{uniqid}}-course-index-section-{{id}}"
data-for="section"
data-id="{{id}}"
data-number="{{number}}"
>
<div class="courseindex-item d-flex {{^visible}}dimmed{{/visible}} courseindex-section-title"
id="courseindexsection{{number}}"
data-for="section_item"
>
<a data-toggle="collapse"
href="#courseindexcollapse{{number}}"
aria-expanded="{{#isactive}}true{{/isactive}}{{^isactive}}false{{/isactive}}"
aria-controls="courseindexcollapse{{number}}"
class="courseindex-link icons-collapse-expand {{^isactive}}collapsed{{/isactive}}"
>
<span class="collapsed-icon icon-no-margin mr-1"
data-toggle="tooltip" title="{{#str}} expand, core {{/str}}">
{{#pix}} t/collapsed, core {{/pix}}
<span class="sr-only">{{#str}} expand, core {{/str}}</span>
</span>
<span class="expanded-icon icon-no-margin mr-1"
data-toggle="tooltip" title="{{#str}} collapse, core {{/str}}">
{{#pix}} t/expanded, core {{/pix}}
<span class="sr-only">{{#str}} collapse, core {{/str}}</span>
</span>
</a>
<a href="{{{sectionurl}}}"
class="courseindex-link text-truncate"
data-action="togglecourseindexsection"
data-for="section_title"
>
{{{title}}}
</a>
</div>
<div id="courseindexcollapse{{number}}"
class="courseindex-item-content collapse {{#isactive}}show{{/isactive}}"
aria-labelledby="courseindexsection{{number}}">
<ul class="courseindex-sectioncontent unlist" data-for="cmlist" data-id="{{id}}">
{{#cms}}
{{> core_courseformat/local/courseindex/cm }}
{{/cms}}
</ul>
</div>
</div>
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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);
}
}
@@ -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);
}
}
+3 -3
View File
@@ -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]);
}
}
}
+11 -11
View File
@@ -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'),
);
+4
View File
@@ -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.
*
+3
View File
@@ -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.
+4
View File
@@ -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
+43 -7
View File
@@ -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
*
+1 -3
View File
@@ -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)) {
+2
View File
@@ -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.
+1
View File
@@ -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.';
-1
View File
@@ -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';
+2 -1
View File
@@ -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;
}
+1
View File
@@ -44,3 +44,4 @@ $breadcrumb-divider-rtl: "◀" !default;
@import "moodle/toasts";
@import "moodle/navbar";
@import "moodle/reportbuilder";
@import "moodle/courseindex";
+84
View File
@@ -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;
}
}
}
+46
View File
@@ -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; }
+1 -1
View File
@@ -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}}}
+47
View File
@@ -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; }