From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Stream
    • Copilot
  • Discussion
    • Comments
    • Suggestion
    • Basic Blocks
      • Blockquote
      • Heading
      • Horizontal Rule
    • Callout
    • Code Block
    • Column
    • Date
    • Equation
    • Link
    • List Classic
    • Media
    • MentionElement
    • Table
    • Table of Contents
    • Footnote
    • Toggle
  • Marks
    • Bold
    • Italic
    • Underline
    • Code
    • Highlight
    • Keyboard Input
    • Strikethrough
    • Subscript
    • Superscript
      • Font
      • Line Height
      • Text Align
    • Indent
    • List
      • Exit Break
      • Single Block
      • Trailing Block
    • Autoformat
    • Block Menu
    • Block Placeholder
    • Combobox
      • Emoji
      • MentionElement
      • Slash Command
    • Cursor Overlay
    • Drag & Drop
    • Navigation Feedback
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • CSV
    • DOCX
    • HTML
    • Markdown

Table

PreviousNext

Organize and display data in a structured and resizable tabular format.

PlusTable Toolbar ButtonTable Nodes
Loading…
MentionTable of Contents

On This Page

FeaturesKit UsageInstallationAdd KitManual UsageInstallationAdd PluginConfigure PluginsAdd Toolbar ButtonInsert Toolbar ButtonDisable Merging ExamplePluginsTablePluginTableRowPluginTableCellPluginAPIeditor.api.table.createeditor.api.table.createCelleditor.api.table.createRoweditor.read.table.getCellBorderseditor.api.table.getCellChildreneditor.read.table.getCellSizeeditor.api.table.getColSpaneditor.api.table.getRowSpaneditor.read.table.getNextCelleditor.read.table.getPreviousCelleditor.api.table.getColumnCounteditor.read.table.getEntrieseditor.read.table.getGridAboveeditor.read.table.getAdjacentCellEditing BehaviorTransformseditor.plugin(TablePlugin).update.inserteditor.plugin(TablePlugin).update.insertColumneditor.plugin(TablePlugin).update.insertRoweditor.plugin(TablePlugin).update.removeColumneditor.plugin(TablePlugin).update.removeRoweditor.plugin(TablePlugin).update.removeeditor.plugin(TablePlugin).update.mergeeditor.plugin(TablePlugin).update.spliteditor.plugin(TablePlugin).update.moveSelectioneditor.plugin(TablePlugin).update.setBorderSizeeditor.plugin(TablePlugin).update.setColumnWidtheditor.plugin(TablePlugin).update.seteditor.plugin(TablePlugin).update.setRowHeightHooksuseTableSelectionDOMeditor.plugin(TablePlugin).read.canMerge()editor.plugin(TablePlugin).read.canSplit()
Build your editor
Production-ready AI template and reusable components.
Get all-access

Features

  • Enables creation and editing of tables with advanced behaviors.
  • Arrow navigation (up/down).
  • Grid cell selection.
  • Cell selection expansion with Shift+Arrow keys.
  • Copying and pasting cells.
  • Row drag-and-drop reordering
  • Row selection via drag handle
Report an issue

Kit Usage

Installation

The fastest way to add table functionality is with the TableKit, which includes pre-configured TablePlugin, TableRowPlugin, and TableCellPlugin descriptors with their Plate UI components.

'use client';
 
import { useDraggable, useDropLine } from '@platejs/dnd';
import { resizeLengthClampStatic } from '@platejs/resizable';
import {
  TableCellPlugin,
  TablePlugin,
  TableRowPlugin,
  useTableSelectionDOM,
} from '@platejs/table/react';
import {
  type LucideProps,
  ArrowDown,
  ArrowLeft,
  ArrowRight,
  ArrowUp,
  CombineIcon,
  EraserIcon,
  Grid2X2Icon,
  GripVertical,
  PaintBucketIcon,
  SquareSplitHorizontalIcon,
  Trash2Icon,
  XIcon,
} from 'lucide-react';
import { type Path, PathApi } from 'platejs';
import {
  type PlateElementProps,
  PlateElement,
  useComposedRef,
  useEditor,
  useEditorPlugin,
  useEditorReadOnly,
  useEditorSelector,
  useElement,
  useElementSelected,
  useFocusedLast,
  usePluginStore,
  useElementSelector,
} from 'platejs/react';
import * as React from 'react';
 
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuPortal,
  DropdownMenuTrigger,
} from '@/components/editor/dropdown-menu';
import {
  FloatingPopover,
  FloatingPopoverAnchor,
  FloatingPopoverContent,
} from '@/components/editor/floating-popover';
import {
  Toolbar,
  ToolbarButton,
  ToolbarGroup,
  ToolbarMenuGroup,
} from '@/components/editor/toolbar';
 
import {
  ColorDropdownMenuItems,
  DEFAULT_COLORS,
} from './font-color-toolbar-button';
 
type TableResizeDirection = 'bottom' | 'left' | 'right';
 
type TableResizeStartOptions = {
  colIndex: number;
  direction: TableResizeDirection;
  handleKey: string;
  rowIndex: number;
};
 
type TableResizeDragState = {
  colIndex: number;
  direction: TableResizeDirection;
  initialPosition: number;
  initialSize: number;
  marginLeft: number;
  rowIndex: number;
};
 
type TableResizeContextValue = {
  colSizeOverrides: Map<number, number>;
  disableMarginLeft: boolean;
  hasMultiRowSelection: boolean;
  marginLeftOverride: number | null;
  rowSizeOverrides: Map<number, number>;
  clearResizePreview: (handleKey: string) => void;
  showResizePreview: (
    event: React.PointerEvent<HTMLDivElement>,
    options: TableResizeStartOptions
  ) => void;
  startResize: (
    event: React.PointerEvent<HTMLDivElement>,
    options: TableResizeStartOptions
  ) => void;
};
 
const TABLE_CONTROL_COLUMN_WIDTH = 8;
 
const TABLE_DEFAULT_COLUMN_WIDTH = 120;
 
const TABLE_DEFERRED_COLUMN_RESIZE_CELL_COUNT = 1200;
 
const TABLE_MULTI_SELECTION_TOOLBAR_DELAY_MS = 150;
const TABLE_SELECTION_OVERLAY_CLASS =
  'pointer-events-none absolute inset-0 z-1 bg-brand/[.13]';
 
const roundCellSizeToStep = (size: number, step?: number) =>
  step ? Math.round(size / step) * step : size;
 
const TableResizeContext = React.createContext<TableResizeContextValue | null>(
  null
);
 
function useTableResizeContext() {
  const context = React.useContext(TableResizeContext);
 
  if (!context) {
    throw new Error('TableResizeContext is missing');
  }
 
  return context;
}
 
function useTableResizeController({
  baseColSizes,
  deferColumnResize,
  dragIndicatorRef,
  hoverIndicatorRef,
  marginLeft,
  overrideColSize,
  overrideMarginLeft,
  overrideRowSize,
  controlColumnWidth,
  tablePath,
  tableRef,
  wrapperRef,
}: {
  baseColSizes: number[];
  deferColumnResize: boolean;
  dragIndicatorRef: React.RefObject<HTMLDivElement | null>;
  hoverIndicatorRef: React.RefObject<HTMLDivElement | null>;
  marginLeft: number;
  overrideColSize: (index: number, size: number | null) => void;
  overrideMarginLeft: React.Dispatch<React.SetStateAction<number | null>>;
  overrideRowSize: (index: number, size: number | null) => void;
  controlColumnWidth: number;
  tablePath: Path;
  tableRef: React.RefObject<HTMLTableElement | null>;
  wrapperRef: React.RefObject<HTMLDivElement | null>;
}) {
  const editor = useEditor();
  const { store } = useEditorPlugin(TablePlugin);
  const { disableMarginLeft = false, minColumnWidth = 0 } = store.get();
  const effectiveColSizes = React.useMemo(
    () => baseColSizes.map((colSize) => colSize || TABLE_DEFAULT_COLUMN_WIDTH),
    [baseColSizes]
  );
  const effectiveColSizesRef = React.useRef(effectiveColSizes);
  const activeHandleKeyRef = React.useRef<string | null>(null);
  const activeRowElementRef = React.useRef<HTMLTableRowElement | null>(null);
  const cleanupListenersRef = React.useRef<(() => void) | null>(null);
  const marginLeftRef = React.useRef(marginLeft);
  const dragStateRef = React.useRef<TableResizeDragState | null>(null);
  const frozenRowIndicesRef = React.useRef<number[] | null>(null);
  const previewHandleKeyRef = React.useRef<string | null>(null);
  React.useEffect(() => {
    effectiveColSizesRef.current = effectiveColSizes;
  }, [effectiveColSizes]);
 
  React.useEffect(() => {
    marginLeftRef.current = marginLeft;
  }, [marginLeft]);
 
  const hideDeferredResizeIndicator = React.useCallback(() => {
    const indicator = dragIndicatorRef.current;
 
    if (!indicator) return;
 
    indicator.style.display = 'none';
    indicator.style.removeProperty('left');
  }, [dragIndicatorRef]);
 
  const showDeferredResizeIndicator = React.useCallback(
    (offset: number) => {
      const indicator = dragIndicatorRef.current;
 
      if (!indicator) return;
 
      indicator.style.display = 'block';
      indicator.style.left = `${offset}px`;
    },
    [dragIndicatorRef]
  );
 
  const hideResizeIndicator = React.useCallback(() => {
    const indicator = hoverIndicatorRef.current;
 
    if (!indicator) return;
 
    indicator.style.display = 'none';
    indicator.style.removeProperty('left');
  }, [hoverIndicatorRef]);
 
  const clearFrozenRowHeights = React.useCallback(() => {
    const frozenRowIndices = frozenRowIndicesRef.current;
 
    if (!frozenRowIndices) return;
 
    frozenRowIndicesRef.current = null;
 
    frozenRowIndices.forEach((rowIndex) => {
      overrideRowSize(rowIndex, null);
    });
  }, [overrideRowSize]);
 
  const freezeRowHeights = React.useCallback(() => {
    const table = tableRef.current;
 
    if (!table || deferColumnResize) return;
 
    clearFrozenRowHeights();
 
    const frozenRowIndices: number[] = [];
 
    Array.from(table.rows).forEach((row, rowIndex) => {
      const { height } = row.getBoundingClientRect();
 
      if (!height) return;
 
      overrideRowSize(rowIndex, height);
      frozenRowIndices.push(rowIndex);
    });
 
    frozenRowIndicesRef.current = frozenRowIndices;
  }, [clearFrozenRowHeights, deferColumnResize, overrideRowSize, tableRef]);
 
  const showResizeIndicatorAtOffset = React.useCallback(
    (offset: number) => {
      const indicator = hoverIndicatorRef.current;
 
      if (!indicator) return;
 
      indicator.style.display = 'block';
      indicator.style.left = `${offset}px`;
    },
    [hoverIndicatorRef]
  );
 
  const showResizeIndicator = React.useCallback(
    ({
      event,
      direction,
    }: Pick<TableResizeStartOptions, 'direction'> & {
      event: React.PointerEvent<HTMLDivElement>;
    }) => {
      if (direction === 'bottom') return;
 
      const wrapper = wrapperRef.current;
 
      if (!wrapper) return;
 
      const handleRect = event.currentTarget.getBoundingClientRect();
      const wrapperRect = wrapper.getBoundingClientRect();
      const boundaryOffset =
        handleRect.left - wrapperRect.left + handleRect.width / 2;
 
      showResizeIndicatorAtOffset(boundaryOffset);
    },
    [showResizeIndicatorAtOffset, wrapperRef]
  );
 
  const showResizePreview = React.useCallback(
    (
      event: React.PointerEvent<HTMLDivElement>,
      options: TableResizeStartOptions
    ) => {
      if (activeHandleKeyRef.current) return;
      if (event.buttons !== 0) return;
 
      previewHandleKeyRef.current = options.handleKey;
      showResizeIndicator({ ...options, event });
    },
    [showResizeIndicator]
  );
 
  const clearResizePreview = React.useCallback(
    (handleKey: string) => {
      if (activeHandleKeyRef.current) return;
      if (previewHandleKeyRef.current !== handleKey) return;
 
      previewHandleKeyRef.current = null;
      hideResizeIndicator();
    },
    [hideResizeIndicator]
  );
 
  const commitColSize = React.useCallback(
    (colIndex: number, width: number) => {
      editor
        .plugin(TablePlugin)
        .update.setColumnWidth({ colIndex, width }, { at: tablePath });
      setTimeout(() => {
        overrideColSize(colIndex, null);
      }, 0);
    },
    [editor, overrideColSize, tablePath]
  );
 
  const commitRowSize = React.useCallback(
    (rowIndex: number, height: number) => {
      editor
        .plugin(TablePlugin)
        .update.setRowHeight({ height, rowIndex }, { at: tablePath });
      setTimeout(() => {
        overrideRowSize(rowIndex, null);
      }, 0);
    },
    [editor, overrideRowSize, tablePath]
  );
 
  const commitMarginLeft = React.useCallback(
    (nextMarginLeft: number) => {
      editor
        .plugin(TablePlugin)
        .update.set({ marginLeft: nextMarginLeft }, { at: tablePath });
      setTimeout(() => {
        overrideMarginLeft(null);
      }, 0);
    },
    [editor, overrideMarginLeft, tablePath]
  );
 
  const getColumnBoundaryOffset = React.useCallback(
    (colIndex: number, currentWidth: number) =>
      controlColumnWidth +
      effectiveColSizesRef.current
        .slice(0, colIndex)
        .reduce((total, colSize) => total + colSize, 0) +
      currentWidth,
    [controlColumnWidth]
  );
 
  const applyResize = React.useCallback(
    (event: PointerEvent, finished: boolean) => {
      const dragState = dragStateRef.current;
 
      if (!dragState) return;
 
      const currentPosition =
        dragState.direction === 'bottom' ? event.clientY : event.clientX;
      const delta = currentPosition - dragState.initialPosition;
 
      if (dragState.direction === 'bottom') {
        const newHeight = roundCellSizeToStep(
          dragState.initialSize + delta,
          undefined
        );
 
        if (finished) {
          commitRowSize(dragState.rowIndex, newHeight);
        } else {
          overrideRowSize(dragState.rowIndex, newHeight);
        }
 
        return;
      }
 
      if (dragState.direction === 'left') {
        const initial =
          effectiveColSizesRef.current[dragState.colIndex] ??
          dragState.initialSize;
        const complement = (width: number) =>
          initial + dragState.marginLeft - width;
        const nextMarginLeft = roundCellSizeToStep(
          resizeLengthClampStatic(dragState.marginLeft + delta, {
            max: complement(minColumnWidth),
            min: 0,
          }),
          undefined
        );
        const nextWidth = complement(nextMarginLeft);
 
        if (finished) {
          commitMarginLeft(nextMarginLeft);
          commitColSize(dragState.colIndex, nextWidth);
        } else if (deferColumnResize) {
          showDeferredResizeIndicator(
            controlColumnWidth + (nextMarginLeft - dragState.marginLeft)
          );
        } else {
          showResizeIndicatorAtOffset(
            controlColumnWidth + (nextMarginLeft - dragState.marginLeft)
          );
          overrideMarginLeft(nextMarginLeft);
          overrideColSize(dragState.colIndex, nextWidth);
        }
 
        return;
      }
 
      const currentInitial =
        effectiveColSizesRef.current[dragState.colIndex] ??
        dragState.initialSize;
      const nextInitial = effectiveColSizesRef.current[dragState.colIndex + 1];
      const complement = (width: number) =>
        currentInitial + nextInitial - width;
      const currentWidth = roundCellSizeToStep(
        resizeLengthClampStatic(currentInitial + delta, {
          max: nextInitial ? complement(minColumnWidth) : undefined,
          min: minColumnWidth,
        }),
        undefined
      );
      const nextWidth = nextInitial ? complement(currentWidth) : undefined;
 
      if (finished) {
        commitColSize(dragState.colIndex, currentWidth);
 
        if (nextWidth !== undefined) {
          commitColSize(dragState.colIndex + 1, nextWidth);
        }
      } else if (deferColumnResize) {
        showDeferredResizeIndicator(
          getColumnBoundaryOffset(dragState.colIndex, currentWidth)
        );
      } else {
        showResizeIndicatorAtOffset(
          getColumnBoundaryOffset(dragState.colIndex, currentWidth)
        );
        overrideColSize(dragState.colIndex, currentWidth);
 
        if (nextWidth !== undefined) {
          overrideColSize(dragState.colIndex + 1, nextWidth);
        }
      }
    },
    [
      commitColSize,
      commitMarginLeft,
      commitRowSize,
      controlColumnWidth,
      deferColumnResize,
      getColumnBoundaryOffset,
      showDeferredResizeIndicator,
      showResizeIndicatorAtOffset,
      minColumnWidth,
      overrideColSize,
      overrideMarginLeft,
      overrideRowSize,
    ]
  );
 
  const stopResize = React.useCallback(() => {
    cleanupListenersRef.current?.();
    cleanupListenersRef.current = null;
    activeHandleKeyRef.current = null;
    previewHandleKeyRef.current = null;
    dragStateRef.current = null;
 
    if (activeRowElementRef.current) {
      delete activeRowElementRef.current.dataset.tableResizing;
      activeRowElementRef.current = null;
    }
 
    hideDeferredResizeIndicator();
    hideResizeIndicator();
    clearFrozenRowHeights();
  }, [clearFrozenRowHeights, hideDeferredResizeIndicator, hideResizeIndicator]);
 
  const stopResizeRef = React.useRef(stopResize);
 
  React.useEffect(() => {
    stopResizeRef.current = stopResize;
  }, [stopResize]);
 
  React.useEffect(
    () => () => {
      stopResizeRef.current();
    },
    []
  );
 
  const startResize = React.useCallback(
    (
      event: React.PointerEvent<HTMLDivElement>,
      { colIndex, direction, handleKey, rowIndex }: TableResizeStartOptions
    ) => {
      const rowHeight =
        tableRef.current?.rows.item(rowIndex)?.getBoundingClientRect().height ??
        0;
 
      dragStateRef.current = {
        colIndex,
        direction,
        initialPosition: direction === 'bottom' ? event.clientY : event.clientX,
        initialSize:
          direction === 'bottom'
            ? rowHeight
            : (effectiveColSizesRef.current[colIndex] ??
              TABLE_DEFAULT_COLUMN_WIDTH),
        marginLeft: marginLeftRef.current,
        rowIndex,
      };
      activeHandleKeyRef.current = handleKey;
      previewHandleKeyRef.current = null;
 
      const rowElement = tableRef.current?.rows.item(rowIndex) ?? null;
 
      if (
        activeRowElementRef.current &&
        activeRowElementRef.current !== rowElement
      ) {
        delete activeRowElementRef.current.dataset.tableResizing;
      }
 
      activeRowElementRef.current = rowElement;
 
      if (rowElement) {
        rowElement.dataset.tableResizing = 'true';
      }
 
      cleanupListenersRef.current?.();
 
      if (direction !== 'bottom') {
        freezeRowHeights();
      }
 
      const handlePointerMove = (pointerEvent: PointerEvent) => {
        applyResize(pointerEvent, false);
      };
 
      const handlePointerEnd = (pointerEvent: PointerEvent) => {
        applyResize(pointerEvent, true);
        stopResize();
      };
 
      // oxlint-disable-next-line react-doctor/effect-needs-cleanup -- [P0 owned teardown] The ref-owned cleanup runs before restart, on pointer end, and on unmount.
      window.addEventListener('pointermove', handlePointerMove);
      window.addEventListener('pointerup', handlePointerEnd);
      window.addEventListener('pointercancel', handlePointerEnd);
 
      cleanupListenersRef.current = () => {
        window.removeEventListener('pointermove', handlePointerMove);
        window.removeEventListener('pointerup', handlePointerEnd);
        window.removeEventListener('pointercancel', handlePointerEnd);
      };
 
      if (deferColumnResize && direction !== 'bottom') {
        hideResizeIndicator();
        showDeferredResizeIndicator(
          direction === 'left'
            ? controlColumnWidth
            : getColumnBoundaryOffset(
                colIndex,
                effectiveColSizesRef.current[colIndex] ??
                  TABLE_DEFAULT_COLUMN_WIDTH
              )
        );
      } else {
        showResizeIndicator({ direction, event });
      }
 
      event.preventDefault();
      event.stopPropagation();
    },
    [
      controlColumnWidth,
      deferColumnResize,
      getColumnBoundaryOffset,
      hideResizeIndicator,
      showDeferredResizeIndicator,
      showResizeIndicator,
      stopResize,
      tableRef,
      applyResize,
      freezeRowHeights,
    ]
  );
 
  return React.useMemo(
    () => ({
      clearResizePreview,
      disableMarginLeft,
      showResizePreview,
      startResize,
    }),
    [clearResizePreview, disableMarginLeft, showResizePreview, startResize]
  );
}
 
export function TableElement(props: PlateElementProps<typeof TablePlugin>) {
  return <TableElementContent {...props} />;
}
 
function TableElementContent({
  children,
  ...props
}: PlateElementProps<typeof TablePlugin>) {
  const editor = useEditor();
  const { api, read, store } = useEditorPlugin(TablePlugin);
  const { disableMarginLeft = false } = store.get();
  const readOnly = useEditorReadOnly();
  const hasControls = !readOnly;
  const controlColumnWidth = hasControls ? TABLE_CONTROL_COLUMN_WIDTH : 0;
  const dragIndicatorRef = React.useRef<HTMLDivElement>(null);
  const hoverIndicatorRef = React.useRef<HTMLDivElement>(null);
  const tableRef = React.useRef<HTMLTableElement>(null);
  const [colSizeOverrides, setColSizeOverrides] = React.useState(
    new Map<number, number>()
  );
  const [rowSizeOverrides, setRowHeightOverrides] = React.useState(
    new Map<number, number>()
  );
  const [marginLeftOverride, overrideMarginLeft] = React.useState<
    number | null
  >(null);
  const overrideColSize = React.useCallback(
    (index: number, size: number | null) => {
      setColSizeOverrides((overrides) => {
        const next = new Map(overrides);
 
        if (size === null) next.delete(index);
        else next.set(index, size);
 
        return next;
      });
    },
    []
  );
  const overrideRowSize = React.useCallback(
    (index: number, size: number | null) => {
      setRowHeightOverrides((overrides) => {
        const next = new Map(overrides);
 
        if (size === null) next.delete(index);
        else next.set(index, size);
 
        return next;
      });
    },
    []
  );
  const marginLeft = disableMarginLeft
    ? 0
    : (marginLeftOverride ?? props.element.marginLeft ?? 0);
  const columnWidths = api.getOverriddenColumnSizes(
    props.element,
    colSizeOverrides
  );
  const baseColSizes = api.getOverriddenColumnSizes(props.element);
 
  useTableSelectionDOM(tableRef);
  const deferColumnResize =
    (columnWidths?.length ?? 0) * (props.element.children?.length ?? 0) >
    TABLE_DEFERRED_COLUMN_RESIZE_CELL_COUNT;
  const tablePath = useElementSelector(TablePlugin, ([, path]) => path);
  const wrapperRef = React.useRef<HTMLDivElement>(null);
  const tableNodeKey = props.editor.key(props.element);
  const hasMultiRowSelection = useEditorSelector((innerEditor) => {
    const view = innerEditor.plugin(TablePlugin).read.selection();
 
    return Boolean(
      view?.complete &&
      view.grid.problems.length === 0 &&
      view.tableKey === tableNodeKey &&
      view.anchors.length > 1 &&
      view.cellKeys.length === view.anchors.length &&
      view.bounds.maxRow > view.bounds.minRow
    );
  });
  const resizeController = useTableResizeController({
    baseColSizes,
    controlColumnWidth,
    deferColumnResize,
    dragIndicatorRef,
    hoverIndicatorRef,
    marginLeft,
    overrideColSize,
    overrideMarginLeft,
    overrideRowSize,
    tablePath,
    tableRef,
    wrapperRef,
  });
  const tableResizeContext = React.useMemo(
    () => ({
      ...resizeController,
      colSizeOverrides,
      hasMultiRowSelection,
      marginLeftOverride,
      rowSizeOverrides,
    }),
    [
      colSizeOverrides,
      hasMultiRowSelection,
      marginLeftOverride,
      resizeController,
      rowSizeOverrides,
    ]
  );
  const resolvedColSizes = React.useMemo(() => {
    if (columnWidths && columnWidths.length > 0) {
      return columnWidths.map(
        (colSize) => colSize || TABLE_DEFAULT_COLUMN_WIDTH
      );
    }
 
    return Array.from(
      { length: api.getColumnCount(props.element) },
      () => TABLE_DEFAULT_COLUMN_WIDTH
    );
  }, [api, columnWidths, props.element]);
  const tableStyle = React.useMemo(
    () => ({
      width: `${
        resolvedColSizes.reduce((total, colSize) => total + colSize, 0) +
        controlColumnWidth
      }px`,
    }),
    [controlColumnWidth, resolvedColSizes]
  );
 
  const isSelectingTable = useElementSelected({
    at: tableNodeKey,
    mode: 'node',
  });
 
  const content = (
    <PlateElement
      {...props}
      attributes={{
        ...props.attributes,
        'data-node-selection-highlight': 'self',
      }}
      className={cn(
        'overflow-x-auto py-5',
        hasControls && '-ml-2 *:data-[slot=node-selection-highlight]:left-2'
      )}
      style={{ paddingLeft: marginLeft }}
    >
      <TableResizeContext value={tableResizeContext}>
        <div
          ref={wrapperRef}
          className="relative w-fit [&:active:not(:has([data-table-resize-handle]:active))_[data-table-resize-handle]]:cursor-text"
        >
          <div
            ref={dragIndicatorRef}
            className="pointer-events-none absolute inset-y-0 z-36 hidden w-[3px] -translate-x-[1.5px] bg-ring/70"
            contentEditable={false}
          />
          <div
            ref={hoverIndicatorRef}
            className="pointer-events-none absolute inset-y-0 z-35 hidden w-[3px] -translate-x-[1.5px] bg-ring/80"
            contentEditable={false}
          />
          {/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- [P0 behavior-boundary] A new table interaction collapses the prior expanded cell selection. */}
          <table
            ref={tableRef}
            className="mr-0 ml-px table h-px table-fixed border-collapse [&:has([data-table-cell-selected=true])_*::selection]:bg-transparent"
            style={tableStyle}
            onMouseDown={() => {
              if ((read.selection()?.anchors.length ?? 0) > 1) {
                editor.update.selection.collapse();
              }
            }}
          >
            {resolvedColSizes.length > 0 && (
              <colgroup>
                {hasControls && (
                  <col
                    style={{
                      maxWidth: TABLE_CONTROL_COLUMN_WIDTH,
                      minWidth: TABLE_CONTROL_COLUMN_WIDTH,
                      width: TABLE_CONTROL_COLUMN_WIDTH,
                    }}
                  />
                )}
                {resolvedColSizes.map((colSize, index) => (
                  <col
                    key={index}
                    style={{
                      maxWidth: colSize,
                      minWidth: colSize,
                      width: colSize,
                    }}
                  />
                ))}
              </colgroup>
            )}
            <tbody className="min-w-full">{children}</tbody>
          </table>
 
          {isSelectingTable && (
            <div
              className={TABLE_SELECTION_OVERLAY_CLASS}
              contentEditable={false}
              data-slot="node-selection-highlight"
            />
          )}
        </div>
      </TableResizeContext>
    </PlateElement>
  );
 
  if (readOnly) {
    return content;
  }
 
  return <TableFloatingToolbar>{content}</TableFloatingToolbar>;
}
 
function TableFloatingToolbar({
  children,
  ...props
}: React.ComponentProps<typeof FloatingPopoverContent>) {
  const selectedCellCount = useEditorSelector(
    (editor) =>
      editor.plugin(TablePlugin).read.selection()?.cellKeys.length ?? 0
  );
  const selected = useElementSelected();
  const collapsedInside = useEditorSelector(
    (editor) => selected && editor.read.selection.isCollapsed()
  );
  const isFocusedLast = useFocusedLast();
  const isCollapsedToolbarOpen = isFocusedLast && collapsedInside;
  const isExpandedSelectionPending =
    isFocusedLast && !collapsedInside && selectedCellCount > 1;
  const isToolbarOpen = isCollapsedToolbarOpen || isExpandedSelectionPending;
 
  return (
    <FloatingPopover open={isToolbarOpen} modal={false}>
      <FloatingPopoverAnchor element={children as React.ReactElement} />
      {isCollapsedToolbarOpen && (
        <CollapsedTableFloatingToolbarContent {...props} />
      )}
      {isExpandedSelectionPending && (
        <DelayedExpandedSelectionTableFloatingToolbarContent {...props} />
      )}
    </FloatingPopover>
  );
}
 
function DelayedExpandedSelectionTableFloatingToolbarContent(
  props: React.ComponentProps<typeof FloatingPopoverContent>
) {
  const [isReady, setIsReady] = React.useState(false);
 
  React.useEffect(() => {
    const timeoutId = window.setTimeout(() => {
      setIsReady(true);
    }, TABLE_MULTI_SELECTION_TOOLBAR_DELAY_MS);
 
    return () => {
      window.clearTimeout(timeoutId);
    };
  }, []);
 
  if (!isReady) return null;
 
  return <ExpandedSelectionTableFloatingToolbarContent {...props} />;
}
 
function ExpandedSelectionTableFloatingToolbarContent(
  props: React.ComponentProps<typeof FloatingPopoverContent>
) {
  const editor = useEditor();
  const disableMerge = usePluginStore(TablePlugin, 'disableMerge');
  const canMergeSelection = useEditorSelector((innerEditor) =>
    innerEditor.plugin(TablePlugin).read.canMerge()
  );
  const canSplitSelection = useEditorSelector((innerEditor) =>
    innerEditor.plugin(TablePlugin).read.canSplit()
  );
  const canMerge = !disableMerge && canMergeSelection;
  const canSplit = !disableMerge && canSplitSelection;
 
  if (!canMerge && !canSplit) return null;
 
  return (
    <TableFloatingToolbarContent
      canMerge={canMerge}
      canSplit={canSplit}
      onMerge={() => {
        editor.plugin(TablePlugin).update.merge();
      }}
      onSplit={() => {
        editor.plugin(TablePlugin).update.split();
      }}
      {...props}
    />
  );
}
 
function CollapsedTableFloatingToolbarContent(
  props: React.ComponentProps<typeof FloatingPopoverContent>
) {
  const editor = useEditor();
  const element = useElement(TablePlugin);
  const disableMerge = usePluginStore(TablePlugin, 'disableMerge');
  const canSplitSelection = useEditorSelector((innerEditor) =>
    innerEditor.plugin(TablePlugin).read.canSplit()
  );
  const canSplit = !disableMerge && canSplitSelection;
 
  return (
    <TableFloatingToolbarContent
      canSplit={canSplit}
      collapsedInside
      onDeleteTable={() => {
        editor.update.nodes.remove({ at: element });
        editor.api.dom.focus();
      }}
      onDeleteColumn={() => {
        editor.plugin(TablePlugin).update.removeColumn();
      }}
      onDeleteRow={() => {
        editor.plugin(TablePlugin).update.removeRow();
      }}
      onInsertColumnAfter={() => {
        editor.plugin(TablePlugin).update.insertColumn();
      }}
      onInsertColumnBefore={() => {
        editor.plugin(TablePlugin).update.insertColumn({ before: true });
      }}
      onInsertRowAfter={() => {
        editor.plugin(TablePlugin).update.insertRow();
      }}
      onInsertRowBefore={() => {
        editor.plugin(TablePlugin).update.insertRow({ before: true });
      }}
      onSplit={() => {
        editor.plugin(TablePlugin).update.split();
      }}
      {...props}
    />
  );
}
 
function TableFloatingToolbarContent({
  canMerge = false,
  canSplit = false,
  collapsedInside = false,
  onDeleteColumn,
  onDeleteRow,
  onDeleteTable,
  onInsertColumnAfter,
  onInsertColumnBefore,
  onInsertRowAfter,
  onInsertRowBefore,
  onMerge,
  onSplit,
  ...props
}: React.ComponentProps<typeof FloatingPopoverContent> & {
  canMerge?: boolean;
  canSplit?: boolean;
  collapsedInside?: boolean;
  onDeleteColumn?: () => void;
  onDeleteRow?: () => void;
  onDeleteTable?: () => void;
  onInsertColumnAfter?: () => void;
  onInsertColumnBefore?: () => void;
  onInsertRowAfter?: () => void;
  onInsertRowBefore?: () => void;
  onMerge?: () => void;
  onSplit?: () => void;
}) {
  return (
    <FloatingPopoverContent
      className="w-auto border-0 bg-transparent p-0 shadow-none ring-0"
      onInitialFocus={(e) => {
        e.preventDefault();
      }}
      contentEditable={false}
      {...props}
    >
      <Toolbar
        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"
        contentEditable={false}
      >
        <ToolbarGroup>
          <ColorDropdownMenu tooltip="Background color">
            <PaintBucketIcon />
          </ColorDropdownMenu>
          {canMerge && onMerge && (
            <ToolbarButton
              aria-label="Merge cells"
              onClick={onMerge}
              tooltip="Merge cells"
            >
              <CombineIcon />
            </ToolbarButton>
          )}
          {canSplit && onSplit && (
            <ToolbarButton
              aria-label="Split cell"
              onClick={onSplit}
              tooltip="Split cell"
            >
              <SquareSplitHorizontalIcon />
            </ToolbarButton>
          )}
 
          <DropdownMenu modal={false}>
            <DropdownMenuTrigger>
              <ToolbarButton aria-label="Cell borders" tooltip="Cell borders">
                <Grid2X2Icon />
              </ToolbarButton>
            </DropdownMenuTrigger>
 
            <DropdownMenuPortal>
              <TableBordersDropdownMenuContent />
            </DropdownMenuPortal>
          </DropdownMenu>
 
          {collapsedInside && (
            <ToolbarGroup>
              <ToolbarButton
                aria-label="Delete table"
                onClick={onDeleteTable}
                tooltip="Delete table"
              >
                <Trash2Icon />
              </ToolbarButton>
            </ToolbarGroup>
          )}
        </ToolbarGroup>
 
        {collapsedInside && (
          <ToolbarGroup>
            <ToolbarButton
              aria-label="Insert row before"
              onClick={onInsertRowBefore}
              tooltip="Insert row before"
            >
              <ArrowUp />
            </ToolbarButton>
            <ToolbarButton
              aria-label="Insert row after"
              onClick={onInsertRowAfter}
              tooltip="Insert row after"
            >
              <ArrowDown />
            </ToolbarButton>
            <ToolbarButton
              aria-label="Delete row"
              onClick={onDeleteRow}
              tooltip="Delete row"
            >
              <XIcon />
            </ToolbarButton>
          </ToolbarGroup>
        )}
 
        {collapsedInside && (
          <ToolbarGroup>
            <ToolbarButton
              aria-label="Insert column before"
              onClick={onInsertColumnBefore}
              tooltip="Insert column before"
            >
              <ArrowLeft />
            </ToolbarButton>
            <ToolbarButton
              aria-label="Insert column after"
              onClick={onInsertColumnAfter}
              tooltip="Insert column after"
            >
              <ArrowRight />
            </ToolbarButton>
            <ToolbarButton
              aria-label="Delete column"
              onClick={onDeleteColumn}
              tooltip="Delete column"
            >
              <XIcon />
            </ToolbarButton>
          </ToolbarGroup>
        )}
      </Toolbar>
    </FloatingPopoverContent>
  );
}
 
function TableBordersDropdownMenuContent(
  props: React.ComponentProps<typeof DropdownMenuContent>
) {
  const editor = useEditor();
  const borderStates = useEditorSelector((innerEditor) =>
    innerEditor.plugin(TablePlugin).read.getSelectedCellsBorders()
  );
 
  return (
    <DropdownMenuContent
      className="min-w-[220px]"
      onFinalFocus={(e) => {
        e.preventDefault();
        editor.api.dom.focus();
      }}
      align="start"
      side="right"
      sideOffset={0}
      {...props}
    >
      <DropdownMenuGroup>
        <DropdownMenuCheckboxItem
          checked={borderStates.top}
          onCheckedChange={() => {
            editor.plugin(TablePlugin).update.toggleBorders({ border: 'top' });
          }}
        >
          <BorderTopIcon />
          <div>Top Border</div>
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={borderStates.right}
          onCheckedChange={() => {
            editor
              .plugin(TablePlugin)
              .update.toggleBorders({ border: 'right' });
          }}
        >
          <BorderRightIcon />
          <div>Right Border</div>
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={borderStates.bottom}
          onCheckedChange={() => {
            editor
              .plugin(TablePlugin)
              .update.toggleBorders({ border: 'bottom' });
          }}
        >
          <BorderBottomIcon />
          <div>Bottom Border</div>
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={borderStates.left}
          onCheckedChange={() => {
            editor.plugin(TablePlugin).update.toggleBorders({ border: 'left' });
          }}
        >
          <BorderLeftIcon />
          <div>Left Border</div>
        </DropdownMenuCheckboxItem>
      </DropdownMenuGroup>
 
      <DropdownMenuGroup>
        <DropdownMenuCheckboxItem
          checked={borderStates.none}
          onCheckedChange={() => {
            editor.plugin(TablePlugin).update.toggleBorders({ border: 'none' });
          }}
        >
          <BorderNoneIcon />
          <div>No Border</div>
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={borderStates.outer}
          onCheckedChange={() => {
            editor
              .plugin(TablePlugin)
              .update.toggleBorders({ border: 'outer' });
          }}
        >
          <BorderAllIcon />
          <div>Outside Borders</div>
        </DropdownMenuCheckboxItem>
      </DropdownMenuGroup>
    </DropdownMenuContent>
  );
}
 
function ColorDropdownMenu({
  children,
  tooltip,
}: {
  children: React.ReactNode;
  tooltip: string;
}) {
  const [open, setOpen] = React.useState(false);
 
  const editor = useEditor();
 
  const onUpdateColor = React.useCallback(
    (color: string) => {
      setOpen(false);
      editor.plugin(TablePlugin).update.setCellBackground({ color });
    },
    [editor]
  );
 
  const onClearColor = React.useCallback(() => {
    setOpen(false);
    editor.plugin(TablePlugin).update.setCellBackground({ color: null });
  }, [editor]);
 
  return (
    <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
      <DropdownMenuTrigger>
        <ToolbarButton aria-label={tooltip} tooltip={tooltip}>
          {children}
        </ToolbarButton>
      </DropdownMenuTrigger>
 
      <DropdownMenuContent align="start">
        <ToolbarMenuGroup label="Colors">
          <ColorDropdownMenuItems
            className="px-2"
            colors={DEFAULT_COLORS}
            updateColor={onUpdateColor}
          />
        </ToolbarMenuGroup>
        <DropdownMenuGroup>
          <DropdownMenuItem className="p-2" onClick={onClearColor}>
            <EraserIcon />
            <span>Clear</span>
          </DropdownMenuItem>
        </DropdownMenuGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
 
export function TableRowElement({
  children,
  ...props
}: PlateElementProps<typeof TableRowPlugin>) {
  const { element } = props;
  const readOnly = useEditorReadOnly();
  const rowIndex = useElementSelector(TableRowPlugin, ([, path]) => {
    const index = path.at(-1);
 
    if (index === undefined) {
      throw new Error('Table row path must include an index.');
    }
 
    return index;
  });
  const rowSize = useElementSelector(TableRowPlugin, ([node]) => node.height);
  const { hasMultiRowSelection, rowSizeOverrides } = useTableResizeContext();
  const rowMinHeight = rowSizeOverrides.get(rowIndex) ?? rowSize;
  const hasControls = !readOnly;
 
  const { isDragging, nodeRef, previewRef, handleRef } = useDraggable({
    element,
    type: element.type,
    canDropNode: ({ dragEntry, dropEntry }) =>
      PathApi.equals(
        PathApi.parent(dragEntry[1]),
        PathApi.parent(dropEntry[1])
      ),
    onDropHandler: (editor, { dragItem }) => {
      if (!('key' in dragItem)) return;
      const key = Array.isArray(dragItem.key) ? dragItem.key[0] : dragItem.key;
 
      if (key) {
        const path = editor.read.nodes.path(key);
 
        if (!path) return;
 
        const range = editor.read.ranges.get(path);
 
        if (!range) return;
 
        editor.update.selection.set(range);
        editor.api.dom.focus();
      }
    },
  });
 
  return (
    <PlateElement
      {...props}
      ref={useComposedRef(props.ref, previewRef, nodeRef)}
      as="tr"
      className={cn('group/row', isDragging && 'opacity-50')}
      style={
        {
          '--tableRowMinHeight': rowMinHeight ? `${rowMinHeight}px` : undefined,
        } as React.CSSProperties
      }
    >
      {hasControls && (
        <td
          className="w-2 max-w-2 min-w-2 p-0 select-none"
          contentEditable={false}
        >
          {!hasMultiRowSelection && (
            <>
              <RowDragHandle dragRef={handleRef} />
              <RowDropLine />
            </>
          )}
        </td>
      )}
 
      {children}
    </PlateElement>
  );
}
 
function RowDragHandle({ dragRef }: { dragRef: React.Ref<HTMLButtonElement> }) {
  const editor = useEditor();
  const element = useElement(TableRowPlugin);
 
  return (
    <Button
      ref={dragRef}
      aria-label="Select or move row"
      variant="outline"
      className={cn(
        '-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',
        'cursor-grab active:cursor-grabbing',
        'opacity-0 transition-opacity duration-100 group-hover/row:opacity-100 group-data-[table-resizing=true]/row:opacity-0'
      )}
      onClick={() => {
        const range = editor.read.ranges.get(element);
 
        if (!range) return;
 
        editor.update.selection.set(range);
        editor.api.dom.focus();
      }}
    >
      <GripVertical className="text-muted-foreground" />
    </Button>
  );
}
 
function RowDropLine() {
  const { dropLine } = useDropLine();
 
  if (!dropLine) return null;
 
  return (
    <div
      className={cn(
        'absolute inset-x-0 left-2 z-50 h-0.5 bg-brand/50',
        dropLine === 'top' ? '-top-px' : '-bottom-px'
      )}
    />
  );
}
 
export function TableCellElement(
  props: PlateElementProps<typeof TableCellPlugin>
) {
  const editor = useEditor();
  const readOnly = useEditorReadOnly();
  const { element } = props;
  const isHeader = element.header === true;
 
  const tableKey = useElementSelector(TablePlugin, ([node]) =>
    editor.key(node)
  );
  const rowKey = useElementSelector(TableRowPlugin, ([node]) =>
    editor.key(node)
  );
  const isSelectingTable = useElementSelected({
    at: tableKey,
    mode: 'node',
  });
  const isSelectingRow =
    useElementSelected({ at: rowKey, mode: 'node' }) || isSelectingTable;
  const cellIndices = useEditorSelector(
    (innerEditor) =>
      innerEditor.plugin(TablePlugin).read.getCellIndices(element),
    {
      equalityFn: (next, previous) =>
        next?.col === previous?.col && next?.row === previous?.row,
      shouldUpdate: (change) =>
        !change ||
        change.changed.hasAny('properties') ||
        change.changed.hasAny('structure') ||
        change.changed.hasAny('replace') ||
        change.changed.hasAny('root-order'),
    }
  );
  const indices = cellIndices ?? { col: 0, row: 0 };
  const table = editor.plugin(TablePlugin);
  const borders = table.read.getCellBorders({
    cellIndices: indices,
    element,
  });
  const colSpan = table.api.getColSpan(element);
  const rowSpan = table.api.getRowSpan(element);
  const colIndex = indices.col + colSpan - 1;
  const rowIndex = indices.row + rowSpan - 1;
 
  return (
    <PlateElement
      {...props}
      as={isHeader ? 'th' : 'td'}
      className={cn(
        'relative h-full overflow-visible border-none bg-background p-0',
        element.backgroundColor ? 'bg-(--cellBackground)' : 'bg-background',
        isHeader && 'text-left *:m-0',
        'before:size-full',
        'data-[table-cell-selected=true]:before:z-10',
        'data-[table-cell-selected=true]:before:bg-brand/5',
        "before:absolute before:box-border before:select-none before:content-['']",
        borders.bottom?.width && 'before:border-b before:border-b-border',
        borders.right?.width && 'before:border-r before:border-r-border',
        borders.left?.width && 'before:border-l before:border-l-border',
        borders.top?.width && 'before:border-t before:border-t-border'
      )}
      style={
        {
          '--cellBackground': element.backgroundColor,
        } as React.CSSProperties
      }
      attributes={{
        ...props.attributes,
        colSpan,
        rowSpan,
      }}
    >
      <div
        className="relative z-20 box-border h-full px-3 py-2"
        style={
          rowSpan === 1
            ? { minHeight: 'var(--tableRowMinHeight, 0px)' }
            : undefined
        }
      >
        {props.children}
      </div>
 
      {!readOnly && (
        <TableCellResizeControls colIndex={colIndex} rowIndex={rowIndex} />
      )}
 
      {isSelectingRow && (
        <div
          className={TABLE_SELECTION_OVERLAY_CLASS}
          contentEditable={false}
          data-slot="node-selection-highlight"
        />
      )}
    </PlateElement>
  );
}
 
function TableCellResizeControls({
  colIndex,
  rowIndex,
}: {
  colIndex: number;
  rowIndex: number;
}) {
  const {
    clearResizePreview,
    disableMarginLeft,
    showResizePreview,
    startResize,
  } = useTableResizeContext();
  const rightHandleKey = `right:${rowIndex}:${colIndex}`;
  const bottomHandleKey = `bottom:${rowIndex}:${colIndex}`;
  const leftHandleKey = `left:${rowIndex}:${colIndex}`;
  const isLeftHandle = colIndex === 0 && !disableMarginLeft;
 
  return (
    <div
      className="pointer-events-none absolute inset-0 z-30 select-none"
      contentEditable={false}
      data-plite-root-chrome-ignore="true"
      suppressContentEditableWarning={true}
    >
      <div
        className="pointer-events-auto absolute -top-2 -right-1 z-40 h-[calc(100%_+_8px)] w-2 cursor-col-resize touch-none"
        data-table-resize-handle="column-end"
        onPointerEnter={(event) => {
          showResizePreview(event, {
            colIndex,
            direction: 'right',
            handleKey: rightHandleKey,
            rowIndex,
          });
        }}
        onPointerMove={(event) => {
          showResizePreview(event, {
            colIndex,
            direction: 'right',
            handleKey: rightHandleKey,
            rowIndex,
          });
        }}
        onPointerLeave={() => {
          clearResizePreview(rightHandleKey);
        }}
        onPointerDown={(event) => {
          startResize(event, {
            colIndex,
            direction: 'right',
            handleKey: rightHandleKey,
            rowIndex,
          });
        }}
      />
      <div
        className="pointer-events-auto absolute -bottom-1 left-0 z-40 h-2 w-full cursor-row-resize touch-none"
        data-table-resize-handle="row-end"
        onPointerEnter={(event) => {
          showResizePreview(event, {
            colIndex,
            direction: 'bottom',
            handleKey: bottomHandleKey,
            rowIndex,
          });
        }}
        onPointerMove={(event) => {
          showResizePreview(event, {
            colIndex,
            direction: 'bottom',
            handleKey: bottomHandleKey,
            rowIndex,
          });
        }}
        onPointerLeave={() => {
          clearResizePreview(bottomHandleKey);
        }}
        onPointerDown={(event) => {
          startResize(event, {
            colIndex,
            direction: 'bottom',
            handleKey: bottomHandleKey,
            rowIndex,
          });
        }}
      />
      {isLeftHandle && (
        <div
          className="pointer-events-auto absolute top-0 -left-1 z-40 h-full w-2 cursor-col-resize touch-none"
          data-table-resize-handle="column-start"
          onPointerEnter={(event) => {
            showResizePreview(event, {
              colIndex,
              direction: 'left',
              handleKey: leftHandleKey,
              rowIndex,
            });
          }}
          onPointerMove={(event) => {
            showResizePreview(event, {
              colIndex,
              direction: 'left',
              handleKey: leftHandleKey,
              rowIndex,
            });
          }}
          onPointerLeave={() => {
            clearResizePreview(leftHandleKey);
          }}
          onPointerDown={(event) => {
            startResize(event, {
              colIndex,
              direction: 'left',
              handleKey: leftHandleKey,
              rowIndex,
            });
          }}
        />
      )}
    </div>
  );
}
 
export const TableKit = [
  TablePlugin.configure({ component: TableElement }),
  TableRowPlugin.configure({ component: TableRowElement }),
  TableCellPlugin.configure({ component: TableCellElement }),
];
 
function BorderAllIcon(props: LucideProps) {
  return (
    <svg
      fill="none"
      height="15"
      viewBox="0 0 15 15"
      width="15"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <title>Border All</title>
      <path
        clipRule="evenodd"
        d="M0.25 1C0.25 0.585786 0.585786 0.25 1 0.25H14C14.4142 0.25 14.75 0.585786 14.75 1V14C14.75 14.4142 14.4142 14.75 14 14.75H1C0.585786 14.75 0.25 14.4142 0.25 14V1ZM1.75 1.75V13.25H13.25V1.75H1.75Z"
        fill="currentColor"
        fillRule="evenodd"
      />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="5" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="3" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="7" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="5" y="7" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="3" y="7" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="9" y="7" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="11" y="7" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="9" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="11" />
    </svg>
  );
}
 
function BorderBottomIcon(props: LucideProps) {
  return (
    <svg
      fill="none"
      height="15"
      viewBox="0 0 15 15"
      width="15"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <title>Border Bottom</title>
      <path
        clipRule="evenodd"
        d="M1 13.25L14 13.25V14.75L1 14.75V13.25Z"
        fill="currentColor"
        fillRule="evenodd"
      />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="5" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="5" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="3" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="3" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="7" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="1" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="7" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="1" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="5" y="7" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="5" y="1" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="3" y="7" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="3" y="1" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="9" y="7" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="9" y="1" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="11" y="7" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="11" y="1" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="9" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="9" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="11" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="11" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="5" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="3" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="7" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="1" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="9" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="11" />
    </svg>
  );
}
 
function BorderLeftIcon(props: LucideProps) {
  return (
    <svg
      fill="none"
      height="15"
      viewBox="0 0 15 15"
      width="15"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <title>Border Left</title>
      <path
        clipRule="evenodd"
        d="M1.75 1L1.75 14L0.249999 14L0.25 1L1.75 1Z"
        fill="currentColor"
        fillRule="evenodd"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 10 7)"
        width="1"
        x="10"
        y="7"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 10 13)"
        width="1"
        x="10"
        y="13"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 12 7)"
        width="1"
        x="12"
        y="7"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 12 13)"
        width="1"
        x="12"
        y="13"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 8 7)"
        width="1"
        x="8"
        y="7"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 14 7)"
        width="1"
        x="14"
        y="7"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 8 13)"
        width="1"
        x="8"
        y="13"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 14 13)"
        width="1"
        x="14"
        y="13"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 8 5)"
        width="1"
        x="8"
        y="5"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 14 5)"
        width="1"
        x="14"
        y="5"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 8 3)"
        width="1"
        x="8"
        y="3"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 14 3)"
        width="1"
        x="14"
        y="3"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 8 9)"
        width="1"
        x="8"
        y="9"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 14 9)"
        width="1"
        x="14"
        y="9"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 8 11)"
        width="1"
        x="8"
        y="11"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 14 11)"
        width="1"
        x="14"
        y="11"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 6 7)"
        width="1"
        x="6"
        y="7"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 6 13)"
        width="1"
        x="6"
        y="13"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 4 7)"
        width="1"
        x="4"
        y="7"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 4 13)"
        width="1"
        x="4"
        y="13"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 10 1)"
        width="1"
        x="10"
        y="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 12 1)"
        width="1"
        x="12"
        y="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 8 1)"
        width="1"
        x="8"
        y="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 14 1)"
        width="1"
        x="14"
        y="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 6 1)"
        width="1"
        x="6"
        y="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(90 4 1)"
        width="1"
        x="4"
        y="1"
      />
    </svg>
  );
}
 
function BorderNoneIcon(props: LucideProps) {
  return (
    <svg
      fill="none"
      height="15"
      viewBox="0 0 15 15"
      width="15"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <title>Border None</title>
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="5.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="5.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="3.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="3.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="7.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="13.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="1.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="7.025" />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        width="1"
        x="13"
        y="13.025"
      />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="1.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="5" y="7.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="5" y="13.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="5" y="1.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="3" y="7.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="3" y="13.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="3" y="1.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="9" y="7.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="9" y="13.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="9" y="1.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="11" y="7.025" />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        width="1"
        x="11"
        y="13.025"
      />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="11" y="1.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="9.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="9.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="11.025" />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        width="1"
        x="13"
        y="11.025"
      />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="5.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="3.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="7.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="13.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="1.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="9.025" />
      <rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="11.025" />
    </svg>
  );
}
 
function BorderRightIcon(props: LucideProps) {
  return (
    <svg
      fill="none"
      height="15"
      viewBox="0 0 15 15"
      width="15"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <title>Border Right</title>
      <path
        clipRule="evenodd"
        d="M13.25 1L13.25 14L14.75 14L14.75 1L13.25 1Z"
        fill="currentColor"
        fillRule="evenodd"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 5 7)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 5 13)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 3 7)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 3 13)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 7 7)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 1 7)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 7 13)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 1 13)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 7 5)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 1 5)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 7 3)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 1 3)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 7 9)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 1 9)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 7 11)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 1 11)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 9 7)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 9 13)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 11 7)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 11 13)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 5 1)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 3 1)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 7 1)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 1 1)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 9 1)"
        width="1"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="matrix(0 1 1 0 11 1)"
        width="1"
      />
    </svg>
  );
}
 
function BorderTopIcon(props: LucideProps) {
  return (
    <svg
      fill="none"
      height="15"
      viewBox="0 0 15 15"
      width="15"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <title>Border Top</title>
      <path
        clipRule="evenodd"
        d="M14 1.75L1 1.75L1 0.249999L14 0.25L14 1.75Z"
        fill="currentColor"
        fillRule="evenodd"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 8 10)"
        width="1"
        x="8"
        y="10"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 2 10)"
        width="1"
        x="2"
        y="10"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 8 12)"
        width="1"
        x="8"
        y="12"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 2 12)"
        width="1"
        x="2"
        y="12"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 8 8)"
        width="1"
        x="8"
        y="8"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 8 14)"
        width="1"
        x="8"
        y="14"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 2 8)"
        width="1"
        x="2"
        y="8"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 2 14)"
        width="1"
        x="2"
        y="14"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 10 8)"
        width="1"
        x="10"
        y="8"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 10 14)"
        width="1"
        x="10"
        y="14"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 12 8)"
        width="1"
        x="12"
        y="8"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 12 14)"
        width="1"
        x="12"
        y="14"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 6 8)"
        width="1"
        x="6"
        y="8"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 6 14)"
        width="1"
        x="6"
        y="14"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 4 8)"
        width="1"
        x="4"
        y="8"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 4 14)"
        width="1"
        x="4"
        y="14"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 8 6)"
        width="1"
        x="8"
        y="6"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 2 6)"
        width="1"
        x="2"
        y="6"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 8 4)"
        width="1"
        x="8"
        y="4"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 2 4)"
        width="1"
        x="2"
        y="4"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 14 10)"
        width="1"
        x="14"
        y="10"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 14 12)"
        width="1"
        x="14"
        y="12"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 14 8)"
        width="1"
        x="14"
        y="8"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 14 14)"
        width="1"
        x="14"
        y="14"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 14 6)"
        width="1"
        x="14"
        y="6"
      />
      <rect
        fill="currentColor"
        height="1"
        rx=".5"
        transform="rotate(-180 14 4)"
        width="1"
        x="14"
        y="4"
      />
    </svg>
  );
}
'use client';
 
import { useDraggable, useDropLine } from '@platejs/dnd';
import { resizeLengthClampStatic } from '@platejs/resizable';
import {
  TableCellPlugin,
  TablePlugin,
  TableRowPlugin,
  useTableSelectionDOM,
} from '@platejs/table/react';
import {
  type LucideProps,
  ArrowDown,
  ArrowLeft,
  ArrowRight,
  ArrowUp,
  CombineIcon,
  EraserIcon,
  Grid2X2Icon,
  GripVertical,
  PaintBucketIcon,
  SquareSplitHorizontalIcon,
  Trash2Icon,
  XIcon,
} from 'lucide-react'














































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































  • TableElement: Renders table containers.
  • TableRowElement: Renders table rows.
  • TableCellElement: Renders tableCell nodes as <td> or <th> from the node's header property.

Add Kit

Add the kit to your plugins:

import { createPlateEditor } from 'platejs/react';
import { TableKit } from '@/components/editor/table';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    ...TableKit,
  ],
});
import { createPlateEditor } from 'platejs/react';
import { TableKit } from '@/components/editor/table';
 
const editor = createPlateEditor({




Manual Usage

Installation

pnpm add @platejs/table
pnpm add @platejs/table

Add Plugin

Include TablePlugin in your Plate plugins array when creating the editor.

TablePlugin installs TableRowPlugin and TableCellPlugin as required structural dependencies.

import { TablePlugin } from '@platejs/table/react';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    TablePlugin,
  ],
});
import { TablePlugin } from '@platejs/table/react';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({




Configure Plugins

Configure the table owner and add complete row or cell descriptors when you need to replace a dependency's component or configuration.

import {
  TableCellPlugin,
  TablePlugin,
  TableRowPlugin,
} from '@platejs/table/react';
import { createPlateEditor } from 'platejs/react';
import {
  TableCellElement,
  TableElement,
  TableRowElement,
} from '@/components/editor/table';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    TablePlugin.configure({
      component: TableElement,
      initialState: {
        initialTableWidth: 600,
        disableMerge: false,
        minColumnWidth: 48,





  • .configure({ component }): Assigns TableElement to render table containers.
  • component: Assigns components for table rows and cells. TableCellElement reads element.header to render header cells.
  • initialState.initialTableWidth: Sets the initial width for new tables.
  • initialState.disableMerge: Disables cell merging functionality.
  • initialState.minColumnWidth: Sets the minimum width for table columns.

Column widths are stored once on the table's columnWidths. Table renderers read that vector in the table renderer; null preserves an unknown imported width without treating it as zero pixels. Pointer previews use transient overrides, then commit through editor.plugin(TablePlugin).update.setColumnWidth. Row resizing follows the same pattern with each row's height.

Add Toolbar Button

You can add TableToolbarButton to your Toolbar to insert tables.

Insert Toolbar Button

You can add this item to the Insert Toolbar Button to insert table elements:

{
  icon: <TableIcon />,
  label: 'Table',
  value: PLUGINS.table,
}
{
  icon: <TableIcon />,
  label: 'Table',
  value: PLUGINS.table,
}

Disable Merging Example

Loading…

Plugins

TablePlugin

Owns the table schema and behavior. Requires the row and cell plugins.

Options

    Disables the merging behavior of cells.

    Disables the expansion of the table when inserting cells.

    Disables the left resizer of the first column in the table.

    Disables unsetting the width of the first column when the table has only one column. Set this to true if you want to resize the table width when there's only one column. Leave it false if you have a full-width table.

    If defined, a normalizer will set each undefined table columnWidths to this value divided by the number of columns. Note that merged cells are not yet supported.

    The minimum width of a column in the table.

    • Default: 48

TableRowPlugin

Required structural plugin for table rows. Add it directly only to replace its default configuration or component.

TableCellPlugin

Required structural plugin for all table cells. Header cells use the same tableCell type with header: true; add the plugin directly only to replace its default configuration or component.

API

editor.api.table.create

Parameters

    Extends GetEmptyRowNodeOptions.

Options

    Specify true if the table has a header row.

    The number of rows in the table.

    • Default: 0

    The number of columns in the table.

Returns

    The table node.

editor.api.table.createCell

Creates an empty cell node for a table.

Options

    Specify true if the cell is a header cell.

    The row element. If header is not specified, it will determine if the cell is a header cell based on the row's children.

    The children of the new cell node.

    • Default: [{ children: [{ text: '' }], type: editor.plugin(PLUGINS.paragraph).schema.type }]

Returns

    The cell node.

editor.api.table.createRow

Creates an empty row node with the specified number of columns.

Options

    Specify true if the row is a header row.

    The number of columns in the row.

    • Default: 1

Returns

    The row node.

editor.read.table.getCellBorders

Gets the border styles for a table cell, handling special cases for first row and first column cells.

Options

    The table cell element to get the border styles for.

    The default border style to use when cell borders are not defined.

Returns

    An object containing:

editor.api.table.getCellChildren

Gets the children of a table cell.

Parameters

    The table cell element.

Returns

    The children of the table cell.

editor.read.table.getCellSize

Gets the width and minimum height of a table cell, taking into account column spans and column sizes.

Options

    The table cell element to get the size for.

    Optional array of column sizes. If not provided, will use the table's overridden column sizes.

    Optional precomputed row and column indices for the cell.

    Optional row height. If not provided, the value is read from the cell's row.

Returns

    The total width of the cell, calculated by summing the widths of all columns it spans.

    The minimum height of the cell, derived from the row's size property or 0.

editor.api.table.getColSpan

Gets the column span of a table cell.

Parameters

    The table cell element to get the column span for.

Returns

    The number of columns this cell spans.

    • Default: 1

editor.api.table.getRowSpan

Gets the row span of a table cell.

Parameters

    The table cell element to get the row span for.

Returns

    The number of rows this cell spans.

    • Default: 1

editor.read.table.getNextCell

Gets the next cell in the table.

Parameters

    The entry of the current cell.

    The path of the current cell.

    The entry of the current row.

Returns

    The node entry of the cell in the next row, or undefined if the current row is the last row.

editor.read.table.getPreviousCell

Gets the previous cell in the table.

Parameters

    The entry of the current cell.

    The path of the current cell.

    The entry of the current row.

Returns

    The node entry of the cell in the previous row, or undefined if the current row is the first row.

editor.api.table.getColumnCount

Gets the number of columns in a table.

Parameters

    The table node for which to retrieve the column count.

Returns

    The number of columns in the table.

editor.read.table.getCellIndices

Gets the row and column indices of a cell.

Parameters

    The table cell.

Returns

    The cell coordinates.

editor.read.table.getEntries

Gets the table, row, and cell node entries based on the current selection or a specified location.

Options

    The location where the table cell is located.

    • Default: editor.read.selection()

Returns

    The table node entry.

    The row node entry.

    The cell node entry.

editor.read.table.getGridAbove

Gets the sub table above the anchor and focus positions based on the specified format (tables or cells).

Options

    The format of the sub table to retrieve.

    • Default: 'table'

Returns

    The sub table entries.

editor.read.table.getGridByRange

Gets the sub table between two cell paths within a given range.

Options

    The range specifying the start and end cell paths.

    The format of the output.

    • Default: 'table'

Returns

    The sub table entries.

editor.read.table.getAdjacentCell

Gets a cell relative to the current cell.

Options

    The current cell path. Defaults to the current selection.

    The column offset.

    The row offset.

Returns

    The adjacent cell entry.

editor.read.table.isBorderHidden

Checks if the border of a table cell or the table itself is hidden based on the specified border direction.

Parameters

    The border direction to check.

Returns

    true if the border is hidden, false otherwise.

Editing Behavior

TablePlugin owns keyboard, clipboard, deletion, paste, normalization, and selection behavior. Use its scoped update portal for table mutations; installing the plugin activates the rest.

Transforms

editor.plugin(TablePlugin).update.insert

Inserts a table at the current selection if there is no existing table in the editor. Selects the start of the inserted table.

Parameters

    Extends GetEmptyRowNodeOptions.

    The options for inserting the table nodes.

editor.plugin(TablePlugin).update.insertColumn

Inserts a column into the table at the current selection or a specified cell path.

Options

    The exact path of the cell to insert the column at. This overrules the fromCell option.

    If true, insert the column before the current column instead of after.

    The path of the cell to insert the column from.

    If true, the inserted column will be treated as a header column.

    If true, the inserted column will be selected after insertion.

editor.plugin(TablePlugin).update.insertRow

Inserts a row into the table at the current selection or a specified row path.

Options

    Exact path of the row to insert the column at. Pass the table path to insert at the end of the table. Will overrule fromRow.

    If true, insert the row before the current row instead of after.

    The path of the row to insert the new row from.

    If true, the inserted row will be treated as a header row.

    If true, the inserted row will be selected after insertion.

editor.plugin(TablePlugin).update.removeColumn

Deletes the column containing the selected cell in a table.

editor.plugin(TablePlugin).update.removeRow

Deletes the row containing the selected cell in a table.

editor.plugin(TablePlugin).update.remove

Deletes the entire table.

editor.plugin(TablePlugin).update.merge

Merges multiple selected cells into one.

The merged cell will:

  • Have a colSpan equal to the number of columns spanned by the selected cells
  • Have a rowSpan equal to the number of rows spanned by the selected cells
  • Contain the combined content of all merged cells (non-empty cells only)
  • Inherit the header status from the first selected cell

editor.plugin(TablePlugin).update.split

Splits a merged cell back into individual cells.

The split operation will:

  • Create new cells for each column and row that was spanned
  • Copy the header status from the original merged cell
  • Place the original cell's content in the first cell
  • Create empty cells for the remaining spaces

editor.plugin(TablePlugin).update.moveSelection

Moves the selection by cell unit within a table.

Options

    The location to move the selection from.

    Set to true to move the selection to the cell above, false to move the selection to the cell below.

    The edge to expand the cell selection to.

    Set to true to move the selection from only one selected cell.

editor.plugin(TablePlugin).update.setBorderSize

Sets the size of the specified border in a table cell.

Parameters

    The size of the border.

    Options for setting the border size.

Options

    The location of the cell to set the border size.

    The border direction to set the size.

    • Default: 'all'

editor.plugin(TablePlugin).update.setColumnWidth

Sets the width of a specific column in a table.

Parameters

    The column index and width to apply.

    Options for locating the table.

editor.plugin(TablePlugin).update.set

Sets typed table properties such as marginLeft through the descriptor-bound element update.

Parameters

    The table properties to apply, such as { marginLeft: 24 }.

    Generic node options. The table type matcher comes from TablePlugin.

editor.plugin(TablePlugin).update.setRowHeight

Sets the size (height) of a table row.

Parameters

    The row index and height to apply.

    Options for locating the table.

Hooks

useTableSelectionDOM

Synchronizes structural cell selection with a custom table renderer. The hook updates selected-cell attributes, hides the caret in the anchor cell, and restores the caret when the selection changes or the renderer unmounts.

Compose column sizing, cell layout, and pointer handlers directly in your renderer with TablePlugin reads and Core selector hooks.

Parameters

    Ref for the rendered <table> that contains data-table-cell-key attributes.

editor.plugin(TablePlugin).read.canMerge()

Returns whether the current editable selection is a complete rectangular multi-cell selection.

editor.plugin(TablePlugin).read.canSplit()

Returns whether the current editable selection is inside one cell with a row or column span. Registry controls also apply the live disableMerge option.

;
import { type Path, PathApi } from 'platejs';
import {
type PlateElementProps,
PlateElement,
useComposedRef,
useEditor,
useEditorPlugin,
useEditorReadOnly,
useEditorSelector,
useElement,
useElementSelected,
useFocusedLast,
usePluginStore,
useElementSelector,
} from 'platejs/react';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuTrigger,
} from '@/components/editor/dropdown-menu';
import {
FloatingPopover,
FloatingPopoverAnchor,
FloatingPopoverContent,
} from '@/components/editor/floating-popover';
import {
Toolbar,
ToolbarButton,
ToolbarGroup,
ToolbarMenuGroup,
} from '@/components/editor/toolbar';
import {
ColorDropdownMenuItems,
DEFAULT_COLORS,
} from './font-color-toolbar-button';
type TableResizeDirection = 'bottom' | 'left' | 'right';
type TableResizeStartOptions = {
colIndex: number;
direction: TableResizeDirection;
handleKey: string;
rowIndex: number;
};
type TableResizeDragState = {
colIndex: number;
direction: TableResizeDirection;
initialPosition: number;
initialSize: number;
marginLeft: number;
rowIndex: number;
};
type TableResizeContextValue = {
colSizeOverrides: Map<number, number>;
disableMarginLeft: boolean;
hasMultiRowSelection: boolean;
marginLeftOverride: number | null;
rowSizeOverrides: Map<number, number>;
clearResizePreview: (handleKey: string) => void;
showResizePreview: (
event: React.PointerEvent<HTMLDivElement>,
options: TableResizeStartOptions
) => void;
startResize: (
event: React.PointerEvent<HTMLDivElement>,
options: TableResizeStartOptions
) => void;
};
const TABLE_CONTROL_COLUMN_WIDTH = 8;
const TABLE_DEFAULT_COLUMN_WIDTH = 120;
const TABLE_DEFERRED_COLUMN_RESIZE_CELL_COUNT = 1200;
const TABLE_MULTI_SELECTION_TOOLBAR_DELAY_MS = 150;
const TABLE_SELECTION_OVERLAY_CLASS =
'pointer-events-none absolute inset-0 z-1 bg-brand/[.13]';
const roundCellSizeToStep = (size: number, step?: number) =>
step ? Math.round(size / step) * step : size;
const TableResizeContext = React.createContext<TableResizeContextValue | null>(
null
);
function useTableResizeContext() {
const context = React.useContext(TableResizeContext);
if (!context) {
throw new Error('TableResizeContext is missing');
}
return context;
}
function useTableResizeController({
baseColSizes,
deferColumnResize,
dragIndicatorRef,
hoverIndicatorRef,
marginLeft,
overrideColSize,
overrideMarginLeft,
overrideRowSize,
controlColumnWidth,
tablePath,
tableRef,
wrapperRef,
}: {
baseColSizes: number[];
deferColumnResize: boolean;
dragIndicatorRef: React.RefObject<HTMLDivElement | null>;
hoverIndicatorRef: React.RefObject<HTMLDivElement | null>;
marginLeft: number;
overrideColSize: (index: number, size: number | null) => void;
overrideMarginLeft: React.Dispatch<React.SetStateAction<number | null>>;
overrideRowSize: (index: number, size: number | null) => void;
controlColumnWidth: number;
tablePath: Path;
tableRef: React.RefObject<HTMLTableElement | null>;
wrapperRef: React.RefObject<HTMLDivElement | null>;
}) {
const editor = useEditor();
const { store } = useEditorPlugin(TablePlugin);
const { disableMarginLeft = false, minColumnWidth = 0 } = store.get();
const effectiveColSizes = React.useMemo(
() => baseColSizes.map((colSize) => colSize || TABLE_DEFAULT_COLUMN_WIDTH),
[baseColSizes]
);
const effectiveColSizesRef = React.useRef(effectiveColSizes);
const activeHandleKeyRef = React.useRef<string | null>(null);
const activeRowElementRef = React.useRef<HTMLTableRowElement | null>(null);
const cleanupListenersRef = React.useRef<(() => void) | null>(null);
const marginLeftRef = React.useRef(marginLeft);
const dragStateRef = React.useRef<TableResizeDragState | null>(null);
const frozenRowIndicesRef = React.useRef<number[] | null>(null);
const previewHandleKeyRef = React.useRef<string | null>(null);
React.useEffect(() => {
effectiveColSizesRef.current = effectiveColSizes;
}, [effectiveColSizes]);
React.useEffect(() => {
marginLeftRef.current = marginLeft;
}, [marginLeft]);
const hideDeferredResizeIndicator = React.useCallback(() => {
const indicator = dragIndicatorRef.current;
if (!indicator) return;
indicator.style.display = 'none';
indicator.style.removeProperty('left');
}, [dragIndicatorRef]);
const showDeferredResizeIndicator = React.useCallback(
(offset: number) => {
const indicator = dragIndicatorRef.current;
if (!indicator) return;
indicator.style.display = 'block';
indicator.style.left = `${offset}px`;
},
[dragIndicatorRef]
);
const hideResizeIndicator = React.useCallback(() => {
const indicator = hoverIndicatorRef.current;
if (!indicator) return;
indicator.style.display = 'none';
indicator.style.removeProperty('left');
}, [hoverIndicatorRef]);
const clearFrozenRowHeights = React.useCallback(() => {
const frozenRowIndices = frozenRowIndicesRef.current;
if (!frozenRowIndices) return;
frozenRowIndicesRef.current = null;
frozenRowIndices.forEach((rowIndex) => {
overrideRowSize(rowIndex, null);
});
}, [overrideRowSize]);
const freezeRowHeights = React.useCallback(() => {
const table = tableRef.current;
if (!table || deferColumnResize) return;
clearFrozenRowHeights();
const frozenRowIndices: number[] = [];
Array.from(table.rows).forEach((row, rowIndex) => {
const { height } = row.getBoundingClientRect();
if (!height) return;
overrideRowSize(rowIndex, height);
frozenRowIndices.push(rowIndex);
});
frozenRowIndicesRef.current = frozenRowIndices;
}, [clearFrozenRowHeights, deferColumnResize, overrideRowSize, tableRef]);
const showResizeIndicatorAtOffset = React.useCallback(
(offset: number) => {
const indicator = hoverIndicatorRef.current;
if (!indicator) return;
indicator.style.display = 'block';
indicator.style.left = `${offset}px`;
},
[hoverIndicatorRef]
);
const showResizeIndicator = React.useCallback(
({
event,
direction,
}: Pick<TableResizeStartOptions, 'direction'> & {
event: React.PointerEvent<HTMLDivElement>;
}) => {
if (direction === 'bottom') return;
const wrapper = wrapperRef.current;
if (!wrapper) return;
const handleRect = event.currentTarget.getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
const boundaryOffset =
handleRect.left - wrapperRect.left + handleRect.width / 2;
showResizeIndicatorAtOffset(boundaryOffset);
},
[showResizeIndicatorAtOffset, wrapperRef]
);
const showResizePreview = React.useCallback(
(
event: React.PointerEvent<HTMLDivElement>,
options: TableResizeStartOptions
) => {
if (activeHandleKeyRef.current) return;
if (event.buttons !== 0) return;
previewHandleKeyRef.current = options.handleKey;
showResizeIndicator({ ...options, event });
},
[showResizeIndicator]
);
const clearResizePreview = React.useCallback(
(handleKey: string) => {
if (activeHandleKeyRef.current) return;
if (previewHandleKeyRef.current !== handleKey) return;
previewHandleKeyRef.current = null;
hideResizeIndicator();
},
[hideResizeIndicator]
);
const commitColSize = React.useCallback(
(colIndex: number, width: number) => {
editor
.plugin(TablePlugin)
.update.setColumnWidth({ colIndex, width }, { at: tablePath });
setTimeout(() => {
overrideColSize(colIndex, null);
}, 0);
},
[editor, overrideColSize, tablePath]
);
const commitRowSize = React.useCallback(
(rowIndex: number, height: number) => {
editor
.plugin(TablePlugin)
.update.setRowHeight({ height, rowIndex }, { at: tablePath });
setTimeout(() => {
overrideRowSize(rowIndex, null);
}, 0);
},
[editor, overrideRowSize, tablePath]
);
const commitMarginLeft = React.useCallback(
(nextMarginLeft: number) => {
editor
.plugin(TablePlugin)
.update.set({ marginLeft: nextMarginLeft }, { at: tablePath });
setTimeout(() => {
overrideMarginLeft(null);
}, 0);
},
[editor, overrideMarginLeft, tablePath]
);
const getColumnBoundaryOffset = React.useCallback(
(colIndex: number, currentWidth: number) =>
controlColumnWidth +
effectiveColSizesRef.current
.slice(0, colIndex)
.reduce((total, colSize) => total + colSize, 0) +
currentWidth,
[controlColumnWidth]
);
const applyResize = React.useCallback(
(event: PointerEvent, finished: boolean) => {
const dragState = dragStateRef.current;
if (!dragState) return;
const currentPosition =
dragState.direction === 'bottom' ? event.clientY : event.clientX;
const delta = currentPosition - dragState.initialPosition;
if (dragState.direction === 'bottom') {
const newHeight = roundCellSizeToStep(
dragState.initialSize + delta,
undefined
);
if (finished) {
commitRowSize(dragState.rowIndex, newHeight);
} else {
overrideRowSize(dragState.rowIndex, newHeight);
}
return;
}
if (dragState.direction === 'left') {
const initial =
effectiveColSizesRef.current[dragState.colIndex] ??
dragState.initialSize;
const complement = (width: number) =>
initial + dragState.marginLeft - width;
const nextMarginLeft = roundCellSizeToStep(
resizeLengthClampStatic(dragState.marginLeft + delta, {
max: complement(minColumnWidth),
min: 0,
}),
undefined
);
const nextWidth = complement(nextMarginLeft);
if (finished) {
commitMarginLeft(nextMarginLeft);
commitColSize(dragState.colIndex, nextWidth);
} else if (deferColumnResize) {
showDeferredResizeIndicator(
controlColumnWidth + (nextMarginLeft - dragState.marginLeft)
);
} else {
showResizeIndicatorAtOffset(
controlColumnWidth + (nextMarginLeft - dragState.marginLeft)
);
overrideMarginLeft(nextMarginLeft);
overrideColSize(dragState.colIndex, nextWidth);
}
return;
}
const currentInitial =
effectiveColSizesRef.current[dragState.colIndex] ??
dragState.initialSize;
const nextInitial = effectiveColSizesRef.current[dragState.colIndex + 1];
const complement = (width: number) =>
currentInitial + nextInitial - width;
const currentWidth = roundCellSizeToStep(
resizeLengthClampStatic(currentInitial + delta, {
max: nextInitial ? complement(minColumnWidth) : undefined,
min: minColumnWidth,
}),
undefined
);
const nextWidth = nextInitial ? complement(currentWidth) : undefined;
if (finished) {
commitColSize(dragState.colIndex, currentWidth);
if (nextWidth !== undefined) {
commitColSize(dragState.colIndex + 1, nextWidth);
}
} else if (deferColumnResize) {
showDeferredResizeIndicator(
getColumnBoundaryOffset(dragState.colIndex, currentWidth)
);
} else {
showResizeIndicatorAtOffset(
getColumnBoundaryOffset(dragState.colIndex, currentWidth)
);
overrideColSize(dragState.colIndex, currentWidth);
if (nextWidth !== undefined) {
overrideColSize(dragState.colIndex + 1, nextWidth);
}
}
},
[
commitColSize,
commitMarginLeft,
commitRowSize,
controlColumnWidth,
deferColumnResize,
getColumnBoundaryOffset,
showDeferredResizeIndicator,
showResizeIndicatorAtOffset,
minColumnWidth,
overrideColSize,
overrideMarginLeft,
overrideRowSize,
]
);
const stopResize = React.useCallback(() => {
cleanupListenersRef.current?.();
cleanupListenersRef.current = null;
activeHandleKeyRef.current = null;
previewHandleKeyRef.current = null;
dragStateRef.current = null;
if (activeRowElementRef.current) {
delete activeRowElementRef.current.dataset.tableResizing;
activeRowElementRef.current = null;
}
hideDeferredResizeIndicator();
hideResizeIndicator();
clearFrozenRowHeights();
}, [clearFrozenRowHeights, hideDeferredResizeIndicator, hideResizeIndicator]);
const stopResizeRef = React.useRef(stopResize);
React.useEffect(() => {
stopResizeRef.current = stopResize;
}, [stopResize]);
React.useEffect(
() => () => {
stopResizeRef.current();
},
[]
);
const startResize = React.useCallback(
(
event: React.PointerEvent<HTMLDivElement>,
{ colIndex, direction, handleKey, rowIndex }: TableResizeStartOptions
) => {
const rowHeight =
tableRef.current?.rows.item(rowIndex)?.getBoundingClientRect().height ??
0;
dragStateRef.current = {
colIndex,
direction,
initialPosition: direction === 'bottom' ? event.clientY : event.clientX,
initialSize:
direction === 'bottom'
? rowHeight
: (effectiveColSizesRef.current[colIndex] ??
TABLE_DEFAULT_COLUMN_WIDTH),
marginLeft: marginLeftRef.current,
rowIndex,
};
activeHandleKeyRef.current = handleKey;
previewHandleKeyRef.current = null;
const rowElement = tableRef.current?.rows.item(rowIndex) ?? null;
if (
activeRowElementRef.current &&
activeRowElementRef.current !== rowElement
) {
delete activeRowElementRef.current.dataset.tableResizing;
}
activeRowElementRef.current = rowElement;
if (rowElement) {
rowElement.dataset.tableResizing = 'true';
}
cleanupListenersRef.current?.();
if (direction !== 'bottom') {
freezeRowHeights();
}
const handlePointerMove = (pointerEvent: PointerEvent) => {
applyResize(pointerEvent, false);
};
const handlePointerEnd = (pointerEvent: PointerEvent) => {
applyResize(pointerEvent, true);
stopResize();
};
// oxlint-disable-next-line react-doctor/effect-needs-cleanup -- [P0 owned teardown] The ref-owned cleanup runs before restart, on pointer end, and on unmount.
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerEnd);
window.addEventListener('pointercancel', handlePointerEnd);
cleanupListenersRef.current = () => {
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerEnd);
window.removeEventListener('pointercancel', handlePointerEnd);
};
if (deferColumnResize && direction !== 'bottom') {
hideResizeIndicator();
showDeferredResizeIndicator(
direction === 'left'
? controlColumnWidth
: getColumnBoundaryOffset(
colIndex,
effectiveColSizesRef.current[colIndex] ??
TABLE_DEFAULT_COLUMN_WIDTH
)
);
} else {
showResizeIndicator({ direction, event });
}
event.preventDefault();
event.stopPropagation();
},
[
controlColumnWidth,
deferColumnResize,
getColumnBoundaryOffset,
hideResizeIndicator,
showDeferredResizeIndicator,
showResizeIndicator,
stopResize,
tableRef,
applyResize,
freezeRowHeights,
]
);
return React.useMemo(
() => ({
clearResizePreview,
disableMarginLeft,
showResizePreview,
startResize,
}),
[clearResizePreview, disableMarginLeft, showResizePreview, startResize]
);
}
export function TableElement(props: PlateElementProps<typeof TablePlugin>) {
return <TableElementContent {...props} />;
}
function TableElementContent({
children,
...props
}: PlateElementProps<typeof TablePlugin>) {
const editor = useEditor();
const { api, read, store } = useEditorPlugin(TablePlugin);
const { disableMarginLeft = false } = store.get();
const readOnly = useEditorReadOnly();
const hasControls = !readOnly;
const controlColumnWidth = hasControls ? TABLE_CONTROL_COLUMN_WIDTH : 0;
const dragIndicatorRef = React.useRef<HTMLDivElement>(null);
const hoverIndicatorRef = React.useRef<HTMLDivElement>(null);
const tableRef = React.useRef<HTMLTableElement>(null);
const [colSizeOverrides, setColSizeOverrides] = React.useState(
new Map<number, number>()
);
const [rowSizeOverrides, setRowHeightOverrides] = React.useState(
new Map<number, number>()
);
const [marginLeftOverride, overrideMarginLeft] = React.useState<
number | null
>(null);
const overrideColSize = React.useCallback(
(index: number, size: number | null) => {
setColSizeOverrides((overrides) => {
const next = new Map(overrides);
if (size === null) next.delete(index);
else next.set(index, size);
return next;
});
},
[]
);
const overrideRowSize = React.useCallback(
(index: number, size: number | null) => {
setRowHeightOverrides((overrides) => {
const next = new Map(overrides);
if (size === null) next.delete(index);
else next.set(index, size);
return next;
});
},
[]
);
const marginLeft = disableMarginLeft
? 0
: (marginLeftOverride ?? props.element.marginLeft ?? 0);
const columnWidths = api.getOverriddenColumnSizes(
props.element,
colSizeOverrides
);
const baseColSizes = api.getOverriddenColumnSizes(props.element);
useTableSelectionDOM(tableRef);
const deferColumnResize =
(columnWidths?.length ?? 0) * (props.element.children?.length ?? 0) >
TABLE_DEFERRED_COLUMN_RESIZE_CELL_COUNT;
const tablePath = useElementSelector(TablePlugin, ([, path]) => path);
const wrapperRef = React.useRef<HTMLDivElement>(null);
const tableNodeKey = props.editor.key(props.element);
const hasMultiRowSelection = useEditorSelector((innerEditor) => {
const view = innerEditor.plugin(TablePlugin).read.selection();
return Boolean(
view?.complete &&
view.grid.problems.length === 0 &&
view.tableKey === tableNodeKey &&
view.anchors.length > 1 &&
view.cellKeys.length === view.anchors.length &&
view.bounds.maxRow > view.bounds.minRow
);
});
const resizeController = useTableResizeController({
baseColSizes,
controlColumnWidth,
deferColumnResize,
dragIndicatorRef,
hoverIndicatorRef,
marginLeft,
overrideColSize,
overrideMarginLeft,
overrideRowSize,
tablePath,
tableRef,
wrapperRef,
});
const tableResizeContext = React.useMemo(
() => ({
...resizeController,
colSizeOverrides,
hasMultiRowSelection,
marginLeftOverride,
rowSizeOverrides,
}),
[
colSizeOverrides,
hasMultiRowSelection,
marginLeftOverride,
resizeController,
rowSizeOverrides,
]
);
const resolvedColSizes = React.useMemo(() => {
if (columnWidths && columnWidths.length > 0) {
return columnWidths.map(
(colSize) => colSize || TABLE_DEFAULT_COLUMN_WIDTH
);
}
return Array.from(
{ length: api.getColumnCount(props.element) },
() => TABLE_DEFAULT_COLUMN_WIDTH
);
}, [api, columnWidths, props.element]);
const tableStyle = React.useMemo(
() => ({
width: `${
resolvedColSizes.reduce((total, colSize) => total + colSize, 0) +
controlColumnWidth
}px`,
}),
[controlColumnWidth, resolvedColSizes]
);
const isSelectingTable = useElementSelected({
at: tableNodeKey,
mode: 'node',
});
const content = (
<PlateElement
{...props}
attributes={{
...props.attributes,
'data-node-selection-highlight': 'self',
}}
className={cn(
'overflow-x-auto py-5',
hasControls && '-ml-2 *:data-[slot=node-selection-highlight]:left-2'
)}
style={{ paddingLeft: marginLeft }}
>
<TableResizeContext value={tableResizeContext}>
<div
ref={wrapperRef}
className="relative w-fit [&:active:not(:has([data-table-resize-handle]:active))_[data-table-resize-handle]]:cursor-text"
>
<div
ref={dragIndicatorRef}
className="pointer-events-none absolute inset-y-0 z-36 hidden w-[3px] -translate-x-[1.5px] bg-ring/70"
contentEditable={false}
/>
<div
ref={hoverIndicatorRef}
className="pointer-events-none absolute inset-y-0 z-35 hidden w-[3px] -translate-x-[1.5px] bg-ring/80"
contentEditable={false}
/>
{/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- [P0 behavior-boundary] A new table interaction collapses the prior expanded cell selection. */}
<table
ref={tableRef}
className="mr-0 ml-px table h-px table-fixed border-collapse [&:has([data-table-cell-selected=true])_*::selection]:bg-transparent"
style={tableStyle}
onMouseDown={() => {
if ((read.selection()?.anchors.length ?? 0) > 1) {
editor.update.selection.collapse();
}
}}
>
{resolvedColSizes.length > 0 && (
<colgroup>
{hasControls && (
<col
style={{
maxWidth: TABLE_CONTROL_COLUMN_WIDTH,
minWidth: TABLE_CONTROL_COLUMN_WIDTH,
width: TABLE_CONTROL_COLUMN_WIDTH,
}}
/>
)}
{resolvedColSizes.map((colSize, index) => (
<col
key={index}
style={{
maxWidth: colSize,
minWidth: colSize,
width: colSize,
}}
/>
))}
</colgroup>
)}
<tbody className="min-w-full">{children}</tbody>
</table>
{isSelectingTable && (
<div
className={TABLE_SELECTION_OVERLAY_CLASS}
contentEditable={false}
data-slot="node-selection-highlight"
/>
)}
</div>
</TableResizeContext>
</PlateElement>
);
if (readOnly) {
return content;
}
return <TableFloatingToolbar>{content}</TableFloatingToolbar>;
}
function TableFloatingToolbar({
children,
...props
}: React.ComponentProps<typeof FloatingPopoverContent>) {
const selectedCellCount = useEditorSelector(
(editor) =>
editor.plugin(TablePlugin).read.selection()?.cellKeys.length ?? 0
);
const selected = useElementSelected();
const collapsedInside = useEditorSelector(
(editor) => selected && editor.read.selection.isCollapsed()
);
const isFocusedLast = useFocusedLast();
const isCollapsedToolbarOpen = isFocusedLast && collapsedInside;
const isExpandedSelectionPending =
isFocusedLast && !collapsedInside && selectedCellCount > 1;
const isToolbarOpen = isCollapsedToolbarOpen || isExpandedSelectionPending;
return (
<FloatingPopover open={isToolbarOpen} modal={false}>
<FloatingPopoverAnchor element={children as React.ReactElement} />
{isCollapsedToolbarOpen && (
<CollapsedTableFloatingToolbarContent {...props} />
)}
{isExpandedSelectionPending && (
<DelayedExpandedSelectionTableFloatingToolbarContent {...props} />
)}
</FloatingPopover>
);
}
function DelayedExpandedSelectionTableFloatingToolbarContent(
props: React.ComponentProps<typeof FloatingPopoverContent>
) {
const [isReady, setIsReady] = React.useState(false);
React.useEffect(() => {
const timeoutId = window.setTimeout(() => {
setIsReady(true);
}, TABLE_MULTI_SELECTION_TOOLBAR_DELAY_MS);
return () => {
window.clearTimeout(timeoutId);
};
}, []);
if (!isReady) return null;
return <ExpandedSelectionTableFloatingToolbarContent {...props} />;
}
function ExpandedSelectionTableFloatingToolbarContent(
props: React.ComponentProps<typeof FloatingPopoverContent>
) {
const editor = useEditor();
const disableMerge = usePluginStore(TablePlugin, 'disableMerge');
const canMergeSelection = useEditorSelector((innerEditor) =>
innerEditor.plugin(TablePlugin).read.canMerge()
);
const canSplitSelection = useEditorSelector((innerEditor) =>
innerEditor.plugin(TablePlugin).read.canSplit()
);
const canMerge = !disableMerge && canMergeSelection;
const canSplit = !disableMerge && canSplitSelection;
if (!canMerge && !canSplit) return null;
return (
<TableFloatingToolbarContent
canMerge={canMerge}
canSplit={canSplit}
onMerge={() => {
editor.plugin(TablePlugin).update.merge();
}}
onSplit={() => {
editor.plugin(TablePlugin).update.split();
}}
{...props}
/>
);
}
function CollapsedTableFloatingToolbarContent(
props: React.ComponentProps<typeof FloatingPopoverContent>
) {
const editor = useEditor();
const element = useElement(TablePlugin);
const disableMerge = usePluginStore(TablePlugin, 'disableMerge');
const canSplitSelection = useEditorSelector((innerEditor) =>
innerEditor.plugin(TablePlugin).read.canSplit()
);
const canSplit = !disableMerge && canSplitSelection;
return (
<TableFloatingToolbarContent
canSplit={canSplit}
collapsedInside
onDeleteTable={() => {
editor.update.nodes.remove({ at: element });
editor.api.dom.focus();
}}
onDeleteColumn={() => {
editor.plugin(TablePlugin).update.removeColumn();
}}
onDeleteRow={() => {
editor.plugin(TablePlugin).update.removeRow();
}}
onInsertColumnAfter={() => {
editor.plugin(TablePlugin).update.insertColumn();
}}
onInsertColumnBefore={() => {
editor.plugin(TablePlugin).update.insertColumn({ before: true });
}}
onInsertRowAfter={() => {
editor.plugin(TablePlugin).update.insertRow();
}}
onInsertRowBefore={() => {
editor.plugin(TablePlugin).update.insertRow({ before: true });
}}
onSplit={() => {
editor.plugin(TablePlugin).update.split();
}}
{...props}
/>
);
}
function TableFloatingToolbarContent({
canMerge = false,
canSplit = false,
collapsedInside = false,
onDeleteColumn,
onDeleteRow,
onDeleteTable,
onInsertColumnAfter,
onInsertColumnBefore,
onInsertRowAfter,
onInsertRowBefore,
onMerge,
onSplit,
...props
}: React.ComponentProps<typeof FloatingPopoverContent> & {
canMerge?: boolean;
canSplit?: boolean;
collapsedInside?: boolean;
onDeleteColumn?: () => void;
onDeleteRow?: () => void;
onDeleteTable?: () => void;
onInsertColumnAfter?: () => void;
onInsertColumnBefore?: () => void;
onInsertRowAfter?: () => void;
onInsertRowBefore?: () => void;
onMerge?: () => void;
onSplit?: () => void;
}) {
return (
<FloatingPopoverContent
className="w-auto border-0 bg-transparent p-0 shadow-none ring-0"
onInitialFocus={(e) => {
e.preventDefault();
}}
contentEditable={false}
{...props}
>
<Toolbar
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"
contentEditable={false}
>
<ToolbarGroup>
<ColorDropdownMenu tooltip="Background color">
<PaintBucketIcon />
</ColorDropdownMenu>
{canMerge && onMerge && (
<ToolbarButton
aria-label="Merge cells"
onClick={onMerge}
tooltip="Merge cells"
>
<CombineIcon />
</ToolbarButton>
)}
{canSplit && onSplit && (
<ToolbarButton
aria-label="Split cell"
onClick={onSplit}
tooltip="Split cell"
>
<SquareSplitHorizontalIcon />
</ToolbarButton>
)}
<DropdownMenu modal={false}>
<DropdownMenuTrigger>
<ToolbarButton aria-label="Cell borders" tooltip="Cell borders">
<Grid2X2Icon />
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<TableBordersDropdownMenuContent />
</DropdownMenuPortal>
</DropdownMenu>
{collapsedInside && (
<ToolbarGroup>
<ToolbarButton
aria-label="Delete table"
onClick={onDeleteTable}
tooltip="Delete table"
>
<Trash2Icon />
</ToolbarButton>
</ToolbarGroup>
)}
</ToolbarGroup>
{collapsedInside && (
<ToolbarGroup>
<ToolbarButton
aria-label="Insert row before"
onClick={onInsertRowBefore}
tooltip="Insert row before"
>
<ArrowUp />
</ToolbarButton>
<ToolbarButton
aria-label="Insert row after"
onClick={onInsertRowAfter}
tooltip="Insert row after"
>
<ArrowDown />
</ToolbarButton>
<ToolbarButton
aria-label="Delete row"
onClick={onDeleteRow}
tooltip="Delete row"
>
<XIcon />
</ToolbarButton>
</ToolbarGroup>
)}
{collapsedInside && (
<ToolbarGroup>
<ToolbarButton
aria-label="Insert column before"
onClick={onInsertColumnBefore}
tooltip="Insert column before"
>
<ArrowLeft />
</ToolbarButton>
<ToolbarButton
aria-label="Insert column after"
onClick={onInsertColumnAfter}
tooltip="Insert column after"
>
<ArrowRight />
</ToolbarButton>
<ToolbarButton
aria-label="Delete column"
onClick={onDeleteColumn}
tooltip="Delete column"
>
<XIcon />
</ToolbarButton>
</ToolbarGroup>
)}
</Toolbar>
</FloatingPopoverContent>
);
}
function TableBordersDropdownMenuContent(
props: React.ComponentProps<typeof DropdownMenuContent>
) {
const editor = useEditor();
const borderStates = useEditorSelector((innerEditor) =>
innerEditor.plugin(TablePlugin).read.getSelectedCellsBorders()
);
return (
<DropdownMenuContent
className="min-w-[220px]"
onFinalFocus={(e) => {
e.preventDefault();
editor.api.dom.focus();
}}
align="start"
side="right"
sideOffset={0}
{...props}
>
<DropdownMenuGroup>
<DropdownMenuCheckboxItem
checked={borderStates.top}
onCheckedChange={() => {
editor.plugin(TablePlugin).update.toggleBorders({ border: 'top' });
}}
>
<BorderTopIcon />
<div>Top Border</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={borderStates.right}
onCheckedChange={() => {
editor
.plugin(TablePlugin)
.update.toggleBorders({ border: 'right' });
}}
>
<BorderRightIcon />
<div>Right Border</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={borderStates.bottom}
onCheckedChange={() => {
editor
.plugin(TablePlugin)
.update.toggleBorders({ border: 'bottom' });
}}
>
<BorderBottomIcon />
<div>Bottom Border</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={borderStates.left}
onCheckedChange={() => {
editor.plugin(TablePlugin).update.toggleBorders({ border: 'left' });
}}
>
<BorderLeftIcon />
<div>Left Border</div>
</DropdownMenuCheckboxItem>
</DropdownMenuGroup>
<DropdownMenuGroup>
<DropdownMenuCheckboxItem
checked={borderStates.none}
onCheckedChange={() => {
editor.plugin(TablePlugin).update.toggleBorders({ border: 'none' });
}}
>
<BorderNoneIcon />
<div>No Border</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={borderStates.outer}
onCheckedChange={() => {
editor
.plugin(TablePlugin)
.update.toggleBorders({ border: 'outer' });
}}
>
<BorderAllIcon />
<div>Outside Borders</div>
</DropdownMenuCheckboxItem>
</DropdownMenuGroup>
</DropdownMenuContent>
);
}
function ColorDropdownMenu({
children,
tooltip,
}: {
children: React.ReactNode;
tooltip: string;
}) {
const [open, setOpen] = React.useState(false);
const editor = useEditor();
const onUpdateColor = React.useCallback(
(color: string) => {
setOpen(false);
editor.plugin(TablePlugin).update.setCellBackground({ color });
},
[editor]
);
const onClearColor = React.useCallback(() => {
setOpen(false);
editor.plugin(TablePlugin).update.setCellBackground({ color: null });
}, [editor]);
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger>
<ToolbarButton aria-label={tooltip} tooltip={tooltip}>
{children}
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<ToolbarMenuGroup label="Colors">
<ColorDropdownMenuItems
className="px-2"
colors={DEFAULT_COLORS}
updateColor={onUpdateColor}
/>
</ToolbarMenuGroup>
<DropdownMenuGroup>
<DropdownMenuItem className="p-2" onClick={onClearColor}>
<EraserIcon />
<span>Clear</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
export function TableRowElement({
children,
...props
}: PlateElementProps<typeof TableRowPlugin>) {
const { element } = props;
const readOnly = useEditorReadOnly();
const rowIndex = useElementSelector(TableRowPlugin, ([, path]) => {
const index = path.at(-1);
if (index === undefined) {
throw new Error('Table row path must include an index.');
}
return index;
});
const rowSize = useElementSelector(TableRowPlugin, ([node]) => node.height);
const { hasMultiRowSelection, rowSizeOverrides } = useTableResizeContext();
const rowMinHeight = rowSizeOverrides.get(rowIndex) ?? rowSize;
const hasControls = !readOnly;
const { isDragging, nodeRef, previewRef, handleRef } = useDraggable({
element,
type: element.type,
canDropNode: ({ dragEntry, dropEntry }) =>
PathApi.equals(
PathApi.parent(dragEntry[1]),
PathApi.parent(dropEntry[1])
),
onDropHandler: (editor, { dragItem }) => {
if (!('key' in dragItem)) return;
const key = Array.isArray(dragItem.key) ? dragItem.key[0] : dragItem.key;
if (key) {
const path = editor.read.nodes.path(key);
if (!path) return;
const range = editor.read.ranges.get(path);
if (!range) return;
editor.update.selection.set(range);
editor.api.dom.focus();
}
},
});
return (
<PlateElement
{...props}
ref={useComposedRef(props.ref, previewRef, nodeRef)}
as="tr"
className={cn('group/row', isDragging && 'opacity-50')}
style={
{
'--tableRowMinHeight': rowMinHeight ? `${rowMinHeight}px` : undefined,
} as React.CSSProperties
}
>
{hasControls && (
<td
className="w-2 max-w-2 min-w-2 p-0 select-none"
contentEditable={false}
>
{!hasMultiRowSelection && (
<>
<RowDragHandle dragRef={handleRef} />
<RowDropLine />
</>
)}
</td>
)}
{children}
</PlateElement>
);
}
function RowDragHandle({ dragRef }: { dragRef: React.Ref<HTMLButtonElement> }) {
const editor = useEditor();
const element = useElement(TableRowPlugin);
return (
<Button
ref={dragRef}
aria-label="Select or move row"
variant="outline"
className={cn(
'-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',
'cursor-grab active:cursor-grabbing',
'opacity-0 transition-opacity duration-100 group-hover/row:opacity-100 group-data-[table-resizing=true]/row:opacity-0'
)}
onClick={() => {
const range = editor.read.ranges.get(element);
if (!range) return;
editor.update.selection.set(range);
editor.api.dom.focus();
}}
>
<GripVertical className="text-muted-foreground" />
</Button>
);
}
function RowDropLine() {
const { dropLine } = useDropLine();
if (!dropLine) return null;
return (
<div
className={cn(
'absolute inset-x-0 left-2 z-50 h-0.5 bg-brand/50',
dropLine === 'top' ? '-top-px' : '-bottom-px'
)}
/>
);
}
export function TableCellElement(
props: PlateElementProps<typeof TableCellPlugin>
) {
const editor = useEditor();
const readOnly = useEditorReadOnly();
const { element } = props;
const isHeader = element.header === true;
const tableKey = useElementSelector(TablePlugin, ([node]) =>
editor.key(node)
);
const rowKey = useElementSelector(TableRowPlugin, ([node]) =>
editor.key(node)
);
const isSelectingTable = useElementSelected({
at: tableKey,
mode: 'node',
});
const isSelectingRow =
useElementSelected({ at: rowKey, mode: 'node' }) || isSelectingTable;
const cellIndices = useEditorSelector(
(innerEditor) =>
innerEditor.plugin(TablePlugin).read.getCellIndices(element),
{
equalityFn: (next, previous) =>
next?.col === previous?.col && next?.row === previous?.row,
shouldUpdate: (change) =>
!change ||
change.changed.hasAny('properties') ||
change.changed.hasAny('structure') ||
change.changed.hasAny('replace') ||
change.changed.hasAny('root-order'),
}
);
const indices = cellIndices ?? { col: 0, row: 0 };
const table = editor.plugin(TablePlugin);
const borders = table.read.getCellBorders({
cellIndices: indices,
element,
});
const colSpan = table.api.getColSpan(element);
const rowSpan = table.api.getRowSpan(element);
const colIndex = indices.col + colSpan - 1;
const rowIndex = indices.row + rowSpan - 1;
return (
<PlateElement
{...props}
as={isHeader ? 'th' : 'td'}
className={cn(
'relative h-full overflow-visible border-none bg-background p-0',
element.backgroundColor ? 'bg-(--cellBackground)' : 'bg-background',
isHeader && 'text-left *:m-0',
'before:size-full',
'data-[table-cell-selected=true]:before:z-10',
'data-[table-cell-selected=true]:before:bg-brand/5',
"before:absolute before:box-border before:select-none before:content-['']",
borders.bottom?.width && 'before:border-b before:border-b-border',
borders.right?.width && 'before:border-r before:border-r-border',
borders.left?.width && 'before:border-l before:border-l-border',
borders.top?.width && 'before:border-t before:border-t-border'
)}
style={
{
'--cellBackground': element.backgroundColor,
} as React.CSSProperties
}
attributes={{
...props.attributes,
colSpan,
rowSpan,
}}
>
<div
className="relative z-20 box-border h-full px-3 py-2"
style={
rowSpan === 1
? { minHeight: 'var(--tableRowMinHeight, 0px)' }
: undefined
}
>
{props.children}
</div>
{!readOnly && (
<TableCellResizeControls colIndex={colIndex} rowIndex={rowIndex} />
)}
{isSelectingRow && (
<div
className={TABLE_SELECTION_OVERLAY_CLASS}
contentEditable={false}
data-slot="node-selection-highlight"
/>
)}
</PlateElement>
);
}
function TableCellResizeControls({
colIndex,
rowIndex,
}: {
colIndex: number;
rowIndex: number;
}) {
const {
clearResizePreview,
disableMarginLeft,
showResizePreview,
startResize,
} = useTableResizeContext();
const rightHandleKey = `right:${rowIndex}:${colIndex}`;
const bottomHandleKey = `bottom:${rowIndex}:${colIndex}`;
const leftHandleKey = `left:${rowIndex}:${colIndex}`;
const isLeftHandle = colIndex === 0 && !disableMarginLeft;
return (
<div
className="pointer-events-none absolute inset-0 z-30 select-none"
contentEditable={false}
data-plite-root-chrome-ignore="true"
suppressContentEditableWarning={true}
>
<div
className="pointer-events-auto absolute -top-2 -right-1 z-40 h-[calc(100%_+_8px)] w-2 cursor-col-resize touch-none"
data-table-resize-handle="column-end"
onPointerEnter={(event) => {
showResizePreview(event, {
colIndex,
direction: 'right',
handleKey: rightHandleKey,
rowIndex,
});
}}
onPointerMove={(event) => {
showResizePreview(event, {
colIndex,
direction: 'right',
handleKey: rightHandleKey,
rowIndex,
});
}}
onPointerLeave={() => {
clearResizePreview(rightHandleKey);
}}
onPointerDown={(event) => {
startResize(event, {
colIndex,
direction: 'right',
handleKey: rightHandleKey,
rowIndex,
});
}}
/>
<div
className="pointer-events-auto absolute -bottom-1 left-0 z-40 h-2 w-full cursor-row-resize touch-none"
data-table-resize-handle="row-end"
onPointerEnter={(event) => {
showResizePreview(event, {
colIndex,
direction: 'bottom',
handleKey: bottomHandleKey,
rowIndex,
});
}}
onPointerMove={(event) => {
showResizePreview(event, {
colIndex,
direction: 'bottom',
handleKey: bottomHandleKey,
rowIndex,
});
}}
onPointerLeave={() => {
clearResizePreview(bottomHandleKey);
}}
onPointerDown={(event) => {
startResize(event, {
colIndex,
direction: 'bottom',
handleKey: bottomHandleKey,
rowIndex,
});
}}
/>
{isLeftHandle && (
<div
className="pointer-events-auto absolute top-0 -left-1 z-40 h-full w-2 cursor-col-resize touch-none"
data-table-resize-handle="column-start"
onPointerEnter={(event) => {
showResizePreview(event, {
colIndex,
direction: 'left',
handleKey: leftHandleKey,
rowIndex,
});
}}
onPointerMove={(event) => {
showResizePreview(event, {
colIndex,
direction: 'left',
handleKey: leftHandleKey,
rowIndex,
});
}}
onPointerLeave={() => {
clearResizePreview(leftHandleKey);
}}
onPointerDown={(event) => {
startResize(event, {
colIndex,
direction: 'left',
handleKey: leftHandleKey,
rowIndex,
});
}}
/>
)}
</div>
);
}
export const TableKit = [
TablePlugin.configure({ component: TableElement }),
TableRowPlugin.configure({ component: TableRowElement }),
TableCellPlugin.configure({ component: TableCellElement }),
];
function BorderAllIcon(props: LucideProps) {
return (
<svg
fill="none"
height="15"
viewBox="0 0 15 15"
width="15"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Border All</title>
<path
clipRule="evenodd"
d="M0.25 1C0.25 0.585786 0.585786 0.25 1 0.25H14C14.4142 0.25 14.75 0.585786 14.75 1V14C14.75 14.4142 14.4142 14.75 14 14.75H1C0.585786 14.75 0.25 14.4142 0.25 14V1ZM1.75 1.75V13.25H13.25V1.75H1.75Z"
fill="currentColor"
fillRule="evenodd"
/>
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="5" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="3" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="7" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="5" y="7" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="3" y="7" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="9" y="7" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="11" y="7" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="9" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="11" />
</svg>
);
}
function BorderBottomIcon(props: LucideProps) {
return (
<svg
fill="none"
height="15"
viewBox="0 0 15 15"
width="15"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Border Bottom</title>
<path
clipRule="evenodd"
d="M1 13.25L14 13.25V14.75L1 14.75V13.25Z"
fill="currentColor"
fillRule="evenodd"
/>
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="5" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="5" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="3" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="3" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="7" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="1" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="7" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="1" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="5" y="7" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="5" y="1" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="3" y="7" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="3" y="1" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="9" y="7" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="9" y="1" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="11" y="7" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="11" y="1" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="9" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="9" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="11" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="11" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="5" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="3" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="7" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="1" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="9" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="11" />
</svg>
);
}
function BorderLeftIcon(props: LucideProps) {
return (
<svg
fill="none"
height="15"
viewBox="0 0 15 15"
width="15"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Border Left</title>
<path
clipRule="evenodd"
d="M1.75 1L1.75 14L0.249999 14L0.25 1L1.75 1Z"
fill="currentColor"
fillRule="evenodd"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 10 7)"
width="1"
x="10"
y="7"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 10 13)"
width="1"
x="10"
y="13"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 12 7)"
width="1"
x="12"
y="7"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 12 13)"
width="1"
x="12"
y="13"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 8 7)"
width="1"
x="8"
y="7"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 14 7)"
width="1"
x="14"
y="7"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 8 13)"
width="1"
x="8"
y="13"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 14 13)"
width="1"
x="14"
y="13"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 8 5)"
width="1"
x="8"
y="5"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 14 5)"
width="1"
x="14"
y="5"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 8 3)"
width="1"
x="8"
y="3"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 14 3)"
width="1"
x="14"
y="3"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 8 9)"
width="1"
x="8"
y="9"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 14 9)"
width="1"
x="14"
y="9"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 8 11)"
width="1"
x="8"
y="11"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 14 11)"
width="1"
x="14"
y="11"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 6 7)"
width="1"
x="6"
y="7"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 6 13)"
width="1"
x="6"
y="13"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 4 7)"
width="1"
x="4"
y="7"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 4 13)"
width="1"
x="4"
y="13"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 10 1)"
width="1"
x="10"
y="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 12 1)"
width="1"
x="12"
y="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 8 1)"
width="1"
x="8"
y="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 14 1)"
width="1"
x="14"
y="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 6 1)"
width="1"
x="6"
y="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(90 4 1)"
width="1"
x="4"
y="1"
/>
</svg>
);
}
function BorderNoneIcon(props: LucideProps) {
return (
<svg
fill="none"
height="15"
viewBox="0 0 15 15"
width="15"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Border None</title>
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="5.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="5.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="3.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="3.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="7.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="13.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="1.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="7.025" />
<rect
fill="currentColor"
height="1"
rx=".5"
width="1"
x="13"
y="13.025"
/>
<rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="1.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="5" y="7.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="5" y="13.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="5" y="1.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="3" y="7.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="3" y="13.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="3" y="1.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="9" y="7.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="9" y="13.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="9" y="1.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="11" y="7.025" />
<rect
fill="currentColor"
height="1"
rx=".5"
width="1"
x="11"
y="13.025"
/>
<rect fill="currentColor" height="1" rx=".5" width="1" x="11" y="1.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="9.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="13" y="9.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="7" y="11.025" />
<rect
fill="currentColor"
height="1"
rx=".5"
width="1"
x="13"
y="11.025"
/>
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="5.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="3.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="7.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="13.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="1.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="9.025" />
<rect fill="currentColor" height="1" rx=".5" width="1" x="1" y="11.025" />
</svg>
);
}
function BorderRightIcon(props: LucideProps) {
return (
<svg
fill="none"
height="15"
viewBox="0 0 15 15"
width="15"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Border Right</title>
<path
clipRule="evenodd"
d="M13.25 1L13.25 14L14.75 14L14.75 1L13.25 1Z"
fill="currentColor"
fillRule="evenodd"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 5 7)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 5 13)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 3 7)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 3 13)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 7 7)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 1 7)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 7 13)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 1 13)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 7 5)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 1 5)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 7 3)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 1 3)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 7 9)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 1 9)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 7 11)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 1 11)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 9 7)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 9 13)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 11 7)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 11 13)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 5 1)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 3 1)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 7 1)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 1 1)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 9 1)"
width="1"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="matrix(0 1 1 0 11 1)"
width="1"
/>
</svg>
);
}
function BorderTopIcon(props: LucideProps) {
return (
<svg
fill="none"
height="15"
viewBox="0 0 15 15"
width="15"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Border Top</title>
<path
clipRule="evenodd"
d="M14 1.75L1 1.75L1 0.249999L14 0.25L14 1.75Z"
fill="currentColor"
fillRule="evenodd"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 8 10)"
width="1"
x="8"
y="10"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 2 10)"
width="1"
x="2"
y="10"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 8 12)"
width="1"
x="8"
y="12"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 2 12)"
width="1"
x="2"
y="12"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 8 8)"
width="1"
x="8"
y="8"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 8 14)"
width="1"
x="8"
y="14"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 2 8)"
width="1"
x="2"
y="8"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 2 14)"
width="1"
x="2"
y="14"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 10 8)"
width="1"
x="10"
y="8"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 10 14)"
width="1"
x="10"
y="14"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 12 8)"
width="1"
x="12"
y="8"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 12 14)"
width="1"
x="12"
y="14"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 6 8)"
width="1"
x="6"
y="8"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 6 14)"
width="1"
x="6"
y="14"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 4 8)"
width="1"
x="4"
y="8"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 4 14)"
width="1"
x="4"
y="14"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 8 6)"
width="1"
x="8"
y="6"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 2 6)"
width="1"
x="2"
y="6"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 8 4)"
width="1"
x="8"
y="4"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 2 4)"
width="1"
x="2"
y="4"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 14 10)"
width="1"
x="14"
y="10"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 14 12)"
width="1"
x="14"
y="12"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 14 8)"
width="1"
x="14"
y="8"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 14 14)"
width="1"
x="14"
y="14"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 14 6)"
width="1"
x="14"
y="6"
/>
<rect
fill="currentColor"
height="1"
rx=".5"
transform="rotate(-180 14 4)"
width="1"
x="14"
y="4"
/>
</svg>
);
}
plugins: [
// ...otherPlugins,
...TableKit,
],
});
plugins: [
// ...otherPlugins,
TablePlugin,
],
});
},
}),
TableRowPlugin.configure({ component: TableRowElement }),
TableCellPlugin.configure({ component: TableCellElement }),
],
});
import {
  TableCellPlugin,
  TablePlugin,
  TableRowPlugin,
} from '@platejs/table/react';
import { createPlateEditor } from 'platejs/react';
import {
  TableCellElement,
  TableElement,
  TableRowElement,
} from '@/components/editor/table';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    TablePlugin.configure({
      component: TableElement,
      initialState: {
        initialTableWidth: 600,
        disableMerge: false,
        minColumnWidth: 48,
      },
    }),
    TableRowPlugin.configure({ component: TableRowElement }),
    TableCellPlugin.configure({ component: TableCellElement }),
  ],
});