1 import Util
from './util'
5 * --------------------------------------------------------------------------
6 * Bootstrap (v4.0.0-alpha.6): modal.js
7 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
8 * --------------------------------------------------------------------------
11 const Modal
= (($) => {
15 * ------------------------------------------------------------------------
17 * ------------------------------------------------------------------------
21 const VERSION
= '4.0.0-alpha.6'
22 const DATA_KEY
= 'bs.modal'
23 const EVENT_KEY
= `.${DATA_KEY}`
24 const DATA_API_KEY
= '.data-api'
25 const JQUERY_NO_CONFLICT
= $.fn
[NAME
]
26 const TRANSITION_DURATION
= 300
27 const BACKDROP_TRANSITION_DURATION
= 150
28 const ESCAPE_KEYCODE
= 27 // KeyboardEvent.which value for Escape (Esc) key
38 backdrop
: '(boolean|string)',
45 HIDE
: `hide${EVENT_KEY}`,
46 HIDDEN
: `hidden${EVENT_KEY}`,
47 SHOW
: `show${EVENT_KEY}`,
48 SHOWN
: `shown${EVENT_KEY}`,
49 FOCUSIN
: `focusin${EVENT_KEY}`,
50 RESIZE
: `resize${EVENT_KEY}`,
51 CLICK_DISMISS
: `click.dismiss${EVENT_KEY}`,
52 KEYDOWN_DISMISS
: `keydown.dismiss${EVENT_KEY}`,
53 MOUSEUP_DISMISS
: `mouseup.dismiss${EVENT_KEY}`,
54 MOUSEDOWN_DISMISS
: `mousedown.dismiss${EVENT_KEY}`,
55 CLICK_DATA_API
: `click${EVENT_KEY}${DATA_API_KEY}`
59 SCROLLBAR_MEASURER
: 'modal-scrollbar-measure',
60 BACKDROP
: 'modal-backdrop',
67 DIALOG
: '.modal-dialog',
68 DATA_TOGGLE
: '[data-toggle="modal"]',
69 DATA_DISMISS
: '[data-dismiss="modal"]',
70 FIXED_CONTENT
: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'
75 * ------------------------------------------------------------------------
77 * ------------------------------------------------------------------------
82 constructor(element
, config
) {
83 this._config
= this._getConfig(config
)
84 this._element
= element
85 this._dialog
= $(element
).find(Selector
.DIALOG
)[0]
88 this._isBodyOverflowing
= false
89 this._ignoreBackdropClick
= false
90 this._isTransitioning
= false
91 this._originalBodyPadding
= 0
92 this._scrollbarWidth
= 0
98 static get VERSION() {
102 static get Default() {
109 toggle(relatedTarget
) {
110 return this._isShown
? this.hide() : this.show(relatedTarget
)
113 show(relatedTarget
) {
114 if (this._isTransitioning
) {
115 throw new Error('Modal is transitioning')
118 if (Util
.supportsTransitionEnd() &&
119 $(this._element
).hasClass(ClassName
.FADE
)) {
120 this._isTransitioning
= true
122 const showEvent
= $.Event(Event
.SHOW
, {
126 $(this._element
).trigger(showEvent
)
128 if (this._isShown
|| showEvent
.isDefaultPrevented()) {
134 this._checkScrollbar()
137 $(document
.body
).addClass(ClassName
.OPEN
)
139 this._setEscapeEvent()
140 this._setResizeEvent()
144 Selector
.DATA_DISMISS
,
145 (event
) => this.hide(event
)
148 $(this._dialog
).on(Event
.MOUSEDOWN_DISMISS
, () => {
149 $(this._element
).one(Event
.MOUSEUP_DISMISS
, (event
) => {
150 if ($(event
.target
).is(this._element
)) {
151 this._ignoreBackdropClick
= true
156 this._showBackdrop(() => this._showElement(relatedTarget
))
161 event
.preventDefault()
164 if (this._isTransitioning
) {
165 throw new Error('Modal is transitioning')
168 const transition
= Util
.supportsTransitionEnd() &&
169 $(this._element
).hasClass(ClassName
.FADE
)
171 this._isTransitioning
= true
174 const hideEvent
= $.Event(Event
.HIDE
)
175 $(this._element
).trigger(hideEvent
)
177 if (!this._isShown
|| hideEvent
.isDefaultPrevented()) {
181 this._isShown
= false
183 this._setEscapeEvent()
184 this._setResizeEvent()
186 $(document
).off(Event
.FOCUSIN
)
188 $(this._element
).removeClass(ClassName
.SHOW
)
190 $(this._element
).off(Event
.CLICK_DISMISS
)
191 $(this._dialog
).off(Event
.MOUSEDOWN_DISMISS
)
195 .one(Util
.TRANSITION_END
, (event
) => this._hideModal(event
))
196 .emulateTransitionEnd(TRANSITION_DURATION
)
203 $.removeData(this._element
, DATA_KEY
)
205 $(window
, document
, this._element
, this._backdrop
).off(EVENT_KEY
)
210 this._backdrop
= null
212 this._isBodyOverflowing
= null
213 this._ignoreBackdropClick
= null
214 this._originalBodyPadding
= null
215 this._scrollbarWidth
= null
222 config
= $.extend({}, Default
, config
)
223 Util
.typeCheckConfig(NAME
, config
, DefaultType
)
227 _showElement(relatedTarget
) {
228 const transition
= Util
.supportsTransitionEnd() &&
229 $(this._element
).hasClass(ClassName
.FADE
)
231 if (!this._element
.parentNode
||
232 this._element
.parentNode
.nodeType
!== Node
.ELEMENT_NODE
) {
233 // don't move modals dom position
234 document
.body
.appendChild(this._element
)
237 this._element
.style
.display
= 'block'
238 this._element
.removeAttribute('aria-hidden')
239 this._element
.scrollTop
= 0
242 Util
.reflow(this._element
)
245 $(this._element
).addClass(ClassName
.SHOW
)
247 if (this._config
.focus
) {
251 const shownEvent
= $.Event(Event
.SHOWN
, {
255 const transitionComplete
= () => {
256 if (this._config
.focus
) {
257 this._element
.focus()
259 this._isTransitioning
= false
260 $(this._element
).trigger(shownEvent
)
265 .one(Util
.TRANSITION_END
, transitionComplete
)
266 .emulateTransitionEnd(TRANSITION_DURATION
)
274 .off(Event
.FOCUSIN
) // guard against infinite focus loop
275 .on(Event
.FOCUSIN
, (event
) => {
276 if (document
!== event
.target
&&
277 this._element
!== event
.target
&&
278 !$(this._element
).has(event
.target
).length
) {
279 this._element
.focus()
285 if (this._isShown
&& this._config
.keyboard
) {
286 $(this._element
).on(Event
.KEYDOWN_DISMISS
, (event
) => {
287 if (event
.which
=== ESCAPE_KEYCODE
) {
292 } else if (!this._isShown
) {
293 $(this._element
).off(Event
.KEYDOWN_DISMISS
)
299 $(window
).on(Event
.RESIZE
, (event
) => this._handleUpdate(event
))
301 $(window
).off(Event
.RESIZE
)
306 this._element
.style
.display
= 'none'
307 this._element
.setAttribute('aria-hidden', 'true')
308 this._isTransitioning
= false
309 this._showBackdrop(() => {
310 $(document
.body
).removeClass(ClassName
.OPEN
)
311 this._resetAdjustments()
312 this._resetScrollbar()
313 $(this._element
).trigger(Event
.HIDDEN
)
318 if (this._backdrop
) {
319 $(this._backdrop
).remove()
320 this._backdrop
= null
324 _showBackdrop(callback
) {
325 const animate
= $(this._element
).hasClass(ClassName
.FADE
) ?
328 if (this._isShown
&& this._config
.backdrop
) {
329 const doAnimate
= Util
.supportsTransitionEnd() && animate
331 this._backdrop
= document
.createElement('div')
332 this._backdrop
.className
= ClassName
.BACKDROP
335 $(this._backdrop
).addClass(animate
)
338 $(this._backdrop
).appendTo(document
.body
)
340 $(this._element
).on(Event
.CLICK_DISMISS
, (event
) => {
341 if (this._ignoreBackdropClick
) {
342 this._ignoreBackdropClick
= false
345 if (event
.target
!== event
.currentTarget
) {
348 if (this._config
.backdrop
=== 'static') {
349 this._element
.focus()
356 Util
.reflow(this._backdrop
)
359 $(this._backdrop
).addClass(ClassName
.SHOW
)
371 .one(Util
.TRANSITION_END
, callback
)
372 .emulateTransitionEnd(BACKDROP_TRANSITION_DURATION
)
374 } else if (!this._isShown
&& this._backdrop
) {
375 $(this._backdrop
).removeClass(ClassName
.SHOW
)
377 const callbackRemove
= () => {
378 this._removeBackdrop()
384 if (Util
.supportsTransitionEnd() &&
385 $(this._element
).hasClass(ClassName
.FADE
)) {
387 .one(Util
.TRANSITION_END
, callbackRemove
)
388 .emulateTransitionEnd(BACKDROP_TRANSITION_DURATION
)
393 } else if (callback
) {
399 // ----------------------------------------------------------------------
400 // the following methods are used to handle overflowing modals
401 // todo (fat): these should probably be refactored out of modal.js
402 // ----------------------------------------------------------------------
409 const isModalOverflowing
=
410 this._element
.scrollHeight
> document
.documentElement
.clientHeight
412 if (!this._isBodyOverflowing
&& isModalOverflowing
) {
413 this._element
.style
.paddingLeft
= `${this._scrollbarWidth}px`
416 if (this._isBodyOverflowing
&& !isModalOverflowing
) {
417 this._element
.style
.paddingRight
= `${this._scrollbarWidth}px`
421 _resetAdjustments() {
422 this._element
.style
.paddingLeft
= ''
423 this._element
.style
.paddingRight
= ''
427 this._isBodyOverflowing
= document
.body
.clientWidth
< window
.innerWidth
428 this._scrollbarWidth
= this._getScrollbarWidth()
432 const bodyPadding
= parseInt(
433 $(Selector
.FIXED_CONTENT
).css('padding-right') || 0,
437 this._originalBodyPadding
= document
.body
.style
.paddingRight
|| ''
439 if (this._isBodyOverflowing
) {
440 document
.body
.style
.paddingRight
=
441 `${bodyPadding + this._scrollbarWidth}px`
446 document
.body
.style
.paddingRight
= this._originalBodyPadding
449 _getScrollbarWidth() { // thx d.walsh
450 const scrollDiv
= document
.createElement('div')
451 scrollDiv
.className
= ClassName
.SCROLLBAR_MEASURER
452 document
.body
.appendChild(scrollDiv
)
453 const scrollbarWidth
= scrollDiv
.offsetWidth
- scrollDiv
.clientWidth
454 document
.body
.removeChild(scrollDiv
)
455 return scrollbarWidth
461 static _jQueryInterface(config
, relatedTarget
) {
462 return this.each(function () {
463 let data
= $(this).data(DATA_KEY
)
464 const _config
= $.extend(
468 typeof config
=== 'object' && config
472 data
= new Modal(this, _config
)
473 $(this).data(DATA_KEY
, data
)
476 if (typeof config
=== 'string') {
477 if (data
[config
] === undefined) {
478 throw new Error(`No method named "${config}"`)
480 data
[config
](relatedTarget
)
481 } else if (_config
.show
) {
482 data
.show(relatedTarget
)
491 * ------------------------------------------------------------------------
492 * Data Api implementation
493 * ------------------------------------------------------------------------
496 $(document
).on(Event
.CLICK_DATA_API
, Selector
.DATA_TOGGLE
, function (event
) {
498 const selector
= Util
.getSelectorFromElement(this)
501 target
= $(selector
)[0]
504 const config
= $(target
).data(DATA_KEY
) ?
505 'toggle' : $.extend({}, $(target
).data(), $(this).data())
507 if (this.tagName
=== 'A' || this.tagName
=== 'AREA') {
508 event
.preventDefault()
511 const $target
= $(target
).one(Event
.SHOW
, (showEvent
) => {
512 if (showEvent
.isDefaultPrevented()) {
513 // only register focus restorer if modal will actually get shown
517 $target
.one(Event
.HIDDEN
, () => {
518 if ($(this).is(':visible')) {
524 Modal
._jQueryInterface
.call($(target
), config
, this)
529 * ------------------------------------------------------------------------
531 * ------------------------------------------------------------------------
534 $.fn
[NAME
] = Modal
._jQueryInterface
535 $.fn
[NAME
].Constructor
= Modal
536 $.fn
[NAME
].noConflict = function () {
537 $.fn
[NAME
] = JQUERY_NO_CONFLICT
538 return Modal
._jQueryInterface