react-intersection-observer
Version:
Monitor if a component is inside the viewport, using IntersectionObserver API
293 lines (289 loc) • 7.99 kB
JavaScript
"use client";
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/InView.tsx
import * as React from "react";
// src/observe.ts
var observerMap = /* @__PURE__ */ new Map();
var RootIds = /* @__PURE__ */ new WeakMap();
var rootId = 0;
var unsupportedValue = void 0;
function defaultFallbackInView(inView) {
unsupportedValue = inView;
}
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);
}
function optionsToId(options) {
return Object.keys(options).sort().filter(
(key) => options[key] !== void 0
).map((key) => {
return `${key}_${key === "root" ? getRootId(options.root) : options[key]}`;
}).toString();
}
function createObserver(options) {
const id = optionsToId(options);
let instance = observerMap.get(id);
if (!instance) {
const elements = /* @__PURE__ */ new Map();
let thresholds;
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
var _a;
const inView = entry.isIntersecting && thresholds.some((threshold) => entry.intersectionRatio >= threshold);
if (options.trackVisibility && typeof entry.isVisible === "undefined") {
entry.isVisible = inView;
}
(_a = elements.get(entry.target)) == null ? void 0 : _a.forEach((callback) => {
callback(inView, entry);
});
});
}, options);
thresholds = observer.thresholds || (Array.isArray(options.threshold) ? options.threshold : [options.threshold || 0]);
instance = {
id,
observer,
elements
};
observerMap.set(id, instance);
}
return instance;
}
function observe(element, callback, options = {}, fallbackInView = unsupportedValue) {
if (typeof window.IntersectionObserver === "undefined" && fallbackInView !== void 0) {
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 () => {
};
}
const { id, observer, elements } = createObserver(options);
const callbacks = elements.get(element) || [];
if (!elements.has(element)) {
elements.set(element, callbacks);
}
callbacks.push(callback);
observer.observe(element);
return function unobserve() {
callbacks.splice(callbacks.indexOf(callback), 1);
if (callbacks.length === 0) {
elements.delete(element);
observer.unobserve(element);
}
if (elements.size === 0) {
observer.disconnect();
observerMap.delete(id);
}
};
}
// src/InView.tsx
function isPlainChildren(props) {
return typeof props.children !== "function";
}
var InView = class extends React.Component {
constructor(props) {
super(props);
__publicField(this, "node", null);
__publicField(this, "_unobserveCb", null);
__publicField(this, "handleNode", (node) => {
if (this.node) {
this.unobserve();
if (!node && !this.props.triggerOnce && !this.props.skip) {
this.setState({ inView: !!this.props.initialInView, entry: void 0 });
}
}
this.node = node ? node : null;
this.observeNode();
});
__publicField(this, "handleChange", (inView, entry) => {
if (inView && this.props.triggerOnce) {
this.unobserve();
}
if (!isPlainChildren(this.props)) {
this.setState({ inView, entry });
}
if (this.props.onChange) {
this.props.onChange(inView, entry);
}
});
this.state = {
inView: !!props.initialInView,
entry: void 0
};
}
componentDidMount() {
this.unobserve();
this.observeNode();
}
componentDidUpdate(prevProps) {
if (prevProps.rootMargin !== this.props.rootMargin || prevProps.root !== this.props.root || prevProps.threshold !== this.props.threshold || prevProps.skip !== this.props.skip || prevProps.trackVisibility !== this.props.trackVisibility || prevProps.delay !== this.props.delay) {
this.unobserve();
this.observeNode();
}
}
componentWillUnmount() {
this.unobserve();
}
observeNode() {
if (!this.node || this.props.skip) return;
const {
threshold,
root,
rootMargin,
trackVisibility,
delay,
fallbackInView
} = this.props;
this._unobserveCb = observe(
this.node,
this.handleChange,
{
threshold,
root,
rootMargin,
// @ts-ignore
trackVisibility,
// @ts-ignore
delay
},
fallbackInView
);
}
unobserve() {
if (this._unobserveCb) {
this._unobserveCb();
this._unobserveCb = null;
}
}
render() {
const { children } = this.props;
if (typeof children === "function") {
const { inView, entry } = this.state;
return children({ inView, entry, ref: this.handleNode });
}
const {
as,
triggerOnce,
threshold,
root,
rootMargin,
onChange,
skip,
trackVisibility,
delay,
initialInView,
fallbackInView,
...props
} = this.props;
return React.createElement(
as || "div",
{ ref: this.handleNode, ...props },
children
);
}
};
// src/useInView.tsx
import * as React2 from "react";
function useInView({
threshold,
delay,
trackVisibility,
rootMargin,
root,
triggerOnce,
skip,
initialInView,
fallbackInView,
onChange
} = {}) {
var _a;
const [ref, setRef] = React2.useState(null);
const callback = React2.useRef(onChange);
const [state, setState] = React2.useState({
inView: !!initialInView,
entry: void 0
});
callback.current = onChange;
React2.useEffect(
() => {
if (skip || !ref) return;
let unobserve;
unobserve = observe(
ref,
(inView, entry) => {
setState({
inView,
entry
});
if (callback.current) callback.current(inView, entry);
if (entry.isIntersecting && triggerOnce && unobserve) {
unobserve();
unobserve = void 0;
}
},
{
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.
Array.isArray(threshold) ? threshold.toString() : threshold,
ref,
root,
rootMargin,
triggerOnce,
skip,
trackVisibility,
fallbackInView,
delay
]
);
const entryTarget = (_a = state.entry) == null ? void 0 : _a.target;
const previousEntryTarget = React2.useRef(void 0);
if (!ref && entryTarget && !triggerOnce && !skip && previousEntryTarget.current !== entryTarget) {
previousEntryTarget.current = entryTarget;
setState({
inView: !!initialInView,
entry: void 0
});
}
const result = [setRef, state.inView, state.entry];
result.ref = result[0];
result.inView = result[1];
result.entry = result[2];
return result;
}
export {
InView,
defaultFallbackInView,
observe,
useInView
};
//# sourceMappingURL=index.mjs.map