"use client"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type Dispatch, type KeyboardEvent, type PointerEvent, type RefObject, type SetStateAction, } from "react"; import { resizeAdjacentColumns } from "@/lib/markdown/table-resize"; type TableGeometry = { boundaries: number[]; columnWidths: number[]; height: number; tableWidth: number; top: number; }; type DragState = { boundaryIndex: number; pointerId: number; restoreTableWidth: number | null; restoreWidths: number[] | null; startWidths: number[]; startX: number; tableWidth: number; }; function readGeometry(table: HTMLTableElement, wrapper: HTMLDivElement): TableGeometry | null { const cells = Array.from(table.rows.item(0)?.cells ?? []); const tableRect = table.getBoundingClientRect(); const wrapperRect = wrapper.getBoundingClientRect(); if (cells.length < 2 || tableRect.width <= 0 || tableRect.height <= 0) return null; const cellRects = cells.map((cell) => cell.getBoundingClientRect()); return { boundaries: cellRects .slice(0, -1) .map((rect) => rect.right - wrapperRect.left + wrapper.scrollLeft), columnWidths: cellRects.map((rect) => rect.width), height: tableRect.height, tableWidth: tableRect.width, top: tableRect.top - wrapperRect.top + wrapper.scrollTop, }; } function setDocumentResizeState(active: boolean) { document.body.style.cursor = active ? "col-resize" : ""; document.body.style.userSelect = active ? "none" : ""; } type TableRefs = { tableRef: RefObject; wrapperRef: RefObject; }; function useTableGeometry( enabled: boolean, refs: TableRefs, widthsRef: RefObject, reset: () => void, columnWidths: number[] | null, ) { const [geometry, setGeometry] = useState(null); const measure = useCallback(() => { const table = refs.tableRef.current; const wrapper = refs.wrapperRef.current; if (!enabled || !table || !wrapper) { if (!enabled && widthsRef.current) reset(); setGeometry(null); return; } const columnCount = table.rows.item(0)?.cells.length ?? 0; if (widthsRef.current && widthsRef.current.length !== columnCount) reset(); const next = readGeometry(table, wrapper); setGeometry(next); }, [enabled, refs, reset, widthsRef]); useLayoutEffect(() => measure(), [columnWidths, measure]); useLayoutEffect(() => { const table = refs.tableRef.current; const wrapper = refs.wrapperRef.current; if (!enabled || !table || !wrapper) return; const resizeObserver = new ResizeObserver(measure); resizeObserver.observe(table); resizeObserver.observe(wrapper); const mutationObserver = new MutationObserver(measure); mutationObserver.observe(table, { characterData: true, childList: true, subtree: true }); window.addEventListener("resize", measure); return () => { resizeObserver.disconnect(); mutationObserver.disconnect(); window.removeEventListener("resize", measure); }; }, [enabled, measure, refs]); return geometry; } type PointerResizeOptions = { enabled: boolean; fixedTableWidth: number | null; refs: TableRefs; setColumnWidths: (widths: number[] | null) => void; setFixedTableWidth: Dispatch>; widthsRef: RefObject; }; function usePointerResize({ enabled, fixedTableWidth, refs, setColumnWidths, setFixedTableWidth, widthsRef, }: PointerResizeOptions) { const dragRef = useRef(null); const [activeBoundary, setActiveBoundary] = useState(null); const finishDrag = useCallback( (event: PointerEvent, cancelled: boolean) => { const drag = dragRef.current; if (!drag || drag.pointerId !== event.pointerId) return; if (cancelled) { setColumnWidths(drag.restoreWidths); setFixedTableWidth(drag.restoreTableWidth); } dragRef.current = null; setActiveBoundary(null); setDocumentResizeState(false); if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } }, [setColumnWidths], ); useEffect( () => () => { if (!dragRef.current) return; dragRef.current = null; setDocumentResizeState(false); }, [], ); useEffect(() => { if (enabled || !dragRef.current) return; dragRef.current = null; setActiveBoundary(null); setDocumentResizeState(false); }, [enabled]); const startResize = useCallback( (boundaryIndex: number, event: PointerEvent) => { if (event.button !== 0) return; const table = refs.tableRef.current; const wrapper = refs.wrapperRef.current; if (!table || !wrapper) return; const measured = readGeometry(table, wrapper); if (!measured) return; event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId); dragRef.current = { boundaryIndex, pointerId: event.pointerId, restoreTableWidth: fixedTableWidth, restoreWidths: widthsRef.current ? [...widthsRef.current] : null, startWidths: measured.columnWidths, startX: event.clientX, tableWidth: measured.tableWidth, }; setActiveBoundary(boundaryIndex); setDocumentResizeState(true); }, [fixedTableWidth, refs, widthsRef], ); const moveResize = useCallback( (event: PointerEvent) => { const drag = dragRef.current; if (!drag || drag.pointerId !== event.pointerId) return; event.preventDefault(); const delta = event.clientX - drag.startX; if (delta === 0) return; setFixedTableWidth(drag.tableWidth); setColumnWidths(resizeAdjacentColumns(drag.startWidths, drag.boundaryIndex, delta)); }, [setColumnWidths, setFixedTableWidth], ); return { activeBoundary, finishDrag, moveResize, startResize }; } export function useMarkdownTableResize(enabled: boolean) { const wrapperRef = useRef(null); const tableRef = useRef(null); const refs = useMemo(() => ({ tableRef, wrapperRef }), []); const widthsRef = useRef(null); const [columnWidths, setColumnWidthsState] = useState(null); const [fixedTableWidth, setFixedTableWidth] = useState(null); const setColumnWidths = useCallback((widths: number[] | null) => { widthsRef.current = widths; setColumnWidthsState(widths); }, []); const reset = useCallback(() => { setColumnWidths(null); setFixedTableWidth(null); }, [setColumnWidths]); const geometry = useTableGeometry(enabled, refs, widthsRef, reset, columnWidths); const pointerResize = usePointerResize({ enabled, fixedTableWidth, refs, setColumnWidths, setFixedTableWidth, widthsRef, }); const resizeWithKeyboard = useCallback( (boundaryIndex: number, event: KeyboardEvent) => { if (event.key === "Enter") { event.preventDefault(); reset(); return; } if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return; const table = refs.tableRef.current; const wrapper = refs.wrapperRef.current; const measured = table && wrapper ? readGeometry(table, wrapper) : null; const widths = widthsRef.current ?? measured?.columnWidths; if (!widths || !measured) return; event.preventDefault(); setFixedTableWidth(fixedTableWidth ?? measured.tableWidth); setColumnWidths( resizeAdjacentColumns(widths, boundaryIndex, event.key === "ArrowRight" ? 8 : -8), ); }, [fixedTableWidth, refs, reset, setColumnWidths], ); return { ...pointerResize, columnWidths, fixedTableWidth, geometry, reset, resizeWithKeyboard, tableRef, wrapperRef, }; }