Files
homarr/apps/nextjs/src/components/board/sections/grid/grid-preview-layer.tsx
T

137 lines
4.7 KiB
TypeScript

"use client";
import type { RefObject } from "react";
import { useCallback, useLayoutEffect, useRef, useSyncExternalStore } from "react";
import { getLayoutRowCount, getLogicalItemStyle, getLogicalTrackSize } from "~/components/board/layout";
import type { TransactionalGridState } from "./dnd";
import type { GridInteractionStore } from "./grid-interaction-store";
import { clearGridPreviewGeometry, createGridPreviewDomState, syncGridPreviewGeometry } from "./grid-preview-geometry";
import type { SectionGridPlacement } from "./use-grid-layout-actions";
export interface GridInteraction {
activeId: string;
sourceGridId: string;
targetGridId: string | null;
targetPlacement: SectionGridPlacement | null;
state: TransactionalGridState<SectionGridPlacement>;
mode: "drag" | "resize";
valid: boolean;
previewRevision: number;
}
interface GridPreviewLayerProps {
sectionId: string;
rowCount: number;
maxRowCount: number | null;
placements: readonly SectionGridPlacement[];
gridRef: RefObject<HTMLDivElement | null>;
entryElements: ReadonlyMap<string, HTMLElement>;
interactionStore: GridInteractionStore<GridInteraction>;
}
export const GridPreviewLayer = ({
sectionId,
rowCount,
maxRowCount,
placements,
gridRef,
entryElements,
interactionStore,
}: GridPreviewLayerProps) => {
const subscribe = useCallback(
(listener: () => void) => interactionStore.subscribeGrid(sectionId, listener),
[interactionStore, sectionId],
);
const getSnapshot = useCallback(() => interactionStore.getGridSnapshot(sectionId), [interactionStore, sectionId]);
const interaction = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
const previewGrid = interaction?.state.grids.find((grid) => grid.id === sectionId);
const previewStateRef = useRef(createGridPreviewDomState());
const placementsRef = useRef(placements);
const isDragging = interaction?.mode === "drag";
const isTargetGrid = isDragging && interaction.targetGridId === sectionId;
let targetPlacement: SectionGridPlacement | null = null;
if (interaction?.valid && interaction.targetGridId === sectionId) {
targetPlacement = interaction.targetPlacement;
}
const growsDuringDrag =
isDragging && (isTargetGrid || (interaction.targetGridId === null && interaction.sourceGridId === sectionId));
const previewRowCount = getLayoutRowCount(previewGrid?.placements ?? placements);
let renderedRowCount = rowCount;
if (maxRowCount === null && interaction) {
renderedRowCount = Math.max(rowCount, previewRowCount);
if (growsDuringDrag) renderedRowCount += 1;
}
useLayoutEffect(() => {
placementsRef.current = placements;
previewStateRef.current = syncGridPreviewGeometry({
elements: entryElements,
placements,
previewPlacements: previewGrid?.placements ?? null,
activeId: interaction?.activeId ?? null,
mode: interaction?.mode ?? null,
previous: previewStateRef.current,
});
const grid = gridRef.current;
if (!grid) return;
grid.dataset.dndDropTarget = String(isTargetGrid);
if (isTargetGrid) grid.dataset.dndDropValid = String(targetPlacement !== null);
else delete grid.dataset.dndDropValid;
if (interaction?.targetGridId === sectionId) {
grid.dataset.dndPreviewRevision = String(interaction.previewRevision);
} else {
delete grid.dataset.dndPreviewRevision;
}
grid.style.height = `${getLogicalTrackSize(renderedRowCount)}px`;
const viewport = grid.parentElement;
if (viewport?.hasAttribute("data-section-id")) {
viewport.style.setProperty("--board-grid-drag-height", `${getLogicalTrackSize(renderedRowCount)}px`);
}
}, [
entryElements,
gridRef,
interaction,
isTargetGrid,
placements,
previewGrid?.placements,
renderedRowCount,
sectionId,
targetPlacement,
]);
useLayoutEffect(
() => () => {
clearGridPreviewGeometry(entryElements, placementsRef.current, previewStateRef.current);
const grid = gridRef.current;
if (!grid) return;
grid.removeAttribute("data-dnd-drop-target");
grid.removeAttribute("data-dnd-drop-valid");
grid.removeAttribute("data-dnd-preview-revision");
grid.parentElement?.style.removeProperty("--board-grid-drag-height");
},
[entryElements, gridRef],
);
if (!targetPlacement) return null;
return (
<div
className="board-grid-placeholder"
style={getLogicalItemStyle(targetPlacement)}
data-grid-placeholder-for={interaction?.activeId}
data-grid-placeholder-mode={interaction?.mode}
data-grid-x={targetPlacement.x}
data-grid-y={targetPlacement.y}
data-grid-w={targetPlacement.w}
data-grid-h={targetPlacement.h}
aria-hidden="true"
/>
);
};