lib/amd/src/utility.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. * Javascript handling for HTML attributes. This module gets autoloaded on page load.
  17. *
  18. * With the appropriate HTML attributes, various functionalities defined in this module can be used such as a displaying
  19. * an alert or a confirmation modal, etc.
  20. *
  21. * @module core/utility
  22. * @copyright 2021 Andrew Nicols <andrew@nicols.co.uk>
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24. * @since 4.0
  25. *
  26. * @example <caption>Calling the confirmation modal to delete a block</caption>
  27. *
  28. * // The following is an example of how to use this module via an indirect PHP call with a button.
  29. *
  30. * $controls[] = new action_menu_link_secondary(
  31. * $deleteactionurl,
  32. * new pix_icon('t/delete', $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
  33. * $str,
  34. * [
  35. * 'class' => 'editing_delete',
  36. * 'data-modal' => 'confirmation', // Needed so this module will pick it up in the click handler.
  37. * 'data-modal-title-str' => json_encode(['deletecheck_modal', 'block']),
  38. * 'data-modal-content-str' => json_encode(['deleteblockcheck', 'block', $blocktitle]),
  39. * 'data-modal-yes-button-str' => json_encode(['delete', 'core']),
  40. * 'data-modal-toast' => 'true', // Can be set to inform the user that their action was a success.
  41. * 'data-modal-toast-confirmation-str' => json_encode(['deleteblockinprogress', 'block', $blocktitle]),
  42. * 'data-modal-destination' => $deleteconfirmationurl->out(false), // Where do you want to direct the user?
  43. * ]
  44. * );
  45. */
  46. import * as Str from 'core/str';
  47. import Pending from 'core/pending';
  48. import {add as addToast} from 'core/toast';
  49. import {saveCancelPromise, deleteCancelPromise, exception} from 'core/notification';
  50. // We want to ensure that we only initialize the listeners only once.
  51. let registered = false;
  52. /**
  53. * Either fetch the string or return it from the dom node.
  54. *
  55. * @method getConfirmationString
  56. * @private
  57. * @param {HTMLElement} dataset The page element to fetch dataset items in
  58. * @param {String} type The type of string to fetch
  59. * @param {String} field The dataset field name to fetch the contents of
  60. * @param {Array|null} [defaultValue=null] The default params to pass to get_string if no value is found in a dataset
  61. * @return {Promise}
  62. *
  63. */
  64. const getModalString = (dataset, type, field, defaultValue = null) => {
  65. if (dataset[`${type}${field}Str`]) {
  66. return Str.get_string.apply(null, JSON.parse(dataset[`${type}${field}Str`]));
  67. }
  68. if (dataset[`${type}${field}`]) {
  69. return Promise.resolve(dataset[`${type}${field}`]);
  70. }
  71. if (defaultValue) {
  72. return Str.get_string.apply(null, defaultValue);
  73. }
  74. return null;
  75. };
  76. /**
  77. * Display a save/cancel confirmation.
  78. *
  79. * @private
  80. * @param {HTMLElement} source The title of the confirmation
  81. * @param {String} type The content of the confirmation
  82. * @returns {Promise}
  83. */
  84. const displayConfirmation = (source, type) => {
  85. let confirmationPromise = null;
  86. if (`${type}Type` in source.dataset && source.dataset[`${type}Type`] === 'delete') {
  87. confirmationPromise = deleteCancelPromise(
  88. getModalString(source.dataset, type, 'Title', ['confirm', 'core']),
  89. getModalString(source.dataset, type, 'Content'),
  90. getModalString(source.dataset, type, 'YesButton', ['yes', 'core'])
  91. );
  92. } else {
  93. confirmationPromise = saveCancelPromise(
  94. getModalString(source.dataset, type, 'Title', ['confirm', 'core']),
  95. getModalString(source.dataset, type, 'Content'),
  96. getModalString(source.dataset, type, 'YesButton', ['yes', 'core'])
  97. );
  98. }
  99. return confirmationPromise.then(() => {
  100. if (source.dataset[`${type}Toast`] === 'true') {
  101. const stringForToast = getModalString(source.dataset, type, 'ToastConfirmation');
  102. if (typeof stringForToast === "string") {
  103. addToast(stringForToast);
  104. } else {
  105. stringForToast.then(str => addToast(str)).catch(e => exception(e));
  106. }
  107. }
  108. if (source.dataset[`${type}Destination`]) {
  109. window.location.href = source.dataset[`${type}Destination`];
  110. return;
  111. }
  112. if (source.closest('form')) {
  113. // Update the modal and confirmation data fields so that we don't loop.
  114. source.dataset.confirmation = 'none';
  115. source.dataset.modal = 'none';
  116. // Click on the button again.
  117. // Note: Do not use the form.submit() because it will not work for cancel buttons.
  118. source.click();
  119. return;
  120. }
  121. const link = source.closest('a');
  122. if (link && link.href && link.href !== '#') {
  123. window.location.href = link.href;
  124. return;
  125. }
  126. const button = source.closest('button, input[type="submit"], input[type="button"], input[type="reset"]');
  127. if (button) {
  128. source.dataset.modalSubmitting = true;
  129. source.click();
  130. return;
  131. }
  132. window.console.error(`No destination found for ${type} modal`);
  133. return;
  134. }).catch(() => {
  135. return;
  136. });
  137. };
  138. /**
  139. * Display an alert and return the promise from it.
  140. *
  141. * @private
  142. * @param {String} title The title of the alert
  143. * @param {String} body The content of the alert
  144. * @returns {Promise<ModalAlert>}
  145. */
  146. const displayAlert = async(title, body) => {
  147. const pendingPromise = new Pending('core/confirm:alert');
  148. const AlertModal = await import('core/local/modal/alert');
  149. return AlertModal.create({
  150. title,
  151. body,
  152. removeOnClose: true,
  153. show: true,
  154. })
  155. .then((modal) => {
  156. pendingPromise.resolve();
  157. return modal;
  158. });
  159. };
  160. /**
  161. * Set up the listeners for the confirmation modal widget within the page.
  162. *
  163. * @method registerConfirmationListeners
  164. * @private
  165. */
  166. const registerConfirmationListeners = () => {
  167. document.addEventListener('click', e => {
  168. if (e.target.closest('[data-modal-submitting]')) {
  169. return;
  170. }
  171. const confirmRequest = e.target.closest('[data-confirmation="modal"]');
  172. if (confirmRequest) {
  173. e.preventDefault();
  174. displayConfirmation(confirmRequest, 'confirmation');
  175. }
  176. const modalConfirmation = e.target.closest('[data-modal="confirmation"]');
  177. if (modalConfirmation) {
  178. e.preventDefault();
  179. displayConfirmation(modalConfirmation, 'modal');
  180. }
  181. const alertRequest = e.target.closest('[data-modal="alert"]');
  182. if (alertRequest) {
  183. e.preventDefault();
  184. displayAlert(
  185. getModalString(alertRequest.dataset, 'modal', 'Title'),
  186. getModalString(alertRequest.dataset, 'modal', 'Content'),
  187. );
  188. }
  189. });
  190. };
  191. if (!registered) {
  192. registerConfirmationListeners();
  193. registered = true;
  194. }