course/format/amd/src/local/content/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. /**
  16. * Course state actions dispatcher.
  17. *
  18. * This module captures all data-dispatch links in the course content and dispatch the proper
  19. * state mutation, including any confirmation and modal required.
  20. *
  21. * @module core_courseformat/local/content/actions
  22. * @class core_courseformat/local/content/actions
  23. * @copyright 2021 Ferran Recio <ferran@moodle.com>
  24. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25. */
  26. import {BaseComponent} from 'core/reactive';
  27. import Modal from 'core/modal';
  28. import ModalSaveCancel from 'core/modal_save_cancel';
  29. import ModalDeleteCancel from 'core/modal_delete_cancel';
  30. import ModalEvents from 'core/modal_events';
  31. import Templates from 'core/templates';
  32. import {prefetchStrings} from 'core/prefetch';
  33. import {getString} from 'core/str';
  34. import {getFirst} from 'core/normalise';
  35. import {toggleBulkSelectionAction} from 'core_courseformat/local/content/actions/bulkselection';
  36. import * as CourseEvents from 'core_course/events';
  37. import Pending from 'core/pending';
  38. import ContentTree from 'core_courseformat/local/courseeditor/contenttree';
  39. // The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.
  40. import jQuery from 'jquery';
  41. // Load global strings.
  42. prefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']);
  43. // Mutations are dispatched by the course content actions.
  44. // Formats can use this module addActions static method to add custom actions.
  45. // Direct mutations can be simple strings (mutation) name or functions.
  46. const directMutations = {
  47. sectionHide: 'sectionHide',
  48. sectionShow: 'sectionShow',
  49. cmHide: 'cmHide',
  50. cmShow: 'cmShow',
  51. cmStealth: 'cmStealth',
  52. cmMoveRight: 'cmMoveRight',
  53. cmMoveLeft: 'cmMoveLeft',
  54. cmNoGroups: 'cmNoGroups',
  55. cmSeparateGroups: 'cmSeparateGroups',
  56. cmVisibleGroups: 'cmVisibleGroups',
  57. };
  58. export default class extends BaseComponent {
  59. /**
  60. * Constructor hook.
  61. */
  62. create() {
  63. // Optional component name for debugging.
  64. this.name = 'content_actions';
  65. // Default query selectors.
  66. this.selectors = {
  67. ACTIONLINK: `[data-action]`,
  68. // Move modal selectors.
  69. SECTIONLINK: `[data-for='section']`,
  70. CMLINK: `[data-for='cm']`,
  71. SECTIONNODE: `[data-for='sectionnode']`,
  72. MODALTOGGLER: `[data-toggle='collapse']`,
  73. ADDSECTION: `[data-action='addSection']`,
  74. CONTENTTREE: `#destination-selector`,
  75. ACTIONMENU: `.action-menu`,
  76. ACTIONMENUTOGGLER: `[data-toggle="dropdown"]`,
  77. // Availability modal selectors.
  78. OPTIONSRADIO: `[type='radio']`,
  79. };
  80. // Component css classes.
  81. this.classes = {
  82. DISABLED: `text-body`,
  83. ITALIC: `font-italic`,
  84. };
  85. }
  86. /**
  87. * Add extra actions to the module.
  88. *
  89. * @param {array} actions array of methods to execute
  90. */
  91. static addActions(actions) {
  92. for (const [action, mutationReference] of Object.entries(actions)) {
  93. if (typeof mutationReference !== 'function' && typeof mutationReference !== 'string') {
  94. throw new Error(`${action} action must be a mutation name or a function`);
  95. }
  96. directMutations[action] = mutationReference;
  97. }
  98. }
  99. /**
  100. * Initial state ready method.
  101. *
  102. * @param {Object} state the state data.
  103. *
  104. */
  105. stateReady(state) {
  106. // Delegate dispatch clicks.
  107. this.addEventListener(
  108. this.element,
  109. 'click',
  110. this._dispatchClick
  111. );
  112. // Check section limit.
  113. this._checkSectionlist({state});
  114. // Add an Event listener to recalculate limits it if a section HTML is altered.
  115. this.addEventListener(
  116. this.element,
  117. CourseEvents.sectionRefreshed,
  118. () => this._checkSectionlist({state})
  119. );
  120. }
  121. /**
  122. * Return the component watchers.
  123. *
  124. * @returns {Array} of watchers
  125. */
  126. getWatchers() {
  127. return [
  128. // Check section limit.
  129. {watch: `course.sectionlist:updated`, handler: this._checkSectionlist},
  130. ];
  131. }
  132. _dispatchClick(event) {
  133. const target = event.target.closest(this.selectors.ACTIONLINK);
  134. if (!target) {
  135. return;
  136. }
  137. if (target.classList.contains(this.classes.DISABLED)) {
  138. event.preventDefault();
  139. return;
  140. }
  141. // Invoke proper method.
  142. const actionName = target.dataset.action;
  143. const methodName = this._actionMethodName(actionName);
  144. if (this[methodName] !== undefined) {
  145. this[methodName](target, event);
  146. return;
  147. }
  148. // Check direct mutations or mutations handlers.
  149. if (directMutations[actionName] !== undefined) {
  150. if (typeof directMutations[actionName] === 'function') {
  151. directMutations[actionName](target, event);
  152. return;
  153. }
  154. this._requestMutationAction(target, event, directMutations[actionName]);
  155. return;
  156. }
  157. }
  158. _actionMethodName(name) {
  159. const requestName = name.charAt(0).toUpperCase() + name.slice(1);
  160. return `_request${requestName}`;
  161. }
  162. /**
  163. * Check the section list and disable some options if needed.
  164. *
  165. * @param {Object} detail the update details.
  166. * @param {Object} detail.state the state object.
  167. */
  168. _checkSectionlist({state}) {
  169. // Disable "add section" actions if the course max sections has been exceeded.
  170. this._setAddSectionLocked(state.course.sectionlist.length > state.course.maxsections);
  171. }
  172. /**
  173. * Return the ids represented by this element.
  174. *
  175. * Depending on the dataset attributes the action could represent a single id
  176. * or a bulk actions with all the current selected ids.
  177. *
  178. * @param {HTMLElement} target
  179. * @returns {Number[]} array of Ids
  180. */
  181. _getTargetIds(target) {
  182. let ids = [];
  183. if (target?.dataset?.id) {
  184. ids.push(target.dataset.id);
  185. }
  186. const bulkType = target?.dataset?.bulk;
  187. if (!bulkType) {
  188. return ids;
  189. }
  190. const bulk = this.reactive.get('bulk');
  191. if (bulk.enabled && bulk.selectedType === bulkType) {
  192. ids = [...ids, ...bulk.selection];
  193. }
  194. return ids;
  195. }
  196. /**
  197. * Handle a move section request.
  198. *
  199. * @param {Element} target the dispatch action element
  200. * @param {Event} event the triggered event
  201. */
  202. async _requestMoveSection(target, event) {
  203. // Check we have an id.
  204. const sectionIds = this._getTargetIds(target);
  205. if (sectionIds.length == 0) {
  206. return;
  207. }
  208. event.preventDefault();
  209. const pendingModalReady = new Pending(`courseformat/actions:prepareMoveSectionModal`);
  210. // The section edit menu to refocus on end.
  211. const editTools = this._getClosestActionMenuToogler(target);
  212. // Collect section information from the state.
  213. const exporter = this.reactive.getExporter();
  214. const data = exporter.course(this.reactive.state);
  215. let titleText = null;
  216. // Add the target section id and title.
  217. let sectionInfo = null;
  218. if (sectionIds.length == 1) {
  219. sectionInfo = this.reactive.get('section', sectionIds[0]);
  220. data.sectionid = sectionInfo.id;
  221. data.sectiontitle = sectionInfo.title;
  222. data.information = await this.reactive.getFormatString('sectionmove_info', data.sectiontitle);
  223. titleText = this.reactive.getFormatString('sectionmove_title');
  224. } else {
  225. data.information = await this.reactive.getFormatString('sectionsmove_info', sectionIds.length);
  226. titleText = this.reactive.getFormatString('sectionsmove_title');
  227. }
  228. // Create the modal.
  229. // Build the modal parameters from the event data.
  230. const modal = await this._modalBodyRenderedPromise(Modal, {
  231. title: titleText,
  232. body: Templates.render('core_courseformat/local/content/movesection', data),
  233. });
  234. const modalBody = getFirst(modal.getBody());
  235. // Disable current selected section ids.
  236. sectionIds.forEach(sectionId => {
  237. const currentElement = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-id='${sectionId}']`);
  238. this._disableLink(currentElement);
  239. });
  240. // Setup keyboard navigation.
  241. new ContentTree(
  242. modalBody.querySelector(this.selectors.CONTENTTREE),
  243. {
  244. SECTION: this.selectors.SECTIONNODE,
  245. TOGGLER: this.selectors.MODALTOGGLER,
  246. COLLAPSE: this.selectors.MODALTOGGLER,
  247. },
  248. true
  249. );
  250. // Capture click.
  251. modalBody.addEventListener('click', (event) => {
  252. const target = event.target;
  253. if (!target.matches('a') || target.dataset.for != 'section' || target.dataset.id === undefined) {
  254. return;
  255. }
  256. if (target.getAttribute('aria-disabled')) {
  257. return;
  258. }
  259. event.preventDefault();
  260. this.reactive.dispatch('sectionMoveAfter', sectionIds, target.dataset.id);
  261. this._destroyModal(modal, editTools);
  262. });
  263. pendingModalReady.resolve();
  264. }
  265. /**
  266. * Handle a move cm request.
  267. *
  268. * @param {Element} target the dispatch action element
  269. * @param {Event} event the triggered event
  270. */
  271. async _requestMoveCm(target, event) {
  272. // Check we have an id.
  273. const cmIds = this._getTargetIds(target);
  274. if (cmIds.length == 0) {
  275. return;
  276. }
  277. event.preventDefault();
  278. const pendingModalReady = new Pending(`courseformat/actions:prepareMoveCmModal`);
  279. // The section edit menu to refocus on end.
  280. const editTools = this._getClosestActionMenuToogler(target);
  281. // Collect information from the state.
  282. const exporter = this.reactive.getExporter();
  283. const data = exporter.course(this.reactive.state);
  284. let titleText = null;
  285. if (cmIds.length == 1) {
  286. const cmInfo = this.reactive.get('cm', cmIds[0]);
  287. data.cmid = cmInfo.id;
  288. data.cmname = cmInfo.name;
  289. data.information = await this.reactive.getFormatString('cmmove_info', data.cmname);
  290. titleText = this.reactive.getFormatString('cmmove_title');
  291. } else {
  292. data.information = await this.reactive.getFormatString('cmsmove_info', cmIds.length);
  293. titleText = this.reactive.getFormatString('cmsmove_title');
  294. }
  295. // Create the modal.
  296. // Build the modal parameters from the event data.
  297. const modal = await this._modalBodyRenderedPromise(Modal, {
  298. title: titleText,
  299. body: Templates.render('core_courseformat/local/content/movecm', data),
  300. });
  301. const modalBody = getFirst(modal.getBody());
  302. // Disable current selected section ids.
  303. cmIds.forEach(cmId => {
  304. const currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);
  305. this._disableLink(currentElement);
  306. });
  307. // Setup keyboard navigation.
  308. new ContentTree(
  309. modalBody.querySelector(this.selectors.CONTENTTREE),
  310. {
  311. SECTION: this.selectors.SECTIONNODE,
  312. TOGGLER: this.selectors.MODALTOGGLER,
  313. COLLAPSE: this.selectors.MODALTOGGLER,
  314. ENTER: this.selectors.SECTIONLINK,
  315. }
  316. );
  317. // Open the cm section node if possible (Bootstrap 4 uses jQuery to interact with collapsibles).
  318. // All jQuery in this code can be replaced when MDL-71979 is integrated.
  319. cmIds.forEach(cmId => {
  320. const currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);
  321. const sectionnode = currentElement.closest(this.selectors.SECTIONNODE);
  322. const toggler = jQuery(sectionnode).find(this.selectors.MODALTOGGLER);
  323. let collapsibleId = toggler.data('target') ?? toggler.attr('href');
  324. if (collapsibleId) {
  325. // We cannot be sure we have # in the id element name.
  326. collapsibleId = collapsibleId.replace('#', '');
  327. const expandNode = modalBody.querySelector(`#${collapsibleId}`);
  328. jQuery(expandNode).collapse('show');
  329. }
  330. });
  331. modalBody.addEventListener('click', (event) => {
  332. const target = event.target;
  333. if (!target.matches('a') || target.dataset.for === undefined || target.dataset.id === undefined) {
  334. return;
  335. }
  336. if (target.getAttribute('aria-disabled')) {
  337. return;
  338. }
  339. event.preventDefault();
  340. let targetSectionId;
  341. let targetCmId;
  342. if (target.dataset.for == 'cm') {
  343. const dropData = exporter.cmDraggableData(this.reactive.state, target.dataset.id);
  344. targetSectionId = dropData.sectionid;
  345. targetCmId = dropData.nextcmid;
  346. } else {
  347. const section = this.reactive.get('section', target.dataset.id);
  348. targetSectionId = target.dataset.id;
  349. targetCmId = section?.cmlist[0];
  350. }
  351. this.reactive.dispatch('cmMove', cmIds, targetSectionId, targetCmId);
  352. this._destroyModal(modal, editTools);
  353. });
  354. pendingModalReady.resolve();
  355. }
  356. /**
  357. * Handle a create section request.
  358. *
  359. * @param {Element} target the dispatch action element
  360. * @param {Event} event the triggered event
  361. */
  362. async _requestAddSection(target, event) {
  363. event.preventDefault();
  364. this.reactive.dispatch('addSection', target.dataset.id ?? 0);
  365. }
  366. /**
  367. * Handle a delete section request.
  368. *
  369. * @param {Element} target the dispatch action element
  370. * @param {Event} event the triggered event
  371. */
  372. async _requestDeleteSection(target, event) {
  373. const sectionIds = this._getTargetIds(target);
  374. if (sectionIds.length == 0) {
  375. return;
  376. }
  377. event.preventDefault();
  378. // We don't need confirmation to delete empty sections.
  379. let needsConfirmation = sectionIds.some(sectionId => {
  380. const sectionInfo = this.reactive.get('section', sectionId);
  381. const cmList = sectionInfo.cmlist ?? [];
  382. return (cmList.length || sectionInfo.hassummary || sectionInfo.rawtitle);
  383. });
  384. if (!needsConfirmation) {
  385. this.reactive.dispatch('sectionDelete', sectionIds);
  386. return;
  387. }
  388. let bodyText = null;
  389. let titleText = null;
  390. if (sectionIds.length == 1) {
  391. titleText = this.reactive.getFormatString('sectiondelete_title');
  392. const sectionInfo = this.reactive.get('section', sectionIds[0]);
  393. bodyText = this.reactive.getFormatString('sectiondelete_info', {name: sectionInfo.title});
  394. } else {
  395. titleText = this.reactive.getFormatString('sectionsdelete_title');
  396. bodyText = this.reactive.getFormatString('sectionsdelete_info', {count: sectionIds.length});
  397. }
  398. const modal = await this._modalBodyRenderedPromise(ModalDeleteCancel, {
  399. title: titleText,
  400. body: bodyText,
  401. });
  402. modal.getRoot().on(
  403. ModalEvents.delete,
  404. e => {
  405. // Stop the default save button behaviour which is to close the modal.
  406. e.preventDefault();
  407. modal.destroy();
  408. this.reactive.dispatch('sectionDelete', sectionIds);
  409. }
  410. );
  411. }
  412. /**
  413. * Handle a toggle cm selection.
  414. *
  415. * @param {Element} target the dispatch action element
  416. * @param {Event} event the triggered event
  417. */
  418. async _requestToggleSelectionCm(target, event) {
  419. toggleBulkSelectionAction(this.reactive, target, event, 'cm');
  420. }
  421. /**
  422. * Handle a toggle section selection.
  423. *
  424. * @param {Element} target the dispatch action element
  425. * @param {Event} event the triggered event
  426. */
  427. async _requestToggleSelectionSection(target, event) {
  428. toggleBulkSelectionAction(this.reactive, target, event, 'section');
  429. }
  430. /**
  431. * Basic mutation action helper.
  432. *
  433. * @param {Element} target the dispatch action element
  434. * @param {Event} event the triggered event
  435. * @param {string} mutationName the mutation name
  436. */
  437. async _requestMutationAction(target, event, mutationName) {
  438. if (!target.dataset.id && target.dataset.for !== 'bulkaction') {
  439. return;
  440. }
  441. event.preventDefault();
  442. if (target.dataset.for === 'bulkaction') {
  443. // If the mutation is a bulk action we use the current selection.
  444. this.reactive.dispatch(mutationName, this.reactive.get('bulk').selection);
  445. } else {
  446. this.reactive.dispatch(mutationName, [target.dataset.id]);
  447. }
  448. }
  449. /**
  450. * Handle a course module duplicate request.
  451. *
  452. * @param {Element} target the dispatch action element
  453. * @param {Event} event the triggered event
  454. */
  455. async _requestCmDuplicate(target, event) {
  456. const cmIds = this._getTargetIds(target);
  457. if (cmIds.length == 0) {
  458. return;
  459. }
  460. const sectionId = target.dataset.sectionid ?? null;
  461. event.preventDefault();
  462. this.reactive.dispatch('cmDuplicate', cmIds, sectionId);
  463. }
  464. /**
  465. * Handle a delete cm request.
  466. *
  467. * @param {Element} target the dispatch action element
  468. * @param {Event} event the triggered event
  469. */
  470. async _requestCmDelete(target, event) {
  471. const cmIds = this._getTargetIds(target);
  472. if (cmIds.length == 0) {
  473. return;
  474. }
  475. event.preventDefault();
  476. let bodyText = null;
  477. let titleText = null;
  478. if (cmIds.length == 1) {
  479. const cmInfo = this.reactive.get('cm', cmIds[0]);
  480. titleText = getString('cmdelete_title', 'core_courseformat');
  481. bodyText = getString(
  482. 'cmdelete_info',
  483. 'core_courseformat',
  484. {
  485. type: cmInfo.modname,
  486. name: cmInfo.name,
  487. }
  488. );
  489. } else {
  490. titleText = getString('cmsdelete_title', 'core_courseformat');
  491. bodyText = getString(
  492. 'cmsdelete_info',
  493. 'core_courseformat',
  494. {count: cmIds.length}
  495. );
  496. }
  497. const modal = await this._modalBodyRenderedPromise(ModalDeleteCancel, {
  498. title: titleText,
  499. body: bodyText,
  500. });
  501. modal.getRoot().on(
  502. ModalEvents.delete,
  503. e => {
  504. // Stop the default save button behaviour which is to close the modal.
  505. e.preventDefault();
  506. modal.destroy();
  507. this.reactive.dispatch('cmDelete', cmIds);
  508. }
  509. );
  510. }
  511. /**
  512. * Handle a cm availability change request.
  513. *
  514. * @param {Element} target the dispatch action element
  515. */
  516. async _requestCmAvailability(target) {
  517. const cmIds = this._getTargetIds(target);
  518. if (cmIds.length == 0) {
  519. return;
  520. }
  521. // Show the availability modal to decide which action to trigger.
  522. const exporter = this.reactive.getExporter();
  523. const data = {
  524. allowstealth: exporter.canUseStealth(this.reactive.state, cmIds),
  525. };
  526. const modal = await this._modalBodyRenderedPromise(ModalSaveCancel, {
  527. title: getString('availability', 'core'),
  528. body: Templates.render('core_courseformat/local/content/cm/availabilitymodal', data),
  529. saveButtonText: getString('apply', 'core'),
  530. });
  531. this._setupMutationRadioButtonModal(modal, cmIds);
  532. }
  533. /**
  534. * Handle a section availability change request.
  535. *
  536. * @param {Element} target the dispatch action element
  537. */
  538. async _requestSectionAvailability(target) {
  539. const sectionIds = this._getTargetIds(target);
  540. if (sectionIds.length == 0) {
  541. return;
  542. }
  543. const title = (sectionIds.length == 1) ? 'sectionavailability_title' : 'sectionsavailability_title';
  544. // Show the availability modal to decide which action to trigger.
  545. const modal = await this._modalBodyRenderedPromise(ModalSaveCancel, {
  546. title: this.reactive.getFormatString(title),
  547. body: Templates.render('core_courseformat/local/content/section/availabilitymodal', []),
  548. saveButtonText: getString('apply', 'core'),
  549. });
  550. this._setupMutationRadioButtonModal(modal, sectionIds);
  551. }
  552. /**
  553. * Add events to a mutation selector radio buttons modal.
  554. * @param {Modal} modal
  555. * @param {Number[]} ids the section or cm ids to apply the mutation
  556. */
  557. _setupMutationRadioButtonModal(modal, ids) {
  558. // The save button is not enabled until the user selects an option.
  559. modal.setButtonDisabled('save', true);
  560. const submitFunction = (radio) => {
  561. const mutation = radio?.value;
  562. if (!mutation) {
  563. return false;
  564. }
  565. this.reactive.dispatch(mutation, ids);
  566. return true;
  567. };
  568. const modalBody = getFirst(modal.getBody());
  569. const radioOptions = modalBody.querySelectorAll(this.selectors.OPTIONSRADIO);
  570. radioOptions.forEach(radio => {
  571. radio.addEventListener('change', () => {
  572. modal.setButtonDisabled('save', false);
  573. });
  574. radio.parentNode.addEventListener('click', () => {
  575. radio.checked = true;
  576. modal.setButtonDisabled('save', false);
  577. });
  578. radio.parentNode.addEventListener('dblclick', dbClickEvent => {
  579. if (submitFunction(radio)) {
  580. dbClickEvent.preventDefault();
  581. modal.destroy();
  582. }
  583. });
  584. });
  585. modal.getRoot().on(
  586. ModalEvents.save,
  587. () => {
  588. const radio = modalBody.querySelector(`${this.selectors.OPTIONSRADIO}:checked`);
  589. submitFunction(radio);
  590. }
  591. );
  592. }
  593. /**
  594. * Disable all add sections actions.
  595. *
  596. * @param {boolean} locked the new locked value.
  597. */
  598. _setAddSectionLocked(locked) {
  599. const targets = this.getElements(this.selectors.ADDSECTION);
  600. targets.forEach(element => {
  601. element.classList.toggle(this.classes.DISABLED, locked);
  602. element.classList.toggle(this.classes.ITALIC, locked);
  603. this.setElementLocked(element, locked);
  604. });
  605. }
  606. /**
  607. * Replace an element with a copy with a different tag name.
  608. *
  609. * @param {Element} element the original element
  610. */
  611. _disableLink(element) {
  612. if (element) {
  613. element.style.pointerEvents = 'none';
  614. element.style.userSelect = 'none';
  615. element.classList.add(this.classes.DISABLED);
  616. element.classList.add(this.classes.ITALIC);
  617. element.setAttribute('aria-disabled', true);
  618. element.addEventListener('click', event => event.preventDefault());
  619. }
  620. }
  621. /**
  622. * Render a modal and return a body ready promise.
  623. *
  624. * @param {Modal} ModalClass the modal class
  625. * @param {object} modalParams the modal params
  626. * @return {Promise} the modal body ready promise
  627. */
  628. _modalBodyRenderedPromise(ModalClass, modalParams) {
  629. return new Promise((resolve, reject) => {
  630. ModalClass.create(modalParams).then((modal) => {
  631. modal.setRemoveOnClose(true);
  632. // Handle body loading event.
  633. modal.getRoot().on(ModalEvents.bodyRendered, () => {
  634. resolve(modal);
  635. });
  636. // Configure some extra modal params.
  637. if (modalParams.saveButtonText !== undefined) {
  638. modal.setSaveButtonText(modalParams.saveButtonText);
  639. }
  640. if (modalParams.deleteButtonText !== undefined) {
  641. modal.setDeleteButtonText(modalParams.saveButtonText);
  642. }
  643. modal.show();
  644. return;
  645. }).catch(() => {
  646. reject(`Cannot load modal content`);
  647. });
  648. });
  649. }
  650. /**
  651. * Hide and later destroy a modal.
  652. *
  653. * Behat will fail if we remove the modal while some boostrap collapse is executing.
  654. *
  655. * @param {Modal} modal
  656. * @param {HTMLElement} element the dom element to focus on.
  657. */
  658. _destroyModal(modal, element) {
  659. modal.hide();
  660. const pendingDestroy = new Pending(`courseformat/actions:destroyModal`);
  661. if (element) {
  662. element.focus();
  663. }
  664. setTimeout(() =>{
  665. modal.destroy();
  666. pendingDestroy.resolve();
  667. }, 500);
  668. }
  669. /**
  670. * Get the closest actions menu toggler to an action element.
  671. *
  672. * @param {HTMLElement} element the action link element
  673. * @returns {HTMLElement|undefined}
  674. */
  675. _getClosestActionMenuToogler(element) {
  676. const actionMenu = element.closest(this.selectors.ACTIONMENU);
  677. if (!actionMenu) {
  678. return undefined;
  679. }
  680. return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER);
  681. }
  682. }