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


/**
 * --------------------------------------------------------------------------
Mark Otto's avatar
Mark Otto committed
6
 * Bootstrap (v4.0.0-beta): modal.js
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */

const Modal = (($) => {


  /**
   * ------------------------------------------------------------------------
   * Constants
   * ------------------------------------------------------------------------
   */

  const NAME                         = 'modal'
Mark Otto's avatar
Mark Otto committed
21
  const VERSION                      = '4.0.0-beta'
22
  const DATA_KEY                     = 'bs.modal'
fat's avatar
fat committed
23
24
  const EVENT_KEY                    = `.${DATA_KEY}`
  const DATA_API_KEY                 = '.data-api'
25
26
27
  const JQUERY_NO_CONFLICT           = $.fn[NAME]
  const TRANSITION_DURATION          = 300
  const BACKDROP_TRANSITION_DURATION = 150
28
  const ESCAPE_KEYCODE               = 27 // KeyboardEvent.which value for Escape (Esc) key
29
30
31
32

  const Default = {
    backdrop : true,
    keyboard : true,
33
    focus    : true,
34
35
36
    show     : true
  }

fat's avatar
fat committed
37
38
39
40
41
42
43
  const DefaultType = {
    backdrop : '(boolean|string)',
    keyboard : 'boolean',
    focus    : 'boolean',
    show     : 'boolean'
  }

44
  const Event = {
XhmikosR's avatar
XhmikosR committed
45
46
47
48
    HIDE              : `hide${EVENT_KEY}`,
    HIDDEN            : `hidden${EVENT_KEY}`,
    SHOW              : `show${EVENT_KEY}`,
    SHOWN             : `shown${EVENT_KEY}`,
fat's avatar
fat committed
49
50
51
52
53
54
55
    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}`
56
57
58
  }

  const ClassName = {
59
60
61
62
    SCROLLBAR_MEASURER : 'modal-scrollbar-measure',
    BACKDROP           : 'modal-backdrop',
    OPEN               : 'modal-open',
    FADE               : 'fade',
Starsam80's avatar
Starsam80 committed
63
    SHOW               : 'show'
64
65
66
67
68
69
  }

  const Selector = {
    DIALOG             : '.modal-dialog',
    DATA_TOGGLE        : '[data-toggle="modal"]',
    DATA_DISMISS       : '[data-dismiss="modal"]',
70
    FIXED_CONTENT      : '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',
71
    STICKY_CONTENT     : '.sticky-top',
72
    NAVBAR_TOGGLER     : '.navbar-toggler'
73
74
75
76
77
78
79
80
81
82
83
84
  }


  /**
   * ------------------------------------------------------------------------
   * Class Definition
   * ------------------------------------------------------------------------
   */

  class Modal {

    constructor(element, config) {
fat's avatar
fat committed
85
      this._config              = this._getConfig(config)
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
      this._element             = element
      this._dialog              = $(element).find(Selector.DIALOG)[0]
      this._backdrop            = null
      this._isShown             = false
      this._isBodyOverflowing   = false
      this._ignoreBackdropClick = false
      this._originalBodyPadding = 0
      this._scrollbarWidth      = 0
    }


    // getters

    static get VERSION() {
      return VERSION
    }

    static get Default() {
      return Default
    }


    // public

    toggle(relatedTarget) {
      return this._isShown ? this.hide() : this.show(relatedTarget)
    }

    show(relatedTarget) {
115
      if (this._isTransitioning) {
116
        return
117
118
      }

119
      if (Util.supportsTransitionEnd() && $(this._element).hasClass(ClassName.FADE)) {
120
121
        this._isTransitioning = true
      }
122

123
      const showEvent = $.Event(Event.SHOW, {
Jacob Thornton's avatar
Jacob Thornton committed
124
        relatedTarget
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
      })

      $(this._element).trigger(showEvent)

      if (this._isShown || showEvent.isDefaultPrevented()) {
        return
      }

      this._isShown = true

      this._checkScrollbar()
      this._setScrollbar()

      $(document.body).addClass(ClassName.OPEN)

      this._setEscapeEvent()
      this._setResizeEvent()

      $(this._element).on(
fat's avatar
fat committed
144
        Event.CLICK_DISMISS,
145
        Selector.DATA_DISMISS,
146
        (event) => this.hide(event)
147
148
      )

fat's avatar
fat committed
149
150
      $(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => {
        $(this._element).one(Event.MOUSEUP_DISMISS, (event) => {
151
          if ($(event.target).is(this._element)) {
152
            this._ignoreBackdropClick = true
153
154
155
156
          }
        })
      })

157
      this._showBackdrop(() => this._showElement(relatedTarget))
158
159
160
161
162
163
164
    }

    hide(event) {
      if (event) {
        event.preventDefault()
      }

165
166
      if (this._isTransitioning || !this._isShown) {
        return
167
      }
168

169
170
      const transition = Util.supportsTransitionEnd() && $(this._element).hasClass(ClassName.FADE)

171
172
173
174
175
      if (transition) {
        this._isTransitioning = true
      }

      const hideEvent = $.Event(Event.HIDE)
176

177
178
179
180
181
182
183
184
185
186
187
188
189
      $(this._element).trigger(hideEvent)

      if (!this._isShown || hideEvent.isDefaultPrevented()) {
        return
      }

      this._isShown = false

      this._setEscapeEvent()
      this._setResizeEvent()

      $(document).off(Event.FOCUSIN)

Starsam80's avatar
Starsam80 committed
190
      $(this._element).removeClass(ClassName.SHOW)
191

fat's avatar
fat committed
192
193
      $(this._element).off(Event.CLICK_DISMISS)
      $(this._dialog).off(Event.MOUSEDOWN_DISMISS)
194

195
      if (transition) {
196

197
        $(this._element)
198
          .one(Util.TRANSITION_END, (event) => this._hideModal(event))
199
200
201
202
203
204
          .emulateTransitionEnd(TRANSITION_DURATION)
      } else {
        this._hideModal()
      }
    }

fat's avatar
fat committed
205
206
207
    dispose() {
      $.removeData(this._element, DATA_KEY)

208
      $(window, document, this._element, this._backdrop).off(EVENT_KEY)
fat's avatar
fat committed
209
210
211
212
213
214
215
216
217
218
219

      this._config              = null
      this._element             = null
      this._dialog              = null
      this._backdrop            = null
      this._isShown             = null
      this._isBodyOverflowing   = null
      this._ignoreBackdropClick = null
      this._scrollbarWidth      = null
    }

220
221
222
    handleUpdate() {
      this._adjustDialog()
    }
223
224
225

    // private

fat's avatar
fat committed
226
227
228
229
230
231
    _getConfig(config) {
      config = $.extend({}, Default, config)
      Util.typeCheckConfig(NAME, config, DefaultType)
      return config
    }

232
    _showElement(relatedTarget) {
233
      const transition = Util.supportsTransitionEnd() &&
234
235
236
        $(this._element).hasClass(ClassName.FADE)

      if (!this._element.parentNode ||
237
         this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
238
239
240
241
242
        // don't move modals dom position
        document.body.appendChild(this._element)
      }

      this._element.style.display = 'block'
243
      this._element.removeAttribute('aria-hidden')
244
245
246
247
248
249
      this._element.scrollTop = 0

      if (transition) {
        Util.reflow(this._element)
      }

Starsam80's avatar
Starsam80 committed
250
      $(this._element).addClass(ClassName.SHOW)
251

Jacob Thornton's avatar
Jacob Thornton committed
252
253
254
      if (this._config.focus) {
        this._enforceFocus()
      }
255

256
      const shownEvent = $.Event(Event.SHOWN, {
Jacob Thornton's avatar
Jacob Thornton committed
257
        relatedTarget
258
259
      })

260
      const transitionComplete = () => {
Jacob Thornton's avatar
Jacob Thornton committed
261
262
263
        if (this._config.focus) {
          this._element.focus()
        }
264
        this._isTransitioning = false
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
        $(this._element).trigger(shownEvent)
      }

      if (transition) {
        $(this._dialog)
          .one(Util.TRANSITION_END, transitionComplete)
          .emulateTransitionEnd(TRANSITION_DURATION)
      } else {
        transitionComplete()
      }
    }

    _enforceFocus() {
      $(document)
        .off(Event.FOCUSIN) // guard against infinite focus loop
        .on(Event.FOCUSIN, (event) => {
281
282
          if (document !== event.target &&
              this._element !== event.target &&
283
              !$(this._element).has(event.target).length) {
284
285
286
287
288
289
290
            this._element.focus()
          }
        })
    }

    _setEscapeEvent() {
      if (this._isShown && this._config.keyboard) {
fat's avatar
fat committed
291
        $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {
292
          if (event.which === ESCAPE_KEYCODE) {
293
            event.preventDefault()
294
295
296
297
298
            this.hide()
          }
        })

      } else if (!this._isShown) {
fat's avatar
fat committed
299
        $(this._element).off(Event.KEYDOWN_DISMISS)
300
301
302
303
304
      }
    }

    _setResizeEvent() {
      if (this._isShown) {
305
        $(window).on(Event.RESIZE, (event) => this.handleUpdate(event))
306
307
308
309
310
311
312
      } else {
        $(window).off(Event.RESIZE)
      }
    }

    _hideModal() {
      this._element.style.display = 'none'
313
      this._element.setAttribute('aria-hidden', true)
314
      this._isTransitioning = false
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
      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
      }
    }

    _showBackdrop(callback) {
331
      const animate = $(this._element).hasClass(ClassName.FADE) ?
332
333
334
        ClassName.FADE : ''

      if (this._isShown && this._config.backdrop) {
335
        const doAnimate = Util.supportsTransitionEnd() && animate
336
337
338
339
340
341
342
343

        this._backdrop = document.createElement('div')
        this._backdrop.className = ClassName.BACKDROP

        if (animate) {
          $(this._backdrop).addClass(animate)
        }

Jacob Thornton's avatar
Jacob Thornton committed
344
        $(this._backdrop).appendTo(document.body)
345

fat's avatar
fat committed
346
        $(this._element).on(Event.CLICK_DISMISS, (event) => {
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
          if (this._ignoreBackdropClick) {
            this._ignoreBackdropClick = false
            return
          }
          if (event.target !== event.currentTarget) {
            return
          }
          if (this._config.backdrop === 'static') {
            this._element.focus()
          } else {
            this.hide()
          }
        })

        if (doAnimate) {
          Util.reflow(this._backdrop)
        }

Starsam80's avatar
Starsam80 committed
365
        $(this._backdrop).addClass(ClassName.SHOW)
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380

        if (!callback) {
          return
        }

        if (!doAnimate) {
          callback()
          return
        }

        $(this._backdrop)
          .one(Util.TRANSITION_END, callback)
          .emulateTransitionEnd(BACKDROP_TRANSITION_DURATION)

      } else if (!this._isShown && this._backdrop) {
Starsam80's avatar
Starsam80 committed
381
        $(this._backdrop).removeClass(ClassName.SHOW)
382

383
        const callbackRemove = () => {
384
385
386
387
388
389
390
          this._removeBackdrop()
          if (callback) {
            callback()
          }
        }

        if (Util.supportsTransitionEnd() &&
391
           $(this._element).hasClass(ClassName.FADE)) {
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
          $(this._backdrop)
            .one(Util.TRANSITION_END, callbackRemove)
            .emulateTransitionEnd(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 out of modal.js
    // ----------------------------------------------------------------------

    _adjustDialog() {
411
      const isModalOverflowing =
412
413
414
        this._element.scrollHeight > document.documentElement.clientHeight

      if (!this._isBodyOverflowing && isModalOverflowing) {
Jacob Thornton's avatar
Jacob Thornton committed
415
        this._element.style.paddingLeft = `${this._scrollbarWidth}px`
416
417
418
      }

      if (this._isBodyOverflowing && !isModalOverflowing) {
419
        this._element.style.paddingRight = `${this._scrollbarWidth}px`
420
421
422
423
424
425
426
427
428
      }
    }

    _resetAdjustments() {
      this._element.style.paddingLeft = ''
      this._element.style.paddingRight = ''
    }

    _checkScrollbar() {
429
      this._isBodyOverflowing = document.body.clientWidth < window.innerWidth
430
431
432
433
      this._scrollbarWidth = this._getScrollbarWidth()
    }

    _setScrollbar() {
434
435
436
437
438
439
440
441
442
443
      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

        // Adjust fixed content padding
        $(Selector.FIXED_CONTENT).each((index, element) => {
          const actualPadding = $(element)[0].style.paddingRight
          const calculatedPadding = $(element).css('padding-right')
          $(element).data('padding-right', actualPadding).css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
        })
444

445
446
447
448
449
450
451
        // Adjust sticky content margin
        $(Selector.STICKY_CONTENT).each((index, element) => {
          const actualMargin = $(element)[0].style.marginRight
          const calculatedMargin = $(element).css('margin-right')
          $(element).data('margin-right', actualMargin).css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)
        })

452
453
454
455
456
457
        // Adjust navbar-toggler margin
        $(Selector.NAVBAR_TOGGLER).each((index, element) => {
          const actualMargin = $(element)[0].style.marginRight
          const calculatedMargin = $(element).css('margin-right')
          $(element).data('margin-right', actualMargin).css('margin-right', `${parseFloat(calculatedMargin) + this._scrollbarWidth}px`)
        })
458

459
460
461
462
        // Adjust body padding
        const actualPadding = document.body.style.paddingRight
        const calculatedPadding = $('body').css('padding-right')
        $('body').data('padding-right', actualPadding).css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
463
464
465
466
      }
    }

    _resetScrollbar() {
467
468
469
470
471
472
473
474
      // Restore fixed content padding
      $(Selector.FIXED_CONTENT).each((index, element) => {
        const padding = $(element).data('padding-right')
        if (typeof padding !== 'undefined') {
          $(element).css('padding-right', padding).removeData('padding-right')
        }
      })

475
476
      // Restore sticky content and navbar-toggler margin
      $(`${Selector.STICKY_CONTENT}, ${Selector.NAVBAR_TOGGLER}`).each((index, element) => {
477
478
479
480
481
482
483
484
485
486
487
        const margin = $(element).data('margin-right')
        if (typeof margin !== 'undefined') {
          $(element).css('margin-right', margin).removeData('margin-right')
        }
      })

      // Restore body padding
      const padding = $('body').data('padding-right')
      if (typeof padding !== 'undefined') {
        $('body').css('padding-right', padding).removeData('padding-right')
      }
488
489
490
    }

    _getScrollbarWidth() { // thx d.walsh
491
      const scrollDiv = document.createElement('div')
492
      scrollDiv.className = ClassName.SCROLLBAR_MEASURER
493
      document.body.appendChild(scrollDiv)
494
      const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth
495
496
497
498
499
500
501
502
503
      document.body.removeChild(scrollDiv)
      return scrollbarWidth
    }


    // static

    static _jQueryInterface(config, relatedTarget) {
      return this.each(function () {
504
505
        let data      = $(this).data(DATA_KEY)
        const _config = $.extend(
506
507
508
509
510
511
512
513
514
515
516
517
          {},
          Modal.Default,
          $(this).data(),
          typeof config === 'object' && config
        )

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

        if (typeof config === 'string') {
XhmikosR's avatar
XhmikosR committed
518
          if (typeof data[config] === 'undefined') {
519
520
            throw new Error(`No method named "${config}"`)
          }
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
          data[config](relatedTarget)
        } else if (_config.show) {
          data.show(relatedTarget)
        }
      })
    }

  }


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

fat's avatar
fat committed
537
  $(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
538
    let target
539
    const selector = Util.getSelectorFromElement(this)
540
541
542
543
544

    if (selector) {
      target = $(selector)[0]
    }

545
    const config = $(target).data(DATA_KEY) ?
546
547
      'toggle' : $.extend({}, $(target).data(), $(this).data())

548
    if (this.tagName === 'A' || this.tagName === 'AREA') {
549
550
551
      event.preventDefault()
    }

552
    const $target = $(target).one(Event.SHOW, (showEvent) => {
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
      if (showEvent.isDefaultPrevented()) {
        // only register focus restorer if modal will actually get shown
        return
      }

      $target.one(Event.HIDDEN, () => {
        if ($(this).is(':visible')) {
          this.focus()
        }
      })
    })

    Modal._jQueryInterface.call($(target), config, this)
  })


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

  $.fn[NAME]             = Modal._jQueryInterface
  $.fn[NAME].Constructor = Modal
  $.fn[NAME].noConflict  = function () {
    $.fn[NAME] = JQUERY_NO_CONFLICT
    return Modal._jQueryInterface
  }

  return Modal

})(jQuery)

export default Modal