MDL-76783 core_courseformat: add bulk editing interface

This commit is contained in:
Ferran Recio 2023-01-13 12:52:50 +01:00
parent c9a8713539
commit 9930b7a2e6
37 changed files with 1235 additions and 30 deletions

View File

@ -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 <ferran@moodle.com>
* @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

View File

@ -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 <http://www.gnu.org/licenses/>.\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 <ferran@moodle.com>\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"}

View File

@ -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 <ferran@moodle.com>
* @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

File diff suppressed because one or more lines are too long

View File

@ -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 <ferran@moodle.com>
* @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

File diff suppressed because one or more lines are too long

View File

@ -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 <ferran@moodle.com>
* @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

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,6 @@ define("core_courseformat/local/courseeditor/dndcmitem",["exports","core/reactiv
* @copyright 2021 Ferran Recio <ferran@moodle.com>
* @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

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,6 @@ define("core_courseformat/local/courseeditor/dndsectionitem",["exports","core/re
* @copyright 2021 Ferran Recio <ferran@moodle.com>
* @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

File diff suppressed because one or more lines are too long

View File

@ -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 <http://www.gnu.org/licenses/>.
/**
* The bulk editor toggler button control.
*
* @module core_courseformat/local/content/bulkedittoggler
* @class core_courseformat/local/content/bulkedittoggler
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
/**
* The bulk editor tools bar.
*
* @module core_courseformat/local/content/bulkedittools
* @class core_courseformat/local/content/bulkedittools
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @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);
}
}

View File

@ -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);
}
}

View File

@ -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});
}
}
/**
* 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);
}
}

View File

@ -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.
/**

View File

@ -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.
/**

View File

@ -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;
}

View File

@ -0,0 +1,62 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <ferran@moodle.com>
* @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;
}
}

View File

@ -0,0 +1,101 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <ferran@moodle.com>
* @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;
}
}

View File

@ -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.

View File

@ -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
*

View File

@ -33,8 +33,10 @@
"hasname": "true"
},
"id": 3,
"cmid": 3,
"module": "forum",
"extraclasses": "newmessages"
"extraclasses": "newmessages",
"anchor": "module-3"
}
}
],
@ -61,9 +63,11 @@
"cmname": "<a class=\"aalink\" href=\"#\"><span class=\"instancename\">Another forum</span></a>",
"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": "<a class=\"aalink\" href=\"#\"><span class=\"instancename\">Assign example</span></a>",
"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}}
</div>
{{#js}}
require(['core_courseformat/local/content'], function(component) {

View File

@ -0,0 +1,37 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_courseformat/content/bulkedittoggler
Displays the bulk actions button in the page header.
Example context (json):
{
}
}}
<button
id="bulk-enable-{{uniqid}}"
class="bulkEnable btn"
data-for="enableBulk"
>
{{#str}} bulkedit, core_courseformat {{/str}} {{#pix}} i/edit, core {{/pix}}
</button>
{{#js}}
require(['core_courseformat/local/content/bulkedittoggler'], function(component) {
component.init('#bulk-enable-{{uniqid}}');
});
{{/js}}

View File

@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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 }}
<div class="form-check">
<input type="checkbox" class="form-check-input" id="selectall" data-for="selectall" disabled>
<label class="form-check-label" for="selectall">
{{#str}} selectall {{/str}}
</label>
</div>
<div data-for="bulktools">
{{^hasactions}}
{{#str}} nobulkaction, core_courseformat {{/str}}
{{/hasactions}}
{{#hasactions}}
<ul class="actions nav" data-for="bulkactions">
{{#actions}}
<li class="nav-item">
<button
class="btn py-0 d-flex flex-column"
data-action="{{action}}"
data-bulk="{{bulk}}"
data-for="bulkaction"
{{#title}} title="{{title}}" {{/title}}
>
<div class="w-100 pl-2">{{#pix}}{{icon}}{{/pix}}</div>
<div>{{name}}</div>
</button>
</li>
{{/actions}}
</ul>
{{/hasactions}}
</div>
<div class="d-flex flex-column">
<div class="ml-auto">
<button
class="btn pr-0 pb-0"
data-action="bulkcancel"
data-for="bulkcancel"
title="{{#str}} bulkeditoff, core_courseformat {{/str}}"
>
{{#pix}} e/cancel, core {{/pix}}
</button>
</div>
<div data-for="bulkcount">
{{#str}} bulkselection, core_courseformat, 0 {{/str}}
</div>
</div>
{{/ stickycontent }}
{{/ core/sticky_footer }}
{{#js}}
require(['core_courseformat/local/content/bulkedittools'], function(component) {
component.init('[data-for="bulkedittools"]');
});
{{/js}}

View File

@ -64,6 +64,9 @@
<div class="activity-item {{#modstealth}}hiddenactivity{{/modstealth}}{{!
}}{{#modhiddenfromstudents}}hiddenactivity{{/modhiddenfromstudents}}{{!
}}{{#modinline}}activityinline{{/modinline}}" data-activityname="{{activityname}}">
{{$ 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.

View File

@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_courseformat/local/content/cm/bulkselect
Displays an activity bulk selector.
Example context (json):
{
"activityname": "Activity example",
"cmid": 42
}
}}
<div class="bulkselect d-none" data-for="cmBulkSelect">
<input
id="cmCheckbox{{cmid}}"
type="checkbox"
data-id="{{cmid}}"
data-action="toggleSelectionCm"
data-bulkcheckbox="1"
/>
<label class="sr-only" for="cmCheckbox{{cmid}}">
{{#str}} selectcm, core_courseformat, {{activityname}}{{/str}}
</label>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_courseformat/local/content/section/bulkselect
Displays an section bulk selector.
Example context (json):
{
"id": 35,
"name": "Section title"
}
}}
<div class="bulkselect align-self-center d-none" data-for="sectionBulkSelect">
<input
id="sectionCheckbox{{id}}"
type="checkbox"
data-id="{{id}}"
data-action="toggleSelectionSection"
data-bulkcheckbox="1"
/>
<label class="sr-only" for="sectionCheckbox{{id}}">
{{#str}} selectsection, core_courseformat, {{name}}{{/str}}
</label>
</div>

View File

@ -36,6 +36,9 @@
</h3>
{{/headerdisplaymultipage}}
{{^headerdisplaymultipage}}
{{$ core_courseformat/local/content/section/bulkselect }}
{{> core_courseformat/local/content/section/bulkselect }}
{{/ core_courseformat/local/content/section/bulkselect }}
{{#sitehome}}
<h2 id="sectionid-{{id}}-title" class="sectionname">
{{{title}}}

View File

@ -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"

View File

@ -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

View File

@ -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();

View File

@ -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}';

View File

@ -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;
}
}

View File

@ -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; }

View File

@ -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; }