react-slips-hook
Version:
Display and layer pages as slips in Gatsby
216 lines (215 loc) • 8.88 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.useSlip = exports.useSlips = exports.useSlipsProvider = void 0;
const react_1 = require("react");
const gatsby_1 = require("gatsby");
const querystring_1 = __importDefault(require("querystring"));
const lodash_throttle_1 = __importDefault(require("lodash.throttle"));
const lodash_isequal_1 = __importDefault(require("lodash.isequal"));
const contexts_1 = require("./contexts");
const throttleTime = 16;
const obstructedOffset = 120;
function useScroll() {
const containerRef = (0, react_1.useRef)(null);
const [scroll, setScroll] = (0, react_1.useState)(0);
const [width, setWidth] = (0, react_1.useState)(0);
const scrollObserver = (0, react_1.useCallback)(() => {
if (!containerRef.current) {
return;
}
setScroll(containerRef.current.scrollLeft);
setWidth(containerRef.current.getBoundingClientRect().width);
}, [setScroll, setWidth, containerRef]);
const throttledScrollObserver = (0, lodash_throttle_1.default)(scrollObserver, throttleTime);
const setRef = (0, react_1.useCallback)((node) => {
if (node) {
// When the ref is first set (after mounting)
node.addEventListener("scroll", throttledScrollObserver);
containerRef.current = node;
window.addEventListener("resize", throttledScrollObserver);
throttledScrollObserver(); // initialization
}
else if (containerRef.current) {
// When unmounting
containerRef.current.removeEventListener("scroll", throttledScrollObserver);
window.removeEventListener("resize", throttledScrollObserver);
}
}, []);
return [scroll, width, setRef, containerRef];
}
function getRoot(firstPage, processPageQuery) {
return firstPage
? [
processPageQuery
? {
data: processPageQuery(firstPage.data, firstPage.slug),
slug: firstPage.slug,
}
: firstPage,
]
: [];
}
function useSlipsProvider({ location, processPageQuery, firstPage, pageWidth = 625, obstructedPageWidth = 40, }) {
const previousFirstPage = (0, react_1.useRef)(firstPage);
const [scroll, containerWidth, setRef, containerRef] = useScroll();
const [slips, setSlips] = (0, react_1.useState)(getRoot(firstPage, processPageQuery));
const [slipStates, setSlipStates] = (0, react_1.useState)(firstPage
? {
[firstPage.slug]: {
obstructed: false,
highlighted: false,
overlay: scroll > pageWidth - obstructedOffset,
active: true,
},
}
: {});
const slipSlugs = (0, react_1.useMemo)(() => {
const res = querystring_1.default.parse(location.search.replace(/^\?/, "")).slips || [];
if (typeof res === "string") {
return [res];
}
return res;
}, [location]);
(0, react_1.useEffect)(() => {
if ((0, lodash_isequal_1.default)(firstPage, previousFirstPage.current)) {
return;
}
setSlips((pages) => {
return getRoot(firstPage, processPageQuery).concat(previousFirstPage.current ? pages.slice(1) : pages);
});
previousFirstPage.current = firstPage;
}, [firstPage, processPageQuery, setSlips]);
(0, react_1.useEffect)(() => {
if (!window.___loader) {
throw new Error("`react-slips-hook` can only be used with Gatsby");
}
Promise.all(
// hook into the internals of Gatsby to dynamically fetch the notes
slipSlugs.map((slug) => window.___loader.loadPage(slug))).then((data) => setSlips(getRoot(firstPage, processPageQuery).concat(
// filter out 404s
data
.map((x, i) => ({
slug: slipSlugs[i],
data: processPageQuery
? processPageQuery(x.json.data, slipSlugs[i])
: x,
}))
.filter((x) => x.data))));
}, [slipSlugs]);
(0, react_1.useEffect)(() => {
if (containerRef.current) {
containerRef.current.scrollTo({
top: 0,
left: pageWidth * (slips.length + 1),
behavior: "smooth",
});
}
}, [slips, containerRef]);
// on scroll or on new page
(0, react_1.useEffect)(() => {
const acc = {};
if (!containerRef.current) {
setSlipStates(slips.reduce((prev, x, i, a) => {
prev[x.slug] = {
overlay: true,
obstructed: false,
highlighted: false,
active: i === a.length - 1,
};
return prev;
}, acc));
return;
}
setSlipStates(slips.reduce((prev, x, i, a) => {
prev[x.slug] = {
highlighted: false,
overlay: scroll >
Math.max(pageWidth * (i - 1) - (obstructedPageWidth * i - 2), 0) || scroll < Math.max(0, pageWidth * (i - 2)),
obstructed: scroll >
Math.max(pageWidth * (i + 1) -
obstructedOffset -
obstructedPageWidth * (i - 1), 0) || scroll + containerWidth < pageWidth * i + obstructedOffset,
active: i === a.length - 1,
};
return prev;
}, acc));
}, [slips, containerRef, scroll, setSlipStates]);
const navigateToSlip = (0, react_1.useCallback)((to, index = 0) => {
const existingPage = slips.findIndex((x) => x.slug === to);
if (existingPage !== -1 && containerRef && containerRef.current) {
setSlipStates((slipStates) => {
if (!slipStates[to]) {
return slipStates;
}
return Object.keys(slipStates).reduce((prev, slug) => {
prev[slug] = Object.assign(Object.assign({}, slipStates[slug]), { highlighted: false, active: slug === to });
return prev;
}, {});
});
containerRef.current.scrollTo({
top: 0,
left: pageWidth * existingPage - (obstructedPageWidth * existingPage - 1),
behavior: "smooth",
});
return;
}
const search = querystring_1.default.parse(window.location.search.replace(/^\?/, ""));
search.slips = slips
.slice(1, index + 1)
.map((x) => x.slug)
.concat(to);
(0, gatsby_1.navigate)(`${window.location.pathname.replace((0, gatsby_1.withPrefix)("/"), "/")}?${querystring_1.default.stringify(search)}`.replace(/^\/\//, "/"));
}, [slips, setSlipStates]);
const highlightSlip = (0, react_1.useCallback)((slug, highlighted) => {
setSlipStates((slipStates) => {
if (!slipStates[slug]) {
return slipStates;
}
return Object.assign(Object.assign({}, slipStates), { [slug]: Object.assign(Object.assign({}, slipStates[slug]), { highlighted: typeof highlighted !== "undefined"
? highlighted
: !slipStates[slug].highlighted }) });
});
}, [setSlipStates]);
const contextValue = (0, react_1.useMemo)(() => ({
slips,
navigateToSlip,
highlightSlip,
slipStates,
}), [
slips,
navigateToSlip,
highlightSlip,
slipStates,
]);
return [contextValue, setRef];
}
exports.useSlipsProvider = useSlipsProvider;
function useSlips() {
const { slips, slipStates, navigateToSlip, highlightSlip, } = (0, react_1.useContext)(contexts_1.SlipContext);
const index = (0, react_1.useContext)(contexts_1.SlipIndexContext);
const hookedNavigateToSlip = (0, react_1.useCallback)((to) => navigateToSlip(to, index), [navigateToSlip, index]);
return [
slips,
slipStates,
hookedNavigateToSlip,
highlightSlip,
];
}
exports.useSlips = useSlips;
function useSlip() {
const { slips, slipStates, navigateToSlip, highlightSlip, } = (0, react_1.useContext)(contexts_1.SlipContext);
const index = (0, react_1.useContext)(contexts_1.SlipIndexContext);
const hookedNavigateToSlip = (0, react_1.useCallback)((to) => navigateToSlip(to, index), [navigateToSlip, index]);
const currentPage = slips[index];
return [
currentPage,
currentPage ? slipStates[currentPage.slug] : {},
index,
hookedNavigateToSlip,
highlightSlip,
];
}
exports.useSlip = useSlip;