Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
import Templates from 'core/templates';
17
import {get_string as getString} from 'core/str';
18
import {disableStickyFooter, enableStickyFooter} from 'core/sticky-footer';
19
 
20
/**
21
 * Base class for defining a bulk actions area within a page.
22
 *
23
 * @module     core/bulkactions/bulk_actions
24
 * @copyright  2023 Mihail Geshoski <mihail@moodle.com>
25
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26
 */
27
 
28
/** @constant {Object} The object containing the relevant selectors. */
29
const Selectors = {
30
    stickyFooterContainer: '#sticky-footer',
31
    selectedItemsCountContainer: '[data-type="bulkactions"] [data-for="bulkcount"]',
32
    cancelBulkActionModeElement: '[data-type="bulkactions"] [data-action="bulkcancel"]',
33
    bulkModeContainer: '[data-type="bulkactions"]',
34
    bulkActionsContainer: '[data-type="bulkactions"] [data-for="bulktools"]'
35
};
36
 
37
export default class BulkActions {
38
 
39
    /** @property {string|null} initialStickyFooterContent The initial content of the sticky footer. */
40
    initialStickyFooterContent = null;
41
 
42
    /** @property {Array} selectedItems The array of selected item elements. */
43
    selectedItems = [];
44
 
45
    /** @property {boolean} isBulkActionsModeEnabled Whether the bulk actions mode is enabled. */
46
    isBulkActionsModeEnabled = false;
47
 
48
    /**
1441 ariadna 49
     * @property {int} maxButtons Sets the maximum number of action buttons to display. If exceeded, additional actions
50
     *                            are shown in a dropdown menu.
51
     */
52
    maxButtons = 5;
53
 
54
    /**
1 efrain 55
     * The class constructor.
56
     *
1441 ariadna 57
     * @param {int|null} maxButtons Sets the maximum number of action buttons to display. If exceeded, additional actions
58
     *                              are shown in a dropdown menu.
1 efrain 59
     * @returns {void}
60
     */
1441 ariadna 61
    constructor(maxButtons = null) {
1 efrain 62
        if (!this.getStickyFooterContainer()) {
63
            throw new Error('Sticky footer not found.');
64
        }
65
        // Store any pre-existing content in the sticky footer. When bulk actions mode is enabled, this content will be
66
        // replaced with the bulk actions content and restored when bulk actions mode is disabled.
67
        this.initialStickyFooterContent = this.getStickyFooterContainer().innerHTML;
1441 ariadna 68
        if (maxButtons) {
69
            this.maxButtons = maxButtons;
70
        }
1 efrain 71
        // Register and handle the item select change event.
72
        this.registerItemSelectChangeEvent(async() => {
73
            this.selectedItems = this.getSelectedItems();
74
            if (this.selectedItems.length > 0) { // At least one item is selected.
75
                // If the bulk actions mode is already enabled only update the selected items count.
76
                if (this.isBulkActionsModeEnabled) {
77
                    await this.updateBulkItemSelection();
78
                } else { // Otherwise, enable the bulk action mode.
79
                    await this.enableBulkActionsMode();
80
                }
81
            } else { // No items are selected, disable the bulk action mode.
82
                this.disableBulkActionsMode();
83
            }
84
        });
85
    }
86
 
87
    /**
88
     * Returns the array of the relevant bulk action objects.
89
     *
90
     * @method getBulkActions
91
     * @returns {Array}
92
     */
93
    getBulkActions() {
94
        throw new Error(`getBulkActions() must be implemented in ${this.constructor.name}`);
95
    }
96
 
97
    /**
98
     * Returns the array of selected items.
99
     *
100
     * @method getSelectedItems
101
     * @returns {Array}
102
     */
103
    getSelectedItems() {
104
        throw new Error(`getSelectedItems() must be implemented in ${this.constructor.name}`);
105
    }
106
 
107
    /**
108
     * Adds the listener for the item select change event.
109
     * The event handler function that is passed as a parameter should be called right after the event is triggered.
110
     *
111
     * @method registerItemSelectChangeEvent
112
     * @param {function} eventHandler The event handler function.
113
     * @returns {void}
114
     */
115
    registerItemSelectChangeEvent(eventHandler) {
116
        throw new Error(`registerItemSelectChangeEvent(${eventHandler}) must be implemented in ${this.constructor.name}`);
117
    }
118
 
119
    /**
1441 ariadna 120
     * Defines the action for deselecting a selected item.
121
     *
122
     * The base bulk actions class supports deselecting all selected items but does not have knowledge of the type of the
123
     * selected element. Therefore, each subclass must explicitly define the action of resetting the attributes that
124
     * indicate a selected state.
125
     *
126
     * @method deselectItem
127
     * @param {HTMLElement} selectedItem The selected element.
128
     * @returns {void}
129
     */
130
    deselectItem(selectedItem) {
131
        throw new Error(`deselectItem(${selectedItem}) must be implemented in ${this.constructor.name}`);
132
    }
133
 
134
    /**
1 efrain 135
     * Returns the sticky footer container.
136
     *
137
     * @method getStickyFooterContainer
138
     * @returns {HTMLElement}
139
     */
140
    getStickyFooterContainer() {
141
        return document.querySelector(Selectors.stickyFooterContainer);
142
    }
143
 
144
    /**
145
     * Enables the bulk action mode.
146
     *
147
     * @method enableBulkActionsMode
1441 ariadna 148
     * @returns {Promise<void>}
1 efrain 149
     */
150
    async enableBulkActionsMode() {
151
        // Make sure that the sticky footer is enabled.
152
        enableStickyFooter();
153
        // Render the bulk actions content in the sticky footer container.
154
        this.getStickyFooterContainer().innerHTML = await this.renderBulkActions();
155
        const bulkModeContainer = this.getStickyFooterContainer().querySelector(Selectors.bulkModeContainer);
156
        const bulkActionsContainer = bulkModeContainer.querySelector(Selectors.bulkActionsContainer);
157
        this.getBulkActions().forEach((bulkAction) => {
158
            // Register the listener events for each available bulk action.
159
            bulkAction.registerListenerEvents(bulkActionsContainer);
160
            // Set the selected items for each available bulk action.
161
            bulkAction.setSelectedItems(this.selectedItems);
162
        });
163
        // Register the click listener event for the cancel bulk mode button.
164
        bulkModeContainer.addEventListener('click', (e) => {
165
            if (e.target.closest(Selectors.cancelBulkActionModeElement)) {
1441 ariadna 166
                // Deselect all selected items.
1 efrain 167
                this.selectedItems.forEach((item) => {
1441 ariadna 168
                    this.deselectItem(item);
1 efrain 169
                });
170
                // Disable the bulk action mode.
171
                this.disableBulkActionsMode();
172
            }
173
        });
174
        this.isBulkActionsModeEnabled = true;
1441 ariadna 175
 
176
        // Calling `renderBulkActions()` already renders the item selection count.
177
        // Because of this, screen readers will not announce the item selection count on first load given that aria-live regions
178
        // must be present in the DOM and have their contents changed before screen readers can announce their content.
179
        // So, we call `updateBulkItemSelection()` after a short delay to ensure that screen readers announce the item count.
180
        setTimeout(async() => {
181
            await this.updateBulkItemSelection();
182
        }, 300);
1 efrain 183
    }
184
 
185
    /**
186
     * Disables the bulk action mode.
187
     *
188
     * @method disableBulkActionsMode
189
     * @returns {void}
190
     */
191
    disableBulkActionsMode() {
192
        // If there was any previous (initial) content in the sticky footer, restore it.
193
        if (this.initialStickyFooterContent.length > 0) {
194
            this.getStickyFooterContainer().innerHTML = this.initialStickyFooterContent;
195
        } else { // No previous content to restore, disable the sticky footer.
196
            disableStickyFooter();
197
        }
198
        this.isBulkActionsModeEnabled = false;
199
    }
200
 
201
    /**
202
     * Renders the bulk actions content.
203
     *
204
     * @method renderBulkActions
1441 ariadna 205
     * @returns {Promise<string>}
1 efrain 206
     */
207
    async renderBulkActions() {
1441 ariadna 208
        const data = {
209
            bulkselectioncount: this.selectedItems.length,
210
            actions: [],
211
            moreactions: [],
212
            hasmoreactions: false,
1 efrain 213
        };
1441 ariadna 214
        const bulkActions = this.getBulkActions();
215
        const showMoreButton = bulkActions.length > this.maxButtons;
1 efrain 216
 
1441 ariadna 217
        // Get all bulk actions and render them in order.
218
        const actions = await Promise.all(
219
            bulkActions.map((bulkAction, index) =>
220
                bulkAction.renderBulkActionTrigger(
221
                    showMoreButton && (index >= this.maxButtons - 1),
222
                    index
223
                )
224
            )
225
        );
226
 
227
        // Separate rendered actions into data.actions and data.moreactions in the correct order.
228
        actions.forEach((actionTrigger, index) => {
229
            if (showMoreButton && (index >= this.maxButtons - 1)) {
230
                data.moreactions.push({'actiontrigger': actionTrigger});
231
            } else {
232
                data.actions.push({'actiontrigger': actionTrigger});
233
            }
234
        });
235
 
236
        data.hasmoreactions = data.moreactions.length > 0;
237
 
1 efrain 238
        return Templates.render('core/bulkactions/bulk_actions', data);
239
    }
240
 
241
    /**
242
     * Updates the selected items count in the bulk actions content.
243
     *
244
     * @method updateBulkItemSelection
245
     * @returns {void}
246
     */
247
    async updateBulkItemSelection() {
248
        const bulkSelection = await getString('bulkselection', 'core', this.selectedItems.length);
249
        document.querySelector(Selectors.selectedItemsCountContainer).innerHTML = bulkSelection;
250
    }
251
}