109 lines
4.6 KiB
TypeScript
109 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
import { createContext, useContext, useLayoutEffect, useRef, useState } from "react";
|
|
import type { CellComponentProps } from "react-spreadsheet";
|
|
|
|
import {
|
|
type ExtraSheetCell,
|
|
type ExtraSheetRange,
|
|
} from "@/lib/guides/extra-sheet";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export const ExtraSheetCellContext = createContext<{ merges: Map<string, ExtraSheetRange>; layoutVersion: number } | null>(null);
|
|
|
|
export function sheetCellElement(table: HTMLTableElement, row: number, column: number): HTMLTableCellElement | null {
|
|
return table.querySelector(`[data-sheet-row="${row}"][data-sheet-column="${column}"]`);
|
|
}
|
|
|
|
export function sheetRangeRect(table: HTMLTableElement, range: ExtraSheetRange) {
|
|
const first = sheetCellElement(table, range.start.row, range.start.column);
|
|
const last = sheetCellElement(table, range.end.row, range.end.column);
|
|
if (!first || !last) return null;
|
|
return {
|
|
top: first.offsetTop,
|
|
left: first.offsetLeft,
|
|
width: last.offsetLeft + last.offsetWidth - first.offsetLeft,
|
|
height: last.offsetTop + last.offsetHeight - first.offsetTop,
|
|
};
|
|
}
|
|
|
|
// Keep physical cells for react-spreadsheet's per-axis measurements. Only the
|
|
// anchor draws the merged content; covered cells remain empty layout slots.
|
|
export function ExtraSheetCellView(props: CellComponentProps<ExtraSheetCell>) {
|
|
const context = useContext(ExtraSheetCellContext)!;
|
|
const { row, column, active, selected, dragging, mode, data, DataViewer, setCellDimensions } = props;
|
|
const rootRef = useRef<HTMLTableCellElement>(null);
|
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
const merge = context.merges.get(`${row}:${column}`);
|
|
const covered = !!merge && (merge.start.row !== row || merge.start.column !== column);
|
|
const [size, setSize] = useState({ width: 0, height: 0, spacer: 36 });
|
|
|
|
useLayoutEffect(() => {
|
|
const root = rootRef.current;
|
|
if (!root) return;
|
|
const table = root.closest("table");
|
|
if (merge && !covered && table) {
|
|
const rect = sheetRangeRect(table, merge);
|
|
if (rect) {
|
|
const contentHeight = contentRef.current?.scrollHeight ?? 36;
|
|
const spacer = Math.max(36, contentHeight - (rect.height - root.offsetHeight));
|
|
setSize((previous) => previous.width === rect.width && previous.height === rect.height && previous.spacer === spacer
|
|
? previous : { width: rect.width, height: rect.height, spacer });
|
|
}
|
|
}
|
|
if (selected || active) {
|
|
setCellDimensions({ row, column }, {
|
|
top: root.offsetTop, left: root.offsetLeft,
|
|
width: root.offsetWidth, height: root.offsetHeight,
|
|
});
|
|
}
|
|
if (active && mode === "view" && !covered) root.focus();
|
|
}, [row, column, active, selected, mode, covered, merge, data, context, setCellDimensions, size.spacer]);
|
|
|
|
const target = merge?.start ?? { row, column };
|
|
return (
|
|
<td
|
|
ref={rootRef}
|
|
className={cn("Spreadsheet__cell", merge && "extra-sheet-merge-slot")}
|
|
data-sheet-row={row}
|
|
data-sheet-column={column}
|
|
style={{ verticalAlign: data?.verticalAlignment ?? "top" }}
|
|
tabIndex={covered ? -1 : 0}
|
|
aria-hidden={covered || undefined}
|
|
aria-rowspan={!covered && merge ? merge.end.row - row + 1 : undefined}
|
|
aria-colspan={!covered && merge ? merge.end.column - column + 1 : undefined}
|
|
onMouseDown={(event) => {
|
|
// Activation focuses the anchor. Browser default focus can otherwise
|
|
// blur the grid after the previous editor unmounts.
|
|
event.preventDefault();
|
|
if (mode !== "view") return;
|
|
if (event.shiftKey) props.select(target);
|
|
else props.activate(target);
|
|
}}
|
|
onMouseOver={() => { if (dragging) props.select(target); }}
|
|
>
|
|
{merge ? (
|
|
<>
|
|
<div aria-hidden="true" style={{ minHeight: covered ? 36 : size.spacer }} />
|
|
{!covered ? (
|
|
<div
|
|
className="extra-sheet-merged-cell"
|
|
style={{
|
|
width: size.width || undefined, minHeight: size.height || 36,
|
|
backgroundColor: data?.backgroundColor,
|
|
justifyContent: data?.verticalAlignment === "middle" ? "center" : data?.verticalAlignment === "bottom" ? "flex-end" : "flex-start",
|
|
}}
|
|
>
|
|
<div ref={contentRef}>
|
|
<DataViewer row={row} column={column} cell={data} evaluatedCell={props.evaluatedData} setCellData={props.setCellData} />
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</>
|
|
) : (
|
|
<DataViewer row={row} column={column} cell={data} evaluatedCell={props.evaluatedData} setCellData={props.setCellData} />
|
|
)}
|
|
</td>
|
|
);
|
|
}
|