From 662a2fca9c5b7b0ad2f991af2c12e85cb525e574 Mon Sep 17 00:00:00 2001 From: Mihail Geshoski Date: Mon, 31 Jul 2023 13:30:48 +0200 Subject: [PATCH] MDL-77035 core: Base js class for a bulk actions area Introduces a base js class that bulk actions area implementations need to extend. This class contains all common selectors, event listeners, methods (including abstract) that each implementation would use. Also, introduces a common template for the bulk actions area. --- lang/en/moodle.php | 2 + lib/amd/build/bulkactions/bulk_actions.min.js | 10 + .../build/bulkactions/bulk_actions.min.js.map | 1 + lib/amd/src/bulkactions/bulk_actions.js | 196 ++++++++++++++++++ .../bulkactions/bulk_actions.mustache | 63 ++++++ theme/boost/scss/moodle/core.scss | 9 + theme/boost/style/moodle.css | 7 + theme/classic/style/moodle.css | 7 + 8 files changed, 295 insertions(+) create mode 100644 lib/amd/build/bulkactions/bulk_actions.min.js create mode 100644 lib/amd/build/bulkactions/bulk_actions.min.js.map create mode 100644 lib/amd/src/bulkactions/bulk_actions.js create mode 100644 lib/templates/bulkactions/bulk_actions.mustache diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 16d4b3576b1..31c13852c23 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -226,7 +226,9 @@ $string['blocksuccess'] = '{$a} tables have been set up correctly'; $string['brief'] = 'Brief'; $string['bulkactions'] = 'Bulk actions'; $string['bulkactionselect'] = '{$a} bulk action selection'; +$string['bulkcancel'] = 'Close bulk edit'; $string['bulkmovecoursessuccess'] = 'Successfully moved {$a->courses} courses into {$a->category}'; +$string['bulkselection'] = '{$a} selected'; $string['bycourseorder'] = 'By course order'; $string['byname'] = 'by {$a}'; $string['bypassed'] = 'Bypassed'; diff --git a/lib/amd/build/bulkactions/bulk_actions.min.js b/lib/amd/build/bulkactions/bulk_actions.min.js new file mode 100644 index 00000000000..00a36ebe326 --- /dev/null +++ b/lib/amd/build/bulkactions/bulk_actions.min.js @@ -0,0 +1,10 @@ +define("core/bulkactions/bulk_actions",["exports","core/templates","core/str","core/sticky-footer"],(function(_exports,_templates,_str,_stickyFooter){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj} +/** + * Base class for defining a bulk actions area within a page. + * + * @module core/bulkactions/bulk_actions + * @copyright 2023 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};const Selectors_stickyFooterContainer="#sticky-footer",Selectors_selectedItemsCountContainer='[data-type="bulkactions"] [data-for="bulkcount"]',Selectors_cancelBulkActionModeElement='[data-type="bulkactions"] [data-action="bulkcancel"]',Selectors_bulkModeContainer='[data-type="bulkactions"]',Selectors_bulkActionsContainer='[data-type="bulkactions"] [data-for="bulktools"]';return _exports.default=class{constructor(){if(_defineProperty(this,"initialStickyFooterContent",null),_defineProperty(this,"selectedItems",[]),_defineProperty(this,"isBulkActionsModeEnabled",!1),!this.getStickyFooterContainer())throw new Error("Sticky footer not found.");this.initialStickyFooterContent=this.getStickyFooterContainer().innerHTML,this.registerItemSelectChangeEvent((async()=>{this.selectedItems=this.getSelectedItems(),this.selectedItems.length>0?this.isBulkActionsModeEnabled?await this.updateBulkItemSelection():await this.enableBulkActionsMode():this.disableBulkActionsMode()}))}getBulkActions(){throw new Error("getBulkActions() must be implemented in ".concat(this.constructor.name))}getSelectedItems(){throw new Error("getSelectedItems() must be implemented in ".concat(this.constructor.name))}registerItemSelectChangeEvent(eventHandler){throw new Error("registerItemSelectChangeEvent(".concat(eventHandler,") must be implemented in ").concat(this.constructor.name))}getStickyFooterContainer(){return document.querySelector(Selectors_stickyFooterContainer)}async enableBulkActionsMode(){(0,_stickyFooter.enableStickyFooter)(),this.getStickyFooterContainer().innerHTML=await this.renderBulkActions();const bulkModeContainer=this.getStickyFooterContainer().querySelector(Selectors_bulkModeContainer),bulkActionsContainer=bulkModeContainer.querySelector(Selectors_bulkActionsContainer);this.getBulkActions().forEach((bulkAction=>{bulkAction.registerListenerEvents(bulkActionsContainer),bulkAction.setSelectedItems(this.selectedItems)})),bulkModeContainer.addEventListener("click",(e=>{e.target.closest(Selectors_cancelBulkActionModeElement)&&(this.selectedItems.forEach((item=>{item.checked=!1})),this.disableBulkActionsMode())})),this.isBulkActionsModeEnabled=!0}disableBulkActionsMode(){this.initialStickyFooterContent.length>0?this.getStickyFooterContainer().innerHTML=this.initialStickyFooterContent:(0,_stickyFooter.disableStickyFooter)(),this.isBulkActionsModeEnabled=!1}async renderBulkActions(){let data={bulkselectioncount:this.selectedItems.length,actions:[]};return await Promise.all(this.getBulkActions().map((async bulkAction=>{data.actions.push({actiontrigger:await bulkAction.renderBulkActionTrigger()})}))),_templates.default.render("core/bulkactions/bulk_actions",data)}async updateBulkItemSelection(){const bulkSelection=await(0,_str.get_string)("bulkselection","core",this.selectedItems.length);document.querySelector(Selectors_selectedItemsCountContainer).innerHTML=bulkSelection}},_exports.default})); + +//# sourceMappingURL=bulk_actions.min.js.map \ No newline at end of file diff --git a/lib/amd/build/bulkactions/bulk_actions.min.js.map b/lib/amd/build/bulkactions/bulk_actions.min.js.map new file mode 100644 index 00000000000..f746110772b --- /dev/null +++ b/lib/amd/build/bulkactions/bulk_actions.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"bulk_actions.min.js","sources":["../../src/bulkactions/bulk_actions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport Templates from 'core/templates';\nimport {get_string as getString} from 'core/str';\nimport {disableStickyFooter, enableStickyFooter} from 'core/sticky-footer';\n\n/**\n * Base class for defining a bulk actions area within a page.\n *\n * @module core/bulkactions/bulk_actions\n * @copyright 2023 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/** @constant {Object} The object containing the relevant selectors. */\nconst Selectors = {\n stickyFooterContainer: '#sticky-footer',\n selectedItemsCountContainer: '[data-type=\"bulkactions\"] [data-for=\"bulkcount\"]',\n cancelBulkActionModeElement: '[data-type=\"bulkactions\"] [data-action=\"bulkcancel\"]',\n bulkModeContainer: '[data-type=\"bulkactions\"]',\n bulkActionsContainer: '[data-type=\"bulkactions\"] [data-for=\"bulktools\"]'\n};\n\nexport default class BulkActions {\n\n /** @property {string|null} initialStickyFooterContent The initial content of the sticky footer. */\n initialStickyFooterContent = null;\n\n /** @property {Array} selectedItems The array of selected item elements. */\n selectedItems = [];\n\n /** @property {boolean} isBulkActionsModeEnabled Whether the bulk actions mode is enabled. */\n isBulkActionsModeEnabled = false;\n\n /**\n * The class constructor.\n *\n * @returns {void}\n */\n constructor() {\n if (!this.getStickyFooterContainer()) {\n throw new Error('Sticky footer not found.');\n }\n // Store any pre-existing content in the sticky footer. When bulk actions mode is enabled, this content will be\n // replaced with the bulk actions content and restored when bulk actions mode is disabled.\n this.initialStickyFooterContent = this.getStickyFooterContainer().innerHTML;\n // Register and handle the item select change event.\n this.registerItemSelectChangeEvent(async() => {\n this.selectedItems = this.getSelectedItems();\n if (this.selectedItems.length > 0) { // At least one item is selected.\n // If the bulk actions mode is already enabled only update the selected items count.\n if (this.isBulkActionsModeEnabled) {\n await this.updateBulkItemSelection();\n } else { // Otherwise, enable the bulk action mode.\n await this.enableBulkActionsMode();\n }\n } else { // No items are selected, disable the bulk action mode.\n this.disableBulkActionsMode();\n }\n });\n }\n\n /**\n * Returns the array of the relevant bulk action objects.\n *\n * @method getBulkActions\n * @returns {Array}\n */\n getBulkActions() {\n throw new Error(`getBulkActions() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Returns the array of selected items.\n *\n * @method getSelectedItems\n * @returns {Array}\n */\n getSelectedItems() {\n throw new Error(`getSelectedItems() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Adds the listener for the item select change event.\n * The event handler function that is passed as a parameter should be called right after the event is triggered.\n *\n * @method registerItemSelectChangeEvent\n * @param {function} eventHandler The event handler function.\n * @returns {void}\n */\n registerItemSelectChangeEvent(eventHandler) {\n throw new Error(`registerItemSelectChangeEvent(${eventHandler}) must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Returns the sticky footer container.\n *\n * @method getStickyFooterContainer\n * @returns {HTMLElement}\n */\n getStickyFooterContainer() {\n return document.querySelector(Selectors.stickyFooterContainer);\n }\n\n /**\n * Enables the bulk action mode.\n *\n * @method enableBulkActionsMode\n * @returns {Promise}\n */\n async enableBulkActionsMode() {\n // Make sure that the sticky footer is enabled.\n enableStickyFooter();\n // Render the bulk actions content in the sticky footer container.\n this.getStickyFooterContainer().innerHTML = await this.renderBulkActions();\n const bulkModeContainer = this.getStickyFooterContainer().querySelector(Selectors.bulkModeContainer);\n const bulkActionsContainer = bulkModeContainer.querySelector(Selectors.bulkActionsContainer);\n this.getBulkActions().forEach((bulkAction) => {\n // Register the listener events for each available bulk action.\n bulkAction.registerListenerEvents(bulkActionsContainer);\n // Set the selected items for each available bulk action.\n bulkAction.setSelectedItems(this.selectedItems);\n });\n // Register the click listener event for the cancel bulk mode button.\n bulkModeContainer.addEventListener('click', (e) => {\n if (e.target.closest(Selectors.cancelBulkActionModeElement)) {\n // Uncheck all selected items.\n this.selectedItems.forEach((item) => {\n item.checked = false;\n });\n // Disable the bulk action mode.\n this.disableBulkActionsMode();\n }\n });\n this.isBulkActionsModeEnabled = true;\n }\n\n /**\n * Disables the bulk action mode.\n *\n * @method disableBulkActionsMode\n * @returns {void}\n */\n disableBulkActionsMode() {\n // If there was any previous (initial) content in the sticky footer, restore it.\n if (this.initialStickyFooterContent.length > 0) {\n this.getStickyFooterContainer().innerHTML = this.initialStickyFooterContent;\n } else { // No previous content to restore, disable the sticky footer.\n disableStickyFooter();\n }\n this.isBulkActionsModeEnabled = false;\n }\n\n /**\n * Renders the bulk actions content.\n *\n * @method renderBulkActions\n * @returns {Promise}\n */\n async renderBulkActions() {\n let data = {\n 'bulkselectioncount': this.selectedItems.length,\n 'actions': []\n };\n // Render the bulk actions trigger element for each available bulk action.\n await Promise.all(this.getBulkActions().map(async(bulkAction) => {\n data.actions.push({'actiontrigger': await bulkAction.renderBulkActionTrigger()});\n }));\n\n return Templates.render('core/bulkactions/bulk_actions', data);\n }\n\n /**\n * Updates the selected items count in the bulk actions content.\n *\n * @method updateBulkItemSelection\n * @returns {void}\n */\n async updateBulkItemSelection() {\n const bulkSelection = await getString('bulkselection', 'core', this.selectedItems.length);\n document.querySelector(Selectors.selectedItemsCountContainer).innerHTML = bulkSelection;\n }\n}\n"],"names":["Selectors","constructor","this","getStickyFooterContainer","Error","initialStickyFooterContent","innerHTML","registerItemSelectChangeEvent","async","selectedItems","getSelectedItems","length","isBulkActionsModeEnabled","updateBulkItemSelection","enableBulkActionsMode","disableBulkActionsMode","getBulkActions","name","eventHandler","document","querySelector","renderBulkActions","bulkModeContainer","bulkActionsContainer","forEach","bulkAction","registerListenerEvents","setSelectedItems","addEventListener","e","target","closest","item","checked","data","Promise","all","map","actions","push","renderBulkActionTrigger","Templates","render","bulkSelection"],"mappings":";;;;;;;yJA4BMA,gCACqB,iBADrBA,sCAE2B,mDAF3BA,sCAG2B,uDAH3BA,4BAIiB,4BAJjBA,+BAKoB,iFAmBtBC,mEAb6B,2CAGb,qDAGW,IAQlBC,KAAKC,iCACA,IAAIC,MAAM,iCAIfC,2BAA6BH,KAAKC,2BAA2BG,eAE7DC,+BAA8BC,eAC1BC,cAAgBP,KAAKQ,mBACtBR,KAAKO,cAAcE,OAAS,EAExBT,KAAKU,+BACCV,KAAKW,gCAELX,KAAKY,6BAGVC,4BAWjBC,uBACU,IAAIZ,wDAAiDF,KAAKD,YAAYgB,OAShFP,yBACU,IAAIN,0DAAmDF,KAAKD,YAAYgB,OAWlFV,8BAA8BW,oBACpB,IAAId,8CAAuCc,iDAAwChB,KAAKD,YAAYgB,OAS9Gd,kCACWgB,SAASC,cAAcpB,2GAazBG,2BAA2BG,gBAAkBJ,KAAKmB,0BACjDC,kBAAoBpB,KAAKC,2BAA2BiB,cAAcpB,6BAClEuB,qBAAuBD,kBAAkBF,cAAcpB,qCACxDgB,iBAAiBQ,SAASC,aAE3BA,WAAWC,uBAAuBH,sBAElCE,WAAWE,iBAAiBzB,KAAKO,kBAGrCa,kBAAkBM,iBAAiB,SAAUC,IACrCA,EAAEC,OAAOC,QAAQ/B,8CAEZS,cAAce,SAASQ,OACxBA,KAAKC,SAAU,UAGdlB,kCAGRH,0BAA2B,EASpCG,yBAEQb,KAAKG,2BAA2BM,OAAS,OACpCR,2BAA2BG,UAAYJ,KAAKG,wEAIhDO,0BAA2B,gCAU5BsB,KAAO,oBACehC,KAAKO,cAAcE,eAC9B,iBAGTwB,QAAQC,IAAIlC,KAAKc,iBAAiBqB,KAAI7B,MAAAA,aACxC0B,KAAKI,QAAQC,KAAK,qBAAwBd,WAAWe,gCAGlDC,mBAAUC,OAAO,gCAAiCR,4CAUnDS,oBAAsB,mBAAU,gBAAiB,OAAQzC,KAAKO,cAAcE,QAClFQ,SAASC,cAAcpB,uCAAuCM,UAAYqC"} \ No newline at end of file diff --git a/lib/amd/src/bulkactions/bulk_actions.js b/lib/amd/src/bulkactions/bulk_actions.js new file mode 100644 index 00000000000..34f531591e9 --- /dev/null +++ b/lib/amd/src/bulkactions/bulk_actions.js @@ -0,0 +1,196 @@ +// 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 . + +import Templates from 'core/templates'; +import {get_string as getString} from 'core/str'; +import {disableStickyFooter, enableStickyFooter} from 'core/sticky-footer'; + +/** + * Base class for defining a bulk actions area within a page. + * + * @module core/bulkactions/bulk_actions + * @copyright 2023 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** @constant {Object} The object containing the relevant selectors. */ +const Selectors = { + stickyFooterContainer: '#sticky-footer', + selectedItemsCountContainer: '[data-type="bulkactions"] [data-for="bulkcount"]', + cancelBulkActionModeElement: '[data-type="bulkactions"] [data-action="bulkcancel"]', + bulkModeContainer: '[data-type="bulkactions"]', + bulkActionsContainer: '[data-type="bulkactions"] [data-for="bulktools"]' +}; + +export default class BulkActions { + + /** @property {string|null} initialStickyFooterContent The initial content of the sticky footer. */ + initialStickyFooterContent = null; + + /** @property {Array} selectedItems The array of selected item elements. */ + selectedItems = []; + + /** @property {boolean} isBulkActionsModeEnabled Whether the bulk actions mode is enabled. */ + isBulkActionsModeEnabled = false; + + /** + * The class constructor. + * + * @returns {void} + */ + constructor() { + if (!this.getStickyFooterContainer()) { + throw new Error('Sticky footer not found.'); + } + // Store any pre-existing content in the sticky footer. When bulk actions mode is enabled, this content will be + // replaced with the bulk actions content and restored when bulk actions mode is disabled. + this.initialStickyFooterContent = this.getStickyFooterContainer().innerHTML; + // Register and handle the item select change event. + this.registerItemSelectChangeEvent(async() => { + this.selectedItems = this.getSelectedItems(); + if (this.selectedItems.length > 0) { // At least one item is selected. + // If the bulk actions mode is already enabled only update the selected items count. + if (this.isBulkActionsModeEnabled) { + await this.updateBulkItemSelection(); + } else { // Otherwise, enable the bulk action mode. + await this.enableBulkActionsMode(); + } + } else { // No items are selected, disable the bulk action mode. + this.disableBulkActionsMode(); + } + }); + } + + /** + * Returns the array of the relevant bulk action objects. + * + * @method getBulkActions + * @returns {Array} + */ + getBulkActions() { + throw new Error(`getBulkActions() must be implemented in ${this.constructor.name}`); + } + + /** + * Returns the array of selected items. + * + * @method getSelectedItems + * @returns {Array} + */ + getSelectedItems() { + throw new Error(`getSelectedItems() must be implemented in ${this.constructor.name}`); + } + + /** + * Adds the listener for the item select change event. + * The event handler function that is passed as a parameter should be called right after the event is triggered. + * + * @method registerItemSelectChangeEvent + * @param {function} eventHandler The event handler function. + * @returns {void} + */ + registerItemSelectChangeEvent(eventHandler) { + throw new Error(`registerItemSelectChangeEvent(${eventHandler}) must be implemented in ${this.constructor.name}`); + } + + /** + * Returns the sticky footer container. + * + * @method getStickyFooterContainer + * @returns {HTMLElement} + */ + getStickyFooterContainer() { + return document.querySelector(Selectors.stickyFooterContainer); + } + + /** + * Enables the bulk action mode. + * + * @method enableBulkActionsMode + * @returns {Promise} + */ + async enableBulkActionsMode() { + // Make sure that the sticky footer is enabled. + enableStickyFooter(); + // Render the bulk actions content in the sticky footer container. + this.getStickyFooterContainer().innerHTML = await this.renderBulkActions(); + const bulkModeContainer = this.getStickyFooterContainer().querySelector(Selectors.bulkModeContainer); + const bulkActionsContainer = bulkModeContainer.querySelector(Selectors.bulkActionsContainer); + this.getBulkActions().forEach((bulkAction) => { + // Register the listener events for each available bulk action. + bulkAction.registerListenerEvents(bulkActionsContainer); + // Set the selected items for each available bulk action. + bulkAction.setSelectedItems(this.selectedItems); + }); + // Register the click listener event for the cancel bulk mode button. + bulkModeContainer.addEventListener('click', (e) => { + if (e.target.closest(Selectors.cancelBulkActionModeElement)) { + // Uncheck all selected items. + this.selectedItems.forEach((item) => { + item.checked = false; + }); + // Disable the bulk action mode. + this.disableBulkActionsMode(); + } + }); + this.isBulkActionsModeEnabled = true; + } + + /** + * Disables the bulk action mode. + * + * @method disableBulkActionsMode + * @returns {void} + */ + disableBulkActionsMode() { + // If there was any previous (initial) content in the sticky footer, restore it. + if (this.initialStickyFooterContent.length > 0) { + this.getStickyFooterContainer().innerHTML = this.initialStickyFooterContent; + } else { // No previous content to restore, disable the sticky footer. + disableStickyFooter(); + } + this.isBulkActionsModeEnabled = false; + } + + /** + * Renders the bulk actions content. + * + * @method renderBulkActions + * @returns {Promise} + */ + async renderBulkActions() { + let data = { + 'bulkselectioncount': this.selectedItems.length, + 'actions': [] + }; + // Render the bulk actions trigger element for each available bulk action. + await Promise.all(this.getBulkActions().map(async(bulkAction) => { + data.actions.push({'actiontrigger': await bulkAction.renderBulkActionTrigger()}); + })); + + return Templates.render('core/bulkactions/bulk_actions', data); + } + + /** + * Updates the selected items count in the bulk actions content. + * + * @method updateBulkItemSelection + * @returns {void} + */ + async updateBulkItemSelection() { + const bulkSelection = await getString('bulkselection', 'core', this.selectedItems.length); + document.querySelector(Selectors.selectedItemsCountContainer).innerHTML = bulkSelection; + } +} diff --git a/lib/templates/bulkactions/bulk_actions.mustache b/lib/templates/bulkactions/bulk_actions.mustache new file mode 100644 index 00000000000..80f95e0051f --- /dev/null +++ b/lib/templates/bulkactions/bulk_actions.mustache @@ -0,0 +1,63 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core/bulkactions/bulk_actions + + Renders a section with all available bulk actions. + + Context variables required for this template: + * actions - Array of objects with the following properties: + * actiontrigger - The HTML for the action trigger. + + Example context (json): + { + "actions": [ + { + "actiontrigger": "" + } + ] + } +}} +
+
+
+
+ +
+
+
+ +
+
+ {{#str}} bulkselection, core, {{bulkselectioncount}} {{/str}} +
+
+
diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index 91d962b809e..efc1c555d99 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -3180,6 +3180,15 @@ blockquote { } } +/* Bulk actions in sticky footer. */ +#sticky-footer { + [data-type="bulkactions"] { + display: flex; + flex: 0 0 100%; + align-items: center; + } +} + /* Choice list component. */ .choicelist { // Choicelist is designed to fit inside a small modal. diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index a6937acb0c8..6c5cb6b4f4b 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -25987,6 +25987,13 @@ blockquote { width: 48px !important; } +/* Bulk actions in sticky footer. */ +#sticky-footer [data-type=bulkactions] { + display: flex; + flex: 0 0 100%; + align-items: center; +} + /* Choice list component. */ .choicelist { min-width: calc(300px - 25px); diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index e9bbe18974d..a15b93c4a83 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -25987,6 +25987,13 @@ blockquote { width: 48px !important; } +/* Bulk actions in sticky footer. */ +#sticky-footer [data-type=bulkactions] { + display: flex; + flex: 0 0 100%; + align-items: center; +} + /* Choice list component. */ .choicelist { min-width: calc(300px - 25px);