lib/amd/src/bulkactions/bulk_actions.js

  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. import Templates from 'core/templates';
  16. import {get_string as getString} from 'core/str';
  17. import {disableStickyFooter, enableStickyFooter} from 'core/sticky-footer';
  18. /**
  19. * Base class for defining a bulk actions area within a page.
  20. *
  21. * @module core/bulkactions/bulk_actions
  22. * @copyright 2023 Mihail Geshoski <mihail@moodle.com>
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24. */
  25. /** @constant {Object} The object containing the relevant selectors. */
  26. const Selectors = {
  27. stickyFooterContainer: '#sticky-footer',
  28. selectedItemsCountContainer: '[data-type="bulkactions"] [data-for="bulkcount"]',
  29. cancelBulkActionModeElement: '[data-type="bulkactions"] [data-action="bulkcancel"]',
  30. bulkModeContainer: '[data-type="bulkactions"]',
  31. bulkActionsContainer: '[data-type="bulkactions"] [data-for="bulktools"]'
  32. };
  33. export default class BulkActions {
  34. /** @property {string|null} initialStickyFooterContent The initial content of the sticky footer. */
  35. initialStickyFooterContent = null;
  36. /** @property {Array} selectedItems The array of selected item elements. */
  37. selectedItems = [];
  38. /** @property {boolean} isBulkActionsModeEnabled Whether the bulk actions mode is enabled. */
  39. isBulkActionsModeEnabled = false;
  40. /**
  41. * @property {int} maxButtons Sets the maximum number of action buttons to display. If exceeded, additional actions
  42. * are shown in a dropdown menu.
  43. */
  44. maxButtons = 5;
  45. /**
  46. * The class constructor.
  47. *
  48. * @param {int|null} maxButtons Sets the maximum number of action buttons to display. If exceeded, additional actions
  49. * are shown in a dropdown menu.
  50. * @returns {void}
  51. */
  52. constructor(maxButtons = null) {
  53. if (!this.getStickyFooterContainer()) {
  54. throw new Error('Sticky footer not found.');
  55. }
  56. // Store any pre-existing content in the sticky footer. When bulk actions mode is enabled, this content will be
  57. // replaced with the bulk actions content and restored when bulk actions mode is disabled.
  58. this.initialStickyFooterContent = this.getStickyFooterContainer().innerHTML;
  59. if (maxButtons) {
  60. this.maxButtons = maxButtons;
  61. }
  62. // Register and handle the item select change event.
  63. this.registerItemSelectChangeEvent(async() => {
  64. this.selectedItems = this.getSelectedItems();
  65. if (this.selectedItems.length > 0) { // At least one item is selected.
  66. // If the bulk actions mode is already enabled only update the selected items count.
  67. if (this.isBulkActionsModeEnabled) {
  68. await this.updateBulkItemSelection();
  69. } else { // Otherwise, enable the bulk action mode.
  70. await this.enableBulkActionsMode();
  71. }
  72. } else { // No items are selected, disable the bulk action mode.
  73. this.disableBulkActionsMode();
  74. }
  75. });
  76. }
  77. /**
  78. * Returns the array of the relevant bulk action objects.
  79. *
  80. * @method getBulkActions
  81. * @returns {Array}
  82. */
  83. getBulkActions() {
  84. throw new Error(`getBulkActions() must be implemented in ${this.constructor.name}`);
  85. }
  86. /**
  87. * Returns the array of selected items.
  88. *
  89. * @method getSelectedItems
  90. * @returns {Array}
  91. */
  92. getSelectedItems() {
  93. throw new Error(`getSelectedItems() must be implemented in ${this.constructor.name}`);
  94. }
  95. /**
  96. * Adds the listener for the item select change event.
  97. * The event handler function that is passed as a parameter should be called right after the event is triggered.
  98. *
  99. * @method registerItemSelectChangeEvent
  100. * @param {function} eventHandler The event handler function.
  101. * @returns {void}
  102. */
  103. registerItemSelectChangeEvent(eventHandler) {
  104. throw new Error(`registerItemSelectChangeEvent(${eventHandler}) must be implemented in ${this.constructor.name}`);
  105. }
  106. /**
  107. * Defines the action for deselecting a selected item.
  108. *
  109. * The base bulk actions class supports deselecting all selected items but does not have knowledge of the type of the
  110. * selected element. Therefore, each subclass must explicitly define the action of resetting the attributes that
  111. * indicate a selected state.
  112. *
  113. * @method deselectItem
  114. * @param {HTMLElement} selectedItem The selected element.
  115. * @returns {void}
  116. */
  117. deselectItem(selectedItem) {
  118. throw new Error(`deselectItem(${selectedItem}) must be implemented in ${this.constructor.name}`);
  119. }
  120. /**
  121. * Returns the sticky footer container.
  122. *
  123. * @method getStickyFooterContainer
  124. * @returns {HTMLElement}
  125. */
  126. getStickyFooterContainer() {
  127. return document.querySelector(Selectors.stickyFooterContainer);
  128. }
  129. /**
  130. * Enables the bulk action mode.
  131. *
  132. * @method enableBulkActionsMode
  133. * @returns {Promise<void>}
  134. */
  135. async enableBulkActionsMode() {
  136. // Make sure that the sticky footer is enabled.
  137. enableStickyFooter();
  138. // Render the bulk actions content in the sticky footer container.
  139. this.getStickyFooterContainer().innerHTML = await this.renderBulkActions();
  140. const bulkModeContainer = this.getStickyFooterContainer().querySelector(Selectors.bulkModeContainer);
  141. const bulkActionsContainer = bulkModeContainer.querySelector(Selectors.bulkActionsContainer);
  142. this.getBulkActions().forEach((bulkAction) => {
  143. // Register the listener events for each available bulk action.
  144. bulkAction.registerListenerEvents(bulkActionsContainer);
  145. // Set the selected items for each available bulk action.
  146. bulkAction.setSelectedItems(this.selectedItems);
  147. });
  148. // Register the click listener event for the cancel bulk mode button.
  149. bulkModeContainer.addEventListener('click', (e) => {
  150. if (e.target.closest(Selectors.cancelBulkActionModeElement)) {
  151. // Deselect all selected items.
  152. this.selectedItems.forEach((item) => {
  153. this.deselectItem(item);
  154. });
  155. // Disable the bulk action mode.
  156. this.disableBulkActionsMode();
  157. }
  158. });
  159. this.isBulkActionsModeEnabled = true;
  160. }
  161. /**
  162. * Disables the bulk action mode.
  163. *
  164. * @method disableBulkActionsMode
  165. * @returns {void}
  166. */
  167. disableBulkActionsMode() {
  168. // If there was any previous (initial) content in the sticky footer, restore it.
  169. if (this.initialStickyFooterContent.length > 0) {
  170. this.getStickyFooterContainer().innerHTML = this.initialStickyFooterContent;
  171. } else { // No previous content to restore, disable the sticky footer.
  172. disableStickyFooter();
  173. }
  174. this.isBulkActionsModeEnabled = false;
  175. }
  176. /**
  177. * Renders the bulk actions content.
  178. *
  179. * @method renderBulkActions
  180. * @returns {Promise<string>}
  181. */
  182. async renderBulkActions() {
  183. const data = {
  184. bulkselectioncount: this.selectedItems.length,
  185. actions: [],
  186. moreactions: [],
  187. hasmoreactions: false,
  188. };
  189. const bulkActions = this.getBulkActions();
  190. const showMoreButton = bulkActions.length > this.maxButtons;
  191. // Get all bulk actions and render them in order.
  192. const actions = await Promise.all(
  193. bulkActions.map((bulkAction, index) =>
  194. bulkAction.renderBulkActionTrigger(
  195. showMoreButton && (index >= this.maxButtons - 1),
  196. index
  197. )
  198. )
  199. );
  200. // Separate rendered actions into data.actions and data.moreactions in the correct order.
  201. actions.forEach((actionTrigger, index) => {
  202. if (showMoreButton && (index >= this.maxButtons - 1)) {
  203. data.moreactions.push({'actiontrigger': actionTrigger});
  204. } else {
  205. data.actions.push({'actiontrigger': actionTrigger});
  206. }
  207. });
  208. data.hasmoreactions = data.moreactions.length > 0;
  209. return Templates.render('core/bulkactions/bulk_actions', data);
  210. }
  211. /**
  212. * Updates the selected items count in the bulk actions content.
  213. *
  214. * @method updateBulkItemSelection
  215. * @returns {void}
  216. */
  217. async updateBulkItemSelection() {
  218. const bulkSelection = await getString('bulkselection', 'core', this.selectedItems.length);
  219. document.querySelector(Selectors.selectedItemsCountContainer).innerHTML = bulkSelection;
  220. }
  221. }