UNPKG

@elacity-js/uikit

Version:

React / Material UI Design kit for Elacity project

235 lines (207 loc) 7.8 kB
import * as React from 'react'; const observerMap = new Map(); const RootIds = new WeakMap(); let rootId = 0; let unsupportedValue = undefined; /** * Generate a unique ID for the root element * @param root */ function getRootId(root) { if (!root) return '0'; if (RootIds.has(root)) return RootIds.get(root); rootId += 1; RootIds.set(root, rootId.toString()); return RootIds.get(root); } /** * Convert the options to a string Id, based on the values. * Ensures we can reuse the same observer when observing elements with the same options. * @param options */ function optionsToId(options) { return Object.keys(options).sort().filter(key => options[key] !== undefined).map(key => { return `${key}_${key === 'root' ? getRootId(options.root) : options[key]}`; }).toString(); } function createObserver(options) { // Create a unique ID for this observer instance, based on the root, root margin and threshold. let id = optionsToId(options); let instance = observerMap.get(id); if (!instance) { // Create a map of elements this observer is going to observe. Each element has a list of callbacks that should be triggered, once it comes into view. const elements = new Map(); let thresholds; const observer = new IntersectionObserver(entries => { entries.forEach(entry => { var _elements$get; // While it would be nice if you could just look at isIntersecting to determine if the component is inside the viewport, browsers can't agree on how to use it. // -Firefox ignores `threshold` when considering `isIntersecting`, so it will never be false again if `threshold` is > 0 const inView = entry.isIntersecting && thresholds.some(threshold => entry.intersectionRatio >= threshold); // @ts-ignore support IntersectionObserver v2 if (options.trackVisibility && typeof entry.isVisible === 'undefined') { // The browser doesn't support Intersection Observer v2, falling back to v1 behavior. // @ts-ignore entry.isVisible = inView; } (_elements$get = elements.get(entry.target)) == null ? void 0 : _elements$get.forEach(callback => { callback(inView, entry); }); }); }, options); // Ensure we have a valid thresholds array. If not, use the threshold from the options thresholds = observer.thresholds || (Array.isArray(options.threshold) ? options.threshold : [options.threshold || 0]); instance = { id, observer, elements }; observerMap.set(id, instance); } return instance; } /** * @param element - DOM Element to observe * @param callback - Callback function to trigger when intersection status changes * @param options - Intersection Observer options * @param fallbackInView - Fallback inView value. * @return Function - Cleanup function that should be triggered to unregister the observer */ function observe(element, callback, options = {}, fallbackInView = unsupportedValue) { if (typeof window.IntersectionObserver === 'undefined' && fallbackInView !== undefined) { const bounds = element.getBoundingClientRect(); callback(fallbackInView, { isIntersecting: fallbackInView, target: element, intersectionRatio: typeof options.threshold === 'number' ? options.threshold : 0, time: 0, boundingClientRect: bounds, intersectionRect: bounds, rootBounds: bounds }); return () => {// Nothing to cleanup }; } // An observer with the same options can be reused, so lets use this fact const { id, observer, elements } = createObserver(options); // Register the callback listener for this element let callbacks = elements.get(element) || []; if (!elements.has(element)) { elements.set(element, callbacks); } callbacks.push(callback); observer.observe(element); return function unobserve() { // Remove the callback from the callback list callbacks.splice(callbacks.indexOf(callback), 1); if (callbacks.length === 0) { // No more callback exists for element, so destroy it elements.delete(element); observer.unobserve(element); } if (elements.size === 0) { // No more elements are being observer by this instance, so destroy it observer.disconnect(); observerMap.delete(id); } }; } /** * React Hooks make it easy to monitor the `inView` state of your components. Call * the `useInView` hook with the (optional) [options](#options) you need. It will * return an array containing a `ref`, the `inView` status and the current * [`entry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry). * Assign the `ref` to the DOM element you want to monitor, and the hook will * report the status. * * @example * ```jsx * import React from 'react'; * import { useInView } from 'react-intersection-observer'; * * const Component = () => { * const { ref, inView, entry } = useInView({ * threshold: 0, * }); * * return ( * <div ref={ref}> * <h2>{`Header inside viewport ${inView}.`}</h2> * </div> * ); * }; * ``` */ function useInView({ threshold, delay, trackVisibility, rootMargin, root, triggerOnce, skip, initialInView, fallbackInView, onChange } = {}) { var _state$entry; const [ref, setRef] = React.useState(null); const callback = React.useRef(); const [state, setState] = React.useState({ inView: !!initialInView, entry: undefined }); // Store the onChange callback in a `ref`, so we can access the latest instance // inside the `useEffect`, but without triggering a rerender. callback.current = onChange; React.useEffect(() => { // Ensure we have node ref, and that we shouldn't skip observing if (skip || !ref) return; let unobserve = observe(ref, (inView, entry) => { setState({ inView, entry }); if (callback.current) callback.current(inView, entry); if (entry.isIntersecting && triggerOnce && unobserve) { // If it should only trigger once, unobserve the element after it's inView unobserve(); unobserve = undefined; } }, { root, rootMargin, threshold, // @ts-ignore trackVisibility, // @ts-ignore delay }, fallbackInView); return () => { if (unobserve) { unobserve(); } }; }, // We break the rule here, because we aren't including the actual `threshold` variable // eslint-disable-next-line react-hooks/exhaustive-deps [// If the threshold is an array, convert it to a string, so it won't change between renders. // eslint-disable-next-line react-hooks/exhaustive-deps Array.isArray(threshold) ? threshold.toString() : threshold, ref, root, rootMargin, triggerOnce, skip, trackVisibility, fallbackInView, delay]); const entryTarget = (_state$entry = state.entry) == null ? void 0 : _state$entry.target; React.useEffect(() => { if (!ref && entryTarget && !triggerOnce && !skip) { // If we don't have a node ref, then reset the state (unless the hook is set to only `triggerOnce` or `skip`) // This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView setState({ inView: !!initialInView, entry: undefined }); } }, [ref, entryTarget, triggerOnce, skip, initialInView]); const result = [setRef, state.inView, state.entry]; // Support object destructuring, by adding the specific values. result.ref = result[0]; result.inView = result[1]; result.entry = result[2]; return result; } export { observe, useInView }; //# sourceMappingURL=react-intersection-observer.modern.mjs.js.map