mirror of
https://github.com/tenrok/bootstrap.git
synced 2026-05-15 11:59:39 +03:00
cbaf7a7b79
_adjustDialog should be called when the modal is first displayed to prevent it jumping position when the viewport is resized
589 lines
16 KiB
JavaScript
589 lines
16 KiB
JavaScript
import Util from './util'
|
|
|
|
|
|
/**
|
|
* --------------------------------------------------------------------------
|
|
* Bootstrap (v4.0.0-beta): modal.js
|
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
|
* --------------------------------------------------------------------------
|
|
*/
|
|
|
|
const Modal = (($) => {
|
|
|
|
|
|
/**
|
|
* ------------------------------------------------------------------------
|
|
* Constants
|
|
* ------------------------------------------------------------------------
|
|
*/
|
|
|
|
const NAME = 'modal'
|
|
const VERSION = '4.0.0-beta'
|
|
const DATA_KEY = 'bs.modal'
|
|
const EVENT_KEY = `.${DATA_KEY}`
|
|
const DATA_API_KEY = '.data-api'
|
|
const JQUERY_NO_CONFLICT = $.fn[NAME]
|
|
const TRANSITION_DURATION = 300
|
|
const BACKDROP_TRANSITION_DURATION = 150
|
|
const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key
|
|
|
|
const Default = {
|
|
backdrop : true,
|
|
keyboard : true,
|
|
focus : true,
|
|
show : true
|
|
}
|
|
|
|
const DefaultType = {
|
|
backdrop : '(boolean|string)',
|
|
keyboard : 'boolean',
|
|
focus : 'boolean',
|
|
show : 'boolean'
|
|
}
|
|
|
|
const Event = {
|
|
HIDE : `hide${EVENT_KEY}`,
|
|
HIDDEN : `hidden${EVENT_KEY}`,
|
|
SHOW : `show${EVENT_KEY}`,
|
|
SHOWN : `shown${EVENT_KEY}`,
|
|
FOCUSIN : `focusin${EVENT_KEY}`,
|
|
RESIZE : `resize${EVENT_KEY}`,
|
|
CLICK_DISMISS : `click.dismiss${EVENT_KEY}`,
|
|
KEYDOWN_DISMISS : `keydown.dismiss${EVENT_KEY}`,
|
|
MOUSEUP_DISMISS : `mouseup.dismiss${EVENT_KEY}`,
|
|
MOUSEDOWN_DISMISS : `mousedown.dismiss${EVENT_KEY}`,
|
|
CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`
|
|
}
|
|
|
|
const ClassName = {
|
|
SCROLLBAR_MEASURER : 'modal-scrollbar-measure',
|
|
BACKDROP : 'modal-backdrop',
|
|
OPEN : 'modal-open',
|
|
FADE : 'fade',
|
|
SHOW : 'show'
|
|
}
|
|
|
|
const Selector = {
|
|
DIALOG : '.modal-dialog',
|
|
DATA_TOGGLE : '[data-toggle="modal"]',
|
|
DATA_DISMISS : '[data-dismiss="modal"]',
|
|
FIXED_CONTENT : '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',
|
|
STICKY_CONTENT : '.sticky-top',
|
|
NAVBAR_TOGGLER : '.navbar-toggler'
|
|
}
|
|
|
|
|
|
/**
|
|
* ------------------------------------------------------------------------
|
|
* Class Definition
|
|
* ------------------------------------------------------------------------
|
|
*/
|
|
|
|
class Modal {
|
|
|
|
constructor(element, config) {
|
|
this._config = this._getConfig(config)
|
|
this._element = element
|
|
this._dialog = $(element).find(Selector.DIALOG)[0]
|
|
this._backdrop = null
|
|
this._isShown = false
|
|
this._isBodyOverflowing = false
|
|
this._ignoreBackdropClick = false
|
|
this._originalBodyPadding = 0
|
|
this._scrollbarWidth = 0
|
|
}
|
|
|
|
|
|
// getters
|
|
|
|
static get VERSION() {
|
|
return VERSION
|
|
}
|
|
|
|
static get Default() {
|
|
return Default
|
|
}
|
|
|
|
|
|
// public
|
|
|
|
toggle(relatedTarget) {
|
|
return this._isShown ? this.hide() : this.show(relatedTarget)
|
|
}
|
|
|
|
show(relatedTarget) {
|
|
if (this._isTransitioning) {
|
|
return
|
|
}
|
|
|
|
if (Util.supportsTransitionEnd() && $(this._element).hasClass(ClassName.FADE)) {
|
|
this._isTransitioning = true
|
|
}
|
|
|
|
const showEvent = $.Event(Event.SHOW, {
|
|
relatedTarget
|
|
})
|
|
|
|
$(this._element).trigger(showEvent)
|
|
|
|
if (this._isShown || showEvent.isDefaultPrevented()) {
|
|
return
|
|
}
|
|
|
|
this._isShown = true
|
|
|
|
this._checkScrollbar()
|
|
this._setScrollbar()
|
|
|
|
this._adjustDialog()
|
|
|
|
$(document.body).addClass(ClassName.OPEN)
|
|
|
|
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)) {
|
|
this._ignoreBackdropClick = true
|
|
}
|
|
})
|
|
})
|
|
|
|
this._showBackdrop(() => this._showElement(relatedTarget))
|
|
}
|
|
|
|
hide(event) {
|
|
if (event) {
|
|
event.preventDefault()
|
|
}
|
|
|
|
if (this._isTransitioning || !this._isShown) {
|
|
return
|
|
}
|
|
|
|
const transition = Util.supportsTransitionEnd() && $(this._element).hasClass(ClassName.FADE)
|
|
|
|
if (transition) {
|
|
this._isTransitioning = true
|
|
}
|
|
|
|
const hideEvent = $.Event(Event.HIDE)
|
|
|
|
$(this._element).trigger(hideEvent)
|
|
|
|
if (!this._isShown || hideEvent.isDefaultPrevented()) {
|
|
return
|
|
}
|
|
|
|
this._isShown = false
|
|
|
|
this._setEscapeEvent()
|
|
this._setResizeEvent()
|
|
|
|
$(document).off(Event.FOCUSIN)
|
|
|
|
$(this._element).removeClass(ClassName.SHOW)
|
|
|
|
$(this._element).off(Event.CLICK_DISMISS)
|
|
$(this._dialog).off(Event.MOUSEDOWN_DISMISS)
|
|
|
|
if (transition) {
|
|
|
|
$(this._element)
|
|
.one(Util.TRANSITION_END, (event) => this._hideModal(event))
|
|
.emulateTransitionEnd(TRANSITION_DURATION)
|
|
} else {
|
|
this._hideModal()
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
$.removeData(this._element, DATA_KEY)
|
|
|
|
$(window, document, this._element, this._backdrop).off(EVENT_KEY)
|
|
|
|
this._config = null
|
|
this._element = null
|
|
this._dialog = null
|
|
this._backdrop = null
|
|
this._isShown = null
|
|
this._isBodyOverflowing = null
|
|
this._ignoreBackdropClick = null
|
|
this._scrollbarWidth = null
|
|
}
|
|
|
|
handleUpdate() {
|
|
this._adjustDialog()
|
|
}
|
|
|
|
// private
|
|
|
|
_getConfig(config) {
|
|
config = $.extend({}, Default, config)
|
|
Util.typeCheckConfig(NAME, config, DefaultType)
|
|
return config
|
|
}
|
|
|
|
_showElement(relatedTarget) {
|
|
const transition = Util.supportsTransitionEnd() &&
|
|
$(this._element).hasClass(ClassName.FADE)
|
|
|
|
if (!this._element.parentNode ||
|
|
this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
|
|
// don't move modals dom position
|
|
document.body.appendChild(this._element)
|
|
}
|
|
|
|
this._element.style.display = 'block'
|
|
this._element.removeAttribute('aria-hidden')
|
|
this._element.scrollTop = 0
|
|
|
|
if (transition) {
|
|
Util.reflow(this._element)
|
|
}
|
|
|
|
$(this._element).addClass(ClassName.SHOW)
|
|
|
|
if (this._config.focus) {
|
|
this._enforceFocus()
|
|
}
|
|
|
|
const shownEvent = $.Event(Event.SHOWN, {
|
|
relatedTarget
|
|
})
|
|
|
|
const transitionComplete = () => {
|
|
if (this._config.focus) {
|
|
this._element.focus()
|
|
}
|
|
this._isTransitioning = false
|
|
$(this._element).trigger(shownEvent)
|
|
}
|
|
|
|
if (transition) {
|
|
$(this._dialog)
|
|
.one(Util.TRANSITION_END, transitionComplete)
|
|
.emulateTransitionEnd(TRANSITION_DURATION)
|
|
} else {
|
|
transitionComplete()
|
|
}
|
|
}
|
|
|
|
_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) {
|
|
this._element.focus()
|
|
}
|
|
})
|
|
}
|
|
|
|
_setEscapeEvent() {
|
|
if (this._isShown && this._config.keyboard) {
|
|
$(this._element).on(Event.KEYDOWN_DISMISS, (event) => {
|
|
if (event.which === ESCAPE_KEYCODE) {
|
|
event.preventDefault()
|
|
this.hide()
|
|
}
|
|
})
|
|
|
|
} else if (!this._isShown) {
|
|
$(this._element).off(Event.KEYDOWN_DISMISS)
|
|
}
|
|
}
|
|
|
|
_setResizeEvent() {
|
|
if (this._isShown) {
|
|
$(window).on(Event.RESIZE, (event) => this.handleUpdate(event))
|
|
} else {
|
|
$(window).off(Event.RESIZE)
|
|
}
|
|
}
|
|
|
|
_hideModal() {
|
|
this._element.style.display = 'none'
|
|
this._element.setAttribute('aria-hidden', true)
|
|
this._isTransitioning = false
|
|
this._showBackdrop(() => {
|
|
$(document.body).removeClass(ClassName.OPEN)
|
|
this._resetAdjustments()
|
|
this._resetScrollbar()
|
|
$(this._element).trigger(Event.HIDDEN)
|
|
})
|
|
}
|
|
|
|
_removeBackdrop() {
|
|
if (this._backdrop) {
|
|
$(this._backdrop).remove()
|
|
this._backdrop = null
|
|
}
|
|
}
|
|
|
|
_showBackdrop(callback) {
|
|
const animate = $(this._element).hasClass(ClassName.FADE) ?
|
|
ClassName.FADE : ''
|
|
|
|
if (this._isShown && this._config.backdrop) {
|
|
const doAnimate = Util.supportsTransitionEnd() && animate
|
|
|
|
this._backdrop = document.createElement('div')
|
|
this._backdrop.className = ClassName.BACKDROP
|
|
|
|
if (animate) {
|
|
$(this._backdrop).addClass(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._element.focus()
|
|
} else {
|
|
this.hide()
|
|
}
|
|
})
|
|
|
|
if (doAnimate) {
|
|
Util.reflow(this._backdrop)
|
|
}
|
|
|
|
$(this._backdrop).addClass(ClassName.SHOW)
|
|
|
|
if (!callback) {
|
|
return
|
|
}
|
|
|
|
if (!doAnimate) {
|
|
callback()
|
|
return
|
|
}
|
|
|
|
$(this._backdrop)
|
|
.one(Util.TRANSITION_END, callback)
|
|
.emulateTransitionEnd(BACKDROP_TRANSITION_DURATION)
|
|
|
|
} else if (!this._isShown && this._backdrop) {
|
|
$(this._backdrop).removeClass(ClassName.SHOW)
|
|
|
|
const callbackRemove = () => {
|
|
this._removeBackdrop()
|
|
if (callback) {
|
|
callback()
|
|
}
|
|
}
|
|
|
|
if (Util.supportsTransitionEnd() &&
|
|
$(this._element).hasClass(ClassName.FADE)) {
|
|
$(this._backdrop)
|
|
.one(Util.TRANSITION_END, callbackRemove)
|
|
.emulateTransitionEnd(BACKDROP_TRANSITION_DURATION)
|
|
} else {
|
|
callbackRemove()
|
|
}
|
|
|
|
} else if (callback) {
|
|
callback()
|
|
}
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 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
|
|
|
|
if (!this._isBodyOverflowing && isModalOverflowing) {
|
|
this._element.style.paddingLeft = `${this._scrollbarWidth}px`
|
|
}
|
|
|
|
if (this._isBodyOverflowing && !isModalOverflowing) {
|
|
this._element.style.paddingRight = `${this._scrollbarWidth}px`
|
|
}
|
|
}
|
|
|
|
_resetAdjustments() {
|
|
this._element.style.paddingLeft = ''
|
|
this._element.style.paddingRight = ''
|
|
}
|
|
|
|
_checkScrollbar() {
|
|
this._isBodyOverflowing = document.body.clientWidth < 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
|
|
|
|
// Adjust fixed content padding
|
|
$(Selector.FIXED_CONTENT).each((index, element) => {
|
|
const actualPadding = $(element)[0].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
|
|
$(Selector.STICKY_CONTENT).each((index, element) => {
|
|
const actualMargin = $(element)[0].style.marginRight
|
|
const calculatedMargin = $(element).css('margin-right')
|
|
$(element).data('margin-right', actualMargin).css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)
|
|
})
|
|
|
|
// Adjust navbar-toggler margin
|
|
$(Selector.NAVBAR_TOGGLER).each((index, element) => {
|
|
const actualMargin = $(element)[0].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 = $('body').css('padding-right')
|
|
$('body').data('padding-right', actualPadding).css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
|
|
}
|
|
}
|
|
|
|
_resetScrollbar() {
|
|
// Restore fixed content padding
|
|
$(Selector.FIXED_CONTENT).each((index, element) => {
|
|
const padding = $(element).data('padding-right')
|
|
if (typeof padding !== 'undefined') {
|
|
$(element).css('padding-right', padding).removeData('padding-right')
|
|
}
|
|
})
|
|
|
|
// Restore sticky content and navbar-toggler margin
|
|
$(`${Selector.STICKY_CONTENT}, ${Selector.NAVBAR_TOGGLER}`).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 = $('body').data('padding-right')
|
|
if (typeof padding !== 'undefined') {
|
|
$('body').css('padding-right', padding).removeData('padding-right')
|
|
}
|
|
}
|
|
|
|
_getScrollbarWidth() { // thx d.walsh
|
|
const scrollDiv = document.createElement('div')
|
|
scrollDiv.className = ClassName.SCROLLBAR_MEASURER
|
|
document.body.appendChild(scrollDiv)
|
|
const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth
|
|
document.body.removeChild(scrollDiv)
|
|
return scrollbarWidth
|
|
}
|
|
|
|
|
|
// static
|
|
|
|
static _jQueryInterface(config, relatedTarget) {
|
|
return this.each(function () {
|
|
let data = $(this).data(DATA_KEY)
|
|
const _config = $.extend(
|
|
{},
|
|
Modal.Default,
|
|
$(this).data(),
|
|
typeof config === 'object' && config
|
|
)
|
|
|
|
if (!data) {
|
|
data = new Modal(this, _config)
|
|
$(this).data(DATA_KEY, data)
|
|
}
|
|
|
|
if (typeof config === 'string') {
|
|
if (typeof data[config] === 'undefined') {
|
|
throw new Error(`No method named "${config}"`)
|
|
}
|
|
data[config](relatedTarget)
|
|
} else if (_config.show) {
|
|
data.show(relatedTarget)
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* ------------------------------------------------------------------------
|
|
* Data Api implementation
|
|
* ------------------------------------------------------------------------
|
|
*/
|
|
|
|
$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
|
|
let target
|
|
const selector = Util.getSelectorFromElement(this)
|
|
|
|
if (selector) {
|
|
target = $(selector)[0]
|
|
}
|
|
|
|
const config = $(target).data(DATA_KEY) ?
|
|
'toggle' : $.extend({}, $(target).data(), $(this).data())
|
|
|
|
if (this.tagName === 'A' || this.tagName === 'AREA') {
|
|
event.preventDefault()
|
|
}
|
|
|
|
const $target = $(target).one(Event.SHOW, (showEvent) => {
|
|
if (showEvent.isDefaultPrevented()) {
|
|
// only register focus restorer if modal will actually get shown
|
|
return
|
|
}
|
|
|
|
$target.one(Event.HIDDEN, () => {
|
|
if ($(this).is(':visible')) {
|
|
this.focus()
|
|
}
|
|
})
|
|
})
|
|
|
|
Modal._jQueryInterface.call($(target), config, this)
|
|
})
|
|
|
|
|
|
/**
|
|
* ------------------------------------------------------------------------
|
|
* jQuery
|
|
* ------------------------------------------------------------------------
|
|
*/
|
|
|
|
$.fn[NAME] = Modal._jQueryInterface
|
|
$.fn[NAME].Constructor = Modal
|
|
$.fn[NAME].noConflict = function () {
|
|
$.fn[NAME] = JQUERY_NO_CONFLICT
|
|
return Modal._jQueryInterface
|
|
}
|
|
|
|
return Modal
|
|
|
|
})(jQuery)
|
|
|
|
export default Modal
|