/** Reachable table actions for compact and coarse-pointer layouts. */ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import { $getTableCellNodeFromLexicalNode, $getTableColumnIndexFromTableCellNode, $getTableNodeFromLexicalNodeOrThrow, $getTableRowIndexFromTableCellNode, } from "@lexical/table"; import { $getSelection, $isRangeSelection } from "lexical"; import { planScroller } from "../scroll"; import { placeSurface } from "../toolbar/placement"; import { editorSurfaceViewport, listenToEditorGeometry } from "../toolbar/surface"; import { $addColumn, $addRow, $describe, $moveColumn, $moveRow, $removeColumn, $removeRow, $setAlign, } from "./ops"; import { alignmentLabel, nextAlign } from "./alignment"; import { touchAvailability } from "./touch-actions"; import type { LexicalEditor, NodeKey } from "lexical"; import type { SurfacePlacement } from "../toolbar/placement"; import type { Table } from "./ops"; type SelectedCell = { cell: NodeKey; column: number; row: number; table: Table }; type DockPlacement = SurfacePlacement & { width: number }; type ActionGroup = "add" | "move" | "remove"; export function TableActionToolbar( { editor, disabled }: { editor: LexicalEditor; disabled?: boolean }, ) { let [selected, setSelected] = useState(); let [position, setPosition] = useState(); let [group, setGroup] = useState("add"); let surface = useRef(null); let sync = useCallback(() => { if (disabled) return setSelected(undefined); editor.getEditorState().read(() => { let selection = $getSelection(); if (!$isRangeSelection(selection)) return setSelected(undefined); let cell = $getTableCellNodeFromLexicalNode(selection.anchor.getNode()); if (!cell) return setSelected(undefined); let table = $getTableNodeFromLexicalNodeOrThrow(cell); setSelected({ cell: cell.getKey(), column: $getTableColumnIndexFromTableCellNode(cell), row: $getTableRowIndexFromTableCellNode(cell), table: $describe(table), }); }); }, [disabled, editor]); useEffect(() => { sync(); let off = editor.registerUpdateListener(sync); let root = editor.getRootElement(); let frame = 0; let afterPointer = () => { cancelAnimationFrame(frame); frame = requestAnimationFrame(sync); }; root?.addEventListener("pointerup", afterPointer); root?.addEventListener("focusin", afterPointer); return () => { cancelAnimationFrame(frame); off(); root?.removeEventListener("pointerup", afterPointer); root?.removeEventListener("focusin", afterPointer); }; }, [editor, sync]); let place = useCallback(() => { let element = surface.current; let cell = selected && editor.getElementByKey(selected.cell); if (!element || !cell) return; let viewport = editorSurfaceViewport(editor); let width = Math.max(0, viewport.width - 16); let bottom = viewport.top + viewport.height; // The previous group's maxHeight can constrain a newly expanded action panel. let height = Math.max( element.offsetHeight, element.scrollHeight + element.offsetHeight - element.clientHeight, ); let next = placeSurface( { left: viewport.left, right: viewport.left, top: bottom, bottom, width: 0, height: 0 }, { width, height }, viewport, ); let dock = { ...next, width }; setPosition(current => current?.left === dock.left && current.top === dock.top && current.maxHeight === dock.maxHeight && current.width === dock.width ? current : dock ); let cellBox = cell.getBoundingClientRect(); if (cellBox.bottom > dock.top - 8 || cellBox.top < viewport.top + 8) { cell.scrollIntoView({ block: "center", inline: "nearest" }); } }, [editor, selected]); useLayoutEffect(place, [group, place]); useEffect(() => { if (!selected) return; return listenToEditorGeometry(editor, place); }, [place, selected]); useLayoutEffect(() => { if (!selected) return; let element = surface.current; let scroller = planScroller(editor.getRootElement()); if (!element || !scroller) return; let previous = scroller.style.scrollPaddingBottom; scroller.style.scrollPaddingBottom = `${element.offsetHeight + 16}px`; return () => { scroller.style.scrollPaddingBottom = previous; }; }, [editor, group, selected]); if (disabled || !selected) return null; let { column, row, table } = selected; let act = (op: () => void) => editor.update(op); let align = table.align[column] ?? null; let button = "plan-table-action"; let available = touchAvailability(table.shape, row, column); let show = (next: ActionGroup) => setGroup(next); return (
event.preventDefault()} ref={surface} role="toolbar" style={position ? { left: position.left, top: position.top, maxHeight: position.maxHeight, width: position.width, } : { left: 8, top: 8, visibility: "hidden" }} >
{group === "add" && (
)} {group === "remove" && (
)} {group === "move" && (
)}
); }