You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

320 lines
11 KiB

5 months ago
  1. /*!
  2. * Bootstrap modal.js v5.3.3 (https://getbootstrap.com/)
  3. * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
  4. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  5. */
  6. (function (global, factory) {
  7. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/backdrop.js'), require('./util/component-functions.js'), require('./util/focustrap.js'), require('./util/index.js'), require('./util/scrollbar.js')) :
  8. typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/backdrop', './util/component-functions', './util/focustrap', './util/index', './util/scrollbar'], factory) :
  9. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Modal = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Backdrop, global.ComponentFunctions, global.Focustrap, global.Index, global.Scrollbar));
  10. })(this, (function (BaseComponent, EventHandler, SelectorEngine, Backdrop, componentFunctions_js, FocusTrap, index_js, ScrollBarHelper) { 'use strict';
  11. /**
  12. * --------------------------------------------------------------------------
  13. * Bootstrap modal.js
  14. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  15. * --------------------------------------------------------------------------
  16. */
  17. /**
  18. * Constants
  19. */
  20. const NAME = 'modal';
  21. const DATA_KEY = 'bs.modal';
  22. const EVENT_KEY = `.${DATA_KEY}`;
  23. const DATA_API_KEY = '.data-api';
  24. const ESCAPE_KEY = 'Escape';
  25. const EVENT_HIDE = `hide${EVENT_KEY}`;
  26. const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`;
  27. const EVENT_HIDDEN = `hidden${EVENT_KEY}`;
  28. const EVENT_SHOW = `show${EVENT_KEY}`;
  29. const EVENT_SHOWN = `shown${EVENT_KEY}`;
  30. const EVENT_RESIZE = `resize${EVENT_KEY}`;
  31. const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`;
  32. const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`;
  33. const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`;
  34. const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;
  35. const CLASS_NAME_OPEN = 'modal-open';
  36. const CLASS_NAME_FADE = 'fade';
  37. const CLASS_NAME_SHOW = 'show';
  38. const CLASS_NAME_STATIC = 'modal-static';
  39. const OPEN_SELECTOR = '.modal.show';
  40. const SELECTOR_DIALOG = '.modal-dialog';
  41. const SELECTOR_MODAL_BODY = '.modal-body';
  42. const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"]';
  43. const Default = {
  44. backdrop: true,
  45. focus: true,
  46. keyboard: true
  47. };
  48. const DefaultType = {
  49. backdrop: '(boolean|string)',
  50. focus: 'boolean',
  51. keyboard: 'boolean'
  52. };
  53. /**
  54. * Class definition
  55. */
  56. class Modal extends BaseComponent {
  57. constructor(element, config) {
  58. super(element, config);
  59. this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);
  60. this._backdrop = this._initializeBackDrop();
  61. this._focustrap = this._initializeFocusTrap();
  62. this._isShown = false;
  63. this._isTransitioning = false;
  64. this._scrollBar = new ScrollBarHelper();
  65. this._addEventListeners();
  66. }
  67. // Getters
  68. static get Default() {
  69. return Default;
  70. }
  71. static get DefaultType() {
  72. return DefaultType;
  73. }
  74. static get NAME() {
  75. return NAME;
  76. }
  77. // Public
  78. toggle(relatedTarget) {
  79. return this._isShown ? this.hide() : this.show(relatedTarget);
  80. }
  81. show(relatedTarget) {
  82. if (this._isShown || this._isTransitioning) {
  83. return;
  84. }
  85. const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {
  86. relatedTarget
  87. });
  88. if (showEvent.defaultPrevented) {
  89. return;
  90. }
  91. this._isShown = true;
  92. this._isTransitioning = true;
  93. this._scrollBar.hide();
  94. document.body.classList.add(CLASS_NAME_OPEN);
  95. this._adjustDialog();
  96. this._backdrop.show(() => this._showElement(relatedTarget));
  97. }
  98. hide() {
  99. if (!this._isShown || this._isTransitioning) {
  100. return;
  101. }
  102. const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE);
  103. if (hideEvent.defaultPrevented) {
  104. return;
  105. }
  106. this._isShown = false;
  107. this._isTransitioning = true;
  108. this._focustrap.deactivate();
  109. this._element.classList.remove(CLASS_NAME_SHOW);
  110. this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());
  111. }
  112. dispose() {
  113. EventHandler.off(window, EVENT_KEY);
  114. EventHandler.off(this._dialog, EVENT_KEY);
  115. this._backdrop.dispose();
  116. this._focustrap.deactivate();
  117. super.dispose();
  118. }
  119. handleUpdate() {
  120. this._adjustDialog();
  121. }
  122. // Private
  123. _initializeBackDrop() {
  124. return new Backdrop({
  125. isVisible: Boolean(this._config.backdrop),
  126. // 'static' option will be translated to true, and booleans will keep their value,
  127. isAnimated: this._isAnimated()
  128. });
  129. }
  130. _initializeFocusTrap() {
  131. return new FocusTrap({
  132. trapElement: this._element
  133. });
  134. }
  135. _showElement(relatedTarget) {
  136. // try to append dynamic modal
  137. if (!document.body.contains(this._element)) {
  138. document.body.append(this._element);
  139. }
  140. this._element.style.display = 'block';
  141. this._element.removeAttribute('aria-hidden');
  142. this._element.setAttribute('aria-modal', true);
  143. this._element.setAttribute('role', 'dialog');
  144. this._element.scrollTop = 0;
  145. const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);
  146. if (modalBody) {
  147. modalBody.scrollTop = 0;
  148. }
  149. index_js.reflow(this._element);
  150. this._element.classList.add(CLASS_NAME_SHOW);
  151. const transitionComplete = () => {
  152. if (this._config.focus) {
  153. this._focustrap.activate();
  154. }
  155. this._isTransitioning = false;
  156. EventHandler.trigger(this._element, EVENT_SHOWN, {
  157. relatedTarget
  158. });
  159. };
  160. this._queueCallback(transitionComplete, this._dialog, this._isAnimated());
  161. }
  162. _addEventListeners() {
  163. EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {
  164. if (event.key !== ESCAPE_KEY) {
  165. return;
  166. }
  167. if (this._config.keyboard) {
  168. this.hide();
  169. return;
  170. }
  171. this._triggerBackdropTransition();
  172. });
  173. EventHandler.on(window, EVENT_RESIZE, () => {
  174. if (this._isShown && !this._isTransitioning) {
  175. this._adjustDialog();
  176. }
  177. });
  178. EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {
  179. // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks
  180. EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {
  181. if (this._element !== event.target || this._element !== event2.target) {
  182. return;
  183. }
  184. if (this._config.backdrop === 'static') {
  185. this._triggerBackdropTransition();
  186. return;
  187. }
  188. if (this._config.backdrop) {
  189. this.hide();
  190. }
  191. });
  192. });
  193. }
  194. _hideModal() {
  195. this._element.style.display = 'none';
  196. this._element.setAttribute('aria-hidden', true);
  197. this._element.removeAttribute('aria-modal');
  198. this._element.removeAttribute('role');
  199. this._isTransitioning = false;
  200. this._backdrop.hide(() => {
  201. document.body.classList.remove(CLASS_NAME_OPEN);
  202. this._resetAdjustments();
  203. this._scrollBar.reset();
  204. EventHandler.trigger(this._element, EVENT_HIDDEN);
  205. });
  206. }
  207. _isAnimated() {
  208. return this._element.classList.contains(CLASS_NAME_FADE);
  209. }
  210. _triggerBackdropTransition() {
  211. const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);
  212. if (hideEvent.defaultPrevented) {
  213. return;
  214. }
  215. const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;
  216. const initialOverflowY = this._element.style.overflowY;
  217. // return if the following background transition hasn't yet completed
  218. if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {
  219. return;
  220. }
  221. if (!isModalOverflowing) {
  222. this._element.style.overflowY = 'hidden';
  223. }
  224. this._element.classList.add(CLASS_NAME_STATIC);
  225. this._queueCallback(() => {
  226. this._element.classList.remove(CLASS_NAME_STATIC);
  227. this._queueCallback(() => {
  228. this._element.style.overflowY = initialOverflowY;
  229. }, this._dialog);
  230. }, this._dialog);
  231. this._element.focus();
  232. }
  233. /**
  234. * The following methods are used to handle overflowing modals
  235. */
  236. _adjustDialog() {
  237. const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;
  238. const scrollbarWidth = this._scrollBar.getWidth();
  239. const isBodyOverflowing = scrollbarWidth > 0;
  240. if (isBodyOverflowing && !isModalOverflowing) {
  241. const property = index_js.isRTL() ? 'paddingLeft' : 'paddingRight';
  242. this._element.style[property] = `${scrollbarWidth}px`;
  243. }
  244. if (!isBodyOverflowing && isModalOverflowing) {
  245. const property = index_js.isRTL() ? 'paddingRight' : 'paddingLeft';
  246. this._element.style[property] = `${scrollbarWidth}px`;
  247. }
  248. }
  249. _resetAdjustments() {
  250. this._element.style.paddingLeft = '';
  251. this._element.style.paddingRight = '';
  252. }
  253. // Static
  254. static jQueryInterface(config, relatedTarget) {
  255. return this.each(function () {
  256. const data = Modal.getOrCreateInstance(this, config);
  257. if (typeof config !== 'string') {
  258. return;
  259. }
  260. if (typeof data[config] === 'undefined') {
  261. throw new TypeError(`No method named "${config}"`);
  262. }
  263. data[config](relatedTarget);
  264. });
  265. }
  266. }
  267. /**
  268. * Data API implementation
  269. */
  270. EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
  271. const target = SelectorEngine.getElementFromSelector(this);
  272. if (['A', 'AREA'].includes(this.tagName)) {
  273. event.preventDefault();
  274. }
  275. EventHandler.one(target, EVENT_SHOW, showEvent => {
  276. if (showEvent.defaultPrevented) {
  277. // only register focus restorer if modal will actually get shown
  278. return;
  279. }
  280. EventHandler.one(target, EVENT_HIDDEN, () => {
  281. if (index_js.isVisible(this)) {
  282. this.focus();
  283. }
  284. });
  285. });
  286. // avoid conflict when clicking modal toggler while another one is open
  287. const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);
  288. if (alreadyOpen) {
  289. Modal.getInstance(alreadyOpen).hide();
  290. }
  291. const data = Modal.getOrCreateInstance(target);
  292. data.toggle(this);
  293. });
  294. componentFunctions_js.enableDismissTrigger(Modal);
  295. /**
  296. * jQuery
  297. */
  298. index_js.defineJQueryPlugin(Modal);
  299. return Modal;
  300. }));
  301. //# sourceMappingURL=modal.js.map