course/format/amd/src/local/content/bulkedittools.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. /**
  16. * The bulk editor tools bar.
  17. *
  18. * @module core_courseformat/local/content/bulkedittools
  19. * @class core_courseformat/local/content/bulkedittools
  20. * @copyright 2023 Ferran Recio <ferran@moodle.com>
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. import {BaseComponent} from 'core/reactive';
  24. import {disableStickyFooter, enableStickyFooter} from 'core/sticky-footer';
  25. import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
  26. import {getString} from 'core/str';
  27. import Pending from 'core/pending';
  28. import {prefetchStrings} from 'core/prefetch';
  29. import {
  30. selectAllBulk,
  31. switchBulkSelection,
  32. checkAllBulkSelected
  33. } from 'core_courseformat/local/content/actions/bulkselection';
  34. import Notification from 'core/notification';
  35. // Load global strings.
  36. prefetchStrings(
  37. 'core_courseformat',
  38. ['bulkselection']
  39. );
  40. export default class Component extends BaseComponent {
  41. /**
  42. * Constructor hook.
  43. */
  44. create() {
  45. // Optional component name for debugging.
  46. this.name = 'bulk_editor_tools';
  47. // Default query selectors.
  48. this.selectors = {
  49. ACTIONS: `[data-for="bulkaction"]`,
  50. ACTIONTOOL: `[data-for="bulkactions"] li`,
  51. CANCEL: `[data-for="bulkcancel"]`,
  52. COUNT: `[data-for='bulkcount']`,
  53. SELECTABLE: `[data-bulkcheckbox][data-is-selectable]`,
  54. SELECTALL: `[data-for="selectall"]`,
  55. BULKBTN: `[data-for="enableBulk"]`,
  56. };
  57. // Most classes will be loaded later by DndCmItem.
  58. this.classes = {
  59. HIDE: 'd-none',
  60. DISABLED: 'disabled',
  61. };
  62. }
  63. /**
  64. * Static method to create a component instance from the mustache template.
  65. *
  66. * @param {string} target optional altentative DOM main element CSS selector
  67. * @param {object} selectors optional css selector overrides
  68. * @return {Component}
  69. */
  70. static init(target, selectors) {
  71. return new this({
  72. element: document.querySelector(target),
  73. reactive: getCurrentCourseEditor(),
  74. selectors
  75. });
  76. }
  77. /**
  78. * Initial state ready method.
  79. */
  80. stateReady() {
  81. const cancelBtn = this.getElement(this.selectors.CANCEL);
  82. if (cancelBtn) {
  83. this.addEventListener(cancelBtn, 'click', this._cancelBulk);
  84. }
  85. const selectAll = this.getElement(this.selectors.SELECTALL);
  86. if (selectAll) {
  87. this.addEventListener(selectAll, 'click', this._selectAllClick);
  88. }
  89. }
  90. /**
  91. * Component watchers.
  92. *
  93. * @returns {Array} of watchers
  94. */
  95. getWatchers() {
  96. return [
  97. {watch: `bulk.enabled:updated`, handler: this._refreshEnabled},
  98. {watch: `bulk:updated`, handler: this._refreshTools},
  99. ];
  100. }
  101. /**
  102. * Hide and show the bulk edit tools.
  103. *
  104. * @param {object} param
  105. * @param {Object} param.element details the update details (state.bulk in this case).
  106. */
  107. _refreshEnabled({element}) {
  108. this._updatePageTitle(element.enabled).catch(Notification.exception);
  109. if (element.enabled) {
  110. enableStickyFooter();
  111. } else {
  112. disableStickyFooter();
  113. }
  114. }
  115. /**
  116. * Refresh the tools depending on the current selection.
  117. *
  118. * @param {object} param the state watcher information
  119. * @param {Object} param.state the full state data.
  120. * @param {Object} param.element the affected element (bulk in this case).
  121. */
  122. _refreshTools(param) {
  123. this._refreshSelectCount(param);
  124. this._refreshSelectAll(param);
  125. this._refreshActions(param);
  126. }
  127. /**
  128. * Refresh the selection count.
  129. *
  130. * @param {object} param
  131. * @param {Object} param.element the affected element (bulk in this case).
  132. */
  133. async _refreshSelectCount({element: bulk}) {
  134. const stringName = (bulk.selection.length > 1) ? 'bulkselection_plural' : 'bulkselection';
  135. const selectedCount = await getString(stringName, 'core_courseformat', bulk.selection.length);
  136. const selectedElement = this.getElement(this.selectors.COUNT);
  137. if (selectedElement) {
  138. selectedElement.innerHTML = selectedCount;
  139. }
  140. }
  141. /**
  142. * Refresh the select all element.
  143. *
  144. * @param {object} param
  145. * @param {Object} param.element the affected element (bulk in this case).
  146. */
  147. _refreshSelectAll({element: bulk}) {
  148. const selectall = this.getElement(this.selectors.SELECTALL);
  149. if (!selectall) {
  150. return;
  151. }
  152. selectall.disabled = (bulk.selectedType === '');
  153. // The changechecker module can prevent the checkbox form changing it's value.
  154. // To avoid that we leave the sniffer to act before changing the value.
  155. const pending = new Pending(`courseformat/bulktools:refreshSelectAll`);
  156. setTimeout(
  157. () => {
  158. selectall.checked = checkAllBulkSelected(this.reactive);
  159. pending.resolve();
  160. },
  161. 100
  162. );
  163. }
  164. /**
  165. * Refresh the visible action buttons depending on the selection type.
  166. *
  167. * @param {object} param
  168. * @param {Object} param.element the affected element (bulk in this case).
  169. */
  170. _refreshActions({element: bulk}) {
  171. // By default, we show the cm options.
  172. const displayType = (bulk.selectedType == 'section') ? 'section' : 'cm';
  173. const enabled = (bulk.selectedType !== '');
  174. this.getElements(this.selectors.ACTIONS).forEach(action => {
  175. action.classList.toggle(this.classes.DISABLED, !enabled);
  176. action.tabIndex = (enabled) ? 0 : -1;
  177. const actionTool = action.closest(this.selectors.ACTIONTOOL);
  178. const isHidden = (action.dataset.bulk != displayType);
  179. actionTool?.classList.toggle(this.classes.HIDE, isHidden);
  180. });
  181. }
  182. /**
  183. * Cancel bulk handler.
  184. */
  185. _cancelBulk() {
  186. const pending = new Pending(`courseformat/content:bulktoggle_off`);
  187. this.reactive.dispatch('bulkEnable', false);
  188. // Wait for a while and focus on enable bulk button.
  189. setTimeout(() => {
  190. document.querySelector(this.selectors.BULKBTN)?.focus();
  191. pending.resolve();
  192. }, 150);
  193. }
  194. /**
  195. * Handle special select all cases.
  196. * @param {Event} event
  197. */
  198. _selectAllClick(event) {
  199. event.preventDefault();
  200. if (event.altKey) {
  201. switchBulkSelection(this.reactive);
  202. return;
  203. }
  204. if (checkAllBulkSelected(this.reactive)) {
  205. this._handleUnselectAll();
  206. return;
  207. }
  208. selectAllBulk(this.reactive, true);
  209. }
  210. /**
  211. * Process unselect all elements.
  212. */
  213. _handleUnselectAll() {
  214. const pending = new Pending(`courseformat/content:bulktUnselectAll`);
  215. selectAllBulk(this.reactive, false);
  216. // Wait for a while and focus on the first checkbox.
  217. setTimeout(() => {
  218. document.querySelector(this.selectors.SELECTABLE)?.focus();
  219. pending.resolve();
  220. }, 150);
  221. }
  222. /**
  223. * Updates the <title> attribute of the page whenever bulk editing is toggled.
  224. *
  225. * This helps users, especially screen reader users, to understand the current state of the course homepage.
  226. *
  227. * @param {Boolean} enabled True when bulk editing is turned on. False, otherwise.
  228. * @returns {Promise<void>}
  229. * @private
  230. */
  231. async _updatePageTitle(enabled) {
  232. const enableBulk = document.querySelector(this.selectors.BULKBTN);
  233. let params, bulkEditTitle, editingTitle;
  234. if (enableBulk.dataset.sectiontitle) {
  235. // Section editing mode.
  236. params = {
  237. course: enableBulk.dataset.coursename,
  238. sectionname: enableBulk.dataset.sectionname,
  239. sectiontitle: enableBulk.dataset.sectiontitle,
  240. };
  241. bulkEditTitle = await getString('coursesectiontitlebulkediting', 'moodle', params);
  242. editingTitle = await getString('coursesectiontitleediting', 'moodle', params);
  243. } else {
  244. // Whole course editing mode.
  245. params = {
  246. course: enableBulk.dataset.coursename
  247. };
  248. bulkEditTitle = await getString('coursetitlebulkediting', 'moodle', params);
  249. editingTitle = await getString('coursetitleediting', 'moodle', params);
  250. }
  251. const pageTitle = document.title;
  252. if (enabled) {
  253. // Use bulk editing string for the page title.
  254. // At this point, the current page title should be the normal editing title.
  255. // So replace the normal editing title with the bulk editing title.
  256. document.title = pageTitle.replace(editingTitle, bulkEditTitle);
  257. } else {
  258. // Use the normal editing string for the page title.
  259. // At this point, the current page title should be the bulk editing title.
  260. // So replace the bulk editing title with the normal editing title.
  261. document.title = pageTitle.replace(bulkEditTitle, editingTitle);
  262. }
  263. }
  264. }