logseq-calendar/lib/ScrollContainer.tsx

103 lines
2.9 KiB
TypeScript
Raw Normal View History

2023-08-24 15:33:14 +00:00
import {
ForwardedRef,
forwardRef,
useCallback,
useEffect,
useState,
} from "react";
import styles from "./ScrollContainer.module.scss";
import { subMonths } from "date-fns";
interface ScrollContainerProps<T> {
direction: "horizontal" | "vertical";
/** A list of things managed */
list: T[];
generatePreviousObject: () => void;
generateNextObject: () => void;
}
/**
* Generic infinite scrolling container
*/
function ScrollContainer<T>(
{ direction, list }: ScrollContainerProps<T>,
ref: ForwardedRef<HTMLDivElement>
) {
const [resetScrollToEl, setResetScrollToEl] = useState<
[HTMLElement, number] | null
>(null);
// Viewport state
const [viewportSize, setViewportSize] = useState<[number, number]>([0, 0]);
const [viewportWidth, viewportHeight] = viewportSize;
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]);
}
}, []);
// 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 (!scrollyEl) return;
const children: HTMLElement[] = [...scrollyEl.children];
const firstChild = children[0];
const lastChild = children[children.length - 1];
const observer = new IntersectionObserver((entries) => {
if (viewportMetric === 0) return;
let newList = [...list];
for (const entry of entries) {
if (!entry.isIntersecting) continue;
if (entry.target === firstChild) {
console.log("intersected");
const firstChildDate = new Date(firstChild.dataset.isodate!);
const prevMonth = subMonths(firstChildDate, 1);
newList = [prevMonth, ...monthsShown];
newList = newMonthsShown.slice(0, NUM_MONTHS_SHOWN);
setResetScrollToEl([
firstChild,
scrollyEl.scrollTop - firstChild.offsetTop,
]);
} else if (entry.target === lastChild) {
const lastChildDate = new Date(lastChild.dataset.isodate!);
const nextMonth = addMonths(lastChildDate, 1);
newMonthsShown = [...monthsShown, nextMonth];
newMonthsShown = newMonthsShown.slice(-NUM_MONTHS_SHOWN);
}
}
setMonthsShown(newMonthsShown);
});
});
return <div className={styles.scrollyEl} ref={scrollyRef}></div>;
}
export default forwardRef(ScrollContainer);