modal.js 15.3 KB
Newer Older
1
import $ from 'jquery'
2
3
4
5
import Util from './util'

/**
 * --------------------------------------------------------------------------
Mark Otto's avatar
Mark Otto committed
6
 * Bootstrap (v4.1.3): modal.js
7
8
9
10
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */

Johann-S's avatar
Johann-S committed
11
12
13
14
15
/**
 * ------------------------------------------------------------------------
 * Constants
 * ------------------------------------------------------------------------
 */
16

Johann-S's avatar
Johann-S committed
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const NAME               = 'modal'
const VERSION            = '4.1.3'
const DATA_KEY           = 'bs.modal'
const EVENT_KEY          = `.${DATA_KEY}`
const DATA_API_KEY       = '.data-api'
const JQUERY_NO_CONFLICT = $.fn[NAME]
const ESCAPE_KEYCODE     = 27 // KeyboardEvent.which value for Escape (Esc) key

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',
  SHOW               : 'show'
}

const Selector = {
  DIALOG             : '.modal-dialog',
  DATA_TOGGLE        : '[data-toggle="modal"]',
  DATA_DISMISS       : '[data-dismiss="modal"]',
  FIXED_CONTENT      : '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',
  STICKY_CONTENT     : '.sticky-top'
}
fat's avatar
fat committed
68

Johann-S's avatar
Johann-S committed
69
70
71
72
73
/**
 * ------------------------------------------------------------------------
 * Class Definition
 * ------------------------------------------------------------------------
 */
74

Johann-S's avatar
Johann-S committed
75
76
77
78
79
80
81
82
83
84
class Modal {
  constructor(element, config) {
    this._config              = this._getConfig(config)
    this._element             = element
    this._dialog              = element.querySelector(Selector.DIALOG)
    this._backdrop            = null
    this._isShown             = false
    this._isBodyOverflowing   = false
    this._ignoreBackdropClick = false
    this._scrollbarWidth      = 0
85
86
  }

Johann-S's avatar
Johann-S committed
87
88
89
90
  // Getters

  static get VERSION() {
    return VERSION
91
92
  }

Johann-S's avatar
Johann-S committed
93
94
95
  static get Default() {
    return Default
  }
96

Johann-S's avatar
Johann-S committed
97
  // Public
98

Johann-S's avatar
Johann-S committed
99
100
101
  toggle(relatedTarget) {
    return this._isShown ? this.hide() : this.show(relatedTarget)
  }
102

Johann-S's avatar
Johann-S committed
103
104
105
  show(relatedTarget) {
    if (this._isTransitioning || this._isShown) {
      return
106
107
    }

Johann-S's avatar
Johann-S committed
108
109
    if ($(this._element).hasClass(ClassName.FADE)) {
      this._isTransitioning = true
110
111
    }

Johann-S's avatar
Johann-S committed
112
113
114
    const showEvent = $.Event(Event.SHOW, {
      relatedTarget
    })
115

Johann-S's avatar
Johann-S committed
116
    $(this._element).trigger(showEvent)
117

Johann-S's avatar
Johann-S committed
118
119
120
    if (this._isShown || showEvent.isDefaultPrevented()) {
      return
    }
121

Johann-S's avatar
Johann-S committed
122
    this._isShown = true
123

Johann-S's avatar
Johann-S committed
124
125
    this._checkScrollbar()
    this._setScrollbar()
126

Johann-S's avatar
Johann-S committed
127
    this._adjustDialog()
128

Johann-S's avatar
Johann-S committed
129
    $(document.body).addClass(ClassName.OPEN)
130

Johann-S's avatar
Johann-S committed
131
132
    this._setEscapeEvent()
    this._setResizeEvent()
David Bailey's avatar
David Bailey committed
133

Johann-S's avatar
Johann-S committed
134
135
136
137
138
    $(this._element).on(
      Event.CLICK_DISMISS,
      Selector.DATA_DISMISS,
      (event) => this.hide(event)
    )
139

Johann-S's avatar
Johann-S committed
140
141
142
143
144
145
146
    $(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => {
      $(this._element).one(Event.MOUSEUP_DISMISS, (event) => {
        if ($(event.target).is(this._element)) {
          this._ignoreBackdropClick = true
        }
      })
    })
147

Johann-S's avatar
Johann-S committed
148
149
    this._showBackdrop(() => this._showElement(relatedTarget))
  }
150

Johann-S's avatar
Johann-S committed
151
152
153
154
  hide(event) {
    if (event) {
      event.preventDefault()
    }
155

Johann-S's avatar
Johann-S committed
156
157
    if (this._isTransitioning || !this._isShown) {
      return
158
159
    }

Johann-S's avatar
Johann-S committed
160
    const hideEvent = $.Event(Event.HIDE)
161

Johann-S's avatar
Johann-S committed
162
    $(this._element).trigger(hideEvent)
163

Johann-S's avatar
Johann-S committed
164
165
166
    if (!this._isShown || hideEvent.isDefaultPrevented()) {
      return
    }
167

Johann-S's avatar
Johann-S committed
168
169
    this._isShown = false
    const transition = $(this._element).hasClass(ClassName.FADE)
170

Johann-S's avatar
Johann-S committed
171
172
173
    if (transition) {
      this._isTransitioning = true
    }
174

Johann-S's avatar
Johann-S committed
175
176
    this._setEscapeEvent()
    this._setResizeEvent()
177

Johann-S's avatar
Johann-S committed
178
    $(document).off(Event.FOCUSIN)
179

Johann-S's avatar
Johann-S committed
180
    $(this._element).removeClass(ClassName.SHOW)
181

Johann-S's avatar
Johann-S committed
182
183
    $(this._element).off(Event.CLICK_DISMISS)
    $(this._dialog).off(Event.MOUSEDOWN_DISMISS)
184
185


Johann-S's avatar
Johann-S committed
186
187
    if (transition) {
      const transitionDuration  = Util.getTransitionDurationFromElement(this._element)
188

Johann-S's avatar
Johann-S committed
189
190
191
192
193
194
195
      $(this._element)
        .one(Util.TRANSITION_END, (event) => this._hideModal(event))
        .emulateTransitionEnd(transitionDuration)
    } else {
      this._hideModal()
    }
  }
196

Johann-S's avatar
Johann-S committed
197
198
  dispose() {
    $.removeData(this._element, DATA_KEY)
199

Johann-S's avatar
Johann-S committed
200
    $(window, document, this._element, this._backdrop).off(EVENT_KEY)
201

Johann-S's avatar
Johann-S committed
202
203
204
205
206
207
208
209
210
    this._config              = null
    this._element             = null
    this._dialog              = null
    this._backdrop            = null
    this._isShown             = null
    this._isBodyOverflowing   = null
    this._ignoreBackdropClick = null
    this._scrollbarWidth      = null
  }
fat's avatar
fat committed
211

Johann-S's avatar
Johann-S committed
212
213
214
  handleUpdate() {
    this._adjustDialog()
  }
fat's avatar
fat committed
215

Johann-S's avatar
Johann-S committed
216
  // Private
fat's avatar
fat committed
217

Johann-S's avatar
Johann-S committed
218
219
220
221
  _getConfig(config) {
    config = {
      ...Default,
      ...config
222
    }
Johann-S's avatar
Johann-S committed
223
224
225
    Util.typeCheckConfig(NAME, config, DefaultType)
    return config
  }
226

Johann-S's avatar
Johann-S committed
227
228
  _showElement(relatedTarget) {
    const transition = $(this._element).hasClass(ClassName.FADE)
229

Johann-S's avatar
Johann-S committed
230
231
232
233
    if (!this._element.parentNode ||
        this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
      // Don't move modal's DOM position
      document.body.appendChild(this._element)
fat's avatar
fat committed
234
235
    }

Johann-S's avatar
Johann-S committed
236
237
238
    this._element.style.display = 'block'
    this._element.removeAttribute('aria-hidden')
    this._element.scrollTop = 0
239

Johann-S's avatar
Johann-S committed
240
241
242
    if (transition) {
      Util.reflow(this._element)
    }
243

Johann-S's avatar
Johann-S committed
244
    $(this._element).addClass(ClassName.SHOW)
245

Johann-S's avatar
Johann-S committed
246
247
248
    if (this._config.focus) {
      this._enforceFocus()
    }
249

Johann-S's avatar
Johann-S committed
250
251
252
    const shownEvent = $.Event(Event.SHOWN, {
      relatedTarget
    })
253

Johann-S's avatar
Johann-S committed
254
    const transitionComplete = () => {
Jacob Thornton's avatar
Jacob Thornton committed
255
      if (this._config.focus) {
Johann-S's avatar
Johann-S committed
256
        this._element.focus()
Jacob Thornton's avatar
Jacob Thornton committed
257
      }
Johann-S's avatar
Johann-S committed
258
259
260
      this._isTransitioning = false
      $(this._element).trigger(shownEvent)
    }
261

Johann-S's avatar
Johann-S committed
262
    if (transition) {
263
      const transitionDuration  = Util.getTransitionDurationFromElement(this._dialog)
264

Johann-S's avatar
Johann-S committed
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
      $(this._dialog)
        .one(Util.TRANSITION_END, transitionComplete)
        .emulateTransitionEnd(transitionDuration)
    } else {
      transitionComplete()
    }
  }

  _enforceFocus() {
    $(document)
      .off(Event.FOCUSIN) // Guard against infinite focus loop
      .on(Event.FOCUSIN, (event) => {
        if (document !== event.target &&
            this._element !== event.target &&
            $(this._element).has(event.target).length === 0) {
Jacob Thornton's avatar
Jacob Thornton committed
280
281
          this._element.focus()
        }
Johann-S's avatar
Johann-S committed
282
283
      })
  }
284

Johann-S's avatar
Johann-S committed
285
286
287
288
289
290
291
292
293
294
  _setEscapeEvent() {
    if (this._isShown && this._config.keyboard) {
      $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {
        if (event.which === ESCAPE_KEYCODE) {
          event.preventDefault()
          this.hide()
        }
      })
    } else if (!this._isShown) {
      $(this._element).off(Event.KEYDOWN_DISMISS)
295
    }
Johann-S's avatar
Johann-S committed
296
  }
297

Johann-S's avatar
Johann-S committed
298
299
300
301
302
  _setResizeEvent() {
    if (this._isShown) {
      $(window).on(Event.RESIZE, (event) => this.handleUpdate(event))
    } else {
      $(window).off(Event.RESIZE)
303
    }
Johann-S's avatar
Johann-S committed
304
  }
305

Johann-S's avatar
Johann-S committed
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
  _hideModal() {
    this._element.style.display = 'none'
    this._element.setAttribute('aria-hidden', true)
    this._isTransitioning = false
    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
322
    }
Johann-S's avatar
Johann-S committed
323
  }
324

Johann-S's avatar
Johann-S committed
325
326
327
328
329
330
331
332
333
334
  _showBackdrop(callback) {
    const animate = $(this._element).hasClass(ClassName.FADE)
      ? ClassName.FADE : ''

    if (this._isShown && this._config.backdrop) {
      this._backdrop = document.createElement('div')
      this._backdrop.className = ClassName.BACKDROP

      if (animate) {
        this._backdrop.classList.add(animate)
335
336
      }

Johann-S's avatar
Johann-S committed
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
      $(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()
        }
352
353
      })

Johann-S's avatar
Johann-S committed
354
355
      if (animate) {
        Util.reflow(this._backdrop)
356
357
      }

Johann-S's avatar
Johann-S committed
358
      $(this._backdrop).addClass(ClassName.SHOW)
359

Johann-S's avatar
Johann-S committed
360
361
362
      if (!callback) {
        return
      }
363

Johann-S's avatar
Johann-S committed
364
365
366
367
      if (!animate) {
        callback()
        return
      }
368

Johann-S's avatar
Johann-S committed
369
      const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
370

Johann-S's avatar
Johann-S committed
371
372
373
374
375
      $(this._backdrop)
        .one(Util.TRANSITION_END, callback)
        .emulateTransitionEnd(backdropTransitionDuration)
    } else if (!this._isShown && this._backdrop) {
      $(this._backdrop).removeClass(ClassName.SHOW)
376

Johann-S's avatar
Johann-S committed
377
378
379
      const callbackRemove = () => {
        this._removeBackdrop()
        if (callback) {
380
381
          callback()
        }
Johann-S's avatar
Johann-S committed
382
      }
383

Johann-S's avatar
Johann-S committed
384
      if ($(this._element).hasClass(ClassName.FADE)) {
385
386
        const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)

387
        $(this._backdrop)
Johann-S's avatar
Johann-S committed
388
          .one(Util.TRANSITION_END, callbackRemove)
389
          .emulateTransitionEnd(backdropTransitionDuration)
Johann-S's avatar
Johann-S committed
390
391
      } else {
        callbackRemove()
392
      }
Johann-S's avatar
Johann-S committed
393
394
    } else if (callback) {
      callback()
395
    }
Johann-S's avatar
Johann-S committed
396
  }
397

Johann-S's avatar
Johann-S committed
398
399
400
401
  // ----------------------------------------------------------------------
  // the following methods are used to handle overflowing modals
  // todo (fat): these should probably be refactored out of modal.js
  // ----------------------------------------------------------------------
402

Johann-S's avatar
Johann-S committed
403
404
405
  _adjustDialog() {
    const isModalOverflowing =
      this._element.scrollHeight > document.documentElement.clientHeight
406

Johann-S's avatar
Johann-S committed
407
408
    if (!this._isBodyOverflowing && isModalOverflowing) {
      this._element.style.paddingLeft = `${this._scrollbarWidth}px`
409
410
    }

Johann-S's avatar
Johann-S committed
411
412
    if (this._isBodyOverflowing && !isModalOverflowing) {
      this._element.style.paddingRight = `${this._scrollbarWidth}px`
413
    }
Johann-S's avatar
Johann-S committed
414
  }
415

Johann-S's avatar
Johann-S committed
416
417
418
419
  _resetAdjustments() {
    this._element.style.paddingLeft = ''
    this._element.style.paddingRight = ''
  }
420

Johann-S's avatar
Johann-S committed
421
422
423
424
425
  _checkScrollbar() {
    const rect = document.body.getBoundingClientRect()
    this._isBodyOverflowing = rect.left + rect.right < window.innerWidth
    this._scrollbarWidth = this._getScrollbarWidth()
  }
426

Johann-S's avatar
Johann-S committed
427
428
429
430
  _setScrollbar() {
    if (this._isBodyOverflowing) {
      // Note: DOMNode.style.paddingRight returns the actual value or '' if not set
      //   while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set
431
      const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))
Johann-S's avatar
Johann-S committed
432
433
434
      const stickyContent = [].slice.call(document.querySelectorAll(Selector.STICKY_CONTENT))

      // Adjust fixed content padding
435
      $(fixedContent).each((index, element) => {
Johann-S's avatar
Johann-S committed
436
437
438
439
440
        const actualPadding = element.style.paddingRight
        const calculatedPadding = $(element).css('padding-right')
        $(element)
          .data('padding-right', actualPadding)
          .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
441
442
      })

Johann-S's avatar
Johann-S committed
443
444
445
446
447
448
449
      // Adjust sticky content margin
      $(stickyContent).each((index, element) => {
        const actualMargin = element.style.marginRight
        const calculatedMargin = $(element).css('margin-right')
        $(element)
          .data('margin-right', actualMargin)
          .css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)
450
451
      })

Johann-S's avatar
Johann-S committed
452
453
454
455
456
457
      // Adjust body padding
      const actualPadding = document.body.style.paddingRight
      const calculatedPadding = $(document.body).css('padding-right')
      $(document.body)
        .data('padding-right', actualPadding)
        .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
458
    }
Johann-S's avatar
Johann-S committed
459
  }
460

Johann-S's avatar
Johann-S committed
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
  _resetScrollbar() {
    // Restore fixed content padding
    const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))
    $(fixedContent).each((index, element) => {
      const padding = $(element).data('padding-right')
      $(element).removeData('padding-right')
      element.style.paddingRight = padding ? padding : ''
    })

    // Restore sticky content
    const elements = [].slice.call(document.querySelectorAll(`${Selector.STICKY_CONTENT}`))
    $(elements).each((index, element) => {
      const margin = $(element).data('margin-right')
      if (typeof margin !== 'undefined') {
        $(element).css('margin-right', margin).removeData('margin-right')
      }
    })
478

Johann-S's avatar
Johann-S committed
479
480
481
482
483
    // Restore body padding
    const padding = $(document.body).data('padding-right')
    $(document.body).removeData('padding-right')
    document.body.style.paddingRight = padding ? padding : ''
  }
484

Johann-S's avatar
Johann-S committed
485
486
487
488
489
490
491
492
  _getScrollbarWidth() { // thx d.walsh
    const scrollDiv = document.createElement('div')
    scrollDiv.className = ClassName.SCROLLBAR_MEASURER
    document.body.appendChild(scrollDiv)
    const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth
    document.body.removeChild(scrollDiv)
    return scrollbarWidth
  }
493

Johann-S's avatar
Johann-S committed
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
  // Static

  static _jQueryInterface(config, relatedTarget) {
    return this.each(function () {
      let data = $(this).data(DATA_KEY)
      const _config = {
        ...Default,
        ...$(this).data(),
        ...typeof config === 'object' && config ? config : {}
      }

      if (!data) {
        data = new Modal(this, _config)
        $(this).data(DATA_KEY, data)
      }
509

Johann-S's avatar
Johann-S committed
510
511
512
      if (typeof config === 'string') {
        if (typeof data[config] === 'undefined') {
          throw new TypeError(`No method named "${config}"`)
513
        }
Johann-S's avatar
Johann-S committed
514
515
516
517
518
        data[config](relatedTarget)
      } else if (_config.show) {
        data.show(relatedTarget)
      }
    })
519
  }
Johann-S's avatar
Johann-S committed
520
521
522
523
524
525
526
}

/**
 * ------------------------------------------------------------------------
 * Data Api implementation
 * ------------------------------------------------------------------------
 */
527

Johann-S's avatar
Johann-S committed
528
529
530
$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
  let target
  const selector = Util.getSelectorFromElement(this)
531

Johann-S's avatar
Johann-S committed
532
533
534
  if (selector) {
    target = document.querySelector(selector)
  }
535

Johann-S's avatar
Johann-S committed
536
537
538
539
  const config = $(target).data(DATA_KEY)
    ? 'toggle' : {
      ...$(target).data(),
      ...$(this).data()
540
541
    }

Johann-S's avatar
Johann-S committed
542
543
544
  if (this.tagName === 'A' || this.tagName === 'AREA') {
    event.preventDefault()
  }
545

Johann-S's avatar
Johann-S committed
546
547
548
549
  const $target = $(target).one(Event.SHOW, (showEvent) => {
    if (showEvent.isDefaultPrevented()) {
      // Only register focus restorer if modal will actually get shown
      return
550
551
    }

Johann-S's avatar
Johann-S committed
552
553
554
    $target.one(Event.HIDDEN, () => {
      if ($(this).is(':visible')) {
        this.focus()
555
556
557
558
      }
    })
  })

Johann-S's avatar
Johann-S committed
559
560
561
562
563
564
565
566
  Modal._jQueryInterface.call($(target), config, this)
})

/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
 */
567

Johann-S's avatar
Johann-S committed
568
569
570
571
572
573
$.fn[NAME] = Modal._jQueryInterface
$.fn[NAME].Constructor = Modal
$.fn[NAME].noConflict = () => {
  $.fn[NAME] = JQUERY_NO_CONFLICT
  return Modal._jQueryInterface
}
574
575

export default Modal