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
187 lines (171 loc) • 5.37 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 looseEqual from '../../utils/loose-equal'
import { requestAF } from '../../utils/dom'
import { isFunction } from '../../utils/inspect'
import { clone, keys } from '../../utils/object'
const OBSERVER_PROP_NAME = '__bv__visibility_observer'
const onlyDgitsRE = /^\d+$/
class VisibilityObserver {
constructor(el, options, vnode) {
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(vnode)
}
createObserver(vnode) {
// 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 */
vnode.context.$nextTick(() => {
requestAF(() => {
// Placed in an `if` just in case we were destroyed before
// this `requestAnimationFrame` runs
if (this.observer) {
this.observer.observe(this.el)
}
})
})
}
handler(entries) /* istanbul ignore next: IntersectionObserver not supported in JSDOM */ {
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() {
const observer = this.observer
/* istanbul ignore next */
if (observer && observer.disconnect) {
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 }, vnode) => {
// `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 (onlyDgitsRE.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, vnode)
// 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
}