lib/form/amd/src/choicedropdown.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. * Field controller for choicedropdown field.
  17. *
  18. * @module core_form/choicedropdown
  19. * @copyright 2023 Ferran Recio <ferran@moodle.com>
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. */
  22. import {getDropdownStatus} from 'core/local/dropdown/status';
  23. import {markFormAsDirty} from 'core_form/changechecker';
  24. const Classes = {
  25. notClickable: 'not-clickable',
  26. hidden: 'd-none',
  27. };
  28. /**
  29. * Internal form element class.
  30. *
  31. * @private
  32. * @class FieldController
  33. * @copyright 2023 Ferran Recio <ferran@moodle.com>
  34. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35. */
  36. class FieldController {
  37. /**
  38. * Class constructor.
  39. *
  40. * @param {String} elementId Form element id
  41. */
  42. constructor(elementId) {
  43. this.elementId = elementId;
  44. this.mainSelect = document.getElementById(this.elementId);
  45. this.dropdown = getDropdownStatus(`[data-form-controls="${this.elementId}"]`);
  46. this.dropdown.getElement().classList.remove(Classes.hidden);
  47. }
  48. /**
  49. * Add form element event listener.
  50. */
  51. addEventListeners() {
  52. this.dropdown.getElement().addEventListener(
  53. 'change',
  54. this.updateSelect.bind(this)
  55. );
  56. // Click on a dropdown link can trigger a wrong dirty form reload warning.
  57. this.dropdown.getElement().addEventListener(
  58. 'click',
  59. (event) => event.preventDefault()
  60. );
  61. this.mainSelect.addEventListener(
  62. 'change',
  63. this.updateDropdown.bind(this)
  64. );
  65. // Enabling or disabling the select does not trigger any JS event.
  66. const observerCallback = (mutations) => {
  67. mutations.forEach((mutation) => {
  68. if (mutation.type !== 'attributes' || mutation.attributeName !== 'disabled') {
  69. return;
  70. }
  71. this.updateDropdown();
  72. });
  73. };
  74. new MutationObserver(observerCallback).observe(
  75. this.mainSelect,
  76. {attributeFilter: ['disabled']}
  77. );
  78. }
  79. /**
  80. * Check if the field is disabled.
  81. * @returns {Boolean}
  82. */
  83. isDisabled() {
  84. return this.mainSelect?.hasAttribute('disabled');
  85. }
  86. /**
  87. * Update selected option preview in form.
  88. */
  89. async updateDropdown() {
  90. this.dropdown.setButtonDisabled(this.isDisabled());
  91. if (this.dropdown.getSelectedValue() == this.mainSelect.value) {
  92. return;
  93. }
  94. this.dropdown.setSelectedValue(this.mainSelect.value);
  95. }
  96. /**
  97. * Update selected option preview in form.
  98. */
  99. async updateSelect() {
  100. if (this.dropdown.getSelectedValue() == this.mainSelect.value) {
  101. return;
  102. }
  103. this.mainSelect.value = this.dropdown.getSelectedValue();
  104. markFormAsDirty(this.mainSelect.closest('form'));
  105. // Change the select element via JS does not trigger the standard change event.
  106. this.mainSelect.dispatchEvent(new Event('change'));
  107. }
  108. /**
  109. * Disable the choice dialog and convert it into a regular select field.
  110. */
  111. disableInteractiveDialog() {
  112. this.mainSelect?.classList.remove(Classes.hidden);
  113. const dropdownElement = this.dropdown.getElement();
  114. dropdownElement.classList.add(Classes.hidden);
  115. }
  116. /**
  117. * Check if the field has a force dialog attribute.
  118. // *
  119. * The force dialog is a setting to force the javascript control even in
  120. * behat test.
  121. *
  122. * @returns {Boolean} if the dialog modal should be forced or not
  123. */
  124. hasForceDialog() {
  125. return !!this.mainSelect?.dataset.forceDialog;
  126. }
  127. }
  128. /**
  129. * Initialises a choice dialog field.
  130. *
  131. * @method init
  132. * @param {String} elementId Form element id
  133. * @listens event:uploadStarted
  134. * @listens event:uploadCompleted
  135. */
  136. export const init = (elementId) => {
  137. const field = new FieldController(elementId);
  138. // This field is just a select wrapper. To optimize tests, we don't want to keep behat
  139. // waiting for extra loadings in this case. The set field steps are about testing other
  140. // stuff, not to test fancy javascript form fields. However, we keep the possibility of
  141. // testing the javascript part using behat when necessary.
  142. if (document.body.classList.contains('behat-site') && !field.hasForceDialog()) {
  143. field.disableInteractiveDialog();
  144. return;
  145. }
  146. field.addEventListeners();
  147. };