logseq-calendar/lib/ScrollContainer.tsx
2023-08-24 17:21:48 -04:00

189 lines
5.2 KiB
TypeScript

import {
Fragment,
Key,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import styles from "./ScrollContainer.module.scss";
import classNames from "classnames";
import { getWeek } from "date-fns";
import { CalendarContext } from "src/calendar/Calendar";
import { monthNameOf, weekString } from "./date";
interface ScrollContainerProps<T> {
direction: "horizontal" | "vertical";
className?: string;
/** A list of things managed */
list: T[];
renderItem: (_: T) => JSX.Element;
keyOf: (_: T) => Key;
generatePreviousObject: (_: HTMLElement) => void;
generateNextObject: (_: HTMLElement) => void;
}
interface OldScrollState {
el: HTMLElement;
offsetLeft: number;
offsetTop: number;
}
/**
* Generic infinite scrolling container
*/
function ScrollContainer<T>({
direction,
className,
list,
renderItem,
generateNextObject,
generatePreviousObject,
keyOf,
}: ScrollContainerProps<T>) {
const [resetScrollToEl, setResetScrollToEl] = useState<OldScrollState | null>(
null
);
// Viewport state
const { viewportWidth, viewportHeight, setViewportSize } =
useContext(CalendarContext);
const viewportMetric =
direction === "horizontal" ? viewportWidth : viewportHeight;
// Scroll element state
const [scrollyEl, setScrollyEl] = useState<HTMLDivElement | null>(null);
const scrollyRef = useCallback((node: HTMLDivElement) => {
if (node) {
setScrollyEl(node);
setViewportSize([node.clientWidth, node.clientHeight]);
}
}, []);
// Scroll to middle on first load
const [firstLoad, setFirstLoad] = useState(true);
// Add resize observer on the viewport
useEffect(() => {
const observer = new ResizeObserver((entries) => {
if (entries && scrollyEl) {
setViewportSize([scrollyEl.clientWidth, scrollyEl.clientHeight]);
}
});
if (scrollyEl) observer.observe(scrollyEl);
return () => {
if (scrollyEl) observer.unobserve(scrollyEl);
};
}, [scrollyEl]);
// Add intersection observer to tell when we scrolled too far
useEffect(() => {
if (viewportWidth === 0 || viewportHeight === 0) return;
if (!scrollyEl) return;
if (firstLoad) {
const scrollLen = scrollyEl.children.length;
const middleIdx = (scrollLen + 1) / 2 - 1;
console.log("middle idx", middleIdx);
// Identify the middle month
const el = scrollyEl.children[middleIdx];
if (el) {
// const date = new Date(el.dataset.isodate!);
// console.log("first run, scrolling into view", date.getMonth());
el.scrollIntoView();
setFirstLoad(false);
}
return;
}
const children = [...scrollyEl.children] as HTMLElement[];
const firstChild = children[0];
const lastChild = children[children.length - 1];
const observer = new IntersectionObserver((entries) => {
if (viewportMetric === 0) return;
for (const entry of entries) {
if (!entry.isIntersecting) continue;
// const intersectionArea =
// entry.intersectionRect.width * entry.intersectionRect.height;
// if (intersectionArea === 0) continue;
// const date = new Date(entry.target.dataset.isodate!);
// console.log("intersected with", weekString(date), entry);
if (entry.target === firstChild) {
const newResetScrollToEl = {
el: firstChild,
offsetLeft: scrollyEl.scrollLeft - firstChild.offsetLeft,
offsetTop: scrollyEl.scrollTop - firstChild.offsetTop,
};
setResetScrollToEl(newResetScrollToEl);
generatePreviousObject(firstChild);
}
if (entry.target === lastChild) {
console.log("child offset left", lastChild.offsetLeft);
const newResetScrollToEl = {
el: lastChild,
offsetLeft: scrollyEl.scrollLeft - lastChild.offsetLeft,
offsetTop: scrollyEl.scrollTop - lastChild.offsetTop,
};
setResetScrollToEl(newResetScrollToEl);
generateNextObject(lastChild);
}
}
});
// const firstDate = new Date(firstChild.dataset.isodate!);
// const lastDate = new Date(lastChild.dataset.isodate!);
// console.log("observing", weekString(firstDate), weekString(lastDate));
observer.observe(firstChild);
observer.observe(lastChild);
return () => {
observer.unobserve(firstChild);
observer.unobserve(lastChild);
};
}, [firstLoad, list, scrollyEl, viewportHeight]);
useEffect(() => {
if (!resetScrollToEl) return;
if (!scrollyEl) return;
const { el, offsetLeft, offsetTop } = resetScrollToEl;
if (direction === "horizontal")
scrollyEl.scrollLeft = el.offsetLeft + offsetLeft;
else if (direction === "vertical")
scrollyEl.scrollTop = el.offsetTop + offsetTop;
setResetScrollToEl(null);
}, [resetScrollToEl, scrollyEl]);
return (
<>
{JSON.stringify([firstLoad, viewportWidth, viewportHeight])}
<div
className={classNames(styles.scrollyEl, styles[direction], className)}
ref={scrollyRef}
>
{list.map((x) => (
<Fragment key={keyOf(x)}>{renderItem(x)}</Fragment>
))}
</div>
</>
);
}
export default ScrollContainer;