mirror of
https://github.com/twbs/bootstrap.git
synced 2022-11-09 12:25:43 -05:00
598 lines
13 KiB
JavaScript
598 lines
13 KiB
JavaScript
/** =======================================================================
|
|
* Bootstrap: modal.js v4.0.0
|
|
* http://getbootstrap.com/javascript/#modal
|
|
* ========================================================================
|
|
* Copyright 2011-2015 Twitter, Inc.
|
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
|
* ========================================================================
|
|
* @fileoverview - Bootstrap's modal plugin. Modals are streamlined, but
|
|
* flexible, dialog prompts with the minimum required functionality and
|
|
* smart defaults.
|
|
*
|
|
* Public Methods & Properties:
|
|
*
|
|
* + $.modal
|
|
* + $.modal.noConflict
|
|
* + $.modal.Constructor
|
|
* + $.modal.Constructor.VERSION
|
|
* + $.modal.Constructor.Defaults
|
|
* + $.modal.Constructor.Defaults.backdrop
|
|
* + $.modal.Constructor.Defaults.keyboard
|
|
* + $.modal.Constructor.Defaults.show
|
|
* + $.modal.Constructor.prototype.toggle
|
|
* + $.modal.Constructor.prototype.show
|
|
* + $.modal.Constructor.prototype.hide
|
|
*
|
|
* ========================================================================
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
|
|
/**
|
|
* Our modal class.
|
|
* @param {Element} element
|
|
* @param {Object} config
|
|
* @constructor
|
|
*/
|
|
var Modal = function (element, config) {
|
|
|
|
/** @private {Object} */
|
|
this._config = config
|
|
|
|
/** @private {Element} */
|
|
this._element = element
|
|
|
|
/** @private {Element} */
|
|
this._backdrop = null
|
|
|
|
/** @private {boolean} */
|
|
this._isShown = false
|
|
|
|
/** @private {boolean} */
|
|
this._isBodyOverflowing = false
|
|
|
|
/** @private {number} */
|
|
this._scrollbarWidth = 0
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* @const
|
|
* @type {string}
|
|
*/
|
|
Modal['VERSION'] = '4.0.0'
|
|
|
|
|
|
/**
|
|
* @const
|
|
* @type {Object}
|
|
*/
|
|
Modal['Defaults'] = {
|
|
'backdrop' : true,
|
|
'keyboard' : true,
|
|
'show' : true
|
|
}
|
|
|
|
|
|
/**
|
|
* @const
|
|
* @type {string}
|
|
* @private
|
|
*/
|
|
Modal._NAME = 'modal'
|
|
|
|
|
|
/**
|
|
* @const
|
|
* @type {string}
|
|
* @private
|
|
*/
|
|
Modal._DATA_KEY = 'bs.modal'
|
|
|
|
|
|
/**
|
|
* @const
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
Modal._TRANSITION_DURATION = 300
|
|
|
|
|
|
/**
|
|
* @const
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
Modal._BACKDROP_TRANSITION_DURATION = 150
|
|
|
|
|
|
/**
|
|
* @const
|
|
* @type {Function}
|
|
* @private
|
|
*/
|
|
Modal._JQUERY_NO_CONFLICT = $.fn[Modal._NAME]
|
|
|
|
|
|
/**
|
|
* @const
|
|
* @enum {string}
|
|
* @private
|
|
*/
|
|
Modal._Event = {
|
|
HIDE : 'hide.bs.modal',
|
|
HIDDEN : 'hidden.bs.modal',
|
|
SHOW : 'show.bs.modal',
|
|
SHOWN : 'shown.bs.modal'
|
|
}
|
|
|
|
|
|
/**
|
|
* @const
|
|
* @enum {string}
|
|
* @private
|
|
*/
|
|
Modal._ClassName = {
|
|
BACKDROP : 'modal-backdrop',
|
|
OPEN : 'modal-open',
|
|
FADE : 'fade',
|
|
IN : 'in'
|
|
}
|
|
|
|
|
|
/**
|
|
* @const
|
|
* @enum {string}
|
|
* @private
|
|
*/
|
|
Modal._Selector = {
|
|
DIALOG : '.modal-dialog',
|
|
DATA_TOGGLE : '[data-toggle="modal"]',
|
|
DATA_DISMISS : '[data-dismiss="modal"]',
|
|
SCROLLBAR_MEASURER : 'modal-scrollbar-measure'
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Provides the jQuery Interface for the alert component.
|
|
* @param {Object|string=} opt_config
|
|
* @param {Element=} opt_relatedTarget
|
|
* @this {jQuery}
|
|
* @return {jQuery}
|
|
* @private
|
|
*/
|
|
Modal._jQueryInterface = function Plugin(opt_config, opt_relatedTarget) {
|
|
return this.each(function () {
|
|
var data = $(this).data(Modal._DATA_KEY)
|
|
var config = $.extend({}, Modal['Defaults'], $(this).data(), typeof opt_config == 'object' && opt_config)
|
|
|
|
if (!data) {
|
|
data = new Modal(this, config)
|
|
$(this).data(Modal._DATA_KEY, data)
|
|
}
|
|
|
|
if (typeof opt_config == 'string') {
|
|
data[opt_config](opt_relatedTarget)
|
|
|
|
} else if (config['show']) {
|
|
data['show'](opt_relatedTarget)
|
|
}
|
|
})
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Element} relatedTarget
|
|
*/
|
|
Modal.prototype['toggle'] = function (relatedTarget) {
|
|
return this._isShown ? this['hide']() : this['show'](relatedTarget)
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Element} relatedTarget
|
|
*/
|
|
Modal.prototype['show'] = function (relatedTarget) {
|
|
var showEvent = $.Event(Modal._Event.SHOW, { relatedTarget: relatedTarget })
|
|
|
|
$(this._element).trigger(showEvent)
|
|
|
|
if (this._isShown || showEvent.isDefaultPrevented()) {
|
|
return
|
|
}
|
|
|
|
this._isShown = true
|
|
|
|
this._checkScrollbar()
|
|
this._setScrollbar()
|
|
|
|
$(document.body).addClass(Modal._ClassName.OPEN)
|
|
|
|
this._escape()
|
|
this._resize()
|
|
|
|
$(this._element).on('click.dismiss.bs.modal', Modal._Selector.DATA_DISMISS, this['hide'].bind(this))
|
|
|
|
this._showBackdrop(this._showElement.bind(this, relatedTarget))
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Event} event
|
|
*/
|
|
Modal.prototype['hide'] = function (event) {
|
|
if (event) {
|
|
event.preventDefault()
|
|
}
|
|
|
|
var hideEvent = $.Event(Modal._Event.HIDE)
|
|
|
|
$(this._element).trigger(hideEvent)
|
|
|
|
if (!this._isShown || hideEvent.isDefaultPrevented()) {
|
|
return
|
|
}
|
|
|
|
this._isShown = false
|
|
|
|
this._escape()
|
|
this._resize()
|
|
|
|
$(document).off('focusin.bs.modal')
|
|
|
|
$(this._element).removeClass(Modal._ClassName.IN)
|
|
this._element.setAttribute('aria-hidden', true)
|
|
|
|
$(this._element).off('click.dismiss.bs.modal')
|
|
|
|
if (Bootstrap.transition && $(this._element).hasClass(Modal._ClassName.FADE)) {
|
|
$(this._element)
|
|
.one(Bootstrap.TRANSITION_END, this._hideModal.bind(this))
|
|
.emulateTransitionEnd(Modal._TRANSITION_DURATION)
|
|
} else {
|
|
this._hideModal()
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Element} relatedTarget
|
|
* @private
|
|
*/
|
|
Modal.prototype._showElement = function (relatedTarget) {
|
|
var transition = Bootstrap.transition && $(this._element).hasClass(Modal._ClassName.FADE)
|
|
|
|
if (!this._element.parentNode || this._element.parentNode.nodeType != Node.ELEMENT_NODE) {
|
|
document.body.appendChild(this._element) // don't move modals dom position
|
|
}
|
|
|
|
this._element.style.display = 'block'
|
|
this._element.scrollTop = 0
|
|
|
|
if (this._config['backdrop']) {
|
|
this._adjustBackdrop()
|
|
}
|
|
|
|
if (transition) {
|
|
Bootstrap.reflow(this._element)
|
|
}
|
|
|
|
$(this._element).addClass(Modal._ClassName.IN)
|
|
this._element.setAttribute('aria-hidden', false)
|
|
|
|
this._enforceFocus()
|
|
|
|
var shownEvent = $.Event(Modal._Event.SHOWN, { relatedTarget: relatedTarget })
|
|
|
|
var transitionComplete = function () {
|
|
this._element.focus()
|
|
$(this._element).trigger(shownEvent)
|
|
}.bind(this)
|
|
|
|
if (transition) {
|
|
var dialog = $(this._element).find(Modal._Selector.DIALOG)[0]
|
|
$(dialog)
|
|
.one(Bootstrap.TRANSITION_END, transitionComplete)
|
|
.emulateTransitionEnd(Modal._TRANSITION_DURATION)
|
|
} else {
|
|
transitionComplete()
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._enforceFocus = function () {
|
|
$(document)
|
|
.off('focusin.bs.modal') // guard against infinite focus loop
|
|
.on('focusin.bs.modal', function (e) {
|
|
if (this._element !== e.target && !$(this._element).has(e.target).length) {
|
|
this._element.focus()
|
|
}
|
|
}.bind(this))
|
|
}
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._escape = function () {
|
|
if (this._isShown && this._config['keyboard']) {
|
|
$(this._element).on('keydown.dismiss.bs.modal', function (event) {
|
|
if (event.which === 27) {
|
|
this['hide']()
|
|
}
|
|
}.bind(this))
|
|
|
|
} else if (!this._isShown) {
|
|
$(this._element).off('keydown.dismiss.bs.modal')
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._resize = function () {
|
|
if (this._isShown) {
|
|
$(window).on('resize.bs.modal', this._handleUpdate.bind(this))
|
|
} else {
|
|
$(window).off('resize.bs.modal')
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._hideModal = function () {
|
|
this._element.style.display = 'none'
|
|
this._showBackdrop(function () {
|
|
$(document.body).removeClass(Modal._ClassName.OPEN)
|
|
this._resetAdjustments()
|
|
this._resetScrollbar()
|
|
$(this._element).trigger(Modal._Event.HIDDEN)
|
|
}.bind(this))
|
|
}
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._removeBackdrop = function () {
|
|
if (this._backdrop) {
|
|
this._backdrop.parentNode.removeChild(this._backdrop)
|
|
this._backdrop = null
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Function} callback
|
|
* @private
|
|
*/
|
|
Modal.prototype._showBackdrop = function (callback) {
|
|
var animate = $(this._element).hasClass(Modal._ClassName.FADE) ? Modal._ClassName.FADE : ''
|
|
|
|
if (this._isShown && this._config['backdrop']) {
|
|
var doAnimate = Bootstrap.transition && animate
|
|
|
|
this._backdrop = document.createElement('div')
|
|
this._backdrop.className = Modal._ClassName.BACKDROP
|
|
|
|
if (animate) {
|
|
$(this._backdrop).addClass(animate)
|
|
}
|
|
|
|
$(this._element).prepend(this._backdrop)
|
|
|
|
$(this._backdrop).on('click.dismiss.bs.modal', function (event) {
|
|
if (event.target !== event.currentTarget) return
|
|
this._config['backdrop'] === 'static'
|
|
? this._element.focus()
|
|
: this['hide']()
|
|
}.bind(this))
|
|
|
|
if (doAnimate) {
|
|
Bootstrap.reflow(this._backdrop)
|
|
}
|
|
|
|
$(this._backdrop).addClass(Modal._ClassName.IN)
|
|
|
|
if (!callback) {
|
|
return
|
|
}
|
|
|
|
if (!doAnimate) {
|
|
callback()
|
|
return
|
|
}
|
|
|
|
$(this._backdrop)
|
|
.one(Bootstrap.TRANSITION_END, callback)
|
|
.emulateTransitionEnd(Modal._BACKDROP_TRANSITION_DURATION)
|
|
|
|
} else if (!this._isShown && this._backdrop) {
|
|
$(this._backdrop).removeClass(Modal._ClassName.IN)
|
|
|
|
var callbackRemove = function () {
|
|
this._removeBackdrop()
|
|
if (callback) {
|
|
callback()
|
|
}
|
|
}.bind(this)
|
|
|
|
if (Bootstrap.transition && $(this._element).hasClass(Modal._ClassName.FADE)) {
|
|
$(this._backdrop)
|
|
.one(Bootstrap.TRANSITION_END, callbackRemove)
|
|
.emulateTransitionEnd(Modal._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 into a
|
|
* ------------------------------------------------------------------------
|
|
*/
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._handleUpdate = function () {
|
|
if (this._config['backdrop']) this._adjustBackdrop()
|
|
this._adjustDialog()
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._adjustBackdrop = function () {
|
|
this._backdrop.style.height = 0 // todo (fat): no clue why we do this
|
|
this._backdrop.style.height = this._element.scrollHeight + 'px'
|
|
}
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._adjustDialog = function () {
|
|
var 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'
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._resetAdjustments = function () {
|
|
this._element.style.paddingLeft = ''
|
|
this._element.style.paddingRight = ''
|
|
}
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._checkScrollbar = function () {
|
|
this._isBodyOverflowing = document.body.scrollHeight > document.documentElement.clientHeight
|
|
this._scrollbarWidth = this._getScrollbarWidth()
|
|
}
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._setScrollbar = function () {
|
|
var bodyPadding = parseInt(($(document.body).css('padding-right') || 0), 10)
|
|
|
|
if (this._isBodyOverflowing) {
|
|
document.body.style.paddingRight = bodyPadding + this._scrollbarWidth + 'px'
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._resetScrollbar = function () {
|
|
document.body.style.paddingRight = ''
|
|
}
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Modal.prototype._getScrollbarWidth = function () { // thx walsh
|
|
var scrollDiv = document.createElement('div')
|
|
scrollDiv.className = Modal._Selector.SCROLLBAR_MEASURER
|
|
document.body.appendChild(scrollDiv)
|
|
var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth
|
|
document.body.removeChild(scrollDiv)
|
|
return scrollbarWidth
|
|
}
|
|
|
|
|
|
/**
|
|
* ------------------------------------------------------------------------
|
|
* jQuery Interface + noConflict implementaiton
|
|
* ------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* @const
|
|
* @type {Function}
|
|
*/
|
|
$.fn[Modal._NAME] = Modal._jQueryInterface
|
|
|
|
|
|
/**
|
|
* @const
|
|
* @type {Function}
|
|
*/
|
|
$.fn[Modal._NAME]['Constructor'] = Modal
|
|
|
|
|
|
/**
|
|
* @const
|
|
* @type {Function}
|
|
*/
|
|
$.fn[Modal._NAME]['noConflict'] = function () {
|
|
$.fn[Modal._NAME] = Modal._JQUERY_NO_CONFLICT
|
|
return this
|
|
}
|
|
|
|
|
|
/**
|
|
* ------------------------------------------------------------------------
|
|
* Data Api implementation
|
|
* ------------------------------------------------------------------------
|
|
*/
|
|
|
|
$(document).on('click.bs.modal.data-api', Modal._Selector.DATA_TOGGLE, function (event) {
|
|
var selector = Bootstrap.getSelectorFromElement(this)
|
|
|
|
if (selector) {
|
|
var target = $(selector)[0]
|
|
}
|
|
|
|
var config = $(target).data(Modal._DATA_KEY) ? 'toggle' : $.extend({}, $(target).data(), $(this).data())
|
|
|
|
if (this.tagName == 'A') {
|
|
event.preventDefault()
|
|
}
|
|
|
|
var $target = $(target).one(Modal._Event.SHOW, function (showEvent) {
|
|
if (showEvent.isDefaultPrevented()) {
|
|
return // only register focus restorer if modal will actually get shown
|
|
}
|
|
|
|
$target.one(Modal._Event.HIDDEN, function () {
|
|
if ($(this).is(':visible')) {
|
|
this.focus()
|
|
}
|
|
}.bind(this))
|
|
}.bind(this))
|
|
|
|
Modal._jQueryInterface.call($(target), config, this)
|
|
})
|