0
0
mirror of https://github.com/twbs/bootstrap.git synced 2025-01-24 16:52:19 +01:00
Bootstrap/js/src/modal.js

535 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Util from './util'
/**
* --------------------------------------------------------------------------
* Bootstrap (v4.0.0): modal.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* --------------------------------------------------------------------------
*/
const Modal = (($) => {
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'modal'
const VERSION = '4.0.0'
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 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',
IN : 'in'
}
const Selector = {
DIALOG : '.modal-dialog',
DATA_TOGGLE : '[data-toggle="modal"]',
DATA_DISMISS : '[data-dismiss="modal"]',
FIXED_CONTENT : '.navbar-fixed-top, .navbar-fixed-bottom, .is-fixed'
}
/**
* ------------------------------------------------------------------------
* 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) {
let showEvent = $.Event(Event.SHOW, {
relatedTarget
})
$(this._element).trigger(showEvent)
if (this._isShown || showEvent.isDefaultPrevented()) {
return
}
this._isShown = true
this._checkScrollbar()
this._setScrollbar()
$(document.body).addClass(ClassName.OPEN)
this._setEscapeEvent()
this._setResizeEvent()
$(this._element).on(
Event.CLICK_DISMISS,
Selector.DATA_DISMISS,
$.proxy(this.hide, this)
)
$(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => {
$(this._element).one(Event.MOUSEUP_DISMISS, (event) => {
if ($(event.target).is(this._element)) {
that._ignoreBackdropClick = true
}
})
})
this._showBackdrop(
$.proxy(this._showElement, this, relatedTarget)
)
}
hide(event) {
if (event) {
event.preventDefault()
}
let 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.IN)
$(this._element).off(Event.CLICK_DISMISS)
$(this._dialog).off(Event.MOUSEDOWN_DISMISS)
if (Util.supportsTransitionEnd() &&
($(this._element).hasClass(ClassName.FADE))) {
$(this._element)
.one(Util.TRANSITION_END, $.proxy(this._hideModal, this))
.emulateTransitionEnd(TRANSITION_DURATION)
} else {
this._hideModal()
}
}
dispose() {
$.removeData(this._element, DATA_KEY)
$(window).off(EVENT_KEY)
$(document).off(EVENT_KEY)
$(this._element).off(EVENT_KEY)
$(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._originalBodyPadding = null
this._scrollbarWidth = null
}
// private
_getConfig(config) {
config = $.extend({}, Default, config)
Util.typeCheckConfig(NAME, config, DefaultType)
return config
}
_showElement(relatedTarget) {
let 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.scrollTop = 0
if (transition) {
Util.reflow(this._element)
}
$(this._element).addClass(ClassName.IN)
if (this._config.focus) {
this._enforceFocus()
}
let shownEvent = $.Event(Event.SHOWN, {
relatedTarget
})
let transitionComplete = () => {
if (this._config.focus) {
this._element.focus()
}
$(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 (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 === 27) {
this.hide()
}
})
} else if (!this._isShown) {
$(this._element).off(Event.KEYDOWN_DISMISS)
}
}
_setResizeEvent() {
if (this._isShown) {
$(window).on(Event.RESIZE, $.proxy(this._handleUpdate, this))
} else {
$(window).off(Event.RESIZE)
}
}
_hideModal() {
this._element.style.display = 'none'
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) {
let animate = $(this._element).hasClass(ClassName.FADE) ?
ClassName.FADE : ''
if (this._isShown && this._config.backdrop) {
let 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.IN)
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.IN)
let 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
// ----------------------------------------------------------------------
_handleUpdate() {
this._adjustDialog()
}
_adjustDialog() {
let 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() {
let fullWindowWidth = window.innerWidth
if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8
let documentElementRect = document.documentElement.getBoundingClientRect()
fullWindowWidth =
documentElementRect.right - Math.abs(documentElementRect.left)
}
this._isBodyOverflowing = document.body.clientWidth < fullWindowWidth
this._scrollbarWidth = this._getScrollbarWidth()
}
_setScrollbar() {
let bodyPadding = parseInt(
$(Selector.FIXED_CONTENT).css('padding-right') || 0,
10
)
this._originalBodyPadding = document.body.style.paddingRight || ''
if (this._isBodyOverflowing) {
document.body.style.paddingRight =
`${bodyPadding + this._scrollbarWidth}px`
}
}
_resetScrollbar() {
document.body.style.paddingRight = this._originalBodyPadding
}
_getScrollbarWidth() { // thx d.walsh
let scrollDiv = document.createElement('div')
scrollDiv.className = ClassName.SCROLLBAR_MEASURER
document.body.appendChild(scrollDiv)
let scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth
document.body.removeChild(scrollDiv)
return scrollbarWidth
}
// static
static _jQueryInterface(config, relatedTarget) {
return this.each(function () {
let data = $(this).data(DATA_KEY)
let _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 (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
let selector = Util.getSelectorFromElement(this)
if (selector) {
target = $(selector)[0]
}
let config = $(target).data(DATA_KEY) ?
'toggle' : $.extend({}, $(target).data(), $(this).data())
if (this.tagName === 'A') {
event.preventDefault()
}
let $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