From 16bb2812abc267b380e32c15b814807288cf8c0e Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 9 Nov 2023 00:08:36 +0300 Subject: [PATCH 01/16] start making poc --- .../src/lib/plate/demo/values/tableValue.tsx | 55 +++++- .../default/plate-ui/table-cell-element.tsx | 3 + .../plate-utils/src/components/PortalBody.tsx | 2 +- .../useTableCellElementState.ts | 1 + .../TableElement/useTableColSizes.ts | 10 +- .../TableElement/useTableElement.ts | 162 +++++++++++++++++- packages/table/src/types.ts | 1 + 7 files changed, 220 insertions(+), 14 deletions(-) diff --git a/apps/www/src/lib/plate/demo/values/tableValue.tsx b/apps/www/src/lib/plate/demo/values/tableValue.tsx index e124c81caf..a6775fb21c 100644 --- a/apps/www/src/lib/plate/demo/values/tableValue.tsx +++ b/apps/www/src/lib/plate/demo/values/tableValue.tsx @@ -105,6 +105,54 @@ export const createSpanningTable = (): any => ( ); +export const createMergedCellsTable = (): any => ( + + + + + + Heading + + + + + Cell 1 + + + + + Cell 2 + + + + + + Cell 3 + + + Cell 7 + + + + + + Cell 4 + + + + Cell 5 + + + Cell 6 + + + Cell 8 + + + + +); + export const tableValue: any = ( 🏓 Table @@ -112,10 +160,11 @@ export const tableValue: any = ( Create customizable tables with resizable columns and rows, allowing you to design structured layouts. - {createTable()} - + {createMergedCellsTable()} + {/* {createTable()} */} + {/* This table is an example of rendering a table spanning multiple columns: - {createSpanningTable()} + {createSpanningTable()} */} ); diff --git a/apps/www/src/registry/default/plate-ui/table-cell-element.tsx b/apps/www/src/registry/default/plate-ui/table-cell-element.tsx index 0effb3e381..46d843c2f7 100644 --- a/apps/www/src/registry/default/plate-ui/table-cell-element.tsx +++ b/apps/www/src/registry/default/plate-ui/table-cell-element.tsx @@ -40,6 +40,9 @@ const TableCellElement = React.forwardRef< colIndex, rowIndex, }); + + + const { rightProps, bottomProps, leftProps, hiddenLeft } = useTableCellElementResizable(resizableState); diff --git a/packages/plate-utils/src/components/PortalBody.tsx b/packages/plate-utils/src/components/PortalBody.tsx index 08c15724b3..de162963d5 100644 --- a/packages/plate-utils/src/components/PortalBody.tsx +++ b/packages/plate-utils/src/components/PortalBody.tsx @@ -14,5 +14,5 @@ export const PortalBody: ({ element || typeof window !== 'undefined' ? document.body : undefined; if (!container) return (<>{children}) as any; - return ReactDOM.createPortal(children, element || document.body); + return ReactDOM.createPortal(children as any, element || document.body); }; diff --git a/packages/table/src/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/components/TableCellElement/useTableCellElementState.ts index 0024203fc6..b76a23d100 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -89,6 +89,7 @@ export const useTableCellElement = ({ return { props: { colSpan: element.colSpan, + rowSpan: element.rowSpan, }, }; }; diff --git a/packages/table/src/components/TableElement/useTableColSizes.ts b/packages/table/src/components/TableElement/useTableColSizes.ts index 95e26f0e49..38b4e5a96b 100644 --- a/packages/table/src/components/TableElement/useTableColSizes.ts +++ b/packages/table/src/components/TableElement/useTableColSizes.ts @@ -2,17 +2,25 @@ import { useEffect } from 'react'; import { findNodePath, getPluginOptions, + PlateEditor, unsetNodes, usePlateEditorRef, } from '@udecode/plate-common'; +import { BaseEditor, Editor, Path } from 'slate'; import { ELEMENT_TABLE } from '../../createTablePlugin'; import { getTableColumnCount, getTableOverriddenColSizes, + getTableRowIndex, } from '../../queries/index'; import { useTableStore } from '../../stores/tableStore'; -import { TablePlugin, TTableElement } from '../../types'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../../types'; /** * Returns colSizes with overrides applied. diff --git a/packages/table/src/components/TableElement/useTableElement.ts b/packages/table/src/components/TableElement/useTableElement.ts index 447b061798..ae0aaf3af7 100644 --- a/packages/table/src/components/TableElement/useTableElement.ts +++ b/packages/table/src/components/TableElement/useTableElement.ts @@ -1,13 +1,24 @@ +import { useEffect } from 'react'; import { collapseSelection, + findNodePath, + getNode, getPluginOptions, + PlateEditor, useElement, usePlateEditorRef, } from '@udecode/plate-common'; +import { Path } from 'slate'; import { ELEMENT_TABLE } from '../../createTablePlugin'; +import { getTableRowIndex } from '../../queries'; import { useTableStore } from '../../stores/tableStore'; -import { TablePlugin, TTableElement } from '../../types'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../../types'; import { useSelectedCells } from './useSelectedCells'; import { useTableColSizes } from './useTableColSizes'; @@ -18,14 +29,16 @@ export interface TableElementState { marginLeft: number; } -export const useTableElementState = ({ - transformColSizes, -}: { - /** - * Transform node column sizes - */ - transformColSizes?: (colSizes: number[]) => number[]; -} = {}): TableElementState => { +export const useTableElementState = ( + { + transformColSizes, + }: { + /** + * Transform node column sizes + */ + transformColSizes?: (colSizes: number[]) => number[]; + } = {} +): TableElementState => { const editor = usePlateEditorRef(); const { minColumnWidth, disableMarginLeft } = getPluginOptions( @@ -37,6 +50,11 @@ export const useTableElementState = ({ const selectedCells = useTableStore().get.selectedCells(); const marginLeftOverride = useTableStore().get.marginLeftOverride(); + // initial calc, than it will be calculated when each individual cell updated + useEffect(() => { + calculateCellIndexes(editor, element); + }, [editor, element]); + const marginLeft = disableMarginLeft ? 0 : marginLeftOverride ?? element.marginLeft ?? 0; @@ -81,3 +99,129 @@ export const useTableElement = () => { }, }; }; + +function getCellIndices( + editor: PlateEditor, + tableEl: TTableElement, + tablePath: Path, + cellPath: Path +) { + const tableNodes = tableEl.children; + + let rowIndex = -1; + let colIndex = -1; + + for (let r = 0; r < tableNodes.length; r++) { + const row = tableNodes[r] as TTableRowElement; + // console.log('row.type', row.type); + // if (row.type === 'tr') { + rowIndex++; + + let cIndex = 0; + for (let c = 0; c < row.children.length; c++) { + const cell = row.children[c] as TTableCellElement; + // console.log('current cell', cell); + // console.log('cell.type', cell.type); + // if (cell.type === 'th') { + const curCellPath = [r, c]; + // const curCellPath = findNodePath(editor, cell)!; + + if (Path.equals(curCellPath, cellPath)) { + // colIndex = cIndex; + console.log('early break', cIndex); + break; + } + cIndex += cell.colSpan || 1; // consider 0 and undefined as 1 + console.log('incrementing cell index,', cIndex); + // } + } + + // If target cell is not in this row, but the rowSpan from previous rows is impacting + // the colIndex for the next row, then increment manually + if (rowIndex >= 1) { + console.log('tableNodes', tableNodes, 'rowIndex', rowIndex); + tableNodes.slice(0, rowIndex).forEach((pR, _rowIndex) => { + const prevRow = pR as TTableRowElement; + console.log('current row', row, 'prevRow', prevRow); + + prevRow.children.forEach((pC) => { + const prevCell = pC as TTableCellElement; + console.log('prevCell', prevCell); + if ( + prevCell.rowSpan && + prevCell.rowSpan > 1 && + rowIndex - _rowIndex < prevCell.rowSpan + ) { + cIndex += prevCell.colSpan || 1; + console.log( + 'increment by affected row span:', + _rowIndex, + cIndex, + cellPath + ); + } + }); + }); + } + // } + + if (colIndex !== -1) { + // Break once we've found the target cell + colIndex = cIndex; + console.log('breaking, we found cell'); + break; + } + } + + if (rowIndex === -1 || colIndex === -1) { + console.log('Invalid cell location.'); + return null; + } + + return { row: rowIndex, col: colIndex }; +} + +const calculateCellIndexes = ( + editor: PlateEditor, + tableNode: TTableElement +) => { + // (Place the `getCellIndices()` function from the previous response here) + + // Initialize an array to store the indices of each cell + const cellIndicesArray = []; + + const tablePath = findNodePath(editor, tableNode)!; + + // Iterate through the table rows + for (let r = 0; r < tableNode.children.length; r++) { + const row = tableNode.children[r] as TTableRowElement; + const rowIndicesArray = []; + + // Iterate through the row cells + for (let c = 0; c < row.children.length; c++) { + const cell = row.children[c] as TTableCellElement; + + // Get cell indices and store them in the row's array + // const cellPath = findNodePath(editor, cell)!; // TODO: use concat instead of findNodePath + const cellPath = [r, c]; + console.log( + 'searching for', + cell.children.map((m) => { + return (m as any).children[0].text; + }), + tableNode, + tablePath, + cellPath + ); + + const indices = getCellIndices(editor, tableNode, tablePath, cellPath); + rowIndicesArray.push(indices); + } + + // Push the rowIndicesArray to the cellIndicesArray + cellIndicesArray.push(rowIndicesArray); + } + + console.log('cellIndicesArray', cellIndicesArray); + return cellIndicesArray; +}; diff --git a/packages/table/src/types.ts b/packages/table/src/types.ts index 36117f5c06..3e4ecc9d42 100644 --- a/packages/table/src/types.ts +++ b/packages/table/src/types.ts @@ -78,6 +78,7 @@ export interface TTableRowElement extends TElement { export interface TTableCellElement extends TElement { colSpan?: number; + rowSpan?: number; size?: number; background?: string; borders?: { From 0561f52e3924db377f4fa2559994620d71570895 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 9 Nov 2023 02:00:03 +0300 Subject: [PATCH 02/16] update implementation of indices detection --- .../src/lib/plate/demo/values/tableValue.tsx | 20 +++-- .../TableElement/useTableColSizes.ts | 10 +-- .../TableElement/useTableElement.ts | 82 ++++++++----------- 3 files changed, 47 insertions(+), 65 deletions(-) diff --git a/apps/www/src/lib/plate/demo/values/tableValue.tsx b/apps/www/src/lib/plate/demo/values/tableValue.tsx index a6775fb21c..30417572f0 100644 --- a/apps/www/src/lib/plate/demo/values/tableValue.tsx +++ b/apps/www/src/lib/plate/demo/values/tableValue.tsx @@ -109,7 +109,7 @@ export const createMergedCellsTable = (): any => ( - + Heading @@ -119,6 +119,11 @@ export const createMergedCellsTable = (): any => ( Cell 1 + + + Cell 11 + + Cell 2 @@ -126,9 +131,12 @@ export const createMergedCellsTable = (): any => ( - + Cell 3 + {/* + Cell 77 + */} Cell 7 @@ -139,12 +147,9 @@ export const createMergedCellsTable = (): any => ( Cell 4 - + {/* Cell 5 - - - Cell 6 - + */} Cell 8 @@ -153,6 +158,7 @@ export const createMergedCellsTable = (): any => ( ); + export const tableValue: any = ( 🏓 Table diff --git a/packages/table/src/components/TableElement/useTableColSizes.ts b/packages/table/src/components/TableElement/useTableColSizes.ts index 5135f4c53e..dbb31290f1 100644 --- a/packages/table/src/components/TableElement/useTableColSizes.ts +++ b/packages/table/src/components/TableElement/useTableColSizes.ts @@ -2,25 +2,17 @@ import { useEffect } from 'react'; import { findNodePath, getPluginOptions, - PlateEditor, unsetNodes, useEditorRef, } from '@udecode/plate-common'; -import { BaseEditor, Editor, Path } from 'slate'; import { ELEMENT_TABLE } from '../../createTablePlugin'; import { getTableColumnCount, getTableOverriddenColSizes, - getTableRowIndex, } from '../../queries/index'; import { useTableStore } from '../../stores/tableStore'; -import { - TablePlugin, - TTableCellElement, - TTableElement, - TTableRowElement, -} from '../../types'; +import { TablePlugin, TTableElement } from '../../types'; /** * Returns colSizes with overrides applied. diff --git a/packages/table/src/components/TableElement/useTableElement.ts b/packages/table/src/components/TableElement/useTableElement.ts index 6de2a58cee..bb139cf579 100644 --- a/packages/table/src/components/TableElement/useTableElement.ts +++ b/packages/table/src/components/TableElement/useTableElement.ts @@ -3,14 +3,15 @@ import { collapseSelection, findNodePath, getNode, + getParentNode, getPluginOptions, + PlateEditor, useEditorRef, useElement, } from '@udecode/plate-common'; import { Path } from 'slate'; import { ELEMENT_TABLE } from '../../createTablePlugin'; -import { getTableRowIndex } from '../../queries'; import { useTableStore } from '../../stores/tableStore'; import { TablePlugin, @@ -97,6 +98,11 @@ export const useTableElement = () => { }; }; +const cellAttributes = new WeakMap< + TTableCellElement, + { row: number; col: number } +>(); + function getCellIndices( editor: PlateEditor, tableEl: TTableElement, @@ -110,66 +116,41 @@ function getCellIndices( for (let r = 0; r < tableNodes.length; r++) { const row = tableNodes[r] as TTableRowElement; - // console.log('row.type', row.type); - // if (row.type === 'tr') { - rowIndex++; let cIndex = 0; for (let c = 0; c < row.children.length; c++) { const cell = row.children[c] as TTableCellElement; - // console.log('current cell', cell); - // console.log('cell.type', cell.type); - // if (cell.type === 'th') { const curCellPath = [r, c]; - // const curCellPath = findNodePath(editor, cell)!; - if (Path.equals(curCellPath, cellPath)) { - // colIndex = cIndex; - console.log('early break', cIndex); + colIndex = cIndex; + rowIndex = r; break; } cIndex += cell.colSpan || 1; // consider 0 and undefined as 1 - console.log('incrementing cell index,', cIndex); - // } - } - - // If target cell is not in this row, but the rowSpan from previous rows is impacting - // the colIndex for the next row, then increment manually - if (rowIndex >= 1) { - console.log('tableNodes', tableNodes, 'rowIndex', rowIndex); - tableNodes.slice(0, rowIndex).forEach((pR, _rowIndex) => { - const prevRow = pR as TTableRowElement; - console.log('current row', row, 'prevRow', prevRow); - - prevRow.children.forEach((pC) => { - const prevCell = pC as TTableCellElement; - console.log('prevCell', prevCell); - if ( - prevCell.rowSpan && - prevCell.rowSpan > 1 && - rowIndex - _rowIndex < prevCell.rowSpan - ) { - cIndex += prevCell.colSpan || 1; - console.log( - 'increment by affected row span:', - _rowIndex, - cIndex, - cellPath - ); - } - }); - }); - } - // } - - if (colIndex !== -1) { - // Break once we've found the target cell - colIndex = cIndex; - console.log('breaking, we found cell'); - break; } } + tableNodes.slice(0, rowIndex).forEach((pR, _rowIndex) => { + const prevRow = pR as TTableRowElement; + prevRow.children.forEach((pC) => { + const prevCell = pC as TTableCellElement; + const prevIndices = cellAttributes.get(prevCell); + if (prevIndices) { + const { col: prevColIndex } = prevIndices; + if ( + // colIndex affects + prevColIndex <= colIndex && + // rowSpan affects + prevCell.rowSpan && + prevCell.rowSpan > 1 && + rowIndex - _rowIndex < prevCell.rowSpan + ) { + colIndex += prevCell.colSpan || 1; + } + } + }); + }); + if (rowIndex === -1 || colIndex === -1) { console.log('Invalid cell location.'); return null; @@ -212,6 +193,9 @@ const calculateCellIndexes = ( ); const indices = getCellIndices(editor, tableNode, tablePath, cellPath); + if (indices) { + cellAttributes.set(cell, indices); + } rowIndicesArray.push(indices); } From 8147846457aa9e06b944873958d5381fbd04e0f4 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 9 Nov 2023 14:56:01 +0300 Subject: [PATCH 03/16] implement resize --- .../default/plate-ui/table-cell-element.tsx | 4 +- .../plate-utils/src/components/PortalBody.tsx | 2 +- .../resizable/src/components/ResizeHandle.tsx | 9 +++- .../useTableCellElementResizable.ts | 12 ++++- .../useTableCellElementState.ts | 44 +++++++++++++++--- .../TableElement/useTableElement.ts | 46 +++++++++++-------- packages/table/src/stores/tableStore.ts | 7 +++ packages/table/src/types.ts | 4 ++ .../plate-ui/table-cell-element.tsx | 2 + 9 files changed, 101 insertions(+), 29 deletions(-) diff --git a/apps/www/src/registry/default/plate-ui/table-cell-element.tsx b/apps/www/src/registry/default/plate-ui/table-cell-element.tsx index 46d843c2f7..fb6f8652d6 100644 --- a/apps/www/src/registry/default/plate-ui/table-cell-element.tsx +++ b/apps/www/src/registry/default/plate-ui/table-cell-element.tsx @@ -34,15 +34,15 @@ const TableCellElement = React.forwardRef< rowSize, borders, isSelectingCell, + colSpan, } = useTableCellElementState(); const { props: cellProps } = useTableCellElement({ element: props.element }); const resizableState = useTableCellElementResizableState({ colIndex, rowIndex, + colSpan, }); - - const { rightProps, bottomProps, leftProps, hiddenLeft } = useTableCellElementResizable(resizableState); diff --git a/packages/plate-utils/src/components/PortalBody.tsx b/packages/plate-utils/src/components/PortalBody.tsx index de162963d5..08c15724b3 100644 --- a/packages/plate-utils/src/components/PortalBody.tsx +++ b/packages/plate-utils/src/components/PortalBody.tsx @@ -14,5 +14,5 @@ export const PortalBody: ({ element || typeof window !== 'undefined' ? document.body : undefined; if (!container) return (<>{children}) as any; - return ReactDOM.createPortal(children as any, element || document.body); + return ReactDOM.createPortal(children, element || document.body); }; diff --git a/packages/resizable/src/components/ResizeHandle.tsx b/packages/resizable/src/components/ResizeHandle.tsx index 4d71263674..74088de05d 100644 --- a/packages/resizable/src/components/ResizeHandle.tsx +++ b/packages/resizable/src/components/ResizeHandle.tsx @@ -51,6 +51,7 @@ export const ResizeHandleProvider = ({ export type ResizeHandleOptions = { direction?: ResizeDirection; + initialSize?: number; onResize?: (event: ResizeEvent) => void; onMouseDown?: MouseEventHandler; onTouchStart?: TouchEventHandler; @@ -60,6 +61,7 @@ export type ResizeHandleOptions = { export const useResizeHandleState = ({ direction = 'left', + initialSize: _initialSize, onResize, onMouseDown, onTouchStart, @@ -88,7 +90,12 @@ export const useResizeHandleState = ({ const currentPosition = isHorizontal ? clientX : clientY; const delta = currentPosition - initialPosition; - onResize?.({ initialSize, delta, finished, direction }); + onResize?.({ + initialSize: _initialSize || initialSize, + delta, + finished, + direction, + }); }; const handleMouseMove = (event: MouseEvent | TouchEvent) => diff --git a/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts b/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts index 7a08cdcb61..8010f3315c 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts @@ -30,7 +30,7 @@ import { TableCellElementState } from './useTableCellElementState'; export type TableCellElementResizableOptions = Pick< TableCellElementState, - 'colIndex' | 'rowIndex' + 'colIndex' | 'rowIndex' | 'colSpan' > & { /** * Resize by step instead of by pixel. @@ -50,6 +50,7 @@ export const useTableCellElementResizableState = ({ step, stepX = step, stepY = step, + colSpan, }: TableCellElementResizableOptions) => { const editor = useEditorRef(); const { disableMarginLeft } = getPluginOptions( @@ -63,6 +64,7 @@ export const useTableCellElementResizableState = ({ rowIndex, stepX, stepY, + colSpan, }; }; @@ -72,6 +74,7 @@ export const useTableCellElementResizable = ({ rowIndex, stepX, stepY, + colSpan, }: ReturnType): { rightProps: ComponentPropsWithoutRef; bottomProps: ComponentPropsWithoutRef; @@ -86,6 +89,12 @@ export const useTableCellElementResizable = ({ ELEMENT_TABLE ); + // MERGE: override width for horizontally merged cell + let initialWidth: number | undefined; + if (colSpan > 1) { + initialWidth = tableElement.colSizes?.[colIndex]; + } + const [hoveredColIndex, setHoveredColIndex] = useTableStore().use.hoveredColIndex(); @@ -246,6 +255,7 @@ export const useTableCellElementResizable = ({ rightProps: { options: { direction: 'right', + initialSize: initialWidth, onResize: handleResizeRight, ...getHandleHoverProps(colIndex), }, diff --git a/packages/table/src/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/components/TableCellElement/useTableCellElementState.ts index 78aee680d0..ff45a0de15 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -26,6 +26,25 @@ export type TableCellElementState = { rowSize: number | undefined; borders: BorderStylesDefault; isSelectingCell: boolean; + colSpan: number; +}; + +/** + * Returns the colspan attribute of the table cell element. + * @default 1 if undefined. + */ +export const getColSpan = (cellElem: TTableCellElement) => { + const attrColSpan = Number(cellElem.attributes?.colspan); + return cellElem.colSpan || attrColSpan || 1; +}; + +/** + * Returns the rowspan attribute of the table cell element. + * @default 1 if undefined + */ +export const getRowSpan = (cellElem: TTableCellElement) => { + const attrRowSpan = Number(cellElem.attributes?.rowspan); + return cellElem.rowSpan || attrRowSpan || 1; }; export const useTableCellElementState = ({ @@ -39,20 +58,32 @@ export const useTableCellElementState = ({ const editor = useEditorRef(); const cellElement = useElement(); - const colIndex = getTableColumnIndex(editor, cellElement); - const rowIndex = getTableRowIndex(editor, cellElement); + const colSpan = getColSpan(cellElement); + const rowSpan = getRowSpan(cellElement); + + const defaultColIndex = getTableColumnIndex(editor, cellElement); + const defaultRowIndex = getTableRowIndex(editor, cellElement); const readOnly = useReadOnly(); const isCellSelected = useIsCellSelected(cellElement); const hoveredColIndex = useTableStore().get.hoveredColIndex(); const selectedCells = useTableStore().get.selectedCells(); + const cellAttributes = useTableStore().get.cellAttributes(); const tableElement = useElement(ELEMENT_TABLE); const rowElement = useElement(ELEMENT_TR); + + const x = cellAttributes.get(cellElement); + const colIndex = x?.col ?? defaultColIndex; + const rowIndex = x?.row ?? defaultRowIndex; + + const endingRowIndex = rowIndex + rowSpan - 1; + const endingColIndex = colIndex + colSpan - 1; + const rowSizeOverrides = useTableStore().get.rowSizeOverrides(); const rowSize = - rowSizeOverrides.get(rowIndex) ?? rowElement?.size ?? undefined; + rowSizeOverrides.get(endingRowIndex) ?? rowElement?.size ?? undefined; const isFirstCell = colIndex === 0; const isFirstRow = tableElement.children?.[0] === rowElement; @@ -63,15 +94,16 @@ export const useTableCellElementState = ({ }); return { - colIndex, - rowIndex, + colIndex: endingColIndex, + rowIndex: endingRowIndex, readOnly: !ignoreReadOnly && readOnly, selected: isCellSelected, - hovered: hoveredColIndex === colIndex, + hovered: hoveredColIndex === endingColIndex, hoveredLeft: isFirstCell && hoveredColIndex === -1, rowSize, borders, isSelectingCell: !!selectedCells, + colSpan, }; }; diff --git a/packages/table/src/components/TableElement/useTableElement.ts b/packages/table/src/components/TableElement/useTableElement.ts index bb139cf579..9cea6ba0f4 100644 --- a/packages/table/src/components/TableElement/useTableElement.ts +++ b/packages/table/src/components/TableElement/useTableElement.ts @@ -12,7 +12,10 @@ import { import { Path } from 'slate'; import { ELEMENT_TABLE } from '../../createTablePlugin'; -import { useTableStore } from '../../stores/tableStore'; +import { + TableStoreCellAttributes, + useTableStore, +} from '../../stores/tableStore'; import { TablePlugin, TTableCellElement, @@ -47,11 +50,13 @@ export const useTableElementState = ({ const element = useElement(); const selectedCells = useTableStore().get.selectedCells(); const marginLeftOverride = useTableStore().get.marginLeftOverride(); + const setCellAttributes = useTableStore().set.cellAttributes(); - // initial calc, than it will be calculated when each individual cell updated useEffect(() => { - calculateCellIndexes(editor, element); - }, [editor, element]); + const newAttributes = new WeakMap() as TableStoreCellAttributes; + calculateCellIndexes(editor, element, newAttributes); + setCellAttributes(newAttributes); + }, [editor, element, setCellAttributes]); const marginLeft = disableMarginLeft ? 0 @@ -98,13 +103,13 @@ export const useTableElement = () => { }; }; -const cellAttributes = new WeakMap< - TTableCellElement, - { row: number; col: number } ->(); +// const cellAttributes = new WeakMap< +// TTableCellElement, +// { row: number; col: number } +// >(); function getCellIndices( - editor: PlateEditor, + cellAttributes: TableStoreCellAttributes, tableEl: TTableElement, tablePath: Path, cellPath: Path @@ -161,7 +166,8 @@ function getCellIndices( const calculateCellIndexes = ( editor: PlateEditor, - tableNode: TTableElement + tableNode: TTableElement, + cellAttributes: TableStoreCellAttributes ) => { // (Place the `getCellIndices()` function from the previous response here) @@ -180,19 +186,23 @@ const calculateCellIndexes = ( const cell = row.children[c] as TTableCellElement; // Get cell indices and store them in the row's array - // const cellPath = findNodePath(editor, cell)!; // TODO: use concat instead of findNodePath const cellPath = [r, c]; - console.log( - 'searching for', - cell.children.map((m) => { - return (m as any).children[0].text; - }), + // console.log( + // 'searching for', + // cell.children.map((m) => { + // return (m as any).children[0].text; + // }), + // tableNode, + // tablePath, + // cellPath + // ); + + const indices = getCellIndices( + cellAttributes, tableNode, tablePath, cellPath ); - - const indices = getCellIndices(editor, tableNode, tablePath, cellPath); if (indices) { cellAttributes.set(cell, indices); } diff --git a/packages/table/src/stores/tableStore.ts b/packages/table/src/stores/tableStore.ts index 91842d9a93..676bf5fcee 100644 --- a/packages/table/src/stores/tableStore.ts +++ b/packages/table/src/stores/tableStore.ts @@ -2,13 +2,20 @@ import { useCallback } from 'react'; import { createAtomStore, TElement } from '@udecode/plate-common'; import { ELEMENT_TABLE } from '../createTablePlugin'; +import { TTableCellElement } from '../types'; export type TableStoreSizeOverrides = Map; +export type TableStoreCellAttributes = Map< + TTableCellElement, + { row: number; col: number } +>; + export const { tableStore, useTableStore } = createAtomStore( { colSizeOverrides: new Map() as TableStoreSizeOverrides, rowSizeOverrides: new Map() as TableStoreSizeOverrides, + cellAttributes: new WeakMap() as TableStoreCellAttributes, marginLeftOverride: null as number | null, hoveredColIndex: null as number | null, selectedCells: null as TElement[] | null, diff --git a/packages/table/src/types.ts b/packages/table/src/types.ts index 3e4ecc9d42..1f21d5b7c8 100644 --- a/packages/table/src/types.ts +++ b/packages/table/src/types.ts @@ -95,6 +95,10 @@ export interface TTableCellElement extends TElement { */ right?: BorderStyle; }; + attributes: { + colspan?: string; + rowspan?: string; + } } export type BorderDirection = 'top' | 'left' | 'bottom' | 'right'; diff --git a/templates/plate-playground-template/src/components/plate-ui/table-cell-element.tsx b/templates/plate-playground-template/src/components/plate-ui/table-cell-element.tsx index 0effb3e381..b367f41877 100644 --- a/templates/plate-playground-template/src/components/plate-ui/table-cell-element.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/table-cell-element.tsx @@ -34,11 +34,13 @@ const TableCellElement = React.forwardRef< rowSize, borders, isSelectingCell, + colSpan, } = useTableCellElementState(); const { props: cellProps } = useTableCellElement({ element: props.element }); const resizableState = useTableCellElementResizableState({ colIndex, rowIndex, + colSpan, }); const { rightProps, bottomProps, leftProps, hiddenLeft } = useTableCellElementResizable(resizableState); From ec077ff9bfc7b8edae131aa293d2bdfc250b6ce4 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 10 Nov 2023 13:21:44 +0300 Subject: [PATCH 04/16] fix issue in ff --- apps/www/src/registry/default/plate-ui/table-cell-element.tsx | 2 +- .../src/components/plate-ui/table-cell-element.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/www/src/registry/default/plate-ui/table-cell-element.tsx b/apps/www/src/registry/default/plate-ui/table-cell-element.tsx index fb6f8652d6..818f19b8b8 100644 --- a/apps/www/src/registry/default/plate-ui/table-cell-element.tsx +++ b/apps/www/src/registry/default/plate-ui/table-cell-element.tsx @@ -53,7 +53,7 @@ const TableCellElement = React.forwardRef< asChild ref={ref} className={cn( - 'relative overflow-visible border-none bg-background p-0', + 'relative h-full overflow-visible border-none bg-background p-0', hideBorder && 'before:border-none', element.background ? 'bg-[--cellBackground]' : 'bg-background', !hideBorder && diff --git a/templates/plate-playground-template/src/components/plate-ui/table-cell-element.tsx b/templates/plate-playground-template/src/components/plate-ui/table-cell-element.tsx index b367f41877..2bfc8712d0 100644 --- a/templates/plate-playground-template/src/components/plate-ui/table-cell-element.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/table-cell-element.tsx @@ -52,7 +52,7 @@ const TableCellElement = React.forwardRef< asChild ref={ref} className={cn( - 'relative overflow-visible border-none bg-background p-0', + 'relative h-full overflow-visible border-none bg-background p-0', hideBorder && 'before:border-none', element.background ? 'bg-[--cellBackground]' : 'bg-background', !hideBorder && From c55ccf39e73acf516f6a4124cc0fede5fffa2b18 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 10 Nov 2023 13:22:17 +0300 Subject: [PATCH 05/16] improve selection, indices computation --- .../src/lib/plate/demo/values/tableValue.tsx | 1 + .../useTableCellElementState.ts | 61 ++++---- .../TableElement/useTableElement.ts | 139 +----------------- packages/table/src/createTablePlugin.ts | 3 +- .../table/src/queries/findCellByIndexes.ts | 41 ++++++ packages/table/src/queries/getCellIdices.ts | 105 +++++++++++++ packages/table/src/queries/getColSpan.ts | 10 ++ packages/table/src/queries/getIndices.ts | 13 ++ .../table/src/queries/getIndicesWithSpans.ts | 19 +++ packages/table/src/queries/getRowSpan.ts | 10 ++ .../table/src/queries/getTableGridByRange.ts | 109 +++++++++++--- packages/table/src/stores/tableStore.ts | 7 - packages/table/src/types.ts | 10 ++ packages/table/src/utils/getEmptyCellNode.ts | 3 +- packages/table/src/utils/getEmptyRowNode.ts | 3 +- 15 files changed, 336 insertions(+), 198 deletions(-) create mode 100644 packages/table/src/queries/findCellByIndexes.ts create mode 100644 packages/table/src/queries/getCellIdices.ts create mode 100644 packages/table/src/queries/getColSpan.ts create mode 100644 packages/table/src/queries/getIndices.ts create mode 100644 packages/table/src/queries/getIndicesWithSpans.ts create mode 100644 packages/table/src/queries/getRowSpan.ts diff --git a/apps/www/src/lib/plate/demo/values/tableValue.tsx b/apps/www/src/lib/plate/demo/values/tableValue.tsx index 30417572f0..0e79843495 100644 --- a/apps/www/src/lib/plate/demo/values/tableValue.tsx +++ b/apps/www/src/lib/plate/demo/values/tableValue.tsx @@ -167,6 +167,7 @@ export const tableValue: any = ( to design structured layouts. {createMergedCellsTable()} + {createSpanningTable()} {/* {createTable()} */} {/* This table is an example of rendering a table spanning multiple columns: diff --git a/packages/table/src/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/components/TableCellElement/useTableCellElementState.ts index ff45a0de15..875503e60a 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -1,11 +1,19 @@ -import { useEffect } from 'react'; -import { useEditorRef, useElement } from '@udecode/plate-common'; +import { useEffect, useMemo } from 'react'; +import { + getPluginOptions, + useEditorRef, + useElement, +} from '@udecode/plate-common'; import { useReadOnly } from 'slate-react'; import { ELEMENT_TABLE, ELEMENT_TR } from '../../createTablePlugin'; +import { getCellIndices } from '../../queries/getCellIdices'; +import { getColSpan } from '../../queries/getColSpan'; +import { getRowSpan } from '../../queries/getRowSpan'; import { getTableColumnIndex, getTableRowIndex } from '../../queries/index'; import { useTableStore } from '../../stores/tableStore'; import { + TablePlugin, TTableCellElement, TTableElement, TTableRowElement, @@ -29,24 +37,6 @@ export type TableCellElementState = { colSpan: number; }; -/** - * Returns the colspan attribute of the table cell element. - * @default 1 if undefined. - */ -export const getColSpan = (cellElem: TTableCellElement) => { - const attrColSpan = Number(cellElem.attributes?.colspan); - return cellElem.colSpan || attrColSpan || 1; -}; - -/** - * Returns the rowspan attribute of the table cell element. - * @default 1 if undefined - */ -export const getRowSpan = (cellElem: TTableCellElement) => { - const attrRowSpan = Number(cellElem.attributes?.rowspan); - return cellElem.rowSpan || attrRowSpan || 1; -}; - export const useTableCellElementState = ({ ignoreReadOnly, }: { @@ -61,22 +51,39 @@ export const useTableCellElementState = ({ const colSpan = getColSpan(cellElement); const rowSpan = getRowSpan(cellElement); - const defaultColIndex = getTableColumnIndex(editor, cellElement); - const defaultRowIndex = getTableRowIndex(editor, cellElement); - const readOnly = useReadOnly(); const isCellSelected = useIsCellSelected(cellElement); const hoveredColIndex = useTableStore().get.hoveredColIndex(); const selectedCells = useTableStore().get.selectedCells(); - const cellAttributes = useTableStore().get.cellAttributes(); const tableElement = useElement(ELEMENT_TABLE); const rowElement = useElement(ELEMENT_TR); - const x = cellAttributes.get(cellElement); - const colIndex = x?.col ?? defaultColIndex; - const rowIndex = x?.row ?? defaultRowIndex; + const { _cellIndices } = useMemo( + () => getPluginOptions(editor as any, ELEMENT_TABLE), + [editor] + ); + + let x: { col: number; row: number }; + const fromWeakMap = _cellIndices.get(cellElement); + if (fromWeakMap) { + x = fromWeakMap; + console.log('from weak map', x, 'cellElement', cellElement); + } else { + const x1 = getCellIndices(editor, tableElement, cellElement); + if (x1) { + x = x1; + console.log('computed', x, 'cellElement', cellElement); + } else { + const defaultColIndex = getTableColumnIndex(editor, cellElement); + const defaultRowIndex = getTableRowIndex(editor, cellElement); + x = { col: defaultColIndex, row: defaultRowIndex }; + console.log('get default', x, 'cellElement', cellElement); + } + } + const colIndex = x.col; + const rowIndex = x.row; const endingRowIndex = rowIndex + rowSpan - 1; const endingColIndex = colIndex + colSpan - 1; diff --git a/packages/table/src/components/TableElement/useTableElement.ts b/packages/table/src/components/TableElement/useTableElement.ts index 9cea6ba0f4..e0d5b76765 100644 --- a/packages/table/src/components/TableElement/useTableElement.ts +++ b/packages/table/src/components/TableElement/useTableElement.ts @@ -1,27 +1,13 @@ -import { useEffect } from 'react'; import { collapseSelection, - findNodePath, - getNode, - getParentNode, getPluginOptions, - PlateEditor, useEditorRef, useElement, } from '@udecode/plate-common'; -import { Path } from 'slate'; import { ELEMENT_TABLE } from '../../createTablePlugin'; -import { - TableStoreCellAttributes, - useTableStore, -} from '../../stores/tableStore'; -import { - TablePlugin, - TTableCellElement, - TTableElement, - TTableRowElement, -} from '../../types'; +import { useTableStore } from '../../stores/tableStore'; +import { TablePlugin, TTableElement } from '../../types'; import { useSelectedCells } from './useSelectedCells'; import { useTableColSizes } from './useTableColSizes'; @@ -50,13 +36,6 @@ export const useTableElementState = ({ const element = useElement(); const selectedCells = useTableStore().get.selectedCells(); const marginLeftOverride = useTableStore().get.marginLeftOverride(); - const setCellAttributes = useTableStore().set.cellAttributes(); - - useEffect(() => { - const newAttributes = new WeakMap() as TableStoreCellAttributes; - calculateCellIndexes(editor, element, newAttributes); - setCellAttributes(newAttributes); - }, [editor, element, setCellAttributes]); const marginLeft = disableMarginLeft ? 0 @@ -102,117 +81,3 @@ export const useTableElement = () => { }, }; }; - -// const cellAttributes = new WeakMap< -// TTableCellElement, -// { row: number; col: number } -// >(); - -function getCellIndices( - cellAttributes: TableStoreCellAttributes, - tableEl: TTableElement, - tablePath: Path, - cellPath: Path -) { - const tableNodes = tableEl.children; - - let rowIndex = -1; - let colIndex = -1; - - for (let r = 0; r < tableNodes.length; r++) { - const row = tableNodes[r] as TTableRowElement; - - let cIndex = 0; - for (let c = 0; c < row.children.length; c++) { - const cell = row.children[c] as TTableCellElement; - const curCellPath = [r, c]; - if (Path.equals(curCellPath, cellPath)) { - colIndex = cIndex; - rowIndex = r; - break; - } - cIndex += cell.colSpan || 1; // consider 0 and undefined as 1 - } - } - - tableNodes.slice(0, rowIndex).forEach((pR, _rowIndex) => { - const prevRow = pR as TTableRowElement; - prevRow.children.forEach((pC) => { - const prevCell = pC as TTableCellElement; - const prevIndices = cellAttributes.get(prevCell); - if (prevIndices) { - const { col: prevColIndex } = prevIndices; - if ( - // colIndex affects - prevColIndex <= colIndex && - // rowSpan affects - prevCell.rowSpan && - prevCell.rowSpan > 1 && - rowIndex - _rowIndex < prevCell.rowSpan - ) { - colIndex += prevCell.colSpan || 1; - } - } - }); - }); - - if (rowIndex === -1 || colIndex === -1) { - console.log('Invalid cell location.'); - return null; - } - - return { row: rowIndex, col: colIndex }; -} - -const calculateCellIndexes = ( - editor: PlateEditor, - tableNode: TTableElement, - cellAttributes: TableStoreCellAttributes -) => { - // (Place the `getCellIndices()` function from the previous response here) - - // Initialize an array to store the indices of each cell - const cellIndicesArray = []; - - const tablePath = findNodePath(editor, tableNode)!; - - // Iterate through the table rows - for (let r = 0; r < tableNode.children.length; r++) { - const row = tableNode.children[r] as TTableRowElement; - const rowIndicesArray = []; - - // Iterate through the row cells - for (let c = 0; c < row.children.length; c++) { - const cell = row.children[c] as TTableCellElement; - - // Get cell indices and store them in the row's array - const cellPath = [r, c]; - // console.log( - // 'searching for', - // cell.children.map((m) => { - // return (m as any).children[0].text; - // }), - // tableNode, - // tablePath, - // cellPath - // ); - - const indices = getCellIndices( - cellAttributes, - tableNode, - tablePath, - cellPath - ); - if (indices) { - cellAttributes.set(cell, indices); - } - rowIndicesArray.push(indices); - } - - // Push the rowIndicesArray to the cellIndicesArray - cellIndicesArray.push(rowIndicesArray); - } - - console.log('cellIndicesArray', cellIndicesArray); - return cellIndicesArray; -}; diff --git a/packages/table/src/createTablePlugin.ts b/packages/table/src/createTablePlugin.ts index 533f30ef13..7ecb072bb6 100644 --- a/packages/table/src/createTablePlugin.ts +++ b/packages/table/src/createTablePlugin.ts @@ -2,7 +2,7 @@ import { createPluginFactory } from '@udecode/plate-common'; import { onKeyDownTable } from './onKeyDownTable'; import { insertTableColumn, insertTableRow } from './transforms/index'; -import { TablePlugin } from './types'; +import { TablePlugin, TableStoreCellAttributes } from './types'; import { withTable } from './withTable'; export const ELEMENT_TABLE = 'table'; @@ -36,6 +36,7 @@ export const createTablePlugin = createPluginFactory({ }); }, minColumnWidth: 48, + _cellIndices: new WeakMap() as TableStoreCellAttributes, }, withOverrides: withTable, plugins: [ diff --git a/packages/table/src/queries/findCellByIndexes.ts b/packages/table/src/queries/findCellByIndexes.ts new file mode 100644 index 0000000000..150c724af0 --- /dev/null +++ b/packages/table/src/queries/findCellByIndexes.ts @@ -0,0 +1,41 @@ +import { Value } from '@udecode/plate-common'; + +import { TablePlugin, TTableCellElement, TTableElement } from '../types'; +import { getIndices } from './getIndices'; +import { getIndicesWithSpans } from './getIndicesWithSpans'; + +export const findCellByIndexes1 = ( + options: TablePlugin, + table: TTableElement, + searchRowIndex: number, + searchColIndex: number +) => { + const allCells = table.children.flatMap( + (current) => current.children + ) as TTableCellElement[]; + + // console.log('searching for', searchRowIndex, searchColIndex); + const foundCell = allCells.find((cell) => { + const cellElement = cell as TTableCellElement; + + const { _startColIndex, _startRowIndex } = getIndices(options, cellElement); + const { _endRowIndex, _endColIndex } = getIndicesWithSpans( + options, + cellElement + ); + + // console.log('current', colIndex, endColIndex, rowIndex, endRowIndex); + if ( + searchColIndex >= _startColIndex && + searchColIndex <= _endColIndex && + searchRowIndex >= _startRowIndex && + searchRowIndex <= _endRowIndex + ) { + return true; + } + + return false; + }); + + return foundCell; +}; diff --git a/packages/table/src/queries/getCellIdices.ts b/packages/table/src/queries/getCellIdices.ts new file mode 100644 index 0000000000..e51356274b --- /dev/null +++ b/packages/table/src/queries/getCellIdices.ts @@ -0,0 +1,105 @@ +import { getPluginOptions, PlateEditor } from '@udecode/plate-common'; + +import { ELEMENT_TABLE } from '../createTablePlugin'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; + +export function getCellIndices( + editor: PlateEditor, + tableEl: TTableElement, + cellEl: TTableCellElement +) { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const tableNodes = tableEl.children; + + let rowIndex = -1; + let colIndex = -1; + + for (let r = 0; r < tableNodes.length; r++) { + const row = tableNodes[r] as TTableRowElement; + + let cIndex = 0; + for (let c = 0; c < row.children.length; c++) { + const cell = row.children[c] as TTableCellElement; + if (cellEl === cell) { + colIndex = cIndex; + rowIndex = r; + break; + } + cIndex += cell.colSpan || 1; // consider 0 and undefined as 1 + } + } + + tableNodes.slice(0, rowIndex).forEach((pR, _rowIndex) => { + const prevRow = pR as TTableRowElement; + prevRow.children.forEach((pC) => { + const prevCell = pC as TTableCellElement; + const prevIndices = options?._cellIndices?.get(prevCell); + if (prevIndices) { + const { col: prevColIndex } = prevIndices; + if ( + // colIndex affects + prevColIndex <= colIndex && + // rowSpan affects + prevCell.rowSpan && + prevCell.rowSpan > 1 && + rowIndex - _rowIndex < prevCell.rowSpan + ) { + colIndex += prevCell.colSpan || 1; + } + } + }); + }); + + if (rowIndex === -1 || colIndex === -1) { + console.log('Invalid cell location.'); + return null; + } + + const indices = { row: rowIndex, col: colIndex }; + + options?._cellIndices?.set(cellEl, indices); + + return indices; +} + +// const calculateCellIndexes = ( +// editor: PlateEditor, +// tableNode: TTableElement +// ) => { +// // Initialize an array to store the indices of each cell +// const cellIndicesArray = []; + +// const tablePath = findNodePath(editor, tableNode)!; + +// // Iterate through the table rows +// for (let r = 0; r < tableNode.children.length; r++) { +// const row = tableNode.children[r] as TTableRowElement; +// const rowIndicesArray = []; + +// // Iterate through the row cells +// for (let c = 0; c < row.children.length; c++) { +// const cell = row.children[c] as TTableCellElement; + +// // Get cell indices and store them in the row's array +// const cellPath = [r, c]; + +// const indices = getCellIndices(editor, tableNode, cell); +// if (indices) { +// cellIndices.set(cell, indices); +// } +// rowIndicesArray.push(indices); +// } + +// // Push the rowIndicesArray to the cellIndicesArray +// cellIndicesArray.push(rowIndicesArray); +// console.log('calculated array', cellIndicesArray); +// } + +// return cellIndicesArray; +// }; diff --git a/packages/table/src/queries/getColSpan.ts b/packages/table/src/queries/getColSpan.ts new file mode 100644 index 0000000000..2df1070a04 --- /dev/null +++ b/packages/table/src/queries/getColSpan.ts @@ -0,0 +1,10 @@ +import { TTableCellElement } from '../types'; + +/** + * Returns the colspan attribute of the table cell element. + * @default 1 if undefined. + */ +export const getColSpan = (cellElem: TTableCellElement) => { + const attrColSpan = Number(cellElem.attributes?.colspan); + return cellElem.colSpan || attrColSpan || 1; +}; diff --git a/packages/table/src/queries/getIndices.ts b/packages/table/src/queries/getIndices.ts new file mode 100644 index 0000000000..c5b6a60bbb --- /dev/null +++ b/packages/table/src/queries/getIndices.ts @@ -0,0 +1,13 @@ +import { Value } from '@udecode/plate-common'; + +import { TablePlugin, TTableCellElement } from '../types'; + +export const getIndices = ( + options: TablePlugin, + startCell: TTableCellElement +) => { + const { col: _startColIndex, row: _startRowIndex } = + options._cellIndices.get(startCell)!; + + return { _startColIndex, _startRowIndex }; +}; diff --git a/packages/table/src/queries/getIndicesWithSpans.ts b/packages/table/src/queries/getIndicesWithSpans.ts new file mode 100644 index 0000000000..c07cf73b62 --- /dev/null +++ b/packages/table/src/queries/getIndicesWithSpans.ts @@ -0,0 +1,19 @@ +import { Value } from '@udecode/plate-common'; + +import { TablePlugin, TTableCellElement } from '../types'; +import { getColSpan } from './getColSpan'; +import { getRowSpan } from './getRowSpan'; + +export const getIndicesWithSpans = ( + options: TablePlugin, + endCell: TTableCellElement +) => { + const { col: __endColIndex, row: __endRowIndex } = + options._cellIndices.get(endCell)!; + + // TODO: improve typing + const _endRowIndex = __endRowIndex + getRowSpan(endCell) - 1; + const _endColIndex = __endColIndex + getColSpan(endCell) - 1; + + return { _endRowIndex, _endColIndex }; +}; diff --git a/packages/table/src/queries/getRowSpan.ts b/packages/table/src/queries/getRowSpan.ts new file mode 100644 index 0000000000..6e83104836 --- /dev/null +++ b/packages/table/src/queries/getRowSpan.ts @@ -0,0 +1,10 @@ +import { TTableCellElement } from '../types'; + +/** + * Returns the rowspan attribute of the table cell element. + * @default 1 if undefined + */ +export const getRowSpan = (cellElem: TTableCellElement) => { + const attrRowSpan = Number(cellElem.attributes?.rowspan); + return cellElem.rowSpan || attrRowSpan || 1; +}; diff --git a/packages/table/src/queries/getTableGridByRange.ts b/packages/table/src/queries/getTableGridByRange.ts index 84caada168..a0f9fceb9a 100644 --- a/packages/table/src/queries/getTableGridByRange.ts +++ b/packages/table/src/queries/getTableGridByRange.ts @@ -1,5 +1,8 @@ import { - getNode, + findNode, + findNodePath, + getPluginOptions, + getPluginType, PlateEditor, TElement, TElementEntry, @@ -7,8 +10,29 @@ import { } from '@udecode/plate-common'; import { Range } from 'slate'; -import { TTableElement } from '../types'; +import { ELEMENT_TABLE } from '../createTablePlugin'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getCellTypes } from '../utils'; import { getEmptyTableNode } from '../utils/getEmptyTableNode'; +import { findCellByIndexes1 } from './findCellByIndexes'; +import { getIndices } from './getIndices'; +import { getIndicesWithSpans } from './getIndicesWithSpans'; + +export type FormatType = 'table' | 'cell' | 'all'; + +export interface TableGridEntries { + tableEntries: TElementEntry[]; + cellEntries: TElementEntry[]; +} + +export type GetTableGridReturnType = T extends 'all' + ? TableGridEntries + : TElementEntry[]; export interface GetTableGridByRangeOptions { at: Range; @@ -28,21 +52,37 @@ export const getTableGridByRange = ( editor: PlateEditor, { at, format = 'table' }: GetTableGridByRangeOptions ): TElementEntry[] => { - const startCellPath = at.anchor.path; - const endCellPath = at.focus.path; + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const startCellEntry = findNode(editor, { + at: (at as any).anchor.path, + match: { type: getCellTypes(editor) }, + })!; // TODO: improve typing + const endCellEntry = findNode(editor, { + at: (at as any).focus.path, + match: { type: getCellTypes(editor) }, + })!; + + const startCell = startCellEntry[0] as TTableCellElement; + const endCell = endCellEntry[0] as TTableCellElement; - const _startRowIndex = startCellPath.at(-2)!; - const _endRowIndex = endCellPath.at(-2)!; - const _startColIndex = startCellPath.at(-1)!; - const _endColIndex = endCellPath.at(-1)!; + const startCellPath = (at as any).anchor.path; + const tablePath = startCellPath.slice(0, -2); + + const tableEntry = findNode(editor, { + at: tablePath, + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + })!; // TODO: improve typing + const realTable = tableEntry[0] as TTableElement; + + const { _startColIndex, _startRowIndex } = getIndices(options, startCell); + const { _endRowIndex, _endColIndex } = getIndicesWithSpans(options, endCell); const startRowIndex = Math.min(_startRowIndex, _endRowIndex); const endRowIndex = Math.max(_startRowIndex, _endRowIndex); const startColIndex = Math.min(_startColIndex, _endColIndex); const endColIndex = Math.max(_startColIndex, _endColIndex); - const tablePath = startCellPath.slice(0, -2); - const relativeRowIndex = endRowIndex - startRowIndex; const relativeColIndex = endColIndex - startColIndex; @@ -52,37 +92,58 @@ export const getTableGridByRange = ( newCellChildren: [], }); - let rowIndex = startRowIndex; - let colIndex = startColIndex; - const cellEntries: TElementEntry[] = []; + const cellsSet = new Set(); + let rowIndex = startRowIndex; + let colIndex = startColIndex; while (true) { - const cellPath = tablePath.concat([rowIndex, colIndex]); - - const cell = getNode(editor, cellPath); - if (!cell) break; + const cell = findCellByIndexes1(options, realTable, rowIndex, colIndex); + if (!cell) { + break; + } - const rows = table.children[rowIndex - startRowIndex] - .children as TElement[]; + const cellPath = findNodePath(editor, cell)!; + const path = cellPath.join(''); + if (!cellsSet.has(path)) { + cellsSet.add(path); - rows[colIndex - startColIndex] = cell; + const rows = table.children[rowIndex - startRowIndex] + .children as TElement[]; + rows[colIndex - startColIndex] = cell; - cellEntries.push([cell, cellPath]); + cellEntries.push([cell, cellPath]); + } if (colIndex + 1 <= endColIndex) { - colIndex += 1; + colIndex = colIndex + 1; } else if (rowIndex + 1 <= endRowIndex) { colIndex = startColIndex; - rowIndex += 1; + rowIndex = rowIndex + 1; } else { break; } } - if (format === 'cell') { + const formatType = (format as string) || 'table'; + + if (formatType === 'cell') { return cellEntries; } + // clear redundant cells + table.children?.forEach((rowEl) => { + const rowElement = rowEl as TTableRowElement; + + const filteredChildren = rowElement.children?.filter((cellEl) => { + const cellElement = cellEl as TTableCellElement; + return !!cellElement?.children.length; + }); + + rowElement.children = filteredChildren; + }); + + console.log('return entries', [[table, tablePath]], cellEntries); + return [[table, tablePath]]; }; diff --git a/packages/table/src/stores/tableStore.ts b/packages/table/src/stores/tableStore.ts index 676bf5fcee..91842d9a93 100644 --- a/packages/table/src/stores/tableStore.ts +++ b/packages/table/src/stores/tableStore.ts @@ -2,20 +2,13 @@ import { useCallback } from 'react'; import { createAtomStore, TElement } from '@udecode/plate-common'; import { ELEMENT_TABLE } from '../createTablePlugin'; -import { TTableCellElement } from '../types'; export type TableStoreSizeOverrides = Map; -export type TableStoreCellAttributes = Map< - TTableCellElement, - { row: number; col: number } ->; - export const { tableStore, useTableStore } = createAtomStore( { colSizeOverrides: new Map() as TableStoreSizeOverrides, rowSizeOverrides: new Map() as TableStoreSizeOverrides, - cellAttributes: new WeakMap() as TableStoreCellAttributes, marginLeftOverride: null as number | null, hoveredColIndex: null as number | null, selectedCells: null as TElement[] | null, diff --git a/packages/table/src/types.ts b/packages/table/src/types.ts index 1f21d5b7c8..173462c7e5 100644 --- a/packages/table/src/types.ts +++ b/packages/table/src/types.ts @@ -58,8 +58,18 @@ export interface TablePlugin { * @default 48 */ minColumnWidth?: number; + + /** + * For internal use. Keeps track of cell indices + */ + _cellIndices: TableStoreCellAttributes; } +export type TableStoreCellAttributes = Map< + TTableCellElement, + { row: number; col: number } +>; + export interface BorderStyle { // https://docx.js.org/api/enums/BorderStyle.html style?: string; diff --git a/packages/table/src/utils/getEmptyCellNode.ts b/packages/table/src/utils/getEmptyCellNode.ts index d9b87b5b5f..98c84984bb 100644 --- a/packages/table/src/utils/getEmptyCellNode.ts +++ b/packages/table/src/utils/getEmptyCellNode.ts @@ -3,7 +3,8 @@ import { getPluginType, PlateEditor, Value } from '@udecode/plate-common'; import { ELEMENT_TD, ELEMENT_TH } from '../createTablePlugin'; import { TablePlugin } from '../types'; -export interface GetEmptyCellNodeOptions extends TablePlugin { +export interface GetEmptyCellNodeOptions + extends Omit { /** * Header cell */ diff --git a/packages/table/src/utils/getEmptyRowNode.ts b/packages/table/src/utils/getEmptyRowNode.ts index 38682507a0..063434e34e 100644 --- a/packages/table/src/utils/getEmptyRowNode.ts +++ b/packages/table/src/utils/getEmptyRowNode.ts @@ -3,7 +3,8 @@ import { getPluginType, PlateEditor, Value } from '@udecode/plate-common'; import { ELEMENT_TR } from '../createTablePlugin'; import { getEmptyCellNode, GetEmptyCellNodeOptions } from './getEmptyCellNode'; -export interface GetEmptyRowNodeOptions extends GetEmptyCellNodeOptions { +export interface GetEmptyRowNodeOptions + extends Omit { colCount?: number; } From 9dc9735a745d834b954fb72dbba8c5e990fa8275 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 15 Nov 2023 16:49:11 +0300 Subject: [PATCH 06/16] add merge / unmerge --- apps/www/src/components/icons.tsx | 4 + .../src/lib/plate/demo/values/tableValue.tsx | 56 +++++++- .../default/plate-ui/table-element.tsx | 85 +++++++++--- .../useTableCellElementState.ts | 19 ++- .../TableElement/useTableElement.ts | 6 + packages/table/src/index.ts | 1 + packages/table/src/merge/index.ts | 6 + packages/table/src/merge/mergeTableCells.ts | 129 ++++++++++++++++++ packages/table/src/merge/unmergeTableCells.ts | 106 ++++++++++++++ .../table/src/queries/computeCellIndices.ts | 116 ++++++++++++++++ .../table/src/queries/findCellByIndexes.ts | 20 ++- packages/table/src/queries/getCellIdices.ts | 105 -------------- packages/table/src/queries/getIndices.ts | 5 +- .../table/src/queries/getIndicesWithSpans.ts | 14 +- .../table/src/queries/getTableGridByRange.ts | 27 ++-- packages/table/src/types.ts | 2 +- 16 files changed, 541 insertions(+), 160 deletions(-) create mode 100644 packages/table/src/merge/index.ts create mode 100644 packages/table/src/merge/mergeTableCells.ts create mode 100644 packages/table/src/merge/unmergeTableCells.ts create mode 100644 packages/table/src/queries/computeCellIndices.ts delete mode 100644 packages/table/src/queries/getCellIdices.ts diff --git a/apps/www/src/components/icons.tsx b/apps/www/src/components/icons.tsx index 793de4ff5d..bc3fb4ef83 100644 --- a/apps/www/src/components/icons.tsx +++ b/apps/www/src/components/icons.tsx @@ -15,6 +15,7 @@ import { ChevronsUpDown, ClipboardCheck, Code2, + Combine, Copy, DownloadCloud, ExternalLink, @@ -73,6 +74,7 @@ import { Trash, Twitter, Underline, + Ungroup, Unlink, WrapText, X, @@ -281,6 +283,8 @@ export const Icons = { codeblock: FileCode, color: Baseline, column: RectangleVertical, + combine: Combine, + ungroup: Ungroup, comment: MessageSquare, commentAdd: MessageSquarePlus, conflict: Unlink, diff --git a/apps/www/src/lib/plate/demo/values/tableValue.tsx b/apps/www/src/lib/plate/demo/values/tableValue.tsx index 0e79843495..09c5493cf3 100644 --- a/apps/www/src/lib/plate/demo/values/tableValue.tsx +++ b/apps/www/src/lib/plate/demo/values/tableValue.tsx @@ -158,6 +158,59 @@ export const createMergedCellsTable = (): any => ( ); +export const createAutoLayoutTable = (): any => ( + + + + + + Heading + + + + + Cell 1 + + + + + Cell 11 + + + + + Cell 2 + + + + + + Cell 3 + + {/* + Cell 77 + */} + + Cell 7 + + + + + + Cell 4 + + + {/* + Cell 5 + */} + + Cell 8 + + + + +); + export const tableValue: any = ( @@ -167,7 +220,8 @@ export const tableValue: any = ( to design structured layouts. {createMergedCellsTable()} - {createSpanningTable()} + {/* {createSpanningTable()} + {createAutoLayoutTable()} */} {/* {createTable()} */} {/* This table is an example of rendering a table spanning multiple columns: diff --git a/apps/www/src/registry/default/plate-ui/table-element.tsx b/apps/www/src/registry/default/plate-ui/table-element.tsx index 713679f69b..15e204fe50 100644 --- a/apps/www/src/registry/default/plate-ui/table-element.tsx +++ b/apps/www/src/registry/default/plate-ui/table-element.tsx @@ -3,6 +3,7 @@ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; import { PopoverAnchor, PopoverContentProps } from '@radix-ui/react-popover'; import { isCollapsed, + isExpanded, PlateElement, PlateElementProps, useEditorState, @@ -10,7 +11,10 @@ import { useRemoveNodeButton, } from '@udecode/plate-common'; import { + getTableGridAbove, + mergeTableCells, TTableElement, + unmergeTableCells, useTableBordersDropdownMenuContentState, useTableElement, useTableElementState, @@ -114,7 +118,66 @@ const TableFloatingToolbar = React.forwardRef< const readOnly = useReadOnly(); const selected = useSelected(); const editor = useEditorState(); - const open = !readOnly && selected && isCollapsed(editor.selection); + // const open = !readOnly && selected && isCollapsed(editor.selection); + + const collapsed = !readOnly && selected && isCollapsed(editor.selection); + const expanded = !readOnly && selected && isExpanded(editor.selection); + const open = !readOnly && selected; + + const cellEntries = getTableGridAbove(editor, { format: 'cell' }); + + const canUnmerge = + collapsed && + cellEntries && + cellEntries.length === 1 && + ((cellEntries[0][0] as any)?.colSpan > 1 || + (cellEntries[0][0] as any)?.rowSpan > 1); + + const mergeContent = expanded && ( + + ); + + const unmergeButton = canUnmerge && ( + + ); + + const bordersContent = collapsed && ( + <> + + + + + + + + + + + + + ); return ( @@ -125,23 +188,9 @@ const TableFloatingToolbar = React.forwardRef< onOpenAutoFocus={(e) => e.preventDefault()} {...props} > - - - - - - - - - - - + {unmergeButton} + {mergeContent} + {bordersContent} ); diff --git a/packages/table/src/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/components/TableCellElement/useTableCellElementState.ts index 875503e60a..cd7a24c125 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -7,7 +7,7 @@ import { import { useReadOnly } from 'slate-react'; import { ELEMENT_TABLE, ELEMENT_TR } from '../../createTablePlugin'; -import { getCellIndices } from '../../queries/getCellIdices'; +import { computeCellIndices } from '../../queries/computeCellIndices'; import { getColSpan } from '../../queries/getColSpan'; import { getRowSpan } from '../../queries/getRowSpan'; import { getTableColumnIndex, getTableRowIndex } from '../../queries/index'; @@ -67,19 +67,28 @@ export const useTableCellElementState = ({ let x: { col: number; row: number }; const fromWeakMap = _cellIndices.get(cellElement); + // const cellContent = cellElement.children.map((i) => { + // return (i.children as any)[0].text; + // }); + + // const spans = { + // colSpan: getColSpan(cellElement), + // rowSpan: getRowSpan(cellElement), + // }; + if (fromWeakMap) { x = fromWeakMap; - console.log('from weak map', x, 'cellElement', cellElement); + // console.log('cellContent', cellContent, x); } else { - const x1 = getCellIndices(editor, tableElement, cellElement); + const x1 = computeCellIndices(editor, tableElement, cellElement); if (x1) { x = x1; - console.log('computed', x, 'cellElement', cellElement); + // console.log('computed', x, 'cellContent', cellContent, 'spans', spans); } else { const defaultColIndex = getTableColumnIndex(editor, cellElement); const defaultRowIndex = getTableRowIndex(editor, cellElement); x = { col: defaultColIndex, row: defaultRowIndex }; - console.log('get default', x, 'cellElement', cellElement); + // console.log('get default', x, 'cellContent', cellContent); } } const colIndex = x.col; diff --git a/packages/table/src/components/TableElement/useTableElement.ts b/packages/table/src/components/TableElement/useTableElement.ts index e0d5b76765..0a84039921 100644 --- a/packages/table/src/components/TableElement/useTableElement.ts +++ b/packages/table/src/components/TableElement/useTableElement.ts @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { collapseSelection, getPluginOptions, @@ -6,6 +7,7 @@ import { } from '@udecode/plate-common'; import { ELEMENT_TABLE } from '../../createTablePlugin'; +import { computeAllCellIndices } from '../../queries/computeCellIndices'; import { useTableStore } from '../../stores/tableStore'; import { TablePlugin, TTableElement } from '../../types'; import { useSelectedCells } from './useSelectedCells'; @@ -43,6 +45,10 @@ export const useTableElementState = ({ let colSizes = useTableColSizes(element); + useEffect(() => { + computeAllCellIndices(editor, element); + }, [editor, element]); + if (transformColSizes) { colSizes = transformColSizes(colSizes); } diff --git a/packages/table/src/index.ts b/packages/table/src/index.ts index 81ea22f4b0..8821555967 100644 --- a/packages/table/src/index.ts +++ b/packages/table/src/index.ts @@ -19,3 +19,4 @@ export * from './queries/index'; export * from './stores/index'; export * from './transforms/index'; export * from './utils/index'; +export * from './merge/index'; diff --git a/packages/table/src/merge/index.ts b/packages/table/src/merge/index.ts new file mode 100644 index 0000000000..9ec11f2d66 --- /dev/null +++ b/packages/table/src/merge/index.ts @@ -0,0 +1,6 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './mergeTableCells'; +export * from './unmergeTableCells'; diff --git a/packages/table/src/merge/mergeTableCells.ts b/packages/table/src/merge/mergeTableCells.ts new file mode 100644 index 0000000000..647fb3bb42 --- /dev/null +++ b/packages/table/src/merge/mergeTableCells.ts @@ -0,0 +1,129 @@ +import { + getBlockAbove, + getPluginOptions, + getPluginType, + insertElements, + PlateEditor, + removeNodes, + Value, + withoutNormalizing, +} from '@udecode/plate-common'; +import { cloneDeep } from 'lodash'; + +import { ELEMENT_TABLE } from '../createTablePlugin'; +import { getTableGridAbove } from '../queries'; +import { computeCellIndices } from '../queries/computeCellIndices'; +import { getColSpan } from '../queries/getColSpan'; +import { getRowSpan } from '../queries/getRowSpan'; +import { TablePlugin, TTableCellElement, TTableElement } from '../types'; +import { getEmptyCellNode } from '../utils'; + +/** + * Merges multiple selected cells into one. + */ +export const mergeTableCells = ( + editor: PlateEditor +) => { + withoutNormalizing(editor, () => { + const options = getPluginOptions(editor, ELEMENT_TABLE); + const tableEntry = getBlockAbove(editor, { + at: editor.selection?.anchor.path, + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + })!; + + const cellEntries = getTableGridAbove(editor, { format: 'cell' }); + + // calculate the colSpan which is the number of horizontal cells that a cell should span. + let colSpan = 0; + for (const entry of cellEntries) { + const [data, path] = entry; + + // count only those cells that are in the first selected row. + if (path[1] === cellEntries[0][1][1]) { + const cellColSpan = getColSpan(data as TTableCellElement); + colSpan += cellColSpan; + } + } + + // calculate the rowSpan which is the number of vertical cells that a cell should span. + let rowSpan = 0; + const { col } = options._cellIndices.get( + cellEntries[0][0] as TTableCellElement + )!; + cellEntries.forEach((cE) => { + const cell = cE[0] as TTableCellElement; + const { col: curCol } = + options._cellIndices.get(cell) || + computeCellIndices(editor, tableEntry[0] as TTableElement, cell)!; + if (col === curCol) { + rowSpan += getRowSpan(cell); + } + }); + + // This will store the content of all cells we are merging + const contents = []; + for (const cellEntry of cellEntries) { + const [el] = cellEntry; + contents.push(...cloneDeep(el.children)); + } + + // Create a hash map where keys are col paths, + // and values are an array of all paths with that column + const cols: { [key: string]: number[][] } = {}; + + // A boolean to keep track if we have a header cell among the cells we are merging + let hasHeaderCell = false; + + cellEntries.forEach(([entry, path]) => { + if (!hasHeaderCell && entry.type === 'table_header_cell') { + hasHeaderCell = true; + } + if (cols[path[1]]) { + cols[path[1]].push(path); + } else { + cols[path[1]] = [path]; + } + }); + + // removes multiple cells with on same path. + // once cell removed, next cell in the row will settle down on that path + Object.values(cols).forEach((paths) => { + paths?.forEach(() => { + removeNodes(editor, { at: paths[0] }); + }); + }); + + // Create a new cell to replace the merged cells, with + // calculated colSpan and rowSpan attributes and combined content + const mergedCell = { + ...getEmptyCellNode(editor, { + header: cellEntries[0][0].type === 'th', + newCellChildren: contents, + }), + colSpan, + rowSpan, + }; + + // insert the new merged cell in place of the first cell in the selection + insertElements(editor, mergedCell, { at: cellEntries[0][1] }); + + // /** + // * Update cell indices in weak map + // */ + // const tableEntry = findNode(editor, { + // at: cellEntries[0][1], + // match: { type: getPluginType(editor, ELEMENT_TABLE) }, + // })!; // TODO: improve typing + // const cellEntry = findNode(editor, { + // at: cellEntries[0][1], + // match: { type: getCellTypes(editor) }, + // })!; // TODO: improve typing + + // const realTable = tableEntry[0] as TTableElement; + // const mC = cellEntry[0] as TTableCellElement; + // console.log('realTable', realTable, 'mC', mC); + + // computeCellIndices(editor, realTable, mC); + // console.log('should be computed'); + }); +}; diff --git a/packages/table/src/merge/unmergeTableCells.ts b/packages/table/src/merge/unmergeTableCells.ts new file mode 100644 index 0000000000..395ba1163d --- /dev/null +++ b/packages/table/src/merge/unmergeTableCells.ts @@ -0,0 +1,106 @@ +import { + findNode, + getPluginOptions, + getPluginType, + insertElements, + PlateEditor, + removeNodes, + TDescendant, + Value, + withoutNormalizing, +} from '@udecode/plate-common'; + +import { ELEMENT_TABLE, ELEMENT_TR } from '../createTablePlugin'; +import { getTableGridAbove } from '../queries'; +import { getColSpan } from '../queries/getColSpan'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getEmptyCellNode } from '../utils'; + +export const unmergeTableCells = ( + editor: PlateEditor +) => { + withoutNormalizing(editor, () => { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const cellEntries = getTableGridAbove(editor, { format: 'cell' }); + const [[cellElem, path]] = cellEntries; + + // creating new object per iteration is essential here + const createEmptyCell = (children?: TDescendant[]) => { + return { + ...getEmptyCellNode(editor, { + header: cellElem.type === 'th', + newCellChildren: children, + }), + colSpan: 1, + rowSpan: 1, + }; + }; + + const tablePath = path.slice(0, -2); + + const cellPath = path.slice(-2); + const [rowPath, colPath] = cellPath; + const colSpan = cellElem.colSpan as number; + const rowSpan = cellElem.rowSpan as number; + + // Generate an array of column paths from the colspan + const colPaths: number[] = []; + for (let i = 0; i < colSpan; i++) { + colPaths.push(colPath + i); + } + + // Remove the original merged cell from the editor + removeNodes(editor, { at: path }); + + const { col } = options._cellIndices.get(cellElem as TTableCellElement)!; + + const getColPathForRow = (row: number) => { + let newColPath = 0; + + const rowEntry = findNode(editor, { + at: [...tablePath, row], + match: { type: getPluginType(editor, ELEMENT_TR) }, + })!; // TODO: improve typing + const rowEl = rowEntry[0] as TTableRowElement; + + for (const item of rowEl.children) { + const { col: c } = options._cellIndices.get(item as TTableCellElement)!; + if (c === col - 1) { + newColPath = rowEl.children.indexOf(item) + 1; + // console.log('found first', newColPath); + break; + } + if (col + getColSpan(cellElem as TTableCellElement) === c - 1) { + newColPath = rowEl.children.indexOf(item); + // console.log('found last', newColPath); + break; + } + } + + return newColPath; + }; + + // Generate an array of cell paths from the row and col spans and then insert empty cells at those paths + for (let i = 0; i < rowSpan; i++) { + const currentRowPath = rowPath + i; + const pathForNextRows = getColPathForRow(currentRowPath); + for (let j = 0; j < colPaths.length; j++) { + const currentColPath = i === 0 ? colPaths[j] : pathForNextRows; + + const pathForNewCell = [...tablePath, currentRowPath, currentColPath]; + const cellToInsert = + i === 0 && j === 0 + ? createEmptyCell(cellElem.children) + : createEmptyCell(); + + insertElements(editor, cellToInsert, { at: pathForNewCell }); + } + } + }); +}; diff --git a/packages/table/src/queries/computeCellIndices.ts b/packages/table/src/queries/computeCellIndices.ts new file mode 100644 index 0000000000..009912c2d5 --- /dev/null +++ b/packages/table/src/queries/computeCellIndices.ts @@ -0,0 +1,116 @@ +import { + findNodePath, + getPluginOptions, + PlateEditor, + Value, +} from '@udecode/plate-common'; + +import { ELEMENT_TABLE } from '../createTablePlugin'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; + +export function computeCellIndices( + editor: PlateEditor, + tableEl: TTableElement, + cellEl: TTableCellElement +) { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const tableNodes = tableEl.children; + + let rowIndex = -1; + let colIndex = -1; + + for (let r = 0; r < tableNodes.length; r++) { + const row = tableNodes[r] as TTableRowElement; + + let cIndex = 0; + for (let c = 0; c < row.children.length; c++) { + const cell = row.children[c] as TTableCellElement; + if (cellEl === cell) { + colIndex = cIndex; + rowIndex = r; + break; + } + cIndex += cell.colSpan || 1; // consider 0 and undefined as 1 + } + } + + tableNodes.slice(0, rowIndex).forEach((pR, _rowIndex) => { + const prevRow = pR as TTableRowElement; + prevRow.children.forEach((pC) => { + const prevCell = pC as TTableCellElement; + const prevIndices = options?._cellIndices?.get(prevCell); + if (prevIndices) { + const { col: prevColIndex } = prevIndices; + if ( + // colIndex affects + prevColIndex <= colIndex && + // rowSpan affects + prevCell.rowSpan && + prevCell.rowSpan > 1 && + rowIndex - _rowIndex < prevCell.rowSpan + ) { + colIndex += prevCell.colSpan || 1; + } + } + }); + }); + + if (rowIndex === -1 || colIndex === -1) { + // console.log('Invalid cell location.'); + return null; + } + + const indices = { row: rowIndex, col: colIndex }; + const cellContent = cellEl.children?.map((i) => { + return (i.children as any)[0].text; + }); + // console.log('new cell location', cellContent, indices); + + options?._cellIndices?.set(cellEl, indices); + + return indices; +} + +export const computeAllCellIndices = ( + editor: PlateEditor, + tableNode: TTableElement +) => { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + // Initialize an array to store the indices of each cell + const cellIndicesArray = []; + + // const tablePath = findNodePath(editor, tableNode)!; + + // Iterate through the table rows + for (let r = 0; r < tableNode.children.length; r++) { + const row = tableNode.children[r] as TTableRowElement; + const rowIndicesArray = []; + + // Iterate through the row cells + for (let c = 0; c < row.children.length; c++) { + const cell = row.children[c] as TTableCellElement; + + // Get cell indices and store them in the row's array + // const cellPath = [r, c]; + + const indices = computeCellIndices(editor, tableNode, cell); + if (indices) { + options._cellIndices.set(cell, indices); + } + rowIndicesArray.push(indices); + } + + // Push the rowIndicesArray to the cellIndicesArray + cellIndicesArray.push(rowIndicesArray); + // console.log('calculated array', cellIndicesArray); + } + + return cellIndicesArray; +}; diff --git a/packages/table/src/queries/findCellByIndexes.ts b/packages/table/src/queries/findCellByIndexes.ts index 150c724af0..09ba7f1aef 100644 --- a/packages/table/src/queries/findCellByIndexes.ts +++ b/packages/table/src/queries/findCellByIndexes.ts @@ -1,15 +1,19 @@ -import { Value } from '@udecode/plate-common'; +import { getPluginOptions, PlateEditor, Value } from '@udecode/plate-common'; +import { ELEMENT_TABLE } from '../createTablePlugin'; import { TablePlugin, TTableCellElement, TTableElement } from '../types'; +import { computeCellIndices } from './computeCellIndices'; import { getIndices } from './getIndices'; import { getIndicesWithSpans } from './getIndicesWithSpans'; -export const findCellByIndexes1 = ( - options: TablePlugin, +export const findCellByIndexes = ( + editor: PlateEditor, table: TTableElement, searchRowIndex: number, searchColIndex: number ) => { + const options = getPluginOptions(editor, ELEMENT_TABLE); + const allCells = table.children.flatMap( (current) => current.children ) as TTableCellElement[]; @@ -18,9 +22,13 @@ export const findCellByIndexes1 = ( const foundCell = allCells.find((cell) => { const cellElement = cell as TTableCellElement; - const { _startColIndex, _startRowIndex } = getIndices(options, cellElement); - const { _endRowIndex, _endColIndex } = getIndicesWithSpans( - options, + const indices = + getIndices(options, cellElement) || + computeCellIndices(editor, table, cellElement)!; + + const { col: _startColIndex, row: _startRowIndex } = indices; + const { row: _endRowIndex, col: _endColIndex } = getIndicesWithSpans( + indices, cellElement ); diff --git a/packages/table/src/queries/getCellIdices.ts b/packages/table/src/queries/getCellIdices.ts deleted file mode 100644 index e51356274b..0000000000 --- a/packages/table/src/queries/getCellIdices.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { getPluginOptions, PlateEditor } from '@udecode/plate-common'; - -import { ELEMENT_TABLE } from '../createTablePlugin'; -import { - TablePlugin, - TTableCellElement, - TTableElement, - TTableRowElement, -} from '../types'; - -export function getCellIndices( - editor: PlateEditor, - tableEl: TTableElement, - cellEl: TTableCellElement -) { - const options = getPluginOptions(editor, ELEMENT_TABLE); - - const tableNodes = tableEl.children; - - let rowIndex = -1; - let colIndex = -1; - - for (let r = 0; r < tableNodes.length; r++) { - const row = tableNodes[r] as TTableRowElement; - - let cIndex = 0; - for (let c = 0; c < row.children.length; c++) { - const cell = row.children[c] as TTableCellElement; - if (cellEl === cell) { - colIndex = cIndex; - rowIndex = r; - break; - } - cIndex += cell.colSpan || 1; // consider 0 and undefined as 1 - } - } - - tableNodes.slice(0, rowIndex).forEach((pR, _rowIndex) => { - const prevRow = pR as TTableRowElement; - prevRow.children.forEach((pC) => { - const prevCell = pC as TTableCellElement; - const prevIndices = options?._cellIndices?.get(prevCell); - if (prevIndices) { - const { col: prevColIndex } = prevIndices; - if ( - // colIndex affects - prevColIndex <= colIndex && - // rowSpan affects - prevCell.rowSpan && - prevCell.rowSpan > 1 && - rowIndex - _rowIndex < prevCell.rowSpan - ) { - colIndex += prevCell.colSpan || 1; - } - } - }); - }); - - if (rowIndex === -1 || colIndex === -1) { - console.log('Invalid cell location.'); - return null; - } - - const indices = { row: rowIndex, col: colIndex }; - - options?._cellIndices?.set(cellEl, indices); - - return indices; -} - -// const calculateCellIndexes = ( -// editor: PlateEditor, -// tableNode: TTableElement -// ) => { -// // Initialize an array to store the indices of each cell -// const cellIndicesArray = []; - -// const tablePath = findNodePath(editor, tableNode)!; - -// // Iterate through the table rows -// for (let r = 0; r < tableNode.children.length; r++) { -// const row = tableNode.children[r] as TTableRowElement; -// const rowIndicesArray = []; - -// // Iterate through the row cells -// for (let c = 0; c < row.children.length; c++) { -// const cell = row.children[c] as TTableCellElement; - -// // Get cell indices and store them in the row's array -// const cellPath = [r, c]; - -// const indices = getCellIndices(editor, tableNode, cell); -// if (indices) { -// cellIndices.set(cell, indices); -// } -// rowIndicesArray.push(indices); -// } - -// // Push the rowIndicesArray to the cellIndicesArray -// cellIndicesArray.push(rowIndicesArray); -// console.log('calculated array', cellIndicesArray); -// } - -// return cellIndicesArray; -// }; diff --git a/packages/table/src/queries/getIndices.ts b/packages/table/src/queries/getIndices.ts index c5b6a60bbb..e21a4998c7 100644 --- a/packages/table/src/queries/getIndices.ts +++ b/packages/table/src/queries/getIndices.ts @@ -6,8 +6,5 @@ export const getIndices = ( options: TablePlugin, startCell: TTableCellElement ) => { - const { col: _startColIndex, row: _startRowIndex } = - options._cellIndices.get(startCell)!; - - return { _startColIndex, _startRowIndex }; + return options._cellIndices.get(startCell); }; diff --git a/packages/table/src/queries/getIndicesWithSpans.ts b/packages/table/src/queries/getIndicesWithSpans.ts index c07cf73b62..ff69016c85 100644 --- a/packages/table/src/queries/getIndicesWithSpans.ts +++ b/packages/table/src/queries/getIndicesWithSpans.ts @@ -5,15 +5,11 @@ import { getColSpan } from './getColSpan'; import { getRowSpan } from './getRowSpan'; export const getIndicesWithSpans = ( - options: TablePlugin, + { col, row }: { col: number; row: number }, endCell: TTableCellElement ) => { - const { col: __endColIndex, row: __endRowIndex } = - options._cellIndices.get(endCell)!; - - // TODO: improve typing - const _endRowIndex = __endRowIndex + getRowSpan(endCell) - 1; - const _endColIndex = __endColIndex + getColSpan(endCell) - 1; - - return { _endRowIndex, _endColIndex }; + return { + row: row + getRowSpan(endCell) - 1, + col: col + getColSpan(endCell) - 1, + }; }; diff --git a/packages/table/src/queries/getTableGridByRange.ts b/packages/table/src/queries/getTableGridByRange.ts index a0f9fceb9a..4171cee1c4 100644 --- a/packages/table/src/queries/getTableGridByRange.ts +++ b/packages/table/src/queries/getTableGridByRange.ts @@ -19,7 +19,8 @@ import { } from '../types'; import { getCellTypes } from '../utils'; import { getEmptyTableNode } from '../utils/getEmptyTableNode'; -import { findCellByIndexes1 } from './findCellByIndexes'; +import { computeCellIndices } from './computeCellIndices'; +import { findCellByIndexes } from './findCellByIndexes'; import { getIndices } from './getIndices'; import { getIndicesWithSpans } from './getIndicesWithSpans'; @@ -75,8 +76,15 @@ export const getTableGridByRange = ( })!; // TODO: improve typing const realTable = tableEntry[0] as TTableElement; - const { _startColIndex, _startRowIndex } = getIndices(options, startCell); - const { _endRowIndex, _endColIndex } = getIndicesWithSpans(options, endCell); + const { col: _startColIndex, row: _startRowIndex } = + getIndices(options, startCell) || + computeCellIndices(editor, realTable, startCell)!; + + const { row: _endRowIndex, col: _endColIndex } = getIndicesWithSpans( + getIndices(options, endCell) || + computeCellIndices(editor, realTable, endCell)!, + endCell + ); const startRowIndex = Math.min(_startRowIndex, _endRowIndex); const endRowIndex = Math.max(_startRowIndex, _endRowIndex); @@ -93,25 +101,24 @@ export const getTableGridByRange = ( }); const cellEntries: TElementEntry[] = []; - const cellsSet = new Set(); + const cellsSet = new WeakSet(); let rowIndex = startRowIndex; let colIndex = startColIndex; while (true) { - const cell = findCellByIndexes1(options, realTable, rowIndex, colIndex); + const cell = findCellByIndexes(editor, realTable, rowIndex, colIndex); if (!cell) { break; } - const cellPath = findNodePath(editor, cell)!; - const path = cellPath.join(''); - if (!cellsSet.has(path)) { - cellsSet.add(path); + if (!cellsSet.has(cell)) { + cellsSet.add(cell); const rows = table.children[rowIndex - startRowIndex] .children as TElement[]; rows[colIndex - startColIndex] = cell; + const cellPath = findNodePath(editor, cell)!; cellEntries.push([cell, cellPath]); } @@ -143,7 +150,5 @@ export const getTableGridByRange = ( rowElement.children = filteredChildren; }); - console.log('return entries', [[table, tablePath]], cellEntries); - return [[table, tablePath]]; }; diff --git a/packages/table/src/types.ts b/packages/table/src/types.ts index 173462c7e5..02a3893dce 100644 --- a/packages/table/src/types.ts +++ b/packages/table/src/types.ts @@ -65,7 +65,7 @@ export interface TablePlugin { _cellIndices: TableStoreCellAttributes; } -export type TableStoreCellAttributes = Map< +export type TableStoreCellAttributes = WeakMap< TTableCellElement, { row: number; col: number } >; From 2b5f37960a238af4db879b7673aef154febd6a8c Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 15 Nov 2023 18:36:12 +0300 Subject: [PATCH 07/16] add columns / rows functions --- .../useTableCellElementState.ts | 2 +- .../TableElement/useTableElement.ts | 2 +- packages/table/src/createTablePlugin.ts | 1 + .../{queries => merge}/computeCellIndices.ts | 7 +- packages/table/src/merge/deleteColumn.ts | 230 ++++++++++++++++++ packages/table/src/merge/deleteRow.ts | 172 +++++++++++++ .../{queries => merge}/findCellByIndexes.ts | 4 +- packages/table/src/merge/getCellPath.ts | 33 +++ .../src/{queries => merge}/getIndices.ts | 0 .../{queries => merge}/getIndicesWithSpans.ts | 8 +- .../table/src/merge/getTableGridByRange.ts | 154 ++++++++++++ packages/table/src/merge/insertTableColumn.ts | 221 +++++++++++++++++ packages/table/src/merge/insertTableRow.ts | 205 ++++++++++++++++ packages/table/src/merge/mergeTableCells.ts | 2 +- .../table/src/queries/getTableGridByRange.ts | 119 +++------ packages/table/src/transforms/deleteColumn.ts | 12 +- packages/table/src/transforms/deleteRow.ts | 12 +- .../table/src/transforms/insertTableColumn.ts | 18 +- .../table/src/transforms/insertTableRow.ts | 18 +- packages/table/src/types.ts | 7 +- 20 files changed, 1109 insertions(+), 118 deletions(-) rename packages/table/src/{queries => merge}/computeCellIndices.ts (93%) create mode 100644 packages/table/src/merge/deleteColumn.ts create mode 100644 packages/table/src/merge/deleteRow.ts rename packages/table/src/{queries => merge}/findCellByIndexes.ts (92%) create mode 100644 packages/table/src/merge/getCellPath.ts rename packages/table/src/{queries => merge}/getIndices.ts (100%) rename packages/table/src/{queries => merge}/getIndicesWithSpans.ts (53%) create mode 100644 packages/table/src/merge/getTableGridByRange.ts create mode 100644 packages/table/src/merge/insertTableColumn.ts create mode 100644 packages/table/src/merge/insertTableRow.ts diff --git a/packages/table/src/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/components/TableCellElement/useTableCellElementState.ts index cd7a24c125..3dff3c1433 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -7,7 +7,7 @@ import { import { useReadOnly } from 'slate-react'; import { ELEMENT_TABLE, ELEMENT_TR } from '../../createTablePlugin'; -import { computeCellIndices } from '../../queries/computeCellIndices'; +import { computeCellIndices } from '../../merge/computeCellIndices'; import { getColSpan } from '../../queries/getColSpan'; import { getRowSpan } from '../../queries/getRowSpan'; import { getTableColumnIndex, getTableRowIndex } from '../../queries/index'; diff --git a/packages/table/src/components/TableElement/useTableElement.ts b/packages/table/src/components/TableElement/useTableElement.ts index 0a84039921..80e239a325 100644 --- a/packages/table/src/components/TableElement/useTableElement.ts +++ b/packages/table/src/components/TableElement/useTableElement.ts @@ -7,7 +7,7 @@ import { } from '@udecode/plate-common'; import { ELEMENT_TABLE } from '../../createTablePlugin'; -import { computeAllCellIndices } from '../../queries/computeCellIndices'; +import { computeAllCellIndices } from '../../merge/computeCellIndices'; import { useTableStore } from '../../stores/tableStore'; import { TablePlugin, TTableElement } from '../../types'; import { useSelectedCells } from './useSelectedCells'; diff --git a/packages/table/src/createTablePlugin.ts b/packages/table/src/createTablePlugin.ts index 7ecb072bb6..0c9b45c77a 100644 --- a/packages/table/src/createTablePlugin.ts +++ b/packages/table/src/createTablePlugin.ts @@ -36,6 +36,7 @@ export const createTablePlugin = createPluginFactory({ }); }, minColumnWidth: 48, + disableCellsMerging: false, _cellIndices: new WeakMap() as TableStoreCellAttributes, }, withOverrides: withTable, diff --git a/packages/table/src/queries/computeCellIndices.ts b/packages/table/src/merge/computeCellIndices.ts similarity index 93% rename from packages/table/src/queries/computeCellIndices.ts rename to packages/table/src/merge/computeCellIndices.ts index 009912c2d5..4a7e396b2c 100644 --- a/packages/table/src/queries/computeCellIndices.ts +++ b/packages/table/src/merge/computeCellIndices.ts @@ -62,16 +62,11 @@ export function computeCellIndices( }); if (rowIndex === -1 || colIndex === -1) { - // console.log('Invalid cell location.'); + console.log('Invalid cell location.'); return null; } const indices = { row: rowIndex, col: colIndex }; - const cellContent = cellEl.children?.map((i) => { - return (i.children as any)[0].text; - }); - // console.log('new cell location', cellContent, indices); - options?._cellIndices?.set(cellEl, indices); return indices; diff --git a/packages/table/src/merge/deleteColumn.ts b/packages/table/src/merge/deleteColumn.ts new file mode 100644 index 0000000000..699db5094b --- /dev/null +++ b/packages/table/src/merge/deleteColumn.ts @@ -0,0 +1,230 @@ +import { + getAboveNode, + getPluginOptions, + getPluginType, + insertElements, + PlateEditor, + removeNodes, + setNodes, + someNode, + Value, + withoutNormalizing, +} from '@udecode/plate-common'; +import { Path } from 'slate'; + +import { ELEMENT_TABLE, ELEMENT_TR } from '../createTablePlugin'; +import { getTableColumnCount } from '../queries'; +import { getColSpan } from '../queries/getColSpan'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getCellTypes } from '../utils'; +import { findCellByIndexes } from './findCellByIndexes'; +import { getCellPath } from './getCellPath'; +import { getIndices } from './getIndices'; + +export const deleteColumn = (editor: PlateEditor) => { + if ( + someNode(editor, { + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + }) + ) { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const tableEntry = getAboveNode(editor, { + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + }); + if (!tableEntry) return; + const table = tableEntry[0] as TTableElement; + + const selectedCellEntry = getAboveNode(editor, { + match: { + type: getCellTypes(editor), + }, + }); + if (!selectedCellEntry) return; + const selectedCell = selectedCellEntry[0] as TTableCellElement; + + const { col: deletingColIndex } = getIndices(options, selectedCell)!; + const colsDeleteNumber = getColSpan(selectedCell); + + const endingColIndex = deletingColIndex + colsDeleteNumber - 1; + + const rowNumber = table.children.length; + const affectedCellsSet = new Set(); + // iterating by rows is important here to keep the order of affected cells + Array.from({ length: rowNumber }, (_, i) => i).forEach((rI) => { + return Array.from({ length: colsDeleteNumber }, (_, i) => i).forEach( + (cI) => { + const colIndex = deletingColIndex + cI; + const found = findCellByIndexes(editor, table, rI, colIndex); + if (found) { + affectedCellsSet.add(found); + } + } + ); + }); + const affectedCells = Array.from(affectedCellsSet) as TTableCellElement[]; + + const { moveToNextColCells, squizeColSpanCells } = affectedCells.reduce<{ + squizeColSpanCells: TTableCellElement[]; + moveToNextColCells: TTableCellElement[]; + }>( + (acc, cur) => { + if (!cur) return acc; + + const currentCell = cur as TTableCellElement; + const { col: curColIndex } = getIndices(options, currentCell)!; + const curColSpan = getColSpan(currentCell); + + if (curColIndex < deletingColIndex && curColSpan > 1) { + acc.squizeColSpanCells.push(currentCell); + } else if ( + curColSpan > 1 && + curColIndex + curColSpan - 1 > endingColIndex + ) { + acc.moveToNextColCells.push(currentCell); + } + return acc; + }, + { moveToNextColCells: [], squizeColSpanCells: [] } + ); + + const nextColIndex = deletingColIndex + colsDeleteNumber; + const colNumber = getTableColumnCount(table); + if (colNumber > nextColIndex) { + moveToNextColCells.forEach((cur) => { + const curCell = cur as TTableCellElement; + const { col: curColIndex, row: curRowIndex } = getIndices( + options, + curCell + )!; + + const curColSpan = getColSpan(curCell); + + // simplify logic here. use getParent + const curRow = table.children[curRowIndex] as TTableRowElement; + const startingCellIndex = curRow.children.findIndex((curC) => { + const cell = curC as TTableCellElement; + const { col: cellColIndex } = getIndices(options, cell)!; + return cellColIndex >= curColIndex + 1; + }); + + const startingCell = curRow.children.at( + startingCellIndex + ) as TTableCellElement; + const { col: startingColIndex, row: startingRowIndex } = getIndices( + options, + startingCell + )!; + + const startingCellPath = getCellPath( + editor, + tableEntry, + startingRowIndex, + startingColIndex + ); + const colsNumberAffected = endingColIndex - curColIndex + 1; + + const newCell = { + ...curCell, + colSpan: curColSpan - colsNumberAffected, + }; + insertElements(editor, newCell, { at: startingCellPath }); + }); + } + + squizeColSpanCells.forEach((cur) => { + const curCell = cur as TTableCellElement; + + const { col: curColIndex, row: curColRowIndex } = getIndices( + options, + curCell + )!; + const curColSpan = getColSpan(curCell); + + const curCellPath = getCellPath( + editor, + tableEntry, + curColRowIndex, + curColIndex + ); + + const curCellEndingColIndex = Math.min( + curColIndex + curColSpan - 1, + endingColIndex + ); + const colsNumberAffected = curCellEndingColIndex - deletingColIndex + 1; + + setNodes( + editor, + { ...curCell, colSpan: curColSpan - colsNumberAffected }, + { at: curCellPath } + ); + }); + + const trEntry = getAboveNode(editor, { + match: { type: getPluginType(editor, ELEMENT_TR) }, + }); + + if ( + selectedCell && + trEntry && + tableEntry && + // Cannot delete the last cell + trEntry[0].children.length > 1 + ) { + const [tableNode, tablePath] = tableEntry; + + // calc paths to delete + const paths: Array = []; + affectedCells.forEach((cur) => { + const curCell = cur as TTableCellElement; + const { col: curColIndex, row: curRowIndex } = getIndices( + options, + curCell + )!; + + if (curColIndex >= deletingColIndex && curColIndex <= endingColIndex) { + const cellPath = getCellPath( + editor, + tableEntry, + curRowIndex, + curColIndex + ); + + if (!paths[curRowIndex]) { + paths[curRowIndex] = []; + } + paths[curRowIndex].push(cellPath); + } + }); + + withoutNormalizing(editor, () => { + paths.forEach((cellPaths) => { + const pathToDelete = cellPaths[0]; + cellPaths.forEach(() => { + removeNodes(editor, { + at: pathToDelete, + }); + }); + }); + + const { colSizes } = tableNode; + if (colSizes) { + const newColSizes = [...colSizes]; + newColSizes.splice(deletingColIndex, 1); + + setNodes( + editor, + { colSizes: newColSizes }, + { at: tablePath } + ); + } + }); + } + } +}; diff --git a/packages/table/src/merge/deleteRow.ts b/packages/table/src/merge/deleteRow.ts new file mode 100644 index 0000000000..9896298195 --- /dev/null +++ b/packages/table/src/merge/deleteRow.ts @@ -0,0 +1,172 @@ +import { + findNodePath, + getAboveNode, + getPluginOptions, + getPluginType, + insertElements, + PlateEditor, + removeNodes, + setNodes, + someNode, + Value, +} from '@udecode/plate-common'; + +import { ELEMENT_TABLE } from '../createTablePlugin'; +import { getTableColumnCount } from '../queries'; +import { getRowSpan } from '../queries/getRowSpan'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getCellTypes } from '../utils'; +import { findCellByIndexes } from './findCellByIndexes'; +import { getIndices } from './getIndices'; + +export const deleteRow = (editor: PlateEditor) => { + if ( + someNode(editor, { + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + }) + ) { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const currentTableItem = getAboveNode(editor, { + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + }); + if (!currentTableItem) return; + const table = currentTableItem[0] as TTableElement; + + const selectedCellEntry = getAboveNode(editor, { + match: { type: getCellTypes(editor) }, + }); + if (!selectedCellEntry) return; + + const selectedCell = selectedCellEntry[0] as TTableCellElement; + const { row: deletingRowIndex } = getIndices(options, selectedCell)!; + const rowsDeleteNumber = getRowSpan(selectedCell); + const endingRowIndex = deletingRowIndex + rowsDeleteNumber - 1; + + const colNumber = getTableColumnCount(table); + const affectedCellsSet = new Set(); + // iterating by columns is important here to keep the order of affected cells + Array.from({ length: colNumber }, (_, i) => i).forEach((cI) => { + return Array.from({ length: rowsDeleteNumber }, (_, i) => i).forEach( + (rI) => { + const rowIndex = deletingRowIndex + rI; + const found = findCellByIndexes(editor, table, rowIndex, cI); + affectedCellsSet.add(found); + } + ); + }); + const affectedCells = Array.from(affectedCellsSet) as TTableCellElement[]; + + const { moveToNextRowCells, squizeRowSpanCells } = affectedCells.reduce<{ + squizeRowSpanCells: TTableCellElement[]; + moveToNextRowCells: TTableCellElement[]; + }>( + (acc, cur) => { + if (!cur) return acc; + + const currentCell = cur as TTableCellElement; + const { row: curRowIndex } = getIndices(options, currentCell)!; + const curRowSpan = getRowSpan(currentCell); + + if (!curRowIndex || !curRowSpan) return acc; + + if (curRowIndex < deletingRowIndex && curRowSpan > 1) { + acc.squizeRowSpanCells.push(currentCell); + } else if ( + curRowSpan > 1 && + curRowIndex + curRowSpan - 1 > endingRowIndex + ) { + acc.moveToNextRowCells.push(currentCell); + } + + return acc; + }, + { squizeRowSpanCells: [], moveToNextRowCells: [] } + ); + + const nextRowIndex = deletingRowIndex + rowsDeleteNumber; + const nextRow = table.children[nextRowIndex] as + | TTableCellElement + | undefined; + + if (nextRow) { + moveToNextRowCells.forEach((cur, index) => { + const curRowCell = cur as TTableCellElement; + const { col: curRowCellColIndex } = getIndices(options, curRowCell)!; + const curRowCellRowSpan = getRowSpan(curRowCell); + + // search for anchor cell where to place current cell + const startingCellIndex = nextRow.children.findIndex((curC) => { + const cell = curC as TTableCellElement; + const { col: curColIndex } = getIndices(options, cell)!; + return curColIndex >= curRowCellColIndex; + }); + + const startingCell = nextRow.children[ + startingCellIndex + ] as TTableCellElement; + const { col: startingColIndex } = getIndices(options, startingCell)!; + + // consider already inserted cell by adding index each time to the col path + let incrementBy = index; + if (startingColIndex < curRowCellColIndex) { + // place current cell after starting cell, if placing cell col index is grather than col index of starting cell + incrementBy += 1; + } + + const startingCellPath = findNodePath(editor, startingCell)!; + const tablePath = startingCellPath.slice(0, -2); + const colPath = startingCellPath.at(-1)!; + + const nextRowStartCellPath = [ + ...tablePath, + nextRowIndex, + colPath + incrementBy, + ]; + + const rowsNumberAffected = endingRowIndex - curRowCellColIndex + 1; + + // TODO: consider make deep clone here + // making cell smaller and moving it to next row + const newCell = { + ...curRowCell, + rowSpan: curRowCellRowSpan - rowsNumberAffected, + }; + insertElements(editor, newCell, { at: nextRowStartCellPath }); + }); + } + + squizeRowSpanCells.forEach((cur) => { + const curRowCell = cur as TTableCellElement; + const { row: curRowCellRowIndex } = getIndices(options, curRowCell)!; + const curRowCellRowSpan = getRowSpan(curRowCell); + + const curCellPath = findNodePath(editor, curRowCell)!; + + const curCellEndingRowIndex = Math.min( + curRowCellRowIndex + curRowCellRowSpan - 1, + endingRowIndex + ); + const rowsNumberAffected = curCellEndingRowIndex - deletingRowIndex + 1; + + setNodes( + editor, + { ...curRowCell, rowSpan: curRowCellRowSpan - rowsNumberAffected }, + { at: curCellPath } + ); + }); + + const rowToDelete = table.children[deletingRowIndex] as TTableRowElement; + const rowPath = findNodePath(editor, rowToDelete); + Array.from({ length: rowsDeleteNumber }).forEach(() => { + removeNodes(editor, { + at: rowPath, + }); + }); + } +}; diff --git a/packages/table/src/queries/findCellByIndexes.ts b/packages/table/src/merge/findCellByIndexes.ts similarity index 92% rename from packages/table/src/queries/findCellByIndexes.ts rename to packages/table/src/merge/findCellByIndexes.ts index 09ba7f1aef..9607f688ca 100644 --- a/packages/table/src/queries/findCellByIndexes.ts +++ b/packages/table/src/merge/findCellByIndexes.ts @@ -1,10 +1,10 @@ import { getPluginOptions, PlateEditor, Value } from '@udecode/plate-common'; import { ELEMENT_TABLE } from '../createTablePlugin'; +import { getIndices } from '../merge/getIndices'; +import { getIndicesWithSpans } from '../merge/getIndicesWithSpans'; import { TablePlugin, TTableCellElement, TTableElement } from '../types'; import { computeCellIndices } from './computeCellIndices'; -import { getIndices } from './getIndices'; -import { getIndicesWithSpans } from './getIndicesWithSpans'; export const findCellByIndexes = ( editor: PlateEditor, diff --git a/packages/table/src/merge/getCellPath.ts b/packages/table/src/merge/getCellPath.ts new file mode 100644 index 0000000000..1195e9251d --- /dev/null +++ b/packages/table/src/merge/getCellPath.ts @@ -0,0 +1,33 @@ +import { + getPluginOptions, + PlateEditor, + TNodeEntry, + Value, +} from '@udecode/plate-common'; + +import { ELEMENT_TABLE } from '../createTablePlugin'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getIndices } from './getIndices'; + +export const getCellPath = ( + editor: PlateEditor, + tableEntry: TNodeEntry, + curRowIndex: number, + curColIndex: number +) => { + const options = getPluginOptions(editor, ELEMENT_TABLE); + const [tableNode, tablePath] = tableEntry; + + const rowElem = tableNode.children[curRowIndex] as TTableRowElement; + const foundColIndex = rowElem.children.findIndex((c) => { + const cE = c as TTableCellElement; + const { col: colIndex } = getIndices(options, cE)!; + return colIndex === curColIndex; + }); + return tablePath.concat([curRowIndex, foundColIndex]); +}; diff --git a/packages/table/src/queries/getIndices.ts b/packages/table/src/merge/getIndices.ts similarity index 100% rename from packages/table/src/queries/getIndices.ts rename to packages/table/src/merge/getIndices.ts diff --git a/packages/table/src/queries/getIndicesWithSpans.ts b/packages/table/src/merge/getIndicesWithSpans.ts similarity index 53% rename from packages/table/src/queries/getIndicesWithSpans.ts rename to packages/table/src/merge/getIndicesWithSpans.ts index ff69016c85..e4e908a634 100644 --- a/packages/table/src/queries/getIndicesWithSpans.ts +++ b/packages/table/src/merge/getIndicesWithSpans.ts @@ -1,8 +1,6 @@ -import { Value } from '@udecode/plate-common'; - -import { TablePlugin, TTableCellElement } from '../types'; -import { getColSpan } from './getColSpan'; -import { getRowSpan } from './getRowSpan'; +import { getColSpan } from '../queries/getColSpan'; +import { getRowSpan } from '../queries/getRowSpan'; +import { TTableCellElement } from '../types'; export const getIndicesWithSpans = ( { col, row }: { col: number; row: number }, diff --git a/packages/table/src/merge/getTableGridByRange.ts b/packages/table/src/merge/getTableGridByRange.ts new file mode 100644 index 0000000000..fe18fcf03b --- /dev/null +++ b/packages/table/src/merge/getTableGridByRange.ts @@ -0,0 +1,154 @@ +import { + findNode, + findNodePath, + getPluginOptions, + getPluginType, + PlateEditor, + TElement, + TElementEntry, + Value, +} from '@udecode/plate-common'; +import { Range } from 'slate'; + +import { ELEMENT_TABLE } from '../createTablePlugin'; +import { computeCellIndices } from '../merge/computeCellIndices'; +import { findCellByIndexes } from '../merge/findCellByIndexes'; +import { getIndices } from '../merge/getIndices'; +import { getIndicesWithSpans } from '../merge/getIndicesWithSpans'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getCellTypes } from '../utils'; +import { getEmptyTableNode } from '../utils/getEmptyTableNode'; + +export type FormatType = 'table' | 'cell' | 'all'; + +export interface TableGridEntries { + tableEntries: TElementEntry[]; + cellEntries: TElementEntry[]; +} + +export type GetTableGridReturnType = T extends 'all' + ? TableGridEntries + : TElementEntry[]; + +export interface GetTableGridByRangeOptions { + at: Range; + + /** + * Format of the output: + * - table element + * - array of cells + */ + format?: 'table' | 'cell'; +} + +/** + * Get sub table between 2 cell paths. + */ +export const getTableGridByRange = ( + editor: PlateEditor, + { at, format = 'table' }: GetTableGridByRangeOptions +): TElementEntry[] => { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const startCellEntry = findNode(editor, { + at: (at as any).anchor.path, + match: { type: getCellTypes(editor) }, + })!; // TODO: improve typing + const endCellEntry = findNode(editor, { + at: (at as any).focus.path, + match: { type: getCellTypes(editor) }, + })!; + + const startCell = startCellEntry[0] as TTableCellElement; + const endCell = endCellEntry[0] as TTableCellElement; + + const startCellPath = (at as any).anchor.path; + const tablePath = startCellPath.slice(0, -2); + + const tableEntry = findNode(editor, { + at: tablePath, + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + })!; // TODO: improve typing + const realTable = tableEntry[0] as TTableElement; + + const { col: _startColIndex, row: _startRowIndex } = + getIndices(options, startCell) || + computeCellIndices(editor, realTable, startCell)!; + + const { row: _endRowIndex, col: _endColIndex } = getIndicesWithSpans( + getIndices(options, endCell) || + computeCellIndices(editor, realTable, endCell)!, + endCell + ); + + const startRowIndex = Math.min(_startRowIndex, _endRowIndex); + const endRowIndex = Math.max(_startRowIndex, _endRowIndex); + const startColIndex = Math.min(_startColIndex, _endColIndex); + const endColIndex = Math.max(_startColIndex, _endColIndex); + + const relativeRowIndex = endRowIndex - startRowIndex; + const relativeColIndex = endColIndex - startColIndex; + + const table: TTableElement = getEmptyTableNode(editor, { + rowCount: relativeRowIndex + 1, + colCount: relativeColIndex + 1, + newCellChildren: [], + }); + + const cellEntries: TElementEntry[] = []; + const cellsSet = new WeakSet(); + + let rowIndex = startRowIndex; + let colIndex = startColIndex; + while (true) { + const cell = findCellByIndexes(editor, realTable, rowIndex, colIndex); + if (!cell) { + break; + } + + if (!cellsSet.has(cell)) { + cellsSet.add(cell); + + const rows = table.children[rowIndex - startRowIndex] + .children as TElement[]; + rows[colIndex - startColIndex] = cell; + + const cellPath = findNodePath(editor, cell)!; + cellEntries.push([cell, cellPath]); + } + + if (colIndex + 1 <= endColIndex) { + colIndex = colIndex + 1; + } else if (rowIndex + 1 <= endRowIndex) { + colIndex = startColIndex; + rowIndex = rowIndex + 1; + } else { + break; + } + } + + const formatType = (format as string) || 'table'; + + if (formatType === 'cell') { + return cellEntries; + } + + // clear redundant cells + table.children?.forEach((rowEl) => { + const rowElement = rowEl as TTableRowElement; + + const filteredChildren = rowElement.children?.filter((cellEl) => { + const cellElement = cellEl as TTableCellElement; + return !!cellElement?.children.length; + }); + + rowElement.children = filteredChildren; + }); + + return [[table, tablePath]]; +}; diff --git a/packages/table/src/merge/insertTableColumn.ts b/packages/table/src/merge/insertTableColumn.ts new file mode 100644 index 0000000000..e244ad77dc --- /dev/null +++ b/packages/table/src/merge/insertTableColumn.ts @@ -0,0 +1,221 @@ +import { + findNode, + getBlockAbove, + getNodeEntry, + getParentNode, + getPluginOptions, + getPluginType, + insertElements, + PlateEditor, + setNodes, + TDescendant, + TElement, + Value, + withoutNormalizing, +} from '@udecode/plate-common'; +import { Path } from 'slate'; + +import { ELEMENT_TABLE, ELEMENT_TH } from '../createTablePlugin'; +import { getColSpan } from '../queries/getColSpan'; +import { getRowSpan } from '../queries/getRowSpan'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getCellTypes, getEmptyCellNode } from '../utils'; +import { findCellByIndexes } from './findCellByIndexes'; +import { getCellPath } from './getCellPath'; +import { getIndices } from './getIndices'; + +const createEmptyCell = ( + editor: PlateEditor, + row: TTableRowElement, + newCellChildren?: TDescendant[], + header?: boolean +) => { + const isHeaderRow = + header === undefined + ? (row as TElement).children.every( + (c) => c.type === getPluginType(editor, ELEMENT_TH) + ) + : header; + + return getEmptyCellNode(editor, { + header: isHeaderRow, + newCellChildren, + }); +}; + +export const insertTableColumn = ( + editor: PlateEditor, + { + disableSelect, + fromCell, + at, + header, + }: { + header?: boolean; + + /** + * Path of the cell to insert the column from. + */ + fromCell?: Path; + + /** + * Exact path of the cell to insert the column at. + * Will overrule `fromCell`. + */ + at?: Path; + + /** + * Disable selection after insertion. + */ + disableSelect?: boolean; + } = {} +) => { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const cellEntry = fromCell + ? findNode(editor, { + at: fromCell, + match: { type: getCellTypes(editor) }, + }) + : getBlockAbove(editor, { + match: { type: getCellTypes(editor) }, + }); + if (!cellEntry) return; + + const [, cellPath] = cellEntry; + const cell = cellEntry[0] as TTableCellElement; + + const tableEntry = getBlockAbove(editor, { + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + at: cellPath, + }); + if (!tableEntry) return; + + const { newCellChildren, initialTableWidth, minColumnWidth } = + getPluginOptions(editor, ELEMENT_TABLE); + const [tableNode, tablePath] = tableEntry; + + const { col: cellColIndex } = getIndices(options, cell)!; + const cellColSpan = getColSpan(cell); + + let nextColIndex: number; + let checkingColIndex: number; + if (Path.isPath(at)) { + nextColIndex = cellColIndex; + checkingColIndex = cellColIndex - 1; + } else { + nextColIndex = cellColIndex + cellColSpan; + checkingColIndex = cellColIndex + cellColSpan - 1; + } + + const currentRowIndex = cellPath.at(-2); // recheck it + const rowNumber = tableNode.children.length; + const firstCol = nextColIndex <= 0; + + // const colCount = getTableColumnCount(tableNode); + // const lastRow = nextColIndex === colCount; + + let placementCorrection = 1; + if (firstCol) { + checkingColIndex = 0; + placementCorrection = 0; + } + + const affectedCellsSet = new Set(); + Array.from({ length: rowNumber }, (_, i) => i).forEach((rI) => { + const found = findCellByIndexes(editor, tableNode, rI, checkingColIndex); + if (found) { + affectedCellsSet.add(found); + } + }); + const affectedCells = Array.from(affectedCellsSet) as TTableCellElement[]; + + affectedCells.forEach((cur) => { + const curCell = cur as TTableCellElement; + const { row: curRowIndex, col: curColIndex } = getIndices( + options, + curCell + )!; + const curRowSpan = getRowSpan(curCell); + const curColSpan = getColSpan(curCell); + + const currentCellPath = getCellPath( + editor, + tableEntry, + curRowIndex, + curColIndex + ); + + const endCurI = curColIndex + curColSpan - 1; + if (endCurI >= nextColIndex && !firstCol) { + // make wider + setNodes( + editor, + { ...curCell, colSpan: curColSpan + 1 }, + { at: currentCellPath } + ); + } else { + // add new + const curRowPath = currentCellPath.slice(0, -1); + const curColPath = currentCellPath.at(-1)!; + const placementPath = [...curRowPath, curColPath + placementCorrection]; + + const row = getParentNode(editor, currentCellPath)!; + const rowElement = row[0] as TTableRowElement; + const emptyCell = { + ...createEmptyCell(editor, rowElement, newCellChildren, header), + rowSpan: curRowSpan, + colSpan: 1, + }; + insertElements(editor, emptyCell, { + at: placementPath, + select: !disableSelect && curRowIndex === currentRowIndex, + }); + } + }); + + withoutNormalizing(editor, () => { + const { colSizes } = tableNode; + + if (colSizes) { + let newColSizes = [ + ...colSizes.slice(0, nextColIndex), + 0, + ...colSizes.slice(nextColIndex), + ]; + + if (initialTableWidth) { + newColSizes[nextColIndex] = + colSizes[nextColIndex] ?? + colSizes[nextColIndex - 1] ?? + initialTableWidth / colSizes.length; + + const oldTotal = colSizes.reduce((a, b) => a + b, 0); + const newTotal = newColSizes.reduce((a, b) => a + b, 0); + const maxTotal = Math.max(oldTotal, initialTableWidth); + + if (newTotal > maxTotal) { + const factor = maxTotal / newTotal; + newColSizes = newColSizes.map((size) => + Math.max(minColumnWidth ?? 0, Math.floor(size * factor)) + ); + } + } + + setNodes( + editor, + { + colSizes: newColSizes, + }, + { + at: tablePath, + } + ); + } + }); +}; diff --git a/packages/table/src/merge/insertTableRow.ts b/packages/table/src/merge/insertTableRow.ts new file mode 100644 index 0000000000..e56f8d084e --- /dev/null +++ b/packages/table/src/merge/insertTableRow.ts @@ -0,0 +1,205 @@ +import { + findNode, + getBlockAbove, + getParentNode, + getPluginOptions, + getPluginType, + insertElements, + PlateEditor, + select, + setNodes, + TDescendant, + TElement, + Value, + withoutNormalizing, +} from '@udecode/plate-common'; +import { Path } from 'slate'; + +import { ELEMENT_TABLE, ELEMENT_TH, ELEMENT_TR } from '../createTablePlugin'; +import { getTableColumnCount } from '../queries'; +import { getColSpan } from '../queries/getColSpan'; +import { getRowSpan } from '../queries/getRowSpan'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getCellTypes, getEmptyCellNode } from '../utils'; +import { findCellByIndexes } from './findCellByIndexes'; +import { getCellPath } from './getCellPath'; +import { getIndices } from './getIndices'; + +const createEmptyCell = ( + editor: PlateEditor, + row: TTableRowElement, + newCellChildren?: TDescendant[], + header?: boolean +) => { + const isHeaderRow = + header === undefined + ? (row as TElement).children.every( + (c) => c.type === getPluginType(editor, ELEMENT_TH) + ) + : header; + + return getEmptyCellNode(editor, { + header: isHeaderRow, + newCellChildren, + }); +}; + +export const insertTableRow = ( + editor: PlateEditor, + { + header, + fromRow, + at, + disableSelect, + }: { + header?: boolean; + fromRow?: Path; + /** + * Exact path of the row to insert the column at. + * Will overrule `fromRow`. + */ + at?: Path; + disableSelect?: boolean; + } = {} +) => { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const trEntry = fromRow + ? findNode(editor, { + at: fromRow, + match: { type: getPluginType(editor, ELEMENT_TR) }, + }) + : getBlockAbove(editor, { + match: { type: getPluginType(editor, ELEMENT_TR) }, + }); + if (!trEntry) return; + + const [, trPath] = trEntry; + + const tableEntry = getBlockAbove(editor, { + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + at: trPath, + }); + if (!tableEntry) return; + const tableNode = tableEntry[0] as TTableElement; + + const { newCellChildren } = getPluginOptions( + editor, + ELEMENT_TABLE + ); + const cellEntry = findNode(editor, { + at: fromRow, + match: { type: getCellTypes(editor) }, + }); + if (!cellEntry) return; + const [cellNode, cellPath] = cellEntry; + const cellElement = cellNode as TTableCellElement; + const cellRowSpan = getRowSpan(cellElement); + const { row: cellRowIndex } = getIndices(options, cellElement)!; + + const rowPath = cellPath.at(-2)!; + const tablePath = cellPath.slice(0, -2)!; + + let nextRowIndex: number; + let checkingRowIndex: number; + let nextRowPath: number[]; + if (Path.isPath(at)) { + nextRowIndex = at.at(-1)!; + checkingRowIndex = cellRowIndex - 1; + nextRowPath = at; + } else { + nextRowIndex = cellRowIndex + cellRowSpan; + checkingRowIndex = cellRowIndex + cellRowSpan - 1; + nextRowPath = [...tablePath, rowPath + cellRowSpan]; + } + + const firstRow = nextRowIndex === 0; + if (firstRow) { + checkingRowIndex = 0; + } + + const colCount = getTableColumnCount(tableNode); + const affectedCellsSet = new Set(); + Array.from({ length: colCount }, (_, i) => i).forEach((cI) => { + const found = findCellByIndexes(editor, tableNode, checkingRowIndex, cI); + if (found) { + affectedCellsSet.add(found); + } + }); + const affectedCells = Array.from(affectedCellsSet) as TTableCellElement[]; + + const newRowChildren: TTableCellElement[] = []; + affectedCells.forEach((cur) => { + if (!cur) return; + + const curCell = cur as TTableCellElement; + const { row: curRowIndex, col: curColIndex } = getIndices( + options, + curCell + )!; + + const curRowSpan = getRowSpan(curCell); + const curColSpan = getColSpan(curCell); + const currentCellPath = getCellPath( + editor, + tableEntry, + curRowIndex, + curColIndex + ); + + const endCurI = curRowIndex + curRowSpan - 1; + if (endCurI >= nextRowIndex && !firstRow) { + // make higher + setNodes( + editor, + { ...curCell, rowSpan: curRowSpan + 1 }, + { at: currentCellPath } + ); + } else { + // add new + const row = getParentNode(editor, currentCellPath)!; + const rowElement = row[0] as TTableRowElement; + const emptyCell = createEmptyCell( + editor, + rowElement, + newCellChildren, + header + ) as TTableCellElement; + + newRowChildren.push({ + ...emptyCell, + colSpan: curColSpan, + rowSpan: 1, + }); + } + }); + + withoutNormalizing(editor, () => { + insertElements( + editor, + { + type: getPluginType(editor, ELEMENT_TR), + children: newRowChildren, + }, + { + at: nextRowPath, + } + ); + }); + + if (!disableSelect) { + const nextCellPath = cellPath; + if (Path.isPath(at)) { + nextCellPath[nextCellPath.length - 2] = at.at(-2)!; + } else { + nextCellPath[nextCellPath.length - 2] += cellRowSpan; + } + + select(editor, nextCellPath); + } +}; diff --git a/packages/table/src/merge/mergeTableCells.ts b/packages/table/src/merge/mergeTableCells.ts index 647fb3bb42..bf397a2f57 100644 --- a/packages/table/src/merge/mergeTableCells.ts +++ b/packages/table/src/merge/mergeTableCells.ts @@ -12,11 +12,11 @@ import { cloneDeep } from 'lodash'; import { ELEMENT_TABLE } from '../createTablePlugin'; import { getTableGridAbove } from '../queries'; -import { computeCellIndices } from '../queries/computeCellIndices'; import { getColSpan } from '../queries/getColSpan'; import { getRowSpan } from '../queries/getRowSpan'; import { TablePlugin, TTableCellElement, TTableElement } from '../types'; import { getEmptyCellNode } from '../utils'; +import { computeCellIndices } from './computeCellIndices'; /** * Merges multiple selected cells into one. diff --git a/packages/table/src/queries/getTableGridByRange.ts b/packages/table/src/queries/getTableGridByRange.ts index 4171cee1c4..083631bc8d 100644 --- a/packages/table/src/queries/getTableGridByRange.ts +++ b/packages/table/src/queries/getTableGridByRange.ts @@ -1,8 +1,6 @@ import { - findNode, - findNodePath, + getNode, getPluginOptions, - getPluginType, PlateEditor, TElement, TElementEntry, @@ -11,29 +9,9 @@ import { import { Range } from 'slate'; import { ELEMENT_TABLE } from '../createTablePlugin'; -import { - TablePlugin, - TTableCellElement, - TTableElement, - TTableRowElement, -} from '../types'; -import { getCellTypes } from '../utils'; +import { getTableGridByRange as getTableGridByRangeMerge } from '../merge/getTableGridByRange'; +import { TablePlugin, TTableElement } from '../types'; import { getEmptyTableNode } from '../utils/getEmptyTableNode'; -import { computeCellIndices } from './computeCellIndices'; -import { findCellByIndexes } from './findCellByIndexes'; -import { getIndices } from './getIndices'; -import { getIndicesWithSpans } from './getIndicesWithSpans'; - -export type FormatType = 'table' | 'cell' | 'all'; - -export interface TableGridEntries { - tableEntries: TElementEntry[]; - cellEntries: TElementEntry[]; -} - -export type GetTableGridReturnType = T extends 'all' - ? TableGridEntries - : TElementEntry[]; export interface GetTableGridByRangeOptions { at: Range; @@ -53,44 +31,29 @@ export const getTableGridByRange = ( editor: PlateEditor, { at, format = 'table' }: GetTableGridByRangeOptions ): TElementEntry[] => { - const options = getPluginOptions(editor, ELEMENT_TABLE); - - const startCellEntry = findNode(editor, { - at: (at as any).anchor.path, - match: { type: getCellTypes(editor) }, - })!; // TODO: improve typing - const endCellEntry = findNode(editor, { - at: (at as any).focus.path, - match: { type: getCellTypes(editor) }, - })!; - - const startCell = startCellEntry[0] as TTableCellElement; - const endCell = endCellEntry[0] as TTableCellElement; - - const startCellPath = (at as any).anchor.path; - const tablePath = startCellPath.slice(0, -2); + const { disableCellsMerging } = getPluginOptions( + editor, + ELEMENT_TABLE + ); + if (!disableCellsMerging) { + return getTableGridByRangeMerge(editor, { at, format }); + } - const tableEntry = findNode(editor, { - at: tablePath, - match: { type: getPluginType(editor, ELEMENT_TABLE) }, - })!; // TODO: improve typing - const realTable = tableEntry[0] as TTableElement; + const startCellPath = at.anchor.path; + const endCellPath = at.focus.path; - const { col: _startColIndex, row: _startRowIndex } = - getIndices(options, startCell) || - computeCellIndices(editor, realTable, startCell)!; - - const { row: _endRowIndex, col: _endColIndex } = getIndicesWithSpans( - getIndices(options, endCell) || - computeCellIndices(editor, realTable, endCell)!, - endCell - ); + const _startRowIndex = startCellPath.at(-2)!; + const _endRowIndex = endCellPath.at(-2)!; + const _startColIndex = startCellPath.at(-1)!; + const _endColIndex = endCellPath.at(-1)!; const startRowIndex = Math.min(_startRowIndex, _endRowIndex); const endRowIndex = Math.max(_startRowIndex, _endRowIndex); const startColIndex = Math.min(_startColIndex, _endColIndex); const endColIndex = Math.max(_startColIndex, _endColIndex); + const tablePath = startCellPath.slice(0, -2); + const relativeRowIndex = endRowIndex - startRowIndex; const relativeColIndex = endColIndex - startColIndex; @@ -100,55 +63,37 @@ export const getTableGridByRange = ( newCellChildren: [], }); - const cellEntries: TElementEntry[] = []; - const cellsSet = new WeakSet(); - let rowIndex = startRowIndex; let colIndex = startColIndex; + + const cellEntries: TElementEntry[] = []; + while (true) { - const cell = findCellByIndexes(editor, realTable, rowIndex, colIndex); - if (!cell) { - break; - } + const cellPath = tablePath.concat([rowIndex, colIndex]); - if (!cellsSet.has(cell)) { - cellsSet.add(cell); + const cell = getNode(editor, cellPath); + if (!cell) break; - const rows = table.children[rowIndex - startRowIndex] - .children as TElement[]; - rows[colIndex - startColIndex] = cell; + const rows = table.children[rowIndex - startRowIndex] + .children as TElement[]; - const cellPath = findNodePath(editor, cell)!; - cellEntries.push([cell, cellPath]); - } + rows[colIndex - startColIndex] = cell; + + cellEntries.push([cell, cellPath]); if (colIndex + 1 <= endColIndex) { - colIndex = colIndex + 1; + colIndex += 1; } else if (rowIndex + 1 <= endRowIndex) { colIndex = startColIndex; - rowIndex = rowIndex + 1; + rowIndex += 1; } else { break; } } - const formatType = (format as string) || 'table'; - - if (formatType === 'cell') { + if (format === 'cell') { return cellEntries; } - // clear redundant cells - table.children?.forEach((rowEl) => { - const rowElement = rowEl as TTableRowElement; - - const filteredChildren = rowElement.children?.filter((cellEl) => { - const cellElement = cellEl as TTableCellElement; - return !!cellElement?.children.length; - }); - - rowElement.children = filteredChildren; - }); - return [[table, tablePath]]; }; diff --git a/packages/table/src/transforms/deleteColumn.ts b/packages/table/src/transforms/deleteColumn.ts index fcd905f7c6..3f9aab2a90 100644 --- a/packages/table/src/transforms/deleteColumn.ts +++ b/packages/table/src/transforms/deleteColumn.ts @@ -1,5 +1,6 @@ import { getAboveNode, + getPluginOptions, getPluginType, PlateEditor, removeNodes, @@ -16,9 +17,18 @@ import { ELEMENT_TH, ELEMENT_TR, } from '../createTablePlugin'; -import { TTableElement } from '../types'; +import { deleteColumn as deleteColumnMerging } from '../merge/deleteColumn'; +import { TablePlugin, TTableElement } from '../types'; export const deleteColumn = (editor: PlateEditor) => { + const { disableCellsMerging } = getPluginOptions( + editor, + ELEMENT_TABLE + ); + if (!disableCellsMerging) { + return deleteColumnMerging(editor); + } + if ( someNode(editor, { match: { type: getPluginType(editor, ELEMENT_TABLE) }, diff --git a/packages/table/src/transforms/deleteRow.ts b/packages/table/src/transforms/deleteRow.ts index 703afed4ea..719d38dc3f 100644 --- a/packages/table/src/transforms/deleteRow.ts +++ b/packages/table/src/transforms/deleteRow.ts @@ -1,5 +1,6 @@ import { getAboveNode, + getPluginOptions, getPluginType, PlateEditor, removeNodes, @@ -8,9 +9,18 @@ import { } from '@udecode/plate-common'; import { ELEMENT_TABLE, ELEMENT_TR } from '../createTablePlugin'; -import { TTableElement } from '../types'; +import { deleteRow as deleteRowMerging } from '../merge/deleteRow'; +import { TablePlugin, TTableElement } from '../types'; export const deleteRow = (editor: PlateEditor) => { + const { disableCellsMerging } = getPluginOptions( + editor, + ELEMENT_TABLE + ); + if (!disableCellsMerging) { + return deleteRowMerging(editor); + } + if ( someNode(editor, { match: { type: getPluginType(editor, ELEMENT_TABLE) }, diff --git a/packages/table/src/transforms/insertTableColumn.ts b/packages/table/src/transforms/insertTableColumn.ts index fea3f48338..e4fac05a6a 100644 --- a/packages/table/src/transforms/insertTableColumn.ts +++ b/packages/table/src/transforms/insertTableColumn.ts @@ -13,18 +13,14 @@ import { import { Path } from 'slate'; import { ELEMENT_TABLE, ELEMENT_TH } from '../createTablePlugin'; +import { insertTableColumn as insertTableColumnMerging } from '../merge/insertTableColumn'; import { TablePlugin, TTableElement } from '../types'; import { getEmptyCellNode } from '../utils/getEmptyCellNode'; import { getCellTypes } from '../utils/index'; export const insertTableColumn = ( editor: PlateEditor, - { - disableSelect, - fromCell, - at, - header, - }: { + options: { header?: boolean; /** @@ -44,6 +40,16 @@ export const insertTableColumn = ( disableSelect?: boolean; } = {} ) => { + const { disableCellsMerging } = getPluginOptions( + editor, + ELEMENT_TABLE + ); + if (!disableCellsMerging) { + return insertTableColumnMerging(editor, options); + } + + const { disableSelect, fromCell, at, header } = options; + const cellEntry = fromCell ? findNode(editor, { at: fromCell, diff --git a/packages/table/src/transforms/insertTableRow.ts b/packages/table/src/transforms/insertTableRow.ts index 5204bd182b..dd33ac6ef2 100644 --- a/packages/table/src/transforms/insertTableRow.ts +++ b/packages/table/src/transforms/insertTableRow.ts @@ -13,17 +13,13 @@ import { import { Path } from 'slate'; import { ELEMENT_TABLE, ELEMENT_TH, ELEMENT_TR } from '../createTablePlugin'; +import { insertTableRow as insertTableRowMerging } from '../merge/insertTableRow'; import { TablePlugin } from '../types'; import { getCellTypes, getEmptyCellNode } from '../utils/index'; export const insertTableRow = ( editor: PlateEditor, - { - header, - fromRow, - at, - disableSelect, - }: { + options: { header?: boolean; fromRow?: Path; /** @@ -34,6 +30,16 @@ export const insertTableRow = ( disableSelect?: boolean; } = {} ) => { + const { disableCellsMerging } = getPluginOptions( + editor, + ELEMENT_TABLE + ); + if (!disableCellsMerging) { + return insertTableRowMerging(editor, options); + } + + const { header, fromRow, at, disableSelect } = options; + const trEntry = fromRow ? findNode(editor, { at: fromRow, diff --git a/packages/table/src/types.ts b/packages/table/src/types.ts index 02a3893dce..cd8eee7c4d 100644 --- a/packages/table/src/types.ts +++ b/packages/table/src/types.ts @@ -60,7 +60,12 @@ export interface TablePlugin { minColumnWidth?: number; /** - * For internal use. Keeps track of cell indices + * Enables / disabled cells merging functionality. + */ + disableCellsMerging?: boolean; + + /** + * For internal use. Keeps track of cell indices. Used only when disableCellsMerging is false. */ _cellIndices: TableStoreCellAttributes; } From 8ee4a9e018656c4fc8c82d56b2feb3cf3d15dc9c Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 15 Nov 2023 20:17:33 +0300 Subject: [PATCH 08/16] sync indices --- .../useTableCellElementState.ts | 73 +++++++++++-------- .../TableElement/useTableElement.ts | 12 +-- .../table/src/merge/computeCellIndices.ts | 25 +------ packages/table/src/merge/deleteRow.ts | 1 + packages/table/src/merge/findCellByIndexes.ts | 3 +- packages/table/src/merge/mergeTableCells.ts | 19 ----- packages/table/src/merge/unmergeTableCells.ts | 9 +-- packages/table/src/withMergedCells.ts | 54 ++++++++++++++ packages/table/src/withTable.ts | 4 + 9 files changed, 109 insertions(+), 91 deletions(-) create mode 100644 packages/table/src/withMergedCells.ts diff --git a/packages/table/src/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/components/TableCellElement/useTableCellElementState.ts index 3dff3c1433..81648be4b5 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { getPluginOptions, useEditorRef, @@ -59,45 +59,60 @@ export const useTableCellElementState = ({ const tableElement = useElement(ELEMENT_TABLE); const rowElement = useElement(ELEMENT_TR); + const rowSizeOverrides = useTableStore().get.rowSizeOverrides(); - const { _cellIndices } = useMemo( - () => getPluginOptions(editor as any, ELEMENT_TABLE), - [editor] + const { disableCellsMerging, _cellIndices } = getPluginOptions( + editor as any, + ELEMENT_TABLE ); + if (disableCellsMerging) { + const colIndex = getTableColumnIndex(editor, cellElement); + const rowIndex = getTableRowIndex(editor, cellElement); + + const rowSize = + rowSizeOverrides.get(rowIndex) ?? rowElement?.size ?? undefined; + + const isFirstCell = colIndex === 0; + const isFirstRow = tableElement.children?.[0] === rowElement; + + const borders = getTableCellBorders(cellElement, { + isFirstCell, + isFirstRow, + }); + + return { + colIndex, + rowIndex, + readOnly: !ignoreReadOnly && readOnly, + selected: isCellSelected, + hovered: hoveredColIndex === colIndex, + hoveredLeft: isFirstCell && hoveredColIndex === -1, + rowSize, + borders, + isSelectingCell: !!selectedCells, + colSpan, + }; + } - let x: { col: number; row: number }; - const fromWeakMap = _cellIndices.get(cellElement); - // const cellContent = cellElement.children.map((i) => { - // return (i.children as any)[0].text; - // }); + let result: { col: number; row: number }; - // const spans = { - // colSpan: getColSpan(cellElement), - // rowSpan: getRowSpan(cellElement), - // }; + const calculated = + _cellIndices.get(cellElement) || + computeCellIndices(editor, tableElement, cellElement); - if (fromWeakMap) { - x = fromWeakMap; - // console.log('cellContent', cellContent, x); + if (calculated) { + result = calculated; } else { - const x1 = computeCellIndices(editor, tableElement, cellElement); - if (x1) { - x = x1; - // console.log('computed', x, 'cellContent', cellContent, 'spans', spans); - } else { - const defaultColIndex = getTableColumnIndex(editor, cellElement); - const defaultRowIndex = getTableRowIndex(editor, cellElement); - x = { col: defaultColIndex, row: defaultRowIndex }; - // console.log('get default', x, 'cellContent', cellContent); - } + const defaultColIndex = getTableColumnIndex(editor, cellElement); + const defaultRowIndex = getTableRowIndex(editor, cellElement); + result = { col: defaultColIndex, row: defaultRowIndex }; } - const colIndex = x.col; - const rowIndex = x.row; + const colIndex = result.col; + const rowIndex = result.row; const endingRowIndex = rowIndex + rowSpan - 1; const endingColIndex = colIndex + colSpan - 1; - const rowSizeOverrides = useTableStore().get.rowSizeOverrides(); const rowSize = rowSizeOverrides.get(endingRowIndex) ?? rowElement?.size ?? undefined; diff --git a/packages/table/src/components/TableElement/useTableElement.ts b/packages/table/src/components/TableElement/useTableElement.ts index 80e239a325..73fef3193f 100644 --- a/packages/table/src/components/TableElement/useTableElement.ts +++ b/packages/table/src/components/TableElement/useTableElement.ts @@ -1,4 +1,3 @@ -import { useEffect } from 'react'; import { collapseSelection, getPluginOptions, @@ -7,7 +6,6 @@ import { } from '@udecode/plate-common'; import { ELEMENT_TABLE } from '../../createTablePlugin'; -import { computeAllCellIndices } from '../../merge/computeCellIndices'; import { useTableStore } from '../../stores/tableStore'; import { TablePlugin, TTableElement } from '../../types'; import { useSelectedCells } from './useSelectedCells'; @@ -30,10 +28,8 @@ export const useTableElementState = ({ } = {}): TableElementState => { const editor = useEditorRef(); - const { minColumnWidth, disableMarginLeft } = getPluginOptions( - editor, - ELEMENT_TABLE - ); + const { minColumnWidth, disableMarginLeft, disableCellsMerging } = + getPluginOptions(editor, ELEMENT_TABLE); const element = useElement(); const selectedCells = useTableStore().get.selectedCells(); @@ -45,10 +41,6 @@ export const useTableElementState = ({ let colSizes = useTableColSizes(element); - useEffect(() => { - computeAllCellIndices(editor, element); - }, [editor, element]); - if (transformColSizes) { colSizes = transformColSizes(colSizes); } diff --git a/packages/table/src/merge/computeCellIndices.ts b/packages/table/src/merge/computeCellIndices.ts index 4a7e396b2c..d6767736c0 100644 --- a/packages/table/src/merge/computeCellIndices.ts +++ b/packages/table/src/merge/computeCellIndices.ts @@ -1,9 +1,4 @@ -import { - findNodePath, - getPluginOptions, - PlateEditor, - Value, -} from '@udecode/plate-common'; +import { getPluginOptions, PlateEditor, Value } from '@udecode/plate-common'; import { ELEMENT_TABLE } from '../createTablePlugin'; import { @@ -62,7 +57,7 @@ export function computeCellIndices( }); if (rowIndex === -1 || colIndex === -1) { - console.log('Invalid cell location.'); + console.log('Invalid cell location.', rowIndex, colIndex); return null; } @@ -78,34 +73,18 @@ export const computeAllCellIndices = ( ) => { const options = getPluginOptions(editor, ELEMENT_TABLE); - // Initialize an array to store the indices of each cell - const cellIndicesArray = []; - - // const tablePath = findNodePath(editor, tableNode)!; - // Iterate through the table rows for (let r = 0; r < tableNode.children.length; r++) { const row = tableNode.children[r] as TTableRowElement; - const rowIndicesArray = []; // Iterate through the row cells for (let c = 0; c < row.children.length; c++) { const cell = row.children[c] as TTableCellElement; - // Get cell indices and store them in the row's array - // const cellPath = [r, c]; - const indices = computeCellIndices(editor, tableNode, cell); if (indices) { options._cellIndices.set(cell, indices); } - rowIndicesArray.push(indices); } - - // Push the rowIndicesArray to the cellIndicesArray - cellIndicesArray.push(rowIndicesArray); - // console.log('calculated array', cellIndicesArray); } - - return cellIndicesArray; }; diff --git a/packages/table/src/merge/deleteRow.ts b/packages/table/src/merge/deleteRow.ts index 9896298195..8292adcc8d 100644 --- a/packages/table/src/merge/deleteRow.ts +++ b/packages/table/src/merge/deleteRow.ts @@ -21,6 +21,7 @@ import { TTableRowElement, } from '../types'; import { getCellTypes } from '../utils'; +import { computeAllCellIndices } from './computeCellIndices'; import { findCellByIndexes } from './findCellByIndexes'; import { getIndices } from './getIndices'; diff --git a/packages/table/src/merge/findCellByIndexes.ts b/packages/table/src/merge/findCellByIndexes.ts index 9607f688ca..6227525b49 100644 --- a/packages/table/src/merge/findCellByIndexes.ts +++ b/packages/table/src/merge/findCellByIndexes.ts @@ -18,13 +18,13 @@ export const findCellByIndexes = ( (current) => current.children ) as TTableCellElement[]; - // console.log('searching for', searchRowIndex, searchColIndex); const foundCell = allCells.find((cell) => { const cellElement = cell as TTableCellElement; const indices = getIndices(options, cellElement) || computeCellIndices(editor, table, cellElement)!; + // getIndices(options, cellElement)!; const { col: _startColIndex, row: _startRowIndex } = indices; const { row: _endRowIndex, col: _endColIndex } = getIndicesWithSpans( @@ -32,7 +32,6 @@ export const findCellByIndexes = ( cellElement ); - // console.log('current', colIndex, endColIndex, rowIndex, endRowIndex); if ( searchColIndex >= _startColIndex && searchColIndex <= _endColIndex && diff --git a/packages/table/src/merge/mergeTableCells.ts b/packages/table/src/merge/mergeTableCells.ts index bf397a2f57..4e8af77107 100644 --- a/packages/table/src/merge/mergeTableCells.ts +++ b/packages/table/src/merge/mergeTableCells.ts @@ -106,24 +106,5 @@ export const mergeTableCells = ( // insert the new merged cell in place of the first cell in the selection insertElements(editor, mergedCell, { at: cellEntries[0][1] }); - - // /** - // * Update cell indices in weak map - // */ - // const tableEntry = findNode(editor, { - // at: cellEntries[0][1], - // match: { type: getPluginType(editor, ELEMENT_TABLE) }, - // })!; // TODO: improve typing - // const cellEntry = findNode(editor, { - // at: cellEntries[0][1], - // match: { type: getCellTypes(editor) }, - // })!; // TODO: improve typing - - // const realTable = tableEntry[0] as TTableElement; - // const mC = cellEntry[0] as TTableCellElement; - // console.log('realTable', realTable, 'mC', mC); - - // computeCellIndices(editor, realTable, mC); - // console.log('should be computed'); }); }; diff --git a/packages/table/src/merge/unmergeTableCells.ts b/packages/table/src/merge/unmergeTableCells.ts index 395ba1163d..9d8076a994 100644 --- a/packages/table/src/merge/unmergeTableCells.ts +++ b/packages/table/src/merge/unmergeTableCells.ts @@ -13,12 +13,7 @@ import { import { ELEMENT_TABLE, ELEMENT_TR } from '../createTablePlugin'; import { getTableGridAbove } from '../queries'; import { getColSpan } from '../queries/getColSpan'; -import { - TablePlugin, - TTableCellElement, - TTableElement, - TTableRowElement, -} from '../types'; +import { TablePlugin, TTableCellElement, TTableRowElement } from '../types'; import { getEmptyCellNode } from '../utils'; export const unmergeTableCells = ( @@ -73,12 +68,10 @@ export const unmergeTableCells = ( const { col: c } = options._cellIndices.get(item as TTableCellElement)!; if (c === col - 1) { newColPath = rowEl.children.indexOf(item) + 1; - // console.log('found first', newColPath); break; } if (col + getColSpan(cellElem as TTableCellElement) === c - 1) { newColPath = rowEl.children.indexOf(item); - // console.log('found last', newColPath); break; } } diff --git a/packages/table/src/withMergedCells.ts b/packages/table/src/withMergedCells.ts new file mode 100644 index 0000000000..f1f754bcfc --- /dev/null +++ b/packages/table/src/withMergedCells.ts @@ -0,0 +1,54 @@ +import { + getBlockAbove, + getPluginType, + PlateEditor, + TElement, + Value, +} from '@udecode/plate-common'; +import { BaseSelection } from 'slate'; + +import { + ELEMENT_TABLE, + ELEMENT_TD, + ELEMENT_TH, + ELEMENT_TR, +} from './createTablePlugin'; +import { computeAllCellIndices } from './merge/computeCellIndices'; +import { TTableElement } from './types'; + +export const withMergedCells = < + V extends Value = Value, + E extends PlateEditor = PlateEditor, +>( + editor: E +) => { + const { apply } = editor; + + editor.apply = (op) => { + const needsSync = + op.type === 'insert_node' || + op.type === 'merge_node' || + op.type === 'move_node' || + op.type === 'remove_node' || + op.type === 'set_node'; + + const updateTable = (selection: BaseSelection) => { + const tableEntry = getBlockAbove(editor, { + at: selection?.anchor, + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + }); + if (tableEntry) { + const realTable = tableEntry[0] as TTableElement; + computeAllCellIndices(editor, realTable); + } + }; + + if (needsSync) { + updateTable(editor.selection); + } + + apply(op); + }; + + return editor; +}; diff --git a/packages/table/src/withTable.ts b/packages/table/src/withTable.ts index 71392dde4e..59d34a0c5e 100644 --- a/packages/table/src/withTable.ts +++ b/packages/table/src/withTable.ts @@ -5,6 +5,7 @@ import { withDeleteTable } from './withDeleteTable'; import { withGetFragmentTable } from './withGetFragmentTable'; import { withInsertFragmentTable } from './withInsertFragmentTable'; import { withInsertTextTable } from './withInsertTextTable'; +import { withMergedCells } from './withMergedCells'; import { withNormalizeTable } from './withNormalizeTable'; import { withSelectionTable } from './withSelectionTable'; import { withSetFragmentDataTable } from './withSetFragmentDataTable'; @@ -16,6 +17,9 @@ export const withTable = < editor: E, plugin: WithPlatePlugin, V, E> ) => { + editor = plugin.options.disableCellsMerging + ? editor + : withMergedCells(editor); editor = withNormalizeTable(editor); editor = withDeleteTable(editor); editor = withGetFragmentTable(editor); From c892c567bcb2c40488cc9069dae2d084cf7a524e Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 16 Nov 2023 16:06:50 +0300 Subject: [PATCH 09/16] apply comments, improve experience --- .../default/plate-ui/table-element.tsx | 38 +++++++--------- .../useTableCellElementState.ts | 10 ++--- .../TableElement/useSelectedCells.ts | 27 +++++++++--- .../TableElement/useTableElement.ts | 6 ++- packages/table/src/merge/deleteColumn.ts | 20 +++++---- packages/table/src/merge/deleteRow.ts | 19 ++++---- packages/table/src/merge/findCellByIndexes.ts | 9 ++-- .../{getIndices.ts => getCellIndices.ts} | 2 +- ...ithSpans.ts => getCellIndicesWithSpans.ts} | 2 +- packages/table/src/merge/getCellPath.ts | 4 +- .../table/src/merge/getTableGridByRange.ts | 42 +++++++++++------- packages/table/src/merge/index.ts | 1 + packages/table/src/merge/insertTableColumn.ts | 8 ++-- packages/table/src/merge/insertTableRow.ts | 8 ++-- .../table/src/merge/isTableRectangular.ts | 29 ++++++++++++ packages/table/src/merge/mergeTableCells.ts | 4 +- packages/table/src/merge/unmergeTableCells.ts | 5 ++- .../table/src/merge/useTableMergeState.ts | 36 +++++++++++++++ .../table/src/queries/getTableGridAbove.ts | 44 +++++++++++++------ .../table/src/queries/getTableGridByRange.ts | 36 +++++++++++---- packages/table/src/stores/tableStore.ts | 1 + packages/table/src/transforms/deleteColumn.ts | 4 +- packages/table/src/transforms/deleteRow.ts | 4 +- .../table/src/transforms/insertTableColumn.ts | 2 +- .../table/src/transforms/insertTableRow.ts | 2 +- packages/table/src/withGetFragmentTable.ts | 2 +- 26 files changed, 246 insertions(+), 119 deletions(-) rename packages/table/src/merge/{getIndices.ts => getCellIndices.ts} (87%) rename packages/table/src/merge/{getIndicesWithSpans.ts => getCellIndicesWithSpans.ts} (89%) create mode 100644 packages/table/src/merge/isTableRectangular.ts create mode 100644 packages/table/src/merge/useTableMergeState.ts diff --git a/apps/www/src/registry/default/plate-ui/table-element.tsx b/apps/www/src/registry/default/plate-ui/table-element.tsx index 15e204fe50..b016d57db9 100644 --- a/apps/www/src/registry/default/plate-ui/table-element.tsx +++ b/apps/www/src/registry/default/plate-ui/table-element.tsx @@ -3,7 +3,6 @@ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; import { PopoverAnchor, PopoverContentProps } from '@radix-ui/react-popover'; import { isCollapsed, - isExpanded, PlateElement, PlateElementProps, useEditorState, @@ -11,13 +10,13 @@ import { useRemoveNodeButton, } from '@udecode/plate-common'; import { - getTableGridAbove, mergeTableCells, TTableElement, unmergeTableCells, useTableBordersDropdownMenuContentState, useTableElement, useTableElementState, + useTableMergeState, } from '@udecode/plate-table'; import { useReadOnly, useSelected } from 'slate-react'; @@ -118,22 +117,13 @@ const TableFloatingToolbar = React.forwardRef< const readOnly = useReadOnly(); const selected = useSelected(); const editor = useEditorState(); - // const open = !readOnly && selected && isCollapsed(editor.selection); const collapsed = !readOnly && selected && isCollapsed(editor.selection); - const expanded = !readOnly && selected && isExpanded(editor.selection); const open = !readOnly && selected; - const cellEntries = getTableGridAbove(editor, { format: 'cell' }); + const { canMerge, canUnmerge } = useTableMergeState(); - const canUnmerge = - collapsed && - cellEntries && - cellEntries.length === 1 && - ((cellEntries[0][0] as any)?.colSpan > 1 || - (cellEntries[0][0] as any)?.rowSpan > 1); - - const mergeContent = expanded && ( + const mergeContent = canMerge && (