Merge branch 'MDL-76783-master' of https://github.com/ferranrecio/moodle

This commit is contained in:
Andrew Nicols 2023-02-06 12:57:43 +08:00
commit 98b8ee2e1e
61 changed files with 1651 additions and 55 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

@ -9,6 +9,6 @@ define("core_courseformat/local/courseeditor/courseeditor",["exports","core/reac
* @class core_courseformat/local/courseeditor/courseeditor
* @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,_notification=_interopRequireDefault(_notification),_exporter=_interopRequireDefault(_exporter),_log=_interopRequireDefault(_log),_ajax=_interopRequireDefault(_ajax),Storage=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Storage);class _default extends _reactive.Reactive{constructor(){super(...arguments),_defineProperty(this,"stateKey",1),_defineProperty(this,"sectionReturn",0)}async loadCourse(courseId,serverStateKey){if(this.courseId)throw new Error("Cannot load ".concat(courseId,", course already loaded with id ").concat(this.courseId));let stateData;serverStateKey||(serverStateKey="invalidStateKey_".concat(Date.now())),this._editing=!1,this._supportscomponents=!1,this.courseId=courseId;const storeStateKey=Storage.get("course/".concat(courseId,"/stateKey"));try{this.isEditing||serverStateKey!=storeStateKey||(stateData=JSON.parse(Storage.get("course/".concat(courseId,"/staticState")))),stateData||(stateData=await this.getServerCourseState())}catch(error){return _log.default.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR"),void _log.default.error(error)}if(this.setInitialState(stateData),this.isEditing)this.stateKey=null;else{const newState=JSON.stringify(stateData);var _stateData$course$sta,_stateData,_stateData$course;if(Storage.get("course/".concat(courseId,"/staticState"))!==newState||storeStateKey!==serverStateKey)Storage.set("course/".concat(courseId,"/staticState"),newState),Storage.set("course/".concat(courseId,"/stateKey"),null!==(_stateData$course$sta=null===(_stateData=stateData)||void 0===_stateData||null===(_stateData$course=_stateData.course)||void 0===_stateData$course?void 0:_stateData$course.statekey)&&void 0!==_stateData$course$sta?_stateData$course$sta:serverStateKey);this.stateKey=Storage.get("course/".concat(courseId,"/stateKey"))}}setViewFormat(setup){var _setup$editing,_setup$supportscompon;this._editing=null!==(_setup$editing=setup.editing)&&void 0!==_setup$editing&&_setup$editing,this._supportscomponents=null!==(_setup$supportscompon=setup.supportscomponents)&&void 0!==_setup$supportscompon&&_setup$supportscompon}async getServerCourseState(){const courseState=await _ajax.default.call([{methodname:"core_courseformat_get_state",args:{courseid:this.courseId}}])[0];return{course:{},section:[],cm:[],...JSON.parse(courseState)}}get isEditing(){var _this$_editing;return null!==(_this$_editing=this._editing)&&void 0!==_this$_editing&&_this$_editing}getExporter(){return new _exporter.default(this)}get supportComponents(){var _this$_supportscompon;return null!==(_this$_supportscompon=this._supportscomponents)&&void 0!==_this$_supportscompon&&_this$_supportscompon}getStorageValue(key){if(this.isEditing||!this.stateKey)return!1;const dataJson=Storage.get("course/".concat(this.courseId,"/").concat(key));if(!dataJson)return!1;try{const data=JSON.parse(dataJson);return(null==data?void 0:data.stateKey)===this.stateKey&&data.value}catch(error){return!1}}setStorageValue(key,value){if(this.isEditing)return!1;const data={stateKey:this.stateKey,value:value};return Storage.set("course/".concat(this.courseId,"/").concat(key),JSON.stringify(data))}async dispatch(){try{await super.dispatch(...arguments)}catch(error){_notification.default.exception(error),super.dispatch("unlockAll")}}}return _exports.default=_default,_exports.default}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=_interopRequireDefault(_notification),_exporter=_interopRequireDefault(_exporter),_log=_interopRequireDefault(_log),_ajax=_interopRequireDefault(_ajax),Storage=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Storage);class _default extends _reactive.Reactive{constructor(){super(...arguments),_defineProperty(this,"stateKey",1),_defineProperty(this,"sectionReturn",0)}async loadCourse(courseId,serverStateKey){if(this.courseId)throw new Error("Cannot load ".concat(courseId,", course already loaded with id ").concat(this.courseId));let stateData;serverStateKey||(serverStateKey="invalidStateKey_".concat(Date.now())),this._editing=!1,this._supportscomponents=!1,this.courseId=courseId;const storeStateKey=Storage.get("course/".concat(courseId,"/stateKey"));try{this.isEditing||serverStateKey!=storeStateKey||(stateData=JSON.parse(Storage.get("course/".concat(courseId,"/staticState")))),stateData||(stateData=await this.getServerCourseState())}catch(error){return _log.default.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR"),void _log.default.error(error)}if(stateData.bulk={enabled:!1,selectedType:"",selection:[]},this.setInitialState(stateData),this.isEditing)this.stateKey=null;else{const newState=JSON.stringify(stateData);var _stateData$course$sta,_stateData,_stateData$course;if(Storage.get("course/".concat(courseId,"/staticState"))!==newState||storeStateKey!==serverStateKey)Storage.set("course/".concat(courseId,"/staticState"),newState),Storage.set("course/".concat(courseId,"/stateKey"),null!==(_stateData$course$sta=null===(_stateData=stateData)||void 0===_stateData||null===(_stateData$course=_stateData.course)||void 0===_stateData$course?void 0:_stateData$course.statekey)&&void 0!==_stateData$course$sta?_stateData$course$sta:serverStateKey);this.stateKey=Storage.get("course/".concat(courseId,"/stateKey"))}}setViewFormat(setup){var _setup$editing,_setup$supportscompon;this._editing=null!==(_setup$editing=setup.editing)&&void 0!==_setup$editing&&_setup$editing,this._supportscomponents=null!==(_setup$supportscompon=setup.supportscomponents)&&void 0!==_setup$supportscompon&&_setup$supportscompon}async getServerCourseState(){const courseState=await _ajax.default.call([{methodname:"core_courseformat_get_state",args:{courseid:this.courseId}}])[0];return{course:{},section:[],cm:[],...JSON.parse(courseState)}}get isEditing(){var _this$_editing;return null!==(_this$_editing=this._editing)&&void 0!==_this$_editing&&_this$_editing}getExporter(){return new _exporter.default(this)}get supportComponents(){var _this$_supportscompon;return null!==(_this$_supportscompon=this._supportscomponents)&&void 0!==_this$_supportscompon&&_this$_supportscompon}getStorageValue(key){if(this.isEditing||!this.stateKey)return!1;const dataJson=Storage.get("course/".concat(this.courseId,"/").concat(key));if(!dataJson)return!1;try{const data=JSON.parse(dataJson);return(null==data?void 0:data.stateKey)===this.stateKey&&data.value}catch(error){return!1}}setStorageValue(key,value){if(this.isEditing)return!1;const data={stateKey:this.stateKey,value:value};return Storage.set("course/".concat(this.courseId,"/").concat(key),JSON.stringify(data))}async dispatch(){try{await super.dispatch(...arguments)}catch(error){_notification.default.exception(error),super.dispatch("unlockAll")}}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=courseeditor.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -393,6 +393,36 @@ export default class extends BaseComponent {
}
}
/**
* Handle a toggle cm selection.
*
* @param {Element} target the dispatch action element
*/
async _requestToggleSelectionCm(target) {
const cmId = target.dataset.id;
if (!cmId) {
return;
}
const value = target.checked ?? false;
const mutation = (value) ? 'cmSelect' : 'cmUnselect';
this.reactive.dispatch(mutation, [cmId]);
}
/**
* Handle a toggle section selection.
*
* @param {Element} target the dispatch action element
*/
async _requestToggleSelectionSection(target) {
const sectionId = target.dataset.id;
if (!sectionId) {
return;
}
const value = target.checked ?? false;
const mutation = (value) ? 'sectionSelect' : 'sectionUnselect';
this.reactive.dispatch(mutation, [sectionId]);
}
/**
* Basic mutation action helper.
*

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

@ -103,6 +103,13 @@ export default class extends Reactive {
return;
}
// The bulk editing only applies to the frontend and the state data is not created in the backend.
stateData.bulk = {
enabled: false,
selectedType: '',
selection: [],
};
this.setInitialState(stateData);
// In editing mode, the session cache is considered dirty always.

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

@ -479,6 +479,126 @@ export default class {
return collapsedSectionIds;
}
/**
* Enable/disable bulk editing.
*
* Note: reenabling the bulk will clean the current selection.
*
* @param {StateManager} stateManager the current state manager
* @param {Boolean} enabled the new bulk state.
*/
bulkEnable(stateManager, enabled) {
const state = stateManager.state;
stateManager.setReadOnly(false);
state.bulk.enabled = enabled;
state.bulk.selectedType = '';
state.bulk.selection = [];
stateManager.setReadOnly(true);
}
/**
* Reset the current selection.
* @param {StateManager} stateManager the current state manager
*/
bulkReset(stateManager) {
const state = stateManager.state;
stateManager.setReadOnly(false);
state.bulk.selectedType = '';
state.bulk.selection = [];
stateManager.setReadOnly(true);
}
/**
* Select a list of cms.
* @param {StateManager} stateManager the current state manager
* @param {array} cmIds the list of cm ids
*/
cmSelect(stateManager, cmIds) {
this._addIdsToSelection(stateManager, 'cm', cmIds);
}
/**
* Unselect a list of cms.
* @param {StateManager} stateManager the current state manager
* @param {array} cmIds the list of cm ids
*/
cmUnselect(stateManager, cmIds) {
this._removeIdsFromSelection(stateManager, 'cm', cmIds);
}
/**
* Select a list of sections.
* @param {StateManager} stateManager the current state manager
* @param {array} sectionIds the list of cm ids
*/
sectionSelect(stateManager, sectionIds) {
this._addIdsToSelection(stateManager, 'section', sectionIds);
}
/**
* Unselect a list of sections.
* @param {StateManager} stateManager the current state manager
* @param {array} sectionIds the list of cm ids
*/
sectionUnselect(stateManager, sectionIds) {
this._removeIdsFromSelection(stateManager, 'section', sectionIds);
}
/**
* Add some ids to the current bulk selection.
* @param {StateManager} stateManager the current state manager
* @param {String} typeName the type name (section/cm)
* @param {array} ids the list of ids
*/
_addIdsToSelection(stateManager, typeName, ids) {
const bulk = stateManager.state.bulk;
if (!bulk?.enabled) {
throw new Error(`Bulk is not enabled`);
}
if (bulk?.selectedType !== "" && bulk?.selectedType !== typeName) {
throw new Error(`Cannot add ${typeName} to the current selection`);
}
// Stored ids are strings for compatability with HTML data attributes.
ids = ids.map(value => value.toString());
stateManager.setReadOnly(false);
bulk.selectedType = typeName;
const newSelection = new Set([...bulk.selection, ...ids]);
bulk.selection = [...newSelection];
stateManager.setReadOnly(true);
}
/**
* Remove some ids to the current bulk selection.
*
* The method resets the selection type if the current selection is empty.
*
* @param {StateManager} stateManager the current state manager
* @param {String} typeName the type name (section/cm)
* @param {array} ids the list of ids
*/
_removeIdsFromSelection(stateManager, typeName, ids) {
const bulk = stateManager.state.bulk;
if (!bulk?.enabled) {
throw new Error(`Bulk is not enabled`);
}
if (bulk?.selectedType !== "" && bulk?.selectedType !== typeName) {
throw new Error(`Cannot remove ${typeName} from the current selection`);
}
// Stored ids are strings for compatability with HTML data attributes.
ids = ids.map(value => value.toString());
stateManager.setReadOnly(false);
const IdsToFilter = new Set(ids);
bulk.selection = bulk.selection.filter(current => !IdsToFilter.has(current));
if (bulk.selection.length === 0) {
bulk.selectedType = '';
}
stateManager.setReadOnly(true);
}
/**
* Get updated state data related to some cm ids.
*

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

@ -88,6 +88,7 @@ class section implements renderable {
'indexcollapsed' => $indexcollapsed,
'contentcollapsed' => $contentcollapsed,
'hasrestrictions' => $this->get_has_restrictions(),
'bulkeditable' => $this->is_bulk_editable(),
];
if (empty($modinfo->sections[$section->section])) {
@ -104,6 +105,15 @@ class section implements renderable {
return $data;
}
/**
* Return if the section can be selected for bulk editing.
* @return bool if the section can be edited in bulk
*/
protected function is_bulk_editable(): bool {
$section = $this->section;
return ($section->section != 0);
}
/**
* Return if the section has a restrictions icon displayed or not.
*

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

@ -39,7 +39,7 @@
}
}}
{{#showaddsection}}
<div class="mdl-left py-2 changenumsections">
<div class="mdl-left py-2 changenumsections bulk-hidden">
{{#increase}}
<a href="{{{url}}}" class="increase-sections">
{{#pix}}t/switch_plus, moodle, {{#str}} increasesections, moodle {{/str}}{{/pix}}

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

@ -57,13 +57,16 @@
}
}}
{{#editing}}
<div class="divider divider-plus" data-action="insert-before-{{activityname}}">
<div class="divider divider-plus bulk-hidden" data-action="insert-before-{{activityname}}">
{{> core_course/activitychooserbuttonactivity}}
</div>
{{/editing}}
<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

@ -85,7 +85,7 @@
</div>
{{#controlmenu}}
<div class="activity-actions align-self-start">
<div class="activity-actions bulk-hidden align-self-start">
{{$ core_courseformat/local/content/cm/controlmenu }}
{{> core_courseformat/local/content/cm/controlmenu }}
{{/ core_courseformat/local/content/cm/controlmenu }}

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

@ -26,7 +26,7 @@
}
}}
{{#hasmenu}}
<div class="section_action_menu ml-auto" data-sectionid="{{id}}">
<div class="section_action_menu bulk-hidden ml-auto" data-sectionid="{{id}}">
{{{menu}}}
</div>
{{/hasmenu}}

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

@ -29,7 +29,7 @@
"sectionreturn": 0
}
}}
<button class="btn btn-link text-decoration-none section-modchooser section-modchooser-link activity-add d-flex align-items-center p-3 mb-3"
<button class="btn btn-link text-decoration-none section-modchooser section-modchooser-link activity-add bulk-hidden d-flex align-items-center p-3 mb-3"
data-action="open-chooser" data-sectionid="{{sectionid}}" data-sectionreturnid="{{sectionreturn}}">
<span class="pluscontainer icon-no-margin icon-size-3 d-flex p-2 mr-3">
{{#pix}} t/add, core {{/pix}}

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

14
lib/amd/build/sticky-footer.min.js vendored Normal file
View file

@ -0,0 +1,14 @@
define("core/sticky-footer",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.registerManager=_exports.init=_exports.enableStickyFooter=_exports.disableStickyFooter=void 0;
/**
* Sticky footer wrapper module.
*
* Themes are responsible for implementing the sticky footer. However,
* modules can interact with the sticky footer using this module.
*
* @module core/sticky-footer
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
let manager={},enabled=!1,initialized=!1;const SELECTORS_STICKYFOOTER=".stickyfooter",CLASSES_INVISIBLE="v-hidden",enableStickyFooter=()=>{var _document$querySelect;(enabled=!0,void 0!==manager.enableStickyFooter)?manager.enableStickyFooter():null===(_document$querySelect=document.querySelector(SELECTORS_STICKYFOOTER))||void 0===_document$querySelect||_document$querySelect.classList.remove(CLASSES_INVISIBLE)};_exports.enableStickyFooter=enableStickyFooter;const disableStickyFooter=()=>{var _document$querySelect2;(enabled=!1,void 0!==manager.disableStickyFooter)?manager.disableStickyFooter():null===(_document$querySelect2=document.querySelector(SELECTORS_STICKYFOOTER))||void 0===_document$querySelect2||_document$querySelect2.classList.add(CLASSES_INVISIBLE)};_exports.disableStickyFooter=disableStickyFooter;_exports.registerManager=themeManager=>{manager=themeManager,enabled&&enableStickyFooter()};_exports.init=()=>{var _document$querySelect3;if(initialized)return;initialized=!0;(null===(_document$querySelect3=document.querySelector(SELECTORS_STICKYFOOTER))||void 0===_document$querySelect3?void 0:_document$querySelect3.dataset.disable)?disableStickyFooter():enableStickyFooter()}}));
//# sourceMappingURL=sticky-footer.min.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"sticky-footer.min.js","sources":["../src/sticky-footer.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 * Sticky footer wrapper module.\n *\n * Themes are responsible for implementing the sticky footer. However,\n * modules can interact with the sticky footer using this module.\n *\n * @module core/sticky-footer\n * @copyright 2023 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nlet manager = {};\n\nlet enabled = false;\n\nlet initialized = false;\n\nconst SELECTORS = {\n STICKYFOOTER: '.stickyfooter',\n};\n\nconst CLASSES = {\n INVISIBLE: 'v-hidden',\n};\n\n/**\n * Enable sticky footer in the page.\n */\nexport const enableStickyFooter = () => {\n enabled = true;\n if (manager.enableStickyFooter === undefined) {\n document.querySelector(SELECTORS.STICKYFOOTER)?.classList.remove(CLASSES.INVISIBLE);\n return;\n }\n manager.enableStickyFooter();\n};\n\n/**\n * Disable sticky footer in the page.\n */\nexport const disableStickyFooter = () => {\n enabled = false;\n if (manager.disableStickyFooter === undefined) {\n document.querySelector(SELECTORS.STICKYFOOTER)?.classList.add(CLASSES.INVISIBLE);\n return;\n }\n manager.disableStickyFooter();\n};\n\n/**\n * Register the theme sticky footer methods.\n *\n * @param {Object} themeManager the manager object with all the needed methods.\n * @param {Function} themeManager.enableStickyFooter enable sticky footer method\n * @param {Function} themeManager.disableStickyFooter disable sticky footer method\n */\nexport const registerManager = (themeManager) => {\n manager = themeManager;\n if (enabled) {\n enableStickyFooter();\n }\n};\n\n/**\n * Initialize the module if the theme does not implement its own init.\n */\nexport const init = () => {\n if (initialized) {\n return;\n }\n initialized = true;\n\n const isDisabled = document.querySelector(SELECTORS.STICKYFOOTER)?.dataset.disable;\n if (isDisabled) {\n disableStickyFooter();\n } else {\n enableStickyFooter();\n }\n};\n"],"names":["manager","enabled","initialized","SELECTORS","CLASSES","enableStickyFooter","undefined","document","querySelector","classList","remove","disableStickyFooter","add","themeManager","_document$querySelect3","dataset","disable"],"mappings":";;;;;;;;;;;IA2BIA,QAAU,GAEVC,SAAU,EAEVC,aAAc,QAEZC,uBACY,gBAGZC,kBACS,WAMFC,mBAAqB,gCAC9BJ,SAAU,OACyBK,IAA/BN,QAAQK,oBAIZL,QAAQK,mDAHJE,SAASC,cAAcL,gFAAyBM,UAAUC,OAAON,yEAS5DO,oBAAsB,iCAC/BV,SAAU,OAC0BK,IAAhCN,QAAQW,qBAIZX,QAAQW,qDAHJJ,SAASC,cAAcL,kFAAyBM,UAAUG,IAAIR,8FAatCS,eAC5Bb,QAAUa,aACNZ,SACAI,oCAOY,mCACZH,mBAGJA,aAAc,kCAEKK,SAASC,cAAcL,iEAAvBW,uBAAgDC,QAAQC,SAEvEL,sBAEAN"}

View file

@ -0,0 +1,95 @@
// 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/>.
/**
* Sticky footer wrapper module.
*
* Themes are responsible for implementing the sticky footer. However,
* modules can interact with the sticky footer using this module.
*
* @module core/sticky-footer
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
let manager = {};
let enabled = false;
let initialized = false;
const SELECTORS = {
STICKYFOOTER: '.stickyfooter',
};
const CLASSES = {
INVISIBLE: 'v-hidden',
};
/**
* Enable sticky footer in the page.
*/
export const enableStickyFooter = () => {
enabled = true;
if (manager.enableStickyFooter === undefined) {
document.querySelector(SELECTORS.STICKYFOOTER)?.classList.remove(CLASSES.INVISIBLE);
return;
}
manager.enableStickyFooter();
};
/**
* Disable sticky footer in the page.
*/
export const disableStickyFooter = () => {
enabled = false;
if (manager.disableStickyFooter === undefined) {
document.querySelector(SELECTORS.STICKYFOOTER)?.classList.add(CLASSES.INVISIBLE);
return;
}
manager.disableStickyFooter();
};
/**
* Register the theme sticky footer methods.
*
* @param {Object} themeManager the manager object with all the needed methods.
* @param {Function} themeManager.enableStickyFooter enable sticky footer method
* @param {Function} themeManager.disableStickyFooter disable sticky footer method
*/
export const registerManager = (themeManager) => {
manager = themeManager;
if (enabled) {
enableStickyFooter();
}
};
/**
* Initialize the module if the theme does not implement its own init.
*/
export const init = () => {
if (initialized) {
return;
}
initialized = true;
const isDisabled = document.querySelector(SELECTORS.STICKYFOOTER)?.dataset.disable;
if (isDisabled) {
disableStickyFooter();
} else {
enableStickyFooter();
}
};

View file

@ -46,6 +46,11 @@ class sticky_footer implements named_templatable, renderable {
*/
protected $stickyclasses = 'justify-content-end';
/**
* @var bool if the footer should auto enable or not.
*/
protected $autoenable = true;
/**
* @var array extra HTML attributes (attribute => value).
*/
@ -75,6 +80,15 @@ class sticky_footer implements named_templatable, renderable {
$this->stickycontent = $stickycontent;
}
/**
* Set the auto enable value.
*
* @param bool $autoenable the footer content
*/
public function set_auto_enable(bool $autoenable) {
$this->autoenable = $autoenable;
}
/**
* Add extra classes to the sticky footer.
*
@ -111,11 +125,15 @@ class sticky_footer implements named_templatable, renderable {
'value' => $value,
];
}
return [
$data = [
'stickycontent' => (string)$this->stickycontent,
'stickyclasses' => $this->stickyclasses,
'extras' => $extras,
];
if (!$this->autoenable) {
$data['disable'] = true;
}
return $data;
}
/**

View file

@ -22,6 +22,15 @@
Sticky footer behaviour depends on the theme. The default template is
a regular element.
Classes required for JS:
* none
Data attributes optional for JS:
* data-disable Number|String - If the sticky footer should be disabled by default
Context variables required for this template:
* disable Boolean - if the sticky footer should be loaded hidden
Example context (json):
{
"stickycontent" : "<a href=\"#\">Moodle</a>",
@ -37,6 +46,9 @@
<div
id="sticky-footer"
class="{{$ stickyclasses }}{{stickyclasses}}{{/ stickyclasses }}"
{{$ disable }}
{{#disable}} data-disable="true" {{/disable}}
{{/ disable }}
{{#extras}}
{{attribute}}="{{value}}"
{{/extras}}
@ -45,3 +57,9 @@
{{{stickycontent}}}
{{/ stickycontent }}
</div>
{{#js}}
require(['core/sticky-footer'], function(footer) {
footer.init();
});
{{/js}}

View file

@ -1,10 +1,10 @@
define("theme_boost/sticky-footer",["exports","core/pending"],(function(_exports,_pending){var obj;
define("theme_boost/sticky-footer",["exports","core/pending","core/sticky-footer"],(function(_exports,_pending,_stickyFooter){var obj;
/**
* Sticky footer module.
*
* @module theme_boost/sticky-footer
* @copyright 2022 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.init=_exports.enableStickyFooter=_exports.disableStickyFooter=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};const SELECTORS_STICKYFOOTER=".stickyfooter",SELECTORS_PAGE="#page",CLASSES_HASSTICKYFOOTER="hasstickyfooter";let initialized=!1,previousScrollPosition=0;const scrollSpy=()=>{if(document.body.clientWidth>=768)return;let scrollPosition=(()=>{const page=document.querySelector(SELECTORS_PAGE);return page?page.scrollTop:window.pageYOffset})();scrollPosition>previousScrollPosition?disableStickyFooter():enableStickyFooter(),previousScrollPosition=scrollPosition},enableStickyFooter=()=>{const pendingPromise=new _pending.default("theme_boost/sticky-footer:enabling"),footer=document.querySelector(SELECTORS_STICKYFOOTER),page=document.querySelector(SELECTORS_PAGE);footer&&page&&(document.body.classList.add(CLASSES_HASSTICKYFOOTER),page.classList.add(CLASSES_HASSTICKYFOOTER)),setTimeout((()=>pendingPromise.resolve()),1e3)};_exports.enableStickyFooter=enableStickyFooter;const disableStickyFooter=()=>{document.body.classList.remove(CLASSES_HASSTICKYFOOTER);const page=document.querySelector(SELECTORS_PAGE);null==page||page.classList.remove(CLASSES_HASSTICKYFOOTER)};_exports.disableStickyFooter=disableStickyFooter;_exports.init=()=>{var _document$querySelect;if(initialized||document.body.classList.contains("behat-site"))return;initialized=!0,enableStickyFooter();(null!==(_document$querySelect=document.querySelector(SELECTORS_PAGE))&&void 0!==_document$querySelect?_document$querySelect:document.body).addEventListener("scroll",scrollSpy)}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.enableStickyFooter=_exports.disableStickyFooter=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};const SELECTORS_STICKYFOOTER=".stickyfooter",SELECTORS_PAGE="#page",CLASSES_HASSTICKYFOOTER="hasstickyfooter";let initialized=!1,previousScrollPosition=0,enabled=!1;const scrollSpy=()=>{if(!enabled)return;if(document.body.clientWidth>=768)return;let scrollPosition=(()=>{const page=document.querySelector(SELECTORS_PAGE);return page?page.scrollTop:window.pageYOffset})();scrollPosition>previousScrollPosition?hideStickyFooter():showStickyFooter(),previousScrollPosition=scrollPosition},showStickyFooter=()=>{const pendingPromise=new _pending.default("theme_boost/sticky-footer:enabling"),footer=document.querySelector(SELECTORS_STICKYFOOTER),page=document.querySelector(SELECTORS_PAGE);footer&&page&&(document.body.classList.add(CLASSES_HASSTICKYFOOTER),page.classList.add(CLASSES_HASSTICKYFOOTER)),setTimeout((()=>pendingPromise.resolve()),1e3)},hideStickyFooter=()=>{document.body.classList.remove(CLASSES_HASSTICKYFOOTER);const page=document.querySelector(SELECTORS_PAGE);null==page||page.classList.remove(CLASSES_HASSTICKYFOOTER)},enableStickyFooter=()=>{enabled=!0,showStickyFooter()};_exports.enableStickyFooter=enableStickyFooter;const disableStickyFooter=()=>{enabled=!1,hideStickyFooter()};_exports.disableStickyFooter=disableStickyFooter;_exports.init=()=>{var _document$querySelect;if(initialized||document.body.classList.contains("behat-site"))return void(0,_stickyFooter.init)();initialized=!0,(()=>{const footer=document.querySelector(SELECTORS_STICKYFOOTER);return!!footer&&!!footer.dataset.disable})()||enableStickyFooter();(null!==(_document$querySelect=document.querySelector(SELECTORS_PAGE))&&void 0!==_document$querySelect?_document$querySelect:document.body).addEventListener("scroll",scrollSpy),(0,_stickyFooter.registerManager)({enableStickyFooter:enableStickyFooter,disableStickyFooter:disableStickyFooter})}}));
//# sourceMappingURL=sticky-footer.min.js.map

File diff suppressed because one or more lines are too long

View file

@ -22,6 +22,7 @@
*/
import Pending from 'core/pending';
import {registerManager, init as defaultInit} from 'core/sticky-footer';
const SELECTORS = {
STICKYFOOTER: '.stickyfooter',
@ -36,6 +37,8 @@ let initialized = false;
let previousScrollPosition = 0;
let enabled = false;
/**
* Return the current page scroll position.
* @package
@ -54,6 +57,9 @@ const getScrollPosition = () => {
* @package
*/
const scrollSpy = () => {
if (!enabled) {
return;
}
// Ignore scroll if page size is not small.
if (document.body.clientWidth >= 768) {
return;
@ -61,17 +67,29 @@ const scrollSpy = () => {
// Detect if scroll is going down.
let scrollPosition = getScrollPosition();
if (scrollPosition > previousScrollPosition) {
disableStickyFooter();
hideStickyFooter();
} else {
enableStickyFooter();
showStickyFooter();
}
previousScrollPosition = scrollPosition;
};
/**
* Enable sticky footer in the page.
* Return if the sticky footer must be enabled by default or not.
* @returns {Boolean} true if the sticky footer is enabled automatic.
*/
export const enableStickyFooter = () => {
const isDisabledByDefault = () => {
const footer = document.querySelector(SELECTORS.STICKYFOOTER);
if (!footer) {
return false;
}
return !!footer.dataset.disable;
};
/**
* Show the sticky footer in the page.
*/
const showStickyFooter = () => {
// We need some seconds to make sure the CSS animation is ready.
const pendingPromise = new Pending('theme_boost/sticky-footer:enabling');
const footer = document.querySelector(SELECTORS.STICKYFOOTER);
@ -84,24 +102,49 @@ export const enableStickyFooter = () => {
};
/**
* Disable sticky footer in the page.
* Hide the sticky footer in the page.
*/
export const disableStickyFooter = () => {
const hideStickyFooter = () => {
document.body.classList.remove(CLASSES.HASSTICKYFOOTER);
const page = document.querySelector(SELECTORS.PAGE);
page?.classList.remove(CLASSES.HASSTICKYFOOTER);
};
/**
* Enable sticky footer in the page.
*/
export const enableStickyFooter = () => {
enabled = true;
showStickyFooter();
};
/**
* Disable sticky footer in the page.
*/
export const disableStickyFooter = () => {
enabled = false;
hideStickyFooter();
};
/**
* Initialize the module.
*/
export const init = () => {
// Prevent sticky footer in behat.
if (initialized || document.body.classList.contains('behat-site')) {
defaultInit();
return;
}
initialized = true;
enableStickyFooter();
if (!isDisabledByDefault()) {
enableStickyFooter();
}
const content = document.querySelector(SELECTORS.PAGE) ?? document.body;
content.addEventListener("scroll", scrollSpy);
registerManager({
enableStickyFooter,
disableStickyFooter,
});
};

View file

@ -2607,6 +2607,10 @@ input[disabled] {
}
}
.v-hidden {
visibility: hidden;
}
// Emoji picker.
$picker-width: 350px !default;
$picker-width-xs: 320px !default;

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,
@ -1574,3 +1575,24 @@ $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

@ -11966,6 +11966,9 @@ input[disabled] {
.border-radius {
border-radius: 0.5rem; }
.v-hidden {
visibility: hidden; }
.emoji-picker {
width: 350px;
height: 400px; }
@ -14946,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 {
@ -15009,6 +15014,22 @@ span.editinstructions {
opacity: 1;
visibility: visible; }
.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

@ -19,10 +19,20 @@
Displays a page sticky footer element.
Classes required for JS:
* none
Data attributes optional for JS:
* data-disable Number|String - If the sticky footer should be disabled by default
Context variables required for this template:
* disable Boolean - if the sticky footer should be loaded hidden
Example context (json):
{
"stickycontent": "<a href=\"#\">Moodle</a>",
"stickyclasses": "justify-content-end",
"disable": false,
"extras": [
{
"attribute" : "data-example",
@ -38,9 +48,14 @@
}}{{# stickyclasses }}{{ stickyclasses }}{{/ stickyclasses }}{{!
}}{{^ stickyclasses }}justify-content-end{{/ stickyclasses }}{{!
}}{{/ stickyclasses }}"
{{#extras}}
{{attribute}}="{{value}}"
{{/extras}}
{{$ disable }}
{{#disable}} data-disable="true" {{/disable}}
{{/ disable }}
{{$ extradata }}
{{#extras}}
{{attribute}}="{{value}}"
{{/extras}}
{{/ extradata }}
>
{{$ stickycontent }}
{{{stickycontent}}}

View file

@ -11966,6 +11966,9 @@ input[disabled] {
.border-radius {
border-radius: 0.25rem; }
.v-hidden {
visibility: hidden; }
.emoji-picker {
width: 350px;
height: 400px; }
@ -14946,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 {
@ -15009,6 +15014,22 @@ span.editinstructions {
opacity: 1;
visibility: visible; }
.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; }