From 29c6ea72215038976991770748b7773a4a564bc5 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 24 Aug 2023 17:35:45 +0300 Subject: [PATCH 01/40] [SlateEditor] implement merge and unmerge --- apps/www/src/components/icons.tsx | 2 + .../default/plate-ui/table-cell-element.tsx | 10 +- .../default/plate-ui/table-element.tsx | 95 ++++++++++--- .../components/TableCellElement/getClosest.ts | 14 ++ .../components/TableCellElement/getColSpan.ts | 6 + .../components/TableCellElement/getRowSpan.ts | 6 + .../src/components/TableCellElement/index.ts | 1 + .../useTableCellElementResizable.ts | 8 +- .../useTableCellElementState.ts | 102 +++++++++++++- .../TableCellElement/useTableCellsMerge.ts | 133 ++++++++++++++++++ .../TableElement/useTableElement.ts | 10 +- .../table/src/queries/getTableColumnCount.ts | 17 ++- .../src/queries/getTableOverriddenColSizes.ts | 6 +- packages/table/src/types.ts | 9 +- yarn.lock | 22 +-- 15 files changed, 388 insertions(+), 53 deletions(-) create mode 100644 packages/table/src/components/TableCellElement/getClosest.ts create mode 100644 packages/table/src/components/TableCellElement/getColSpan.ts create mode 100644 packages/table/src/components/TableCellElement/getRowSpan.ts create mode 100644 packages/table/src/components/TableCellElement/useTableCellsMerge.ts diff --git a/apps/www/src/components/icons.tsx b/apps/www/src/components/icons.tsx index b8082ec933..63ad1d2de5 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, Edit2, @@ -281,6 +282,7 @@ export const Icons = { codeblock: FileCode, color: Baseline, column: RectangleVertical, + combine: Combine, comment: MessageSquare, commentAdd: MessageSquarePlus, conflict: Unlink, 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..c9632235a0 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,17 +34,17 @@ const TableCellElement = React.forwardRef< rowSize, borders, isSelectingCell, + cellRef, + resizableState, } = useTableCellElementState(); const { props: cellProps } = useTableCellElement({ element: props.element }); - const resizableState = useTableCellElementResizableState({ - colIndex, - rowIndex, - }); + const { rightProps, bottomProps, leftProps, hiddenLeft } = useTableCellElementResizable(resizableState); const Cell = isHeader ? 'th' : 'td'; + return ( - +
n === element, }) && isCollapsed(editor.selection); + const cellEntries = getTableGridAbove(editor, { format: 'cell' }); + + const canUnmerge = + collapsedToolbar && + cellEntries && + cellEntries.length === 1 && + ((cellEntries[0][0] as any)?.colSpan > 1 || + (cellEntries[0][0] as any)?.rowSpan > 1); + + const mergeToolbar = + !readOnly && + someNode(editor, { + match: (n) => n === element, + }) && + !isCollapsed(editor.selection); + + const mergeContent = mergeToolbar && ( + + ); + + const unmergeButton = canUnmerge && ( + + ); + + + const bordersContent = collapsedToolbar && ( + <> + + + + + + + + + + + + + ); + return ( - + {children} e.preventDefault()} {...props} > - - - - - - - - - - - + {bordersContent} + {unmergeButton} + {mergeContent} ); @@ -157,10 +206,12 @@ const TableElement = React.forwardRef< React.ElementRef, PlateElementProps >(({ className, children, ...props }, ref) => { - const { colSizes, isSelectingCell, minColumnWidth, marginLeft } = + const { colSizes, tableWidth, isSelectingCell, minColumnWidth, marginLeft } = useTableElementState(); const { props: tableProps, colGroupProps } = useTableElement(); + // console.log('colSizes', colSizes); + return (
@@ -168,14 +219,14 @@ const TableElement = React.forwardRef< asChild ref={ref} className={cn( - 'my-4 ml-px mr-0 table h-px w-full table-fixed border-collapse', + 'relative my-4 ml-px mr-0 table h-px w-full table-fixed border-collapse', isSelectingCell && '[&_*::selection]:bg-none', className )} {...tableProps} {...props} > - +
{colSizes.map((width, index) => ( { + const closest = offsets.reduce( + (acc, current, index) => { + return Math.abs(current - target) < Math.abs(acc.value - target) + ? { value: current, index } + : acc; + }, + { + value: 0, + index: 0, + } + ); + return closest.index; +}; diff --git a/packages/table/src/components/TableCellElement/getColSpan.ts b/packages/table/src/components/TableCellElement/getColSpan.ts new file mode 100644 index 0000000000..236c62cc62 --- /dev/null +++ b/packages/table/src/components/TableCellElement/getColSpan.ts @@ -0,0 +1,6 @@ +import { TTableCellElement } from '../../types'; + +export const getColSpan = (cellElem: TTableCellElement) => { + const attrColSpan = Number(cellElem.attributes?.colspan); + return cellElem.colSpan || attrColSpan || 1; +}; diff --git a/packages/table/src/components/TableCellElement/getRowSpan.ts b/packages/table/src/components/TableCellElement/getRowSpan.ts new file mode 100644 index 0000000000..bd4aa2037d --- /dev/null +++ b/packages/table/src/components/TableCellElement/getRowSpan.ts @@ -0,0 +1,6 @@ +import { TTableCellElement } from '../../types'; + +export const getRowSpan = (cellElem: TTableCellElement) => { + const attrRowSpan = Number(cellElem.attributes?.rowspan); + return cellElem.rowSpan || attrRowSpan || 1; +}; diff --git a/packages/table/src/components/TableCellElement/index.ts b/packages/table/src/components/TableCellElement/index.ts index c5e7bb010a..4f0e6fbc06 100644 --- a/packages/table/src/components/TableCellElement/index.ts +++ b/packages/table/src/components/TableCellElement/index.ts @@ -9,3 +9,4 @@ export * from './roundCellSizeToStep'; export * from './useIsCellSelected'; export * from './useTableBordersDropdownMenuContentState'; export * from './useTableCellElementState'; +export * from './useTableCellsMerge'; diff --git a/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts b/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts index ac1bd1334b..467c50678a 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts @@ -145,6 +145,7 @@ export const useTableCellElementResizable = ({ const handleResizeRight = useCallback( ({ initialSize: currentInitial, delta, finished }: ResizeEvent) => { const nextInitial = colSizesWithoutOverrides[colIndex + 1]; + console.log('handle resize', currentInitial, nextInitial, minColumnWidth); const complement = (width: number) => currentInitial + nextInitial - width; @@ -157,11 +158,11 @@ export const useTableCellElementResizable = ({ stepX ); - const nextNew = nextInitial ? complement(currentNew) : undefined; + // const nextNew = nextInitial ? complement(currentNew) : undefined; const fn = finished ? setColSize : overrideColSize; fn(colIndex, currentNew); - if (nextNew) fn(colIndex + 1, nextNew); + // if (nextNew) fn(colIndex + 1, nextNew); }, [ colIndex, @@ -229,12 +230,15 @@ export const useTableCellElementResizable = ({ /* eslint-disable @typescript-eslint/no-shadow */ const getHandleHoverProps = (colIndex: number) => ({ onHover: () => { + console.log('hovering over handle', colIndex); if (hoveredColIndex === null) { + console.log('setting hovered col index', colIndex); setHoveredColIndex(colIndex); } }, onHoverEnd: () => { if (hoveredColIndex === colIndex) { + console.log('unsetting hovered col index'); setHoveredColIndex(null); } }, diff --git a/packages/table/src/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/components/TableCellElement/useTableCellElementState.ts index 0024203fc6..62813aedeb 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { MutableRefObject, useEffect, useRef } from 'react'; import { useElement, usePlateEditorRef } from '@udecode/plate-common'; import { useReadOnly } from 'slate-react'; @@ -10,11 +10,15 @@ import { TTableElement, TTableRowElement, } from '../../types'; +import { getClosest } from './getClosest'; +import { getColSpan } from './getColSpan'; +import { getRowSpan } from './getRowSpan'; import { BorderStylesDefault, getTableCellBorders, } from './getTableCellBorders'; import { useIsCellSelected } from './useIsCellSelected'; +import { useTableCellElementResizableState } from './useTableCellElementResizable'; export type TableCellElementState = { colIndex: number; @@ -26,6 +30,14 @@ export type TableCellElementState = { rowSize: number | undefined; borders: BorderStylesDefault; isSelectingCell: boolean; + cellRef: MutableRefObject; + resizableState: { + disableMarginLeft: boolean | undefined; + colIndex: number; + rowIndex: number; + stepX: number | undefined; + stepY: number | undefined; + }; }; export const useTableCellElementState = ({ @@ -38,9 +50,14 @@ export const useTableCellElementState = ({ } = {}): TableCellElementState => { const editor = usePlateEditorRef(); const cellElement = useElement(); + const cellRef = useRef(); + + // TODO: get rid of mutating element here + cellElement.colSpan = getColSpan(cellElement); + cellElement.rowSpan = getRowSpan(cellElement); - const colIndex = getTableColumnIndex(editor, cellElement); - const rowIndex = getTableRowIndex(editor, cellElement); + const rowIndex = + getTableRowIndex(editor, cellElement) + cellElement.rowSpan - 1; const readOnly = useReadOnly(); @@ -54,7 +71,77 @@ export const useTableCellElementState = ({ const rowSize = rowSizeOverrides.get(rowIndex) ?? rowElement?.size ?? undefined; - const isFirstCell = colIndex === 0; + const endColIndex = useRef(getTableColumnIndex(editor, cellElement)); + const startCIndex = useRef(getTableColumnIndex(editor, cellElement)); + + if (cellRef.current && hoveredColIndex === null) { + const cellOffset = cellRef.current?.offsetLeft; + + // TODO: improve typing. colSizes always presented when rendering cell + const colSizes = tableElement.colSizes!; + const { offsets } = colSizes.reduce( + (acc, current) => { + const currentOffset = acc.prevOffset + current; + acc.offsets.push(currentOffset); + acc.prevOffset = currentOffset; + return acc; + }, + { + offsets: [0], + prevOffset: 0, + } + ); + + const startColIndex = getClosest(cellOffset, offsets); + cellElement.colIndex = startColIndex; + startCIndex.current = startColIndex; + endColIndex.current = startColIndex + cellElement.colSpan - 1; + } + + const resizableState = useTableCellElementResizableState({ + colIndex: endColIndex.current, + rowIndex, + }); + + const content = cellElement.children + .map((node) => (node as TTableCellElement).children[0].text) + .join(' '); + + console.log( + 'content', + content, + resizableState + // 'rowIndex', + // rowIndex, + // 'colIndex', + // cIndex.current, + // 'path', + // path, + // 'props.nodeProps', + // props.nodeProps, + // 'cellRef.current', + // cellRef.current, + // 'offset', + // cellOffset, + + // 'cellElement.colSpan', + // cellElement.colSpan, + // 'cellWidth', + // cellWidth, + // 'offsets', + // offsets, + // 'colSpan', + // cellElement.colSpan, + // 'rowSpan', + // cellElement.rowSpan, + // 'startColIndex', + // startCIndex.current, + // 'endColIndex', + // endColIndex.current + // colSizes, + ); + + const isFirstCell = startCIndex.current === 0; const isFirstRow = tableElement.children?.[0] === rowElement; const borders = getTableCellBorders(cellElement, { @@ -63,15 +150,17 @@ export const useTableCellElementState = ({ }); return { - colIndex, + colIndex: endColIndex.current, rowIndex, readOnly: !ignoreReadOnly && readOnly, selected: isCellSelected, - hovered: hoveredColIndex === colIndex, + hovered: hoveredColIndex === endColIndex.current, hoveredLeft: isFirstCell && hoveredColIndex === -1, rowSize, borders, isSelectingCell: !!selectedCells, + cellRef, + resizableState, }; }; @@ -89,6 +178,7 @@ export const useTableCellElement = ({ return { props: { colSpan: element.colSpan, + rowSpan: element.rowSpan, }, }; }; diff --git a/packages/table/src/components/TableCellElement/useTableCellsMerge.ts b/packages/table/src/components/TableCellElement/useTableCellsMerge.ts new file mode 100644 index 0000000000..d245f3be79 --- /dev/null +++ b/packages/table/src/components/TableCellElement/useTableCellsMerge.ts @@ -0,0 +1,133 @@ +import { + insertElements, + removeNodes, + TDescendant, + usePlateEditorState, +} from '@udecode/plate-common'; + +import { getTableGridAbove } from '../../queries'; +import { getEmptyCellNode } from '../../utils/index'; +import { getColSpan } from './getColSpan'; +import { getRowSpan } from './getRowSpan'; + +export const useTableCellsMerge = () => { + const editor = usePlateEditorState(); + const cellEntries = getTableGridAbove(editor, { format: 'cell' }); + + const onMergeCells = () => { + // define colSpan + const colSpan = cellEntries.reduce((acc, [data, path]: any) => { + if (path[1] === cellEntries[0][1][1]) { + const cellColSpan = getColSpan(data); + return acc + cellColSpan; + } + return acc; + }, 0); + + // define rowSpan + const alreadyCounted: number[] = []; + const rowSpan = cellEntries.reduce((acc, [data, path]: any) => { + const curRowCounted = alreadyCounted.includes(path[1]); + if (path[1] !== cellEntries[0][1][1] && !curRowCounted) { + alreadyCounted.push(path[1]); + + const cellRowSpan = getRowSpan(data); + return acc + cellRowSpan; + } + return acc; + }, 1); + + const contents = []; + for (const cellEntry of cellEntries) { + const [el] = cellEntry; + contents.push(...el.children); // TODO: make deep clone here + } + + const cols: { [key: string]: number[][] } = {}; + 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] }); + }); + }); + + const mergedCell = { + ...getEmptyCellNode(editor, { + header: cellEntries[0][0].type === 'th', + newCellChildren: contents, + }), + colSpan, + rowSpan, + }; + + insertElements(editor, mergedCell, { at: cellEntries[0][1] }); + }; + + const onUnmerge = () => { + 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; + const rowSpan = cellElem.rowSpan; + + const colPaths = Array.from( + { length: colSpan } as ArrayLike, + (_, index) => { + return index; + } + ).map((current) => { + return colPath + current; + }); + + removeNodes(editor, { at: path }); + + Array.from({ length: rowSpan } as ArrayLike, (_, index) => { + return index; + }) + .flatMap((current) => { + const currentRowPath = rowPath + current; + return colPaths.map((currentColPath) => [ + ...tablePath, + currentRowPath, + currentColPath, + ]); + }) + .forEach((p, index) => + insertElements( + editor, + index === 0 ? createEmptyCell(cellElem.children) : createEmptyCell(), + { at: p } + ) + ); + }; + + return { onMergeCells, onUnmerge }; +}; diff --git a/packages/table/src/components/TableElement/useTableElement.ts b/packages/table/src/components/TableElement/useTableElement.ts index 447b061798..dece4e0116 100644 --- a/packages/table/src/components/TableElement/useTableElement.ts +++ b/packages/table/src/components/TableElement/useTableElement.ts @@ -13,6 +13,7 @@ import { useTableColSizes } from './useTableColSizes'; export interface TableElementState { colSizes: number[]; + tableWidth: number; isSelectingCell: boolean; minColumnWidth: number; marginLeft: number; @@ -47,13 +48,14 @@ export const useTableElementState = ({ colSizes = transformColSizes(colSizes); } - // add a last col to fill the remaining space - if (!colSizes.includes(0)) { - colSizes.push('100%' as any); - } + // TODO: get rid of mutating here + element.colSizes = colSizes; + + const tableWidth = colSizes.reduce((acc, cur) => acc + cur, 0); return { colSizes, + tableWidth, isSelectingCell: !!selectedCells, minColumnWidth: minColumnWidth!, marginLeft, diff --git a/packages/table/src/queries/getTableColumnCount.ts b/packages/table/src/queries/getTableColumnCount.ts index 77b4426feb..6db7a7b1c3 100644 --- a/packages/table/src/queries/getTableColumnCount.ts +++ b/packages/table/src/queries/getTableColumnCount.ts @@ -1,5 +1,20 @@ import { TElement } from '@udecode/plate-common'; +import { TTableCellElement } from '../types'; + export const getTableColumnCount = (tableNode: TElement) => { - return (tableNode.children as TElement[])?.[0]?.children?.length ?? 0; + const firstRow = (tableNode.children as TElement[])?.[0]; + const colCount = firstRow?.children.reduce((acc, current) => { + let next = acc + 1; + + const cellElement = current as TTableCellElement; + const attrColSpan = Number(cellElement.attributes?.colspan); + const colSpan = cellElement.colSpan || attrColSpan; + if (colSpan && colSpan > 1) { + next += colSpan - 1; + } + + return next; + }, 0); + return colCount; }; diff --git a/packages/table/src/queries/getTableOverriddenColSizes.ts b/packages/table/src/queries/getTableOverriddenColSizes.ts index c9e5b96a03..e2d8a402e9 100644 --- a/packages/table/src/queries/getTableOverriddenColSizes.ts +++ b/packages/table/src/queries/getTableOverriddenColSizes.ts @@ -2,6 +2,8 @@ import { TableStoreSizeOverrides } from '../stores/index'; import { TTableElement } from '../types'; import { getTableColumnCount } from './index'; +const DEFAULT_COL_WIDTH = 200; + /** * Returns node.colSizes if it exists, applying overrides, otherwise returns a * 0-filled array. @@ -16,7 +18,9 @@ export const getTableOverriddenColSizes = ( tableNode.colSizes ? [...tableNode.colSizes] : (Array.from({ length: colCount }).fill(0) as number[]) - ).map((size, index) => colSizeOverrides?.get(index) ?? size); + ).map( + (size, index) => colSizeOverrides?.get(index) ?? size ?? DEFAULT_COL_WIDTH + ); return colSizes; }; diff --git a/packages/table/src/types.ts b/packages/table/src/types.ts index 36117f5c06..fe277f2762 100644 --- a/packages/table/src/types.ts +++ b/packages/table/src/types.ts @@ -77,9 +77,16 @@ export interface TTableRowElement extends TElement { } export interface TTableCellElement extends TElement { - colSpan?: number; size?: number; background?: string; + colSpan?: number; + rowSpan?: number; + colIndex?: number; + rowIndex?: number; + attributes?: { + colspan?: string; + rowspan?: string; + }; borders?: { top?: BorderStyle; left?: BorderStyle; diff --git a/yarn.lock b/yarn.lock index 3e96465808..2668e251b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7170,7 +7170,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-resizable@npm:23.0.0, @udecode/plate-resizable@workspace:^, @udecode/plate-resizable@workspace:packages/resizable": +"@udecode/plate-resizable@npm:23.1.0, @udecode/plate-resizable@workspace:^, @udecode/plate-resizable@workspace:packages/resizable": version: 0.0.0-use.local resolution: "@udecode/plate-resizable@workspace:packages/resizable" dependencies: @@ -7214,13 +7214,13 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-serializer-csv@npm:23.0.0, @udecode/plate-serializer-csv@workspace:^, @udecode/plate-serializer-csv@workspace:packages/serializer-csv": +"@udecode/plate-serializer-csv@npm:23.1.0, @udecode/plate-serializer-csv@workspace:^, @udecode/plate-serializer-csv@workspace:packages/serializer-csv": version: 0.0.0-use.local resolution: "@udecode/plate-serializer-csv@workspace:packages/serializer-csv" dependencies: "@types/papaparse": "npm:^5.3.7" "@udecode/plate-common": "npm:22.0.2" - "@udecode/plate-table": "npm:23.0.0" + "@udecode/plate-table": "npm:23.1.0" papaparse: "npm:^5.4.1" peerDependencies: react: ">=16.8.0" @@ -7232,7 +7232,7 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-serializer-docx@npm:23.0.0, @udecode/plate-serializer-docx@workspace:^, @udecode/plate-serializer-docx@workspace:packages/serializer-docx": +"@udecode/plate-serializer-docx@npm:23.1.0, @udecode/plate-serializer-docx@workspace:^, @udecode/plate-serializer-docx@workspace:packages/serializer-docx": version: 0.0.0-use.local resolution: "@udecode/plate-serializer-docx@workspace:packages/serializer-docx" dependencies: @@ -7242,7 +7242,7 @@ __metadata: "@udecode/plate-indent-list": "npm:22.0.2" "@udecode/plate-media": "npm:23.0.0" "@udecode/plate-paragraph": "npm:22.0.2" - "@udecode/plate-table": "npm:23.0.0" + "@udecode/plate-table": "npm:23.1.0" validator: "npm:^13.9.0" peerDependencies: react: ">=16.8.0" @@ -7322,12 +7322,12 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-table@npm:23.0.0, @udecode/plate-table@workspace:^, @udecode/plate-table@workspace:packages/table": +"@udecode/plate-table@npm:23.1.0, @udecode/plate-table@workspace:^, @udecode/plate-table@workspace:packages/table": version: 0.0.0-use.local resolution: "@udecode/plate-table@workspace:packages/table" dependencies: "@udecode/plate-common": "npm:22.0.2" - "@udecode/plate-resizable": "npm:23.0.0" + "@udecode/plate-resizable": "npm:23.1.0" peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" @@ -7457,15 +7457,15 @@ __metadata: "@udecode/plate-normalizers": "npm:22.0.2" "@udecode/plate-paragraph": "npm:22.0.2" "@udecode/plate-reset-node": "npm:22.0.2" - "@udecode/plate-resizable": "npm:23.0.0" + "@udecode/plate-resizable": "npm:23.1.0" "@udecode/plate-select": "npm:22.0.2" - "@udecode/plate-serializer-csv": "npm:23.0.0" - "@udecode/plate-serializer-docx": "npm:23.0.0" + "@udecode/plate-serializer-csv": "npm:23.1.0" + "@udecode/plate-serializer-docx": "npm:23.1.0" "@udecode/plate-serializer-html": "npm:22.0.2" "@udecode/plate-serializer-md": "npm:22.0.2" "@udecode/plate-suggestion": "npm:22.0.2" "@udecode/plate-tabbable": "npm:22.0.2" - "@udecode/plate-table": "npm:23.0.0" + "@udecode/plate-table": "npm:23.1.0" "@udecode/plate-trailing-block": "npm:22.0.2" peerDependencies: react: ">=16.8.0" From 79c3548e68151b31f19ca79daf020392c6aebe44 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 25 Aug 2023 12:56:04 +0300 Subject: [PATCH 02/40] [TablePlugin] fix resize --- .../resizable/src/components/ResizeHandle.tsx | 18 ++++- .../useTableCellElementResizable.ts | 41 ++++++++--- .../useTableCellElementState.ts | 72 ++++++++++--------- .../TableElement/useTableElement.ts | 3 - 4 files changed, 87 insertions(+), 47 deletions(-) diff --git a/packages/resizable/src/components/ResizeHandle.tsx b/packages/resizable/src/components/ResizeHandle.tsx index 4d71263674..c42405b106 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, @@ -71,7 +73,7 @@ export const useResizeHandleState = ({ const [isResizing, setIsResizing] = useState(false); const [initialPosition, setInitialPosition] = useState(0); - const [initialSize, setInitialSize] = useState(0); + const [initialSize, setInitialSize] = useState(_initialSize ?? 0); const isHorizontal = direction === 'left' || direction === 'right'; @@ -88,7 +90,18 @@ export const useResizeHandleState = ({ const currentPosition = isHorizontal ? clientX : clientY; const delta = currentPosition - initialPosition; - onResize?.({ initialSize, delta, finished, direction }); + // console.log( + // 'send resize event, _initialSize', + // _initialSize, + // 'initialSize', + // initialSize + // ); + onResize?.({ + initialSize: _initialSize ?? initialSize, + delta, + finished, + direction, + }); }; const handleMouseMove = (event: MouseEvent | TouchEvent) => @@ -119,6 +132,7 @@ export const useResizeHandleState = ({ isHorizontal, onHoverEnd, direction, + _initialSize, ]); return { diff --git a/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts b/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts index 467c50678a..81edf40d77 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts @@ -23,14 +23,14 @@ import { setTableMarginLeft, setTableRowSize, } from '../../transforms/index'; -import { TablePlugin, TTableElement } from '../../types'; +import { TablePlugin, TTableCellElement, TTableElement } from '../../types'; import { useTableColSizes } from '../TableElement/useTableColSizes'; import { roundCellSizeToStep } from './roundCellSizeToStep'; import { TableCellElementState } from './useTableCellElementState'; export type TableCellElementResizableOptions = Pick< TableCellElementState, - 'colIndex' | 'rowIndex' + 'colIndex' | 'rowIndex' | 'colSpan' > & { /** * Resize by step instead of by pixel. @@ -47,6 +47,7 @@ export type TableCellElementResizableOptions = Pick< export const useTableCellElementResizableState = ({ colIndex, rowIndex, + colSpan, step, stepX = step, stepY = step, @@ -61,6 +62,7 @@ export const useTableCellElementResizableState = ({ disableMarginLeft, colIndex, rowIndex, + colSpan, stepX, stepY, }; @@ -70,6 +72,7 @@ export const useTableCellElementResizable = ({ disableMarginLeft, colIndex, rowIndex, + colSpan, stepX, stepY, }: ReturnType): { @@ -79,13 +82,19 @@ export const useTableCellElementResizable = ({ hiddenLeft: boolean; } => { const editor = usePlateEditorRef(); - const element = useElement(); + const element = useElement(); const tableElement = useElement(ELEMENT_TABLE); const { minColumnWidth = 0 } = getPluginOptions( editor, ELEMENT_TABLE ); + // override width for horizontally merged cell + let initialWidth: number | undefined; + if (colSpan > 1) { + initialWidth = tableElement.colSizes?.[colIndex]; + } + const [hoveredColIndex, setHoveredColIndex] = useTableStore().use.hoveredColIndex(); @@ -145,7 +154,6 @@ export const useTableCellElementResizable = ({ const handleResizeRight = useCallback( ({ initialSize: currentInitial, delta, finished }: ResizeEvent) => { const nextInitial = colSizesWithoutOverrides[colIndex + 1]; - console.log('handle resize', currentInitial, nextInitial, minColumnWidth); const complement = (width: number) => currentInitial + nextInitial - width; @@ -158,11 +166,25 @@ export const useTableCellElementResizable = ({ stepX ); - // const nextNew = nextInitial ? complement(currentNew) : undefined; + const nextNew = nextInitial ? complement(currentNew) : undefined; + // console.log( + // 'currentInitial', + // currentInitial, + // 'currentNew', + // currentNew, + // 'colSizesWithoutOverrides', + // colSizesWithoutOverrides, + // 'nextInitial', + // nextInitial, + // 'nextNew', + // nextNew, + // 'finished', + // finished + // ); const fn = finished ? setColSize : overrideColSize; fn(colIndex, currentNew); - // if (nextNew) fn(colIndex + 1, nextNew); + if (nextNew) fn(colIndex + 1, nextNew); }, [ colIndex, @@ -230,15 +252,15 @@ export const useTableCellElementResizable = ({ /* eslint-disable @typescript-eslint/no-shadow */ const getHandleHoverProps = (colIndex: number) => ({ onHover: () => { - console.log('hovering over handle', colIndex); + // console.log('hovering over handle', colIndex); if (hoveredColIndex === null) { - console.log('setting hovered col index', colIndex); + // console.log('setting hovered col index', colIndex); setHoveredColIndex(colIndex); } }, onHoverEnd: () => { if (hoveredColIndex === colIndex) { - console.log('unsetting hovered col index'); + // console.log('unsetting hovered col index'); setHoveredColIndex(null); } }, @@ -250,6 +272,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 62813aedeb..9cc7d0894b 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -22,6 +22,7 @@ import { useTableCellElementResizableState } from './useTableCellElementResizabl export type TableCellElementState = { colIndex: number; + colSpan: number; rowIndex: number; readOnly: boolean; hovered: boolean; @@ -35,6 +36,7 @@ export type TableCellElementState = { disableMarginLeft: boolean | undefined; colIndex: number; rowIndex: number; + colSpan: number; stepX: number | undefined; stepY: number | undefined; }; @@ -101,45 +103,48 @@ export const useTableCellElementState = ({ const resizableState = useTableCellElementResizableState({ colIndex: endColIndex.current, rowIndex, + colSpan: cellElement.colSpan!, }); const content = cellElement.children .map((node) => (node as TTableCellElement).children[0].text) .join(' '); - console.log( - 'content', - content, - resizableState - // 'rowIndex', - // rowIndex, - // 'colIndex', - // cIndex.current, - // 'path', - // path, - // 'props.nodeProps', - // props.nodeProps, - // 'cellRef.current', - // cellRef.current, - // 'offset', - // cellOffset, - - // 'cellElement.colSpan', - // cellElement.colSpan, - // 'cellWidth', - // cellWidth, - // 'offsets', - // offsets, - // 'colSpan', - // cellElement.colSpan, - // 'rowSpan', - // cellElement.rowSpan, - // 'startColIndex', - // startCIndex.current, - // 'endColIndex', - // endColIndex.current - // colSizes, - ); + console.log('cell element component'); + + // console.log( + // 'content', + // content, + // resizableState + // // 'rowIndex', + // // rowIndex, + // // 'colIndex', + // // cIndex.current, + // // 'path', + // // path, + // // 'props.nodeProps', + // // props.nodeProps, + // // 'cellRef.current', + // // cellRef.current, + // // 'offset', + // // cellOffset, + + // // 'cellElement.colSpan', + // // cellElement.colSpan, + // // 'cellWidth', + // // cellWidth, + // // 'offsets', + // // offsets, + // // 'colSpan', + // // cellElement.colSpan, + // // 'rowSpan', + // // cellElement.rowSpan, + // // 'startColIndex', + // // startCIndex.current, + // // 'endColIndex', + // // endColIndex.current + // // colSizes, + // ); const isFirstCell = startCIndex.current === 0; const isFirstRow = tableElement.children?.[0] === rowElement; @@ -152,6 +157,7 @@ export const useTableCellElementState = ({ return { colIndex: endColIndex.current, rowIndex, + colSpan: cellElement.colSpan, readOnly: !ignoreReadOnly && readOnly, selected: isCellSelected, hovered: hoveredColIndex === endColIndex.current, diff --git a/packages/table/src/components/TableElement/useTableElement.ts b/packages/table/src/components/TableElement/useTableElement.ts index dece4e0116..0ca2cbd507 100644 --- a/packages/table/src/components/TableElement/useTableElement.ts +++ b/packages/table/src/components/TableElement/useTableElement.ts @@ -48,9 +48,6 @@ export const useTableElementState = ({ colSizes = transformColSizes(colSizes); } - // TODO: get rid of mutating here - element.colSizes = colSizes; - const tableWidth = colSizes.reduce((acc, cur) => acc + cur, 0); return { From 2f33bf0e4dfd3537e5502dc8ae15cbeb3dd32882 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 25 Aug 2023 17:22:15 +0300 Subject: [PATCH 03/40] [TablePlugin] fix pasting table from word and other sites --- .../default/plate-ui/table-cell-element.tsx | 9 ++- .../default/plate-ui/table-element.tsx | 5 +- .../resizable/src/components/ResizeHandle.tsx | 6 -- .../TableCellElement/getCellsOffsets.ts | 15 ++++ .../useTableCellElementResizable.ts | 17 ---- .../useTableCellElementState.ts | 81 ++----------------- .../TableElement/useTableColSizes.ts | 22 +++-- .../TableElement/useTableElement.ts | 3 +- .../src/queries/getTableOverriddenColSizes.ts | 26 +++--- packages/table/src/stores/tableStore.ts | 1 + .../table/src/transforms/insertTableRow.ts | 26 ++++-- packages/table/src/types.ts | 2 - 12 files changed, 81 insertions(+), 132 deletions(-) create mode 100644 packages/table/src/components/TableCellElement/getCellsOffsets.ts 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 c9632235a0..4bdb1d3ea6 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 @@ -27,6 +27,7 @@ const TableCellElement = React.forwardRef< const { colIndex, rowIndex, + colSpan, readOnly, selected, hovered, @@ -35,16 +36,20 @@ const TableCellElement = React.forwardRef< borders, isSelectingCell, cellRef, - resizableState, } = useTableCellElementState(); const { props: cellProps } = useTableCellElement({ element: props.element }); + const resizableState = useTableCellElementResizableState({ + colIndex, + rowIndex, + colSpan, + }); + const { rightProps, bottomProps, leftProps, hiddenLeft } = useTableCellElementResizable(resizableState); const Cell = isHeader ? 'th' : 'td'; - return ( ); - const bordersContent = collapsedToolbar && ( <> @@ -193,9 +192,9 @@ const TableFloatingToolbar = React.forwardRef< onOpenAutoFocus={(e) => e.preventDefault()} {...props} > - {bordersContent} {unmergeButton} {mergeContent} + {bordersContent} ); @@ -210,8 +209,6 @@ const TableElement = React.forwardRef< useTableElementState(); const { props: tableProps, colGroupProps } = useTableElement(); - // console.log('colSizes', colSizes); - return (
diff --git a/packages/resizable/src/components/ResizeHandle.tsx b/packages/resizable/src/components/ResizeHandle.tsx index c42405b106..4e63dcc804 100644 --- a/packages/resizable/src/components/ResizeHandle.tsx +++ b/packages/resizable/src/components/ResizeHandle.tsx @@ -90,12 +90,6 @@ export const useResizeHandleState = ({ const currentPosition = isHorizontal ? clientX : clientY; const delta = currentPosition - initialPosition; - // console.log( - // 'send resize event, _initialSize', - // _initialSize, - // 'initialSize', - // initialSize - // ); onResize?.({ initialSize: _initialSize ?? initialSize, delta, diff --git a/packages/table/src/components/TableCellElement/getCellsOffsets.ts b/packages/table/src/components/TableCellElement/getCellsOffsets.ts new file mode 100644 index 0000000000..356e53dbd3 --- /dev/null +++ b/packages/table/src/components/TableCellElement/getCellsOffsets.ts @@ -0,0 +1,15 @@ +export const getCellOffsets = (colSizes: number[]) => { + const { offsets } = colSizes.reduce( + (acc, current) => { + const currentOffset = acc.prevOffset + current; + acc.offsets.push(currentOffset); + acc.prevOffset = currentOffset; + return acc; + }, + { + offsets: [0], + prevOffset: 0, + } + ); + return offsets; +}; diff --git a/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts b/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts index 81edf40d77..4159956f71 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementResizable.ts @@ -167,20 +167,6 @@ export const useTableCellElementResizable = ({ ); const nextNew = nextInitial ? complement(currentNew) : undefined; - // console.log( - // 'currentInitial', - // currentInitial, - // 'currentNew', - // currentNew, - // 'colSizesWithoutOverrides', - // colSizesWithoutOverrides, - // 'nextInitial', - // nextInitial, - // 'nextNew', - // nextNew, - // 'finished', - // finished - // ); const fn = finished ? setColSize : overrideColSize; fn(colIndex, currentNew); @@ -252,15 +238,12 @@ export const useTableCellElementResizable = ({ /* eslint-disable @typescript-eslint/no-shadow */ const getHandleHoverProps = (colIndex: number) => ({ onHover: () => { - // console.log('hovering over handle', colIndex); if (hoveredColIndex === null) { - // console.log('setting hovered col index', colIndex); setHoveredColIndex(colIndex); } }, onHoverEnd: () => { if (hoveredColIndex === colIndex) { - // console.log('unsetting hovered col index'); setHoveredColIndex(null); } }, diff --git a/packages/table/src/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/components/TableCellElement/useTableCellElementState.ts index 9cc7d0894b..96f4ee2d97 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -18,7 +18,6 @@ import { getTableCellBorders, } from './getTableCellBorders'; import { useIsCellSelected } from './useIsCellSelected'; -import { useTableCellElementResizableState } from './useTableCellElementResizable'; export type TableCellElementState = { colIndex: number; @@ -32,14 +31,6 @@ export type TableCellElementState = { borders: BorderStylesDefault; isSelectingCell: boolean; cellRef: MutableRefObject; - resizableState: { - disableMarginLeft: boolean | undefined; - colIndex: number; - rowIndex: number; - colSpan: number; - stepX: number | undefined; - stepY: number | undefined; - }; }; export const useTableCellElementState = ({ @@ -66,6 +57,7 @@ export const useTableCellElementState = ({ const isCellSelected = useIsCellSelected(cellElement); const hoveredColIndex = useTableStore().get.hoveredColIndex(); const selectedCells = useTableStore().get.selectedCells(); + const cellOffsets = useTableStore().get.cellsOffsets(); const tableElement = useElement(ELEMENT_TABLE); const rowElement = useElement(ELEMENT_TR); @@ -76,76 +68,14 @@ export const useTableCellElementState = ({ const endColIndex = useRef(getTableColumnIndex(editor, cellElement)); const startCIndex = useRef(getTableColumnIndex(editor, cellElement)); - if (cellRef.current && hoveredColIndex === null) { - const cellOffset = cellRef.current?.offsetLeft; - - // TODO: improve typing. colSizes always presented when rendering cell - const colSizes = tableElement.colSizes!; - const { offsets } = colSizes.reduce( - (acc, current) => { - const currentOffset = acc.prevOffset + current; - acc.offsets.push(currentOffset); - acc.prevOffset = currentOffset; - return acc; - }, - { - offsets: [0], - prevOffset: 0, - } - ); - - const startColIndex = getClosest(cellOffset, offsets); - cellElement.colIndex = startColIndex; + if (cellRef.current && hoveredColIndex === null && cellOffsets) { + const cellOffset = cellRef.current.offsetLeft; + + const startColIndex = getClosest(cellOffset, cellOffsets); startCIndex.current = startColIndex; endColIndex.current = startColIndex + cellElement.colSpan - 1; } - const resizableState = useTableCellElementResizableState({ - colIndex: endColIndex.current, - rowIndex, - colSpan: cellElement.colSpan!, - }); - - const content = cellElement.children - .map((node) => (node as TTableCellElement).children[0].text) - .join(' '); - - console.log('cell element component'); - - // console.log( - // 'content', - // content, - // resizableState - // // 'rowIndex', - // // rowIndex, - // // 'colIndex', - // // cIndex.current, - // // 'path', - // // path, - // // 'props.nodeProps', - // // props.nodeProps, - // // 'cellRef.current', - // // cellRef.current, - // // 'offset', - // // cellOffset, - - // // 'cellElement.colSpan', - // // cellElement.colSpan, - // // 'cellWidth', - // // cellWidth, - // // 'offsets', - // // offsets, - // // 'colSpan', - // // cellElement.colSpan, - // // 'rowSpan', - // // cellElement.rowSpan, - // // 'startColIndex', - // // startCIndex.current, - // // 'endColIndex', - // // endColIndex.current - // // colSizes, - // ); - const isFirstCell = startCIndex.current === 0; const isFirstRow = tableElement.children?.[0] === rowElement; @@ -166,7 +96,6 @@ export const useTableCellElementState = ({ borders, isSelectingCell: !!selectedCells, cellRef, - resizableState, }; }; diff --git a/packages/table/src/components/TableElement/useTableColSizes.ts b/packages/table/src/components/TableElement/useTableColSizes.ts index 95e26f0e49..fbdc872e88 100644 --- a/packages/table/src/components/TableElement/useTableColSizes.ts +++ b/packages/table/src/components/TableElement/useTableColSizes.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { findNodePath, getPluginOptions, @@ -13,6 +13,7 @@ import { } from '../../queries/index'; import { useTableStore } from '../../stores/tableStore'; import { TablePlugin, TTableElement } from '../../types'; +import { getCellOffsets } from '../TableCellElement/getCellsOffsets'; /** * Returns colSizes with overrides applied. @@ -30,12 +31,23 @@ export const useTableColSizes = ( ELEMENT_TABLE ); - const overriddenColSizes = getTableOverriddenColSizes( - tableNode, - disableOverrides ? undefined : colSizeOverrides + const colCount = getTableColumnCount(tableNode); + + // spread needed here to apply new array, not a reference + const overriddenColSizes = useMemo( + () => + getTableOverriddenColSizes( + colCount, + tableNode.colSizes ? [...tableNode.colSizes] : undefined, + disableOverrides ? undefined : colSizeOverrides + ), + [colCount, colSizeOverrides, disableOverrides, tableNode.colSizes] ); - const colCount = getTableColumnCount(tableNode); + const setCellsOffsets = useTableStore().set.cellsOffsets(); + useEffect(() => { + setCellsOffsets(getCellOffsets(overriddenColSizes)); + }, [overriddenColSizes, setCellsOffsets]); useEffect(() => { if ( diff --git a/packages/table/src/components/TableElement/useTableElement.ts b/packages/table/src/components/TableElement/useTableElement.ts index 0ca2cbd507..7633e6a1ae 100644 --- a/packages/table/src/components/TableElement/useTableElement.ts +++ b/packages/table/src/components/TableElement/useTableElement.ts @@ -43,11 +43,10 @@ export const useTableElementState = ({ : marginLeftOverride ?? element.marginLeft ?? 0; let colSizes = useTableColSizes(element); - if (transformColSizes) { colSizes = transformColSizes(colSizes); } - + const tableWidth = colSizes.reduce((acc, cur) => acc + cur, 0); return { diff --git a/packages/table/src/queries/getTableOverriddenColSizes.ts b/packages/table/src/queries/getTableOverriddenColSizes.ts index e2d8a402e9..28591ede56 100644 --- a/packages/table/src/queries/getTableOverriddenColSizes.ts +++ b/packages/table/src/queries/getTableOverriddenColSizes.ts @@ -1,26 +1,26 @@ import { TableStoreSizeOverrides } from '../stores/index'; -import { TTableElement } from '../types'; -import { getTableColumnCount } from './index'; const DEFAULT_COL_WIDTH = 200; /** * Returns node.colSizes if it exists, applying overrides, otherwise returns a - * 0-filled array. + * colSizes with default widths. Since colSizes should always return valid widths + * of the columns for table cells merging feature. */ export const getTableOverriddenColSizes = ( - tableNode: TTableElement, + colCount: number, + colSizes?: number[], colSizeOverrides?: TableStoreSizeOverrides ): number[] => { - const colCount = getTableColumnCount(tableNode); + const newColSizes = ( + colSizes ?? (Array.from({ length: colCount }).fill(0) as number[]) + ).map((size, index) => { + const overridden = colSizeOverrides?.get(index); + if (overridden) return overridden; + if (size > 0) return size; - const colSizes = ( - tableNode.colSizes - ? [...tableNode.colSizes] - : (Array.from({ length: colCount }).fill(0) as number[]) - ).map( - (size, index) => colSizeOverrides?.get(index) ?? size ?? DEFAULT_COL_WIDTH - ); + return DEFAULT_COL_WIDTH; + }); - return colSizes; + return newColSizes; }; diff --git a/packages/table/src/stores/tableStore.ts b/packages/table/src/stores/tableStore.ts index 91842d9a93..6b7fdd430f 100644 --- a/packages/table/src/stores/tableStore.ts +++ b/packages/table/src/stores/tableStore.ts @@ -12,6 +12,7 @@ export const { tableStore, useTableStore } = createAtomStore( marginLeftOverride: null as number | null, hoveredColIndex: null as number | null, selectedCells: null as TElement[] | null, + cellsOffsets: null as number[] | null, }, { name: 'table' as const, scope: ELEMENT_TABLE } ); diff --git a/packages/table/src/transforms/insertTableRow.ts b/packages/table/src/transforms/insertTableRow.ts index 2a67a764e4..589b810bff 100644 --- a/packages/table/src/transforms/insertTableRow.ts +++ b/packages/table/src/transforms/insertTableRow.ts @@ -12,8 +12,10 @@ import { } from '@udecode/plate-common'; import { Path } from 'slate'; +import { getRowSpan } from '../components/TableCellElement/getRowSpan'; import { ELEMENT_TABLE, ELEMENT_TR } from '../createTablePlugin'; -import { TablePlugin } from '../types'; +import { getTableColumnCount } from '../queries'; +import * as types from '../types'; import { getCellTypes, getEmptyRowNode } from '../utils/index'; export const insertTableRow = ( @@ -44,7 +46,7 @@ export const insertTableRow = ( }); if (!trEntry) return; - const [trNode, trPath] = trEntry; + const [, trPath] = trEntry; const tableEntry = getBlockAbove(editor, { match: { type: getPluginType(editor, ELEMENT_TABLE) }, @@ -52,21 +54,35 @@ export const insertTableRow = ( }); if (!tableEntry) return; - const { newCellChildren } = getPluginOptions( + const { newCellChildren } = getPluginOptions( editor, ELEMENT_TABLE ); + const currentCellEntry = findNode(editor, { + at: fromRow, + match: { type: getCellTypes(editor) }, + }); + if (!currentCellEntry) return; + + const [cellNode] = currentCellEntry; + const cellElement = cellNode as types.TTableCellElement; + const rowSpan = getRowSpan(cellElement); + + // consider merged cell with rowSpan > 1 + const rowIndex = trPath.at(-1)!; // TODO: improve typing + const updateTrPath = [...trPath.slice(0, -1), rowIndex + rowSpan - 1]; + withoutNormalizing(editor, () => { insertElements( editor, getEmptyRowNode(editor, { header, - colCount: (trNode.children as TElement[]).length, + colCount: getTableColumnCount(tableEntry[0] as TElement), newCellChildren, }), { - at: Path.isPath(at) ? at : Path.next(trPath), + at: Path.isPath(at) ? at : Path.next(updateTrPath), } ); }); diff --git a/packages/table/src/types.ts b/packages/table/src/types.ts index fe277f2762..863754734f 100644 --- a/packages/table/src/types.ts +++ b/packages/table/src/types.ts @@ -81,8 +81,6 @@ export interface TTableCellElement extends TElement { background?: string; colSpan?: number; rowSpan?: number; - colIndex?: number; - rowIndex?: number; attributes?: { colspan?: string; rowspan?: string; From 278bfd11cc6f25884f177f9d9bd06cddf0275b32 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 25 Aug 2023 17:31:09 +0300 Subject: [PATCH 04/40] [TablePlugin] add comment --- .../components/TableCellElement/useTableCellElementState.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/table/src/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/components/TableCellElement/useTableCellElementState.ts index 96f4ee2d97..08f668dbb5 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -68,10 +68,12 @@ export const useTableCellElementState = ({ const endColIndex = useRef(getTableColumnIndex(editor, cellElement)); const startCIndex = useRef(getTableColumnIndex(editor, cellElement)); + // TODO: measure performance on huge table with the following approach. + // consider using lodash memoize for getting closest if (cellRef.current && hoveredColIndex === null && cellOffsets) { const cellOffset = cellRef.current.offsetLeft; - const startColIndex = getClosest(cellOffset, cellOffsets); + startCIndex.current = startColIndex; endColIndex.current = startColIndex + cellElement.colSpan - 1; } From b9b8bdbbb48755f21fb73c6d23d7a2e68c522983 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 25 Aug 2023 17:39:08 +0300 Subject: [PATCH 05/40] [TablePlugin] change icon for unmerge button --- apps/www/src/components/icons.tsx | 2 ++ apps/www/src/registry/default/plate-ui/table-element.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/www/src/components/icons.tsx b/apps/www/src/components/icons.tsx index 63ad1d2de5..b3c9cf9a3f 100644 --- a/apps/www/src/components/icons.tsx +++ b/apps/www/src/components/icons.tsx @@ -74,6 +74,7 @@ import { Trash, Twitter, Underline, + Ungroup, Unlink, WrapText, X, @@ -283,6 +284,7 @@ export const Icons = { color: Baseline, column: RectangleVertical, combine: Combine, + ungroup: Ungroup, comment: MessageSquare, commentAdd: MessageSquarePlus, conflict: Unlink, 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 9ee8f4abf4..c3338b3000 100644 --- a/apps/www/src/registry/default/plate-ui/table-element.tsx +++ b/apps/www/src/registry/default/plate-ui/table-element.tsx @@ -156,7 +156,7 @@ const TableFloatingToolbar = React.forwardRef< const unmergeButton = canUnmerge && ( ); From 00197fc89802a46f8f689fe03398cf5a9b286f66 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 28 Aug 2023 12:31:05 +0300 Subject: [PATCH 06/40] [TablePlugin] fix bug in ff --- .../www/src/registry/default/plate-ui/table-cell-element.tsx | 2 +- .../components/TableCellElement/useTableCellElementState.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 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 4bdb1d3ea6..3395bfb4e4 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 @@ -55,7 +55,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/packages/table/src/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/components/TableCellElement/useTableCellElementState.ts index 08f668dbb5..1d0b03e395 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -46,6 +46,7 @@ export const useTableCellElementState = ({ const cellRef = useRef(); // TODO: get rid of mutating element here + // currently needed only for pasting tables from clipboard to gather span attributes cellElement.colSpan = getColSpan(cellElement); cellElement.rowSpan = getRowSpan(cellElement); @@ -68,8 +69,8 @@ export const useTableCellElementState = ({ const endColIndex = useRef(getTableColumnIndex(editor, cellElement)); const startCIndex = useRef(getTableColumnIndex(editor, cellElement)); - // TODO: measure performance on huge table with the following approach. - // consider using lodash memoize for getting closest + // TODO: measure performance on huge tables with the following approach. + // consider using cached offsets to calculate "closest" per column only (not for each cell) if (cellRef.current && hoveredColIndex === null && cellOffsets) { const cellOffset = cellRef.current.offsetLeft; const startColIndex = getClosest(cellOffset, cellOffsets); From c06db8ce8eb74d9dcf86976896d18d5cdf5c702c Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 29 Aug 2023 16:46:06 +0300 Subject: [PATCH 07/40] [TablePlugin] fix lint problems --- apps/e2e-examples/src/main.tsx | 1 + .../src/components/TableCellElement/useTableCellElementState.ts | 2 +- packages/table/src/components/TableElement/useTableElement.ts | 2 +- packages/yjs/src/withPlateYjs.ts | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/e2e-examples/src/main.tsx b/apps/e2e-examples/src/main.tsx index a20cddf2cb..c25ae1fa3a 100644 --- a/apps/e2e-examples/src/main.tsx +++ b/apps/e2e-examples/src/main.tsx @@ -13,6 +13,7 @@ const router = createBrowserRouter([ const rootElement = document.querySelector('#root'); +// eslint-disable-next-line react/no-deprecated ReactDOM.render( diff --git a/packages/table/src/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/components/TableCellElement/useTableCellElementState.ts index 1d0b03e395..8cbb3f69e9 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -43,7 +43,7 @@ export const useTableCellElementState = ({ } = {}): TableCellElementState => { const editor = usePlateEditorRef(); const cellElement = useElement(); - const cellRef = useRef(); + const cellRef = useRef(); // TODO: get rid of mutating element here // currently needed only for pasting tables from clipboard to gather span attributes diff --git a/packages/table/src/components/TableElement/useTableElement.ts b/packages/table/src/components/TableElement/useTableElement.ts index 7633e6a1ae..bf782c2d53 100644 --- a/packages/table/src/components/TableElement/useTableElement.ts +++ b/packages/table/src/components/TableElement/useTableElement.ts @@ -46,7 +46,7 @@ export const useTableElementState = ({ if (transformColSizes) { colSizes = transformColSizes(colSizes); } - + const tableWidth = colSizes.reduce((acc, cur) => acc + cur, 0); return { diff --git a/packages/yjs/src/withPlateYjs.ts b/packages/yjs/src/withPlateYjs.ts index abbe2241bd..fe1c4a8ac7 100644 --- a/packages/yjs/src/withPlateYjs.ts +++ b/packages/yjs/src/withPlateYjs.ts @@ -87,7 +87,7 @@ export const withPlateYjs = < autoConnect: false, ...yjsOptions, }), - provider.awareness, + provider.awareness!, cursorOptions ) ) as EE; From 32c75d4cf76ed90d8107f21e2e7929bdbc85aa08 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 30 Aug 2023 12:27:29 +0300 Subject: [PATCH 08/40] [TablePlugin] fix types and tests --- .../default/plate-ui/table-cell-element.tsx | 4 +-- .../getTableOverriddenColSizes.spec.ts | 25 ++++++++----------- 2 files changed, 12 insertions(+), 17 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 3395bfb4e4..9fe89e8dcc 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 @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { LegacyRef } from 'react'; import { PlateElement, PlateElementProps, Value } from '@udecode/plate-common'; import { TTableCellElement, @@ -84,7 +84,7 @@ const TableCellElement = React.forwardRef< } as React.CSSProperties } > - + | undefined}>
{ describe('when colSizes is not defined', () => { - it('should return all zeros', () => { - const tableElement = makeTableElement(3); + it('should return all default widths', () => { const overrides: Map = new Map(); - expect(getTableOverriddenColSizes(tableElement, overrides)).toEqual([ - 0, 0, 0, + expect(getTableOverriddenColSizes(3, undefined, overrides)).toEqual([ + 200, 200, 200, ]); }); - it('should apply overrides', () => { - const tableElement = makeTableElement(3); + it('should apply overrides and default instead of zero', () => { const overrides: Map = new Map([ [0, 100], [2, 200], ]); - expect(getTableOverriddenColSizes(tableElement, overrides)).toEqual([ - 100, 0, 200, + expect(getTableOverriddenColSizes(3, undefined, overrides)).toEqual([ + 100, 200, 200, ]); }); }); describe('when colSizes is defined', () => { it('should return colSizes', () => { - const tableElement = makeTableElement(3, [100, 200, 300]); - const overrides: Map = new Map(); - expect(getTableOverriddenColSizes(tableElement, overrides)).toEqual([ + expect(getTableOverriddenColSizes(3, [100, 200, 300])).toEqual([ 100, 200, 300, ]); }); it('should apply overrides', () => { - const tableElement = makeTableElement(3, [100, 200, 300]); const overrides: Map = new Map([ [0, 1000], [2, 2000], ]); - expect(getTableOverriddenColSizes(tableElement, overrides)).toEqual([ - 1000, 200, 2000, - ]); + expect(getTableOverriddenColSizes(3, [100, 200, 300], overrides)).toEqual( + [1000, 200, 2000] + ); }); }); }); From f56a9eca2c28dea80078cfb074925062e8fea389 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 30 Aug 2023 17:43:01 +0300 Subject: [PATCH 09/40] [TablePlugin] fix serialisation tests for table element --- apps/www/src/registry/default/plate-ui/table-element.tsx | 4 ++-- .../src/components/TableCellElement/useTableCellsMerge.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 c3338b3000..a54812b2cc 100644 --- a/apps/www/src/registry/default/plate-ui/table-element.tsx +++ b/apps/www/src/registry/default/plate-ui/table-element.tsx @@ -6,8 +6,8 @@ import { PlateElement, PlateElementProps, someNode, + useEditorRef, useElement, - usePlateEditorState, useRemoveNodeButton, } from '@udecode/plate-common'; import { @@ -115,7 +115,7 @@ const TableFloatingToolbar = React.forwardRef< const { props: buttonProps } = useRemoveNodeButton({ element }); const readOnly = useReadOnly(); - const editor = usePlateEditorState(); + const editor = useEditorRef(); const { onMergeCells, onUnmerge } = useTableCellsMerge(); diff --git a/packages/table/src/components/TableCellElement/useTableCellsMerge.ts b/packages/table/src/components/TableCellElement/useTableCellsMerge.ts index d245f3be79..0be19dcafb 100644 --- a/packages/table/src/components/TableCellElement/useTableCellsMerge.ts +++ b/packages/table/src/components/TableCellElement/useTableCellsMerge.ts @@ -2,7 +2,7 @@ import { insertElements, removeNodes, TDescendant, - usePlateEditorState, + useEditorRef, } from '@udecode/plate-common'; import { getTableGridAbove } from '../../queries'; @@ -11,7 +11,7 @@ import { getColSpan } from './getColSpan'; import { getRowSpan } from './getRowSpan'; export const useTableCellsMerge = () => { - const editor = usePlateEditorState(); + const editor = useEditorRef(); const cellEntries = getTableGridAbove(editor, { format: 'cell' }); const onMergeCells = () => { From ecf14a88afd62a1857331995f75ff8eae4832aec Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 6 Sep 2023 08:46:09 +0300 Subject: [PATCH 10/40] [TablePlugin] fix some issues --- .../default/plate-ui/table-element.tsx | 80 ++--- yarn.lock | 298 +++++++++--------- 2 files changed, 193 insertions(+), 185 deletions(-) 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 a54812b2cc..8708cd758b 100644 --- a/apps/www/src/registry/default/plate-ui/table-element.tsx +++ b/apps/www/src/registry/default/plate-ui/table-element.tsx @@ -5,7 +5,6 @@ import { isCollapsed, PlateElement, PlateElementProps, - someNode, useEditorRef, useElement, useRemoveNodeButton, @@ -18,7 +17,7 @@ import { useTableElement, useTableElementState, } from '@udecode/plate-table'; -import { useReadOnly } from 'slate-react'; +import { useReadOnly, useSelected } from 'slate-react'; import { cn } from '@/lib/utils'; import { Icons, iconVariants } from '@/components/icons'; @@ -116,33 +115,29 @@ const TableFloatingToolbar = React.forwardRef< const readOnly = useReadOnly(); const editor = useEditorRef(); + const isSelected = useSelected(); const { onMergeCells, onUnmerge } = useTableCellsMerge(); - const collapsedToolbar = - !readOnly && - someNode(editor, { - match: (n) => n === element, - }) && - isCollapsed(editor.selection); + const collapsedToolbarActive = + isSelected && !readOnly && isCollapsed(editor.selection); const cellEntries = getTableGridAbove(editor, { format: 'cell' }); - + const hasEntries = !!cellEntries?.length; const canUnmerge = - collapsedToolbar && - cellEntries && + hasEntries && cellEntries.length === 1 && ((cellEntries[0][0] as any)?.colSpan > 1 || (cellEntries[0][0] as any)?.rowSpan > 1); - const mergeToolbar = + const mergeToolbarActive = + isSelected && !readOnly && - someNode(editor, { - match: (n) => n === element, - }) && + hasEntries && + cellEntries.length > 1 && !isCollapsed(editor.selection); - const mergeContent = mergeToolbar && ( + const mergeButton = ( - ); - - const bordersContent = collapsedToolbar && ( + const collapsedContent = ( <> + {canUnmerge && ( + + )} ); - const collapsedContent = ( + const unmergeButton = canUnmerge && ( + + ); + + const bordersContent = collapsed && ( <> - {canUnmerge && ( - - )}