Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Table perf
Review or Edit in CodeSandboxOpen the branch in Web Editor • VS Code • Insiders |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 021c1c5660
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| { | ||
| "path": "src/registry/ui/table-node.tsx", | ||
| "content": "'use client';\n\nimport * as React from 'react';\n\nimport type * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\n\nimport { useDraggable, useDropLine } from '@platejs/dnd';\nimport {\n BlockSelectionPlugin,\n useBlockSelected,\n} from '@platejs/selection/react';\nimport { setCellBackground } from '@platejs/table';\nimport {\n TablePlugin,\n TableProvider,\n useTableBordersDropdownMenuContentState,\n useTableCellElement,\n useTableCellElementResizable,\n useTableElement,\n useTableMergeState,\n} from '@platejs/table/react';\nimport { PopoverAnchor } from '@radix-ui/react-popover';\nimport { cva } from 'class-variance-authority';\nimport {\n ArrowDown,\n ArrowLeft,\n ArrowRight,\n ArrowUp,\n CombineIcon,\n EraserIcon,\n Grid2X2Icon,\n GripVertical,\n PaintBucketIcon,\n SquareSplitHorizontalIcon,\n Trash2Icon,\n XIcon,\n} from 'lucide-react';\nimport {\n type TElement,\n type TTableCellElement,\n type TTableElement,\n type TTableRowElement,\n KEYS,\n PathApi,\n} from 'platejs';\nimport {\n type PlateElementProps,\n PlateElement,\n useComposedRef,\n useEditorPlugin,\n useEditorRef,\n useEditorSelector,\n useElement,\n useFocusedLast,\n usePluginOption,\n useReadOnly,\n useRemoveNodeButton,\n useSelected,\n withHOC,\n} from 'platejs/react';\nimport { useElementSelector } from 'platejs/react';\n\nimport { Button } from '@/components/ui/button';\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuPortal,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { Popover, PopoverContent } from '@/components/ui/popover';\nimport { cn } from '@/lib/utils';\n\nimport { blockSelectionVariants } from './block-selection';\nimport {\n ColorDropdownMenuItems,\n DEFAULT_COLORS,\n} from './font-color-toolbar-button';\nimport { ResizeHandle } from './resize-handle';\nimport {\n BorderAllIcon,\n BorderBottomIcon,\n BorderLeftIcon,\n BorderNoneIcon,\n BorderRightIcon,\n BorderTopIcon,\n} from './table-icons';\nimport {\n Toolbar,\n ToolbarButton,\n ToolbarGroup,\n ToolbarMenuGroup,\n} from './toolbar';\nexport const TableElement = withHOC(\n TableProvider,\n function TableElement({\n children,\n ...props\n }: PlateElementProps<TTableElement>) {\n const readOnly = useReadOnly();\n const isSelectionAreaVisible = usePluginOption(\n BlockSelectionPlugin,\n 'isSelectionAreaVisible'\n );\n const hasControls = !readOnly && !isSelectionAreaVisible;\n const {\n isSelectingCell,\n marginLeft,\n props: tableProps,\n } = useTableElement();\n\n const isSelectingTable = useBlockSelected(props.element.id as string);\n\n const content = (\n <PlateElement\n {...props}\n className={cn(\n 'overflow-x-auto py-5',\n hasControls && '-ml-2 *:data-[slot=block-selection]:left-2'\n )}\n style={{ paddingLeft: marginLeft }}\n >\n <div className=\"group/table relative w-fit\">\n <table\n className={cn(\n 'mr-0 ml-px table h-px table-fixed border-collapse',\n isSelectingCell && 'selection:bg-transparent'\n )}\n {...tableProps}\n >\n <tbody className=\"min-w-full\">{children}</tbody>\n </table>\n\n {isSelectingTable && (\n <div className={blockSelectionVariants()} contentEditable={false} />\n )}\n </div>\n </PlateElement>\n );\n\n if (readOnly) {\n return content;\n }\n\n return <TableFloatingToolbar>{content}</TableFloatingToolbar>;\n }\n);\n\nfunction TableFloatingToolbar({\n children,\n ...props\n}: React.ComponentProps<typeof PopoverContent>) {\n const { tf } = useEditorPlugin(TablePlugin);\n const selected = useSelected();\n const element = useElement<TTableElement>();\n const { props: buttonProps } = useRemoveNodeButton({ element });\n const collapsedInside = useEditorSelector(\n (editor) => selected && editor.api.isCollapsed(),\n [selected]\n );\n const isFocusedLast = useFocusedLast();\n\n const { canMerge, canSplit } = useTableMergeState();\n\n return (\n <Popover\n open={isFocusedLast && (canMerge || canSplit || collapsedInside)}\n modal={false}\n >\n <PopoverAnchor asChild>{children}</PopoverAnchor>\n <PopoverContent\n asChild\n onOpenAutoFocus={(e) => e.preventDefault()}\n contentEditable={false}\n {...props}\n >\n <Toolbar\n className=\"scrollbar-hide flex w-auto max-w-[80vw] flex-row overflow-x-auto rounded-md border bg-popover p-1 shadow-md print:hidden\"\n contentEditable={false}\n >\n <ToolbarGroup>\n <ColorDropdownMenu tooltip=\"Background color\">\n <PaintBucketIcon />\n </ColorDropdownMenu>\n {canMerge && (\n <ToolbarButton\n onClick={() => tf.table.merge()}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Merge cells\"\n >\n <CombineIcon />\n </ToolbarButton>\n )}\n {canSplit && (\n <ToolbarButton\n onClick={() => tf.table.split()}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Split cell\"\n >\n <SquareSplitHorizontalIcon />\n </ToolbarButton>\n )}\n\n <DropdownMenu modal={false}>\n <DropdownMenuTrigger asChild>\n <ToolbarButton tooltip=\"Cell borders\">\n <Grid2X2Icon />\n </ToolbarButton>\n </DropdownMenuTrigger>\n\n <DropdownMenuPortal>\n <TableBordersDropdownMenuContent />\n </DropdownMenuPortal>\n </DropdownMenu>\n\n {collapsedInside && (\n <ToolbarGroup>\n <ToolbarButton tooltip=\"Delete table\" {...buttonProps}>\n <Trash2Icon />\n </ToolbarButton>\n </ToolbarGroup>\n )}\n </ToolbarGroup>\n\n {collapsedInside && (\n <ToolbarGroup>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableRow({ before: true });\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert row before\"\n >\n <ArrowUp />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableRow();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert row after\"\n >\n <ArrowDown />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.remove.tableRow();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Delete row\"\n >\n <XIcon />\n </ToolbarButton>\n </ToolbarGroup>\n )}\n\n {collapsedInside && (\n <ToolbarGroup>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableColumn({ before: true });\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert column before\"\n >\n <ArrowLeft />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableColumn();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert column after\"\n >\n <ArrowRight />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.remove.tableColumn();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Delete column\"\n >\n <XIcon />\n </ToolbarButton>\n </ToolbarGroup>\n )}\n </Toolbar>\n </PopoverContent>\n </Popover>\n );\n}\n\nfunction TableBordersDropdownMenuContent(\n props: React.ComponentProps<typeof DropdownMenuPrimitive.Content>\n) {\n const editor = useEditorRef();\n const {\n getOnSelectTableBorder,\n hasBottomBorder,\n hasLeftBorder,\n hasNoBorders,\n hasOuterBorders,\n hasRightBorder,\n hasTopBorder,\n } = useTableBordersDropdownMenuContentState();\n\n return (\n <DropdownMenuContent\n className=\"min-w-[220px]\"\n onCloseAutoFocus={(e) => {\n e.preventDefault();\n editor.tf.focus();\n }}\n align=\"start\"\n side=\"right\"\n sideOffset={0}\n {...props}\n >\n <DropdownMenuGroup>\n <DropdownMenuCheckboxItem\n checked={hasTopBorder}\n onCheckedChange={getOnSelectTableBorder('top')}\n >\n <BorderTopIcon />\n <div>Top Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasRightBorder}\n onCheckedChange={getOnSelectTableBorder('right')}\n >\n <BorderRightIcon />\n <div>Right Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasBottomBorder}\n onCheckedChange={getOnSelectTableBorder('bottom')}\n >\n <BorderBottomIcon />\n <div>Bottom Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasLeftBorder}\n onCheckedChange={getOnSelectTableBorder('left')}\n >\n <BorderLeftIcon />\n <div>Left Border</div>\n </DropdownMenuCheckboxItem>\n </DropdownMenuGroup>\n\n <DropdownMenuGroup>\n <DropdownMenuCheckboxItem\n checked={hasNoBorders}\n onCheckedChange={getOnSelectTableBorder('none')}\n >\n <BorderNoneIcon />\n <div>No Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasOuterBorders}\n onCheckedChange={getOnSelectTableBorder('outer')}\n >\n <BorderAllIcon />\n <div>Outside Borders</div>\n </DropdownMenuCheckboxItem>\n </DropdownMenuGroup>\n </DropdownMenuContent>\n );\n}\n\nfunction ColorDropdownMenu({\n children,\n tooltip,\n}: {\n children: React.ReactNode;\n tooltip: string;\n}) {\n const [open, setOpen] = React.useState(false);\n\n const editor = useEditorRef();\n const selectedCells = usePluginOption(TablePlugin, 'selectedCells');\n\n const onUpdateColor = React.useCallback(\n (color: string) => {\n setOpen(false);\n setCellBackground(editor, { color, selectedCells: selectedCells ?? [] });\n },\n [selectedCells, editor]\n );\n\n const onClearColor = React.useCallback(() => {\n setOpen(false);\n setCellBackground(editor, {\n color: null,\n selectedCells: selectedCells ?? [],\n });\n }, [selectedCells, editor]);\n\n return (\n <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>\n <DropdownMenuTrigger asChild>\n <ToolbarButton tooltip={tooltip}>{children}</ToolbarButton>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent align=\"start\">\n <ToolbarMenuGroup label=\"Colors\">\n <ColorDropdownMenuItems\n className=\"px-2\"\n colors={DEFAULT_COLORS}\n updateColor={onUpdateColor}\n />\n </ToolbarMenuGroup>\n <DropdownMenuGroup>\n <DropdownMenuItem className=\"p-2\" onClick={onClearColor}>\n <EraserIcon />\n <span>Clear</span>\n </DropdownMenuItem>\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\nexport function TableRowElement({\n children,\n ...props\n}: PlateElementProps<TTableRowElement>) {\n const { element } = props;\n const readOnly = useReadOnly();\n const selected = useSelected();\n const editor = useEditorRef();\n const isSelectionAreaVisible = usePluginOption(\n BlockSelectionPlugin,\n 'isSelectionAreaVisible'\n );\n const hasControls = !readOnly && !isSelectionAreaVisible;\n\n const { isDragging, nodeRef, previewRef, handleRef } = useDraggable({\n element,\n type: element.type,\n canDropNode: ({ dragEntry, dropEntry }) =>\n PathApi.equals(\n PathApi.parent(dragEntry[1]),\n PathApi.parent(dropEntry[1])\n ),\n onDropHandler: (_, { dragItem }) => {\n const dragElement = (dragItem as { element: TElement }).element;\n\n if (dragElement) {\n editor.tf.select(dragElement);\n }\n },\n });\n\n return (\n <PlateElement\n {...props}\n ref={useComposedRef(props.ref, previewRef, nodeRef)}\n as=\"tr\"\n className={cn('group/row', isDragging && 'opacity-50')}\n attributes={{\n ...props.attributes,\n 'data-selected': selected ? 'true' : undefined,\n }}\n >\n {hasControls && (\n <td className=\"w-2 select-none\" contentEditable={false}>\n <RowDragHandle dragRef={handleRef} />\n <RowDropLine />\n </td>\n )}\n\n {children}\n </PlateElement>\n );\n}\n\nfunction RowDragHandle({ dragRef }: { dragRef: React.Ref<any> }) {\n const editor = useEditorRef();\n const element = useElement();\n\n return (\n <Button\n ref={dragRef}\n variant=\"outline\"\n className={cn(\n '-translate-y-1/2 absolute top-1/2 left-0 z-51 h-6 w-4 p-0 focus-visible:ring-0 focus-visible:ring-offset-0',\n 'cursor-grab active:cursor-grabbing',\n 'opacity-0 transition-opacity duration-100 group-hover/row:opacity-100 group-has-data-[resizing=\"true\"]/row:opacity-0'\n )}\n onClick={() => {\n editor.tf.select(element);\n }}\n >\n <GripVertical className=\"text-muted-foreground\" />\n </Button>\n );\n}\n\nfunction RowDropLine() {\n const { dropLine } = useDropLine();\n\n if (!dropLine) return null;\n\n return (\n <div\n className={cn(\n 'absolute inset-x-0 left-2 z-50 h-0.5 bg-brand/50',\n dropLine === 'top' ? '-top-px' : '-bottom-px'\n )}\n />\n );\n}\n\nexport function TableCellElement({\n isHeader,\n ...props\n}: PlateElementProps<TTableCellElement> & {\n isHeader?: boolean;\n}) {\n const { api } = useEditorPlugin(TablePlugin);\n const readOnly = useReadOnly();\n const element = props.element;\n\n const tableId = useElementSelector(([node]) => node.id as string, [], {\n key: KEYS.table,\n });\n const rowId = useElementSelector(([node]) => node.id as string, [], {\n key: KEYS.tr,\n });\n const isSelectingTable = useBlockSelected(tableId);\n const isSelectingRow = useBlockSelected(rowId) || isSelectingTable;\n const isSelectionAreaVisible = usePluginOption(\n BlockSelectionPlugin,\n 'isSelectionAreaVisible'\n );\n\n const { borders, colIndex, colSpan, minHeight, rowIndex, selected, width } =\n useTableCellElement();\n\n const { bottomProps, hiddenLeft, leftProps, rightProps } =\n useTableCellElementResizable({\n colIndex,\n colSpan,\n rowIndex,\n });\n\n return (\n <PlateElement\n {...props}\n as={isHeader ? 'th' : 'td'}\n className={cn(\n 'h-full overflow-visible border-none bg-background p-0',\n element.background ? 'bg-(--cellBackground)' : 'bg-background',\n isHeader && 'text-left *:m-0',\n 'before:size-full',\n selected && 'before:z-10 before:bg-brand/5',\n \"before:absolute before:box-border before:select-none before:content-['']\",\n borders.bottom?.size && 'before:border-b before:border-b-border',\n borders.right?.size && 'before:border-r before:border-r-border',\n borders.left?.size && 'before:border-l before:border-l-border',\n borders.top?.size && 'before:border-t before:border-t-border'\n )}\n style={\n {\n '--cellBackground': element.background,\n maxWidth: width || 240,\n minWidth: width || 120,\n } as React.CSSProperties\n }\n attributes={{\n ...props.attributes,\n colSpan: api.table.getColSpan(element),\n rowSpan: api.table.getRowSpan(element),\n }}\n >\n <div\n className=\"relative z-20 box-border h-full px-3 py-2\"\n style={{ minHeight }}\n >\n {props.children}\n </div>\n\n {!isSelectionAreaVisible && (\n <div\n className=\"group absolute top-0 size-full select-none\"\n contentEditable={false}\n suppressContentEditableWarning={true}\n >\n {!readOnly && (\n <>\n <ResizeHandle\n {...rightProps}\n className=\"-top-2 -right-1 h-[calc(100%_+_8px)] w-2\"\n data-col={colIndex}\n />\n <ResizeHandle {...bottomProps} className=\"-bottom-1 h-2\" />\n {!hiddenLeft && (\n <ResizeHandle\n {...leftProps}\n className=\"-left-1 top-0 w-2\"\n data-resizer-left={colIndex === 0 ? 'true' : undefined}\n />\n )}\n\n <div\n className={cn(\n 'absolute top-0 z-30 hidden h-full w-1 bg-ring',\n 'right-[-1.5px]',\n columnResizeVariants({ colIndex: colIndex as any })\n )}\n />\n {colIndex === 0 && (\n <div\n className={cn(\n 'absolute top-0 z-30 h-full w-1 bg-ring',\n 'left-[-1.5px]',\n 'fade-in hidden animate-in group-has-[[data-resizer-left]:hover]/table:block group-has-[[data-resizer-left][data-resizing=\"true\"]]/table:block'\n )}\n />\n )}\n </>\n )}\n </div>\n )}\n\n {isSelectingRow && (\n <div className={blockSelectionVariants()} contentEditable={false} />\n )}\n </PlateElement>\n );\n}\n\nexport function TableCellHeaderElement(\n props: React.ComponentProps<typeof TableCellElement>\n) {\n return <TableCellElement {...props} isHeader />;\n}\n\nconst columnResizeVariants = cva('fade-in hidden animate-in', {\n variants: {\n colIndex: {\n 0: 'group-has-[[data-col=\"0\"]:hover]/table:block group-has-[[data-col=\"0\"][data-resizing=\"true\"]]/table:block',\n 1: 'group-has-[[data-col=\"1\"]:hover]/table:block group-has-[[data-col=\"1\"][data-resizing=\"true\"]]/table:block',\n 2: 'group-has-[[data-col=\"2\"]:hover]/table:block group-has-[[data-col=\"2\"][data-resizing=\"true\"]]/table:block',\n 3: 'group-has-[[data-col=\"3\"]:hover]/table:block group-has-[[data-col=\"3\"][data-resizing=\"true\"]]/table:block',\n 4: 'group-has-[[data-col=\"4\"]:hover]/table:block group-has-[[data-col=\"4\"][data-resizing=\"true\"]]/table:block',\n 5: 'group-has-[[data-col=\"5\"]:hover]/table:block group-has-[[data-col=\"5\"][data-resizing=\"true\"]]/table:block',\n 6: 'group-has-[[data-col=\"6\"]:hover]/table:block group-has-[[data-col=\"6\"][data-resizing=\"true\"]]/table:block',\n 7: 'group-has-[[data-col=\"7\"]:hover]/table:block group-has-[[data-col=\"7\"][data-resizing=\"true\"]]/table:block',\n 8: 'group-has-[[data-col=\"8\"]:hover]/table:block group-has-[[data-col=\"8\"][data-resizing=\"true\"]]/table:block',\n 9: 'group-has-[[data-col=\"9\"]:hover]/table:block group-has-[[data-col=\"9\"][data-resizing=\"true\"]]/table:block',\n 10: 'group-has-[[data-col=\"10\"]:hover]/table:block group-has-[[data-col=\"10\"][data-resizing=\"true\"]]/table:block',\n },\n },\n});\n", | ||
| "content": "'use client';\n\nimport * as React from 'react';\n\nimport { useDraggable, useDropLine } from '@platejs/dnd';\nimport {\n BlockSelectionPlugin,\n useBlockSelected,\n} from '@platejs/selection/react';\nimport { resizeLengthClampStatic } from '@platejs/resizable';\nimport {\n setCellBackground,\n setTableColSize,\n setTableMarginLeft,\n setTableRowSize,\n} from '@platejs/table';\nimport {\n TablePlugin,\n TableProvider,\n roundCellSizeToStep,\n useCellIndices,\n useIsCellSelected,\n useOverrideColSize,\n useOverrideMarginLeft,\n useOverrideRowSize,\n useTableCellBorders,\n useTableBordersDropdownMenuContentState,\n useTableColSizes,\n useTableElement,\n useTableMergeState,\n useTableValue,\n} from '@platejs/table/react';\nimport {\n ArrowDown,\n ArrowLeft,\n ArrowRight,\n ArrowUp,\n CombineIcon,\n EraserIcon,\n Grid2X2Icon,\n GripVertical,\n PaintBucketIcon,\n SquareSplitHorizontalIcon,\n Trash2Icon,\n XIcon,\n} from 'lucide-react';\nimport {\n type TElement,\n type TTableCellElement,\n type TTableElement,\n type TTableRowElement,\n KEYS,\n PathApi,\n} from 'platejs';\nimport {\n type PlateElementProps,\n PlateElement,\n useComposedRef,\n useEditorPlugin,\n useEditorRef,\n useEditorSelector,\n useElement,\n useFocusedLast,\n usePluginOption,\n useReadOnly,\n useRemoveNodeButton,\n useSelected,\n withHOC,\n} from 'platejs/react';\nimport { useElementSelector } from 'platejs/react';\n\nimport { Button } from '@/components/ui/button';\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuPortal,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport {\n Popover,\n PopoverAnchor,\n PopoverContent,\n} from '@/components/ui/popover';\nimport { cn } from '@/lib/utils';\n\nimport { blockSelectionVariants } from './block-selection';\nimport {\n ColorDropdownMenuItems,\n DEFAULT_COLORS,\n} from './font-color-toolbar-button';\nimport {\n BorderAllIcon,\n BorderBottomIcon,\n BorderLeftIcon,\n BorderNoneIcon,\n BorderRightIcon,\n BorderTopIcon,\n} from './table-icons';\nimport {\n Toolbar,\n ToolbarButton,\n ToolbarGroup,\n ToolbarMenuGroup,\n} from './toolbar';\n\ntype TableResizeDirection = 'bottom' | 'left' | 'right';\n\ntype TableResizeStartOptions = {\n colIndex: number;\n direction: TableResizeDirection;\n handleKey: string;\n rowIndex: number;\n};\n\ntype TableResizeDragState = {\n colIndex: number;\n direction: TableResizeDirection;\n initialPosition: number;\n initialSize: number;\n marginLeft: number;\n rowIndex: number;\n};\n\ntype TableResizeContextValue = {\n disableMarginLeft: boolean;\n clearResizePreview: (handleKey: string) => void;\n setResizePreview: (\n event: React.PointerEvent<HTMLDivElement>,\n options: TableResizeStartOptions\n ) => void;\n startResize: (\n event: React.PointerEvent<HTMLDivElement>,\n options: TableResizeStartOptions\n ) => void;\n};\n\nconst TABLE_CONTROL_COLUMN_WIDTH = 8;\nconst TABLE_DEFERRED_COLUMN_RESIZE_CELL_COUNT = 1200;\n\nconst TableResizeContext = React.createContext<TableResizeContextValue | null>(\n null\n);\n\nfunction useTableResizeContext() {\n const context = React.useContext(TableResizeContext);\n\n if (!context) {\n throw new Error('TableResizeContext is missing');\n }\n\n return context;\n}\n\nfunction useTableResizeController({\n deferColumnResize,\n dragIndicatorRef,\n hoverIndicatorRef,\n marginLeft,\n controlColumnWidth,\n tablePath,\n tableRef,\n wrapperRef,\n}: {\n deferColumnResize: boolean;\n dragIndicatorRef: React.RefObject<HTMLDivElement | null>;\n hoverIndicatorRef: React.RefObject<HTMLDivElement | null>;\n marginLeft: number;\n controlColumnWidth: number;\n tablePath: number[];\n tableRef: React.RefObject<HTMLTableElement | null>;\n wrapperRef: React.RefObject<HTMLDivElement | null>;\n}) {\n const { editor, getOptions } = useEditorPlugin(TablePlugin);\n const { disableMarginLeft = false, minColumnWidth = 0 } = getOptions();\n const colSizes = useTableColSizes({ disableOverrides: true });\n const colSizesRef = React.useRef(colSizes);\n const activeHandleKeyRef = React.useRef<string | null>(null);\n const activeRowElementRef = React.useRef<HTMLTableRowElement | null>(null);\n const cleanupListenersRef = React.useRef<(() => void) | null>(null);\n const marginLeftRef = React.useRef(marginLeft);\n const dragStateRef = React.useRef<TableResizeDragState | null>(null);\n const previewHandleKeyRef = React.useRef<string | null>(null);\n const overrideColSize = useOverrideColSize();\n const overrideMarginLeft = useOverrideMarginLeft();\n const overrideRowSize = useOverrideRowSize();\n\n React.useEffect(() => {\n colSizesRef.current = colSizes;\n }, [colSizes]);\n\n React.useEffect(() => {\n marginLeftRef.current = marginLeft;\n }, [marginLeft]);\n\n const hideDeferredResizeIndicator = React.useCallback(() => {\n const indicator = dragIndicatorRef.current;\n\n if (!indicator) return;\n\n indicator.style.display = 'none';\n indicator.style.removeProperty('left');\n }, [dragIndicatorRef]);\n\n const showDeferredResizeIndicator = React.useCallback(\n (offset: number) => {\n const indicator = dragIndicatorRef.current;\n\n if (!indicator) return;\n\n indicator.style.display = 'block';\n indicator.style.left = `${offset}px`;\n },\n [dragIndicatorRef]\n );\n\n const hideResizeIndicator = React.useCallback(() => {\n const indicator = hoverIndicatorRef.current;\n\n if (!indicator) return;\n\n indicator.style.display = 'none';\n indicator.style.removeProperty('left');\n }, [hoverIndicatorRef]);\n\n const showResizeIndicatorAtOffset = React.useCallback(\n (offset: number) => {\n const indicator = hoverIndicatorRef.current;\n\n if (!indicator) return;\n\n indicator.style.display = 'block';\n indicator.style.left = `${offset}px`;\n },\n [hoverIndicatorRef]\n );\n\n const showResizeIndicator = React.useCallback(\n ({\n event,\n direction,\n }: Pick<TableResizeStartOptions, 'direction'> & {\n event: React.PointerEvent<HTMLDivElement>;\n }) => {\n if (direction === 'bottom') return;\n\n const wrapper = wrapperRef.current;\n\n if (!wrapper) return;\n\n const handleRect = event.currentTarget.getBoundingClientRect();\n const wrapperRect = wrapper.getBoundingClientRect();\n const boundaryOffset =\n handleRect.left - wrapperRect.left + handleRect.width / 2;\n\n showResizeIndicatorAtOffset(boundaryOffset);\n },\n [showResizeIndicatorAtOffset, wrapperRef]\n );\n\n const setResizePreview = React.useCallback(\n (\n event: React.PointerEvent<HTMLDivElement>,\n options: TableResizeStartOptions\n ) => {\n if (activeHandleKeyRef.current) return;\n\n previewHandleKeyRef.current = options.handleKey;\n showResizeIndicator({ ...options, event });\n },\n [showResizeIndicator]\n );\n\n const clearResizePreview = React.useCallback(\n (handleKey: string) => {\n if (activeHandleKeyRef.current) return;\n if (previewHandleKeyRef.current !== handleKey) return;\n\n previewHandleKeyRef.current = null;\n hideResizeIndicator();\n },\n [hideResizeIndicator]\n );\n\n const commitColSize = React.useCallback(\n (colIndex: number, width: number) => {\n setTableColSize(editor, { colIndex, width }, { at: tablePath });\n setTimeout(() => overrideColSize(colIndex, null), 0);\n },\n [editor, overrideColSize, tablePath]\n );\n\n const commitRowSize = React.useCallback(\n (rowIndex: number, height: number) => {\n setTableRowSize(editor, { height, rowIndex }, { at: tablePath });\n setTimeout(() => overrideRowSize(rowIndex, null), 0);\n },\n [editor, overrideRowSize, tablePath]\n );\n\n const commitMarginLeft = React.useCallback(\n (nextMarginLeft: number) => {\n setTableMarginLeft(\n editor,\n { marginLeft: nextMarginLeft },\n { at: tablePath }\n );\n setTimeout(() => overrideMarginLeft(null), 0);\n },\n [editor, overrideMarginLeft, tablePath]\n );\n\n const getColumnBoundaryOffset = React.useCallback(\n (colIndex: number, currentWidth: number) =>\n controlColumnWidth +\n colSizesRef.current\n .slice(0, colIndex)\n .reduce((total, colSize) => total + colSize, 0) +\n currentWidth,\n [controlColumnWidth]\n );\n\n const applyResize = React.useCallback(\n (event: PointerEvent, finished: boolean) => {\n const dragState = dragStateRef.current;\n\n if (!dragState) return;\n\n const currentPosition =\n dragState.direction === 'bottom' ? event.clientY : event.clientX;\n const delta = currentPosition - dragState.initialPosition;\n\n if (dragState.direction === 'bottom') {\n const newHeight = roundCellSizeToStep(\n dragState.initialSize + delta,\n undefined\n );\n\n if (finished) {\n commitRowSize(dragState.rowIndex, newHeight);\n } else {\n overrideRowSize(dragState.rowIndex, newHeight);\n }\n\n return;\n }\n\n if (dragState.direction === 'left') {\n const initial =\n colSizesRef.current[dragState.colIndex] ?? dragState.initialSize;\n const complement = (width: number) =>\n initial + dragState.marginLeft - width;\n const nextMarginLeft = roundCellSizeToStep(\n resizeLengthClampStatic(dragState.marginLeft + delta, {\n max: complement(minColumnWidth),\n min: 0,\n }),\n undefined\n );\n const nextWidth = complement(nextMarginLeft);\n\n if (finished) {\n commitMarginLeft(nextMarginLeft);\n commitColSize(dragState.colIndex, nextWidth);\n } else if (deferColumnResize) {\n showDeferredResizeIndicator(\n controlColumnWidth + (nextMarginLeft - dragState.marginLeft)\n );\n } else {\n showResizeIndicatorAtOffset(\n controlColumnWidth + (nextMarginLeft - dragState.marginLeft)\n );\n overrideMarginLeft(nextMarginLeft);\n overrideColSize(dragState.colIndex, nextWidth);\n }\n\n return;\n }\n\n const currentInitial =\n colSizesRef.current[dragState.colIndex] ?? dragState.initialSize;\n const nextInitial = colSizesRef.current[dragState.colIndex + 1];\n const complement = (width: number) =>\n currentInitial + nextInitial - width;\n const currentWidth = roundCellSizeToStep(\n resizeLengthClampStatic(currentInitial + delta, {\n max: nextInitial ? complement(minColumnWidth) : undefined,\n min: minColumnWidth,\n }),\n undefined\n );\n const nextWidth = nextInitial ? complement(currentWidth) : undefined;\n\n if (finished) {\n commitColSize(dragState.colIndex, currentWidth);\n\n if (nextWidth !== undefined) {\n commitColSize(dragState.colIndex + 1, nextWidth);\n }\n } else if (deferColumnResize) {\n showDeferredResizeIndicator(\n getColumnBoundaryOffset(dragState.colIndex, currentWidth)\n );\n } else {\n showResizeIndicatorAtOffset(\n getColumnBoundaryOffset(dragState.colIndex, currentWidth)\n );\n overrideColSize(dragState.colIndex, currentWidth);\n\n if (nextWidth !== undefined) {\n overrideColSize(dragState.colIndex + 1, nextWidth);\n }\n }\n },\n [\n commitColSize,\n commitMarginLeft,\n commitRowSize,\n controlColumnWidth,\n deferColumnResize,\n getColumnBoundaryOffset,\n showDeferredResizeIndicator,\n showResizeIndicatorAtOffset,\n minColumnWidth,\n overrideColSize,\n overrideMarginLeft,\n overrideRowSize,\n ]\n );\n\n const stopResize = React.useCallback(() => {\n cleanupListenersRef.current?.();\n cleanupListenersRef.current = null;\n activeHandleKeyRef.current = null;\n previewHandleKeyRef.current = null;\n dragStateRef.current = null;\n\n if (activeRowElementRef.current) {\n delete activeRowElementRef.current.dataset.tableResizing;\n activeRowElementRef.current = null;\n }\n\n hideDeferredResizeIndicator();\n hideResizeIndicator();\n }, [hideDeferredResizeIndicator, hideResizeIndicator]);\n\n React.useEffect(() => stopResize, [stopResize]);\n\n const startResize = React.useCallback(\n (\n event: React.PointerEvent<HTMLDivElement>,\n { colIndex, direction, handleKey, rowIndex }: TableResizeStartOptions\n ) => {\n const rowHeight =\n tableRef.current?.rows.item(rowIndex)?.getBoundingClientRect().height ??\n 0;\n\n dragStateRef.current = {\n colIndex,\n direction,\n initialPosition: direction === 'bottom' ? event.clientY : event.clientX,\n initialSize:\n direction === 'bottom'\n ? rowHeight\n : (colSizesRef.current[colIndex] ?? 0),\n marginLeft: marginLeftRef.current,\n rowIndex,\n };\n activeHandleKeyRef.current = handleKey;\n previewHandleKeyRef.current = null;\n\n const rowElement = tableRef.current?.rows.item(rowIndex) ?? null;\n\n if (\n activeRowElementRef.current &&\n activeRowElementRef.current !== rowElement\n ) {\n delete activeRowElementRef.current.dataset.tableResizing;\n }\n\n activeRowElementRef.current = rowElement;\n\n if (rowElement) {\n rowElement.dataset.tableResizing = 'true';\n }\n\n cleanupListenersRef.current?.();\n\n const handlePointerMove = (pointerEvent: PointerEvent) => {\n applyResize(pointerEvent, false);\n };\n\n const handlePointerEnd = (pointerEvent: PointerEvent) => {\n applyResize(pointerEvent, true);\n stopResize();\n };\n\n window.addEventListener('pointermove', handlePointerMove);\n window.addEventListener('pointerup', handlePointerEnd);\n window.addEventListener('pointercancel', handlePointerEnd);\n\n cleanupListenersRef.current = () => {\n window.removeEventListener('pointermove', handlePointerMove);\n window.removeEventListener('pointerup', handlePointerEnd);\n window.removeEventListener('pointercancel', handlePointerEnd);\n };\n\n if (deferColumnResize && direction !== 'bottom') {\n hideResizeIndicator();\n showDeferredResizeIndicator(\n direction === 'left'\n ? controlColumnWidth\n : getColumnBoundaryOffset(\n colIndex,\n colSizesRef.current[colIndex] ?? 0\n )\n );\n } else {\n showResizeIndicator({ direction, event });\n }\n\n event.preventDefault();\n event.stopPropagation();\n },\n [\n controlColumnWidth,\n deferColumnResize,\n getColumnBoundaryOffset,\n hideResizeIndicator,\n showDeferredResizeIndicator,\n showResizeIndicator,\n stopResize,\n tableRef,\n applyResize,\n ]\n );\n\n return React.useMemo(\n () => ({\n clearResizePreview,\n disableMarginLeft,\n setResizePreview,\n startResize,\n }),\n [clearResizePreview, disableMarginLeft, setResizePreview, startResize]\n );\n}\n\nexport const TableElement = withHOC(\n TableProvider,\n function TableElement({\n children,\n ...props\n }: PlateElementProps<TTableElement>) {\n const readOnly = useReadOnly();\n const isSelectionAreaVisible = usePluginOption(\n BlockSelectionPlugin,\n 'isSelectionAreaVisible'\n );\n const hasControls = !readOnly && !isSelectionAreaVisible;\n const {\n isSelectingCell,\n marginLeft,\n props: tableProps,\n } = useTableElement();\n const colSizes = useTableColSizes();\n const controlColumnWidth = hasControls ? TABLE_CONTROL_COLUMN_WIDTH : 0;\n const dragIndicatorRef = React.useRef<HTMLDivElement>(null);\n const hoverIndicatorRef = React.useRef<HTMLDivElement>(null);\n const deferColumnResize =\n colSizes.length * props.element.children.length >\n TABLE_DEFERRED_COLUMN_RESIZE_CELL_COUNT;\n const tablePath = useElementSelector(([, path]) => path, [], {\n key: KEYS.table,\n });\n const tableRef = React.useRef<HTMLTableElement>(null);\n const wrapperRef = React.useRef<HTMLDivElement>(null);\n const resizeController = useTableResizeController({\n controlColumnWidth,\n deferColumnResize,\n dragIndicatorRef,\n hoverIndicatorRef,\n marginLeft,\n tablePath,\n tableRef,\n wrapperRef,\n });\n const tableVariableStyle = React.useMemo(() => {\n if (colSizes.length === 0) {\n return;\n }\n\n return {\n ...Object.fromEntries(\n colSizes.map((colSize, index) => [\n `--table-col-${index}`,\n `${colSize}px`,\n ])\n ),\n } as React.CSSProperties;\n }, [colSizes]);\n const tableStyle = React.useMemo(\n () =>\n ({\n width: `${\n colSizes.reduce((total, colSize) => total + colSize, 0) +\n controlColumnWidth\n }px`,\n }) as React.CSSProperties,\n [colSizes, controlColumnWidth]\n );\n\n const isSelectingTable = useBlockSelected(props.element.id as string);\n\n const content = (\n <PlateElement\n {...props}\n className={cn(\n 'overflow-x-auto py-5',\n hasControls && '-ml-2 *:data-[slot=block-selection]:left-2'\n )}\n style={{ paddingLeft: marginLeft }}\n >\n <TableResizeContext.Provider value={resizeController}>\n <div\n ref={wrapperRef}\n className=\"group/table relative w-fit\"\n style={tableVariableStyle}\n >\n <div\n ref={dragIndicatorRef}\n className=\"-translate-x-[1.5px] pointer-events-none absolute inset-y-0 z-36 hidden w-[3px] bg-ring/70\"\n contentEditable={false}\n />\n <div\n ref={hoverIndicatorRef}\n className=\"-translate-x-[1.5px] pointer-events-none absolute inset-y-0 z-35 hidden w-[3px] bg-ring/80\"\n contentEditable={false}\n />\n <table\n ref={tableRef}\n className={cn(\n 'mr-0 ml-px table h-px table-fixed border-collapse',\n isSelectingCell && 'selection:bg-transparent'\n )}\n style={tableStyle}\n {...tableProps}\n >\n {colSizes.length > 0 && (\n <colgroup>\n {hasControls && (\n <col\n style={{\n maxWidth: TABLE_CONTROL_COLUMN_WIDTH,\n minWidth: TABLE_CONTROL_COLUMN_WIDTH,\n width: TABLE_CONTROL_COLUMN_WIDTH,\n }}\n />\n )}\n {colSizes.map((colSize, index) => (\n <col\n key={index}\n style={\n colSize\n ? {\n maxWidth: colSize,\n minWidth: colSize,\n width: colSize,\n }\n : undefined\n }\n />\n ))}\n </colgroup>\n )}\n <tbody className=\"min-w-full\">{children}</tbody>\n </table>\n\n {isSelectingTable && (\n <div\n className={blockSelectionVariants()}\n contentEditable={false}\n />\n )}\n </div>\n </TableResizeContext.Provider>\n </PlateElement>\n );\n\n if (readOnly) {\n return content;\n }\n\n return <TableFloatingToolbar>{content}</TableFloatingToolbar>;\n }\n);\n\nfunction TableFloatingToolbar({\n children,\n ...props\n}: React.ComponentProps<typeof PopoverContent>) {\n const { tf } = useEditorPlugin(TablePlugin);\n const selected = useSelected();\n const element = useElement<TTableElement>();\n const { props: buttonProps } = useRemoveNodeButton({ element });\n const collapsedInside = useEditorSelector(\n (editor) => selected && editor.api.isCollapsed(),\n [selected]\n );\n const isFocusedLast = useFocusedLast();\n\n const { canMerge, canSplit } = useTableMergeState();\n\n return (\n <Popover\n open={isFocusedLast && (canMerge || canSplit || collapsedInside)}\n modal={false}\n >\n <PopoverAnchor asChild>{children}</PopoverAnchor>\n <PopoverContent\n asChild\n onOpenAutoFocus={(e) => e.preventDefault()}\n contentEditable={false}\n {...props}\n >\n <Toolbar\n className=\"scrollbar-hide flex w-auto max-w-[80vw] flex-row overflow-x-auto rounded-md border bg-popover p-1 shadow-md print:hidden\"\n contentEditable={false}\n >\n <ToolbarGroup>\n <ColorDropdownMenu tooltip=\"Background color\">\n <PaintBucketIcon />\n </ColorDropdownMenu>\n {canMerge && (\n <ToolbarButton\n onClick={() => tf.table.merge()}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Merge cells\"\n >\n <CombineIcon />\n </ToolbarButton>\n )}\n {canSplit && (\n <ToolbarButton\n onClick={() => tf.table.split()}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Split cell\"\n >\n <SquareSplitHorizontalIcon />\n </ToolbarButton>\n )}\n\n <DropdownMenu modal={false}>\n <DropdownMenuTrigger asChild>\n <ToolbarButton tooltip=\"Cell borders\">\n <Grid2X2Icon />\n </ToolbarButton>\n </DropdownMenuTrigger>\n\n <DropdownMenuPortal>\n <TableBordersDropdownMenuContent />\n </DropdownMenuPortal>\n </DropdownMenu>\n\n {collapsedInside && (\n <ToolbarGroup>\n <ToolbarButton tooltip=\"Delete table\" {...buttonProps}>\n <Trash2Icon />\n </ToolbarButton>\n </ToolbarGroup>\n )}\n </ToolbarGroup>\n\n {collapsedInside && (\n <ToolbarGroup>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableRow({ before: true });\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert row before\"\n >\n <ArrowUp />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableRow();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert row after\"\n >\n <ArrowDown />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.remove.tableRow();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Delete row\"\n >\n <XIcon />\n </ToolbarButton>\n </ToolbarGroup>\n )}\n\n {collapsedInside && (\n <ToolbarGroup>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableColumn({ before: true });\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert column before\"\n >\n <ArrowLeft />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.insert.tableColumn();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Insert column after\"\n >\n <ArrowRight />\n </ToolbarButton>\n <ToolbarButton\n onClick={() => {\n tf.remove.tableColumn();\n }}\n onMouseDown={(e) => e.preventDefault()}\n tooltip=\"Delete column\"\n >\n <XIcon />\n </ToolbarButton>\n </ToolbarGroup>\n )}\n </Toolbar>\n </PopoverContent>\n </Popover>\n );\n}\n\nfunction TableBordersDropdownMenuContent(\n props: React.ComponentProps<typeof DropdownMenuContent>\n) {\n const editor = useEditorRef();\n const {\n getOnSelectTableBorder,\n hasBottomBorder,\n hasLeftBorder,\n hasNoBorders,\n hasOuterBorders,\n hasRightBorder,\n hasTopBorder,\n } = useTableBordersDropdownMenuContentState();\n\n return (\n <DropdownMenuContent\n className=\"min-w-[220px]\"\n onCloseAutoFocus={(e) => {\n e.preventDefault();\n editor.tf.focus();\n }}\n align=\"start\"\n side=\"right\"\n sideOffset={0}\n {...props}\n >\n <DropdownMenuGroup>\n <DropdownMenuCheckboxItem\n checked={hasTopBorder}\n onCheckedChange={getOnSelectTableBorder('top')}\n >\n <BorderTopIcon />\n <div>Top Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasRightBorder}\n onCheckedChange={getOnSelectTableBorder('right')}\n >\n <BorderRightIcon />\n <div>Right Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasBottomBorder}\n onCheckedChange={getOnSelectTableBorder('bottom')}\n >\n <BorderBottomIcon />\n <div>Bottom Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasLeftBorder}\n onCheckedChange={getOnSelectTableBorder('left')}\n >\n <BorderLeftIcon />\n <div>Left Border</div>\n </DropdownMenuCheckboxItem>\n </DropdownMenuGroup>\n\n <DropdownMenuGroup>\n <DropdownMenuCheckboxItem\n checked={hasNoBorders}\n onCheckedChange={getOnSelectTableBorder('none')}\n >\n <BorderNoneIcon />\n <div>No Border</div>\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={hasOuterBorders}\n onCheckedChange={getOnSelectTableBorder('outer')}\n >\n <BorderAllIcon />\n <div>Outside Borders</div>\n </DropdownMenuCheckboxItem>\n </DropdownMenuGroup>\n </DropdownMenuContent>\n );\n}\n\nfunction ColorDropdownMenu({\n children,\n tooltip,\n}: {\n children: React.ReactNode;\n tooltip: string;\n}) {\n const [open, setOpen] = React.useState(false);\n\n const editor = useEditorRef();\n const selectedCells = usePluginOption(TablePlugin, 'selectedCells') as\n | TElement[]\n | null;\n\n const onUpdateColor = React.useCallback(\n (color: string) => {\n setOpen(false);\n setCellBackground(editor, { color, selectedCells: selectedCells ?? [] });\n },\n [selectedCells, editor]\n );\n\n const onClearColor = React.useCallback(() => {\n setOpen(false);\n setCellBackground(editor, {\n color: null,\n selectedCells: selectedCells ?? [],\n });\n }, [selectedCells, editor]);\n\n return (\n <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>\n <DropdownMenuTrigger asChild>\n <ToolbarButton tooltip={tooltip}>{children}</ToolbarButton>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent align=\"start\">\n <ToolbarMenuGroup label=\"Colors\">\n <ColorDropdownMenuItems\n className=\"px-2\"\n colors={DEFAULT_COLORS}\n updateColor={onUpdateColor}\n />\n </ToolbarMenuGroup>\n <DropdownMenuGroup>\n <DropdownMenuItem className=\"p-2\" onClick={onClearColor}>\n <EraserIcon />\n <span>Clear</span>\n </DropdownMenuItem>\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\nexport function TableRowElement({\n children,\n ...props\n}: PlateElementProps<TTableRowElement>) {\n const { element } = props;\n const readOnly = useReadOnly();\n const selected = useSelected();\n const editor = useEditorRef();\n const rowIndex = useElementSelector(([, path]) => path.at(-1) as number, [], {\n key: KEYS.tr,\n });\n const rowSize = useElementSelector(\n ([node]) => (node as TTableRowElement).size,\n [],\n {\n key: KEYS.tr,\n }\n );\n const rowSizeOverrides = useTableValue('rowSizeOverrides');\n const rowMinHeight = rowSizeOverrides.get?.(rowIndex) ?? rowSize;\n const isSelectionAreaVisible = usePluginOption(\n BlockSelectionPlugin,\n 'isSelectionAreaVisible'\n );\n const hasControls = !readOnly && !isSelectionAreaVisible;\n\n const { isDragging, nodeRef, previewRef, handleRef } = useDraggable({\n element,\n type: element.type,\n canDropNode: ({ dragEntry, dropEntry }) =>\n PathApi.equals(\n PathApi.parent(dragEntry[1]),\n PathApi.parent(dropEntry[1])\n ),\n onDropHandler: (_, { dragItem }) => {\n const dragElement = (dragItem as { element: TElement }).element;\n\n if (dragElement) {\n editor.tf.select(dragElement);\n }\n },\n });\n\n return (\n <PlateElement\n {...props}\n ref={useComposedRef(props.ref, previewRef, nodeRef)}\n as=\"tr\"\n className={cn('group/row', isDragging && 'opacity-50')}\n style={\n {\n ...props.style,\n '--tableRowMinHeight': rowMinHeight ? `${rowMinHeight}px` : undefined,\n } as React.CSSProperties\n }\n attributes={{\n ...props.attributes,\n 'data-selected': selected ? 'true' : undefined,\n }}\n >\n {hasControls && (\n <td\n className=\"w-2 min-w-2 max-w-2 select-none p-0\"\n contentEditable={false}\n >\n <RowDragHandle dragRef={handleRef} />\n <RowDropLine />\n </td>\n )}\n\n {children}\n </PlateElement>\n );\n}\n\nfunction useTableCellPresentation(element: TTableCellElement) {\n const { api, setOption } = useEditorPlugin(TablePlugin);\n const borders = useTableCellBorders({ element });\n const { col, row } = useCellIndices();\n const selected = useIsCellSelected(element);\n const selectedCells = usePluginOption(TablePlugin, 'selectedCells') as\n | TElement[]\n | null;\n\n React.useEffect(() => {\n if (\n selectedCells?.some((cell) => cell.id === element.id && cell !== element)\n ) {\n setOption(\n 'selectedCells',\n selectedCells.map((cell) => (cell.id === element.id ? element : cell))\n );\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [element]);\n\n const colSpan = api.table.getColSpan(element);\n const rowSpan = api.table.getRowSpan(element);\n const width = React.useMemo(() => {\n const terms = Array.from(\n { length: colSpan },\n (_, offset) => `var(--table-col-${col + offset}, 120px)`\n );\n\n return terms.length === 1 ? terms[0]! : `calc(${terms.join(' + ')})`;\n }, [col, colSpan]);\n\n return {\n borders,\n colIndex: col + colSpan - 1,\n colSpan,\n rowIndex: row + rowSpan - 1,\n rowSpan,\n selected,\n width,\n };\n}\n\nfunction RowDragHandle({ dragRef }: { dragRef: React.Ref<any> }) {\n const editor = useEditorRef();\n const element = useElement();\n\n return (\n <Button\n ref={dragRef}\n variant=\"outline\"\n className={cn(\n '-translate-y-1/2 absolute top-1/2 left-0 z-51 h-6 w-4 p-0 focus-visible:ring-0 focus-visible:ring-offset-0',\n 'cursor-grab active:cursor-grabbing',\n 'opacity-0 transition-opacity duration-100 group-hover/row:opacity-100 group-data-[table-resizing=true]/row:opacity-0'\n )}\n onClick={() => {\n editor.tf.select(element);\n }}\n >\n <GripVertical className=\"text-muted-foreground\" />\n </Button>\n );\n}\n\nfunction RowDropLine() {\n const { dropLine } = useDropLine();\n\n if (!dropLine) return null;\n\n return (\n <div\n className={cn(\n 'absolute inset-x-0 left-2 z-50 h-0.5 bg-brand/50',\n dropLine === 'top' ? '-top-px' : '-bottom-px'\n )}\n />\n );\n}\n\nexport function TableCellElement({\n isHeader,\n ...props\n}: PlateElementProps<TTableCellElement> & {\n isHeader?: boolean;\n}) {\n const readOnly = useReadOnly();\n const element = props.element;\n\n const tableId = useElementSelector(([node]) => node.id as string, [], {\n key: KEYS.table,\n });\n const rowId = useElementSelector(([node]) => node.id as string, [], {\n key: KEYS.tr,\n });\n const isSelectingTable = useBlockSelected(tableId);\n const isSelectingRow = useBlockSelected(rowId) || isSelectingTable;\n const isSelectionAreaVisible = usePluginOption(\n BlockSelectionPlugin,\n 'isSelectionAreaVisible'\n );\n\n const {\n borders,\n colIndex,\n colSpan,\n rowIndex,\n rowSpan,\n selected: cellSelected,\n width,\n } = useTableCellPresentation(element);\n\n return (\n <PlateElement\n {...props}\n as={isHeader ? 'th' : 'td'}\n className={cn(\n 'relative h-full overflow-visible border-none bg-background p-0',\n element.background ? 'bg-(--cellBackground)' : 'bg-background',\n isHeader && 'text-left *:m-0',\n 'before:size-full',\n cellSelected && 'before:z-10 before:bg-brand/5',\n \"before:absolute before:box-border before:select-none before:content-['']\",\n borders.bottom?.size && 'before:border-b before:border-b-border',\n borders.right?.size && 'before:border-r before:border-r-border',\n borders.left?.size && 'before:border-l before:border-l-border',\n borders.top?.size && 'before:border-t before:border-t-border'\n )}\n style={\n {\n '--cellBackground': element.background,\n maxWidth: width,\n minWidth: width,\n } as React.CSSProperties\n }\n attributes={{\n ...props.attributes,\n colSpan,\n rowSpan,\n }}\n >\n <div\n className=\"relative z-20 box-border h-full px-3 py-2\"\n style={\n rowSpan === 1\n ? { minHeight: 'var(--tableRowMinHeight, 0px)' }\n : undefined\n }\n >\n {props.children}\n </div>\n\n {!readOnly && !isSelectionAreaVisible && (\n <TableCellResizeControls colIndex={colIndex} rowIndex={rowIndex} />\n )}\n\n {isSelectingRow && (\n <div className={blockSelectionVariants()} contentEditable={false} />\n )}\n </PlateElement>\n );\n}\n\nexport function TableCellHeaderElement(\n props: React.ComponentProps<typeof TableCellElement>\n) {\n return <TableCellElement {...props} isHeader />;\n}\n\nconst TableCellResizeControls = React.memo(function TableCellResizeControls({\n colIndex,\n rowIndex,\n}: {\n colIndex: number;\n rowIndex: number;\n}) {\n const {\n clearResizePreview,\n disableMarginLeft,\n setResizePreview,\n startResize,\n } = useTableResizeContext();\n const rightHandleKey = `right:${rowIndex}:${colIndex}`;\n const bottomHandleKey = `bottom:${rowIndex}:${colIndex}`;\n const leftHandleKey = `left:${rowIndex}:${colIndex}`;\n const isLeftHandle = colIndex === 0 && !disableMarginLeft;\n\n return (\n <div\n className=\"group/resize pointer-events-none absolute inset-0 z-30 select-none\"\n contentEditable={false}\n suppressContentEditableWarning={true}\n >\n <div\n className=\"-top-2 -right-1 pointer-events-auto absolute z-40 h-[calc(100%_+_8px)] w-2 cursor-col-resize touch-none\"\n onPointerEnter={(event) => {\n setResizePreview(event, {\n colIndex,\n direction: 'right',\n handleKey: rightHandleKey,\n rowIndex,\n });\n }}\n onPointerLeave={() => {\n clearResizePreview(rightHandleKey);\n }}\n onPointerDown={(event) => {\n startResize(event, {\n colIndex,\n direction: 'right',\n handleKey: rightHandleKey,\n rowIndex,\n });\n }}\n />\n <div\n className=\"-bottom-1 pointer-events-auto absolute left-0 z-40 h-2 w-full cursor-row-resize touch-none\"\n onPointerEnter={(event) => {\n setResizePreview(event, {\n colIndex,\n direction: 'bottom',\n handleKey: bottomHandleKey,\n rowIndex,\n });\n }}\n onPointerLeave={() => {\n clearResizePreview(bottomHandleKey);\n }}\n onPointerDown={(event) => {\n startResize(event, {\n colIndex,\n direction: 'bottom',\n handleKey: bottomHandleKey,\n rowIndex,\n });\n }}\n />\n {isLeftHandle && (\n <div\n className=\"-left-1 pointer-events-auto absolute top-0 z-40 h-full w-2 cursor-col-resize touch-none\"\n onPointerEnter={(event) => {\n setResizePreview(event, {\n colIndex,\n direction: 'left',\n handleKey: leftHandleKey,\n rowIndex,\n });\n }}\n onPointerLeave={() => {\n clearResizePreview(leftHandleKey);\n }}\n onPointerDown={(event) => {\n startResize(event, {\n colIndex,\n direction: 'left',\n handleKey: leftHandleKey,\n rowIndex,\n });\n }}\n />\n )}\n </div>\n );\n});\n\nTableCellResizeControls.displayName = 'TableCellResizeControls';\n", |
There was a problem hiding this comment.
Keep non-zero default widths for tables without colSizes
This update makes table-node always derive CSS column widths from useTableColSizes() and sets <table style.width> to their sum, but tables created without explicit colSizes (e.g. default insert.table() output unless initialTableWidth is configured) produce zero-filled sizes, so the new code resolves each column to 0px and collapses the table to the control column. The previous implementation had a 120/240 fallback path for unset widths, so this is a rendering regression for valid table content that lacks persisted column sizes.
Useful? React with 👍 / 👎.
Update Registry.