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.
This commit is contained in:
Mihail Geshoski
2023-09-20 23:53:43 +02:00
parent 6cf7908652
commit 662a2fca9c
8 changed files with 295 additions and 0 deletions
+2
View File
@@ -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';
+10
View File
@@ -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 <mihail@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,_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
File diff suppressed because one or more lines are too long
+196
View File
@@ -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 <http://www.gnu.org/licenses/>.
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 <[email protected]>
* @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;
}
}
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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": "<button>Move</button>"
}
]
}
}}
<div class="justify-content-between" data-type="bulkactions">
<div class="d-flex">
</div>
<div data-for="bulktools">
<ul class="actions nav" data-for="bulkactions">
{{#actions}}
<li class="nav-item">
{{{actiontrigger}}}
</li>
{{/actions}}
</ul>
</div>
<div class="d-flex flex-column">
<div class="ml-auto">
<button
type="button"
class="btn pr-0 pb-0"
data-action="bulkcancel"
data-for="bulkcancel"
title="{{#str}} bulkcancel, core {{/str}}"
>
{{#pix}} e/cancel, core {{/pix}}
</button>
</div>
<div data-for="bulkcount">
{{#str}} bulkselection, core, {{bulkselectioncount}} {{/str}}
</div>
</div>
</div>
+9
View File
@@ -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.
+7
View File
@@ -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);
+7
View File
@@ -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);