codekingpro/portable-devtools
115k
1/**2 * --------------------------------------------------------------------------3 * Bootstrap (v4.6.2): modal.js4 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)5 * --------------------------------------------------------------------------6 */7 8import $ from 'jquery'9import Util from './util'10 11/**12 * Constants13 */14 15const NAME = 'modal'16const VERSION = '4.6.2'17const DATA_KEY = 'bs.modal'18const EVENT_KEY = `.${DATA_KEY}`19const DATA_API_KEY = '.data-api'20const JQUERY_NO_CONFLICT = $.fn[NAME]21const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key22 23const CLASS_NAME_SCROLLABLE = 'modal-dialog-scrollable'24const CLASS_NAME_SCROLLBAR_MEASURER = 'modal-scrollbar-measure'25const CLASS_NAME_BACKDROP = 'modal-backdrop'26const CLASS_NAME_OPEN = 'modal-open'27const CLASS_NAME_FADE = 'fade'28const CLASS_NAME_SHOW = 'show'29const CLASS_NAME_STATIC = 'modal-static'30 31const EVENT_HIDE = `hide${EVENT_KEY}`32const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`33const EVENT_HIDDEN = `hidden${EVENT_KEY}`34const EVENT_SHOW = `show${EVENT_KEY}`35const EVENT_SHOWN = `shown${EVENT_KEY}`36const EVENT_FOCUSIN = `focusin${EVENT_KEY}`37const EVENT_RESIZE = `resize${EVENT_KEY}`38const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`39const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`40const EVENT_MOUSEUP_DISMISS = `mouseup.dismiss${EVENT_KEY}`41const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`42const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`43 44const SELECTOR_DIALOG = '.modal-dialog'45const SELECTOR_MODAL_BODY = '.modal-body'46const SELECTOR_DATA_TOGGLE = '[data-toggle="modal"]'47const SELECTOR_DATA_DISMISS = '[data-dismiss="modal"]'48const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'49const SELECTOR_STICKY_CONTENT = '.sticky-top'50 51const Default = {52 backdrop: true,53 keyboard: true,54 focus: true,55 show: true56}57 58const DefaultType = {59 backdrop: '(boolean|string)',60 keyboard: 'boolean',61 focus: 'boolean',62 show: 'boolean'63}64 65/**66 * Class definition67 */68 69class Modal {70 constructor(element, config) {71 this._config = this._getConfig(config)72 this._element = element73 this._dialog = element.querySelector(SELECTOR_DIALOG)74 this._backdrop = null75 this._isShown = false76 this._isBodyOverflowing = false77 this._ignoreBackdropClick = false78 this._isTransitioning = false79 this._scrollbarWidth = 080 }81 82 // Getters83 static get VERSION() {84 return VERSION85 }86 87 static get Default() {88 return Default89 }90 91 // Public92 toggle(relatedTarget) {93 return this._isShown ? this.hide() : this.show(relatedTarget)94 }95 96 show(relatedTarget) {97 if (this._isShown || this._isTransitioning) {98 return99 }100 101 const showEvent = $.Event(EVENT_SHOW, {102 relatedTarget103 })104 105 $(this._element).trigger(showEvent)106 107 if (showEvent.isDefaultPrevented()) {108 return109 }110 111 this._isShown = true112 113 if ($(this._element).hasClass(CLASS_NAME_FADE)) {114 this._isTransitioning = true115 }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 = true135 }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 return149 }150 151 const hideEvent = $.Event(EVENT_HIDE)152 153 $(this._element).trigger(hideEvent)154 155 if (!this._isShown || hideEvent.isDefaultPrevented()) {156 return157 }158 159 this._isShown = false160 const transition = $(this._element).hasClass(CLASS_NAME_FADE)161 162 if (transition) {163 this._isTransitioning = true164 }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` array194 * It will remove `EVENT_CLICK_DATA_API` event that should remain195 */196 $(document).off(EVENT_FOCUSIN)197 198 $.removeData(this._element, DATA_KEY)199 200 this._config = null201 this._element = null202 this._dialog = null203 this._backdrop = null204 this._isShown = null205 this._isBodyOverflowing = null206 this._ignoreBackdropClick = null207 this._isTransitioning = null208 this._scrollbarWidth = null209 }210 211 handleUpdate() {212 this._adjustDialog()213 }214 215 // Private216 _getConfig(config) {217 config = {218 ...Default,219 ...config220 }221 Util.typeCheckConfig(NAME, config, DefaultType)222 return config223 }224 225 _triggerBackdropTransition() {226 const hideEventPrevented = $.Event(EVENT_HIDE_PREVENTED)227 228 $(this._element).trigger(hideEventPrevented)229 if (hideEventPrevented.isDefaultPrevented()) {230 return231 }232 233 const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight234 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) : null260 261 if (!this._element.parentNode ||262 this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {263 // Don't move modal's DOM position264 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 = 0274 } else {275 this._element.scrollTop = 0276 }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 relatedTarget290 })291 292 const transitionComplete = () => {293 if (this._config.focus) {294 this._element.focus()295 }296 297 this._isTransitioning = false298 $(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 loop315 .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 = false353 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 = null365 }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_BACKDROP375 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 = false385 return386 }387 388 if (event.target !== event.currentTarget) {389 return390 }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 return407 }408 409 if (!animate) {410 callback()411 return412 }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 modals445 // todo (fat): these should probably be refactored out of modal.js446 // ----------------------------------------------------------------------447 448 _adjustDialog() {449 const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight450 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.innerWidth468 this._scrollbarWidth = this._getScrollbarWidth()469 }470 471 _setScrollbar() {472 if (this._isBodyOverflowing) {473 // Note: DOMNode.style.paddingRight returns the actual value or '' if not set474 // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set475 const fixedContent = [].slice.call(document.querySelectorAll(SELECTOR_FIXED_CONTENT))476 const stickyContent = [].slice.call(document.querySelectorAll(SELECTOR_STICKY_CONTENT))477 478 // Adjust fixed content padding479 $(fixedContent).each((index, element) => {480 const actualPadding = element.style.paddingRight481 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 margin488 $(stickyContent).each((index, element) => {489 const actualMargin = element.style.marginRight490 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 padding497 const actualPadding = document.body.style.paddingRight498 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 padding509 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 content517 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 padding526 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.walsh532 const scrollDiv = document.createElement('div')533 scrollDiv.className = CLASS_NAME_SCROLLBAR_MEASURER534 document.body.appendChild(scrollDiv)535 const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth536 document.body.removeChild(scrollDiv)537 return scrollbarWidth538 }539 540 // Static541 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 implementation570 */571 572$(document).on(EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {573 let target574 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 shown593 return594 }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 * jQuery608 */609 610$.fn[NAME] = Modal._jQueryInterface611$.fn[NAME].Constructor = Modal612$.fn[NAME].noConflict = () => {613 $.fn[NAME] = JQUERY_NO_CONFLICT614 return Modal._jQueryInterface615}616 617export default Modal618 