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:
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user