talos

Форк
0
617 строк · 16.5 Кб
1
/**
2
 * --------------------------------------------------------------------------
3
 * Bootstrap (v4.6.1): modal.js
4
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5
 * --------------------------------------------------------------------------
6
 */
7

8
import $ from 'jquery'
9
import Util from './util'
10

11
/**
12
 * Constants
13
 */
14

15
const NAME = 'modal'
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 // KeyboardEvent.which value for Escape (Esc) key
22

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'
30

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}`
43

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'
50

51
const Default = {
52
  backdrop: true,
53
  keyboard: true,
54
  focus: true,
55
  show: true
56
}
57

58
const DefaultType = {
59
  backdrop: '(boolean|string)',
60
  keyboard: 'boolean',
61
  focus: 'boolean',
62
  show: 'boolean'
63
}
64

65
/**
66
 * Class definition
67
 */
68

69
class Modal {
70
  constructor(element, config) {
71
    this._config = this._getConfig(config)
72
    this._element = element
73
    this._dialog = element.querySelector(SELECTOR_DIALOG)
74
    this._backdrop = null
75
    this._isShown = false
76
    this._isBodyOverflowing = false
77
    this._ignoreBackdropClick = false
78
    this._isTransitioning = false
79
    this._scrollbarWidth = 0
80
  }
81

82
  // Getters
83
  static get VERSION() {
84
    return VERSION
85
  }
86

87
  static get Default() {
88
    return Default
89
  }
90

91
  // Public
92
  toggle(relatedTarget) {
93
    return this._isShown ? this.hide() : this.show(relatedTarget)
94
  }
95

96
  show(relatedTarget) {
97
    if (this._isShown || this._isTransitioning) {
98
      return
99
    }
100

101
    const showEvent = $.Event(EVENT_SHOW, {
102
      relatedTarget
103
    })
104

105
    $(this._element).trigger(showEvent)
106

107
    if (showEvent.isDefaultPrevented()) {
108
      return
109
    }
110

111
    this._isShown = true
112

113
    if ($(this._element).hasClass(CLASS_NAME_FADE)) {
114
      this._isTransitioning = true
115
    }
116

117
    this._checkScrollbar()
118
    this._setScrollbar()
119

120
    this._adjustDialog()
121

122
    this._setEscapeEvent()
123
    this._setResizeEvent()
124

125
    $(this._element).on(
126
      EVENT_CLICK_DISMISS,
127
      SELECTOR_DATA_DISMISS,
128
      event => this.hide(event)
129
    )
130

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
135
        }
136
      })
137
    })
138

139
    this._showBackdrop(() => this._showElement(relatedTarget))
140
  }
141

142
  hide(event) {
143
    if (event) {
144
      event.preventDefault()
145
    }
146

147
    if (!this._isShown || this._isTransitioning) {
148
      return
149
    }
150

151
    const hideEvent = $.Event(EVENT_HIDE)
152

153
    $(this._element).trigger(hideEvent)
154

155
    if (!this._isShown || hideEvent.isDefaultPrevented()) {
156
      return
157
    }
158

159
    this._isShown = false
160
    const transition = $(this._element).hasClass(CLASS_NAME_FADE)
161

162
    if (transition) {
163
      this._isTransitioning = true
164
    }
165

166
    this._setEscapeEvent()
167
    this._setResizeEvent()
168

169
    $(document).off(EVENT_FOCUSIN)
170

171
    $(this._element).removeClass(CLASS_NAME_SHOW)
172

173
    $(this._element).off(EVENT_CLICK_DISMISS)
174
    $(this._dialog).off(EVENT_MOUSEDOWN_DISMISS)
175

176
    if (transition) {
177
      const transitionDuration = Util.getTransitionDurationFromElement(this._element)
178

179
      $(this._element)
180
        .one(Util.TRANSITION_END, event => this._hideModal(event))
181
        .emulateTransitionEnd(transitionDuration)
182
    } else {
183
      this._hideModal()
184
    }
185
  }
186

187
  dispose() {
188
    [window, this._element, this._dialog]
189
      .forEach(htmlElement => $(htmlElement).off(EVENT_KEY))
190

191
    /**
192
     * `document` has 2 events `EVENT_FOCUSIN` and `EVENT_CLICK_DATA_API`
193
     * Do not move `document` in `htmlElements` array
194
     * It will remove `EVENT_CLICK_DATA_API` event that should remain
195
     */
196
    $(document).off(EVENT_FOCUSIN)
197

198
    $.removeData(this._element, DATA_KEY)
199

200
    this._config = null
201
    this._element = null
202
    this._dialog = null
203
    this._backdrop = null
204
    this._isShown = null
205
    this._isBodyOverflowing = null
206
    this._ignoreBackdropClick = null
207
    this._isTransitioning = null
208
    this._scrollbarWidth = null
209
  }
210

211
  handleUpdate() {
212
    this._adjustDialog()
213
  }
214

215
  // Private
216
  _getConfig(config) {
217
    config = {
218
      ...Default,
219
      ...config
220
    }
221
    Util.typeCheckConfig(NAME, config, DefaultType)
222
    return config
223
  }
224

225
  _triggerBackdropTransition() {
226
    const hideEventPrevented = $.Event(EVENT_HIDE_PREVENTED)
227

228
    $(this._element).trigger(hideEventPrevented)
229
    if (hideEventPrevented.isDefaultPrevented()) {
230
      return
231
    }
232

233
    const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
234

235
    if (!isModalOverflowing) {
236
      this._element.style.overflowY = 'hidden'
237
    }
238

239
    this._element.classList.add(CLASS_NAME_STATIC)
240

241
    const modalTransitionDuration = Util.getTransitionDurationFromElement(this._dialog)
242
    $(this._element).off(Util.TRANSITION_END)
243

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 = ''
249
        })
250
          .emulateTransitionEnd(this._element, modalTransitionDuration)
251
      }
252
    })
253
      .emulateTransitionEnd(modalTransitionDuration)
254
    this._element.focus()
255
  }
256

257
  _showElement(relatedTarget) {
258
    const transition = $(this._element).hasClass(CLASS_NAME_FADE)
259
    const modalBody = this._dialog ? this._dialog.querySelector(SELECTOR_MODAL_BODY) : null
260

261
    if (!this._element.parentNode ||
262
        this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
263
      // Don't move modal's DOM position
264
      document.body.appendChild(this._element)
265
    }
266

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')
271

272
    if ($(this._dialog).hasClass(CLASS_NAME_SCROLLABLE) && modalBody) {
273
      modalBody.scrollTop = 0
274
    } else {
275
      this._element.scrollTop = 0
276
    }
277

278
    if (transition) {
279
      Util.reflow(this._element)
280
    }
281

282
    $(this._element).addClass(CLASS_NAME_SHOW)
283

284
    if (this._config.focus) {
285
      this._enforceFocus()
286
    }
287

288
    const shownEvent = $.Event(EVENT_SHOWN, {
289
      relatedTarget
290
    })
291

292
    const transitionComplete = () => {
293
      if (this._config.focus) {
294
        this._element.focus()
295
      }
296

297
      this._isTransitioning = false
298
      $(this._element).trigger(shownEvent)
299
    }
300

301
    if (transition) {
302
      const transitionDuration = Util.getTransitionDurationFromElement(this._dialog)
303

304
      $(this._dialog)
305
        .one(Util.TRANSITION_END, transitionComplete)
306
        .emulateTransitionEnd(transitionDuration)
307
    } else {
308
      transitionComplete()
309
    }
310
  }
311

312
  _enforceFocus() {
313
    $(document)
314
      .off(EVENT_FOCUSIN) // Guard against infinite focus loop
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()
320
        }
321
      })
322
  }
323

324
  _setEscapeEvent() {
325
    if (this._isShown) {
326
      $(this._element).on(EVENT_KEYDOWN_DISMISS, event => {
327
        if (this._config.keyboard && event.which === ESCAPE_KEYCODE) {
328
          event.preventDefault()
329
          this.hide()
330
        } else if (!this._config.keyboard && event.which === ESCAPE_KEYCODE) {
331
          this._triggerBackdropTransition()
332
        }
333
      })
334
    } else if (!this._isShown) {
335
      $(this._element).off(EVENT_KEYDOWN_DISMISS)
336
    }
337
  }
338

339
  _setResizeEvent() {
340
    if (this._isShown) {
341
      $(window).on(EVENT_RESIZE, event => this.handleUpdate(event))
342
    } else {
343
      $(window).off(EVENT_RESIZE)
344
    }
345
  }
346

347
  _hideModal() {
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)
358
    })
359
  }
360

361
  _removeBackdrop() {
362
    if (this._backdrop) {
363
      $(this._backdrop).remove()
364
      this._backdrop = null
365
    }
366
  }
367

368
  _showBackdrop(callback) {
369
    const animate = $(this._element).hasClass(CLASS_NAME_FADE) ?
370
      CLASS_NAME_FADE : ''
371

372
    if (this._isShown && this._config.backdrop) {
373
      this._backdrop = document.createElement('div')
374
      this._backdrop.className = CLASS_NAME_BACKDROP
375

376
      if (animate) {
377
        this._backdrop.classList.add(animate)
378
      }
379

380
      $(this._backdrop).appendTo(document.body)
381

382
      $(this._element).on(EVENT_CLICK_DISMISS, event => {
383
        if (this._ignoreBackdropClick) {
384
          this._ignoreBackdropClick = false
385
          return
386
        }
387

388
        if (event.target !== event.currentTarget) {
389
          return
390
        }
391

392
        if (this._config.backdrop === 'static') {
393
          this._triggerBackdropTransition()
394
        } else {
395
          this.hide()
396
        }
397
      })
398

399
      if (animate) {
400
        Util.reflow(this._backdrop)
401
      }
402

403
      $(this._backdrop).addClass(CLASS_NAME_SHOW)
404

405
      if (!callback) {
406
        return
407
      }
408

409
      if (!animate) {
410
        callback()
411
        return
412
      }
413

414
      const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
415

416
      $(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)
421

422
      const callbackRemove = () => {
423
        this._removeBackdrop()
424
        if (callback) {
425
          callback()
426
        }
427
      }
428

429
      if ($(this._element).hasClass(CLASS_NAME_FADE)) {
430
        const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
431

432
        $(this._backdrop)
433
          .one(Util.TRANSITION_END, callbackRemove)
434
          .emulateTransitionEnd(backdropTransitionDuration)
435
      } else {
436
        callbackRemove()
437
      }
438
    } else if (callback) {
439
      callback()
440
    }
441
  }
442

443
  // ----------------------------------------------------------------------
444
  // the following methods are used to handle overflowing modals
445
  // todo (fat): these should probably be refactored out of modal.js
446
  // ----------------------------------------------------------------------
447

448
  _adjustDialog() {
449
    const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
450

451
    if (!this._isBodyOverflowing && isModalOverflowing) {
452
      this._element.style.paddingLeft = `${this._scrollbarWidth}px`
453
    }
454

455
    if (this._isBodyOverflowing && !isModalOverflowing) {
456
      this._element.style.paddingRight = `${this._scrollbarWidth}px`
457
    }
458
  }
459

460
  _resetAdjustments() {
461
    this._element.style.paddingLeft = ''
462
    this._element.style.paddingRight = ''
463
  }
464

465
  _checkScrollbar() {
466
    const rect = document.body.getBoundingClientRect()
467
    this._isBodyOverflowing = Math.round(rect.left + rect.right) < window.innerWidth
468
    this._scrollbarWidth = this._getScrollbarWidth()
469
  }
470

471
  _setScrollbar() {
472
    if (this._isBodyOverflowing) {
473
      // Note: DOMNode.style.paddingRight returns the actual value or '' if not set
474
      //   while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set
475
      const fixedContent = [].slice.call(document.querySelectorAll(SELECTOR_FIXED_CONTENT))
476
      const stickyContent = [].slice.call(document.querySelectorAll(SELECTOR_STICKY_CONTENT))
477

478
      // Adjust fixed content padding
479
      $(fixedContent).each((index, element) => {
480
        const actualPadding = element.style.paddingRight
481
        const calculatedPadding = $(element).css('padding-right')
482
        $(element)
483
          .data('padding-right', actualPadding)
484
          .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
485
      })
486

487
      // Adjust sticky content margin
488
      $(stickyContent).each((index, element) => {
489
        const actualMargin = element.style.marginRight
490
        const calculatedMargin = $(element).css('margin-right')
491
        $(element)
492
          .data('margin-right', actualMargin)
493
          .css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)
494
      })
495

496
      // Adjust body padding
497
      const actualPadding = document.body.style.paddingRight
498
      const calculatedPadding = $(document.body).css('padding-right')
499
      $(document.body)
500
        .data('padding-right', actualPadding)
501
        .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
502
    }
503

504
    $(document.body).addClass(CLASS_NAME_OPEN)
505
  }
506

507
  _resetScrollbar() {
508
    // Restore fixed content padding
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 : ''
514
    })
515

516
    // Restore sticky content
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')
522
      }
523
    })
524

525
    // Restore body padding
526
    const padding = $(document.body).data('padding-right')
527
    $(document.body).removeData('padding-right')
528
    document.body.style.paddingRight = padding ? padding : ''
529
  }
530

531
  _getScrollbarWidth() { // thx d.walsh
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
538
  }
539

540
  // Static
541
  static _jQueryInterface(config, relatedTarget) {
542
    return this.each(function () {
543
      let data = $(this).data(DATA_KEY)
544
      const _config = {
545
        ...Default,
546
        ...$(this).data(),
547
        ...(typeof config === 'object' && config ? config : {})
548
      }
549

550
      if (!data) {
551
        data = new Modal(this, _config)
552
        $(this).data(DATA_KEY, data)
553
      }
554

555
      if (typeof config === 'string') {
556
        if (typeof data[config] === 'undefined') {
557
          throw new TypeError(`No method named "${config}"`)
558
        }
559

560
        data[config](relatedTarget)
561
      } else if (_config.show) {
562
        data.show(relatedTarget)
563
      }
564
    })
565
  }
566
}
567

568
/**
569
 * Data API implementation
570
 */
571

572
$(document).on(EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
573
  let target
574
  const selector = Util.getSelectorFromElement(this)
575

576
  if (selector) {
577
    target = document.querySelector(selector)
578
  }
579

580
  const config = $(target).data(DATA_KEY) ?
581
    'toggle' : {
582
      ...$(target).data(),
583
      ...$(this).data()
584
    }
585

586
  if (this.tagName === 'A' || this.tagName === 'AREA') {
587
    event.preventDefault()
588
  }
589

590
  const $target = $(target).one(EVENT_SHOW, showEvent => {
591
    if (showEvent.isDefaultPrevented()) {
592
      // Only register focus restorer if modal will actually get shown
593
      return
594
    }
595

596
    $target.one(EVENT_HIDDEN, () => {
597
      if ($(this).is(':visible')) {
598
        this.focus()
599
      }
600
    })
601
  })
602

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

606
/**
607
 * jQuery
608
 */
609

610
$.fn[NAME] = Modal._jQueryInterface
611
$.fn[NAME].Constructor = Modal
612
$.fn[NAME].noConflict = () => {
613
  $.fn[NAME] = JQUERY_NO_CONFLICT
614
  return Modal._jQueryInterface
615
}
616

617
export default Modal
618

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.