bootstrap-vue
Version:
With more than 85 components, over 45 available plugins, several directives, and 1000+ icons, BootstrapVue provides one of the most comprehensive implementations of the Bootstrap v4 component and grid system available for Vue.js v2.6, complete with extens
224 lines (217 loc) • 7.75 kB
JavaScript
/**
* Private ModalManager helper
* Handles controlling modal stacking zIndexes and body adjustments/classes
*/
import Vue from '../../../vue'
import {
addClass,
getAttr,
getBCR,
getCS,
getStyle,
hasAttr,
removeAttr,
removeClass,
requestAF,
selectAll,
setAttr,
setStyle
} 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) {
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')
addClass(div, 'modal-backdrop')
addClass(div, 'd-none')
setStyle(div, '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')
addClass(div, '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 = getStyle(el, 'paddingRight') || ''
setAttr(el, 'data-padding-right', actualPadding)
setStyle(el, 'paddingRight', `${toFloat(getCS(el).paddingRight, 0) + 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 = getStyle(el, 'marginRight') || ''
setAttr(el, 'data-margin-right', actualMargin)
setStyle(el, 'marginRight', `${toFloat(getCS(el).marginRight, 0) - 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 = getStyle(el, 'marginRight') || ''
setAttr(el, 'data-margin-right', actualMargin)
setStyle(el, 'marginRight', `${toFloat(getCS(el).marginRight, 0) + scrollbarWidth}px`)
body._marginChangedForModal.push(el)
})
// Adjust body padding
const actualPadding = getStyle(body, 'paddingRight') || ''
setAttr(body, 'data-padding-right', actualPadding)
setStyle(body, 'paddingRight', `${toFloat(getCS(body).paddingRight, 0) + 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')) {
setStyle(el, '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')) {
setStyle(el, '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')) {
setStyle(body, 'paddingRight', getAttr(body, 'data-padding-right') || '')
removeAttr(body, 'data-padding-right')
}
}
}
})
// Create and export our modal manager instance
export const modalManager = new ModalManager()