diff options
Diffstat (limited to 'vendor/twbs/bootstrap/js/src/modal.js')
-rw-r--r-- | vendor/twbs/bootstrap/js/src/modal.js | 548 |
1 files changed, 178 insertions, 370 deletions
diff --git a/vendor/twbs/bootstrap/js/src/modal.js b/vendor/twbs/bootstrap/js/src/modal.js index 2e3017024..00df5c482 100644 --- a/vendor/twbs/bootstrap/js/src/modal.js +++ b/vendor/twbs/bootstrap/js/src/modal.js @@ -1,12 +1,26 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v4.6.0): modal.js + * Bootstrap (v5.1.3): modal.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import $ from 'jquery' -import Util from './util' +import { + defineJQueryPlugin, + getElementFromSelector, + isRTL, + isVisible, + reflow, + typeCheckConfig +} from './util/index' +import EventHandler from './dom/event-handler' +import Manipulator from './dom/manipulator' +import SelectorEngine from './dom/selector-engine' +import ScrollBarHelper from './util/scrollbar' +import BaseComponent from './base-component' +import Backdrop from './util/backdrop' +import FocusTrap from './util/focustrap' +import { enableDismissTrigger } from './util/component-functions' /** * ------------------------------------------------------------------------ @@ -15,25 +29,21 @@ import Util from './util' */ const NAME = 'modal' -const VERSION = '4.6.0' const DATA_KEY = 'bs.modal' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' -const JQUERY_NO_CONFLICT = $.fn[NAME] -const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key +const ESCAPE_KEY = 'Escape' const Default = { backdrop: true, keyboard: true, - focus: true, - show: true + focus: true } const DefaultType = { backdrop: '(boolean|string)', keyboard: 'boolean', - focus: 'boolean', - show: 'boolean' + focus: 'boolean' } const EVENT_HIDE = `hide${EVENT_KEY}` @@ -41,7 +51,6 @@ const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}` const EVENT_HIDDEN = `hidden${EVENT_KEY}` const EVENT_SHOW = `show${EVENT_KEY}` const EVENT_SHOWN = `shown${EVENT_KEY}` -const EVENT_FOCUSIN = `focusin${EVENT_KEY}` const EVENT_RESIZE = `resize${EVENT_KEY}` const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}` const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}` @@ -49,20 +58,15 @@ const EVENT_MOUSEUP_DISMISS = `mouseup.dismiss${EVENT_KEY}` const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}` const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` -const CLASS_NAME_SCROLLABLE = 'modal-dialog-scrollable' -const CLASS_NAME_SCROLLBAR_MEASURER = 'modal-scrollbar-measure' -const CLASS_NAME_BACKDROP = 'modal-backdrop' const CLASS_NAME_OPEN = 'modal-open' const CLASS_NAME_FADE = 'fade' const CLASS_NAME_SHOW = 'show' const CLASS_NAME_STATIC = 'modal-static' +const OPEN_SELECTOR = '.modal.show' const SELECTOR_DIALOG = '.modal-dialog' const SELECTOR_MODAL_BODY = '.modal-body' -const SELECTOR_DATA_TOGGLE = '[data-toggle="modal"]' -const SELECTOR_DATA_DISMISS = '[data-dismiss="modal"]' -const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top' -const SELECTOR_STICKY_CONTENT = '.sticky-top' +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"]' /** * ------------------------------------------------------------------------ @@ -70,29 +74,30 @@ const SELECTOR_STICKY_CONTENT = '.sticky-top' * ------------------------------------------------------------------------ */ -class Modal { +class Modal extends BaseComponent { constructor(element, config) { + super(element) + this._config = this._getConfig(config) - this._element = element - this._dialog = element.querySelector(SELECTOR_DIALOG) - this._backdrop = null + this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element) + this._backdrop = this._initializeBackDrop() + this._focustrap = this._initializeFocusTrap() this._isShown = false - this._isBodyOverflowing = false this._ignoreBackdropClick = false this._isTransitioning = false - this._scrollbarWidth = 0 + this._scrollBar = new ScrollBarHelper() } // Getters - static get VERSION() { - return VERSION - } - static get Default() { return Default } + static get NAME() { + return NAME + } + // Public toggle(relatedTarget) { @@ -104,39 +109,32 @@ class Modal { return } - if ($(this._element).hasClass(CLASS_NAME_FADE)) { - this._isTransitioning = true - } - - const showEvent = $.Event(EVENT_SHOW, { + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget }) - $(this._element).trigger(showEvent) - - if (this._isShown || showEvent.isDefaultPrevented()) { + if (showEvent.defaultPrevented) { return } this._isShown = true - this._checkScrollbar() - this._setScrollbar() + if (this._isAnimated()) { + this._isTransitioning = true + } + + this._scrollBar.hide() + + document.body.classList.add(CLASS_NAME_OPEN) this._adjustDialog() this._setEscapeEvent() this._setResizeEvent() - $(this._element).on( - EVENT_CLICK_DISMISS, - SELECTOR_DATA_DISMISS, - event => this.hide(event) - ) - - $(this._dialog).on(EVENT_MOUSEDOWN_DISMISS, () => { - $(this._element).one(EVENT_MOUSEUP_DISMISS, event => { - if ($(event.target).is(this._element)) { + EventHandler.on(this._dialog, EVENT_MOUSEDOWN_DISMISS, () => { + EventHandler.one(this._element, EVENT_MOUSEUP_DISMISS, event => { + if (event.target === this._element) { this._ignoreBackdropClick = true } }) @@ -145,73 +143,44 @@ class Modal { this._showBackdrop(() => this._showElement(relatedTarget)) } - hide(event) { - if (event) { - event.preventDefault() - } - + hide() { if (!this._isShown || this._isTransitioning) { return } - const hideEvent = $.Event(EVENT_HIDE) - - $(this._element).trigger(hideEvent) + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE) - if (!this._isShown || hideEvent.isDefaultPrevented()) { + if (hideEvent.defaultPrevented) { return } this._isShown = false - const transition = $(this._element).hasClass(CLASS_NAME_FADE) + const isAnimated = this._isAnimated() - if (transition) { + if (isAnimated) { this._isTransitioning = true } this._setEscapeEvent() this._setResizeEvent() - $(document).off(EVENT_FOCUSIN) + this._focustrap.deactivate() - $(this._element).removeClass(CLASS_NAME_SHOW) + this._element.classList.remove(CLASS_NAME_SHOW) - $(this._element).off(EVENT_CLICK_DISMISS) - $(this._dialog).off(EVENT_MOUSEDOWN_DISMISS) + EventHandler.off(this._element, EVENT_CLICK_DISMISS) + EventHandler.off(this._dialog, EVENT_MOUSEDOWN_DISMISS) - if (transition) { - const transitionDuration = Util.getTransitionDurationFromElement(this._element) - - $(this._element) - .one(Util.TRANSITION_END, event => this._hideModal(event)) - .emulateTransitionEnd(transitionDuration) - } else { - this._hideModal() - } + this._queueCallback(() => this._hideModal(), this._element, isAnimated) } dispose() { - [window, this._element, this._dialog] - .forEach(htmlElement => $(htmlElement).off(EVENT_KEY)) - - /** - * `document` has 2 events `EVENT_FOCUSIN` and `EVENT_CLICK_DATA_API` - * Do not move `document` in `htmlElements` array - * It will remove `EVENT_CLICK_DATA_API` event that should remain - */ - $(document).off(EVENT_FOCUSIN) - - $.removeData(this._element, DATA_KEY) - - this._config = null - this._element = null - this._dialog = null - this._backdrop = null - this._isShown = null - this._isBodyOverflowing = null - this._ignoreBackdropClick = null - this._isTransitioning = null - this._scrollbarWidth = null + [window, this._dialog] + .forEach(htmlElement => EventHandler.off(htmlElement, EVENT_KEY)) + + this._backdrop.dispose() + this._focustrap.deactivate() + super.dispose() } handleUpdate() { @@ -220,134 +189,88 @@ class Modal { // Private + _initializeBackDrop() { + return new Backdrop({ + isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value + isAnimated: this._isAnimated() + }) + } + + _initializeFocusTrap() { + return new FocusTrap({ + trapElement: this._element + }) + } + _getConfig(config) { config = { ...Default, - ...config + ...Manipulator.getDataAttributes(this._element), + ...(typeof config === 'object' ? config : {}) } - Util.typeCheckConfig(NAME, config, DefaultType) + typeCheckConfig(NAME, config, DefaultType) return config } - _triggerBackdropTransition() { - const hideEventPrevented = $.Event(EVENT_HIDE_PREVENTED) - - $(this._element).trigger(hideEventPrevented) - if (hideEventPrevented.isDefaultPrevented()) { - return - } - - const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight - - if (!isModalOverflowing) { - this._element.style.overflowY = 'hidden' - } - - this._element.classList.add(CLASS_NAME_STATIC) - - const modalTransitionDuration = Util.getTransitionDurationFromElement(this._dialog) - $(this._element).off(Util.TRANSITION_END) - - $(this._element).one(Util.TRANSITION_END, () => { - this._element.classList.remove(CLASS_NAME_STATIC) - if (!isModalOverflowing) { - $(this._element).one(Util.TRANSITION_END, () => { - this._element.style.overflowY = '' - }) - .emulateTransitionEnd(this._element, modalTransitionDuration) - } - }) - .emulateTransitionEnd(modalTransitionDuration) - this._element.focus() - } - _showElement(relatedTarget) { - const transition = $(this._element).hasClass(CLASS_NAME_FADE) - const modalBody = this._dialog ? this._dialog.querySelector(SELECTOR_MODAL_BODY) : null + const isAnimated = this._isAnimated() + const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog) - if (!this._element.parentNode || - this._element.parentNode.nodeType !== Node.ELEMENT_NODE) { + if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) { // Don't move modal's DOM position - document.body.appendChild(this._element) + document.body.append(this._element) } this._element.style.display = 'block' this._element.removeAttribute('aria-hidden') this._element.setAttribute('aria-modal', true) this._element.setAttribute('role', 'dialog') + this._element.scrollTop = 0 - if ($(this._dialog).hasClass(CLASS_NAME_SCROLLABLE) && modalBody) { + if (modalBody) { modalBody.scrollTop = 0 - } else { - this._element.scrollTop = 0 } - if (transition) { - Util.reflow(this._element) + if (isAnimated) { + reflow(this._element) } - $(this._element).addClass(CLASS_NAME_SHOW) - - if (this._config.focus) { - this._enforceFocus() - } - - const shownEvent = $.Event(EVENT_SHOWN, { - relatedTarget - }) + this._element.classList.add(CLASS_NAME_SHOW) const transitionComplete = () => { if (this._config.focus) { - this._element.focus() + this._focustrap.activate() } this._isTransitioning = false - $(this._element).trigger(shownEvent) - } - - if (transition) { - const transitionDuration = Util.getTransitionDurationFromElement(this._dialog) - - $(this._dialog) - .one(Util.TRANSITION_END, transitionComplete) - .emulateTransitionEnd(transitionDuration) - } else { - transitionComplete() + EventHandler.trigger(this._element, EVENT_SHOWN, { + relatedTarget + }) } - } - _enforceFocus() { - $(document) - .off(EVENT_FOCUSIN) // Guard against infinite focus loop - .on(EVENT_FOCUSIN, event => { - if (document !== event.target && - this._element !== event.target && - $(this._element).has(event.target).length === 0) { - this._element.focus() - } - }) + this._queueCallback(transitionComplete, this._dialog, isAnimated) } _setEscapeEvent() { if (this._isShown) { - $(this._element).on(EVENT_KEYDOWN_DISMISS, event => { - if (this._config.keyboard && event.which === ESCAPE_KEYCODE) { + EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => { + if (this._config.keyboard && event.key === ESCAPE_KEY) { event.preventDefault() this.hide() - } else if (!this._config.keyboard && event.which === ESCAPE_KEYCODE) { + } else if (!this._config.keyboard && event.key === ESCAPE_KEY) { this._triggerBackdropTransition() } }) - } else if (!this._isShown) { - $(this._element).off(EVENT_KEYDOWN_DISMISS) + } else { + EventHandler.off(this._element, EVENT_KEYDOWN_DISMISS) } } _setResizeEvent() { if (this._isShown) { - $(window).on(EVENT_RESIZE, event => this.handleUpdate(event)) + EventHandler.on(window, EVENT_RESIZE, () => this._adjustDialog()) } else { - $(window).off(EVENT_RESIZE) + EventHandler.off(window, EVENT_RESIZE) } } @@ -357,110 +280,85 @@ class Modal { this._element.removeAttribute('aria-modal') this._element.removeAttribute('role') this._isTransitioning = false - this._showBackdrop(() => { - $(document.body).removeClass(CLASS_NAME_OPEN) + this._backdrop.hide(() => { + document.body.classList.remove(CLASS_NAME_OPEN) this._resetAdjustments() - this._resetScrollbar() - $(this._element).trigger(EVENT_HIDDEN) + this._scrollBar.reset() + EventHandler.trigger(this._element, EVENT_HIDDEN) }) } - _removeBackdrop() { - if (this._backdrop) { - $(this._backdrop).remove() - this._backdrop = null - } - } - _showBackdrop(callback) { - const animate = $(this._element).hasClass(CLASS_NAME_FADE) ? - CLASS_NAME_FADE : '' - - if (this._isShown && this._config.backdrop) { - this._backdrop = document.createElement('div') - this._backdrop.className = CLASS_NAME_BACKDROP - - if (animate) { - this._backdrop.classList.add(animate) - } - - $(this._backdrop).appendTo(document.body) - - $(this._element).on(EVENT_CLICK_DISMISS, event => { - if (this._ignoreBackdropClick) { - this._ignoreBackdropClick = false - return - } - - if (event.target !== event.currentTarget) { - return - } - - if (this._config.backdrop === 'static') { - this._triggerBackdropTransition() - } else { - this.hide() - } - }) - - if (animate) { - Util.reflow(this._backdrop) + EventHandler.on(this._element, EVENT_CLICK_DISMISS, event => { + if (this._ignoreBackdropClick) { + this._ignoreBackdropClick = false + return } - $(this._backdrop).addClass(CLASS_NAME_SHOW) - - if (!callback) { + if (event.target !== event.currentTarget) { return } - if (!animate) { - callback() - return + if (this._config.backdrop === true) { + this.hide() + } else if (this._config.backdrop === 'static') { + this._triggerBackdropTransition() } + }) - const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop) + this._backdrop.show(callback) + } - $(this._backdrop) - .one(Util.TRANSITION_END, callback) - .emulateTransitionEnd(backdropTransitionDuration) - } else if (!this._isShown && this._backdrop) { - $(this._backdrop).removeClass(CLASS_NAME_SHOW) + _isAnimated() { + return this._element.classList.contains(CLASS_NAME_FADE) + } - const callbackRemove = () => { - this._removeBackdrop() - if (callback) { - callback() - } - } + _triggerBackdropTransition() { + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED) + if (hideEvent.defaultPrevented) { + return + } - if ($(this._element).hasClass(CLASS_NAME_FADE)) { - const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop) + const { classList, scrollHeight, style } = this._element + const isModalOverflowing = scrollHeight > document.documentElement.clientHeight - $(this._backdrop) - .one(Util.TRANSITION_END, callbackRemove) - .emulateTransitionEnd(backdropTransitionDuration) - } else { - callbackRemove() - } - } else if (callback) { - callback() + // return if the following background transition hasn't yet completed + if ((!isModalOverflowing && style.overflowY === 'hidden') || classList.contains(CLASS_NAME_STATIC)) { + return + } + + if (!isModalOverflowing) { + style.overflowY = 'hidden' } + + classList.add(CLASS_NAME_STATIC) + this._queueCallback(() => { + classList.remove(CLASS_NAME_STATIC) + if (!isModalOverflowing) { + this._queueCallback(() => { + style.overflowY = '' + }, this._dialog) + } + }, this._dialog) + + this._element.focus() } // ---------------------------------------------------------------------- // the following methods are used to handle overflowing modals - // todo (fat): these should probably be refactored out of modal.js // ---------------------------------------------------------------------- _adjustDialog() { const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight + const scrollbarWidth = this._scrollBar.getWidth() + const isBodyOverflowing = scrollbarWidth > 0 - if (!this._isBodyOverflowing && isModalOverflowing) { - this._element.style.paddingLeft = `${this._scrollbarWidth}px` + if ((!isBodyOverflowing && isModalOverflowing && !isRTL()) || (isBodyOverflowing && !isModalOverflowing && isRTL())) { + this._element.style.paddingLeft = `${scrollbarWidth}px` } - if (this._isBodyOverflowing && !isModalOverflowing) { - this._element.style.paddingRight = `${this._scrollbarWidth}px` + if ((isBodyOverflowing && !isModalOverflowing && !isRTL()) || (!isBodyOverflowing && isModalOverflowing && isRTL())) { + this._element.style.paddingRight = `${scrollbarWidth}px` } } @@ -469,106 +367,21 @@ class Modal { this._element.style.paddingRight = '' } - _checkScrollbar() { - const rect = document.body.getBoundingClientRect() - this._isBodyOverflowing = Math.round(rect.left + rect.right) < window.innerWidth - this._scrollbarWidth = this._getScrollbarWidth() - } - - _setScrollbar() { - if (this._isBodyOverflowing) { - // Note: DOMNode.style.paddingRight returns the actual value or '' if not set - // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set - const fixedContent = [].slice.call(document.querySelectorAll(SELECTOR_FIXED_CONTENT)) - const stickyContent = [].slice.call(document.querySelectorAll(SELECTOR_STICKY_CONTENT)) - - // Adjust fixed content padding - $(fixedContent).each((index, element) => { - const actualPadding = element.style.paddingRight - const calculatedPadding = $(element).css('padding-right') - $(element) - .data('padding-right', actualPadding) - .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`) - }) - - // Adjust sticky content margin - $(stickyContent).each((index, element) => { - const actualMargin = element.style.marginRight - const calculatedMargin = $(element).css('margin-right') - $(element) - .data('margin-right', actualMargin) - .css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`) - }) - - // Adjust body padding - const actualPadding = document.body.style.paddingRight - const calculatedPadding = $(document.body).css('padding-right') - $(document.body) - .data('padding-right', actualPadding) - .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`) - } - - $(document.body).addClass(CLASS_NAME_OPEN) - } - - _resetScrollbar() { - // Restore fixed content padding - const fixedContent = [].slice.call(document.querySelectorAll(SELECTOR_FIXED_CONTENT)) - $(fixedContent).each((index, element) => { - const padding = $(element).data('padding-right') - $(element).removeData('padding-right') - element.style.paddingRight = padding ? padding : '' - }) - - // Restore sticky content - const elements = [].slice.call(document.querySelectorAll(`${SELECTOR_STICKY_CONTENT}`)) - $(elements).each((index, element) => { - const margin = $(element).data('margin-right') - if (typeof margin !== 'undefined') { - $(element).css('margin-right', margin).removeData('margin-right') - } - }) - - // Restore body padding - const padding = $(document.body).data('padding-right') - $(document.body).removeData('padding-right') - document.body.style.paddingRight = padding ? padding : '' - } - - _getScrollbarWidth() { // thx d.walsh - const scrollDiv = document.createElement('div') - scrollDiv.className = CLASS_NAME_SCROLLBAR_MEASURER - document.body.appendChild(scrollDiv) - const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth - document.body.removeChild(scrollDiv) - return scrollbarWidth - } - // Static - static _jQueryInterface(config, relatedTarget) { + static jQueryInterface(config, relatedTarget) { return this.each(function () { - let data = $(this).data(DATA_KEY) - const _config = { - ...Default, - ...$(this).data(), - ...(typeof config === 'object' && config ? config : {}) - } + const data = Modal.getOrCreateInstance(this, config) - if (!data) { - data = new Modal(this, _config) - $(this).data(DATA_KEY, data) + if (typeof config !== 'string') { + return } - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`) - } - - data[config](relatedTarget) - } else if (_config.show) { - data.show(relatedTarget) + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`) } + + data[config](relatedTarget) }) } } @@ -579,51 +392,46 @@ class Modal { * ------------------------------------------------------------------------ */ -$(document).on(EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - let target - const selector = Util.getSelectorFromElement(this) - - if (selector) { - target = document.querySelector(selector) - } - - const config = $(target).data(DATA_KEY) ? - 'toggle' : { - ...$(target).data(), - ...$(this).data() - } +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + const target = getElementFromSelector(this) - if (this.tagName === 'A' || this.tagName === 'AREA') { + if (['A', 'AREA'].includes(this.tagName)) { event.preventDefault() } - const $target = $(target).one(EVENT_SHOW, showEvent => { - if (showEvent.isDefaultPrevented()) { - // Only register focus restorer if modal will actually get shown + EventHandler.one(target, EVENT_SHOW, showEvent => { + if (showEvent.defaultPrevented) { + // only register focus restorer if modal will actually get shown return } - $target.one(EVENT_HIDDEN, () => { - if ($(this).is(':visible')) { + EventHandler.one(target, EVENT_HIDDEN, () => { + if (isVisible(this)) { this.focus() } }) }) - Modal._jQueryInterface.call($(target), config, this) + // avoid conflict when clicking moddal toggler while another one is open + const allReadyOpen = SelectorEngine.findOne(OPEN_SELECTOR) + if (allReadyOpen) { + Modal.getInstance(allReadyOpen).hide() + } + + const data = Modal.getOrCreateInstance(target) + + data.toggle(this) }) +enableDismissTrigger(Modal) + /** * ------------------------------------------------------------------------ * jQuery * ------------------------------------------------------------------------ + * add .Modal to jQuery only if jQuery is present */ -$.fn[NAME] = Modal._jQueryInterface -$.fn[NAME].Constructor = Modal -$.fn[NAME].noConflict = () => { - $.fn[NAME] = JQUERY_NO_CONFLICT - return Modal._jQueryInterface -} +defineJQueryPlugin(Modal) export default Modal |