@elacity-js/uikit
Version:
React / Material UI Design kit for Elacity project
235 lines (207 loc) • 7.8 kB
JavaScript
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