react-scrollama
Version:
A lightweight scrollytelling interface for React using the IntersectionObserver.
466 lines (458 loc) • 15.4 kB
JavaScript
import * as React from 'react';
import React__default, { useState, useEffect, useMemo, useRef, useCallback, cloneElement } from 'react';
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _arrayWithHoles(r) {
if (Array.isArray(r)) return r;
}
function _defineProperty(e, r, t) {
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: true,
configurable: true,
writable: true
}) : e[r] = t, e;
}
function _iterableToArrayLimit(r, l) {
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (null != t) {
var e,
n,
i,
u,
a = [],
f = true,
o = false;
try {
if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
} catch (r) {
o = true, n = r;
} finally {
try {
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
} finally {
if (o) throw n;
}
}
return a;
}
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function ownKeys(e, r) {
var t = Object.keys(e);
if (Object.getOwnPropertySymbols) {
var o = Object.getOwnPropertySymbols(e);
r && (o = o.filter(function (r) {
return Object.getOwnPropertyDescriptor(e, r).enumerable;
})), t.push.apply(t, o);
}
return t;
}
function _objectSpread2(e) {
for (var r = 1; r < arguments.length; r++) {
var t = null != arguments[r] ? arguments[r] : {};
r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
_defineProperty(e, r, t[r]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
});
}
return e;
}
function _slicedToArray(r, e) {
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
}
function _toPrimitive(t, r) {
if ("object" != typeof t || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r);
if ("object" != typeof i) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
var isOffsetInPixels = function isOffsetInPixels(offset) {
return typeof offset === 'string' && offset.includes('px');
};
var markerStyles = {
position: 'fixed',
left: 0,
width: '100%',
height: 0,
borderTop: '2px dashed black',
zIndex: 9999
};
var offsetTextStyles = {
fontSize: '12px',
fontFamily: 'monospace',
margin: 0,
padding: 6
};
var useTop = function useTop(offset) {
var offsetInPixels = isOffsetInPixels(offset);
if (offsetInPixels) {
return offset;
} else {
return "".concat(offset * 100, "%");
}
};
var DebugOffset = function DebugOffset(_ref) {
var offset = _ref.offset;
var top = useTop(offset);
return /*#__PURE__*/React__default.createElement("div", {
style: _objectSpread2(_objectSpread2({}, markerStyles), {}, {
top: top
})
}, /*#__PURE__*/React__default.createElement("p", {
style: offsetTextStyles
}, "trigger: ", offset));
};
var createThreshold = function createThreshold(theta, height) {
var count = Math.ceil(height / theta);
var t = [];
var ratio = 1 / count;
for (var i = 0; i <= count; i += 1) {
t.push(i * ratio);
}
return t;
};
var Scrollama = function Scrollama(props) {
var debug = props.debug,
children = props.children,
_props$offset = props.offset,
offset = _props$offset === void 0 ? 0.3 : _props$offset,
_props$onStepEnter = props.onStepEnter,
onStepEnter = _props$onStepEnter === void 0 ? function () {} : _props$onStepEnter,
_props$onStepExit = props.onStepExit,
onStepExit = _props$onStepExit === void 0 ? function () {} : _props$onStepExit,
_props$onStepProgress = props.onStepProgress,
onStepProgress = _props$onStepProgress === void 0 ? null : _props$onStepProgress,
_props$threshold = props.threshold,
threshold = _props$threshold === void 0 ? 4 : _props$threshold;
var isOffsetDefinedInPixels = isOffsetInPixels(offset);
var _useState = useState(0),
_useState2 = _slicedToArray(_useState, 2),
lastScrollTop = _useState2[0],
setLastScrollTop = _useState2[1];
var _useState3 = useState(null),
_useState4 = _slicedToArray(_useState3, 2),
windowInnerHeight = _useState4[0],
setWindowInnerHeight = _useState4[1];
var handleSetLastScrollTop = function handleSetLastScrollTop(scrollTop) {
setLastScrollTop(scrollTop);
};
var handleWindowResize = function handleWindowResize(e) {
setWindowInnerHeight(window.innerHeight);
};
useEffect(function () {
if (isOffsetDefinedInPixels) {
window.addEventListener('resize', handleWindowResize);
return function () {
window.removeEventListener('resize', handleWindowResize);
};
}
}, []);
var isBrowser = typeof window !== 'undefined';
var innerHeight = isBrowser ? windowInnerHeight || window.innerHeight : 0;
var offsetValue = isOffsetDefinedInPixels ? +offset.replace('px', '') / innerHeight : offset;
var progressThreshold = useMemo(function () {
return createThreshold(threshold, innerHeight);
}, [innerHeight]);
return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, debug && /*#__PURE__*/React__default.createElement(DebugOffset, {
offset: offset
}), React__default.Children.map(children, function (child, i) {
return /*#__PURE__*/React__default.cloneElement(child, {
scrollamaId: "react-scrollama-".concat(i),
offset: offsetValue,
onStepEnter: onStepEnter,
onStepExit: onStepExit,
onStepProgress: onStepProgress,
lastScrollTop: lastScrollTop,
handleSetLastScrollTop: handleSetLastScrollTop,
progressThreshold: progressThreshold,
innerHeight: innerHeight
});
}));
};
// src/observe.ts
var observerMap = /* @__PURE__ */ new Map();
var RootIds = /* @__PURE__ */ new WeakMap();
var rootId = 0;
var unsupportedValue = void 0;
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);
}
};
}
function useInView({
threshold,
delay,
trackVisibility,
rootMargin,
root,
triggerOnce,
skip,
initialInView,
fallbackInView,
onChange
} = {}) {
var _a;
const [ref, setRef] = React.useState(null);
const callback = React.useRef(onChange);
const [state, setState] = React.useState({
inView: !!initialInView,
entry: void 0
});
callback.current = onChange;
React.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 = React.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;
}
var useRootMargin = function useRootMargin(offset) {
return "-".concat(offset * 100, "% 0px -").concat(100 - offset * 100, "% 0px");
};
var useProgressRootMargin = function useProgressRootMargin(direction, offset, node, innerHeight) {
if (!node.current) return '0px';
var offsetHeight = node.current.offsetHeight / innerHeight;
if (direction === 'down') return "".concat((offsetHeight - offset) * 100, "% 0px ").concat(offset * 100 - 100, "% 0px");
return "-".concat(offset * 100, "% 0px ").concat(offsetHeight * 100 - (100 - offset * 100), "% 0px");
};
var Step = function Step(props) {
var children = props.children,
data = props.data,
handleSetLastScrollTop = props.handleSetLastScrollTop,
lastScrollTop = props.lastScrollTop,
_props$onStepEnter = props.onStepEnter,
onStepEnter = _props$onStepEnter === void 0 ? function () {} : _props$onStepEnter,
_props$onStepExit = props.onStepExit,
onStepExit = _props$onStepExit === void 0 ? function () {} : _props$onStepExit,
_props$onStepProgress = props.onStepProgress,
onStepProgress = _props$onStepProgress === void 0 ? null : _props$onStepProgress,
offset = props.offset,
scrollamaId = props.scrollamaId,
progressThreshold = props.progressThreshold,
innerHeight = props.innerHeight;
var isBrowser = typeof window !== 'undefined';
var scrollTop = isBrowser ? document.documentElement.scrollTop : 0;
var direction = lastScrollTop >= scrollTop ? 'up' : 'down';
var rootMargin = useRootMargin(offset);
var ref = useRef(null);
var _useState = useState(false),
_useState2 = _slicedToArray(_useState, 2),
isIntersecting = _useState2[0],
setIsIntersecting = _useState2[1];
var _useInView = useInView({
rootMargin: rootMargin,
threshold: 0
}),
inViewRef = _useInView.ref,
entry = _useInView.entry;
var progressRootMargin = useMemo(function () {
return useProgressRootMargin(direction, offset, ref, innerHeight);
}, [direction, offset, ref, innerHeight]);
var _useInView2 = useInView({
rootMargin: progressRootMargin,
threshold: progressThreshold
}),
scrollProgressRef = _useInView2.ref,
scrollProgressEntry = _useInView2.entry;
var setRefs = useCallback(function (node) {
ref.current = node;
inViewRef(node);
scrollProgressRef(node);
}, [inViewRef, scrollProgressRef]);
useEffect(function () {
if (isIntersecting) {
var _scrollProgressEntry$ = scrollProgressEntry.target.getBoundingClientRect(),
height = _scrollProgressEntry$.height,
top = _scrollProgressEntry$.top;
var progress = Math.min(1, Math.max(0, (window.innerHeight * offset - top) / height));
onStepProgress && onStepProgress({
progress: progress,
scrollamaId: scrollamaId,
data: data,
element: scrollProgressEntry.target,
entry: scrollProgressEntry,
direction: direction
});
}
}, [scrollProgressEntry]);
useEffect(function () {
if (entry && !entry.isIntersecting && isIntersecting) {
onStepExit({
element: entry.target,
scrollamaId: scrollamaId,
data: data,
entry: entry,
direction: direction
});
setIsIntersecting(false);
handleSetLastScrollTop(scrollTop);
} else if (entry && entry.isIntersecting && !isIntersecting) {
setIsIntersecting(true);
onStepEnter({
element: entry.target,
scrollamaId: scrollamaId,
data: data,
entry: entry,
direction: direction
});
handleSetLastScrollTop(scrollTop);
}
}, [entry]);
return /*#__PURE__*/cloneElement(React__default.Children.only(children), {
'data-react-scrollama-id': scrollamaId,
ref: setRefs,
entry: entry
});
};
export { Scrollama, Step };
//# sourceMappingURL=index.es.js.map