JAVASCRIPT   11

modal js

Guest on 6th July 2022 01:08:13 AM

  1. /**
  2.  * --------------------------------------------------------------------------
  3.  * Bootstrap (v4.3.1): modal.js
  4.  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  5.  * --------------------------------------------------------------------------
  6.  */
  7.  
  8. import $ from 'jquery'
  9. import Util from './util'
  10.  
  11. /**
  12.  * ------------------------------------------------------------------------
  13.  * Constants
  14.  * ------------------------------------------------------------------------
  15.  */
  16.  
  17. const NAME               = 'modal'
  18. const VERSION            = '4.3.1'
  19. const DATA_KEY           = 'bs.modal'
  20. const EVENT_KEY          = `.${DATA_KEY}`
  21. const DATA_API_KEY       = '.data-api'
  22. const JQUERY_NO_CONFLICT = $.fn[NAME]
  23. const ESCAPE_KEYCODE     = 27 // KeyboardEvent.which value for Escape (Esc) key
  24.  
  25. const Default = {
  26.   backdrop : true,
  27.   keyboard : true,
  28.   focus    : true,
  29.   show     : true
  30. }
  31.  
  32. const DefaultType = {
  33.   backdrop : '(boolean|string)',
  34.   keyboard : 'boolean',
  35.   focus    : 'boolean',
  36.   show     : 'boolean'
  37. }
  38.  
  39. const Event = {
  40.   HIDE              : `hide${EVENT_KEY}`,
  41.   HIDDEN            : `hidden${EVENT_KEY}`,
  42.   SHOW              : `show${EVENT_KEY}`,
  43.   SHOWN             : `shown${EVENT_KEY}`,
  44.   FOCUSIN           : `focusin${EVENT_KEY}`,
  45.   RESIZE            : `resize${EVENT_KEY}`,
  46.   CLICK_DISMISS     : `click.dismiss${EVENT_KEY}`,
  47.   KEYDOWN_DISMISS   : `keydown.dismiss${EVENT_KEY}`,
  48.   MOUSEUP_DISMISS   : `mouseup.dismiss${EVENT_KEY}`,
  49.   MOUSEDOWN_DISMISS : `mousedown.dismiss${EVENT_KEY}`,
  50.   CLICK_DATA_API    : `click${EVENT_KEY}${DATA_API_KEY}`
  51. }
  52.  
  53. const ClassName = {
  54.   SCROLLABLE         : 'modal-dialog-scrollable',
  55.   SCROLLBAR_MEASURER : 'modal-scrollbar-measure',
  56.   BACKDROP           : 'modal-backdrop',
  57.   OPEN               : 'modal-open',
  58.   FADE               : 'fade',
  59.   SHOW               : 'show'
  60. }
  61.  
  62. const Selector = {
  63.   DIALOG         : '.modal-dialog',
  64.   MODAL_BODY     : '.modal-body',
  65.   DATA_TOGGLE    : '[data-toggle="modal"]',
  66.   DATA_DISMISS   : '[data-dismiss="modal"]',
  67.   FIXED_CONTENT  : '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',
  68.   STICKY_CONTENT : '.sticky-top'
  69. }
  70.  
  71. /**
  72.  * ------------------------------------------------------------------------
  73.  * Class Definition
  74.  * ------------------------------------------------------------------------
  75.  */
  76.  
  77. class Modal {
  78.   constructor(element, config) {
  79.     this._config              = this._getConfig(config)
  80.     this._element             = element
  81.     this._dialog              = element.querySelector(Selector.DIALOG)
  82.     this._backdrop            = null
  83.     this._isShown             = false
  84.     this._isBodyOverflowing   = false
  85.     this._ignoreBackdropClick = false
  86.     this._isTransitioning     = false
  87.     this._scrollbarWidth      = 0
  88.   }
  89.  
  90.   // Getters
  91.  
  92.   static get VERSION() {
  93.     return VERSION
  94.   }
  95.  
  96.   static get Default() {
  97.     return Default
  98.   }
  99.  
  100.   // Public
  101.  
  102.   toggle(relatedTarget) {
  103.     return this._isShown ? this.hide() : this.show(relatedTarget)
  104.   }
  105.  
  106.   show(relatedTarget) {
  107.     if (this._isShown || this._isTransitioning) {
  108.       return
  109.     }
  110.  
  111.     if ($(this._element).hasClass(ClassName.FADE)) {
  112.       this._isTransitioning = true
  113.     }
  114.  
  115.     const showEvent = $.Event(Event.SHOW, {
  116.       relatedTarget
  117.     })
  118.  
  119.     $(this._element).trigger(showEvent)
  120.  
  121.     if (this._isShown || showEvent.isDefaultPrevented()) {
  122.       return
  123.     }
  124.  
  125.     this._isShown = true
  126.  
  127.     this._checkScrollbar()
  128.     this._setScrollbar()
  129.  
  130.     this._adjustDialog()
  131.  
  132.     this._setEscapeEvent()
  133.     this._setResizeEvent()
  134.  
  135.     $(this._element).on(
  136.       Event.CLICK_DISMISS,
  137.       Selector.DATA_DISMISS,
  138.       (event) => this.hide(event)
  139.     )
  140.  
  141.     $(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => {
  142.       $(this._element).one(Event.MOUSEUP_DISMISS, (event) => {
  143.         if ($(event.target).is(this._element)) {
  144.           this._ignoreBackdropClick = true
  145.         }
  146.       })
  147.     })
  148.  
  149.     this._showBackdrop(() => this._showElement(relatedTarget))
  150.   }
  151.  
  152.   hide(event) {
  153.     if (event) {
  154.       event.preventDefault()
  155.     }
  156.  
  157.     if (!this._isShown || this._isTransitioning) {
  158.       return
  159.     }
  160.  
  161.     const hideEvent = $.Event(Event.HIDE)
  162.  
  163.     $(this._element).trigger(hideEvent)
  164.  
  165.     if (!this._isShown || hideEvent.isDefaultPrevented()) {
  166.       return
  167.     }
  168.  
  169.     this._isShown = false
  170.     const transition = $(this._element).hasClass(ClassName.FADE)
  171.  
  172.     if (transition) {
  173.       this._isTransitioning = true
  174.     }
  175.  
  176.     this._setEscapeEvent()
  177.     this._setResizeEvent()
  178.  
  179.     $(document).off(Event.FOCUSIN)
  180.  
  181.     $(this._element).removeClass(ClassName.SHOW)
  182.  
  183.     $(this._element).off(Event.CLICK_DISMISS)
  184.     $(this._dialog).off(Event.MOUSEDOWN_DISMISS)
  185.  
  186.  
  187.     if (transition) {
  188.       const transitionDuration  = Util.getTransitionDurationFromElement(this._element)
  189.  
  190.       $(this._element)
  191.         .one(Util.TRANSITION_END, (event) => this._hideModal(event))
  192.         .emulateTransitionEnd(transitionDuration)
  193.     } else {
  194.       this._hideModal()
  195.     }
  196.   }
  197.  
  198.   dispose() {
  199.     [window, this._element, this._dialog]
  200.       .forEach((htmlElement) => $(htmlElement).off(EVENT_KEY))
  201.  
  202.     /**
  203.      * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`
  204.      * Do not move `document` in `htmlElements` array
  205.      * It will remove `Event.CLICK_DATA_API` event that should remain
  206.      */
  207.     $(document).off(Event.FOCUSIN)
  208.  
  209.     $.removeData(this._element, DATA_KEY)
  210.  
  211.     this._config              = null
  212.     this._element             = null
  213.     this._dialog              = null
  214.     this._backdrop            = null
  215.     this._isShown             = null
  216.     this._isBodyOverflowing   = null
  217.     this._ignoreBackdropClick = null
  218.     this._isTransitioning     = null
  219.     this._scrollbarWidth      = null
  220.   }
  221.  
  222.   handleUpdate() {
  223.     this._adjustDialog()
  224.   }
  225.  
  226.   // Private
  227.  
  228.   _getConfig(config) {
  229.     config = {
  230.       ...Default,
  231.       ...config
  232.     }
  233.     Util.typeCheckConfig(NAME, config, DefaultType)
  234.     return config
  235.   }
  236.  
  237.   _showElement(relatedTarget) {
  238.     const transition = $(this._element).hasClass(ClassName.FADE)
  239.  
  240.     if (!this._element.parentNode ||
  241.         this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
  242.       // Don't move modal's DOM position
  243.       document.body.appendChild(this._element)
  244.     }
  245.  
  246.     this._element.style.display = 'block'
  247.     this._element.removeAttribute('aria-hidden')
  248.     this._element.setAttribute('aria-modal', true)
  249.  
  250.     if ($(this._dialog).hasClass(ClassName.SCROLLABLE)) {
  251.       this._dialog.querySelector(Selector.MODAL_BODY).scrollTop = 0
  252.     } else {
  253.       this._element.scrollTop = 0
  254.     }
  255.  
  256.     if (transition) {
  257.       Util.reflow(this._element)
  258.     }
  259.  
  260.     $(this._element).addClass(ClassName.SHOW)
  261.  
  262.     if (this._config.focus) {
  263.       this._enforceFocus()
  264.     }
  265.  
  266.     const shownEvent = $.Event(Event.SHOWN, {
  267.       relatedTarget
  268.     })
  269.  
  270.     const transitionComplete = () => {
  271.       if (this._config.focus) {
  272.         this._element.focus()
  273.       }
  274.       this._isTransitioning = false
  275.       $(this._element).trigger(shownEvent)
  276.     }
  277.  
  278.     if (transition) {
  279.       const transitionDuration  = Util.getTransitionDurationFromElement(this._dialog)
  280.  
  281.       $(this._dialog)
  282.         .one(Util.TRANSITION_END, transitionComplete)
  283.         .emulateTransitionEnd(transitionDuration)
  284.     } else {
  285.       transitionComplete()
  286.     }
  287.   }
  288.  
  289.   _enforceFocus() {
  290.     $(document)
  291.       .off(Event.FOCUSIN) // Guard against infinite focus loop
  292.       .on(Event.FOCUSIN, (event) => {
  293.         if (document !== event.target &&
  294.             this._element !== event.target &&
  295.             $(this._element).has(event.target).length === 0) {
  296.           this._element.focus()
  297.         }
  298.       })
  299.   }
  300.  
  301.   _setEscapeEvent() {
  302.     if (this._isShown && this._config.keyboard) {
  303.       $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {
  304.         if (event.which === ESCAPE_KEYCODE) {
  305.           event.preventDefault()
  306.           this.hide()
  307.         }
  308.       })
  309.     } else if (!this._isShown) {
  310.       $(this._element).off(Event.KEYDOWN_DISMISS)
  311.     }
  312.   }
  313.  
  314.   _setResizeEvent() {
  315.     if (this._isShown) {
  316.       $(window).on(Event.RESIZE, (event) => this.handleUpdate(event))
  317.     } else {
  318.       $(window).off(Event.RESIZE)
  319.     }
  320.   }
  321.  
  322.   _hideModal() {
  323.     this._element.style.display = 'none'
  324.     this._element.setAttribute('aria-hidden', true)
  325.     this._element.removeAttribute('aria-modal')
  326.     this._isTransitioning = false
  327.     this._showBackdrop(() => {
  328.       $(document.body).removeClass(ClassName.OPEN)
  329.       this._resetAdjustments()
  330.       this._resetScrollbar()
  331.       $(this._element).trigger(Event.HIDDEN)
  332.     })
  333.   }
  334.  
  335.   _removeBackdrop() {
  336.     if (this._backdrop) {
  337.       $(this._backdrop).remove()
  338.       this._backdrop = null
  339.     }
  340.   }
  341.  
  342.   _showBackdrop(callback) {
  343.     const animate = $(this._element).hasClass(ClassName.FADE)
  344.       ? ClassName.FADE : ''
  345.  
  346.     if (this._isShown && this._config.backdrop) {
  347.       this._backdrop = document.createElement('div')
  348.       this._backdrop.className = ClassName.BACKDROP
  349.  
  350.       if (animate) {
  351.         this._backdrop.classList.add(animate)
  352.       }
  353.  
  354.       $(this._backdrop).appendTo(document.body)
  355.  
  356.       $(this._element).on(Event.CLICK_DISMISS, (event) => {
  357.         if (this._ignoreBackdropClick) {
  358.           this._ignoreBackdropClick = false
  359.           return
  360.         }
  361.         if (event.target !== event.currentTarget) {
  362.           return
  363.         }
  364.         if (this._config.backdrop === 'static') {
  365.           this._element.focus()
  366.         } else {
  367.           this.hide()
  368.         }
  369.       })
  370.  
  371.       if (animate) {
  372.         Util.reflow(this._backdrop)
  373.       }
  374.  
  375.       $(this._backdrop).addClass(ClassName.SHOW)
  376.  
  377.       if (!callback) {
  378.         return
  379.       }
  380.  
  381.       if (!animate) {
  382.         callback()
  383.         return
  384.       }
  385.  
  386.       const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
  387.  
  388.       $(this._backdrop)
  389.         .one(Util.TRANSITION_END, callback)
  390.         .emulateTransitionEnd(backdropTransitionDuration)
  391.     } else if (!this._isShown && this._backdrop) {
  392.       $(this._backdrop).removeClass(ClassName.SHOW)
  393.  
  394.       const callbackRemove = () => {
  395.         this._removeBackdrop()
  396.         if (callback) {
  397.           callback()
  398.         }
  399.       }
  400.  
  401.       if ($(this._element).hasClass(ClassName.FADE)) {
  402.         const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
  403.  
  404.         $(this._backdrop)
  405.           .one(Util.TRANSITION_END, callbackRemove)
  406.           .emulateTransitionEnd(backdropTransitionDuration)
  407.       } else {
  408.         callbackRemove()
  409.       }
  410.     } else if (callback) {
  411.       callback()
  412.     }
  413.   }
  414.  
  415.   // ----------------------------------------------------------------------
  416.   // the following methods are used to handle overflowing modals
  417.   // todo (fat): these should probably be refactored out of modal.js
  418.   // ----------------------------------------------------------------------
  419.  
  420.   _adjustDialog() {
  421.     const isModalOverflowing =
  422.       this._element.scrollHeight > document.documentElement.clientHeight
  423.  
  424.     if (!this._isBodyOverflowing && isModalOverflowing) {
  425.       this._element.style.paddingLeft = `${this._scrollbarWidth}px`
  426.     }
  427.  
  428.     if (this._isBodyOverflowing && !isModalOverflowing) {
  429.       this._element.style.paddingRight = `${this._scrollbarWidth}px`
  430.     }
  431.   }
  432.  
  433.   _resetAdjustments() {
  434.     this._element.style.paddingLeft = ''
  435.     this._element.style.paddingRight = ''
  436.   }
  437.  
  438.   _checkScrollbar() {
  439.     const rect = document.body.getBoundingClientRect()
  440.     this._isBodyOverflowing = rect.left + rect.right < window.innerWidth
  441.     this._scrollbarWidth = this._getScrollbarWidth()
  442.   }
  443.  
  444.   _setScrollbar() {
  445.     if (this._isBodyOverflowing) {
  446.       // Note: DOMNode.style.paddingRight returns the actual value or '' if not set
  447.       //   while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set
  448.       const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))
  449.       const stickyContent = [].slice.call(document.querySelectorAll(Selector.STICKY_CONTENT))
  450.  
  451.       // Adjust fixed content padding
  452.       $(fixedContent).each((index, element) => {
  453.         const actualPadding = element.style.paddingRight
  454.         const calculatedPadding = $(element).css('padding-right')
  455.         $(element)
  456.           .data('padding-right', actualPadding)
  457.           .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
  458.       })
  459.  
  460.       // Adjust sticky content margin
  461.       $(stickyContent).each((index, element) => {
  462.         const actualMargin = element.style.marginRight
  463.         const calculatedMargin = $(element).css('margin-right')
  464.         $(element)
  465.           .data('margin-right', actualMargin)
  466.           .css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)
  467.       })
  468.  
  469.       // Adjust body padding
  470.       const actualPadding = document.body.style.paddingRight
  471.       const calculatedPadding = $(document.body).css('padding-right')
  472.       $(document.body)
  473.         .data('padding-right', actualPadding)
  474.         .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
  475.     }
  476.  
  477.     $(document.body).addClass(ClassName.OPEN)
  478.   }
  479.  
  480.   _resetScrollbar() {
  481.     // Restore fixed content padding
  482.     const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))
  483.     $(fixedContent).each((index, element) => {
  484.       const padding = $(element).data('padding-right')
  485.       $(element).removeData('padding-right')
  486.       element.style.paddingRight = padding ? padding : ''
  487.     })
  488.  
  489.     // Restore sticky content
  490.     const elements = [].slice.call(document.querySelectorAll(`${Selector.STICKY_CONTENT}`))
  491.     $(elements).each((index, element) => {
  492.       const margin = $(element).data('margin-right')
  493.       if (typeof margin !== 'undefined') {
  494.         $(element).css('margin-right', margin).removeData('margin-right')
  495.       }
  496.     })
  497.  
  498.     // Restore body padding
  499.     const padding = $(document.body).data('padding-right')
  500.     $(document.body).removeData('padding-right')
  501.     document.body.style.paddingRight = padding ? padding : ''
  502.   }
  503.  
  504.   _getScrollbarWidth() { // thx d.walsh
  505.     const scrollDiv = document.createElement('div')
  506.     scrollDiv.className = ClassName.SCROLLBAR_MEASURER
  507.     document.body.appendChild(scrollDiv)
  508.     const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth
  509.     document.body.removeChild(scrollDiv)
  510.     return scrollbarWidth
  511.   }
  512.  
  513.   // Static
  514.  
  515.   static _jQueryInterface(config, relatedTarget) {
  516.     return this.each(function () {
  517.       let data = $(this).data(DATA_KEY)
  518.       const _config = {
  519.         ...Default,
  520.         ...$(this).data(),
  521.         ...typeof config === 'object' && config ? config : {}
  522.       }
  523.  
  524.       if (!data) {
  525.         data = new Modal(this, _config)
  526.         $(this).data(DATA_KEY, data)
  527.       }
  528.  
  529.       if (typeof config === 'string') {
  530.         if (typeof data[config] === 'undefined') {
  531.           throw new TypeError(`No method named "${config}"`)
  532.         }
  533.         data[config](relatedTarget)
  534.       } else if (_config.show) {
  535.         data.show(relatedTarget)
  536.       }
  537.     })
  538.   }
  539. }
  540.  
  541. /**
  542.  * ------------------------------------------------------------------------
  543.  * Data Api implementation
  544.  * ------------------------------------------------------------------------
  545.  */
  546.  
  547. $(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
  548.   let target
  549.   const selector = Util.getSelectorFromElement(this)
  550.  
  551.   if (selector) {
  552.     target = document.querySelector(selector)
  553.   }
  554.  
  555.   const config = $(target).data(DATA_KEY)
  556.     ? 'toggle' : {
  557.       ...$(target).data(),
  558.       ...$(this).data()
  559.     }
  560.  
  561.   if (this.tagName === 'A' || this.tagName === 'AREA') {
  562.     event.preventDefault()
  563.   }
  564.  
  565.   const $target = $(target).one(Event.SHOW, (showEvent) => {
  566.     if (showEvent.isDefaultPrevented()) {
  567.       // Only register focus restorer if modal will actually get shown
  568.       return
  569.     }
  570.  
  571.     $target.one(Event.HIDDEN, () => {
  572.       if ($(this).is(':visible')) {
  573.         this.focus()
  574.       }
  575.     })
  576.   })
  577.  
  578.   Modal._jQueryInterface.call($(target), config, this)
  579. })
  580.  
  581. /**
  582.  * ------------------------------------------------------------------------
  583.  * jQuery
  584.  * ------------------------------------------------------------------------
  585.  */
  586.  
  587. $.fn[NAME] = Modal._jQueryInterface
  588. $.fn[NAME].Constructor = Modal
  589. $.fn[NAME].noConflict = () => {
  590.   $.fn[NAME] = JQUERY_NO_CONFLICT
  591.   return Modal._jQueryInterface
  592. }
  593.  
  594. export default Modal

Raw Paste


Login or Register to edit or fork this paste. It's free.