188 lines
5.2 KiB
TypeScript
188 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;
|