9
import Util from './util'
16
const VERSION = '4.6.1'
17
const DATA_KEY = 'bs.modal'
18
const EVENT_KEY = `.${DATA_KEY}`
19
const DATA_API_KEY = '.data-api'
20
const JQUERY_NO_CONFLICT = $.fn[NAME]
21
const ESCAPE_KEYCODE = 27
23
const CLASS_NAME_SCROLLABLE = 'modal-dialog-scrollable'
24
const CLASS_NAME_SCROLLBAR_MEASURER = 'modal-scrollbar-measure'
25
const CLASS_NAME_BACKDROP = 'modal-backdrop'
26
const CLASS_NAME_OPEN = 'modal-open'
27
const CLASS_NAME_FADE = 'fade'
28
const CLASS_NAME_SHOW = 'show'
29
const CLASS_NAME_STATIC = 'modal-static'
31
const EVENT_HIDE = `hide${EVENT_KEY}`
32
const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`
33
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
34
const EVENT_SHOW = `show${EVENT_KEY}`
35
const EVENT_SHOWN = `shown${EVENT_KEY}`
36
const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
37
const EVENT_RESIZE = `resize${EVENT_KEY}`
38
const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
39
const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
40
const EVENT_MOUSEUP_DISMISS = `mouseup.dismiss${EVENT_KEY}`
41
const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`
42
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
44
const SELECTOR_DIALOG = '.modal-dialog'
45
const SELECTOR_MODAL_BODY = '.modal-body'
46
const SELECTOR_DATA_TOGGLE = '[data-toggle="modal"]'
47
const SELECTOR_DATA_DISMISS = '[data-dismiss="modal"]'
48
const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'
49
const SELECTOR_STICKY_CONTENT = '.sticky-top'
59
backdrop: '(boolean|string)',
70
constructor(element, config) {
71
this._config = this._getConfig(config)
72
this._element = element
73
this._dialog = element.querySelector(SELECTOR_DIALOG)
76
this._isBodyOverflowing = false
77
this._ignoreBackdropClick = false
78
this._isTransitioning = false
79
this._scrollbarWidth = 0
83
static get VERSION() {
87
static get Default() {
92
toggle(relatedTarget) {
93
return this._isShown ? this.hide() : this.show(relatedTarget)
97
if (this._isShown || this._isTransitioning) {
101
const showEvent = $.Event(EVENT_SHOW, {
105
$(this._element).trigger(showEvent)
107
if (showEvent.isDefaultPrevented()) {
113
if ($(this._element).hasClass(CLASS_NAME_FADE)) {
114
this._isTransitioning = true
117
this._checkScrollbar()
122
this._setEscapeEvent()
123
this._setResizeEvent()
127
SELECTOR_DATA_DISMISS,
128
event => this.hide(event)
131
$(this._dialog).on(EVENT_MOUSEDOWN_DISMISS, () => {
132
$(this._element).one(EVENT_MOUSEUP_DISMISS, event => {
133
if ($(event.target).is(this._element)) {
134
this._ignoreBackdropClick = true
139
this._showBackdrop(() => this._showElement(relatedTarget))
144
event.preventDefault()
147
if (!this._isShown || this._isTransitioning) {
151
const hideEvent = $.Event(EVENT_HIDE)
153
$(this._element).trigger(hideEvent)
155
if (!this._isShown || hideEvent.isDefaultPrevented()) {
159
this._isShown = false
160
const transition = $(this._element).hasClass(CLASS_NAME_FADE)
163
this._isTransitioning = true
166
this._setEscapeEvent()
167
this._setResizeEvent()
169
$(document).off(EVENT_FOCUSIN)
171
$(this._element).removeClass(CLASS_NAME_SHOW)
173
$(this._element).off(EVENT_CLICK_DISMISS)
174
$(this._dialog).off(EVENT_MOUSEDOWN_DISMISS)
177
const transitionDuration = Util.getTransitionDurationFromElement(this._element)
180
.one(Util.TRANSITION_END, event => this._hideModal(event))
181
.emulateTransitionEnd(transitionDuration)
188
[window, this._element, this._dialog]
189
.forEach(htmlElement => $(htmlElement).off(EVENT_KEY))
196
$(document).off(EVENT_FOCUSIN)
198
$.removeData(this._element, DATA_KEY)
203
this._backdrop = null
205
this._isBodyOverflowing = null
206
this._ignoreBackdropClick = null
207
this._isTransitioning = null
208
this._scrollbarWidth = null
221
Util.typeCheckConfig(NAME, config, DefaultType)
225
_triggerBackdropTransition() {
226
const hideEventPrevented = $.Event(EVENT_HIDE_PREVENTED)
228
$(this._element).trigger(hideEventPrevented)
229
if (hideEventPrevented.isDefaultPrevented()) {
233
const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
235
if (!isModalOverflowing) {
236
this._element.style.overflowY = 'hidden'
239
this._element.classList.add(CLASS_NAME_STATIC)
241
const modalTransitionDuration = Util.getTransitionDurationFromElement(this._dialog)
242
$(this._element).off(Util.TRANSITION_END)
244
$(this._element).one(Util.TRANSITION_END, () => {
245
this._element.classList.remove(CLASS_NAME_STATIC)
246
if (!isModalOverflowing) {
247
$(this._element).one(Util.TRANSITION_END, () => {
248
this._element.style.overflowY = ''
250
.emulateTransitionEnd(this._element, modalTransitionDuration)
253
.emulateTransitionEnd(modalTransitionDuration)
254
this._element.focus()
257
_showElement(relatedTarget) {
258
const transition = $(this._element).hasClass(CLASS_NAME_FADE)
259
const modalBody = this._dialog ? this._dialog.querySelector(SELECTOR_MODAL_BODY) : null
261
if (!this._element.parentNode ||
262
this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
264
document.body.appendChild(this._element)
267
this._element.style.display = 'block'
268
this._element.removeAttribute('aria-hidden')
269
this._element.setAttribute('aria-modal', true)
270
this._element.setAttribute('role', 'dialog')
272
if ($(this._dialog).hasClass(CLASS_NAME_SCROLLABLE) && modalBody) {
273
modalBody.scrollTop = 0
275
this._element.scrollTop = 0
279
Util.reflow(this._element)
282
$(this._element).addClass(CLASS_NAME_SHOW)
284
if (this._config.focus) {
288
const shownEvent = $.Event(EVENT_SHOWN, {
292
const transitionComplete = () => {
293
if (this._config.focus) {
294
this._element.focus()
297
this._isTransitioning = false
298
$(this._element).trigger(shownEvent)
302
const transitionDuration = Util.getTransitionDurationFromElement(this._dialog)
305
.one(Util.TRANSITION_END, transitionComplete)
306
.emulateTransitionEnd(transitionDuration)
315
.on(EVENT_FOCUSIN, event => {
316
if (document !== event.target &&
317
this._element !== event.target &&
318
$(this._element).has(event.target).length === 0) {
319
this._element.focus()
326
$(this._element).on(EVENT_KEYDOWN_DISMISS, event => {
327
if (this._config.keyboard && event.which === ESCAPE_KEYCODE) {
328
event.preventDefault()
330
} else if (!this._config.keyboard && event.which === ESCAPE_KEYCODE) {
331
this._triggerBackdropTransition()
334
} else if (!this._isShown) {
335
$(this._element).off(EVENT_KEYDOWN_DISMISS)
341
$(window).on(EVENT_RESIZE, event => this.handleUpdate(event))
343
$(window).off(EVENT_RESIZE)
348
this._element.style.display = 'none'
349
this._element.setAttribute('aria-hidden', true)
350
this._element.removeAttribute('aria-modal')
351
this._element.removeAttribute('role')
352
this._isTransitioning = false
353
this._showBackdrop(() => {
354
$(document.body).removeClass(CLASS_NAME_OPEN)
355
this._resetAdjustments()
356
this._resetScrollbar()
357
$(this._element).trigger(EVENT_HIDDEN)
362
if (this._backdrop) {
363
$(this._backdrop).remove()
364
this._backdrop = null
368
_showBackdrop(callback) {
369
const animate = $(this._element).hasClass(CLASS_NAME_FADE) ?
372
if (this._isShown && this._config.backdrop) {
373
this._backdrop = document.createElement('div')
374
this._backdrop.className = CLASS_NAME_BACKDROP
377
this._backdrop.classList.add(animate)
380
$(this._backdrop).appendTo(document.body)
382
$(this._element).on(EVENT_CLICK_DISMISS, event => {
383
if (this._ignoreBackdropClick) {
384
this._ignoreBackdropClick = false
388
if (event.target !== event.currentTarget) {
392
if (this._config.backdrop === 'static') {
393
this._triggerBackdropTransition()
400
Util.reflow(this._backdrop)
403
$(this._backdrop).addClass(CLASS_NAME_SHOW)
414
const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
417
.one(Util.TRANSITION_END, callback)
418
.emulateTransitionEnd(backdropTransitionDuration)
419
} else if (!this._isShown && this._backdrop) {
420
$(this._backdrop).removeClass(CLASS_NAME_SHOW)
422
const callbackRemove = () => {
423
this._removeBackdrop()
429
if ($(this._element).hasClass(CLASS_NAME_FADE)) {
430
const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
433
.one(Util.TRANSITION_END, callbackRemove)
434
.emulateTransitionEnd(backdropTransitionDuration)
438
} else if (callback) {
449
const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
451
if (!this._isBodyOverflowing && isModalOverflowing) {
452
this._element.style.paddingLeft = `${this._scrollbarWidth}px`
455
if (this._isBodyOverflowing && !isModalOverflowing) {
456
this._element.style.paddingRight = `${this._scrollbarWidth}px`
460
_resetAdjustments() {
461
this._element.style.paddingLeft = ''
462
this._element.style.paddingRight = ''
466
const rect = document.body.getBoundingClientRect()
467
this._isBodyOverflowing = Math.round(rect.left + rect.right) < window.innerWidth
468
this._scrollbarWidth = this._getScrollbarWidth()
472
if (this._isBodyOverflowing) {
475
const fixedContent = [].slice.call(document.querySelectorAll(SELECTOR_FIXED_CONTENT))
476
const stickyContent = [].slice.call(document.querySelectorAll(SELECTOR_STICKY_CONTENT))
479
$(fixedContent).each((index, element) => {
480
const actualPadding = element.style.paddingRight
481
const calculatedPadding = $(element).css('padding-right')
483
.data('padding-right', actualPadding)
484
.css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
488
$(stickyContent).each((index, element) => {
489
const actualMargin = element.style.marginRight
490
const calculatedMargin = $(element).css('margin-right')
492
.data('margin-right', actualMargin)
493
.css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)
497
const actualPadding = document.body.style.paddingRight
498
const calculatedPadding = $(document.body).css('padding-right')
500
.data('padding-right', actualPadding)
501
.css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
504
$(document.body).addClass(CLASS_NAME_OPEN)
509
const fixedContent = [].slice.call(document.querySelectorAll(SELECTOR_FIXED_CONTENT))
510
$(fixedContent).each((index, element) => {
511
const padding = $(element).data('padding-right')
512
$(element).removeData('padding-right')
513
element.style.paddingRight = padding ? padding : ''
517
const elements = [].slice.call(document.querySelectorAll(`${SELECTOR_STICKY_CONTENT}`))
518
$(elements).each((index, element) => {
519
const margin = $(element).data('margin-right')
520
if (typeof margin !== 'undefined') {
521
$(element).css('margin-right', margin).removeData('margin-right')
526
const padding = $(document.body).data('padding-right')
527
$(document.body).removeData('padding-right')
528
document.body.style.paddingRight = padding ? padding : ''
531
_getScrollbarWidth() {
532
const scrollDiv = document.createElement('div')
533
scrollDiv.className = CLASS_NAME_SCROLLBAR_MEASURER
534
document.body.appendChild(scrollDiv)
535
const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth
536
document.body.removeChild(scrollDiv)
537
return scrollbarWidth
541
static _jQueryInterface(config, relatedTarget) {
542
return this.each(function () {
543
let data = $(this).data(DATA_KEY)
547
...(typeof config === 'object' && config ? config : {})
551
data = new Modal(this, _config)
552
$(this).data(DATA_KEY, data)
555
if (typeof config === 'string') {
556
if (typeof data[config] === 'undefined') {
557
throw new TypeError(`No method named "${config}"`)
560
data[config](relatedTarget)
561
} else if (_config.show) {
562
data.show(relatedTarget)
572
$(document).on(EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
574
const selector = Util.getSelectorFromElement(this)
577
target = document.querySelector(selector)
580
const config = $(target).data(DATA_KEY) ?
586
if (this.tagName === 'A' || this.tagName === 'AREA') {
587
event.preventDefault()
590
const $target = $(target).one(EVENT_SHOW, showEvent => {
591
if (showEvent.isDefaultPrevented()) {
596
$target.one(EVENT_HIDDEN, () => {
597
if ($(this).is(':visible')) {
603
Modal._jQueryInterface.call($(target), config, this)
610
$.fn[NAME] = Modal._jQueryInterface
611
$.fn[NAME].Constructor = Modal
612
$.fn[NAME].noConflict = () => {
613
$.fn[NAME] = JQUERY_NO_CONFLICT
614
return Modal._jQueryInterface