theme/boost/amd/src/bs4-compat.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. * Backward compatibility for Bootstrap 5.
  17. *
  18. * This module silently adapts the current page to Bootstrap 5.
  19. * When the Boostrap 4 backward compatibility period ends in MDL-84465,
  20. * this module will be removed.
  21. *
  22. * @module theme_boost/bs4-compat
  23. * @copyright 2025 Mikel Martín <mikel@moodle.com>
  24. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25. * @deprecated since Moodle 5.0
  26. * @todo Final deprecation in Moodle 6.0. See MDL-84465.
  27. */
  28. import {DefaultAllowlist} from './bootstrap/util/sanitizer';
  29. import Popover from 'theme_boost/bootstrap/popover';
  30. import Tooltip from 'theme_boost/bootstrap/tooltip';
  31. import log from 'core/log';
  32. /**
  33. * List of Bootstrap 4 elements to replace with Bootstrap 5 elements.
  34. * This list is based on the Bootstrap 4 to 5 migration guide:
  35. * https://getbootstrap.com/docs/5.0/migration/
  36. *
  37. * The list is not exhaustive and it will be updated as needed.
  38. */
  39. const bootstrapElements = [
  40. {
  41. selector: '.alert button.close',
  42. replacements: [
  43. {bs4: 'data-dismiss', bs5: 'data-bs-dismiss'},
  44. ],
  45. },
  46. {
  47. selector: '[data-toggle="modal"]',
  48. replacements: [
  49. {bs4: 'data-toggle', bs5: 'data-bs-toggle'},
  50. {bs4: 'data-target', bs5: 'data-bs-target'},
  51. ],
  52. },
  53. {
  54. selector: '.modal .modal-header button.close',
  55. replacements: [
  56. {bs4: 'data-dismiss', bs5: 'data-bs-dismiss'},
  57. ],
  58. },
  59. {
  60. selector: '[data-toggle="dropdown"]',
  61. replacements: [
  62. {bs4: 'data-toggle', bs5: 'data-bs-toggle'},
  63. ],
  64. },
  65. {
  66. selector: '[data-toggle="collapse"]',
  67. replacements: [
  68. {bs4: 'data-toggle', bs5: 'data-bs-toggle'},
  69. {bs4: 'data-target', bs5: 'data-bs-target'},
  70. {bs4: 'data-parent', bs5: 'data-bs-parent'},
  71. ],
  72. },
  73. {
  74. selector: '.carousel [data-slide]',
  75. replacements: [
  76. {bs4: 'data-slide', bs5: 'data-bs-slide'},
  77. {bs4: 'data-target', bs5: 'data-bs-target'},
  78. ],
  79. },
  80. {
  81. selector: '[data-toggle="tooltip"]',
  82. replacements: [
  83. {bs4: 'data-toggle', bs5: 'data-bs-toggle'},
  84. {bs4: 'data-placement', bs5: 'data-bs-placement'},
  85. {bs4: 'data-animation', bs5: 'data-bs-animation'},
  86. {bs4: 'data-delay', bs5: 'data-bs-delay'},
  87. {bs4: 'data-title', bs5: 'data-bs-title'},
  88. {bs4: 'data-html', bs5: 'data-bs-html'},
  89. {bs4: 'data-trigger', bs5: 'data-bs-trigger'},
  90. {bs4: 'data-selector', bs5: 'data-bs-selector'},
  91. {bs4: 'data-container', bs5: 'data-bs-container'},
  92. ],
  93. },
  94. {
  95. selector: '[data-toggle="popover"]',
  96. replacements: [
  97. {bs4: 'data-toggle', bs5: 'data-bs-toggle'},
  98. {bs4: 'data-content', bs5: 'data-bs-content'},
  99. {bs4: 'data-placement', bs5: 'data-bs-placement'},
  100. {bs4: 'data-animation', bs5: 'data-bs-animation'},
  101. {bs4: 'data-delay', bs5: 'data-bs-delay'},
  102. {bs4: 'data-title', bs5: 'data-bs-title'},
  103. {bs4: 'data-html', bs5: 'data-bs-html'},
  104. {bs4: 'data-trigger', bs5: 'data-bs-trigger'},
  105. {bs4: 'data-selector', bs5: 'data-bs-selector'},
  106. {bs4: 'data-container', bs5: 'data-bs-container'},
  107. ],
  108. },
  109. {
  110. selector: '[data-toggle="tab"]',
  111. replacements: [
  112. {bs4: 'data-toggle', bs5: 'data-bs-toggle'},
  113. {bs4: 'data-target', bs5: 'data-bs-target'},
  114. ],
  115. },
  116. ];
  117. /**
  118. * Replace Bootstrap 4 attributes with Bootstrap 5 attributes.
  119. *
  120. * @param {HTMLElement} container The element to search for Bootstrap 4 elements.
  121. */
  122. const replaceBootstrap4Attributes = (container) => {
  123. for (const bootstrapElement of bootstrapElements) {
  124. const elements = container.querySelectorAll(bootstrapElement.selector);
  125. for (const element of elements) {
  126. for (const replacement of bootstrapElement.replacements) {
  127. if (element.hasAttribute(replacement.bs4)) {
  128. element.setAttribute(replacement.bs5, element.getAttribute(replacement.bs4));
  129. element.removeAttribute(replacement.bs4);
  130. log.debug(`Silent Bootstrap 4 to 5 compatibility: ${replacement.bs4} replaced by ${replacement.bs5}`);
  131. log.debug(element);
  132. }
  133. }
  134. }
  135. }
  136. };
  137. /**
  138. * Ensure Bootstrap 4 components are initialized.
  139. *
  140. * Some elements (tooltip and popovers) needs to be initialized manually after adding the data attributes.
  141. *
  142. * @param {HTMLElement} container The element to search for Bootstrap 4 elements.
  143. */
  144. const initializeBootsrap4Components = (container) => {
  145. const popoverConfig = {
  146. container: 'body',
  147. trigger: 'focus',
  148. allowList: Object.assign(DefaultAllowlist, {table: [], thead: [], tbody: [], tr: [], th: [], td: []}),
  149. };
  150. container.querySelectorAll('[data-bs-toggle="popover"]').forEach((tooltipTriggerEl) => {
  151. const popOverInstance = Popover.getInstance(tooltipTriggerEl);
  152. if (!popOverInstance) {
  153. new Popover(tooltipTriggerEl, popoverConfig);
  154. }
  155. });
  156. container.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((tooltipTriggerEl) => {
  157. const tooltipInstance = Tooltip.getInstance(tooltipTriggerEl);
  158. if (!tooltipInstance) {
  159. new Tooltip(tooltipTriggerEl);
  160. }
  161. });
  162. };
  163. /**
  164. * Init Bootstrap 4 compatibility.
  165. *
  166. * @deprecated since Moodle 5.0
  167. * @param {HTMLElement} element The element to search for Bootstrap 4 elements.
  168. */
  169. export const init = (element) => {
  170. if (!element) {
  171. element = document;
  172. }
  173. replaceBootstrap4Attributes(element);
  174. initializeBootsrap4Components(element);
  175. };