UNPKG

@gitlab/ui

Version:
157 lines (147 loc) 4.42 kB
import { RX_DIGITS } from '../../constants/regex'; import { requestAF } from '../../utils/dom'; import { isFunction } from '../../utils/inspect'; import { looseEqual } from '../../utils/loose-equal'; import { keys, clone } from '../../utils/object'; import { nextTick } from '../../vue'; // v-b-visible 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, _ref) => { let { value, modifiers } = _ref; // `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, _ref2, vnode) => { let { value, oldValue, modifiers } = _ref2; // 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 }); } }; // When directive un-binds from element const unbind = el => { // Remove the observer destroy(el); }; // Export the directive const VBVisible = { bind, componentUpdated, unbind }; export { VBVisible };