From 9930b7a2e6cb6fe5aaaa118cb6bbe28030193140 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Fri, 13 Jan 2023 12:52:50 +0100 Subject: [PATCH] MDL-76783 core_courseformat: add bulk editing interface --- .../local/content/bulkedittoggler.min.js | 11 + .../local/content/bulkedittoggler.min.js.map | 1 + .../build/local/content/bulkedittools.min.js | 11 + .../local/content/bulkedittools.min.js.map | 1 + .../build/local/content/section/cmitem.min.js | 2 +- .../local/content/section/cmitem.min.js.map | 2 +- .../build/local/content/section/header.min.js | 2 +- .../local/content/section/header.min.js.map | 2 +- .../build/local/courseeditor/dndcmitem.min.js | 2 +- .../local/courseeditor/dndcmitem.min.js.map | 2 +- .../local/courseeditor/dndsectionitem.min.js | 2 +- .../courseeditor/dndsectionitem.min.js.map | 2 +- .../amd/src/local/content/bulkedittoggler.js | 115 ++++++++ .../amd/src/local/content/bulkedittools.js | 247 ++++++++++++++++++ .../amd/src/local/content/section/cmitem.js | 109 +++++++- .../amd/src/local/content/section/header.js | 100 ++++++- .../amd/src/local/courseeditor/dndcmitem.js | 9 + .../src/local/courseeditor/dndsectionitem.js | 11 + .../format/classes/output/local/content.php | 9 + .../output/local/content/bulkedittoggler.php | 62 +++++ .../output/local/content/bulkedittools.php | 101 +++++++ .../classes/output/local/content/cm.php | 1 + .../classes/output/section_renderer.php | 14 + .../format/templates/local/content.mustache | 29 +- .../local/content/bulkedittoggler.mustache | 37 +++ .../local/content/bulkedittools.mustache | 92 +++++++ .../templates/local/content/cm.mustache | 3 + .../local/content/cm/bulkselect.mustache | 39 +++ .../local/content/section/bulkselect.mustache | 39 +++ .../local/content/section/header.mustache | 3 + course/format/tests/behat/bulk_select.feature | 118 +++++++++ course/format/upgrade.txt | 6 + course/view.php | 8 +- lang/en/courseformat.php | 7 + theme/boost/scss/moodle/course.scss | 20 +- theme/boost/style/moodle.css | 23 +- theme/classic/style/moodle.css | 23 +- 37 files changed, 1235 insertions(+), 30 deletions(-) create mode 100644 course/format/amd/build/local/content/bulkedittoggler.min.js create mode 100644 course/format/amd/build/local/content/bulkedittoggler.min.js.map create mode 100644 course/format/amd/build/local/content/bulkedittools.min.js create mode 100644 course/format/amd/build/local/content/bulkedittools.min.js.map create mode 100644 course/format/amd/src/local/content/bulkedittoggler.js create mode 100644 course/format/amd/src/local/content/bulkedittools.js create mode 100644 course/format/classes/output/local/content/bulkedittoggler.php create mode 100644 course/format/classes/output/local/content/bulkedittools.php create mode 100644 course/format/templates/local/content/bulkedittoggler.mustache create mode 100644 course/format/templates/local/content/bulkedittools.mustache create mode 100644 course/format/templates/local/content/cm/bulkselect.mustache create mode 100644 course/format/templates/local/content/section/bulkselect.mustache create mode 100644 course/format/tests/behat/bulk_select.feature diff --git a/course/format/amd/build/local/content/bulkedittoggler.min.js b/course/format/amd/build/local/content/bulkedittoggler.min.js new file mode 100644 index 00000000000..e34fbe676ec --- /dev/null +++ b/course/format/amd/build/local/content/bulkedittoggler.min.js @@ -0,0 +1,11 @@ +define("core_courseformat/local/content/bulkedittoggler",["exports","core/reactive","core_courseformat/courseeditor","core/pending"],(function(_exports,_reactive,_courseeditor,_pending){var obj; +/** + * The bulk editor toggler button control. + * + * @module core_courseformat/local/content/bulkedittoggler + * @class core_courseformat/local/content/bulkedittoggler + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};class Component extends _reactive.BaseComponent{create(){this.name="bulk_editor_toogler",this.selectors={BODY:"body",SELECTABLE:"[data-bulkcheckbox][data-is-selectable]"},this.classes={HIDDEN:"d-none",BULK:"bulkenabled"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}stateReady(){this.addEventListener(this.element,"click",this._enableBulk)}getWatchers(){return[{watch:"bulk.enabled:updated",handler:this._refreshToggler}]}_refreshToggler(_ref){var _element$enabled,_document$querySelect;let{element:element}=_ref;this.element.classList.toggle(this.classes.HIDDEN,null!==(_element$enabled=element.enabled)&&void 0!==_element$enabled&&_element$enabled),null===(_document$querySelect=document.querySelector(this.selectors.BODY))||void 0===_document$querySelect||_document$querySelect.classList.toggle(this.classes.BULK,element.enabled)}_enableBulk(){const pendingToggle=new _pending.default("courseformat/content:bulktoggle_on");this.reactive.dispatch("bulkEnable",!0),setTimeout((()=>{var _document$querySelect2;null===(_document$querySelect2=document.querySelector(this.selectors.SELECTABLE))||void 0===_document$querySelect2||_document$querySelect2.focus(),pendingToggle.resolve()}),150)}}return _exports.default=Component,_exports.default})); + +//# sourceMappingURL=bulkedittoggler.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/content/bulkedittoggler.min.js.map b/course/format/amd/build/local/content/bulkedittoggler.min.js.map new file mode 100644 index 00000000000..0b95717fcbc --- /dev/null +++ b/course/format/amd/build/local/content/bulkedittoggler.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"bulkedittoggler.min.js","sources":["../../../src/local/content/bulkedittoggler.js"],"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 * The bulk editor toggler button control.\n *\n * @module core_courseformat/local/content/bulkedittoggler\n * @class core_courseformat/local/content/bulkedittoggler\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport Pending from 'core/pending';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'bulk_editor_toogler';\n // Default query selectors.\n this.selectors = {\n BODY: `body`,\n SELECTABLE: `[data-bulkcheckbox][data-is-selectable]`,\n };\n // Component css classes.\n this.classes = {\n HIDDEN: `d-none`,\n BULK: `bulkenabled`,\n };\n }\n\n /**\n * Static method to create a component instance from the mustache template.\n *\n * @param {string} target optional altentative DOM main element CSS selector\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: getCurrentCourseEditor(),\n selectors\n });\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n // Capture completion events.\n this.addEventListener(\n this.element,\n 'click',\n this._enableBulk\n );\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `bulk.enabled:updated`, handler: this._refreshToggler},\n ];\n }\n\n /**\n * Update a content section using the state information.\n *\n * @param {object} param\n * @param {Object} param.element details the update details (state.bulk in this case).\n */\n _refreshToggler({element}) {\n this.element.classList.toggle(this.classes.HIDDEN, element.enabled ?? false);\n document.querySelector(this.selectors.BODY)?.classList.toggle(this.classes.BULK, element.enabled);\n }\n\n /**\n * Dispatch the enable bulk mutation.\n *\n * The enable bulk button is outside of the course content main div.\n * Because content/actions captures click events only in the course\n * content, this button needs to trigger the enable bulk mutation\n * by itself.\n */\n _enableBulk() {\n const pendingToggle = new Pending(`courseformat/content:bulktoggle_on`);\n this.reactive.dispatch('bulkEnable', true);\n // Wait for a while and focus on the first checkbox.\n setTimeout(() => {\n document.querySelector(this.selectors.SELECTABLE)?.focus();\n pendingToggle.resolve();\n }, 150);\n }\n}\n"],"names":["Component","BaseComponent","create","name","selectors","BODY","SELECTABLE","classes","HIDDEN","BULK","target","this","element","document","querySelector","reactive","stateReady","addEventListener","_enableBulk","getWatchers","watch","handler","_refreshToggler","classList","toggle","enabled","pendingToggle","Pending","dispatch","setTimeout","focus","resolve"],"mappings":";;;;;;;;qJA4BqBA,kBAAkBC,wBAKnCC,cAESC,KAAO,2BAEPC,UAAY,CACbC,YACAC,2DAGCC,QAAU,CACXC,gBACAC,gCAWIC,OAAQN,kBACT,IAAIO,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,UAAU,0CACVX,UAAAA,YAORY,kBAESC,iBACDN,KAAKC,QACL,QACAD,KAAKO,aASbC,oBACW,CACH,CAACC,6BAA+BC,QAASV,KAAKW,kBAUtDA,qEAAgBV,QAACA,mBACRA,QAAQW,UAAUC,OAAOb,KAAKJ,QAAQC,gCAAQI,QAAQa,qFAC3DZ,SAASC,cAAcH,KAAKP,UAAUC,8DAAOkB,UAAUC,OAAOb,KAAKJ,QAAQE,KAAMG,QAAQa,SAW7FP,oBACUQ,cAAgB,IAAIC,4DACrBZ,SAASa,SAAS,cAAc,GAErCC,YAAW,+DACPhB,SAASC,cAAcH,KAAKP,UAAUE,sEAAawB,QACnDJ,cAAcK,YACf"} \ No newline at end of file diff --git a/course/format/amd/build/local/content/bulkedittools.min.js b/course/format/amd/build/local/content/bulkedittools.min.js new file mode 100644 index 00000000000..8c1323f39d8 --- /dev/null +++ b/course/format/amd/build/local/content/bulkedittools.min.js @@ -0,0 +1,11 @@ +define("core_courseformat/local/content/bulkedittools",["exports","core/reactive","core/sticky-footer","core_courseformat/courseeditor","core/str","core/pending","core/prefetch"],(function(_exports,_reactive,_stickyFooter,_courseeditor,_str,_pending,_prefetch){var obj; +/** + * The bulk editor tools bar. + * + * @module core_courseformat/local/content/bulkedittools + * @class core_courseformat/local/content/bulkedittools + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj},(0,_prefetch.prefetchStrings)("core_courseformat",["bulkselection"]);class Component extends _reactive.BaseComponent{create(){this.name="bulk_editor_tools",this.selectors={ACTIONS:'[data-for="bulkaction"]',ACTIONTOOL:'[data-for="bulkactions"] li',CANCEL:'[data-for="bulkcancel"]',COUNT:"[data-for='bulkcount']",SELECTABLE:"[data-bulkcheckbox][data-is-selectable]",SELECTALL:'[data-for="selectall"]',BULKBTN:'[data-for="enableBulk"]'},this.classes={HIDE:"d-none",DISABLED:"disabled"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}stateReady(){const cancelBtn=this.getElement(this.selectors.CANCEL);cancelBtn&&this.addEventListener(cancelBtn,"click",this._cancelBulk);const selectAll=this.getElement(this.selectors.SELECTALL);selectAll&&this.addEventListener(selectAll,"change",this._selectAllClick)}getWatchers(){return[{watch:"bulk.enabled:updated",handler:this._refreshEnabled},{watch:"bulk:updated",handler:this._refreshTools}]}_refreshEnabled(_ref){let{element:element}=_ref;element.enabled?(0,_stickyFooter.enableStickyFooter)():(0,_stickyFooter.disableStickyFooter)()}_refreshTools(param){this._refreshSelectCount(param),this._refreshSelectAll(param),this._refreshActions(param)}async _refreshSelectCount(_ref2){let{element:bulk}=_ref2;const selectedCount=await(0,_str.get_string)("bulkselection","core_courseformat",bulk.selection.length),selectedElement=this.getElement(this.selectors.COUNT);selectedElement&&(selectedElement.innerHTML=selectedCount)}_refreshSelectAll(_ref3){let{element:bulk}=_ref3;const selectall=this.getElement(this.selectors.SELECTALL);if(!selectall)return;if(""===bulk.selectedType)return selectall.checked=!1,void(selectall.disabled=!0);selectall.disabled=!1;const maxSelection=document.querySelectorAll(this.selectors.SELECTABLE).length;selectall.checked=bulk.selection.length==maxSelection}_refreshActions(_ref4){let{element:bulk}=_ref4;const displayType="section"==bulk.selectedType?"section":"cm",enabled=""!==bulk.selectedType;this.getElements(this.selectors.ACTIONS).forEach((action=>{action.classList.toggle(this.classes.DISABLED,!enabled);const actionTool=action.closest(this.selectors.ACTIONTOOL),isHidden=action.dataset.bulk!=displayType;null==actionTool||actionTool.classList.toggle(this.classes.HIDE,isHidden)}))}_cancelBulk(){const pending=new _pending.default("courseformat/content:bulktoggle_off");this.reactive.dispatch("bulkEnable",!1),setTimeout((()=>{var _document$querySelect;null===(_document$querySelect=document.querySelector(this.selectors.BULKBTN))||void 0===_document$querySelect||_document$querySelect.focus(),pending.resolve()}),150)}_selectAllClick(event){const target=event.target,bulk=this.reactive.get("bulk");""!==bulk.selectedType&&(target.checked?this._handleSelectAll(bulk):this._handleUnselectAll())}_handleUnselectAll(){const pending=new _pending.default("courseformat/content:bulktUnselectAll");this.reactive.dispatch("bulkEnable",!0),setTimeout((()=>{var _document$querySelect2;null===(_document$querySelect2=document.querySelector(this.selectors.SELECTABLE))||void 0===_document$querySelect2||_document$querySelect2.focus(),pending.resolve()}),150)}_handleSelectAll(bulk){const selectableIds=[],selectables=document.querySelectorAll(this.selectors.SELECTABLE);if(0==selectables.length)return;selectables.forEach((selectable=>{selectableIds.push(selectable.dataset.id)}));const mutation="cm"===bulk.selectedType?"cmSelect":"sectionSelect";this.reactive.dispatch(mutation,selectableIds)}}return _exports.default=Component,_exports.default})); + +//# sourceMappingURL=bulkedittools.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/content/bulkedittools.min.js.map b/course/format/amd/build/local/content/bulkedittools.min.js.map new file mode 100644 index 00000000000..35b2b824b8e --- /dev/null +++ b/course/format/amd/build/local/content/bulkedittools.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"bulkedittools.min.js","sources":["../../../src/local/content/bulkedittools.js"],"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 * The bulk editor tools bar.\n *\n * @module core_courseformat/local/content/bulkedittools\n * @class core_courseformat/local/content/bulkedittools\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {disableStickyFooter, enableStickyFooter} from 'core/sticky-footer';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport {get_string as getString} from 'core/str';\nimport Pending from 'core/pending';\nimport {prefetchStrings} from 'core/prefetch';\n\n// Load global strings.\nprefetchStrings(\n 'core_courseformat',\n ['bulkselection']\n);\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'bulk_editor_tools';\n // Default query selectors.\n this.selectors = {\n ACTIONS: `[data-for=\"bulkaction\"]`,\n ACTIONTOOL: `[data-for=\"bulkactions\"] li`,\n CANCEL: `[data-for=\"bulkcancel\"]`,\n COUNT: `[data-for='bulkcount']`,\n SELECTABLE: `[data-bulkcheckbox][data-is-selectable]`,\n SELECTALL: `[data-for=\"selectall\"]`,\n BULKBTN: `[data-for=\"enableBulk\"]`,\n };\n // Most classes will be loaded later by DndCmItem.\n this.classes = {\n HIDE: 'd-none',\n DISABLED: 'disabled',\n };\n }\n\n /**\n * Static method to create a component instance from the mustache template.\n *\n * @param {string} target optional altentative DOM main element CSS selector\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: getCurrentCourseEditor(),\n selectors\n });\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n const cancelBtn = this.getElement(this.selectors.CANCEL);\n if (cancelBtn) {\n this.addEventListener(cancelBtn, 'click', this._cancelBulk);\n }\n const selectAll = this.getElement(this.selectors.SELECTALL);\n if (selectAll) {\n this.addEventListener(selectAll, 'change', this._selectAllClick);\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `bulk.enabled:updated`, handler: this._refreshEnabled},\n {watch: `bulk:updated`, handler: this._refreshTools},\n ];\n }\n\n /**\n * Hide and show the bulk edit tools.\n *\n * @param {object} param\n * @param {Object} param.element details the update details (state.bulk in this case).\n */\n _refreshEnabled({element}) {\n if (element.enabled) {\n enableStickyFooter();\n } else {\n disableStickyFooter();\n }\n }\n\n /**\n * Refresh the tools depending on the current selection.\n *\n * @param {object} param the state watcher information\n * @param {Object} param.state the full state data.\n * @param {Object} param.element the affected element (bulk in this case).\n */\n _refreshTools(param) {\n this._refreshSelectCount(param);\n this._refreshSelectAll(param);\n this._refreshActions(param);\n }\n\n /**\n * Refresh the selection count.\n *\n * @param {object} param\n * @param {Object} param.element the affected element (bulk in this case).\n */\n async _refreshSelectCount({element: bulk}) {\n const selectedCount = await getString('bulkselection', 'core_courseformat', bulk.selection.length);\n const selectedElement = this.getElement(this.selectors.COUNT);\n if (selectedElement) {\n selectedElement.innerHTML = selectedCount;\n }\n }\n\n /**\n * Refresh the select all element.\n *\n * @param {object} param\n * @param {Object} param.element the affected element (bulk in this case).\n */\n _refreshSelectAll({element: bulk}) {\n const selectall = this.getElement(this.selectors.SELECTALL);\n if (!selectall) {\n return;\n }\n if (bulk.selectedType === '') {\n selectall.checked = false;\n selectall.disabled = true;\n return;\n }\n\n selectall.disabled = false;\n const maxSelection = document.querySelectorAll(this.selectors.SELECTABLE).length;\n selectall.checked = (bulk.selection.length == maxSelection);\n }\n\n /**\n * Refresh the visible action buttons depending on the selection type.\n *\n * @param {object} param\n * @param {Object} param.element the affected element (bulk in this case).\n */\n _refreshActions({element: bulk}) {\n // By default, we show the cm options.\n const displayType = (bulk.selectedType == 'section') ? 'section' : 'cm';\n const enabled = (bulk.selectedType !== '');\n this.getElements(this.selectors.ACTIONS).forEach(action => {\n action.classList.toggle(this.classes.DISABLED, !enabled);\n\n const actionTool = action.closest(this.selectors.ACTIONTOOL);\n const isHidden = (action.dataset.bulk != displayType);\n actionTool?.classList.toggle(this.classes.HIDE, isHidden);\n });\n }\n\n /**\n * Cancel bulk handler.\n */\n _cancelBulk() {\n const pending = new Pending(`courseformat/content:bulktoggle_off`);\n this.reactive.dispatch('bulkEnable', false);\n // Wait for a while and focus on enable bulk button.\n setTimeout(() => {\n document.querySelector(this.selectors.BULKBTN)?.focus();\n pending.resolve();\n }, 150);\n }\n\n /**\n * Select all elements click handler.\n * @param {Event} event\n */\n _selectAllClick(event) {\n const target = event.target;\n const bulk = this.reactive.get('bulk');\n if (bulk.selectedType === '') {\n return;\n }\n if (!target.checked) {\n this._handleUnselectAll();\n return;\n }\n this._handleSelectAll(bulk);\n }\n\n /**\n * Process unselect all elements.\n */\n _handleUnselectAll() {\n const pending = new Pending(`courseformat/content:bulktUnselectAll`);\n // Re-enable bulk will clean the selection and the selection type.\n this.reactive.dispatch('bulkEnable', true);\n // Wait for a while and focus on the first checkbox.\n setTimeout(() => {\n document.querySelector(this.selectors.SELECTABLE)?.focus();\n pending.resolve();\n }, 150);\n }\n\n /**\n * Process a select all selectable elements.\n * @param {Object} bulk the state bulk data\n * @param {String} bulk.selectedType the current selected type (section/cm)\n */\n _handleSelectAll(bulk) {\n const selectableIds = [];\n const selectables = document.querySelectorAll(this.selectors.SELECTABLE);\n if (selectables.length == 0) {\n return;\n }\n selectables.forEach(selectable => {\n selectableIds.push(selectable.dataset.id);\n });\n const mutation = (bulk.selectedType === 'cm') ? 'cmSelect' : 'sectionSelect';\n this.reactive.dispatch(mutation, selectableIds);\n }\n}\n"],"names":["Component","BaseComponent","create","name","selectors","ACTIONS","ACTIONTOOL","CANCEL","COUNT","SELECTABLE","SELECTALL","BULKBTN","classes","HIDE","DISABLED","target","this","element","document","querySelector","reactive","stateReady","cancelBtn","getElement","addEventListener","_cancelBulk","selectAll","_selectAllClick","getWatchers","watch","handler","_refreshEnabled","_refreshTools","enabled","param","_refreshSelectCount","_refreshSelectAll","_refreshActions","bulk","selectedCount","selection","length","selectedElement","innerHTML","selectall","selectedType","checked","disabled","maxSelection","querySelectorAll","displayType","getElements","forEach","action","classList","toggle","actionTool","closest","isHidden","dataset","pending","Pending","dispatch","setTimeout","focus","resolve","event","get","_handleSelectAll","_handleUnselectAll","selectableIds","selectables","selectable","push","id","mutation"],"mappings":";;;;;;;;6KAiCI,oBACA,CAAC,wBAGgBA,kBAAkBC,wBAKnCC,cAESC,KAAO,yBAEPC,UAAY,CACbC,kCACAC,yCACAC,iCACAC,+BACAC,qDACAC,mCACAC,wCAGCC,QAAU,CACXC,KAAM,SACNC,SAAU,wBAWNC,OAAQX,kBACT,IAAIY,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,UAAU,0CACVhB,UAAAA,YAORiB,mBACUC,UAAYN,KAAKO,WAAWP,KAAKZ,UAAUG,QAC7Ce,gBACKE,iBAAiBF,UAAW,QAASN,KAAKS,mBAE7CC,UAAYV,KAAKO,WAAWP,KAAKZ,UAAUM,WAC7CgB,gBACKF,iBAAiBE,UAAW,SAAUV,KAAKW,iBASxDC,oBACW,CACH,CAACC,6BAA+BC,QAASd,KAAKe,iBAC9C,CAACF,qBAAuBC,QAASd,KAAKgB,gBAU9CD,0BAAgBd,QAACA,cACTA,QAAQgB,uFAchBD,cAAcE,YACLC,oBAAoBD,YACpBE,kBAAkBF,YAClBG,gBAAgBH,4CASEjB,QAASqB,kBAC1BC,oBAAsB,mBAAU,gBAAiB,oBAAqBD,KAAKE,UAAUC,QACrFC,gBAAkB1B,KAAKO,WAAWP,KAAKZ,UAAUI,OACnDkC,kBACAA,gBAAgBC,UAAYJ,eAUpCH,6BAAmBnB,QAASqB,kBAClBM,UAAY5B,KAAKO,WAAWP,KAAKZ,UAAUM,eAC5CkC,oBAGqB,KAAtBN,KAAKO,oBACLD,UAAUE,SAAU,OACpBF,UAAUG,UAAW,GAIzBH,UAAUG,UAAW,QACfC,aAAe9B,SAAS+B,iBAAiBjC,KAAKZ,UAAUK,YAAYgC,OAC1EG,UAAUE,QAAWR,KAAKE,UAAUC,QAAUO,aASlDX,2BAAiBpB,QAASqB,kBAEhBY,YAAoC,WAArBZ,KAAKO,aAA6B,UAAY,KAC7DZ,QAAiC,KAAtBK,KAAKO,kBACjBM,YAAYnC,KAAKZ,UAAUC,SAAS+C,SAAQC,SAC7CA,OAAOC,UAAUC,OAAOvC,KAAKJ,QAAQE,UAAWmB,eAE1CuB,WAAaH,OAAOI,QAAQzC,KAAKZ,UAAUE,YAC3CoD,SAAYL,OAAOM,QAAQrB,MAAQY,YACzCM,MAAAA,YAAAA,WAAYF,UAAUC,OAAOvC,KAAKJ,QAAQC,KAAM6C,aAOxDjC,oBACUmC,QAAU,IAAIC,6DACfzC,SAAS0C,SAAS,cAAc,GAErCC,YAAW,6DACP7C,SAASC,cAAcH,KAAKZ,UAAUO,iEAAUqD,QAChDJ,QAAQK,YACT,KAOPtC,gBAAgBuC,aACNnD,OAASmD,MAAMnD,OACfuB,KAAOtB,KAAKI,SAAS+C,IAAI,QACL,KAAtB7B,KAAKO,eAGJ9B,OAAO+B,aAIPsB,iBAAiB9B,WAHb+B,sBASbA,2BACUT,QAAU,IAAIC,+DAEfzC,SAAS0C,SAAS,cAAc,GAErCC,YAAW,+DACP7C,SAASC,cAAcH,KAAKZ,UAAUK,sEAAauD,QACnDJ,QAAQK,YACT,KAQPG,iBAAiB9B,YACPgC,cAAgB,GAChBC,YAAcrD,SAAS+B,iBAAiBjC,KAAKZ,UAAUK,eACnC,GAAtB8D,YAAY9B,cAGhB8B,YAAYnB,SAAQoB,aAChBF,cAAcG,KAAKD,WAAWb,QAAQe,aAEpCC,SAAkC,OAAtBrC,KAAKO,aAAyB,WAAa,qBACxDzB,SAAS0C,SAASa,SAAUL"} \ No newline at end of file diff --git a/course/format/amd/build/local/content/section/cmitem.min.js b/course/format/amd/build/local/content/section/cmitem.min.js index 85204cfc3fb..5dadc8d7944 100644 --- a/course/format/amd/build/local/content/section/cmitem.min.js +++ b/course/format/amd/build/local/content/section/cmitem.min.js @@ -8,6 +8,6 @@ define("core_courseformat/local/content/section/cmitem",["exports","core_coursef * @class core_courseformat/local/content/section/cmitem * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=(obj=_dndcmitem)&&obj.__esModule?obj:{default:obj};class _default extends _dndcmitem.default{create(){this.name="content_section_cmitem",this.selectors={DRAGICON:".editing_move"},this.classes={LOCKED:"editinprogress"},this.id=this.element.dataset.id}stateReady(){var _this$getElement;this.configDragDrop(this.id),null===(_this$getElement=this.getElement(this.selectors.DRAGICON))||void 0===_this$getElement||_this$getElement.classList.add(this.classes.DRAGICON)}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.unregister},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm}]}_refreshCm(_ref){var _element$dragging,_element$locked;let{element:element}=_ref;this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked}}return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=(obj=_dndcmitem)&&obj.__esModule?obj:{default:obj};class _default extends _dndcmitem.default{create(){this.name="content_section_cmitem",this.selectors={BULKSELECT:"[data-for='cmBulkSelect']",BULKCHECKBOX:"[data-bulkcheckbox]",CARD:".activity-item",DRAGICON:".editing_move",INPLACEEDITABLE:"[data-inplaceeditablelink]"},this.classes={LOCKED:"editinprogress",HIDE:"d-none",SELECTED:"selected"},this.id=this.element.dataset.id}stateReady(state){var _this$getElement;this.configDragDrop(this.id),null===(_this$getElement=this.getElement(this.selectors.DRAGICON))||void 0===_this$getElement||_this$getElement.classList.add(this.classes.DRAGICON),this._refreshBulk({state:state}),this.addEventListener(this.element,"click",this._handleBulkModeClick)}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.unregister},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"bulk:updated",handler:this._refreshBulk}]}_refreshCm(_ref){var _element$dragging,_element$locked;let{element:element}=_ref;this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked}_refreshBulk(_ref2){var _this$getElement2;let{state:state}=_ref2;const bulk=state.bulk;this.setDraggable(!bulk.enabled),null===(_this$getElement2=this.getElement(this.selectors.BULKSELECT))||void 0===_this$getElement2||_this$getElement2.classList.toggle(this.classes.HIDE,!bulk.enabled);const disabled=!this._isCmBulkEnabled(bulk),selected=this._isSelected(bulk);this._refreshActivityCard(bulk,selected),this._setCheckboxValue(selected,disabled)}_refreshActivityCard(bulk,selected){var _this$getElement3,_this$getElement4;null===(_this$getElement3=this.getElement(this.selectors.INPLACEEDITABLE))||void 0===_this$getElement3||_this$getElement3.classList.toggle(this.classes.HIDE,bulk.enabled),null===(_this$getElement4=this.getElement(this.selectors.CARD))||void 0===_this$getElement4||_this$getElement4.classList.toggle(this.classes.SELECTED,selected),this.element.classList.toggle(this.classes.SELECTED,selected)}_setCheckboxValue(checked,disabled){const checkbox=this.getElement(this.selectors.BULKCHECKBOX);checkbox&&(checkbox.checked=checked,checkbox.disabled=disabled,disabled?checkbox.removeAttribute("data-is-selectable"):checkbox.dataset.isSelectable=1)}_handleBulkModeClick(event){if(event.target.closest(this.selectors.BULKSELECT))return;const bulk=this.reactive.get("bulk");if(!this._isCmBulkEnabled(bulk))return;event.preventDefault();const mutation=this._isSelected(bulk)?"cmUnselect":"cmSelect";this.reactive.dispatch(mutation,[this.id])}_isCmBulkEnabled(bulk){return!!bulk.enabled&&(""===bulk.selectedType||"cm"===bulk.selectedType)}_isSelected(bulk){return"cm"===bulk.selectedType&&bulk.selection.includes(this.id)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=cmitem.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/content/section/cmitem.min.js.map b/course/format/amd/build/local/content/section/cmitem.min.js.map index 6b487964f74..559f7b5952d 100644 --- a/course/format/amd/build/local/content/section/cmitem.min.js.map +++ b/course/format/amd/build/local/content/section/cmitem.min.js.map @@ -1 +1 @@ -{"version":3,"file":"cmitem.min.js","sources":["../../../../src/local/content/section/cmitem.js"],"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 course module item component.\n *\n * This component is used to control specific course modules interactions like drag and drop.\n *\n * @module core_courseformat/local/content/section/cmitem\n * @class core_courseformat/local/content/section/cmitem\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 extends DndCmItem {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_section_cmitem';\n // Default query selectors.\n this.selectors = {\n DRAGICON: `.editing_move`,\n };\n // Most classes will be loaded later by DndCmItem.\n this.classes = {\n LOCKED: 'editinprogress',\n };\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n this.configDragDrop(this.id);\n this.getElement(this.selectors.DRAGICON)?.classList.add(this.classes.DRAGICON);\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.unregister},\n {watch: `cm[${this.id}]:updated`, handler: this._refreshCm},\n ];\n }\n\n /**\n * Update a course index cm using the state information.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshCm({element}) {\n // Update classes.\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.locked = element.locked;\n }\n}\n"],"names":["DndCmItem","create","name","selectors","DRAGICON","classes","LOCKED","id","this","element","dataset","stateReady","configDragDrop","getElement","classList","add","getWatchers","watch","handler","unregister","_refreshCm","toggle","DRAGGING","dragging","locked"],"mappings":";;;;;;;;;;0KA4B6BA,mBAKzBC,cAESC,KAAO,8BAEPC,UAAY,CACbC,+BAGCC,QAAU,CACXC,OAAQ,uBAGPC,GAAKC,KAAKC,QAAQC,QAAQH,GAMnCI,uCACSC,eAAeJ,KAAKD,kCACpBM,WAAWL,KAAKL,UAAUC,wDAAWU,UAAUC,IAAIP,KAAKH,QAAQD,UAQzEY,oBACW,CACH,CAACC,mBAAaT,KAAKD,gBAAeW,QAASV,KAAKW,YAChD,CAACF,mBAAaT,KAAKD,gBAAeW,QAASV,KAAKY,aAUxDA,2DAAWX,QAACA,mBAEHA,QAAQK,UAAUO,OAAOb,KAAKH,QAAQiB,mCAAUb,QAAQc,+DACxDd,QAAQK,UAAUO,OAAOb,KAAKH,QAAQC,+BAAQG,QAAQe,yDACtDA,OAASf,QAAQe"} \ No newline at end of file +{"version":3,"file":"cmitem.min.js","sources":["../../../../src/local/content/section/cmitem.js"],"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 course module item component.\n *\n * This component is used to control specific course modules interactions like drag and drop.\n *\n * @module core_courseformat/local/content/section/cmitem\n * @class core_courseformat/local/content/section/cmitem\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 extends DndCmItem {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_section_cmitem';\n // Default query selectors.\n this.selectors = {\n BULKSELECT: `[data-for='cmBulkSelect']`,\n BULKCHECKBOX: `[data-bulkcheckbox]`,\n CARD: `.activity-item`,\n DRAGICON: `.editing_move`,\n INPLACEEDITABLE: `[data-inplaceeditablelink]`,\n };\n // Most classes will be loaded later by DndCmItem.\n this.classes = {\n LOCKED: 'editinprogress',\n HIDE: 'd-none',\n SELECTED: 'selected',\n };\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n }\n\n /**\n * Initial state ready method.\n * @param {Object} state the state data\n */\n stateReady(state) {\n this.configDragDrop(this.id);\n this.getElement(this.selectors.DRAGICON)?.classList.add(this.classes.DRAGICON);\n this._refreshBulk({state});\n this.addEventListener(this.element, 'click', this._handleBulkModeClick);\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.unregister},\n {watch: `cm[${this.id}]:updated`, handler: this._refreshCm},\n {watch: `bulk:updated`, handler: this._refreshBulk},\n ];\n }\n\n /**\n * Update a course index cm using the state information.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshCm({element}) {\n // Update classes.\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.locked = element.locked;\n }\n\n /**\n * Update the bulk editing interface.\n *\n * @param {object} param\n * @param {Object} param.state the state data\n */\n _refreshBulk({state}) {\n const bulk = state.bulk;\n // For now, dragging elements in bulk is not possible.\n this.setDraggable(!bulk.enabled);\n\n this.getElement(this.selectors.BULKSELECT)?.classList.toggle(this.classes.HIDE, !bulk.enabled);\n\n const disabled = !this._isCmBulkEnabled(bulk);\n const selected = this._isSelected(bulk);\n this._refreshActivityCard(bulk, selected);\n this._setCheckboxValue(selected, disabled);\n }\n\n /**\n * Update the activity card depending on the bulk selection.\n *\n * @param {Object} bulk the current bulk state data\n * @param {Boolean} selected if the activity is selected.\n */\n _refreshActivityCard(bulk, selected) {\n this.getElement(this.selectors.INPLACEEDITABLE)?.classList.toggle(this.classes.HIDE, bulk.enabled);\n this.getElement(this.selectors.CARD)?.classList.toggle(this.classes.SELECTED, selected);\n this.element.classList.toggle(this.classes.SELECTED, selected);\n }\n\n /**\n * Modify the checkbox element.\n * @param {Boolean} checked the new checked value\n * @param {Boolean} disabled the new disabled value\n */\n _setCheckboxValue(checked, disabled) {\n const checkbox = this.getElement(this.selectors.BULKCHECKBOX);\n if (!checkbox) {\n return;\n }\n checkbox.checked = checked;\n checkbox.disabled = disabled;\n // Is selectable is used to easily scan the page for bulk checkboxes.\n if (disabled) {\n checkbox.removeAttribute('data-is-selectable');\n } else {\n checkbox.dataset.isSelectable = 1;\n }\n }\n\n /**\n * Handle the activity card click in bulk mode.\n * @param {Event} event the click event\n */\n _handleBulkModeClick(event) {\n const selectElement = event.target.closest(this.selectors.BULKSELECT);\n if (selectElement) {\n // The select element checkbox execute a normal content action as\n // any regular action button. This is because the chengechecker module\n // is sniffing any form element and will with the checked value\n // changing it twice.\n return;\n }\n const bulk = this.reactive.get('bulk');\n if (!this._isCmBulkEnabled(bulk)) {\n return;\n }\n event.preventDefault();\n const mutation = (this._isSelected(bulk)) ? 'cmUnselect' : 'cmSelect';\n this.reactive.dispatch(mutation, [this.id]);\n }\n\n /**\n * Check if cm bulk selection is available.\n * @param {Object} bulk the current state bulk attribute\n * @returns {Boolean}\n */\n _isCmBulkEnabled(bulk) {\n if (!bulk.enabled) {\n return false;\n }\n return (bulk.selectedType === '' || bulk.selectedType === 'cm');\n }\n\n /**\n * Check if the cm id is part of the current bulk selection.\n * @param {Object} bulk the current state bulk attribute\n * @returns {Boolean}\n */\n _isSelected(bulk) {\n if (bulk.selectedType !== 'cm') {\n return false;\n }\n return bulk.selection.includes(this.id);\n }\n}\n"],"names":["DndCmItem","create","name","selectors","BULKSELECT","BULKCHECKBOX","CARD","DRAGICON","INPLACEEDITABLE","classes","LOCKED","HIDE","SELECTED","id","this","element","dataset","stateReady","state","configDragDrop","getElement","classList","add","_refreshBulk","addEventListener","_handleBulkModeClick","getWatchers","watch","handler","unregister","_refreshCm","toggle","DRAGGING","dragging","locked","bulk","setDraggable","enabled","disabled","_isCmBulkEnabled","selected","_isSelected","_refreshActivityCard","_setCheckboxValue","checked","checkbox","removeAttribute","isSelectable","event","target","closest","reactive","get","preventDefault","mutation","dispatch","selectedType","selection","includes"],"mappings":";;;;;;;;;;0KA4B6BA,mBAKzBC,cAESC,KAAO,8BAEPC,UAAY,CACbC,uCACAC,mCACAC,sBACAC,yBACAC,mDAGCC,QAAU,CACXC,OAAQ,iBACRC,KAAM,SACNC,SAAU,iBAGTC,GAAKC,KAAKC,QAAQC,QAAQH,GAOnCI,WAAWC,iCACFC,eAAeL,KAAKD,kCACpBO,WAAWN,KAAKX,UAAUI,wDAAWc,UAAUC,IAAIR,KAAKL,QAAQF,eAChEgB,aAAa,CAACL,MAAAA,aACdM,iBAAiBV,KAAKC,QAAS,QAASD,KAAKW,sBAQtDC,oBACW,CACH,CAACC,mBAAab,KAAKD,gBAAee,QAASd,KAAKe,YAChD,CAACF,mBAAab,KAAKD,gBAAee,QAASd,KAAKgB,YAChD,CAACH,qBAAuBC,QAASd,KAAKS,eAU9CO,2DAAWf,QAACA,mBAEHA,QAAQM,UAAUU,OAAOjB,KAAKL,QAAQuB,mCAAUjB,QAAQkB,+DACxDlB,QAAQM,UAAUU,OAAOjB,KAAKL,QAAQC,+BAAQK,QAAQmB,yDACtDA,OAASnB,QAAQmB,OAS1BX,8CAAaL,MAACA,mBACJiB,KAAOjB,MAAMiB,UAEdC,cAAcD,KAAKE,wCAEnBjB,WAAWN,KAAKX,UAAUC,4DAAaiB,UAAUU,OAAOjB,KAAKL,QAAQE,MAAOwB,KAAKE,eAEhFC,UAAYxB,KAAKyB,iBAAiBJ,MAClCK,SAAW1B,KAAK2B,YAAYN,WAC7BO,qBAAqBP,KAAMK,eAC3BG,kBAAkBH,SAAUF,UASrCI,qBAAqBP,KAAMK,iFAClBpB,WAAWN,KAAKX,UAAUK,iEAAkBa,UAAUU,OAAOjB,KAAKL,QAAQE,KAAMwB,KAAKE,wCACrFjB,WAAWN,KAAKX,UAAUG,sDAAOe,UAAUU,OAAOjB,KAAKL,QAAQG,SAAU4B,eACzEzB,QAAQM,UAAUU,OAAOjB,KAAKL,QAAQG,SAAU4B,UAQzDG,kBAAkBC,QAASN,gBACjBO,SAAW/B,KAAKM,WAAWN,KAAKX,UAAUE,cAC3CwC,WAGLA,SAASD,QAAUA,QACnBC,SAASP,SAAWA,SAEhBA,SACAO,SAASC,gBAAgB,sBAEzBD,SAAS7B,QAAQ+B,aAAe,GAQxCtB,qBAAqBuB,UACKA,MAAMC,OAAOC,QAAQpC,KAAKX,UAAUC,yBAQpD+B,KAAOrB,KAAKqC,SAASC,IAAI,YAC1BtC,KAAKyB,iBAAiBJ,aAG3Ba,MAAMK,uBACAC,SAAYxC,KAAK2B,YAAYN,MAAS,aAAe,gBACtDgB,SAASI,SAASD,SAAU,CAACxC,KAAKD,KAQ3C0B,iBAAiBJ,cACRA,KAAKE,UAGoB,KAAtBF,KAAKqB,cAA6C,OAAtBrB,KAAKqB,cAQ7Cf,YAAYN,YACkB,OAAtBA,KAAKqB,cAGFrB,KAAKsB,UAAUC,SAAS5C,KAAKD"} \ No newline at end of file diff --git a/course/format/amd/build/local/content/section/header.min.js b/course/format/amd/build/local/content/section/header.min.js index c2fe16d5239..da3747cee19 100644 --- a/course/format/amd/build/local/content/section/header.min.js +++ b/course/format/amd/build/local/content/section/header.min.js @@ -8,6 +8,6 @@ define("core_courseformat/local/content/section/header",["exports","core_coursef * @class core_courseformat/local/content/section/header * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndsectionitem=(obj=_dndsectionitem)&&obj.__esModule?obj:{default:obj};class _default extends _dndsectionitem.default{create(descriptor){this.name="content_section_header",this.id=descriptor.id,this.section=descriptor.section,this.course=descriptor.course,this.fullregion=descriptor.fullregion}stateReady(state){this.configDragDrop(this.id,state,this.fullregion)}}return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndsectionitem=(obj=_dndsectionitem)&&obj.__esModule?obj:{default:obj};class _default extends _dndsectionitem.default{create(descriptor){this.name="content_section_header",this.selectors={ACTIONSMENU:".section_action_menu",BULKSELECT:"[data-for='sectionBulkSelect']",BULKCHECKBOX:"[data-bulkcheckbox]"},this.classes={HIDE:"d-none",SELECTED:"selected"},this.id=descriptor.id,this.section=descriptor.section,this.course=descriptor.course,this.fullregion=descriptor.fullregion}stateReady(state){this.configDragDrop(this.id,state,this.fullregion),this._refreshBulk({state:state})}getWatchers(){return[{watch:"bulk:updated",handler:this._refreshBulk}]}_refreshBulk(_ref){var _this$getElement;let{state:state}=_ref;const bulk=state.bulk;if(!this._isSectionBulkEditable())return;this.setDraggable(!bulk.enabled),null===(_this$getElement=this.getElement(this.selectors.BULKSELECT))||void 0===_this$getElement||_this$getElement.classList.toggle(this.classes.HIDE,!bulk.enabled);const disabled=!this._isSectionBulkEnabled(bulk),selected=this._isSelected(bulk);this.element.classList.toggle(this.classes.SELECTED,selected),this._setCheckboxValue(selected,disabled)}_setCheckboxValue(checked,disabled){const checkbox=this.getElement(this.selectors.BULKCHECKBOX);checkbox&&(checkbox.checked=checked,checkbox.disabled=disabled,disabled?checkbox.removeAttribute("data-is-selectable"):checkbox.dataset.isSelectable=1)}_isSectionBulkEnabled(bulk){return!!bulk.enabled&&(""===bulk.selectedType||"section"===bulk.selectedType)}_isSectionBulkEditable(){var _section$bulkeditable;const section=this.reactive.get("section",this.id);return null!==(_section$bulkeditable=null==section?void 0:section.bulkeditable)&&void 0!==_section$bulkeditable&&_section$bulkeditable}_isSelected(bulk){return"section"===bulk.selectedType&&bulk.selection.includes(this.id)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=header.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/content/section/header.min.js.map b/course/format/amd/build/local/content/section/header.min.js.map index 91e9ac7f0b8..ccd32234d2e 100644 --- a/course/format/amd/build/local/content/section/header.min.js.map +++ b/course/format/amd/build/local/content/section/header.min.js.map @@ -1 +1 @@ -{"version":3,"file":"header.min.js","sources":["../../../../src/local/content/section/header.js"],"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 section header component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/content/section/header\n * @class core_courseformat/local/content/section/header\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 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 = 'content_section_header';\n // We need our id to watch specific events.\n\n // Get main info from the descriptor.\n this.id = descriptor.id;\n this.section = descriptor.section;\n this.course = descriptor.course;\n this.fullregion = descriptor.fullregion;\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}"],"names":["DndSectionItem","create","descriptor","name","id","section","course","fullregion","stateReady","state","configDragDrop","this"],"mappings":";;;;;;;;;;oLA4B6BA,wBAOzBC,OAAOC,iBAEEC,KAAO,8BAIPC,GAAKF,WAAWE,QAChBC,QAAUH,WAAWG,aACrBC,OAASJ,WAAWI,YACpBC,WAAaL,WAAWK,WAQjCC,WAAWC,YACFC,eAAeC,KAAKP,GAAIK,MAAOE,KAAKJ"} \ No newline at end of file +{"version":3,"file":"header.min.js","sources":["../../../../src/local/content/section/header.js"],"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 section header component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/content/section/header\n * @class core_courseformat/local/content/section/header\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 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 = 'content_section_header';\n // Default query selectors.\n this.selectors = {\n ACTIONSMENU: `.section_action_menu`,\n BULKSELECT: `[data-for='sectionBulkSelect']`,\n BULKCHECKBOX: `[data-bulkcheckbox]`,\n };\n this.classes = {\n HIDE: 'd-none',\n SELECTED: 'selected',\n };\n // Get main info from the descriptor.\n this.id = descriptor.id;\n this.section = descriptor.section;\n this.course = descriptor.course;\n this.fullregion = descriptor.fullregion;\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 this._refreshBulk({state});\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `bulk:updated`, handler: this._refreshBulk},\n ];\n }\n\n /**\n * Update a bulk options.\n *\n * @param {object} param\n * @param {Object} param.state the state data\n */\n _refreshBulk({state}) {\n const bulk = state.bulk;\n if (!this._isSectionBulkEditable()) {\n return;\n }\n // For now, dragging elements in bulk is not possible.\n this.setDraggable(!bulk.enabled);\n this.getElement(this.selectors.BULKSELECT)?.classList.toggle(this.classes.HIDE, !bulk.enabled);\n\n const disabled = !this._isSectionBulkEnabled(bulk);\n const selected = this._isSelected(bulk);\n this.element.classList.toggle(this.classes.SELECTED, selected);\n this._setCheckboxValue(selected, disabled);\n }\n\n /**\n * Modify the checkbox element.\n * @param {Boolean} checked the new checked value\n * @param {Boolean} disabled the new disabled value\n */\n _setCheckboxValue(checked, disabled) {\n const checkbox = this.getElement(this.selectors.BULKCHECKBOX);\n if (!checkbox) {\n return;\n }\n checkbox.checked = checked;\n checkbox.disabled = disabled;\n // Is selectable is used to easily scan the page for bulk checkboxes.\n if (disabled) {\n checkbox.removeAttribute('data-is-selectable');\n } else {\n checkbox.dataset.isSelectable = 1;\n }\n }\n\n /**\n * Check if cm bulk selection is available.\n * @param {Object} bulk the current state bulk attribute\n * @returns {Boolean}\n */\n _isSectionBulkEnabled(bulk) {\n if (!bulk.enabled) {\n return false;\n }\n return (bulk.selectedType === '' || bulk.selectedType === 'section');\n }\n\n /**\n * Check if the section is bulk editable.\n * @return {Boolean}\n */\n _isSectionBulkEditable() {\n const section = this.reactive.get('section', this.id);\n return section?.bulkeditable ?? false;\n }\n\n /**\n * Check if the cm id is part of the current bulk selection.\n * @param {Object} bulk the current state bulk attribute\n * @returns {Boolean}\n */\n _isSelected(bulk) {\n if (bulk.selectedType !== 'section') {\n return false;\n }\n return bulk.selection.includes(this.id);\n }\n}\n"],"names":["DndSectionItem","create","descriptor","name","selectors","ACTIONSMENU","BULKSELECT","BULKCHECKBOX","classes","HIDE","SELECTED","id","section","course","fullregion","stateReady","state","configDragDrop","this","_refreshBulk","getWatchers","watch","handler","bulk","_isSectionBulkEditable","setDraggable","enabled","getElement","classList","toggle","disabled","_isSectionBulkEnabled","selected","_isSelected","element","_setCheckboxValue","checked","checkbox","removeAttribute","dataset","isSelectable","selectedType","reactive","get","bulkeditable","selection","includes"],"mappings":";;;;;;;;;;oLA4B6BA,wBAOzBC,OAAOC,iBAEEC,KAAO,8BAEPC,UAAY,CACbC,mCACAC,4CACAC,yCAECC,QAAU,CACXC,KAAM,SACNC,SAAU,iBAGTC,GAAKT,WAAWS,QAChBC,QAAUV,WAAWU,aACrBC,OAASX,WAAWW,YACpBC,WAAaZ,WAAWY,WAQjCC,WAAWC,YACFC,eAAeC,KAAKP,GAAIK,MAAOE,KAAKJ,iBACpCK,aAAa,CAACH,MAAAA,QAQvBI,oBACW,CACH,CAACC,qBAAuBC,QAASJ,KAAKC,eAU9CA,4CAAaH,MAACA,kBACJO,KAAOP,MAAMO,SACdL,KAAKM,qCAILC,cAAcF,KAAKG,uCACnBC,WAAWT,KAAKd,UAAUE,0DAAasB,UAAUC,OAAOX,KAAKV,QAAQC,MAAOc,KAAKG,eAEhFI,UAAYZ,KAAKa,sBAAsBR,MACvCS,SAAWd,KAAKe,YAAYV,WAC7BW,QAAQN,UAAUC,OAAOX,KAAKV,QAAQE,SAAUsB,eAChDG,kBAAkBH,SAAUF,UAQrCK,kBAAkBC,QAASN,gBACjBO,SAAWnB,KAAKS,WAAWT,KAAKd,UAAUG,cAC3C8B,WAGLA,SAASD,QAAUA,QACnBC,SAASP,SAAWA,SAEhBA,SACAO,SAASC,gBAAgB,sBAEzBD,SAASE,QAAQC,aAAe,GASxCT,sBAAsBR,cACbA,KAAKG,UAGoB,KAAtBH,KAAKkB,cAA6C,YAAtBlB,KAAKkB,cAO7CjB,yDACUZ,QAAUM,KAAKwB,SAASC,IAAI,UAAWzB,KAAKP,yCAC3CC,MAAAA,eAAAA,QAASgC,qEAQpBX,YAAYV,YACkB,YAAtBA,KAAKkB,cAGFlB,KAAKsB,UAAUC,SAAS5B,KAAKP"} \ 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 index f079c4a46b0..6b3f0d6ea42 100644 --- a/course/format/amd/build/local/courseeditor/dndcmitem.min.js +++ b/course/format/amd/build/local/courseeditor/dndcmitem.min.js @@ -10,6 +10,6 @@ define("core_courseformat/local/courseeditor/dndcmitem",["exports","core/reactiv * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class _default extends _reactive.BaseComponent{configDragDrop(cmid){this.id=cmid,this.reactive.isEditing&&this.reactive.supportComponents&&(this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.dragdrop&&this.dragdrop.unregister()}dragStart(dropdata){this.reactive.dispatch("cmDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("cmDrag",[dropdata.id],!1)}getDraggableData(){return this.reactive.getExporter().cmDraggableData(this.reactive.state,this.id)}validateDropData(dropdata){return"cm"===(null==dropdata?void 0:dropdata.type)}showDropZone(dropdata){dropdata.nextcmid!=this.id&&dropdata.id!=this.id&&this.element.classList.add(this.classes.DROPUP)}hideDropZone(){this.element.classList.remove(this.classes.DROPUP)}drop(dropdata,event){if(dropdata.id!=this.id&&dropdata.nextcmid!=this.id){const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[dropdata.id],null,this.id)}}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{configDragDrop(cmid){this.id=cmid,this.reactive.isEditing&&this.reactive.supportComponents&&(this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.dragdrop&&this.dragdrop.unregister()}setDraggable(value){var _this$dragdrop;null===(_this$dragdrop=this.dragdrop)||void 0===_this$dragdrop||_this$dragdrop.setDraggable(value)}dragStart(dropdata){this.reactive.dispatch("cmDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("cmDrag",[dropdata.id],!1)}getDraggableData(){return this.reactive.getExporter().cmDraggableData(this.reactive.state,this.id)}validateDropData(dropdata){return"cm"===(null==dropdata?void 0:dropdata.type)}showDropZone(dropdata){dropdata.nextcmid!=this.id&&dropdata.id!=this.id&&this.element.classList.add(this.classes.DROPUP)}hideDropZone(){this.element.classList.remove(this.classes.DROPUP)}drop(dropdata,event){if(dropdata.id!=this.id&&dropdata.nextcmid!=this.id){const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[dropdata.id],null,this.id)}}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=dndcmitem.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/dndcmitem.min.js.map b/course/format/amd/build/local/courseeditor/dndcmitem.min.js.map index dad9c2135b2..8dfc0b8ecfe 100644 --- a/course/format/amd/build/local/courseeditor/dndcmitem.min.js.map +++ b/course/format/amd/build/local/courseeditor/dndcmitem.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dndcmitem.min.js","sources":["../../../src/local/courseeditor/dndcmitem.js"],"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 * 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 * The element drop start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragStart(dropdata) {\n this.reactive.dispatch('cmDrag', [dropdata.id], true);\n }\n\n /**\n * The element drop end hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragEnd(dropdata) {\n this.reactive.dispatch('cmDrag', [dropdata.id], false);\n }\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 * @param {Event} event the drop event\n */\n drop(dropdata, event) {\n // Call the move mutation if necessary.\n if (dropdata.id != this.id && dropdata.nextcmid != this.id) {\n const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove';\n this.reactive.dispatch(mutation, [dropdata.id], null, this.id);\n }\n }\n\n}\n"],"names":["BaseComponent","configDragDrop","cmid","id","this","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","destroy","undefined","unregister","dragStart","dropdata","dispatch","dragEnd","getDraggableData","getExporter","cmDraggableData","state","validateDropData","type","showDropZone","nextcmid","element","classList","add","DROPUP","hideDropZone","remove","drop","event","mutation","altKey"],"mappings":";;;;;;;;;;;;uBA6B6BA,wBAOzBC,eAAeC,WAENC,GAAKD,KAGNE,KAAKC,SAASC,WAAaF,KAAKC,SAASE,yBAEpCC,SAAW,IAAIC,mBAASL,WAExBM,QAAUN,KAAKI,SAASG,cAOrCC,eAC0BC,IAAlBT,KAAKI,eACAA,SAASM,aAWtBC,UAAUC,eACDX,SAASY,SAAS,SAAU,CAACD,SAASb,KAAK,GAQpDe,QAAQF,eACCX,SAASY,SAAS,SAAU,CAACD,SAASb,KAAK,GAQpDgB,0BACqBf,KAAKC,SAASe,cACfC,gBAAgBjB,KAAKC,SAASiB,MAAOlB,KAAKD,IAS9DoB,iBAAiBP,gBACa,QAAnBA,MAAAA,gBAAAA,SAAUQ,MAQrBC,aAAaT,UAGLA,SAASU,UAAYtB,KAAKD,IAAMa,SAASb,IAAMC,KAAKD,SAC/CwB,QAAQC,UAAUC,IAAIzB,KAAKM,QAAQoB,QAOhDC,oBACSJ,QAAQC,UAAUI,OAAO5B,KAAKM,QAAQoB,QAS/CG,KAAKjB,SAAUkB,UAEPlB,SAASb,IAAMC,KAAKD,IAAMa,SAASU,UAAYtB,KAAKD,GAAI,OAClDgC,SAAYD,MAAME,OAAU,cAAgB,cAC7C/B,SAASY,SAASkB,SAAU,CAACnB,SAASb,IAAK,KAAMC,KAAKD"} \ No newline at end of file +{"version":3,"file":"dndcmitem.min.js","sources":["../../../src/local/courseeditor/dndcmitem.js"],"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 * 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 /**\n * Enable or disable the draggable property.\n *\n * @param {bool} value the new draggable value\n */\n setDraggable(value) {\n this.dragdrop?.setDraggable(value);\n }\n\n // Drag and drop methods.\n\n /**\n * The element drop start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragStart(dropdata) {\n this.reactive.dispatch('cmDrag', [dropdata.id], true);\n }\n\n /**\n * The element drop end hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragEnd(dropdata) {\n this.reactive.dispatch('cmDrag', [dropdata.id], false);\n }\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 * @param {Event} event the drop event\n */\n drop(dropdata, event) {\n // Call the move mutation if necessary.\n if (dropdata.id != this.id && dropdata.nextcmid != this.id) {\n const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove';\n this.reactive.dispatch(mutation, [dropdata.id], null, this.id);\n }\n }\n\n}\n"],"names":["BaseComponent","configDragDrop","cmid","id","this","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","destroy","undefined","unregister","setDraggable","value","dragStart","dropdata","dispatch","dragEnd","getDraggableData","getExporter","cmDraggableData","state","validateDropData","type","showDropZone","nextcmid","element","classList","add","DROPUP","hideDropZone","remove","drop","event","mutation","altKey"],"mappings":";;;;;;;;;;;;uBA6B6BA,wBAOzBC,eAAeC,WAENC,GAAKD,KAGNE,KAAKC,SAASC,WAAaF,KAAKC,SAASE,yBAEpCC,SAAW,IAAIC,mBAASL,WAExBM,QAAUN,KAAKI,SAASG,cAOrCC,eAC0BC,IAAlBT,KAAKI,eACAA,SAASM,aAStBC,aAAaC,sDACJR,mDAAUO,aAAaC,OAUhCC,UAAUC,eACDb,SAASc,SAAS,SAAU,CAACD,SAASf,KAAK,GAQpDiB,QAAQF,eACCb,SAASc,SAAS,SAAU,CAACD,SAASf,KAAK,GAQpDkB,0BACqBjB,KAAKC,SAASiB,cACfC,gBAAgBnB,KAAKC,SAASmB,MAAOpB,KAAKD,IAS9DsB,iBAAiBP,gBACa,QAAnBA,MAAAA,gBAAAA,SAAUQ,MAQrBC,aAAaT,UAGLA,SAASU,UAAYxB,KAAKD,IAAMe,SAASf,IAAMC,KAAKD,SAC/C0B,QAAQC,UAAUC,IAAI3B,KAAKM,QAAQsB,QAOhDC,oBACSJ,QAAQC,UAAUI,OAAO9B,KAAKM,QAAQsB,QAS/CG,KAAKjB,SAAUkB,UAEPlB,SAASf,IAAMC,KAAKD,IAAMe,SAASU,UAAYxB,KAAKD,GAAI,OAClDkC,SAAYD,MAAME,OAAU,cAAgB,cAC7CjC,SAASc,SAASkB,SAAU,CAACnB,SAASf,IAAK,KAAMC,KAAKD"} \ 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 index 172e9968592..64bb51dc12b 100644 --- a/course/format/amd/build/local/courseeditor/dndsectionitem.min.js +++ b/course/format/amd/build/local/courseeditor/dndsectionitem.min.js @@ -10,6 +10,6 @@ define("core_courseformat/local/courseeditor/dndsectionitem",["exports","core/re * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class _default extends _reactive.BaseComponent{configDragDrop(sectionid,state,fullregion){this.id=sectionid,void 0===this.section&&(this.section=state.section.get(this.id)),void 0===this.course&&(this.course=state.course),this.section.number>0&&(this.getDraggableData=this._getDraggableData),this.fullregion=fullregion,this.reactive.isEditing&&this.reactive.supportComponents&&(this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.dragdrop&&this.dragdrop.unregister()}dragStart(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!1)}_getDraggableData(){return this.reactive.getExporter().sectionDraggableData(this.reactive.state,this.id)}validateDropData(dropdata){if("cm"===(null==dropdata?void 0:dropdata.type)){var _this$section;const firstcmid=null===(_this$section=this.section)||void 0===_this$section?void 0:_this$section.cmlist[0];return dropdata.id!==firstcmid}return!1}showDropZone(){this.element.classList.add(this.classes.DROPZONE)}hideDropZone(){this.element.classList.remove(this.classes.DROPZONE)}drop(dropdata,event){if("cm"==dropdata.type){var _this$section2;const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[dropdata.id],this.id,null===(_this$section2=this.section)||void 0===_this$section2?void 0:_this$section2.cmlist[0])}}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{configDragDrop(sectionid,state,fullregion){this.id=sectionid,void 0===this.section&&(this.section=state.section.get(this.id)),void 0===this.course&&(this.course=state.course),this.section.number>0&&(this.getDraggableData=this._getDraggableData),this.fullregion=fullregion,this.reactive.isEditing&&this.reactive.supportComponents&&(this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.dragdrop&&this.dragdrop.unregister()}setDraggable(value){var _this$dragdrop;this.getDraggableData&&(null===(_this$dragdrop=this.dragdrop)||void 0===_this$dragdrop||_this$dragdrop.setDraggable(value))}dragStart(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!1)}_getDraggableData(){return this.reactive.getExporter().sectionDraggableData(this.reactive.state,this.id)}validateDropData(dropdata){if("cm"===(null==dropdata?void 0:dropdata.type)){var _this$section;const firstcmid=null===(_this$section=this.section)||void 0===_this$section?void 0:_this$section.cmlist[0];return dropdata.id!==firstcmid}return!1}showDropZone(){this.element.classList.add(this.classes.DROPZONE)}hideDropZone(){this.element.classList.remove(this.classes.DROPZONE)}drop(dropdata,event){if("cm"==dropdata.type){var _this$section2;const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[dropdata.id],this.id,null===(_this$section2=this.section)||void 0===_this$section2?void 0:_this$section2.cmlist[0])}}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=dndsectionitem.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/dndsectionitem.min.js.map b/course/format/amd/build/local/courseeditor/dndsectionitem.min.js.map index 5954657b2a5..4ee9d06b318 100644 --- a/course/format/amd/build/local/courseeditor/dndsectionitem.min.js.map +++ b/course/format/amd/build/local/courseeditor/dndsectionitem.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dndsectionitem.min.js","sources":["../../../src/local/courseeditor/dndsectionitem.js"],"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 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 * The element drop start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragStart(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], true);\n }\n\n /**\n * The element end start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragEnd(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], false);\n }\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 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 * @param {Event} event the drop event\n */\n drop(dropdata, event) {\n // Call the move mutation.\n if (dropdata.type == 'cm') {\n const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove';\n this.reactive.dispatch(mutation, [dropdata.id], this.id, this.section?.cmlist[0]);\n }\n }\n}\n"],"names":["BaseComponent","configDragDrop","sectionid","state","fullregion","id","undefined","this","section","get","course","number","getDraggableData","_getDraggableData","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","destroy","unregister","dragStart","dropdata","dispatch","dragEnd","getExporter","sectionDraggableData","validateDropData","type","firstcmid","_this$section","cmlist","showDropZone","element","classList","add","DROPZONE","hideDropZone","remove","drop","event","mutation","altKey","_this$section2"],"mappings":";;;;;;;;;;;;uBA6B6BA,wBASzBC,eAAeC,UAAWC,MAAOC,iBAExBC,GAAKH,eACWI,IAAjBC,KAAKC,eACAA,QAAUL,MAAMK,QAAQC,IAAIF,KAAKF,UAEtBC,IAAhBC,KAAKG,cACAA,OAASP,MAAMO,QAIpBH,KAAKC,QAAQG,OAAS,SACjBC,iBAAmBL,KAAKM,wBAG5BT,WAAaA,WAGdG,KAAKO,SAASC,WAAaR,KAAKO,SAASE,yBAEpCC,SAAW,IAAIC,mBAASX,WAExBY,QAAUZ,KAAKU,SAASG,cAOrCC,eAC0Bf,IAAlBC,KAAKU,eACAA,SAASK,aAWtBC,UAAUC,eACDV,SAASW,SAAS,cAAe,CAACD,SAASnB,KAAK,GAQzDqB,QAAQF,eACCV,SAASW,SAAS,cAAe,CAACD,SAASnB,KAAK,GAQzDQ,2BACqBN,KAAKO,SAASa,cACfC,qBAAqBrB,KAAKO,SAASX,MAAOI,KAAKF,IASnEwB,iBAAiBL,aAEU,QAAnBA,MAAAA,gBAAAA,SAAUM,MAAe,yBAEnBC,gCAAYxB,KAAKC,wCAALwB,cAAcC,OAAO,UAChCT,SAASnB,KAAO0B,iBAEpB,EAMXG,oBACSC,QAAQC,UAAUC,IAAI9B,KAAKY,QAAQmB,UAM5CC,oBACSJ,QAAQC,UAAUI,OAAOjC,KAAKY,QAAQmB,UAS/CG,KAAKjB,SAAUkB,UAEU,MAAjBlB,SAASM,KAAc,0BACjBa,SAAYD,MAAME,OAAU,cAAgB,cAC7C9B,SAASW,SAASkB,SAAU,CAACnB,SAASnB,IAAKE,KAAKF,0BAAIE,KAAKC,yCAALqC,eAAcZ,OAAO"} \ No newline at end of file +{"version":3,"file":"dndsectionitem.min.js","sources":["../../../src/local/courseeditor/dndsectionitem.js"],"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 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 /**\n * Enable or disable the draggable property.\n *\n * @param {bool} value the new draggable value\n */\n setDraggable(value) {\n if (this.getDraggableData) {\n this.dragdrop?.setDraggable(value);\n }\n }\n\n // Drag and drop methods.\n\n /**\n * The element drop start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragStart(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], true);\n }\n\n /**\n * The element end start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragEnd(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], false);\n }\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 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 * @param {Event} event the drop event\n */\n drop(dropdata, event) {\n // Call the move mutation.\n if (dropdata.type == 'cm') {\n const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove';\n this.reactive.dispatch(mutation, [dropdata.id], this.id, this.section?.cmlist[0]);\n }\n }\n}\n"],"names":["BaseComponent","configDragDrop","sectionid","state","fullregion","id","undefined","this","section","get","course","number","getDraggableData","_getDraggableData","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","destroy","unregister","setDraggable","value","dragStart","dropdata","dispatch","dragEnd","getExporter","sectionDraggableData","validateDropData","type","firstcmid","_this$section","cmlist","showDropZone","element","classList","add","DROPZONE","hideDropZone","remove","drop","event","mutation","altKey","_this$section2"],"mappings":";;;;;;;;;;;;uBA6B6BA,wBASzBC,eAAeC,UAAWC,MAAOC,iBAExBC,GAAKH,eACWI,IAAjBC,KAAKC,eACAA,QAAUL,MAAMK,QAAQC,IAAIF,KAAKF,UAEtBC,IAAhBC,KAAKG,cACAA,OAASP,MAAMO,QAIpBH,KAAKC,QAAQG,OAAS,SACjBC,iBAAmBL,KAAKM,wBAG5BT,WAAaA,WAGdG,KAAKO,SAASC,WAAaR,KAAKO,SAASE,yBAEpCC,SAAW,IAAIC,mBAASX,WAExBY,QAAUZ,KAAKU,SAASG,cAOrCC,eAC0Bf,IAAlBC,KAAKU,eACAA,SAASK,aAStBC,aAAaC,0BACLjB,KAAKK,+CACAK,mDAAUM,aAAaC,QAWpCC,UAAUC,eACDZ,SAASa,SAAS,cAAe,CAACD,SAASrB,KAAK,GAQzDuB,QAAQF,eACCZ,SAASa,SAAS,cAAe,CAACD,SAASrB,KAAK,GAQzDQ,2BACqBN,KAAKO,SAASe,cACfC,qBAAqBvB,KAAKO,SAASX,MAAOI,KAAKF,IASnE0B,iBAAiBL,aAEU,QAAnBA,MAAAA,gBAAAA,SAAUM,MAAe,yBAEnBC,gCAAY1B,KAAKC,wCAAL0B,cAAcC,OAAO,UAChCT,SAASrB,KAAO4B,iBAEpB,EAMXG,oBACSC,QAAQC,UAAUC,IAAIhC,KAAKY,QAAQqB,UAM5CC,oBACSJ,QAAQC,UAAUI,OAAOnC,KAAKY,QAAQqB,UAS/CG,KAAKjB,SAAUkB,UAEU,MAAjBlB,SAASM,KAAc,0BACjBa,SAAYD,MAAME,OAAU,cAAgB,cAC7ChC,SAASa,SAASkB,SAAU,CAACnB,SAASrB,IAAKE,KAAKF,0BAAIE,KAAKC,yCAALuC,eAAcZ,OAAO"} \ No newline at end of file diff --git a/course/format/amd/src/local/content/bulkedittoggler.js b/course/format/amd/src/local/content/bulkedittoggler.js new file mode 100644 index 00000000000..f7b21145754 --- /dev/null +++ b/course/format/amd/src/local/content/bulkedittoggler.js @@ -0,0 +1,115 @@ +// 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 . + +/** + * The bulk editor toggler button control. + * + * @module core_courseformat/local/content/bulkedittoggler + * @class core_courseformat/local/content/bulkedittoggler + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {BaseComponent} from 'core/reactive'; +import {getCurrentCourseEditor} from 'core_courseformat/courseeditor'; +import Pending from 'core/pending'; + +export default class Component extends BaseComponent { + + /** + * Constructor hook. + */ + create() { + // Optional component name for debugging. + this.name = 'bulk_editor_toogler'; + // Default query selectors. + this.selectors = { + BODY: `body`, + SELECTABLE: `[data-bulkcheckbox][data-is-selectable]`, + }; + // Component css classes. + this.classes = { + HIDDEN: `d-none`, + BULK: `bulkenabled`, + }; + } + + /** + * Static method to create a component instance from the mustache template. + * + * @param {string} target optional altentative DOM main element CSS selector + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new this({ + element: document.querySelector(target), + reactive: getCurrentCourseEditor(), + selectors + }); + } + + /** + * Initial state ready method. + */ + stateReady() { + // Capture completion events. + this.addEventListener( + this.element, + 'click', + this._enableBulk + ); + } + + /** + * Component watchers. + * + * @returns {Array} of watchers + */ + getWatchers() { + return [ + {watch: `bulk.enabled:updated`, handler: this._refreshToggler}, + ]; + } + + /** + * Update a content section using the state information. + * + * @param {object} param + * @param {Object} param.element details the update details (state.bulk in this case). + */ + _refreshToggler({element}) { + this.element.classList.toggle(this.classes.HIDDEN, element.enabled ?? false); + document.querySelector(this.selectors.BODY)?.classList.toggle(this.classes.BULK, element.enabled); + } + + /** + * Dispatch the enable bulk mutation. + * + * The enable bulk button is outside of the course content main div. + * Because content/actions captures click events only in the course + * content, this button needs to trigger the enable bulk mutation + * by itself. + */ + _enableBulk() { + const pendingToggle = new Pending(`courseformat/content:bulktoggle_on`); + this.reactive.dispatch('bulkEnable', true); + // Wait for a while and focus on the first checkbox. + setTimeout(() => { + document.querySelector(this.selectors.SELECTABLE)?.focus(); + pendingToggle.resolve(); + }, 150); + } +} diff --git a/course/format/amd/src/local/content/bulkedittools.js b/course/format/amd/src/local/content/bulkedittools.js new file mode 100644 index 00000000000..75cf06e8bc5 --- /dev/null +++ b/course/format/amd/src/local/content/bulkedittools.js @@ -0,0 +1,247 @@ +// 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 . + +/** + * The bulk editor tools bar. + * + * @module core_courseformat/local/content/bulkedittools + * @class core_courseformat/local/content/bulkedittools + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {BaseComponent} from 'core/reactive'; +import {disableStickyFooter, enableStickyFooter} from 'core/sticky-footer'; +import {getCurrentCourseEditor} from 'core_courseformat/courseeditor'; +import {get_string as getString} from 'core/str'; +import Pending from 'core/pending'; +import {prefetchStrings} from 'core/prefetch'; + +// Load global strings. +prefetchStrings( + 'core_courseformat', + ['bulkselection'] +); + +export default class Component extends BaseComponent { + + /** + * Constructor hook. + */ + create() { + // Optional component name for debugging. + this.name = 'bulk_editor_tools'; + // Default query selectors. + this.selectors = { + ACTIONS: `[data-for="bulkaction"]`, + ACTIONTOOL: `[data-for="bulkactions"] li`, + CANCEL: `[data-for="bulkcancel"]`, + COUNT: `[data-for='bulkcount']`, + SELECTABLE: `[data-bulkcheckbox][data-is-selectable]`, + SELECTALL: `[data-for="selectall"]`, + BULKBTN: `[data-for="enableBulk"]`, + }; + // Most classes will be loaded later by DndCmItem. + this.classes = { + HIDE: 'd-none', + DISABLED: 'disabled', + }; + } + + /** + * Static method to create a component instance from the mustache template. + * + * @param {string} target optional altentative DOM main element CSS selector + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new this({ + element: document.querySelector(target), + reactive: getCurrentCourseEditor(), + selectors + }); + } + + /** + * Initial state ready method. + */ + stateReady() { + const cancelBtn = this.getElement(this.selectors.CANCEL); + if (cancelBtn) { + this.addEventListener(cancelBtn, 'click', this._cancelBulk); + } + const selectAll = this.getElement(this.selectors.SELECTALL); + if (selectAll) { + this.addEventListener(selectAll, 'change', this._selectAllClick); + } + } + + /** + * Component watchers. + * + * @returns {Array} of watchers + */ + getWatchers() { + return [ + {watch: `bulk.enabled:updated`, handler: this._refreshEnabled}, + {watch: `bulk:updated`, handler: this._refreshTools}, + ]; + } + + /** + * Hide and show the bulk edit tools. + * + * @param {object} param + * @param {Object} param.element details the update details (state.bulk in this case). + */ + _refreshEnabled({element}) { + if (element.enabled) { + enableStickyFooter(); + } else { + disableStickyFooter(); + } + } + + /** + * Refresh the tools depending on the current selection. + * + * @param {object} param the state watcher information + * @param {Object} param.state the full state data. + * @param {Object} param.element the affected element (bulk in this case). + */ + _refreshTools(param) { + this._refreshSelectCount(param); + this._refreshSelectAll(param); + this._refreshActions(param); + } + + /** + * Refresh the selection count. + * + * @param {object} param + * @param {Object} param.element the affected element (bulk in this case). + */ + async _refreshSelectCount({element: bulk}) { + const selectedCount = await getString('bulkselection', 'core_courseformat', bulk.selection.length); + const selectedElement = this.getElement(this.selectors.COUNT); + if (selectedElement) { + selectedElement.innerHTML = selectedCount; + } + } + + /** + * Refresh the select all element. + * + * @param {object} param + * @param {Object} param.element the affected element (bulk in this case). + */ + _refreshSelectAll({element: bulk}) { + const selectall = this.getElement(this.selectors.SELECTALL); + if (!selectall) { + return; + } + if (bulk.selectedType === '') { + selectall.checked = false; + selectall.disabled = true; + return; + } + + selectall.disabled = false; + const maxSelection = document.querySelectorAll(this.selectors.SELECTABLE).length; + selectall.checked = (bulk.selection.length == maxSelection); + } + + /** + * Refresh the visible action buttons depending on the selection type. + * + * @param {object} param + * @param {Object} param.element the affected element (bulk in this case). + */ + _refreshActions({element: bulk}) { + // By default, we show the cm options. + const displayType = (bulk.selectedType == 'section') ? 'section' : 'cm'; + const enabled = (bulk.selectedType !== ''); + this.getElements(this.selectors.ACTIONS).forEach(action => { + action.classList.toggle(this.classes.DISABLED, !enabled); + + const actionTool = action.closest(this.selectors.ACTIONTOOL); + const isHidden = (action.dataset.bulk != displayType); + actionTool?.classList.toggle(this.classes.HIDE, isHidden); + }); + } + + /** + * Cancel bulk handler. + */ + _cancelBulk() { + const pending = new Pending(`courseformat/content:bulktoggle_off`); + this.reactive.dispatch('bulkEnable', false); + // Wait for a while and focus on enable bulk button. + setTimeout(() => { + document.querySelector(this.selectors.BULKBTN)?.focus(); + pending.resolve(); + }, 150); + } + + /** + * Select all elements click handler. + * @param {Event} event + */ + _selectAllClick(event) { + const target = event.target; + const bulk = this.reactive.get('bulk'); + if (bulk.selectedType === '') { + return; + } + if (!target.checked) { + this._handleUnselectAll(); + return; + } + this._handleSelectAll(bulk); + } + + /** + * Process unselect all elements. + */ + _handleUnselectAll() { + const pending = new Pending(`courseformat/content:bulktUnselectAll`); + // Re-enable bulk will clean the selection and the selection type. + this.reactive.dispatch('bulkEnable', true); + // Wait for a while and focus on the first checkbox. + setTimeout(() => { + document.querySelector(this.selectors.SELECTABLE)?.focus(); + pending.resolve(); + }, 150); + } + + /** + * Process a select all selectable elements. + * @param {Object} bulk the state bulk data + * @param {String} bulk.selectedType the current selected type (section/cm) + */ + _handleSelectAll(bulk) { + const selectableIds = []; + const selectables = document.querySelectorAll(this.selectors.SELECTABLE); + if (selectables.length == 0) { + return; + } + selectables.forEach(selectable => { + selectableIds.push(selectable.dataset.id); + }); + const mutation = (bulk.selectedType === 'cm') ? 'cmSelect' : 'sectionSelect'; + this.reactive.dispatch(mutation, selectableIds); + } +} diff --git a/course/format/amd/src/local/content/section/cmitem.js b/course/format/amd/src/local/content/section/cmitem.js index e129425914a..5efcdb2c318 100644 --- a/course/format/amd/src/local/content/section/cmitem.js +++ b/course/format/amd/src/local/content/section/cmitem.js @@ -36,11 +36,17 @@ export default class extends DndCmItem { this.name = 'content_section_cmitem'; // Default query selectors. this.selectors = { + BULKSELECT: `[data-for='cmBulkSelect']`, + BULKCHECKBOX: `[data-bulkcheckbox]`, + CARD: `.activity-item`, DRAGICON: `.editing_move`, + INPLACEEDITABLE: `[data-inplaceeditablelink]`, }; // Most classes will be loaded later by DndCmItem. this.classes = { LOCKED: 'editinprogress', + HIDE: 'd-none', + SELECTED: 'selected', }; // We need our id to watch specific events. this.id = this.element.dataset.id; @@ -48,10 +54,13 @@ export default class extends DndCmItem { /** * Initial state ready method. + * @param {Object} state the state data */ - stateReady() { + stateReady(state) { this.configDragDrop(this.id); this.getElement(this.selectors.DRAGICON)?.classList.add(this.classes.DRAGICON); + this._refreshBulk({state}); + this.addEventListener(this.element, 'click', this._handleBulkModeClick); } /** @@ -63,6 +72,7 @@ export default class extends DndCmItem { return [ {watch: `cm[${this.id}]:deleted`, handler: this.unregister}, {watch: `cm[${this.id}]:updated`, handler: this._refreshCm}, + {watch: `bulk:updated`, handler: this._refreshBulk}, ]; } @@ -78,4 +88,101 @@ export default class extends DndCmItem { this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false); this.locked = element.locked; } + + /** + * Update the bulk editing interface. + * + * @param {object} param + * @param {Object} param.state the state data + */ + _refreshBulk({state}) { + const bulk = state.bulk; + // For now, dragging elements in bulk is not possible. + this.setDraggable(!bulk.enabled); + + this.getElement(this.selectors.BULKSELECT)?.classList.toggle(this.classes.HIDE, !bulk.enabled); + + const disabled = !this._isCmBulkEnabled(bulk); + const selected = this._isSelected(bulk); + this._refreshActivityCard(bulk, selected); + this._setCheckboxValue(selected, disabled); + } + + /** + * Update the activity card depending on the bulk selection. + * + * @param {Object} bulk the current bulk state data + * @param {Boolean} selected if the activity is selected. + */ + _refreshActivityCard(bulk, selected) { + this.getElement(this.selectors.INPLACEEDITABLE)?.classList.toggle(this.classes.HIDE, bulk.enabled); + this.getElement(this.selectors.CARD)?.classList.toggle(this.classes.SELECTED, selected); + this.element.classList.toggle(this.classes.SELECTED, selected); + } + + /** + * Modify the checkbox element. + * @param {Boolean} checked the new checked value + * @param {Boolean} disabled the new disabled value + */ + _setCheckboxValue(checked, disabled) { + const checkbox = this.getElement(this.selectors.BULKCHECKBOX); + if (!checkbox) { + return; + } + checkbox.checked = checked; + checkbox.disabled = disabled; + // Is selectable is used to easily scan the page for bulk checkboxes. + if (disabled) { + checkbox.removeAttribute('data-is-selectable'); + } else { + checkbox.dataset.isSelectable = 1; + } + } + + /** + * Handle the activity card click in bulk mode. + * @param {Event} event the click event + */ + _handleBulkModeClick(event) { + const selectElement = event.target.closest(this.selectors.BULKSELECT); + if (selectElement) { + // The select element checkbox execute a normal content action as + // any regular action button. This is because the chengechecker module + // is sniffing any form element and will with the checked value + // changing it twice. + return; + } + const bulk = this.reactive.get('bulk'); + if (!this._isCmBulkEnabled(bulk)) { + return; + } + event.preventDefault(); + const mutation = (this._isSelected(bulk)) ? 'cmUnselect' : 'cmSelect'; + this.reactive.dispatch(mutation, [this.id]); + } + + /** + * Check if cm bulk selection is available. + * @param {Object} bulk the current state bulk attribute + * @returns {Boolean} + */ + _isCmBulkEnabled(bulk) { + if (!bulk.enabled) { + return false; + } + return (bulk.selectedType === '' || bulk.selectedType === 'cm'); + } + + /** + * Check if the cm id is part of the current bulk selection. + * @param {Object} bulk the current state bulk attribute + * @returns {Boolean} + */ + _isSelected(bulk) { + if (bulk.selectedType !== 'cm') { + return false; + } + return bulk.selection.includes(this.id); + } } diff --git a/course/format/amd/src/local/content/section/header.js b/course/format/amd/src/local/content/section/header.js index e5130ec0c1d..ca1a25f5a9e 100644 --- a/course/format/amd/src/local/content/section/header.js +++ b/course/format/amd/src/local/content/section/header.js @@ -36,8 +36,16 @@ export default class extends DndSectionItem { create(descriptor) { // Optional component name for debugging. this.name = 'content_section_header'; - // We need our id to watch specific events. - + // Default query selectors. + this.selectors = { + ACTIONSMENU: `.section_action_menu`, + BULKSELECT: `[data-for='sectionBulkSelect']`, + BULKCHECKBOX: `[data-bulkcheckbox]`, + }; + this.classes = { + HIDE: 'd-none', + SELECTED: 'selected', + }; // Get main info from the descriptor. this.id = descriptor.id; this.section = descriptor.section; @@ -52,5 +60,91 @@ export default class extends DndSectionItem { */ stateReady(state) { this.configDragDrop(this.id, state, this.fullregion); + this._refreshBulk({state}); } -} \ No newline at end of file + + /** + * Component watchers. + * + * @returns {Array} of watchers + */ + getWatchers() { + return [ + {watch: `bulk:updated`, handler: this._refreshBulk}, + ]; + } + + /** + * Update a bulk options. + * + * @param {object} param + * @param {Object} param.state the state data + */ + _refreshBulk({state}) { + const bulk = state.bulk; + if (!this._isSectionBulkEditable()) { + return; + } + // For now, dragging elements in bulk is not possible. + this.setDraggable(!bulk.enabled); + this.getElement(this.selectors.BULKSELECT)?.classList.toggle(this.classes.HIDE, !bulk.enabled); + + const disabled = !this._isSectionBulkEnabled(bulk); + const selected = this._isSelected(bulk); + this.element.classList.toggle(this.classes.SELECTED, selected); + this._setCheckboxValue(selected, disabled); + } + + /** + * Modify the checkbox element. + * @param {Boolean} checked the new checked value + * @param {Boolean} disabled the new disabled value + */ + _setCheckboxValue(checked, disabled) { + const checkbox = this.getElement(this.selectors.BULKCHECKBOX); + if (!checkbox) { + return; + } + checkbox.checked = checked; + checkbox.disabled = disabled; + // Is selectable is used to easily scan the page for bulk checkboxes. + if (disabled) { + checkbox.removeAttribute('data-is-selectable'); + } else { + checkbox.dataset.isSelectable = 1; + } + } + + /** + * Check if cm bulk selection is available. + * @param {Object} bulk the current state bulk attribute + * @returns {Boolean} + */ + _isSectionBulkEnabled(bulk) { + if (!bulk.enabled) { + return false; + } + return (bulk.selectedType === '' || bulk.selectedType === 'section'); + } + + /** + * Check if the section is bulk editable. + * @return {Boolean} + */ + _isSectionBulkEditable() { + const section = this.reactive.get('section', this.id); + return section?.bulkeditable ?? false; + } + + /** + * Check if the cm id is part of the current bulk selection. + * @param {Object} bulk the current state bulk attribute + * @returns {Boolean} + */ + _isSelected(bulk) { + if (bulk.selectedType !== 'section') { + return false; + } + return bulk.selection.includes(this.id); + } +} diff --git a/course/format/amd/src/local/courseeditor/dndcmitem.js b/course/format/amd/src/local/courseeditor/dndcmitem.js index 3a0077e9d2b..d11fad0d148 100644 --- a/course/format/amd/src/local/courseeditor/dndcmitem.js +++ b/course/format/amd/src/local/courseeditor/dndcmitem.js @@ -56,6 +56,15 @@ export default class extends BaseComponent { } } + /** + * Enable or disable the draggable property. + * + * @param {bool} value the new draggable value + */ + setDraggable(value) { + this.dragdrop?.setDraggable(value); + } + // Drag and drop methods. /** diff --git a/course/format/amd/src/local/courseeditor/dndsectionitem.js b/course/format/amd/src/local/courseeditor/dndsectionitem.js index 18288b918a8..fe04c323b00 100644 --- a/course/format/amd/src/local/courseeditor/dndsectionitem.js +++ b/course/format/amd/src/local/courseeditor/dndsectionitem.js @@ -71,6 +71,17 @@ export default class extends BaseComponent { } } + /** + * Enable or disable the draggable property. + * + * @param {bool} value the new draggable value + */ + setDraggable(value) { + if (this.getDraggableData) { + this.dragdrop?.setDraggable(value); + } + } + // Drag and drop methods. /** diff --git a/course/format/classes/output/local/content.php b/course/format/classes/output/local/content.php index ea9da8a8376..e7d1ec4bf6f 100644 --- a/course/format/classes/output/local/content.php +++ b/course/format/classes/output/local/content.php @@ -54,6 +54,9 @@ class content implements named_templatable, renderable { /** @var string section selector class name */ protected $sectionselectorclass; + /** @var string bulk editor bar toolbox */ + protected $bulkedittoolsclass; + /** @var bool if uses add section */ protected $hasaddsection = true; @@ -70,6 +73,7 @@ class content implements named_templatable, renderable { $this->addsectionclass = $format->get_output_classname('content\\addsection'); $this->sectionnavigationclass = $format->get_output_classname('content\\sectionnavigation'); $this->sectionselectorclass = $format->get_output_classname('content\\sectionselector'); + $this->bulkedittoolsclass = $format->get_output_classname('content\\bulkedittools'); } /** @@ -117,6 +121,11 @@ class content implements named_templatable, renderable { $data->numsections = $addsection->export_for_template($output); } + if ($format->show_editor()) { + $bulkedittools = new $this->bulkedittoolsclass($format); + $data->bulkedittools = $bulkedittools->export_for_template($output); + } + return $data; } diff --git a/course/format/classes/output/local/content/bulkedittoggler.php b/course/format/classes/output/local/content/bulkedittoggler.php new file mode 100644 index 00000000000..c66388157ce --- /dev/null +++ b/course/format/classes/output/local/content/bulkedittoggler.php @@ -0,0 +1,62 @@ +. + +namespace core_courseformat\output\local\content; + +use core\output\named_templatable; +use core_courseformat\base as course_format; +use core_courseformat\output\local\courseformat_named_templatable; +use renderable; + +/** + * Course bulk edit mode toggler button. + * + * @package core_courseformat + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class bulkedittoggler implements named_templatable, renderable { + use courseformat_named_templatable; + + /** @var core_courseformat\base the course format class */ + protected $format; + + /** + * Constructor. + * + * @param course_format $format the course format + */ + public function __construct(course_format $format) { + $this->format = $format; + } + + /** + * Export this data so it can be used as the context for a mustache template (core/inplace_editable). + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return stdClass data context for a mustache template + */ + public function export_for_template(\renderer_base $output) { + $format = $this->format; + $course = $format->get_course(); + + $data = (object)[ + 'id' => $course->id, + ]; + + return $data; + } +} diff --git a/course/format/classes/output/local/content/bulkedittools.php b/course/format/classes/output/local/content/bulkedittools.php new file mode 100644 index 00000000000..2a23897939a --- /dev/null +++ b/course/format/classes/output/local/content/bulkedittools.php @@ -0,0 +1,101 @@ +. + +namespace core_courseformat\output\local\content; + +use core\output\named_templatable; +use core_courseformat\base as course_format; +use core_courseformat\output\local\courseformat_named_templatable; +use renderable; +use stdClass; + +/** + * Contains the bulk editor tools bar. + * + * @package core_courseformat + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class bulkedittools implements named_templatable, renderable { + use courseformat_named_templatable; + + /** @var core_courseformat\base the course format class */ + protected $format; + + /** + * Constructor. + * + * @param course_format $format the course format + */ + public function __construct(course_format $format) { + $this->format = $format; + } + + /** + * Export this data so it can be used as the context for a mustache template (core/inplace_editable). + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return stdClass data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + $format = $this->format; + $course = $format->get_course(); + + $data = (object)[ + 'id' => $course->id, + 'actions' => $this->get_toolbar_actions(), + ]; + $data->hasactions = !empty($data->actions); + return $data; + } + + /** + * Get the toolbar actions. + * @return array the array of buttons + */ + protected function get_toolbar_actions(): array { + return array_merge( + array_values($this->section_control_items()), + array_values($this->cm_control_items()), + ); + } + + /** + * Generate the bulk edit control items of a course module. + * + * Format plugins can override the method to add or remove elements + * from the toolbar. + * + * @return array of edit control items + */ + protected function cm_control_items(): array { + $controls = []; + return $controls; + } + + /** + * Generate the bulk edit control items of a section. + * + * Format plugins can override the method to add or remove elements + * from the toolbar. + * + * @return array of edit control items + */ + protected function section_control_items(): array { + $controls = []; + return $controls; + } +} diff --git a/course/format/classes/output/local/content/cm.php b/course/format/classes/output/local/content/cm.php index 9bb6ea48634..2e1b89003de 100644 --- a/course/format/classes/output/local/content/cm.php +++ b/course/format/classes/output/local/content/cm.php @@ -108,6 +108,7 @@ class cm implements named_templatable, renderable { 'activityname' => $mod->get_formatted_name(), 'textclasses' => $displayoptions['textclasses'], 'classlist' => [], + 'cmid' => $mod->id, ]; // Add partial data segments. diff --git a/course/format/classes/output/section_renderer.php b/course/format/classes/output/section_renderer.php index db939119089..c3133997dbc 100644 --- a/course/format/classes/output/section_renderer.php +++ b/course/format/classes/output/section_renderer.php @@ -207,6 +207,20 @@ abstract class section_renderer extends core_course_renderer { return ''; } + /** + * Render the enable bulk editing button. + * @param course_format $format the course format + * @return string|null the enable bulk button HTML (or null if no bulk available). + */ + public function bulk_editing_button(course_format $format): ?string { + if (!$format->show_editor() || !$format->supports_components()) { + return null; + } + $widgetclass = $format->get_output_classname('content\\bulkedittoggler'); + $widget = new $widgetclass($format); + return $this->render($widget); + } + /** * Generate the edit control action menu * diff --git a/course/format/templates/local/content.mustache b/course/format/templates/local/content.mustache index ed049b92589..d27b44a4be8 100644 --- a/course/format/templates/local/content.mustache +++ b/course/format/templates/local/content.mustache @@ -33,8 +33,10 @@ "hasname": "true" }, "id": 3, + "cmid": 3, "module": "forum", - "extraclasses": "newmessages" + "extraclasses": "newmessages", + "anchor": "module-3" } } ], @@ -61,9 +63,11 @@ "cmname": "Another forum", "hasname": "true" }, - "id": 3, + "id": 4, + "cmid": 4, "module": "forum", - "extraclasses": "newmessages" + "extraclasses": "newmessages", + "anchor": "module-4" } } ], @@ -90,8 +94,10 @@ "hasname": "true" }, "id": 5, + "cmid": 5, "module": "forum", - "extraclasses": "newmessages" + "extraclasses": "newmessages", + "anchor": "module-5" } } ], @@ -129,8 +135,8 @@ }, "sectionreturn": 1, "singlesection": { - "num": 1, - "id": 35, + "num": 5, + "id": 37, "header": { "name": "Single Section Example", "url": "#" @@ -143,9 +149,11 @@ "cmname": "Assign example", "hasname": "true" }, - "id": 4, + "id": 6, + "cmid": 6, "module": "assign", - "extraclasses": "" + "extraclasses": "", + "anchor": "module-6" } } ], @@ -199,6 +207,11 @@ {{> core_courseformat/local/content/addsection}} {{/ core_courseformat/local/content/addsection}} {{/numsections}} + {{#bulkedittools}} + {{$ core_courseformat/local/content/bulkedittools}} + {{> core_courseformat/local/content/bulkedittools}} + {{/ core_courseformat/local/content/bulkedittools}} + {{/bulkedittools}} {{#js}} require(['core_courseformat/local/content'], function(component) { diff --git a/course/format/templates/local/content/bulkedittoggler.mustache b/course/format/templates/local/content/bulkedittoggler.mustache new file mode 100644 index 00000000000..8e5a62ff5eb --- /dev/null +++ b/course/format/templates/local/content/bulkedittoggler.mustache @@ -0,0 +1,37 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_courseformat/content/bulkedittoggler + + Displays the bulk actions button in the page header. + + Example context (json): + { + } +}} + +{{#js}} +require(['core_courseformat/local/content/bulkedittoggler'], function(component) { + component.init('#bulk-enable-{{uniqid}}'); +}); +{{/js}} diff --git a/course/format/templates/local/content/bulkedittools.mustache b/course/format/templates/local/content/bulkedittools.mustache new file mode 100644 index 00000000000..0bd0faf8992 --- /dev/null +++ b/course/format/templates/local/content/bulkedittools.mustache @@ -0,0 +1,92 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_courseformat/content/bulkedittoggler + + Displays the bulk actions button in the page header. + + Example context (json): + { + "id": 42, + "hasactions": true, + "actions": [ + { + "icon": "i/delete", + "action": "cmDelete", + "name": "delete", + "bulk": "cm", + "title": "Delete activities" + } + ] + } +}} +{{< core/sticky_footer }} + {{$ stickyclasses }} justify-content-between {{/ stickyclasses }} + {{$ disable }} data-disable="true" {{/ disable }} + {{$ extradata }} data-for="bulkedittools" {{/ extradata }} + {{$ stickycontent }} +
+ + +
+
+ {{^hasactions}} + {{#str}} nobulkaction, core_courseformat {{/str}} + {{/hasactions}} + {{#hasactions}} + + {{/hasactions}} +
+
+
+ +
+
+ {{#str}} bulkselection, core_courseformat, 0 {{/str}} +
+
+ {{/ stickycontent }} +{{/ core/sticky_footer }} +{{#js}} +require(['core_courseformat/local/content/bulkedittools'], function(component) { + component.init('[data-for="bulkedittools"]'); +}); +{{/js}} diff --git a/course/format/templates/local/content/cm.mustache b/course/format/templates/local/content/cm.mustache index 6096d74ac60..e7cc20a504c 100644 --- a/course/format/templates/local/content/cm.mustache +++ b/course/format/templates/local/content/cm.mustache @@ -64,6 +64,9 @@
+ {{$ core_courseformat/local/content/cm/bulkselect }} + {{> core_courseformat/local/content/cm/bulkselect }} + {{/ core_courseformat/local/content/cm/bulkselect }} {{! Place the actual content of the activity-item in a separate template to make it easier for other formats to add additional content to the activity wrapper. diff --git a/course/format/templates/local/content/cm/bulkselect.mustache b/course/format/templates/local/content/cm/bulkselect.mustache new file mode 100644 index 00000000000..02a4a7fe085 --- /dev/null +++ b/course/format/templates/local/content/cm/bulkselect.mustache @@ -0,0 +1,39 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_courseformat/local/content/cm/bulkselect + + Displays an activity bulk selector. + + Example context (json): + { + "activityname": "Activity example", + "cmid": 42 + } +}} +
+ + +
diff --git a/course/format/templates/local/content/section/bulkselect.mustache b/course/format/templates/local/content/section/bulkselect.mustache new file mode 100644 index 00000000000..fe2151e36aa --- /dev/null +++ b/course/format/templates/local/content/section/bulkselect.mustache @@ -0,0 +1,39 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_courseformat/local/content/section/bulkselect + + Displays an section bulk selector. + + Example context (json): + { + "id": 35, + "name": "Section title" + } +}} +
+ + +
diff --git a/course/format/templates/local/content/section/header.mustache b/course/format/templates/local/content/section/header.mustache index db842cdad49..6b267b79b5c 100644 --- a/course/format/templates/local/content/section/header.mustache +++ b/course/format/templates/local/content/section/header.mustache @@ -36,6 +36,9 @@ {{/headerdisplaymultipage}} {{^headerdisplaymultipage}} + {{$ core_courseformat/local/content/section/bulkselect }} + {{> core_courseformat/local/content/section/bulkselect }} + {{/ core_courseformat/local/content/section/bulkselect }} {{#sitehome}}

{{{title}}} diff --git a/course/format/tests/behat/bulk_select.feature b/course/format/tests/behat/bulk_select.feature new file mode 100644 index 00000000000..c937fc31ead --- /dev/null +++ b/course/format/tests/behat/bulk_select.feature @@ -0,0 +1,118 @@ +@core @core_courseformat @show_editor @javascript +Feature: Bulk activity and section selection. + In order to edit the course activities + As a teacher with capability 'moodle/course:manageactivities' + I need to be able to bulk select activities or sections. + + Background: + Given the following "course" exists: + | fullname | Course 1 | + | shortname | C1 | + | category | 0 | + | numsections | 2 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | section | + | assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 | + | assign | Activity sample 2 | Test assignment description | C1 | sample2 | 1 | + | assign | Activity sample 3 | Test assignment description | C1 | sample3 | 2 | + | assign | Activity sample 4 | Test assignment description | C1 | sample4 | 2 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And I am on the "C1" "Course" page logged in as "teacher1" + And I turn editing mode on + + Scenario: Enable and disable bulk editing + When I click on "Bulk edit" "button" + Then I should see "0 selected" in the "sticky-footer" "region" + And the focused element is "Select section Topic 1" "checkbox" + And I click on "Close bulk edit" "button" in the "sticky-footer" "region" + And "sticky-footer" "region" should not be visible + And the focused element is "Bulk edit" "button" + + Scenario: Selecting activities disable section selection + Given I click on "Bulk edit" "button" + And I should see "0 selected" in the "sticky-footer" "region" + When I click on "Select activity Activity sample 1" "checkbox" + And I should see "1 selected" in the "sticky-footer" "region" + Then the "Select section Topic 1" "checkbox" should be disabled + + Scenario: Selecting sections disable activity selection + Given I click on "Bulk edit" "button" + And I should see "0 selected" in the "sticky-footer" "region" + When I click on "Select section Topic 1" "checkbox" + And I should see "1 selected" in the "sticky-footer" "region" + Then the "Select activity Activity sample 1" "checkbox" should be disabled + + Scenario: Disable bulk resets the selection + Given I click on "Bulk edit" "button" + And I click on "Select activity Activity sample 1" "checkbox" + And I click on "Select activity Activity sample 2" "checkbox" + And I should see "2 selected" in the "sticky-footer" "region" + When I click on "Close bulk edit" "button" in the "sticky-footer" "region" + And I click on "Bulk edit" "button" + Then I should see "0 selected" in the "sticky-footer" "region" + + Scenario: Select all is disabled until an activity is selected + Given I click on "Bulk edit" "button" + And the "Select all" "checkbox" should be disabled + When I click on "Select activity Activity sample 1" "checkbox" + And I should see "1 selected" in the "sticky-footer" "region" + Then the "Select all" "checkbox" should be enabled + + Scenario: Select all is disabled until a section is selected + Given I click on "Bulk edit" "button" + And the "Select all" "checkbox" should be disabled + When I click on "Select section Topic 1" "checkbox" + And I should see "1 selected" in the "sticky-footer" "region" + Then the "Select all" "checkbox" should be enabled + + Scenario: Select all when an activity is selected will select all activities + Given I click on "Bulk edit" "button" + And I click on "Select activity Activity sample 1" "checkbox" + And I should see "1 selected" in the "sticky-footer" "region" + And the "Select all" "checkbox" should be enabled + When I click on "Select all" "checkbox" in the "sticky-footer" "region" + Then I should see "4 selected" in the "sticky-footer" "region" + + Scenario: Select all when a section is selected will select all sections + Given I click on "Bulk edit" "button" + And I click on "Select section Topic 1" "checkbox" + And I should see "1 selected" in the "sticky-footer" "region" + And the "Select all" "checkbox" should be enabled + When I click on "Select all" "checkbox" in the "sticky-footer" "region" + Then I should see "2 selected" in the "sticky-footer" "region" + + Scenario: Click on a select all with all sections selected unselects all sections + Given I click on "Bulk edit" "button" + And I click on "Select section Topic 1" "checkbox" + And I click on "Select section Topic 2" "checkbox" + And I should see "2 selected" in the "sticky-footer" "region" + And the "Select all" "checkbox" should be enabled + When I click on "Select all" "checkbox" in the "sticky-footer" "region" + Then I should see "0 selected" in the "sticky-footer" "region" + And the focused element is "Select section Topic 1" "checkbox" + + Scenario: Click on a select all with all activity selected unselects all activities + Given I click on "Bulk edit" "button" + And I click on "Select activity Activity sample 1" "checkbox" + And I click on "Select activity Activity sample 2" "checkbox" + And I click on "Select activity Activity sample 3" "checkbox" + And I click on "Select activity Activity sample 4" "checkbox" + And I should see "4 selected" in the "sticky-footer" "region" + And the "Select all" "checkbox" should be enabled + When I click on "Select all" "checkbox" in the "sticky-footer" "region" + Then I should see "0 selected" in the "sticky-footer" "region" + And the focused element is "Select section Topic 1" "checkbox" + + Scenario: Click an activity name in bulk mode select and unselects the activity + Given I click on "Bulk edit" "button" + And I should see "0 selected" in the "sticky-footer" "region" + When I click on "Activity sample 1" "link" in the "Topic 1" "section" + And I click on "Activity sample 2" "link" in the "Topic 1" "section" + And I should see "2 selected" in the "sticky-footer" "region" + Then I click on "Activity sample 1" "link" in the "Topic 1" "section" + And I should see "1 selected" in the "sticky-footer" "region" diff --git a/course/format/upgrade.txt b/course/format/upgrade.txt index 67b6d6c7d55..d3b30556675 100644 --- a/course/format/upgrade.txt +++ b/course/format/upgrade.txt @@ -10,6 +10,12 @@ Overview of this plugin type at http://docs.moodle.org/dev/Course_formats in the course content. Instead of using adhoc YUI methods and webservice, the new fragment methods are: - core_courseformat_output_fragment_cmitem - core_courseformat_output_fragment_section +* New methods and outputs added for bulk editing (only available for formats compatible with reactive components): + - Mutations for editing the bulk data: bulkEnable, bulkReset, cmSelect, cmUnselect, sectionSelect and sectionUnselect. + - Output classes overridable by the plugins: content\bulkedittools, content\bulkedittoggler + - Renderer method: core_courseformat\output\section_renderer::bulk_editing_button + - New overridable checkboxes: content/cm/bulkselect.mustache and content/section/bulkselect.mustache +* Plugins can use the CSS class "bulk-hidden" to hide elements when the bulk editing is enabled. === 4.1 === * New \core_courseformat\stateupdates methods add_section_remove() and add_cm_remove() have been added to replace diff --git a/course/view.php b/course/view.php index 659834c565a..594e5ffd489 100644 --- a/course/view.php +++ b/course/view.php @@ -137,7 +137,7 @@ // Preload course format renderer before output starts. // This is a little hacky but necessary since // format.php is not included until after output starts - $format->get_renderer($PAGE); + $renderer = $format->get_renderer($PAGE); if ($reset_user_allowed_editing) { // ugly hack @@ -236,6 +236,12 @@ $PAGE->set_title(get_string('coursetitle', 'moodle', array('course' => $course->fullname))); } + // Add bulk editing control. + $bulkbutton = $renderer->bulk_editing_button($format); + if (!empty($bulkbutton)) { + $PAGE->add_header_action($bulkbutton); + } + $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); diff --git a/lang/en/courseformat.php b/lang/en/courseformat.php index 674fb1f0cf7..4a581d86092 100644 --- a/lang/en/courseformat.php +++ b/lang/en/courseformat.php @@ -22,6 +22,13 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['bulkedit'] = 'Bulk edit'; +$string['bulkeditoff'] = 'Close bulk edit'; +$string['bulkcancel'] = 'Close bulk editing'; +$string['bulkselection'] = '{$a} selected'; $string['courseindex'] = 'Course index'; +$string['nobulkaction'] = 'No bulk actions available'; $string['preference:coursesectionspreferences'] = 'Section user preferences for course {$a}'; $string['privacy:metadata:preference:coursesectionspreferences'] = 'Section user preferences like collapsed and expanded.'; +$string['selectcm'] = 'Select activity {$a}'; +$string['selectsection'] = 'Select section {$a}'; diff --git a/theme/boost/scss/moodle/course.scss b/theme/boost/scss/moodle/course.scss index ac236fc267a..2816e542ae8 100644 --- a/theme/boost/scss/moodle/course.scss +++ b/theme/boost/scss/moodle/course.scss @@ -1500,7 +1500,8 @@ $activity-add-hover: theme-color-level('primary', -10) !default; cursor: pointer; } - &:hover { + &:hover, + &.selected { @include alert-variant($activity-item-hover, $activity-item-border, $activity-item-color); .description .course-description-item, @@ -1578,3 +1579,20 @@ $activity-add-hover: theme-color-level('primary', -10) !default; .bulkenabled .bulk-hidden { display: none !important; // stylelint-disable-line declaration-no-important } + +.activity-item .bulkselect { + position: absolute; + left: -2rem; +} + +.course-section-header .bulkselect { + left: -2rem; + position: relative; + width: 0; +} + +@include media-breakpoint-down(sm) { + .bulkenabled .course-content { + margin-left: 2rem; + } +} diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index f97945c2114..039f06b5208 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -14949,17 +14949,19 @@ span.editinstructions { cursor: move; } .editing .activity-item .a { cursor: pointer; } - .editing .activity-item:hover { + .editing .activity-item:hover, .editing .activity-item.selected { color: #1d2125; background-color: #f5f9fc; border-color: #3584c9; } - .editing .activity-item:hover hr { + .editing .activity-item:hover hr, .editing .activity-item.selected hr { border-top-color: #3077b5; } - .editing .activity-item:hover .alert-link { + .editing .activity-item:hover .alert-link, .editing .activity-item.selected .alert-link { color: #070808; } .editing .activity-item:hover .description .course-description-item, .editing .activity-item:hover .activityiconcontainer, - .editing .activity-item:hover .badge { + .editing .activity-item:hover .badge, .editing .activity-item.selected .description .course-description-item, + .editing .activity-item.selected .activityiconcontainer, + .editing .activity-item.selected .badge { mix-blend-mode: multiply; } .section .draggable .activity-item .dragicon { @@ -15015,6 +15017,19 @@ span.editinstructions { .bulkenabled .bulk-hidden { display: none !important; } +.activity-item .bulkselect { + position: absolute; + left: -2rem; } + +.course-section-header .bulkselect { + left: -2rem; + position: relative; + width: 0; } + +@media (max-width: 767.98px) { + .bulkenabled .course-content { + margin-left: 2rem; } } + /* Anchor link offset fix. This makes hash links scroll 60px down to account for the fixed header. */ :target { scroll-margin-top: 70px; } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index fda085bec4f..1723fb0ae66 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -14949,17 +14949,19 @@ span.editinstructions { cursor: move; } .editing .activity-item .a { cursor: pointer; } - .editing .activity-item:hover { + .editing .activity-item:hover, .editing .activity-item.selected { color: #1d2125; background-color: #f5f9fc; border-color: #3584c9; } - .editing .activity-item:hover hr { + .editing .activity-item:hover hr, .editing .activity-item.selected hr { border-top-color: #3077b5; } - .editing .activity-item:hover .alert-link { + .editing .activity-item:hover .alert-link, .editing .activity-item.selected .alert-link { color: #070808; } .editing .activity-item:hover .description .course-description-item, .editing .activity-item:hover .activityiconcontainer, - .editing .activity-item:hover .badge { + .editing .activity-item:hover .badge, .editing .activity-item.selected .description .course-description-item, + .editing .activity-item.selected .activityiconcontainer, + .editing .activity-item.selected .badge { mix-blend-mode: multiply; } .section .draggable .activity-item .dragicon { @@ -15015,6 +15017,19 @@ span.editinstructions { .bulkenabled .bulk-hidden { display: none !important; } +.activity-item .bulkselect { + position: absolute; + left: -2rem; } + +.course-section-header .bulkselect { + left: -2rem; + position: relative; + width: 0; } + +@media (max-width: 767.98px) { + .bulkenabled .course-content { + margin-left: 2rem; } } + /* Anchor link offset fix. This makes hash links scroll 60px down to account for the fixed header. */ :target { scroll-margin-top: 60px; }