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
185 lines (170 loc) • 5.28 kB
JavaScript
// v-b-visible
// Private visibility check directive
// Based on IntersectionObserver
//
// Usage:
// v-b-visibility.<margin>.<once>="<callback>"
//
// Value:
// <callback>: method to be called when visibility state changes, receives one arg:
// true: element is visible
// false: element is not visible
// null: IntersectionObserver not supported
//
// Modifiers:
// <margin>: a positive decimal value of pixels away from viewport edge
// before being considered "visible". default is 0
// <once>: keyword 'once', meaning when the element becomes visible and
// callback is called observation/notification will stop.
//
// When used in a render function:
// export default {
// directives: { 'b-visible': VBVisible },
// render(h) {
// h(
// 'div',
// {
// directives: [
// { name: 'b-visible', value=this.callback, modifiers: { '123':true, 'once':true } }
// ]
// }
// )
// }
import { RX_DIGITS } from '../../constants/regex'
import { requestAF } from '../../utils/dom'
import { isFunction } from '../../utils/inspect'
import { looseEqual } from '../../utils/loose-equal'
import { clone, keys } from '../../utils/object'
import { nextTick } from '../../vue'
const OBSERVER_PROP_NAME = '__bv__visibility_observer'
class VisibilityObserver {
constructor(el, options) {
this.el = el
this.callback = options.callback
this.margin = options.margin || 0
this.once = options.once || false
this.observer = null
this.visible = undefined
this.doneOnce = false
// Create the observer instance (if possible)
this.createObserver()
}
createObserver() {
// Remove any previous observer
if (this.observer) {
/* istanbul ignore next */
this.stop()
}
// Should only be called once and `callback` prop should be a function
if (this.doneOnce || !isFunction(this.callback)) {
/* istanbul ignore next */
return
}
// Create the observer instance
try {
// Future: Possibly add in other modifiers for left/right/top/bottom
// offsets, root element reference, and thresholds
this.observer = new IntersectionObserver(this.handler.bind(this), {
// `null` = 'viewport'
root: null,
// Pixels away from view port to consider "visible"
rootMargin: this.margin,
// Intersection ratio of el and root (as a value from 0 to 1)
threshold: 0
})
} catch {
// No IntersectionObserver support, so just stop trying to observe
this.doneOnce = true
this.observer = undefined
this.callback(null)
return
}
// Start observing in a `$nextTick()` (to allow DOM to complete rendering)
/* istanbul ignore next: IntersectionObserver not supported in JSDOM */
nextTick(() => {
requestAF(() => {
// Placed in an `if` just in case we were destroyed before
// this `requestAnimationFrame` runs
if (this.observer) {
this.observer.observe(this.el)
}
})
})
}
/* istanbul ignore next */
handler(entries) {
const entry = entries ? entries[0] : {}
const isIntersecting = Boolean(entry.isIntersecting || entry.intersectionRatio > 0.0)
if (isIntersecting !== this.visible) {
this.visible = isIntersecting
this.callback(isIntersecting)
if (this.once && this.visible) {
this.doneOnce = true
this.stop()
}
}
}
stop() {
/* istanbul ignore next */
this.observer && this.observer.disconnect()
this.observer = null
}
}
const destroy = el => {
const observer = el[OBSERVER_PROP_NAME]
if (observer && observer.stop) {
observer.stop()
}
delete el[OBSERVER_PROP_NAME]
}
const bind = (el, { value, modifiers }) => {
// `value` is the callback function
const options = {
margin: '0px',
once: false,
callback: value
}
// Parse modifiers
keys(modifiers).forEach(mod => {
/* istanbul ignore else: Until <b-img-lazy> is switched to use this directive */
if (RX_DIGITS.test(mod)) {
options.margin = `${mod}px`
} else if (mod.toLowerCase() === 'once') {
options.once = true
}
})
// Destroy any previous observer
destroy(el)
// Create new observer
el[OBSERVER_PROP_NAME] = new VisibilityObserver(el, options)
// Store the current modifiers on the object (cloned)
el[OBSERVER_PROP_NAME]._prevModifiers = clone(modifiers)
}
// When the directive options may have been updated (or element)
const componentUpdated = (el, { value, oldValue, modifiers }, vnode) => {
// Compare value/oldValue and modifiers to see if anything has changed
// and if so, destroy old observer and create new observer
/* istanbul ignore next */
modifiers = clone(modifiers)
/* istanbul ignore next */
if (
el &&
(value !== oldValue ||
!el[OBSERVER_PROP_NAME] ||
!looseEqual(modifiers, el[OBSERVER_PROP_NAME]._prevModifiers))
) {
// Re-bind on element
bind(el, { value, modifiers }, vnode)
}
}
// When directive un-binds from element
const unbind = el => {
// Remove the observer
destroy(el)
}
// Export the directive
export const VBVisible = {
bind,
componentUpdated,
unbind
}