bootstrap-vue
Version:
BootstrapVue, with over 40 plugins and more than 80 custom components, custom directives, and over 300 icons, provides one of the most comprehensive implementations of Bootstrap v4 components and grid system for Vue.js. With extensive and automated WAI-AR
225 lines (218 loc) • 7.83 kB
JavaScript
/**
* Private ModalManager helper
* Handles controlling modal stacking zIndexes and body adjustments/classes
*/
import Vue from '../../../utils/vue'
import {
getAttr,
hasAttr,
removeAttr,
setAttr,
addClass,
removeClass,
getBCR,
getCS,
selectAll,
requestAF
} from '../../../utils/dom'
import { isBrowser } from '../../../utils/env'
import { isNull } from '../../../utils/inspect'
import { toFloat, toInteger } from '../../../utils/number'
// --- Constants ---
// Default modal backdrop z-index
const DEFAULT_ZINDEX = 1040
// Selectors for padding/margin adjustments
const Selector = {
FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',
STICKY_CONTENT: '.sticky-top',
NAVBAR_TOGGLER: '.navbar-toggler'
}
// @vue/component
const ModalManager = /*#__PURE__*/ Vue.extend({
data() {
return {
modals: [],
baseZIndex: null,
scrollbarWidth: null,
isBodyOverflowing: false
}
},
computed: {
modalCount() {
return this.modals.length
},
modalsAreOpen() {
return this.modalCount > 0
}
},
watch: {
modalCount(newCount, oldCount) {
if (isBrowser) {
this.getScrollbarWidth()
if (newCount > 0 && oldCount === 0) {
// Transitioning to modal(s) open
this.checkScrollbar()
this.setScrollbar()
addClass(document.body, 'modal-open')
} else if (newCount === 0 && oldCount > 0) {
// Transitioning to modal(s) closed
this.resetScrollbar()
removeClass(document.body, 'modal-open')
}
setAttr(document.body, 'data-modal-open-count', String(newCount))
}
},
modals(newVal, oldVal) {
this.checkScrollbar()
requestAF(() => {
this.updateModals(newVal || [])
})
}
},
methods: {
// Public methods
registerModal(modal) {
// Register the modal if not already registered
if (modal && this.modals.indexOf(modal) === -1) {
// Add modal to modals array
this.modals.push(modal)
modal.$once('hook:beforeDestroy', () => {
this.unregisterModal(modal)
})
}
},
unregisterModal(modal) {
const index = this.modals.indexOf(modal)
if (index > -1) {
// Remove modal from modals array
this.modals.splice(index, 1)
// Reset the modal's data
if (!(modal._isBeingDestroyed || modal._isDestroyed)) {
this.resetModal(modal)
}
}
},
getBaseZIndex() {
if (isNull(this.baseZIndex) && isBrowser) {
// Create a temporary `div.modal-backdrop` to get computed z-index
const div = document.createElement('div')
div.className = 'modal-backdrop d-none'
div.style.display = 'none'
document.body.appendChild(div)
this.baseZIndex = toInteger(getCS(div).zIndex || DEFAULT_ZINDEX)
document.body.removeChild(div)
}
return this.baseZIndex || DEFAULT_ZINDEX
},
getScrollbarWidth() {
if (isNull(this.scrollbarWidth) && isBrowser) {
// Create a temporary `div.measure-scrollbar` to get computed z-index
const div = document.createElement('div')
div.className = 'modal-scrollbar-measure'
document.body.appendChild(div)
this.scrollbarWidth = getBCR(div).width - div.clientWidth
document.body.removeChild(div)
}
return this.scrollbarWidth || 0
},
// Private methods
updateModals(modals) {
const baseZIndex = this.getBaseZIndex()
const scrollbarWidth = this.getScrollbarWidth()
modals.forEach((modal, index) => {
// We update data values on each modal
modal.zIndex = baseZIndex + index
modal.scrollbarWidth = scrollbarWidth
modal.isTop = index === this.modals.length - 1
modal.isBodyOverflowing = this.isBodyOverflowing
})
},
resetModal(modal) {
if (modal) {
modal.zIndex = this.getBaseZIndex()
modal.isTop = true
modal.isBodyOverflowing = false
}
},
checkScrollbar() {
// Determine if the body element is overflowing
const { left, right } = getBCR(document.body)
this.isBodyOverflowing = left + right < window.innerWidth
},
setScrollbar() {
const body = document.body
// Storage place to cache changes to margins and padding
// Note: This assumes the following element types are not added to the
// document after the modal has opened.
body._paddingChangedForModal = body._paddingChangedForModal || []
body._marginChangedForModal = body._marginChangedForModal || []
if (this.isBodyOverflowing) {
const scrollbarWidth = this.scrollbarWidth
// Adjust fixed content padding
/* istanbul ignore next: difficult to test in JSDOM */
selectAll(Selector.FIXED_CONTENT).forEach(el => {
const actualPadding = el.style.paddingRight
const calculatedPadding = getCS(el).paddingRight || 0
setAttr(el, 'data-padding-right', actualPadding)
el.style.paddingRight = `${toFloat(calculatedPadding) + scrollbarWidth}px`
body._paddingChangedForModal.push(el)
})
// Adjust sticky content margin
/* istanbul ignore next: difficult to test in JSDOM */
selectAll(Selector.STICKY_CONTENT).forEach(el => /* istanbul ignore next */ {
const actualMargin = el.style.marginRight
const calculatedMargin = getCS(el).marginRight || 0
setAttr(el, 'data-margin-right', actualMargin)
el.style.marginRight = `${toFloat(calculatedMargin) - scrollbarWidth}px`
body._marginChangedForModal.push(el)
})
// Adjust <b-navbar-toggler> margin
/* istanbul ignore next: difficult to test in JSDOM */
selectAll(Selector.NAVBAR_TOGGLER).forEach(el => /* istanbul ignore next */ {
const actualMargin = el.style.marginRight
const calculatedMargin = getCS(el).marginRight || 0
setAttr(el, 'data-margin-right', actualMargin)
el.style.marginRight = `${toFloat(calculatedMargin) + scrollbarWidth}px`
body._marginChangedForModal.push(el)
})
// Adjust body padding
const actualPadding = body.style.paddingRight
const calculatedPadding = getCS(body).paddingRight
setAttr(body, 'data-padding-right', actualPadding)
body.style.paddingRight = `${toFloat(calculatedPadding) + scrollbarWidth}px`
}
},
resetScrollbar() {
const body = document.body
if (body._paddingChangedForModal) {
// Restore fixed content padding
body._paddingChangedForModal.forEach(el => {
/* istanbul ignore next: difficult to test in JSDOM */
if (hasAttr(el, 'data-padding-right')) {
el.style.paddingRight = getAttr(el, 'data-padding-right') || ''
removeAttr(el, 'data-padding-right')
}
})
}
if (body._marginChangedForModal) {
// Restore sticky content and navbar-toggler margin
body._marginChangedForModal.forEach(el => {
/* istanbul ignore next: difficult to test in JSDOM */
if (hasAttr(el, 'data-margin-right')) {
el.style.marginRight = getAttr(el, 'data-margin-right') || ''
removeAttr(el, 'data-margin-right')
}
})
}
body._paddingChangedForModal = null
body._marginChangedForModal = null
// Restore body padding
if (hasAttr(body, 'data-padding-right')) {
body.style.paddingRight = getAttr(body, 'data-padding-right') || ''
removeAttr(body, 'data-padding-right')
}
}
}
})
// Create and export our modal manager instance
export const modalManager = new ModalManager()