]> git.ipfire.org Git - ipfire.org.git/blob - src/scss/bootstrap-4.0.0-alpha.6/js/src/modal.js
.gitignore: Add .vscode
[ipfire.org.git] / src / scss / bootstrap-4.0.0-alpha.6 / js / src / modal.js
1 import Util from './util'
2
3
4 /**
5 * --------------------------------------------------------------------------
6 * Bootstrap (v4.0.0-alpha.6): modal.js
7 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
8 * --------------------------------------------------------------------------
9 */
10
11 const Modal = (($) => {
12
13
14 /**
15 * ------------------------------------------------------------------------
16 * Constants
17 * ------------------------------------------------------------------------
18 */
19
20 const NAME = 'modal'
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
29
30 const Default = {
31 backdrop : true,
32 keyboard : true,
33 focus : true,
34 show : true
35 }
36
37 const DefaultType = {
38 backdrop : '(boolean|string)',
39 keyboard : 'boolean',
40 focus : 'boolean',
41 show : 'boolean'
42 }
43
44 const Event = {
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}`
56 }
57
58 const ClassName = {
59 SCROLLBAR_MEASURER : 'modal-scrollbar-measure',
60 BACKDROP : 'modal-backdrop',
61 OPEN : 'modal-open',
62 FADE : 'fade',
63 SHOW : 'show'
64 }
65
66 const Selector = {
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'
71 }
72
73
74 /**
75 * ------------------------------------------------------------------------
76 * Class Definition
77 * ------------------------------------------------------------------------
78 */
79
80 class Modal {
81
82 constructor(element, config) {
83 this._config = this._getConfig(config)
84 this._element = element
85 this._dialog = $(element).find(Selector.DIALOG)[0]
86 this._backdrop = null
87 this._isShown = false
88 this._isBodyOverflowing = false
89 this._ignoreBackdropClick = false
90 this._isTransitioning = false
91 this._originalBodyPadding = 0
92 this._scrollbarWidth = 0
93 }
94
95
96 // getters
97
98 static get VERSION() {
99 return VERSION
100 }
101
102 static get Default() {
103 return Default
104 }
105
106
107 // public
108
109 toggle(relatedTarget) {
110 return this._isShown ? this.hide() : this.show(relatedTarget)
111 }
112
113 show(relatedTarget) {
114 if (this._isTransitioning) {
115 throw new Error('Modal is transitioning')
116 }
117
118 if (Util.supportsTransitionEnd() &&
119 $(this._element).hasClass(ClassName.FADE)) {
120 this._isTransitioning = true
121 }
122 const showEvent = $.Event(Event.SHOW, {
123 relatedTarget
124 })
125
126 $(this._element).trigger(showEvent)
127
128 if (this._isShown || showEvent.isDefaultPrevented()) {
129 return
130 }
131
132 this._isShown = true
133
134 this._checkScrollbar()
135 this._setScrollbar()
136
137 $(document.body).addClass(ClassName.OPEN)
138
139 this._setEscapeEvent()
140 this._setResizeEvent()
141
142 $(this._element).on(
143 Event.CLICK_DISMISS,
144 Selector.DATA_DISMISS,
145 (event) => this.hide(event)
146 )
147
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
152 }
153 })
154 })
155
156 this._showBackdrop(() => this._showElement(relatedTarget))
157 }
158
159 hide(event) {
160 if (event) {
161 event.preventDefault()
162 }
163
164 if (this._isTransitioning) {
165 throw new Error('Modal is transitioning')
166 }
167
168 const transition = Util.supportsTransitionEnd() &&
169 $(this._element).hasClass(ClassName.FADE)
170 if (transition) {
171 this._isTransitioning = true
172 }
173
174 const hideEvent = $.Event(Event.HIDE)
175 $(this._element).trigger(hideEvent)
176
177 if (!this._isShown || hideEvent.isDefaultPrevented()) {
178 return
179 }
180
181 this._isShown = false
182
183 this._setEscapeEvent()
184 this._setResizeEvent()
185
186 $(document).off(Event.FOCUSIN)
187
188 $(this._element).removeClass(ClassName.SHOW)
189
190 $(this._element).off(Event.CLICK_DISMISS)
191 $(this._dialog).off(Event.MOUSEDOWN_DISMISS)
192
193 if (transition) {
194 $(this._element)
195 .one(Util.TRANSITION_END, (event) => this._hideModal(event))
196 .emulateTransitionEnd(TRANSITION_DURATION)
197 } else {
198 this._hideModal()
199 }
200 }
201
202 dispose() {
203 $.removeData(this._element, DATA_KEY)
204
205 $(window, document, this._element, this._backdrop).off(EVENT_KEY)
206
207 this._config = null
208 this._element = null
209 this._dialog = null
210 this._backdrop = null
211 this._isShown = null
212 this._isBodyOverflowing = null
213 this._ignoreBackdropClick = null
214 this._originalBodyPadding = null
215 this._scrollbarWidth = null
216 }
217
218
219 // private
220
221 _getConfig(config) {
222 config = $.extend({}, Default, config)
223 Util.typeCheckConfig(NAME, config, DefaultType)
224 return config
225 }
226
227 _showElement(relatedTarget) {
228 const transition = Util.supportsTransitionEnd() &&
229 $(this._element).hasClass(ClassName.FADE)
230
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)
235 }
236
237 this._element.style.display = 'block'
238 this._element.removeAttribute('aria-hidden')
239 this._element.scrollTop = 0
240
241 if (transition) {
242 Util.reflow(this._element)
243 }
244
245 $(this._element).addClass(ClassName.SHOW)
246
247 if (this._config.focus) {
248 this._enforceFocus()
249 }
250
251 const shownEvent = $.Event(Event.SHOWN, {
252 relatedTarget
253 })
254
255 const transitionComplete = () => {
256 if (this._config.focus) {
257 this._element.focus()
258 }
259 this._isTransitioning = false
260 $(this._element).trigger(shownEvent)
261 }
262
263 if (transition) {
264 $(this._dialog)
265 .one(Util.TRANSITION_END, transitionComplete)
266 .emulateTransitionEnd(TRANSITION_DURATION)
267 } else {
268 transitionComplete()
269 }
270 }
271
272 _enforceFocus() {
273 $(document)
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()
280 }
281 })
282 }
283
284 _setEscapeEvent() {
285 if (this._isShown && this._config.keyboard) {
286 $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {
287 if (event.which === ESCAPE_KEYCODE) {
288 this.hide()
289 }
290 })
291
292 } else if (!this._isShown) {
293 $(this._element).off(Event.KEYDOWN_DISMISS)
294 }
295 }
296
297 _setResizeEvent() {
298 if (this._isShown) {
299 $(window).on(Event.RESIZE, (event) => this._handleUpdate(event))
300 } else {
301 $(window).off(Event.RESIZE)
302 }
303 }
304
305 _hideModal() {
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)
314 })
315 }
316
317 _removeBackdrop() {
318 if (this._backdrop) {
319 $(this._backdrop).remove()
320 this._backdrop = null
321 }
322 }
323
324 _showBackdrop(callback) {
325 const animate = $(this._element).hasClass(ClassName.FADE) ?
326 ClassName.FADE : ''
327
328 if (this._isShown && this._config.backdrop) {
329 const doAnimate = Util.supportsTransitionEnd() && animate
330
331 this._backdrop = document.createElement('div')
332 this._backdrop.className = ClassName.BACKDROP
333
334 if (animate) {
335 $(this._backdrop).addClass(animate)
336 }
337
338 $(this._backdrop).appendTo(document.body)
339
340 $(this._element).on(Event.CLICK_DISMISS, (event) => {
341 if (this._ignoreBackdropClick) {
342 this._ignoreBackdropClick = false
343 return
344 }
345 if (event.target !== event.currentTarget) {
346 return
347 }
348 if (this._config.backdrop === 'static') {
349 this._element.focus()
350 } else {
351 this.hide()
352 }
353 })
354
355 if (doAnimate) {
356 Util.reflow(this._backdrop)
357 }
358
359 $(this._backdrop).addClass(ClassName.SHOW)
360
361 if (!callback) {
362 return
363 }
364
365 if (!doAnimate) {
366 callback()
367 return
368 }
369
370 $(this._backdrop)
371 .one(Util.TRANSITION_END, callback)
372 .emulateTransitionEnd(BACKDROP_TRANSITION_DURATION)
373
374 } else if (!this._isShown && this._backdrop) {
375 $(this._backdrop).removeClass(ClassName.SHOW)
376
377 const callbackRemove = () => {
378 this._removeBackdrop()
379 if (callback) {
380 callback()
381 }
382 }
383
384 if (Util.supportsTransitionEnd() &&
385 $(this._element).hasClass(ClassName.FADE)) {
386 $(this._backdrop)
387 .one(Util.TRANSITION_END, callbackRemove)
388 .emulateTransitionEnd(BACKDROP_TRANSITION_DURATION)
389 } else {
390 callbackRemove()
391 }
392
393 } else if (callback) {
394 callback()
395 }
396 }
397
398
399 // ----------------------------------------------------------------------
400 // the following methods are used to handle overflowing modals
401 // todo (fat): these should probably be refactored out of modal.js
402 // ----------------------------------------------------------------------
403
404 _handleUpdate() {
405 this._adjustDialog()
406 }
407
408 _adjustDialog() {
409 const isModalOverflowing =
410 this._element.scrollHeight > document.documentElement.clientHeight
411
412 if (!this._isBodyOverflowing && isModalOverflowing) {
413 this._element.style.paddingLeft = `${this._scrollbarWidth}px`
414 }
415
416 if (this._isBodyOverflowing && !isModalOverflowing) {
417 this._element.style.paddingRight = `${this._scrollbarWidth}px`
418 }
419 }
420
421 _resetAdjustments() {
422 this._element.style.paddingLeft = ''
423 this._element.style.paddingRight = ''
424 }
425
426 _checkScrollbar() {
427 this._isBodyOverflowing = document.body.clientWidth < window.innerWidth
428 this._scrollbarWidth = this._getScrollbarWidth()
429 }
430
431 _setScrollbar() {
432 const bodyPadding = parseInt(
433 $(Selector.FIXED_CONTENT).css('padding-right') || 0,
434 10
435 )
436
437 this._originalBodyPadding = document.body.style.paddingRight || ''
438
439 if (this._isBodyOverflowing) {
440 document.body.style.paddingRight =
441 `${bodyPadding + this._scrollbarWidth}px`
442 }
443 }
444
445 _resetScrollbar() {
446 document.body.style.paddingRight = this._originalBodyPadding
447 }
448
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
456 }
457
458
459 // static
460
461 static _jQueryInterface(config, relatedTarget) {
462 return this.each(function () {
463 let data = $(this).data(DATA_KEY)
464 const _config = $.extend(
465 {},
466 Modal.Default,
467 $(this).data(),
468 typeof config === 'object' && config
469 )
470
471 if (!data) {
472 data = new Modal(this, _config)
473 $(this).data(DATA_KEY, data)
474 }
475
476 if (typeof config === 'string') {
477 if (data[config] === undefined) {
478 throw new Error(`No method named "${config}"`)
479 }
480 data[config](relatedTarget)
481 } else if (_config.show) {
482 data.show(relatedTarget)
483 }
484 })
485 }
486
487 }
488
489
490 /**
491 * ------------------------------------------------------------------------
492 * Data Api implementation
493 * ------------------------------------------------------------------------
494 */
495
496 $(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
497 let target
498 const selector = Util.getSelectorFromElement(this)
499
500 if (selector) {
501 target = $(selector)[0]
502 }
503
504 const config = $(target).data(DATA_KEY) ?
505 'toggle' : $.extend({}, $(target).data(), $(this).data())
506
507 if (this.tagName === 'A' || this.tagName === 'AREA') {
508 event.preventDefault()
509 }
510
511 const $target = $(target).one(Event.SHOW, (showEvent) => {
512 if (showEvent.isDefaultPrevented()) {
513 // only register focus restorer if modal will actually get shown
514 return
515 }
516
517 $target.one(Event.HIDDEN, () => {
518 if ($(this).is(':visible')) {
519 this.focus()
520 }
521 })
522 })
523
524 Modal._jQueryInterface.call($(target), config, this)
525 })
526
527
528 /**
529 * ------------------------------------------------------------------------
530 * jQuery
531 * ------------------------------------------------------------------------
532 */
533
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
539 }
540
541 return Modal
542
543 })(jQuery)
544
545 export default Modal