From 7f750dc01cab2d17cd7a3302140db59d96ae6b84 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Mon, 21 Jun 2021 15:47:09 +0200 Subject: [PATCH] MDL-71228 course: course index drag and drop --- .../format/amd/build/courseeditor.min.js.map | 2 +- .../local/courseeditor/courseeditor.min.js | 2 +- .../courseeditor/courseeditor.min.js.map | 2 +- .../build/local/courseeditor/dndcmitem.min.js | 2 + .../local/courseeditor/dndcmitem.min.js.map | 1 + .../local/courseeditor/dndsection.min.js | 2 + .../local/courseeditor/dndsection.min.js.map | 1 + .../local/courseeditor/dndsectionitem.min.js | 2 + .../courseeditor/dndsectionitem.min.js.map | 1 + .../build/local/courseeditor/exporter.min.js | 2 +- .../local/courseeditor/exporter.min.js.map | 2 +- .../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 | 2 +- .../build/local/courseindex/section.min.js | 2 + .../local/courseindex/section.min.js.map | 1 + .../local/courseindex/sectiontitle.min.js | 2 + .../local/courseindex/sectiontitle.min.js.map | 1 + course/format/amd/src/courseeditor.js | 1 + .../src/local/courseeditor/courseeditor.js | 12 + .../amd/src/local/courseeditor/dndcmitem.js | 113 +++++ .../amd/src/local/courseeditor/dndsection.js | 146 ++++++ .../src/local/courseeditor/dndsectionitem.js | 129 +++++ .../amd/src/local/courseeditor/exporter.js | 55 +++ .../amd/src/local/courseeditor/mutations.js | 61 ++- course/format/amd/src/local/courseindex/cm.js | 14 +- .../amd/src/local/courseindex/section.js | 86 ++++ .../amd/src/local/courseindex/sectiontitle.js | 73 +++ course/format/classes/base.php | 15 + .../classes/output/local/state/section.php | 1 + course/format/classes/stateactions.php | 157 ++++++ .../templates/local/courseindex/cm.mustache | 1 + .../local/courseindex/section.mustache | 6 + course/format/topics/lib.php | 4 + course/format/upgrade.txt | 3 +- course/format/weeks/lib.php | 4 + course/lib.php | 1 + lib/amd/build/local/reactive/dragdrop.min.js | 2 + .../build/local/reactive/dragdrop.min.js.map | 1 + lib/amd/build/reactive.min.js | 2 +- lib/amd/build/reactive.min.js.map | 2 +- lib/amd/src/local/reactive/dragdrop.js | 465 ++++++++++++++++++ lib/amd/src/reactive.js | 3 +- theme/boost/scss/moodle/core.scss | 31 ++ theme/boost/scss/moodle/courseindex.scss | 11 +- theme/boost/style/moodle.css | 23 + theme/classic/style/moodle.css | 23 + 48 files changed, 1451 insertions(+), 27 deletions(-) create mode 100644 course/format/amd/build/local/courseeditor/dndcmitem.min.js create mode 100644 course/format/amd/build/local/courseeditor/dndcmitem.min.js.map create mode 100644 course/format/amd/build/local/courseeditor/dndsection.min.js create mode 100644 course/format/amd/build/local/courseeditor/dndsection.min.js.map create mode 100644 course/format/amd/build/local/courseeditor/dndsectionitem.min.js create mode 100644 course/format/amd/build/local/courseeditor/dndsectionitem.min.js.map create mode 100644 course/format/amd/build/local/courseindex/section.min.js create mode 100644 course/format/amd/build/local/courseindex/section.min.js.map create mode 100644 course/format/amd/build/local/courseindex/sectiontitle.min.js create mode 100644 course/format/amd/build/local/courseindex/sectiontitle.min.js.map create mode 100644 course/format/amd/src/local/courseeditor/dndcmitem.js create mode 100644 course/format/amd/src/local/courseeditor/dndsection.js create mode 100644 course/format/amd/src/local/courseeditor/dndsectionitem.js create mode 100644 course/format/amd/src/local/courseindex/section.js create mode 100644 course/format/amd/src/local/courseindex/sectiontitle.js create mode 100644 lib/amd/build/local/reactive/dragdrop.min.js create mode 100644 lib/amd/build/local/reactive/dragdrop.min.js.map create mode 100644 lib/amd/src/local/reactive/dragdrop.js diff --git a/course/format/amd/build/courseeditor.min.js.map b/course/format/amd/build/courseeditor.min.js.map index fd66829a1d4..8ad88346fcd 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","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 +{"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,gBAU4B,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 * @property {boolean} setup.supportscomponents if the format supports components for content\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 21e7feb77ba..7940e7be3e2 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_courseformat/local/courseeditor/exporter","core/log","core/ajax"],function(a,b,c,d,e,f){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;c=g(c);d=g(d);e=g(e);f=g(f);function g(a){return a&&a.__esModule?a:{default:a}}function h(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){h=function(a){return typeof a}}else{h=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return h(a)}function i(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 j(a){for(var b=1,c;b.\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 +{"version":3,"sources":["../../../src/local/courseeditor/courseeditor.js"],"names":["courseId","Error","_editing","_supportscomponents","getServerCourseState","stateData","log","error","setInitialState","setup","editing","supportscomponents","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,IACA,KAAKC,mBAAL,IAEA,KAAKH,QAAL,CAAgBA,CAAhB,C,wBAKsB,MAAKI,oBAAL,E,QAAlBC,C,6DAEAC,UAAIC,KAAJ,CAAU,2CAAV,EACAD,UAAIC,KAAJ,O,kCAIJ,KAAKC,eAAL,CAAqBH,CAArB,E,6KAUUI,C,CAAO,SACjB,KAAKP,QAAL,WAAgBO,CAAK,CAACC,OAAtB,mBACA,KAAKP,mBAAL,WAA2BM,CAAK,CAACE,kBAAjC,kBACH,C,oMAQ6BC,WAAKC,IAAL,CAAU,CAAC,CACjCC,UAAU,CAAE,6BADqB,CAEjCC,IAAI,CAAE,CACFC,QAAQ,CAAE,KAAKhB,QADb,CAF2B,CAAD,CAAV,EAKtB,CALsB,C,QAApBiB,C,QAOAZ,C,CAAYa,IAAI,CAACC,KAAL,CAAWF,CAAX,C,6BAGdG,MAAM,CAAE,E,CACRC,OAAO,CAAE,E,CACTC,EAAE,CAAE,E,EACDjB,C,6KAoBG,CACV,MAAO,IAAIkB,UAAJ,CAAa,IAAb,CACV,C,iNAqBiBR,C,uBAAAA,C,yFAEcA,C,4DAExBS,UAAaC,SAAb,O,0JApCQ,OACZ,iBAAO,KAAKvB,QAAZ,kBACH,C,6CAgBuB,OACpB,iBAAO,KAAKC,mBAAZ,kBACH,C,cAhGwBuB,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 this._supportscomponents = 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 * @property {boolean} setup.supportscomponents if the format supports components for content\n */\n setViewFormat(setup) {\n this._editing = setup.editing ?? false;\n this._supportscomponents = setup.supportscomponents ?? 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 * Return if the current course support components to refresh the content.\n *\n * @returns {boolean} if the current content support components\n */\n get supportComponents() {\n return this._supportscomponents ?? 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 diff --git a/course/format/amd/build/local/courseeditor/dndcmitem.min.js b/course/format/amd/build/local/courseeditor/dndcmitem.min.js new file mode 100644 index 00000000000..7bd1d49c60f --- /dev/null +++ b/course/format/amd/build/local/courseeditor/dndcmitem.min.js @@ -0,0 +1,2 @@ +define ("core_courseformat/local/courseeditor/dndcmitem",["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 * in both course index and course content.\n *\n * @module core_courseformat/local/courseeditor/dndcmitem\n * @class core_courseformat/local/courseeditor/dndcmitem\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\n\nexport default class extends BaseComponent {\n\n /**\n * Configure the component drag and drop.\n *\n * @param {number} cmid course module id\n */\n configDragDrop(cmid) {\n\n this.id = cmid;\n\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init element drag and drop.\n this.dragdrop = new DragDrop(this);\n // Save dropzone classes.\n this.classes = this.dragdrop.getClasses();\n }\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n // Drag and drop methods.\n\n /**\n * Get the draggable data of this component.\n *\n * @returns {Object} exported course module drop data\n */\n getDraggableData() {\n const exporter = this.reactive.getExporter();\n return exporter.cmDraggableData(this.reactive.state, this.id);\n }\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n return dropdata?.type === 'cm';\n }\n\n /**\n * Display the component dropzone.\n *\n * @param {Object} dropdata the accepted drop data\n */\n showDropZone(dropdata) {\n // If we are the next cmid of the dragged element we accept the drop because otherwise it\n // will get captured by the section. However, we won't trigger any mutation.\n if (dropdata.nextcmid != this.id && dropdata.id != this.id) {\n this.element.classList.add(this.classes.DROPUP);\n }\n }\n\n /**\n * Hide the component dropzone.\n */\n hideDropZone() {\n this.element.classList.remove(this.classes.DROPUP);\n }\n\n /**\n * Drop event handler.\n *\n * @param {Object} dropdata the accepted drop data\n */\n drop(dropdata) {\n // Call the move mutation if necessary.\n if (dropdata.id != this.id && dropdata.nextcmid != this.id) {\n this.reactive.dispatch('cmMove', [dropdata.id], null, this.id);\n }\n }\n\n}\n"],"file":"dndcmitem.min.js"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/dndsection.min.js b/course/format/amd/build/local/courseeditor/dndsection.min.js new file mode 100644 index 00000000000..df8e2755369 --- /dev/null +++ b/course/format/amd/build/local/courseeditor/dndsection.min.js @@ -0,0 +1,2 @@ +define ("core_courseformat/local/courseeditor/dndsection",["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;ca.number){this.element.classList.remove(this.classes.DROPUP);this.element.classList.add(this.classes.DROPDOWN)}else{this.element.classList.add(this.classes.DROPUP);this.element.classList.remove(this.classes.DROPDOWN)}}}},{key:"hideDropZone",value:function hideDropZone(){var a;null===(a=this.getLastCm())||void 0===a?void 0:a.classList.remove(this.classes.DROPDOWN);this.element.classList.remove(this.classes.DROPUP);this.element.classList.remove(this.classes.DROPDOWN)}},{key:"drop",value:function drop(a){if("cm"==a.type){this.reactive.dispatch("cmMove",[a.id],this.id)}if("section"==a.type){this.reactive.dispatch("sectionMove",[a.id],this.id)}}}]);return c}(b.BaseComponent);a.default=n;return a.default}); +//# sourceMappingURL=dndsection.min.js.map diff --git a/course/format/amd/build/local/courseeditor/dndsection.min.js.map b/course/format/amd/build/local/courseeditor/dndsection.min.js.map new file mode 100644 index 00000000000..0e1b3ff474e --- /dev/null +++ b/course/format/amd/build/local/courseeditor/dndsection.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/local/courseeditor/dndsection.js"],"names":["state","id","element","dataset","section","get","course","sectionitem","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","unregister","dropdata","type","sectionzeroid","sectionlist","getLastCm","classList","add","DROPDOWN","number","remove","DROPUP","dispatch","BaseComponent"],"mappings":"47DAoCgBA,C,CAAO,CACf,KAAKC,EAAL,CAAU,KAAKC,OAAL,CAAaC,OAAb,CAAqBF,EAA/B,CACA,KAAKG,OAAL,CAAeJ,CAAK,CAACI,OAAN,CAAcC,GAAd,CAAkB,KAAKJ,EAAvB,CAAf,CACA,KAAKK,MAAL,CAAcN,CAAK,CAACM,MACvB,C,sDAOcC,C,CAAa,CAExB,GAAI,KAAKC,QAAL,CAAcC,SAAd,EAA2B,KAAKD,QAAL,CAAcE,iBAA7C,CAAgE,CAE5D,KAAKH,WAAL,CAAmBA,CAAnB,CAEA,KAAKI,QAAL,CAAgB,GAAIC,WAAJ,CAAa,IAAb,CAAhB,CAEA,KAAKC,OAAL,CAAe,KAAKF,QAAL,CAAcG,UAAd,EAClB,CACJ,C,yCAKS,CACN,GAAI,KAAKP,WAAL,SAAJ,CAAoC,CAChC,KAAKA,WAAL,CAAiBQ,UAAjB,EACH,CACD,GAAI,KAAKJ,QAAL,SAAJ,CAAiC,CAC7B,KAAKA,QAAL,CAAcI,UAAd,EACH,CACJ,C,6CAOW,CACR,MAAO,KACV,C,0DAUgBC,C,CAAU,CAEvB,GAAuB,IAAnB,WAAAA,CAAQ,WAARA,SAAAA,CAAQ,CAAEC,IAAV,CAAJ,CAA6B,CACzB,QACH,CAED,GAAuB,SAAnB,WAAAD,CAAQ,WAARA,SAAAA,CAAQ,CAAEC,IAAV,CAAJ,CAAkC,CAC9B,GAAMC,CAAAA,CAAa,CAAG,KAAKZ,MAAL,CAAYa,WAAZ,CAAwB,CAAxB,CAAtB,CACA,MAAO,QAAAH,CAAQ,WAARA,SAAAA,CAAQ,CAAEf,EAAV,GAAgB,KAAKA,EAArB,EAA2B,QAAAe,CAAQ,WAARA,SAAAA,CAAQ,CAAEf,EAAV,GAAgBiB,CAA3C,EAA4D,KAAKjB,EAAL,EAAWiB,CACjF,CACD,QACH,C,kDAOYF,C,CAAU,CACnB,GAAqB,IAAjB,EAAAA,CAAQ,CAACC,IAAb,CAA2B,OACvB,eAAKG,SAAL,yBAAkBC,SAAlB,CAA4BC,GAA5B,CAAgC,KAAKT,OAAL,CAAaU,QAA7C,CACH,CACD,GAAqB,SAAjB,EAAAP,CAAQ,CAACC,IAAb,CAAgC,CAE5B,GAAI,KAAKb,OAAL,CAAaoB,MAAb,CAAsBR,CAAQ,CAACQ,MAAnC,CAA2C,CACvC,KAAKtB,OAAL,CAAamB,SAAb,CAAuBI,MAAvB,CAA8B,KAAKZ,OAAL,CAAaa,MAA3C,EACA,KAAKxB,OAAL,CAAamB,SAAb,CAAuBC,GAAvB,CAA2B,KAAKT,OAAL,CAAaU,QAAxC,CACH,CAHD,IAGO,CACH,KAAKrB,OAAL,CAAamB,SAAb,CAAuBC,GAAvB,CAA2B,KAAKT,OAAL,CAAaa,MAAxC,EACA,KAAKxB,OAAL,CAAamB,SAAb,CAAuBI,MAAvB,CAA8B,KAAKZ,OAAL,CAAaU,QAA3C,CACH,CACJ,CACJ,C,mDAKc,OACX,eAAKH,SAAL,yBAAkBC,SAAlB,CAA4BI,MAA5B,CAAmC,KAAKZ,OAAL,CAAaU,QAAhD,EACA,KAAKrB,OAAL,CAAamB,SAAb,CAAuBI,MAAvB,CAA8B,KAAKZ,OAAL,CAAaa,MAA3C,EACA,KAAKxB,OAAL,CAAamB,SAAb,CAAuBI,MAAvB,CAA8B,KAAKZ,OAAL,CAAaU,QAA3C,CACH,C,kCAOIP,C,CAAU,CAEX,GAAqB,IAAjB,EAAAA,CAAQ,CAACC,IAAb,CAA2B,CACvB,KAAKT,QAAL,CAAcmB,QAAd,CAAuB,QAAvB,CAAiC,CAACX,CAAQ,CAACf,EAAV,CAAjC,CAAgD,KAAKA,EAArD,CACH,CACD,GAAqB,SAAjB,EAAAe,CAAQ,CAACC,IAAb,CAAgC,CAC5B,KAAKT,QAAL,CAAcmB,QAAd,CAAuB,aAAvB,CAAsC,CAACX,CAAQ,CAACf,EAAV,CAAtC,CAAqD,KAAKA,EAA1D,CACH,CACJ,C,cAnHwB2B,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 section component.\n *\n * This component is used to control specific course section interactions like drag and drop\n * in both course index and course content.\n *\n * @module core_courseformat/local/courseeditor/dndsection\n * @class core_courseformat/local/courseeditor/dndsection\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\n\nexport default class extends BaseComponent {\n\n /**\n * Save some values form the state.\n *\n * @param {Object} state the current state\n */\n configState(state) {\n this.id = this.element.dataset.id;\n this.section = state.section.get(this.id);\n this.course = state.course;\n }\n\n /**\n * Register state values and the drag and drop subcomponent.\n *\n * @param {BaseComponent} sectionitem section item component\n */\n configDragDrop(sectionitem) {\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init the inner dragable element.\n this.sectionitem = sectionitem;\n // Init the dropzone.\n this.dragdrop = new DragDrop(this);\n // Save dropzone classes.\n this.classes = this.dragdrop.getClasses();\n }\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.sectionitem !== undefined) {\n this.sectionitem.unregister();\n }\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null} the las course module element of the section.\n */\n getLastCm() {\n return null;\n }\n\n // Drag and drop methods.\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n // We accept any course module.\n if (dropdata?.type === 'cm') {\n return true;\n }\n // We accept any section bu the section 0 or ourself\n if (dropdata?.type === 'section') {\n const sectionzeroid = this.course.sectionlist[0];\n return dropdata?.id != this.id && dropdata?.id != sectionzeroid && this.id != sectionzeroid;\n }\n return false;\n }\n\n /**\n * Display the component dropzone.\n *\n * @param {Object} dropdata the accepted drop data\n */\n showDropZone(dropdata) {\n if (dropdata.type == 'cm') {\n this.getLastCm()?.classList.add(this.classes.DROPDOWN);\n }\n if (dropdata.type == 'section') {\n // The relative move of section depends on the section number.\n if (this.section.number > dropdata.number) {\n this.element.classList.remove(this.classes.DROPUP);\n this.element.classList.add(this.classes.DROPDOWN);\n } else {\n this.element.classList.add(this.classes.DROPUP);\n this.element.classList.remove(this.classes.DROPDOWN);\n }\n }\n }\n\n /**\n * Hide the component dropzone.\n */\n hideDropZone() {\n this.getLastCm()?.classList.remove(this.classes.DROPDOWN);\n this.element.classList.remove(this.classes.DROPUP);\n this.element.classList.remove(this.classes.DROPDOWN);\n }\n\n /**\n * Drop event handler.\n *\n * @param {Object} dropdata the accepted drop data\n */\n drop(dropdata) {\n // Call the move mutation.\n if (dropdata.type == 'cm') {\n this.reactive.dispatch('cmMove', [dropdata.id], this.id);\n }\n if (dropdata.type == 'section') {\n this.reactive.dispatch('sectionMove', [dropdata.id], this.id);\n }\n }\n}\n"],"file":"dndsection.min.js"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/dndsectionitem.min.js b/course/format/amd/build/local/courseeditor/dndsectionitem.min.js new file mode 100644 index 00000000000..b8638579b77 --- /dev/null +++ b/course/format/amd/build/local/courseeditor/dndsectionitem.min.js @@ -0,0 +1,2 @@ +define ("core_courseformat/local/courseeditor/dndsectionitem",["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 section title draggable component.\n *\n * This component is used to control specific course section interactions like drag and drop\n * in both course index and course content.\n *\n * @module core_courseformat/local/courseeditor/dndsectionitem\n * @class core_courseformat/local/courseeditor/dndsectionitem\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\n\nexport default class extends BaseComponent {\n\n /**\n * Initial state ready method.\n *\n * @param {number} sectionid the section id\n * @param {Object} state the initial state\n * @param {Element} fullregion the complete section region to mark as dragged\n */\n configDragDrop(sectionid, state, fullregion) {\n\n this.id = sectionid;\n if (this.section === undefined) {\n this.section = state.section.get(this.id);\n }\n if (this.course === undefined) {\n this.course = state.course;\n }\n\n // Prevent topic zero from being draggable.\n if (this.section.number > 0) {\n this.getDraggableData = this._getDraggableData;\n }\n\n this.fullregion = fullregion;\n\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init the dropzone.\n this.dragdrop = new DragDrop(this);\n // Save dropzone classes.\n this.classes = this.dragdrop.getClasses();\n }\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n // Drag and drop methods.\n\n /**\n * Get the draggable data of this component.\n *\n * @returns {Object} exported course module drop data\n */\n _getDraggableData() {\n const exporter = this.reactive.getExporter();\n return exporter.sectionDraggableData(this.reactive.state, this.id);\n }\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n // Course module validation.\n if (dropdata?.type === 'cm') {\n // The first section element is already there so we can ignore it.\n const firstcmid = this.section?.cmlist[0];\n return dropdata.id !== firstcmid;\n }\n return false;\n }\n\n /**\n * Display the component dropzone.\n *\n * @param {Object} dropdata the accepted drop data\n */\n showDropZone() {\n this.element.classList.add(this.classes.DROPZONE);\n }\n\n /**\n * Hide the component dropzone.\n */\n hideDropZone() {\n this.element.classList.remove(this.classes.DROPZONE);\n }\n\n /**\n * Drop event handler.\n *\n * @param {Object} dropdata the accepted drop data\n */\n drop(dropdata) {\n // Call the move mutation.\n if (dropdata.type == 'cm') {\n this.reactive.dispatch('cmMove', [dropdata.id], this.id, this.section?.cmlist[0]);\n }\n }\n}\n"],"file":"dndsectionitem.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 index 7f492602506..09973029b2f 100644 --- a/course/format/amd/build/local/courseeditor/exporter.min.js +++ b/course/format/amd/build/local/courseeditor/exporter.min.js @@ -1,2 +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 +{"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","nextcmid","currentindex","indexOf","id","type","name","number"],"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,wDAYepB,C,CAAOkB,C,CAAM,CACzB,GAAMC,CAAAA,CAAM,CAAGnB,CAAK,CAACoB,EAAN,CAAST,GAAT,CAAaO,CAAb,CAAf,CACA,GAAI,CAACC,CAAL,CAAa,CACT,MAAO,KACV,CAJwB,GAOrBG,CAAAA,CAPqB,CAQnBZ,CAAO,CAAGV,CAAK,CAACU,OAAN,CAAcC,GAAd,CAAkBQ,CAAM,CAACX,SAAzB,CARS,CASnBe,CAAY,QAAGb,CAAH,WAAGA,CAAH,QAAGA,CAAO,CAAEO,MAAT,CAAgBO,OAAhB,CAAwBL,CAAM,CAACM,EAA/B,CATI,CAUzB,GAAIF,CAAY,SAAhB,CAAgC,CAC5BD,CAAQ,QAAGZ,CAAH,WAAGA,CAAH,QAAGA,CAAO,CAAEO,MAAT,CAAgBM,CAAY,CAAG,CAA/B,CACd,CAED,MAAO,CACHG,IAAI,CAAE,IADH,CAEHD,EAAE,CAAEN,CAAM,CAACM,EAFR,CAGHE,IAAI,CAAER,CAAM,CAACQ,IAHV,CAIHL,QAAQ,CAARA,CAJG,CAMV,C,kEAYoBtB,C,CAAOQ,C,CAAW,CACnC,GAAMC,CAAAA,CAAW,CAAGT,CAAK,CAACU,OAAN,CAAcC,GAAd,CAAkBH,CAAlB,CAApB,CACA,GAAI,CAACC,CAAL,CAAkB,CACd,MAAO,KACV,CACD,MAAO,CACHiB,IAAI,CAAE,SADH,CAEHD,EAAE,CAAEhB,CAAW,CAACgB,EAFb,CAGHE,IAAI,CAAElB,CAAW,CAACkB,IAHf,CAIHC,MAAM,CAAEnB,CAAW,CAACmB,MAJjB,CAMV,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 * 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 /**\n * Generate a dragable cm data structure.\n *\n * This method is used by any draggable course module element to generate drop data\n * for its reactive/dragdrop instance.\n *\n * @param {*} state the state object\n * @param {*} cmid the cours emodule id\n * @returns {Object|null}\n */\n cmDraggableData(state, cmid) {\n const cminfo = state.cm.get(cmid);\n if (!cminfo) {\n return null;\n }\n\n // Drop an activity over the next activity is the same as doing anything.\n let nextcmid;\n const section = state.section.get(cminfo.sectionid);\n const currentindex = section?.cmlist.indexOf(cminfo.id);\n if (currentindex !== undefined) {\n nextcmid = section?.cmlist[currentindex + 1];\n }\n\n return {\n type: 'cm',\n id: cminfo.id,\n name: cminfo.name,\n nextcmid,\n };\n }\n\n /**\n * Generate a dragable cm data structure.\n *\n * This method is used by any draggable section element to generate drop data\n * for its reactive/dragdrop instance.\n *\n * @param {*} state the state object\n * @param {*} sectionid the cours section id\n * @returns {Object|null}\n */\n sectionDraggableData(state, sectionid) {\n const sectioninfo = state.section.get(sectionid);\n if (!sectioninfo) {\n return null;\n }\n return {\n type: 'section',\n id: sectioninfo.id,\n name: sectioninfo.name,\n number: sectioninfo.number,\n };\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 c247ae2c35b..bafbd33d1a2 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 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 +{"version":3,"sources":["../../../src/local/courseeditor/mutations.js"],"names":["action","courseId","ids","targetSectionId","targetCmId","args","courseid","targetsectionid","targetcmid","ajax","call","methodname","ajaxresult","JSON","parse","stateManager","cmids","Error","course","get","_callEditWebservice","id","updates","processUpdates","sectionIds"],"mappings":"8KAeA,uD,owBAwB8BA,C,CAAQC,C,CAAUC,C,CAAKC,C,CAAiBC,C,2FACxDC,C,CAAO,CACTL,MAAM,CAANA,CADS,CAETM,QAAQ,CAAEL,CAFD,CAGTC,GAAG,CAAHA,CAHS,C,CAKb,GAAIC,CAAJ,CAAqB,CACjBE,CAAI,CAACE,eAAL,CAAuBJ,CAC1B,CACD,GAAIC,CAAJ,CAAgB,CACZC,CAAI,CAACG,UAAL,CAAkBJ,CACrB,C,eACsBK,WAAKC,IAAL,CAAU,CAAC,CAC9BC,UAAU,CAAE,iCADkB,CAE9BN,IAAI,CAAJA,CAF8B,CAAD,CAAV,EAGnB,CAHmB,C,QAAnBO,C,iCAIGC,IAAI,CAACC,KAAL,CAAWF,CAAX,C,iMAkBEG,C,CAAcC,C,CAAOb,C,CAAiBC,C,gGAC3C,CAACD,CAAD,EAAoB,CAACC,C,uBACf,IAAIa,CAAAA,KAAJ,0D,QAEJC,C,CAASH,CAAY,CAACI,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,SAAzB,CAAoCF,CAAM,CAACG,EAA3C,CAA+CL,CAA/C,CAAsDb,CAAtD,CAAuEC,CAAvE,C,QAAhBkB,C,QACNP,CAAY,CAACQ,cAAb,CAA4BD,CAA5B,E,4LAUcP,C,CAAcS,C,CAAYrB,C,8FACnCA,C,sBACK,IAAIc,CAAAA,KAAJ,iD,QAEJC,C,CAASH,CAAY,CAACI,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,cAAzB,CAAyCF,CAAM,CAACG,EAAhD,CAAoDG,CAApD,CAAgErB,CAAhE,C,QAAhBmB,C,QACNP,CAAY,CAACQ,cAAb,CAA4BD,CAA5B,E,6LAUUP,C,CAAcC,C,2FAClBE,C,CAASH,CAAY,CAACI,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,UAAzB,CAAqCF,CAAM,CAACG,EAA5C,CAAgDL,CAAhD,C,QAAhBM,C,QACNP,CAAY,CAACQ,cAAb,CAA4BD,CAA5B,E,8LAUeP,C,CAAcS,C,2FACvBN,C,CAASH,CAAY,CAACI,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,eAAzB,CAA0CF,CAAM,CAACG,EAAjD,CAAqDG,CAArD,C,QAAhBF,C,QACNP,CAAY,CAACQ,cAAb,CAA4BD,CAA5B,E,kMAQcP,C,2FACRG,C,CAASH,CAAY,CAACI,GAAb,CAAiB,QAAjB,C,gBACO,MAAKC,mBAAL,CAAyB,cAAzB,CAAyCF,CAAM,CAACG,EAAhD,C,QAAhBC,C,QACNP,CAAY,CAACQ,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 * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _callEditWebservice(action, courseId, ids, targetSectionId, targetCmId) {\n const args = {\n action,\n courseid: courseId,\n ids,\n };\n if (targetSectionId) {\n args.targetsectionid = targetSectionId;\n }\n if (targetCmId) {\n args.targetcmid = targetCmId;\n }\n let ajaxresult = await ajax.call([{\n methodname: 'core_courseformat_update_course',\n args,\n }])[0];\n return JSON.parse(ajaxresult);\n }\n\n /**\n * Move course modules to specific course location.\n *\n * Note that one of targetSectionId or targetCmId should be provided in order to identify the\n * new location:\n * - targetCmId: the activities will be located avobe the target cm. The targetSectionId\n * value will be ignored in this case.\n * - targetSectionId: the activities will be appended to the section. In this case\n * targetSectionId should not be present.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmids the list of cm ids to move\n * @param {number} targetSectionId the target section id\n * @param {number} targetCmId the target course module id\n */\n async cmMove(stateManager, cmids, targetSectionId, targetCmId) {\n if (!targetSectionId && !targetCmId) {\n throw new Error(`Mutation cmMove requires targetSectionId or targetCmId`);\n }\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('cm_move', course.id, cmids, targetSectionId, targetCmId);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Move course modules to specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids to move\n * @param {number} targetSectionId the target section id\n */\n async sectionMove(stateManager, sectionIds, targetSectionId) {\n if (!targetSectionId) {\n throw new Error(`Mutation sectionMove requires targetSectionId`);\n }\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_move', course.id, sectionIds, targetSectionId);\n stateManager.processUpdates(updates);\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 index 9c94cb35821..3e0d337a941 100644 --- a/course/format/amd/build/local/courseindex/cm.min.js +++ b/course/format/amd/build/local/courseindex/cm.min.js @@ -1,2 +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 +{"version":3,"sources":["../../../src/local/courseindex/cm.js"],"names":["Component","name","id","element","dataset","configDragDrop","watch","handler","remove","target","selectors","document","getElementById","DndCmItem"],"mappings":"2MA0BA,uD,+nDAEqBA,CAAAA,C,+HAKR,CAEL,KAAKC,IAAL,CAAY,gBAAZ,CAEA,KAAKC,EAAL,CAAU,KAAKC,OAAL,CAAaC,OAAb,CAAqBF,EAClC,C,+CAmBY,CACT,KAAKG,cAAL,CAAoB,KAAKH,EAAzB,CACH,C,iDAOa,CACV,MAAO,CACH,CAACI,KAAK,cAAQ,KAAKJ,EAAb,aAAN,CAAkCK,OAAO,CAAE,KAAKC,MAAhD,CADG,CAGV,C,oCAvBWC,C,CAAQC,C,CAAW,CAC3B,MAAO,IAAIV,CAAAA,CAAJ,CAAc,CACjBG,OAAO,CAAEQ,QAAQ,CAACC,cAAT,CAAwBH,CAAxB,CADQ,CAEjBC,SAAS,CAATA,CAFiB,CAAd,CAIV,C,cAxBkCG,S","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 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 DndCmItem from 'core_courseformat/local/courseeditor/dndcmitem';\n\nexport default class Component extends DndCmItem {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_cm';\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 this.configDragDrop(this.id);\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\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/section.min.js b/course/format/amd/build/local/courseindex/section.min.js new file mode 100644 index 00000000000..6f408af9f75 --- /dev/null +++ b/course/format/amd/build/local/courseindex/section.min.js @@ -0,0 +1,2 @@ +define ("core_courseformat/local/courseindex/section",["exports","core_courseformat/local/courseindex/sectiontitle","core_courseformat/local/courseeditor/dndsection"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}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){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 g(a){for(var b=1,c;b.\n\n/**\n * Course index section component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/section\n * @class core_courseformat/local/courseindex/section\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport SectionTitle from 'core_courseformat/local/courseindex/sectiontitle';\nimport DndSection from 'core_courseformat/local/courseeditor/dndsection';\n\nexport default class Component extends DndSection {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_section';\n // Default query selectors.\n this.selectors = {\n SECTION_ITEM: `[data-for='section_item']`,\n CM_LAST: `[data-for=\"cm\"]:last-child`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {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 * @param {Object} state the initial state\n */\n stateReady(state) {\n this.configState(state);\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init the inner dragable element passing the full section as affected region.\n const titleitem = new SectionTitle({\n ...this,\n element: this.getElement(this.selectors.SECTION_ITEM),\n fullregion: this.element,\n });\n this.configDragDrop(titleitem);\n }\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null}\n */\n getLastCm() {\n return this.getElement(this.selectors.CM_LAST);\n }\n}\n"],"file":"section.min.js"} \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/sectiontitle.min.js b/course/format/amd/build/local/courseindex/sectiontitle.min.js new file mode 100644 index 00000000000..f63442fa0a5 --- /dev/null +++ b/course/format/amd/build/local/courseindex/sectiontitle.min.js @@ -0,0 +1,2 @@ +define ("core_courseformat/local/courseindex/sectiontitle",["exports","core_courseformat/local/courseeditor/dndsectionitem"],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){"@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 section title component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/sectiontitle\n * @class core_courseformat/local/courseindex/sectiontitle\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DndSectionItem from 'core_courseformat/local/courseeditor/dndsectionitem';\n\nexport default class Component extends DndSectionItem {\n\n /**\n * Constructor hook.\n *\n * @param {Object} descriptor\n */\n create(descriptor) {\n // Optional component name for debugging.\n this.name = 'courseindex_sectiontitle';\n\n this.id = descriptor.id;\n this.section = descriptor.section;\n this.course = descriptor.course;\n this.fullregion = descriptor.fullregion;\n\n // Prevent topic zero from being draggable.\n if (this.section.number > 0) {\n this.getDraggableData = this._getDraggableData;\n }\n }\n\n /**\n * Static method to create a component instance form the mustahce 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 * @param {Object} state the initial state\n */\n stateReady(state) {\n this.configDragDrop(this.id, state, this.fullregion);\n }\n}\n"],"file":"sectiontitle.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 a17a5f91a96..056434e0bb6 100644 --- a/course/format/amd/src/courseeditor.js +++ b/course/format/amd/src/courseeditor.js @@ -54,6 +54,7 @@ function dispatchStateChangedEvent(detail, target) { * @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 + * @property {boolean} setup.supportscomponents if the format supports components for content */ export const setViewFormat = (courseId, setup) => { const editor = getCourseEditor(courseId); diff --git a/course/format/amd/src/local/courseeditor/courseeditor.js b/course/format/amd/src/local/courseeditor/courseeditor.js index 557841715de..c8f44c6fcc1 100644 --- a/course/format/amd/src/local/courseeditor/courseeditor.js +++ b/course/format/amd/src/local/courseeditor/courseeditor.js @@ -47,6 +47,7 @@ export default class extends Reactive { // Default view format setup. this._editing = false; + this._supportscomponents = false; this.courseId = courseId; @@ -68,9 +69,11 @@ export default class extends Reactive { * * @param {Object} setup format, page and course settings * @property {boolean} setup.editing if the page is in edit mode + * @property {boolean} setup.supportscomponents if the format supports components for content */ setViewFormat(setup) { this._editing = setup.editing ?? false; + this._supportscomponents = setup.supportscomponents ?? false; } /** @@ -116,6 +119,15 @@ export default class extends Reactive { return new Exporter(this); } + /** + * Return if the current course support components to refresh the content. + * + * @returns {boolean} if the current content support components + */ + get supportComponents() { + return this._supportscomponents ?? false; + } + /** * Dispatch a change in the state. * diff --git a/course/format/amd/src/local/courseeditor/dndcmitem.js b/course/format/amd/src/local/courseeditor/dndcmitem.js new file mode 100644 index 00000000000..19e3aef0ca4 --- /dev/null +++ b/course/format/amd/src/local/courseeditor/dndcmitem.js @@ -0,0 +1,113 @@ +// 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 + * in both course index and course content. + * + * @module core_courseformat/local/courseeditor/dndcmitem + * @class core_courseformat/local/courseeditor/dndcmitem + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {BaseComponent, DragDrop} from 'core/reactive'; + +export default class extends BaseComponent { + + /** + * Configure the component drag and drop. + * + * @param {number} cmid course module id + */ + configDragDrop(cmid) { + + this.id = cmid; + + // Drag and drop is only available for components compatible course formats. + if (this.reactive.isEditing && this.reactive.supportComponents) { + // Init element drag and drop. + this.dragdrop = new DragDrop(this); + // Save dropzone classes. + this.classes = this.dragdrop.getClasses(); + } + } + + /** + * Remove all subcomponents dependencies. + */ + destroy() { + if (this.dragdrop !== undefined) { + this.dragdrop.unregister(); + } + } + + // Drag and drop methods. + + /** + * Get the draggable data of this component. + * + * @returns {Object} exported course module drop data + */ + getDraggableData() { + const exporter = this.reactive.getExporter(); + return exporter.cmDraggableData(this.reactive.state, this.id); + } + + /** + * Validate if the drop data can be dropped over the component. + * + * @param {Object} dropdata the exported drop data. + * @returns {boolean} + */ + validateDropData(dropdata) { + return dropdata?.type === 'cm'; + } + + /** + * Display the component dropzone. + * + * @param {Object} dropdata the accepted drop data + */ + showDropZone(dropdata) { + // If we are the next cmid of the dragged element we accept the drop because otherwise it + // will get captured by the section. However, we won't trigger any mutation. + if (dropdata.nextcmid != this.id && dropdata.id != this.id) { + this.element.classList.add(this.classes.DROPUP); + } + } + + /** + * Hide the component dropzone. + */ + hideDropZone() { + this.element.classList.remove(this.classes.DROPUP); + } + + /** + * Drop event handler. + * + * @param {Object} dropdata the accepted drop data + */ + drop(dropdata) { + // Call the move mutation if necessary. + if (dropdata.id != this.id && dropdata.nextcmid != this.id) { + this.reactive.dispatch('cmMove', [dropdata.id], null, this.id); + } + } + +} diff --git a/course/format/amd/src/local/courseeditor/dndsection.js b/course/format/amd/src/local/courseeditor/dndsection.js new file mode 100644 index 00000000000..9d6568f8989 --- /dev/null +++ b/course/format/amd/src/local/courseeditor/dndsection.js @@ -0,0 +1,146 @@ +// 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 section component. + * + * This component is used to control specific course section interactions like drag and drop + * in both course index and course content. + * + * @module core_courseformat/local/courseeditor/dndsection + * @class core_courseformat/local/courseeditor/dndsection + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {BaseComponent, DragDrop} from 'core/reactive'; + +export default class extends BaseComponent { + + /** + * Save some values form the state. + * + * @param {Object} state the current state + */ + configState(state) { + this.id = this.element.dataset.id; + this.section = state.section.get(this.id); + this.course = state.course; + } + + /** + * Register state values and the drag and drop subcomponent. + * + * @param {BaseComponent} sectionitem section item component + */ + configDragDrop(sectionitem) { + // Drag and drop is only available for components compatible course formats. + if (this.reactive.isEditing && this.reactive.supportComponents) { + // Init the inner dragable element. + this.sectionitem = sectionitem; + // Init the dropzone. + this.dragdrop = new DragDrop(this); + // Save dropzone classes. + this.classes = this.dragdrop.getClasses(); + } + } + + /** + * Remove all subcomponents dependencies. + */ + destroy() { + if (this.sectionitem !== undefined) { + this.sectionitem.unregister(); + } + if (this.dragdrop !== undefined) { + this.dragdrop.unregister(); + } + } + + /** + * Get the last CM element of that section. + * + * @returns {element|null} the las course module element of the section. + */ + getLastCm() { + return null; + } + + // Drag and drop methods. + + /** + * Validate if the drop data can be dropped over the component. + * + * @param {Object} dropdata the exported drop data. + * @returns {boolean} + */ + validateDropData(dropdata) { + // We accept any course module. + if (dropdata?.type === 'cm') { + return true; + } + // We accept any section bu the section 0 or ourself + if (dropdata?.type === 'section') { + const sectionzeroid = this.course.sectionlist[0]; + return dropdata?.id != this.id && dropdata?.id != sectionzeroid && this.id != sectionzeroid; + } + return false; + } + + /** + * Display the component dropzone. + * + * @param {Object} dropdata the accepted drop data + */ + showDropZone(dropdata) { + if (dropdata.type == 'cm') { + this.getLastCm()?.classList.add(this.classes.DROPDOWN); + } + if (dropdata.type == 'section') { + // The relative move of section depends on the section number. + if (this.section.number > dropdata.number) { + this.element.classList.remove(this.classes.DROPUP); + this.element.classList.add(this.classes.DROPDOWN); + } else { + this.element.classList.add(this.classes.DROPUP); + this.element.classList.remove(this.classes.DROPDOWN); + } + } + } + + /** + * Hide the component dropzone. + */ + hideDropZone() { + this.getLastCm()?.classList.remove(this.classes.DROPDOWN); + this.element.classList.remove(this.classes.DROPUP); + this.element.classList.remove(this.classes.DROPDOWN); + } + + /** + * Drop event handler. + * + * @param {Object} dropdata the accepted drop data + */ + drop(dropdata) { + // Call the move mutation. + if (dropdata.type == 'cm') { + this.reactive.dispatch('cmMove', [dropdata.id], this.id); + } + if (dropdata.type == 'section') { + this.reactive.dispatch('sectionMove', [dropdata.id], this.id); + } + } +} diff --git a/course/format/amd/src/local/courseeditor/dndsectionitem.js b/course/format/amd/src/local/courseeditor/dndsectionitem.js new file mode 100644 index 00000000000..850c6e5c93a --- /dev/null +++ b/course/format/amd/src/local/courseeditor/dndsectionitem.js @@ -0,0 +1,129 @@ +// 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 section title draggable component. + * + * This component is used to control specific course section interactions like drag and drop + * in both course index and course content. + * + * @module core_courseformat/local/courseeditor/dndsectionitem + * @class core_courseformat/local/courseeditor/dndsectionitem + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {BaseComponent, DragDrop} from 'core/reactive'; + +export default class extends BaseComponent { + + /** + * Initial state ready method. + * + * @param {number} sectionid the section id + * @param {Object} state the initial state + * @param {Element} fullregion the complete section region to mark as dragged + */ + configDragDrop(sectionid, state, fullregion) { + + this.id = sectionid; + if (this.section === undefined) { + this.section = state.section.get(this.id); + } + if (this.course === undefined) { + this.course = state.course; + } + + // Prevent topic zero from being draggable. + if (this.section.number > 0) { + this.getDraggableData = this._getDraggableData; + } + + this.fullregion = fullregion; + + // Drag and drop is only available for components compatible course formats. + if (this.reactive.isEditing && this.reactive.supportComponents) { + // Init the dropzone. + this.dragdrop = new DragDrop(this); + // Save dropzone classes. + this.classes = this.dragdrop.getClasses(); + } + } + + /** + * Remove all subcomponents dependencies. + */ + destroy() { + if (this.dragdrop !== undefined) { + this.dragdrop.unregister(); + } + } + + // Drag and drop methods. + + /** + * Get the draggable data of this component. + * + * @returns {Object} exported course module drop data + */ + _getDraggableData() { + const exporter = this.reactive.getExporter(); + return exporter.sectionDraggableData(this.reactive.state, this.id); + } + + /** + * Validate if the drop data can be dropped over the component. + * + * @param {Object} dropdata the exported drop data. + * @returns {boolean} + */ + validateDropData(dropdata) { + // Course module validation. + if (dropdata?.type === 'cm') { + // The first section element is already there so we can ignore it. + const firstcmid = this.section?.cmlist[0]; + return dropdata.id !== firstcmid; + } + return false; + } + + /** + * Display the component dropzone. + * + * @param {Object} dropdata the accepted drop data + */ + showDropZone() { + this.element.classList.add(this.classes.DROPZONE); + } + + /** + * Hide the component dropzone. + */ + hideDropZone() { + this.element.classList.remove(this.classes.DROPZONE); + } + + /** + * Drop event handler. + * + * @param {Object} dropdata the accepted drop data + */ + drop(dropdata) { + // Call the move mutation. + if (dropdata.type == 'cm') { + this.reactive.dispatch('cmMove', [dropdata.id], this.id, this.section?.cmlist[0]); + } + } +} diff --git a/course/format/amd/src/local/courseeditor/exporter.js b/course/format/amd/src/local/courseeditor/exporter.js index 80b9ebf18b4..e54fe439f55 100644 --- a/course/format/amd/src/local/courseeditor/exporter.js +++ b/course/format/amd/src/local/courseeditor/exporter.js @@ -94,4 +94,59 @@ export default class { }; return cm; } + + /** + * Generate a dragable cm data structure. + * + * This method is used by any draggable course module element to generate drop data + * for its reactive/dragdrop instance. + * + * @param {*} state the state object + * @param {*} cmid the cours emodule id + * @returns {Object|null} + */ + cmDraggableData(state, cmid) { + const cminfo = state.cm.get(cmid); + if (!cminfo) { + return null; + } + + // Drop an activity over the next activity is the same as doing anything. + let nextcmid; + const section = state.section.get(cminfo.sectionid); + const currentindex = section?.cmlist.indexOf(cminfo.id); + if (currentindex !== undefined) { + nextcmid = section?.cmlist[currentindex + 1]; + } + + return { + type: 'cm', + id: cminfo.id, + name: cminfo.name, + nextcmid, + }; + } + + /** + * Generate a dragable cm data structure. + * + * This method is used by any draggable section element to generate drop data + * for its reactive/dragdrop instance. + * + * @param {*} state the state object + * @param {*} sectionid the cours section id + * @returns {Object|null} + */ + sectionDraggableData(state, sectionid) { + const sectioninfo = state.section.get(sectionid); + if (!sectioninfo) { + return null; + } + return { + type: 'section', + id: sectioninfo.id, + name: sectioninfo.name, + number: sectioninfo.number, + }; + } } diff --git a/course/format/amd/src/local/courseeditor/mutations.js b/course/format/amd/src/local/courseeditor/mutations.js index 23d3e332730..6b431d6e126 100644 --- a/course/format/amd/src/local/courseeditor/mutations.js +++ b/course/format/amd/src/local/courseeditor/mutations.js @@ -34,19 +34,68 @@ export default class { * @param {string} action * @param {number} courseId * @param {array} ids + * @param {number} targetSectionId optional target section id (for moving actions) + * @param {number} targetCmId optional target cm id (for moving actions) */ - async _callEditWebservice(action, courseId, ids) { + async _callEditWebservice(action, courseId, ids, targetSectionId, targetCmId) { + const args = { + action, + courseid: courseId, + ids, + }; + if (targetSectionId) { + args.targetsectionid = targetSectionId; + } + if (targetCmId) { + args.targetcmid = targetCmId; + } let ajaxresult = await ajax.call([{ methodname: 'core_courseformat_update_course', - args: { - action, - courseid: courseId, - ids, - } + args, }])[0]; return JSON.parse(ajaxresult); } + /** + * Move course modules to specific course location. + * + * Note that one of targetSectionId or targetCmId should be provided in order to identify the + * new location: + * - targetCmId: the activities will be located avobe the target cm. The targetSectionId + * value will be ignored in this case. + * - targetSectionId: the activities will be appended to the section. In this case + * targetSectionId should not be present. + * + * @param {StateManager} stateManager the current state manager + * @param {array} cmids the list of cm ids to move + * @param {number} targetSectionId the target section id + * @param {number} targetCmId the target course module id + */ + async cmMove(stateManager, cmids, targetSectionId, targetCmId) { + if (!targetSectionId && !targetCmId) { + throw new Error(`Mutation cmMove requires targetSectionId or targetCmId`); + } + const course = stateManager.get('course'); + const updates = await this._callEditWebservice('cm_move', course.id, cmids, targetSectionId, targetCmId); + stateManager.processUpdates(updates); + } + + /** + * Move course modules to specific course location. + * + * @param {StateManager} stateManager the current state manager + * @param {array} sectionIds the list of section ids to move + * @param {number} targetSectionId the target section id + */ + async sectionMove(stateManager, sectionIds, targetSectionId) { + if (!targetSectionId) { + throw new Error(`Mutation sectionMove requires targetSectionId`); + } + const course = stateManager.get('course'); + const updates = await this._callEditWebservice('section_move', course.id, sectionIds, targetSectionId); + stateManager.processUpdates(updates); + } + /** * Get updated state data related to some cm ids. * diff --git a/course/format/amd/src/local/courseindex/cm.js b/course/format/amd/src/local/courseindex/cm.js index b264bcbeded..f0e8b8cf5fb 100644 --- a/course/format/amd/src/local/courseindex/cm.js +++ b/course/format/amd/src/local/courseindex/cm.js @@ -24,9 +24,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import {BaseComponent} from 'core/reactive'; +import DndCmItem from 'core_courseformat/local/courseeditor/dndcmitem'; -export default class Component extends BaseComponent { +export default class Component extends DndCmItem { /** * Constructor hook. @@ -34,9 +34,6 @@ export default class Component extends BaseComponent { 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; } @@ -59,9 +56,14 @@ export default class Component extends BaseComponent { * Initial state ready method. */ stateReady() { - // Activate drag and drop soon. + this.configDragDrop(this.id); } + /** + * Component watchers. + * + * @returns {Array} of watchers + */ getWatchers() { return [ {watch: `cm[${this.id}]:deleted`, handler: this.remove}, diff --git a/course/format/amd/src/local/courseindex/section.js b/course/format/amd/src/local/courseindex/section.js new file mode 100644 index 00000000000..98b1a48205f --- /dev/null +++ b/course/format/amd/src/local/courseindex/section.js @@ -0,0 +1,86 @@ +// 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 section component. + * + * This component is used to control specific course section interactions like drag and drop. + * + * @module core_courseformat/local/courseindex/section + * @class core_courseformat/local/courseindex/section + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import SectionTitle from 'core_courseformat/local/courseindex/sectiontitle'; +import DndSection from 'core_courseformat/local/courseeditor/dndsection'; + +export default class Component extends DndSection { + + /** + * Constructor hook. + */ + create() { + // Optional component name for debugging. + this.name = 'courseindex_section'; + // Default query selectors. + this.selectors = { + SECTION_ITEM: `[data-for='section_item']`, + CM_LAST: `[data-for="cm"]:last-child`, + }; + } + + /** + * Static method to create a component instance form the mustahce template. + * + * @param {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. + * + * @param {Object} state the initial state + */ + stateReady(state) { + this.configState(state); + // Drag and drop is only available for components compatible course formats. + if (this.reactive.isEditing && this.reactive.supportComponents) { + // Init the inner dragable element passing the full section as affected region. + const titleitem = new SectionTitle({ + ...this, + element: this.getElement(this.selectors.SECTION_ITEM), + fullregion: this.element, + }); + this.configDragDrop(titleitem); + } + } + + /** + * Get the last CM element of that section. + * + * @returns {element|null} + */ + getLastCm() { + return this.getElement(this.selectors.CM_LAST); + } +} diff --git a/course/format/amd/src/local/courseindex/sectiontitle.js b/course/format/amd/src/local/courseindex/sectiontitle.js new file mode 100644 index 00000000000..12d05eeddd2 --- /dev/null +++ b/course/format/amd/src/local/courseindex/sectiontitle.js @@ -0,0 +1,73 @@ +// 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 section title component. + * + * This component is used to control specific course section interactions like drag and drop. + * + * @module core_courseformat/local/courseindex/sectiontitle + * @class core_courseformat/local/courseindex/sectiontitle + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import DndSectionItem from 'core_courseformat/local/courseeditor/dndsectionitem'; + +export default class Component extends DndSectionItem { + + /** + * Constructor hook. + * + * @param {Object} descriptor + */ + create(descriptor) { + // Optional component name for debugging. + this.name = 'courseindex_sectiontitle'; + + this.id = descriptor.id; + this.section = descriptor.section; + this.course = descriptor.course; + this.fullregion = descriptor.fullregion; + + // Prevent topic zero from being draggable. + if (this.section.number > 0) { + this.getDraggableData = this._getDraggableData; + } + } + + /** + * Static method to create a component instance form the mustahce 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. + * + * @param {Object} state the initial state + */ + stateReady(state) { + this.configDragDrop(this.id, state, this.fullregion); + } +} diff --git a/course/format/classes/base.php b/course/format/classes/base.php index 333edd754e7..dc8f5847658 100644 --- a/course/format/classes/base.php +++ b/course/format/classes/base.php @@ -494,6 +494,21 @@ abstract class base { return $ajaxsupport; } + /** + * Returns true if this course format is compatible with content components. + * + * Using components means the content elements can watch the frontend course state and + * react to the changes. Formats with component compatibility can have more interactions + * without refreshing the page, like having drag and drop from the course index to reorder + * sections and activities. + * + * @return bool if the format is compatible with components. + */ + public function supports_components() { + return false; + } + + /** * Custom action after section has been moved in AJAX mode * diff --git a/course/format/classes/output/local/state/section.php b/course/format/classes/output/local/state/section.php index b56671aca0f..4c3f8cc9eb3 100644 --- a/course/format/classes/output/local/state/section.php +++ b/course/format/classes/output/local/state/section.php @@ -64,6 +64,7 @@ class section implements renderable { 'section' => $section->section, 'number' => $section->section, 'title' => $format->get_section_name($section), + 'rawtitle' => $section->name, 'cmlist' => [], 'visible' => !empty($section->visible), 'sectionurl' => course_get_url($course, $section->section)->out(), diff --git a/course/format/classes/stateactions.php b/course/format/classes/stateactions.php index f37bf700702..c42d463ac62 100644 --- a/course/format/classes/stateactions.php +++ b/course/format/classes/stateactions.php @@ -17,9 +17,13 @@ namespace core_courseformat; use core_courseformat\stateupdates; +use cm_info; +use section_info; use stdClass; use course_modinfo; use moodle_exception; +use context_module; +use context_course; /** * Contains the core course state actions. @@ -36,6 +40,159 @@ use moodle_exception; */ class stateactions { + /** + * Move course modules to another location in the same course. + * + * @param stateupdates $updates the affected course elements track + * @param stdClass $course the course object + * @param int[] $ids the list of affected course module ids + * @param int $targetsectionid optional target section id + * @param int $targetcmid optional target cm id + */ + public function cm_move( + stateupdates $updates, + stdClass $course, + array $ids, + ?int $targetsectionid = null, + ?int $targetcmid = null + ): void { + // Validate target elements. + if (!$targetsectionid && !$targetcmid) { + throw new moodle_exception("Action cm_move requires targetsectionid or targetcmid"); + } + + $this->validate_cms($course, $ids, __FUNCTION__); + + // Check capabilities on every activity context. + foreach ($ids as $cmid) { + $modcontext = context_module::instance($cmid); + require_capability('moodle/course:manageactivities', $modcontext); + } + + $modinfo = get_fast_modinfo($course); + + // Target cm has more priority than target section. + if (!empty($targetcmid)) { + $this->validate_cms($course, [$targetcmid], __FUNCTION__); + $targetcm = $modinfo->get_cm($targetcmid); + $targetsection = $modinfo->get_section_info_by_id($targetcm->section, MUST_EXIST); + } else { + $this->validate_sections($course, [$targetsectionid], __FUNCTION__); + $targetcm = null; + $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST); + } + + // The origin sections must be updated as well. + $originalsections = []; + + $cms = $this->get_cm_info($modinfo, $ids); + foreach ($cms as $cm) { + $currentsection = $modinfo->get_section_info_by_id($cm->section, MUST_EXIST); + moveto_module($cm, $targetsection, $targetcm); + $updates->add_cm_put($cm->id); + if ($currentsection->id != $targetsection->id) { + $originalsections[$currentsection->id] = true; + } + // If some of the original sections are also target sections, we don't need to update them. + if (array_key_exists($targetsection->id, $originalsections)) { + unset($originalsections[$targetsection->id]); + } + } + + // Use section_state to return the full affected section and activities updated state. + $this->cm_state($updates, $course, $ids, $targetsectionid, $targetcmid); + + foreach (array_keys($originalsections) as $sectionid) { + $updates->add_section_put($sectionid); + } + } + + /** + * Move course sections to another location in the same course. + * + * @param stateupdates $updates the affected course elements track + * @param stdClass $course the course object + * @param int[] $ids the list of affected course module ids + * @param int $targetsectionid optional target section id + * @param int $targetcmid optional target cm id + */ + public function section_move( + stateupdates $updates, + stdClass $course, + array $ids, + ?int $targetsectionid = null, + ?int $targetcmid = null + ): void { + // Validate target elements. + if (!$targetsectionid) { + throw new moodle_exception("Action cm_move requires targetsectionid"); + } + + $this->validate_sections($course, $ids, __FUNCTION__); + + $coursecontext = context_course::instance($course->id); + require_capability('moodle/course:movesections', $coursecontext); + + $modinfo = get_fast_modinfo($course); + + // Target section. + $this->validate_sections($course, [$targetsectionid], __FUNCTION__); + $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST); + + $affectedsections = [$targetsection->section => true]; + + $sections = $this->get_section_info($modinfo, $ids); + foreach ($sections as $section) { + $affectedsections[$section->section] = true; + move_section_to($course, $section->section, $targetsection->section); + } + + // Use section_state to return the section and activities updated state. + $this->section_state($updates, $course, $ids, $targetsectionid); + + // All course sections can be renamed because of the resort. + $allsections = $modinfo->get_section_info_all(); + foreach ($allsections as $section) { + // Ignore the affected sections because they are already in the updates. + if (isset($affectedsections[$section->section])) { + continue; + } + $updates->add_section_put($section->id); + } + // The section order is at a course level. + $updates->add_course_put(); + } + + /** + * Extract several cm_info from the course_modinfo. + * + * @param course_modinfo $modinfo the course modinfo. + * @param int[] $ids the course modules $ids + * @return cm_info[] the extracted cm_info objects + */ + protected function get_cm_info (course_modinfo $modinfo, array $ids): array { + $cms = []; + foreach ($ids as $cmid) { + $cms[$cmid] = $modinfo->get_cm($cmid); + } + return $cms; + } + + /** + * Extract several section_info from the course_modinfo. + * + * @param course_modinfo $modinfo the course modinfo. + * @param int[] $ids the course modules $ids + * @return section_info[] the extracted section_info objects + */ + protected function get_section_info(course_modinfo $modinfo, array $ids): array { + $sections = []; + foreach ($ids as $sectionid) { + $sections[$sectionid] = $modinfo->get_section_info_by_id($sectionid); + } + return $sections; + } + /** * Add the update messages of the updated version of any cm and section related to the cm ids. * diff --git a/course/format/templates/local/courseindex/cm.mustache b/course/format/templates/local/courseindex/cm.mustache index c17b17a35e4..ef89ba58f29 100644 --- a/course/format/templates/local/courseindex/cm.mustache +++ b/course/format/templates/local/courseindex/cm.mustache @@ -56,6 +56,7 @@ {{{name}}} {{/url}} + {{#pix}}i/dragdrop{{/pix}} {{#js}} require(['core_courseformat/local/courseindex/cm'], function(component) { diff --git a/course/format/templates/local/courseindex/section.mustache b/course/format/templates/local/courseindex/section.mustache index 91899cb16ff..96ba678a527 100644 --- a/course/format/templates/local/courseindex/section.mustache +++ b/course/format/templates/local/courseindex/section.mustache @@ -89,6 +89,7 @@ > {{{title}}} + {{#pix}}i/dragdrop{{/pix}}
+{{#js}} +require(['core_courseformat/local/courseindex/section'], function(component) { + component.init('{{uniqid}}-course-index-section-{{id}}'); +}); +{{/js}} diff --git a/course/format/topics/lib.php b/course/format/topics/lib.php index b0e84ef9659..d4e34e8a2c5 100644 --- a/course/format/topics/lib.php +++ b/course/format/topics/lib.php @@ -159,6 +159,10 @@ class format_topics extends core_courseformat\base { return $ajaxsupport; } + public function supports_components() { + return true; + } + /** * Loads all of the course sections into the navigation. * diff --git a/course/format/upgrade.txt b/course/format/upgrade.txt index bf173ba590a..ecf5c18a592 100644 --- a/course/format/upgrade.txt +++ b/course/format/upgrade.txt @@ -3,7 +3,8 @@ 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. +* New core_courseformat\base::uses_course_index() to define whether the course format uses course index or not. +* New core_courseformat\base::supports_components() to specify if the format is compatible with reactive components. === 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 5526b01f7b0..cdb38b355ac 100644 --- a/course/format/weeks/lib.php +++ b/course/format/weeks/lib.php @@ -160,6 +160,10 @@ class format_weeks extends core_courseformat\base { return $ajaxsupport; } + public function supports_components() { + return true; + } + /** * Loads all of the course sections into the navigation * diff --git a/course/lib.php b/course/lib.php index 1f6b8765e7f..5b9bd4f013c 100644 --- a/course/lib.php +++ b/course/lib.php @@ -3308,6 +3308,7 @@ function include_course_editor(course_format $format) { // Edition mode and some format specs must be passed to the init method. $setup = (object)[ 'editing' => $format->show_editor(), + 'supportscomponents' => $format->supports_components(), ]; // 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. diff --git a/lib/amd/build/local/reactive/dragdrop.min.js b/lib/amd/build/local/reactive/dragdrop.min.js new file mode 100644 index 00000000000..14fbdd1bdf1 --- /dev/null +++ b/lib/amd/build/local/reactive/dragdrop.min.js @@ -0,0 +1,2 @@ +define ("core/local/reactive/dragdrop",["exports","core/local/reactive/basecomponent"],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){"@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 * Drag and drop helper component.\n *\n * This component is used to delegate drag and drop handling.\n *\n * To delegate the logic to this particular element the component should create a new instance\n * passing \"this\" as param. The component will use all the necessary callbacks and add all the\n * necessary listeners to the component element.\n *\n * Component attributes used by dragdrop module:\n * - element: the draggable or dropzone element.\n * - (optional) classes: object with alternative CSS classes\n * - (optional) fullregion: page element affeted by the elementy dragging. Use this attribute if\n * the draggable element affects a bigger region (for example a draggable\n * title).\n * - (optional) autoconfigDraggable: by default, the component will be draggable if it has a\n * getDraggableData method. If this value is false draggable\n * property must be defined using setDraggable method.\n * - (optional) relativeDrag: by default the drag image is located at point (0,0) relative to the\n * mouse position to prevent the mouse from covering it. If this attribute\n * is true the drag image will be located at the click offset.\n *\n * Methods the parent component should have for making it draggable:\n *\n * - getDraggableData(): Object|data\n * Return the data that will be passed to any valid dropzone while it is dragged.\n * If the component has this method, the dragdrop module will enable the dragging,\n * this is the only required method for dragging.\n * If at the dragging moment this method returns a false|null|undefined, the dragging\n * actions won't be captured.\n *\n * - (optional) dragStart(Object dropdata, Event event): void\n * - (optional) dragEnd(Object dropdata, Event event): void\n * Callbacks dragdrop will call when the element is dragged and getDraggableData\n * return some data.\n *\n * Methods the parent component should have for enabling it as a dropzone:\n *\n * - validateDropData(Object dropdata): boolean\n * If that method exists, the dragdrop module will automathically configure the element as dropzone.\n * This method will return true if the dropdata is accepted. In case it returns false, no drag and\n * drop event will be listened for this specific dragged dropdata.\n *\n * - (Optional) showDropZone(Object dropdata, Event event): void\n * - (Optional) hideDropZone(Object dropdata, Event event): void\n * Methods called when a valid dragged data pass over the element.\n *\n * - (Optional) drop(Object dropdata, Event event): void\n * Called when a valid dragged element is dropped over the element.\n *\n * Note that none of this methods will be called if validateDropData\n * returns a false value.\n *\n * This module will also add or remove several CSS classes from both dragged elements and dropzones.\n * See the \"this.classes\" in the create method for more details. In case the parent component wants\n * to use the same classes, it can use the getClasses method. On the other hand, if the parent\n * component has an alternative \"classes\" attribute, this will override the default drag and drop\n * classes.\n *\n * @module core/local/reactive/dragdrop\n * @class core/local/reactive/dragdrop\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/local/reactive/basecomponent';\n\n// Map with the dragged element generate by an specific reactive applications.\n// Potentially, any component can generate a draggable element to interact with other\n// page elements. However, the dragged data is specific and could only interact with\n// components of the same reactive instance.\nlet activeDropData = new Map();\n\n// Drag & Drop API provides the final drop point and incremental movements but we can\n// provide also starting points and displacements. Absolute displacements simplifies\n// moving components with aboslute position around the page.\nlet dragStartPoint = {};\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n *\n * @param {BaseComponent} parent the parent component.\n */\n create(parent) {\n // Optional component name for debugging.\n this.name = `${parent.name ?? 'unkown'}_dragdrop`;\n\n // Default drag and drop classes.\n this.classes = Object.assign(\n {\n // This class indicate a dragging action is active at a page level.\n BODYDRAGGING: 'dragging',\n\n // Added when draggable and drop are ready.\n DRAGGABLEREADY: 'draggable',\n DROPREADY: 'dropready',\n\n // When a valid drag element is over the element.\n DRAGOVER: 'dragover',\n // When a the component is dragged.\n DRAGGING: 'dragging',\n\n // Dropzones classes names.\n DROPUP: 'drop-up',\n DROPDOWN: 'drop-down',\n DROPZONE: 'drop-zone',\n\n // Drag icon class.\n DRAGICON: 'dragicon',\n },\n parent?.classes ?? {}\n );\n\n // Add the affected region if any.\n this.fullregion = parent.fullregion;\n\n // Keep parent to execute drap and drop handlers.\n this.parent = parent;\n\n // Check if parent handle draggable manually.\n this.autoconfigDraggable = this.parent.draggable ?? true;\n\n // Drag image relative position.\n this.relativeDrag = this.parent.relativeDrag ?? false;\n\n // Sub HTML elements will trigger extra dragEnter and dragOver all the time.\n // To prevent that from affecting dropzones, we need to count the enters and leaves.\n this.entercount = 0;\n\n // Stores if the droparea is shown or not.\n this.dropzonevisible = false;\n\n }\n\n /**\n * Return the component drag and drop CSS classes.\n *\n * @returns {Object} the dragdrop css classes\n */\n getClasses() {\n return this.classes;\n }\n\n /**\n * Initial state ready method.\n *\n * This method will add all the necessary event listeners to the component depending on the\n * parent methods.\n * - Add drop events to the element if the parent component has validateDropData method.\n * - Configure the elements draggable if the parent component has getDraggableData method.\n */\n stateReady() {\n // Add drop events to the element if the parent component has dropable types.\n if (typeof this.parent.validateDropData === 'function') {\n this.element.classList.add(this.classes.DROPREADY);\n this.addEventListener(this.element, 'dragenter', this._dragEnter);\n this.addEventListener(this.element, 'dragleave', this._dragLeave);\n this.addEventListener(this.element, 'dragover', this._dragOver);\n this.addEventListener(this.element, 'drop', this._drop);\n }\n\n // Configure the elements draggable if the parent component has dragable data.\n if (this.autoconfigDraggable && typeof this.parent.getDraggableData === 'function') {\n this.setDraggable(true);\n }\n }\n\n /**\n * Enable or disable the draggable property.\n *\n * @param {bool} value the new draggable value\n */\n setDraggable(value) {\n if (typeof this.parent.getDraggableData !== 'function') {\n throw new Error(`Draggable components must have a getDraggableData method`);\n }\n this.element.setAttribute('draggable', value);\n if (value) {\n this.addEventListener(this.element, 'dragstart', this._dragStart);\n this.addEventListener(this.element, 'dragend', this._dragEnd);\n this.element.classList.add(this.classes.DRAGGABLEREADY);\n } else {\n this.removeEventListener(this.element, 'dragstart', this._dragStart);\n this.removeEventListener(this.element, 'dragend', this._dragEnd);\n this.element.classList.remove(this.classes.DRAGGABLEREADY);\n }\n }\n\n /**\n * Drag start event handler.\n *\n * This method will generate the current dropable data. This data is the one used to determine\n * if a droparea accepts the dropping or not.\n *\n * @param {Event} event the event.\n */\n _dragStart(event) {\n const dropdata = this.parent.getDraggableData();\n if (!dropdata) {\n return;\n }\n\n // Save the starting point.\n dragStartPoint = {\n pageX: event.pageX,\n pageY: event.pageY,\n };\n\n // If the drag event is accepted we prevent any other draggable element from interfiering.\n event.stopPropagation();\n\n // Save the drop data of the current reactive intance.\n activeDropData.set(this.reactive, dropdata);\n\n // Add some CSS classes to indicate the state.\n document.body.classList.add(this.classes.BODYDRAGGING);\n this.element.classList.add(this.classes.DRAGGING);\n this.fullregion?.classList.add(this.classes.DRAGGING);\n\n // Force the drag image. This makes the UX more consistent in case the\n // user dragged an internal element like a link or some other element.\n let dragImage = this.element;\n if (this.parent.setDragImage !== undefined) {\n const customImage = this.parent.setDragImage(dropdata, event);\n if (customImage) {\n dragImage = customImage;\n }\n }\n // Define the image position relative to the mouse.\n const position = {x: 0, y: 0};\n if (this.relativeDrag) {\n position.x = event.offsetX;\n position.y = event.offsetY;\n }\n event.dataTransfer.setDragImage(dragImage, position.x, position.y);\n\n this._callParentMethod('dragStart', dropdata, event);\n }\n\n /**\n * Drag end event handler.\n *\n * @param {Event} event the event.\n */\n _dragEnd(event) {\n const dropdata = activeDropData.get(this.reactive);\n if (!dropdata) {\n return;\n }\n\n // Remove the current dropdata.\n activeDropData.delete(this.reactive);\n\n // Remove the dragging classes.\n document.body.classList.remove(this.classes.BODYDRAGGING);\n this.element.classList.remove(this.classes.DRAGGING);\n this.fullregion?.classList.remove(this.classes.DRAGGING);\n\n // We add the total movement to the event in case the component\n // wants to move its absolute position.\n this._addEventTotalMovement(event);\n\n this._callParentMethod('dragEnd', dropdata, event);\n }\n\n /**\n * Drag enter event handler.\n *\n * The JS drag&drop API triggers several dragenter events on the same element because it bubbles the\n * child events as well. To prevent this form affecting the dropzones display, this methods use\n * \"entercount\" to determine if it's one extra child event or a valid one.\n *\n * @param {Event} event the event.\n */\n _dragEnter(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount++;\n this.element.classList.add(this.classes.DRAGOVER);\n if (this.entercount == 1 && !this.dropzonevisible) {\n this.dropzonevisible = true;\n this.element.classList.add(this.classes.DRAGOVER);\n this._callParentMethod('showDropZone', dropdata, event);\n }\n }\n }\n\n /**\n * Drag over event handler.\n *\n * We only use dragover event when a draggable action starts inside a valid dropzone. In those cases\n * the API won't trigger any dragEnter because the dragged alement was already there. We use the\n * dropzonevisible to determine if the component needs to display the dropzones or not.\n *\n * @param {Event} event the event.\n */\n _dragOver(event) {\n const dropdata = this._processEvent(event);\n if (dropdata && !this.dropzonevisible) {\n this.dropzonevisible = true;\n this.element.classList.add(this.classes.DRAGOVER);\n this._callParentMethod('showDropZone', dropdata, event);\n }\n }\n\n /**\n * Drag over leave handler.\n *\n * The JS drag&drop API triggers several dragleave events on the same element because it bubbles the\n * child events as well. To prevent this form affecting the dropzones display, this methods use\n * \"entercount\" to determine if it's one extra child event or a valid one.\n *\n * @param {Event} event the event.\n */\n _dragLeave(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount--;\n if (this.entercount == 0 && this.dropzonevisible) {\n this.dropzonevisible = false;\n this.element.classList.remove(this.classes.DRAGOVER);\n this._callParentMethod('hideDropZone', dropdata, event);\n }\n }\n }\n\n /**\n * Drop event handler.\n *\n * This method will call both hideDropZones and drop methods on the parent component.\n *\n * @param {Event} event the event.\n */\n _drop(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount = 0;\n if (this.dropzonevisible) {\n this.dropzonevisible = false;\n this._callParentMethod('hideDropZone', dropdata, event);\n }\n this.element.classList.remove(this.classes.DRAGOVER);\n this._callParentMethod('drop', dropdata, event);\n // An accepted drop resets the initial position.\n // Save the starting point.\n dragStartPoint = {};\n }\n }\n\n /**\n * Process a drag and drop event and delegate logic to the parent component.\n *\n * @param {Event} event the drag and drop event\n * @return {Object|false} the dropdata or null if the event should not be processed\n */\n _processEvent(event) {\n const dropdata = this._getDropData(event);\n if (!dropdata) {\n return null;\n }\n if (this.parent.validateDropData(dropdata)) {\n // All accepted drag&drop event must prevent bubbling and defaults, otherwise\n // parent dragdrop instances could capture it by mistake.\n event.preventDefault();\n event.stopPropagation();\n this._addEventTotalMovement(event);\n return dropdata;\n }\n return null;\n }\n\n /**\n * Add the total amout of movement to a mouse event.\n *\n * @param {MouseEvent} event\n */\n _addEventTotalMovement(event) {\n if (dragStartPoint.pageX === undefined || event.pageX === undefined) {\n return;\n }\n event.fixedMovementX = event.pageX - dragStartPoint.pageX;\n event.fixedMovementY = event.pageY - dragStartPoint.pageY;\n event.initialPageX = dragStartPoint.pageX;\n event.initialPageY = dragStartPoint.pageY;\n // The element possible new top.\n const current = this.element.getBoundingClientRect();\n // Add the new position fixed position.\n event.newFixedTop = current.top + event.fixedMovementY;\n event.newFixedLeft = current.left + event.fixedMovementX;\n // The affected region possible new top.\n if (this.fullregion !== undefined) {\n const current = this.fullregion.getBoundingClientRect();\n event.newRegionFixedxTop = current.top + event.fixedMovementY;\n event.newRegionFixedxLeft = current.left + event.fixedMovementX;\n }\n }\n\n /**\n * Convenient method for calling parent component functions if present.\n *\n * @param {string} methodname the name of the method\n * @param {Object} dropdata the current drop data object\n * @param {Event} event the original event\n */\n _callParentMethod(methodname, dropdata, event) {\n if (typeof this.parent[methodname] === 'function') {\n this.parent[methodname](dropdata, event);\n }\n }\n\n /**\n * Get the current dropdata for a specific event.\n *\n * The browser can generate drag&drop events related to several user interactions:\n * - Drag a page elements: this case is registered in the activeDropData map\n * - Drag some HTML selections: ignored for now\n * - Drag a file over the browser: file drag may appear in the future but for now they are ignored.\n *\n * @param {Event} event the original event.\n * @returns {Object|undefined} with the dragged data (or undefined if none)\n */\n _getDropData(event) {\n if (this._containsFiles(event)) {\n return undefined;\n }\n return activeDropData.get(this.reactive);\n }\n\n /**\n * Check if the dragged event contains files.\n *\n * Files dragging does not generate drop data because they came from outsite the page and the component\n * must check it before validating the event.\n *\n * @param {Event} event the original event.\n * @returns {boolean} if the drag dataTransfers contains files.\n */\n _containsFiles(event) {\n if (event.dataTransfer.types) {\n for (var i = 0; i < event.dataTransfer.types.length; i++) {\n if (event.dataTransfer.types[i] == \"Files\") {\n return true;\n }\n }\n }\n return false;\n }\n}\n"],"file":"dragdrop.min.js"} \ No newline at end of file diff --git a/lib/amd/build/reactive.min.js b/lib/amd/build/reactive.min.js index 3c8bf71f586..2ec39b1b507 100644 --- a/lib/amd/build/reactive.min.js +++ b/lib/amd/build/reactive.min.js @@ -1,2 +1,2 @@ -define ("core/reactive",["exports","core/local/reactive/basecomponent","core/local/reactive/reactive"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"BaseComponent",{enumerable:!0,get:function get(){return b.default}});Object.defineProperty(a,"Reactive",{enumerable:!0,get:function get(){return c.default}});b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}}); +define ("core/reactive",["exports","core/local/reactive/basecomponent","core/local/reactive/reactive","core/local/reactive/dragdrop"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"BaseComponent",{enumerable:!0,get:function get(){return b.default}});Object.defineProperty(a,"Reactive",{enumerable:!0,get:function get(){return c.default}});Object.defineProperty(a,"DragDrop",{enumerable:!0,get:function get(){return d.default}});b=e(b);c=e(c);d=e(d);function e(a){return a&&a.__esModule?a:{default:a}}}); //# sourceMappingURL=reactive.min.js.map diff --git a/lib/amd/build/reactive.min.js.map b/lib/amd/build/reactive.min.js.map index 9444c6216ef..3956add0c16 100644 --- a/lib/amd/build/reactive.min.js.map +++ b/lib/amd/build/reactive.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/reactive.js"],"names":[],"mappings":"4WAuBA,OACA,O","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/reactive\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/local/reactive/basecomponent';\nimport Reactive from 'core/local/reactive/reactive';\n\nexport {Reactive, BaseComponent};\n"],"file":"reactive.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/reactive.js"],"names":[],"mappings":"seAuBA,OACA,OACA,O","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/reactive\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/local/reactive/basecomponent';\nimport Reactive from 'core/local/reactive/reactive';\nimport DragDrop from 'core/local/reactive/dragdrop';\n\nexport {Reactive, BaseComponent, DragDrop};\n"],"file":"reactive.min.js"} \ No newline at end of file diff --git a/lib/amd/src/local/reactive/dragdrop.js b/lib/amd/src/local/reactive/dragdrop.js new file mode 100644 index 00000000000..e1ce8fdf449 --- /dev/null +++ b/lib/amd/src/local/reactive/dragdrop.js @@ -0,0 +1,465 @@ +// 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 . + +/** + * Drag and drop helper component. + * + * This component is used to delegate drag and drop handling. + * + * To delegate the logic to this particular element the component should create a new instance + * passing "this" as param. The component will use all the necessary callbacks and add all the + * necessary listeners to the component element. + * + * Component attributes used by dragdrop module: + * - element: the draggable or dropzone element. + * - (optional) classes: object with alternative CSS classes + * - (optional) fullregion: page element affeted by the elementy dragging. Use this attribute if + * the draggable element affects a bigger region (for example a draggable + * title). + * - (optional) autoconfigDraggable: by default, the component will be draggable if it has a + * getDraggableData method. If this value is false draggable + * property must be defined using setDraggable method. + * - (optional) relativeDrag: by default the drag image is located at point (0,0) relative to the + * mouse position to prevent the mouse from covering it. If this attribute + * is true the drag image will be located at the click offset. + * + * Methods the parent component should have for making it draggable: + * + * - getDraggableData(): Object|data + * Return the data that will be passed to any valid dropzone while it is dragged. + * If the component has this method, the dragdrop module will enable the dragging, + * this is the only required method for dragging. + * If at the dragging moment this method returns a false|null|undefined, the dragging + * actions won't be captured. + * + * - (optional) dragStart(Object dropdata, Event event): void + * - (optional) dragEnd(Object dropdata, Event event): void + * Callbacks dragdrop will call when the element is dragged and getDraggableData + * return some data. + * + * Methods the parent component should have for enabling it as a dropzone: + * + * - validateDropData(Object dropdata): boolean + * If that method exists, the dragdrop module will automathically configure the element as dropzone. + * This method will return true if the dropdata is accepted. In case it returns false, no drag and + * drop event will be listened for this specific dragged dropdata. + * + * - (Optional) showDropZone(Object dropdata, Event event): void + * - (Optional) hideDropZone(Object dropdata, Event event): void + * Methods called when a valid dragged data pass over the element. + * + * - (Optional) drop(Object dropdata, Event event): void + * Called when a valid dragged element is dropped over the element. + * + * Note that none of this methods will be called if validateDropData + * returns a false value. + * + * This module will also add or remove several CSS classes from both dragged elements and dropzones. + * See the "this.classes" in the create method for more details. In case the parent component wants + * to use the same classes, it can use the getClasses method. On the other hand, if the parent + * component has an alternative "classes" attribute, this will override the default drag and drop + * classes. + * + * @module core/local/reactive/dragdrop + * @class core/local/reactive/dragdrop + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import BaseComponent from 'core/local/reactive/basecomponent'; + +// Map with the dragged element generate by an specific reactive applications. +// Potentially, any component can generate a draggable element to interact with other +// page elements. However, the dragged data is specific and could only interact with +// components of the same reactive instance. +let activeDropData = new Map(); + +// Drag & Drop API provides the final drop point and incremental movements but we can +// provide also starting points and displacements. Absolute displacements simplifies +// moving components with aboslute position around the page. +let dragStartPoint = {}; + +export default class extends BaseComponent { + + /** + * Constructor hook. + * + * @param {BaseComponent} parent the parent component. + */ + create(parent) { + // Optional component name for debugging. + this.name = `${parent.name ?? 'unkown'}_dragdrop`; + + // Default drag and drop classes. + this.classes = Object.assign( + { + // This class indicate a dragging action is active at a page level. + BODYDRAGGING: 'dragging', + + // Added when draggable and drop are ready. + DRAGGABLEREADY: 'draggable', + DROPREADY: 'dropready', + + // When a valid drag element is over the element. + DRAGOVER: 'dragover', + // When a the component is dragged. + DRAGGING: 'dragging', + + // Dropzones classes names. + DROPUP: 'drop-up', + DROPDOWN: 'drop-down', + DROPZONE: 'drop-zone', + + // Drag icon class. + DRAGICON: 'dragicon', + }, + parent?.classes ?? {} + ); + + // Add the affected region if any. + this.fullregion = parent.fullregion; + + // Keep parent to execute drap and drop handlers. + this.parent = parent; + + // Check if parent handle draggable manually. + this.autoconfigDraggable = this.parent.draggable ?? true; + + // Drag image relative position. + this.relativeDrag = this.parent.relativeDrag ?? false; + + // Sub HTML elements will trigger extra dragEnter and dragOver all the time. + // To prevent that from affecting dropzones, we need to count the enters and leaves. + this.entercount = 0; + + // Stores if the droparea is shown or not. + this.dropzonevisible = false; + + } + + /** + * Return the component drag and drop CSS classes. + * + * @returns {Object} the dragdrop css classes + */ + getClasses() { + return this.classes; + } + + /** + * Initial state ready method. + * + * This method will add all the necessary event listeners to the component depending on the + * parent methods. + * - Add drop events to the element if the parent component has validateDropData method. + * - Configure the elements draggable if the parent component has getDraggableData method. + */ + stateReady() { + // Add drop events to the element if the parent component has dropable types. + if (typeof this.parent.validateDropData === 'function') { + this.element.classList.add(this.classes.DROPREADY); + this.addEventListener(this.element, 'dragenter', this._dragEnter); + this.addEventListener(this.element, 'dragleave', this._dragLeave); + this.addEventListener(this.element, 'dragover', this._dragOver); + this.addEventListener(this.element, 'drop', this._drop); + } + + // Configure the elements draggable if the parent component has dragable data. + if (this.autoconfigDraggable && typeof this.parent.getDraggableData === 'function') { + this.setDraggable(true); + } + } + + /** + * Enable or disable the draggable property. + * + * @param {bool} value the new draggable value + */ + setDraggable(value) { + if (typeof this.parent.getDraggableData !== 'function') { + throw new Error(`Draggable components must have a getDraggableData method`); + } + this.element.setAttribute('draggable', value); + if (value) { + this.addEventListener(this.element, 'dragstart', this._dragStart); + this.addEventListener(this.element, 'dragend', this._dragEnd); + this.element.classList.add(this.classes.DRAGGABLEREADY); + } else { + this.removeEventListener(this.element, 'dragstart', this._dragStart); + this.removeEventListener(this.element, 'dragend', this._dragEnd); + this.element.classList.remove(this.classes.DRAGGABLEREADY); + } + } + + /** + * Drag start event handler. + * + * This method will generate the current dropable data. This data is the one used to determine + * if a droparea accepts the dropping or not. + * + * @param {Event} event the event. + */ + _dragStart(event) { + const dropdata = this.parent.getDraggableData(); + if (!dropdata) { + return; + } + + // Save the starting point. + dragStartPoint = { + pageX: event.pageX, + pageY: event.pageY, + }; + + // If the drag event is accepted we prevent any other draggable element from interfiering. + event.stopPropagation(); + + // Save the drop data of the current reactive intance. + activeDropData.set(this.reactive, dropdata); + + // Add some CSS classes to indicate the state. + document.body.classList.add(this.classes.BODYDRAGGING); + this.element.classList.add(this.classes.DRAGGING); + this.fullregion?.classList.add(this.classes.DRAGGING); + + // Force the drag image. This makes the UX more consistent in case the + // user dragged an internal element like a link or some other element. + let dragImage = this.element; + if (this.parent.setDragImage !== undefined) { + const customImage = this.parent.setDragImage(dropdata, event); + if (customImage) { + dragImage = customImage; + } + } + // Define the image position relative to the mouse. + const position = {x: 0, y: 0}; + if (this.relativeDrag) { + position.x = event.offsetX; + position.y = event.offsetY; + } + event.dataTransfer.setDragImage(dragImage, position.x, position.y); + + this._callParentMethod('dragStart', dropdata, event); + } + + /** + * Drag end event handler. + * + * @param {Event} event the event. + */ + _dragEnd(event) { + const dropdata = activeDropData.get(this.reactive); + if (!dropdata) { + return; + } + + // Remove the current dropdata. + activeDropData.delete(this.reactive); + + // Remove the dragging classes. + document.body.classList.remove(this.classes.BODYDRAGGING); + this.element.classList.remove(this.classes.DRAGGING); + this.fullregion?.classList.remove(this.classes.DRAGGING); + + // We add the total movement to the event in case the component + // wants to move its absolute position. + this._addEventTotalMovement(event); + + this._callParentMethod('dragEnd', dropdata, event); + } + + /** + * Drag enter event handler. + * + * The JS drag&drop API triggers several dragenter events on the same element because it bubbles the + * child events as well. To prevent this form affecting the dropzones display, this methods use + * "entercount" to determine if it's one extra child event or a valid one. + * + * @param {Event} event the event. + */ + _dragEnter(event) { + const dropdata = this._processEvent(event); + if (dropdata) { + this.entercount++; + this.element.classList.add(this.classes.DRAGOVER); + if (this.entercount == 1 && !this.dropzonevisible) { + this.dropzonevisible = true; + this.element.classList.add(this.classes.DRAGOVER); + this._callParentMethod('showDropZone', dropdata, event); + } + } + } + + /** + * Drag over event handler. + * + * We only use dragover event when a draggable action starts inside a valid dropzone. In those cases + * the API won't trigger any dragEnter because the dragged alement was already there. We use the + * dropzonevisible to determine if the component needs to display the dropzones or not. + * + * @param {Event} event the event. + */ + _dragOver(event) { + const dropdata = this._processEvent(event); + if (dropdata && !this.dropzonevisible) { + this.dropzonevisible = true; + this.element.classList.add(this.classes.DRAGOVER); + this._callParentMethod('showDropZone', dropdata, event); + } + } + + /** + * Drag over leave handler. + * + * The JS drag&drop API triggers several dragleave events on the same element because it bubbles the + * child events as well. To prevent this form affecting the dropzones display, this methods use + * "entercount" to determine if it's one extra child event or a valid one. + * + * @param {Event} event the event. + */ + _dragLeave(event) { + const dropdata = this._processEvent(event); + if (dropdata) { + this.entercount--; + if (this.entercount == 0 && this.dropzonevisible) { + this.dropzonevisible = false; + this.element.classList.remove(this.classes.DRAGOVER); + this._callParentMethod('hideDropZone', dropdata, event); + } + } + } + + /** + * Drop event handler. + * + * This method will call both hideDropZones and drop methods on the parent component. + * + * @param {Event} event the event. + */ + _drop(event) { + const dropdata = this._processEvent(event); + if (dropdata) { + this.entercount = 0; + if (this.dropzonevisible) { + this.dropzonevisible = false; + this._callParentMethod('hideDropZone', dropdata, event); + } + this.element.classList.remove(this.classes.DRAGOVER); + this._callParentMethod('drop', dropdata, event); + // An accepted drop resets the initial position. + // Save the starting point. + dragStartPoint = {}; + } + } + + /** + * Process a drag and drop event and delegate logic to the parent component. + * + * @param {Event} event the drag and drop event + * @return {Object|false} the dropdata or null if the event should not be processed + */ + _processEvent(event) { + const dropdata = this._getDropData(event); + if (!dropdata) { + return null; + } + if (this.parent.validateDropData(dropdata)) { + // All accepted drag&drop event must prevent bubbling and defaults, otherwise + // parent dragdrop instances could capture it by mistake. + event.preventDefault(); + event.stopPropagation(); + this._addEventTotalMovement(event); + return dropdata; + } + return null; + } + + /** + * Add the total amout of movement to a mouse event. + * + * @param {MouseEvent} event + */ + _addEventTotalMovement(event) { + if (dragStartPoint.pageX === undefined || event.pageX === undefined) { + return; + } + event.fixedMovementX = event.pageX - dragStartPoint.pageX; + event.fixedMovementY = event.pageY - dragStartPoint.pageY; + event.initialPageX = dragStartPoint.pageX; + event.initialPageY = dragStartPoint.pageY; + // The element possible new top. + const current = this.element.getBoundingClientRect(); + // Add the new position fixed position. + event.newFixedTop = current.top + event.fixedMovementY; + event.newFixedLeft = current.left + event.fixedMovementX; + // The affected region possible new top. + if (this.fullregion !== undefined) { + const current = this.fullregion.getBoundingClientRect(); + event.newRegionFixedxTop = current.top + event.fixedMovementY; + event.newRegionFixedxLeft = current.left + event.fixedMovementX; + } + } + + /** + * Convenient method for calling parent component functions if present. + * + * @param {string} methodname the name of the method + * @param {Object} dropdata the current drop data object + * @param {Event} event the original event + */ + _callParentMethod(methodname, dropdata, event) { + if (typeof this.parent[methodname] === 'function') { + this.parent[methodname](dropdata, event); + } + } + + /** + * Get the current dropdata for a specific event. + * + * The browser can generate drag&drop events related to several user interactions: + * - Drag a page elements: this case is registered in the activeDropData map + * - Drag some HTML selections: ignored for now + * - Drag a file over the browser: file drag may appear in the future but for now they are ignored. + * + * @param {Event} event the original event. + * @returns {Object|undefined} with the dragged data (or undefined if none) + */ + _getDropData(event) { + if (this._containsFiles(event)) { + return undefined; + } + return activeDropData.get(this.reactive); + } + + /** + * Check if the dragged event contains files. + * + * Files dragging does not generate drop data because they came from outsite the page and the component + * must check it before validating the event. + * + * @param {Event} event the original event. + * @returns {boolean} if the drag dataTransfers contains files. + */ + _containsFiles(event) { + if (event.dataTransfer.types) { + for (var i = 0; i < event.dataTransfer.types.length; i++) { + if (event.dataTransfer.types[i] == "Files") { + return true; + } + } + } + return false; + } +} diff --git a/lib/amd/src/reactive.js b/lib/amd/src/reactive.js index ac621b8e841..317f3ca7601 100644 --- a/lib/amd/src/reactive.js +++ b/lib/amd/src/reactive.js @@ -23,5 +23,6 @@ import BaseComponent from 'core/local/reactive/basecomponent'; import Reactive from 'core/local/reactive/reactive'; +import DragDrop from 'core/local/reactive/dragdrop'; -export {Reactive, BaseComponent}; +export {Reactive, BaseComponent, DragDrop}; diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index fe42c6447ea..8d9705a5c81 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -5,6 +5,8 @@ // want white default colour. $bg-inverse-link-color: #fff !default; +$dropzone-border: $gray-900 !default; + $font-size-xs: ($font-size-base * .75) !default; #region-main { @@ -2784,3 +2786,32 @@ $scrollbar-track: lighten($primary, 40%); border-right: $border-width solid $white; } } +// Generic dropzones and dragging styles. + +body.dragging { + + .drop-zone { + border: 2px dashed $dropzone-border; + } + + .drop-up { + border-top: 2px solid $dropzone-border; + } + + .drop-down { + border-bottom: 2px solid $dropzone-border; + } + + .dragging { + opacity: .6; + } +} + +.dragicon { + visibility: hidden; +} + +.draggable:hover .dragicon { + visibility: visible; + cursor: move; +} diff --git a/theme/boost/scss/moodle/courseindex.scss b/theme/boost/scss/moodle/courseindex.scss index 33e32e764c8..f14faa3b303 100644 --- a/theme/boost/scss/moodle/courseindex.scss +++ b/theme/boost/scss/moodle/courseindex.scss @@ -56,8 +56,15 @@ $courseindex-item-current: $primary !default; } } - .courseindex-section.current { - border-left: solid 3px $courseindex-item-current; + .courseindex-section { + &.current { + border-left: solid 3px $courseindex-item-current; + } + + &.dropready .courseindex-item-content { + /* Extra dropzone space */ + padding-bottom: 1em; + } } .d-flex-noedit { diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index cc89d9003ce..2af2b498a48 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -11944,6 +11944,25 @@ input[disabled] { background-color: #a8d2f8; border-right: 1px solid #fff; } +body.dragging .drop-zone { + border: 2px dashed #212529; } + +body.dragging .drop-up { + border-top: 2px solid #212529; } + +body.dragging .drop-down { + border-bottom: 2px solid #212529; } + +body.dragging .dragging { + opacity: .6; } + +.dragicon { + visibility: hidden; } + +.draggable:hover .dragicon { + visibility: visible; + cursor: move; } + .icon { font-size: 16px; width: 16px; @@ -20124,6 +20143,10 @@ div.editor_atto_toolbar button .icon { .courseindex .courseindex-section.current { border-left: solid 3px #0f6fc5; } +.courseindex .courseindex-section.dropready .courseindex-item-content { + /* Extra dropzone space */ + padding-bottom: 1em; } + .courseindex .d-flex-noedit { display: none; } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 75bd52a5002..bae611db3a9 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -12165,6 +12165,25 @@ input[disabled] { background-color: #a8d2f8; border-right: 1px solid #fff; } +body.dragging .drop-zone { + border: 2px dashed #212529; } + +body.dragging .drop-up { + border-top: 2px solid #212529; } + +body.dragging .drop-down { + border-bottom: 2px solid #212529; } + +body.dragging .dragging { + opacity: .6; } + +.dragicon { + visibility: hidden; } + +.draggable:hover .dragicon { + visibility: visible; + cursor: move; } + .icon { font-size: 16px; width: 16px; @@ -20316,6 +20335,10 @@ div.editor_atto_toolbar button .icon { .courseindex .courseindex-section.current { border-left: solid 3px #0f6fc5; } +.courseindex .courseindex-section.dropready .courseindex-item-content { + /* Extra dropzone space */ + padding-bottom: 1em; } + .courseindex .d-flex-noedit { display: none; }