From e179bf02b6f2406ce58d06b1af854469569dfac3 Mon Sep 17 00:00:00 2001 From: Ziad Beyens Date: Fri, 12 Aug 2022 04:54:45 +0200 Subject: [PATCH] Fix: table (#1778) * style * feat * feat * fix: cell height * fix: delete table cells should replace cell children then reselect the range * fix: default level should be 0 * fix: copy from a single cell * feat * feat * fix: selection, replace cell children instead of cell, many blocks inside a table * feat * feat * fix: insert text across many cells * fix: normalize cell children * style * fix * fix * core.md * Create table.md * Create dnd.md * Create ui-table.md * fix * fix --- .changeset/dnd.md | 5 + .changeset/khaki-comics-fold.md | 8 + .changeset/table.md | 25 ++ .changeset/ui-table.md | 5 + packages/core/src/queries/index.ts | 1 + .../core/src/queries/isRangeAcrossBlocks.ts | 11 +- .../core/src/queries/isRangeInSameBlock.ts | 31 ++ packages/core/src/transforms/index.ts | 2 + .../core/src/transforms/removeNodeChildren.ts | 26 ++ .../src/transforms/replaceNodeChildren.ts | 41 ++ packages/nodes/table/src/index.ts | 3 + .../nodes/table/src/queries/getTableAbove.ts | 19 + packages/nodes/table/src/queries/index.ts | 1 + .../src/transforms/insertTableColumn.spec.tsx | 82 +++- .../src/transforms/insertTableRow.spec.tsx | 42 +- .../nodes/table/src/withDeleteTable.spec.tsx | 63 ++- packages/nodes/table/src/withDeleteTable.ts | 26 +- .../table/src/withGetFragmentTable.spec.tsx | 38 ++ .../nodes/table/src/withGetFragmentTable.ts | 38 +- .../src/withInsertFragmentTable.spec.tsx | 411 ++++++++++++++---- .../table/src/withInsertFragmentTable.ts | 65 ++- .../table/src/withInsertTextTable.spec.tsx | 75 ++++ .../nodes/table/src/withInsertTextTable.ts | 44 ++ .../table/src/withNormalizeTable.spec.tsx | 58 +++ .../nodes/table/src/withNormalizeTable.ts | 52 +++ .../nodes/table/src/withSelectionTable.ts | 74 ++++ packages/nodes/table/src/withTable.ts | 6 + packages/ui/combobox/src/Combobox.tsx | 8 +- .../ui/dnd/src/components/withDraggable.tsx | 2 +- .../TableCellElement.styles.ts | 2 +- .../src/TableElement/TableElement.styles.ts | 2 +- 31 files changed, 1089 insertions(+), 177 deletions(-) create mode 100644 .changeset/dnd.md create mode 100644 .changeset/khaki-comics-fold.md create mode 100644 .changeset/table.md create mode 100644 .changeset/ui-table.md create mode 100644 packages/core/src/queries/isRangeInSameBlock.ts create mode 100644 packages/core/src/transforms/removeNodeChildren.ts create mode 100644 packages/core/src/transforms/replaceNodeChildren.ts create mode 100644 packages/nodes/table/src/queries/getTableAbove.ts create mode 100644 packages/nodes/table/src/withInsertTextTable.spec.tsx create mode 100644 packages/nodes/table/src/withInsertTextTable.ts create mode 100644 packages/nodes/table/src/withNormalizeTable.spec.tsx create mode 100644 packages/nodes/table/src/withNormalizeTable.ts create mode 100644 packages/nodes/table/src/withSelectionTable.ts diff --git a/.changeset/dnd.md b/.changeset/dnd.md new file mode 100644 index 0000000000..f37a45f436 --- /dev/null +++ b/.changeset/dnd.md @@ -0,0 +1,5 @@ +--- +"@udecode/plate-ui-dnd": patch +--- + +- `withDraggable`: default `level` option is now 0 as expected diff --git a/.changeset/khaki-comics-fold.md b/.changeset/khaki-comics-fold.md new file mode 100644 index 0000000000..e9e839b392 --- /dev/null +++ b/.changeset/khaki-comics-fold.md @@ -0,0 +1,8 @@ +--- +"@udecode/plate-core": minor +--- + +- `isRangeAcrossBlocks`: Now returns true if one of the block above is found but not the other and returns undefined if no block is found. +- `isRangeInSameBlock`: Whether the range is in the same block. +- `removeNodeChildren`: Remove node children. +- `replaceNodeChildren`: Replace node children: remove then insert. diff --git a/.changeset/table.md b/.changeset/table.md new file mode 100644 index 0000000000..e5e071f5ca --- /dev/null +++ b/.changeset/table.md @@ -0,0 +1,25 @@ +--- +"@udecode/plate-table": minor +--- + +- on delete many cells: + - replace cell children by a paragraph then reselect all the selected cells +- on get fragment (copy): + - copying in a single cell should not copy the table anymore +- on insert fragment (paste): + - pasting multiple blocks into many selected cells will replace these cells children by the same blocks + - replace cell children by a paragraph then reselect all the selected cells +- on insert text: + - it should delete the cells content by preserving the cells +- normalize cells: + - wrap cell children in a paragraph if they are texts +- normalize selection: + - it was easy to destroy the table structure when selection goes beyond a table. The current fix is to normalize the selection so it selects the whole table (see the specs) +- specs: + - https://github.com/udecode/editor-protocol/issues/63 + - https://github.com/udecode/editor-protocol/issues/64 + - https://github.com/udecode/editor-protocol/issues/65 + - https://github.com/udecode/editor-protocol/issues/66 + - https://github.com/udecode/editor-protocol/issues/67 + - https://github.com/udecode/editor-protocol/issues/68 + - https://github.com/udecode/editor-protocol/issues/69 diff --git a/.changeset/ui-table.md b/.changeset/ui-table.md new file mode 100644 index 0000000000..176382fb58 --- /dev/null +++ b/.changeset/ui-table.md @@ -0,0 +1,5 @@ +--- +"@udecode/plate-ui-table": patch +--- + +- fix: table cells are now full height and not vertically centered anymore diff --git a/packages/core/src/queries/index.ts b/packages/core/src/queries/index.ts index 185dce47a1..2105f2fa30 100644 --- a/packages/core/src/queries/index.ts +++ b/packages/core/src/queries/index.ts @@ -30,6 +30,7 @@ export * from './isFirstChild'; export * from './isMarkActive'; export * from './isPointAtWordEnd'; export * from './isRangeAcrossBlocks'; +export * from './isRangeInSameBlock'; export * from './isRangeInSingleText'; export * from './isSelectionAtBlockEnd'; export * from './isSelectionAtBlockStart'; diff --git a/packages/core/src/queries/isRangeAcrossBlocks.ts b/packages/core/src/queries/isRangeAcrossBlocks.ts index fb320f45fa..5f389a3ce1 100644 --- a/packages/core/src/queries/isRangeAcrossBlocks.ts +++ b/packages/core/src/queries/isRangeAcrossBlocks.ts @@ -5,6 +5,9 @@ import { getBlockAbove } from './getBlockAbove'; /** * Is the range (default: selection) across blocks. + * - return undefined if block not found + * - return boolean whether one of the block is not found, but the other is found + * - return boolean whether block paths are unequal */ export const isRangeAcrossBlocks = ( editor: TEditor, @@ -14,7 +17,7 @@ export const isRangeAcrossBlocks = ( }: Omit, 'at'> & { at?: Range | null } = {} ) => { if (!at) at = editor.selection; - if (!at) return false; + if (!at) return; const [start, end] = Range.edges(at); const startBlock = getBlockAbove(editor, { @@ -26,5 +29,9 @@ export const isRangeAcrossBlocks = ( ...options, }); - return startBlock && endBlock && !Path.equals(startBlock[1], endBlock[1]); + if (!startBlock && !endBlock) return; + + if (!startBlock || !endBlock) return true; + + return !Path.equals(startBlock[1], endBlock[1]); }; diff --git a/packages/core/src/queries/isRangeInSameBlock.ts b/packages/core/src/queries/isRangeInSameBlock.ts new file mode 100644 index 0000000000..dda96f3903 --- /dev/null +++ b/packages/core/src/queries/isRangeInSameBlock.ts @@ -0,0 +1,31 @@ +import { Path, Range } from 'slate'; +import { GetAboveNodeOptions, TEditor, Value } from '../slate/index'; +import { getBlockAbove } from './getBlockAbove'; + +/** + * Whether the range is in the same block. + */ +export const isRangeInSameBlock = ( + editor: TEditor, + { + at, + ...options + }: Omit, 'at'> & { at?: Range | null } = {} +) => { + if (!at) at = editor.selection; + if (!at) return; + + const [start, end] = Range.edges(at); + const startBlock = getBlockAbove(editor, { + at: start, + ...options, + }); + const endBlock = getBlockAbove(editor, { + at: end, + ...options, + }); + + if (!startBlock || !endBlock) return; + + return Path.equals(startBlock[1], endBlock[1]); +}; diff --git a/packages/core/src/transforms/index.ts b/packages/core/src/transforms/index.ts index 926659e6bb..03345d3fdd 100644 --- a/packages/core/src/transforms/index.ts +++ b/packages/core/src/transforms/index.ts @@ -7,6 +7,8 @@ export * from './insertElements'; export * from './insertEmptyElement'; export * from './moveChildren'; export * from './removeMark'; +export * from './removeNodeChildren'; +export * from './replaceNodeChildren'; export * from './selectEditor'; export * from './selectEndOfBlockAboveSelection'; export * from './setElements'; diff --git a/packages/core/src/transforms/removeNodeChildren.ts b/packages/core/src/transforms/removeNodeChildren.ts new file mode 100644 index 0000000000..3d77863d7d --- /dev/null +++ b/packages/core/src/transforms/removeNodeChildren.ts @@ -0,0 +1,26 @@ +import { Path } from 'slate'; +import { + getNodeChildren, + removeNodes, + RemoveNodesOptions, + TEditor, + Value, + withoutNormalizing, +} from '../slate/index'; + +/** + * Remove node children. + */ +export const removeNodeChildren = ( + editor: TEditor, + path: Path, + options?: Omit, 'at'> +) => { + withoutNormalizing(editor, () => { + for (const [, childPath] of getNodeChildren(editor, path, { + reverse: true, + })) { + removeNodes(editor, { ...options, at: childPath }); + } + }); +}; diff --git a/packages/core/src/transforms/replaceNodeChildren.ts b/packages/core/src/transforms/replaceNodeChildren.ts new file mode 100644 index 0000000000..481ff8dcb5 --- /dev/null +++ b/packages/core/src/transforms/replaceNodeChildren.ts @@ -0,0 +1,41 @@ +import { Path } from 'slate'; +import { + EElementOrText, + insertNodes, + InsertNodesOptions, + RemoveNodesOptions, + TEditor, + Value, + withoutNormalizing, +} from '../slate/index'; +import { removeNodeChildren } from './removeNodeChildren'; + +/** + * Replace node children: remove then insert. + */ +export const replaceNodeChildren = < + N extends EElementOrText, + V extends Value = Value +>( + editor: TEditor, + { + at, + nodes, + insertOptions, + removeOptions, + }: { + at: Path; + nodes: N | N[]; + removeOptions?: Omit, 'at'>; + insertOptions?: Omit, 'at'>; + } +) => { + withoutNormalizing(editor, () => { + removeNodeChildren(editor, at, removeOptions); + + insertNodes(editor, nodes, { + ...insertOptions, + at: at.concat([0]), + }); + }); +}; diff --git a/packages/nodes/table/src/index.ts b/packages/nodes/table/src/index.ts index 0d1d8a0871..1e1972f5cb 100644 --- a/packages/nodes/table/src/index.ts +++ b/packages/nodes/table/src/index.ts @@ -8,6 +8,9 @@ export * from './types'; export * from './withDeleteTable'; export * from './withGetFragmentTable'; export * from './withInsertFragmentTable'; +export * from './withInsertTextTable'; +export * from './withNormalizeTable'; +export * from './withSelectionTable'; export * from './withTable'; export * from './queries/index'; export * from './transforms/index'; diff --git a/packages/nodes/table/src/queries/getTableAbove.ts b/packages/nodes/table/src/queries/getTableAbove.ts new file mode 100644 index 0000000000..9ca0fa6bbe --- /dev/null +++ b/packages/nodes/table/src/queries/getTableAbove.ts @@ -0,0 +1,19 @@ +import { + getBlockAbove, + getPluginType, + PlateEditor, + Value, +} from '@udecode/plate-core'; +import { GetAboveNodeOptions } from '@udecode/plate-core/src/index'; +import { ELEMENT_TABLE } from '../createTablePlugin'; + +export const getTableAbove = ( + editor: PlateEditor, + options?: GetAboveNodeOptions +) => + getBlockAbove(editor, { + match: { + type: getPluginType(editor, ELEMENT_TABLE), + }, + ...options, + }); diff --git a/packages/nodes/table/src/queries/index.ts b/packages/nodes/table/src/queries/index.ts index f853c39b9d..fa38ed2584 100644 --- a/packages/nodes/table/src/queries/index.ts +++ b/packages/nodes/table/src/queries/index.ts @@ -6,6 +6,7 @@ export * from './getCellInNextTableRow'; export * from './getCellInPreviousTableRow'; export * from './getNextTableCell'; export * from './getPreviousTableCell'; +export * from './getTableAbove'; export * from './getTableCellEntry'; export * from './getTableColumnCount'; export * from './getTableColumnIndex'; diff --git a/packages/nodes/table/src/transforms/insertTableColumn.spec.tsx b/packages/nodes/table/src/transforms/insertTableColumn.spec.tsx index b1379d16e3..d15eb29ce7 100644 --- a/packages/nodes/table/src/transforms/insertTableColumn.spec.tsx +++ b/packages/nodes/table/src/transforms/insertTableColumn.spec.tsx @@ -14,14 +14,22 @@ describe('insertTableColumn', () => { - 11 - 12 + + 11 + + + 12 + - 21 - 22 - + 21 + + + + 22 + + @@ -32,17 +40,29 @@ describe('insertTableColumn', () => { - 11 - 12 - + 11 + + + 12 + + + + + - 21 - 22 - + 21 + + + 22 + + + + + @@ -71,12 +91,20 @@ describe('insertTableColumn', () => { - 11 - 12 + + 11 + + + 12 + - 21 - 22 + + 21 + + + 22 + @@ -86,18 +114,30 @@ describe('insertTableColumn', () => { - 11 - + 11 + + + + + + + + 12 - 12 - 21 - + 21 + + + + + + + + 22 - 22 diff --git a/packages/nodes/table/src/transforms/insertTableRow.spec.tsx b/packages/nodes/table/src/transforms/insertTableRow.spec.tsx index a47b04fdbd..b26da2a4cd 100644 --- a/packages/nodes/table/src/transforms/insertTableRow.spec.tsx +++ b/packages/nodes/table/src/transforms/insertTableRow.spec.tsx @@ -14,15 +14,23 @@ describe('insertTableRow', () => { - 11 - 12 + + 11 + + + 12 + - 21 - + + 21 + + + + + 22 - 22 @@ -32,19 +40,31 @@ describe('insertTableRow', () => { - 11 - 12 + + 11 + + + 12 + - 21 - 22 + + 21 + + + 22 + - + + + - + + + diff --git a/packages/nodes/table/src/withDeleteTable.spec.tsx b/packages/nodes/table/src/withDeleteTable.spec.tsx index bf85b772e0..adf122223f 100644 --- a/packages/nodes/table/src/withDeleteTable.spec.tsx +++ b/packages/nodes/table/src/withDeleteTable.spec.tsx @@ -14,8 +14,12 @@ describe('withDeleteTable', () => { - 11 - 12 + + 11 + + + 12 + @@ -28,10 +32,14 @@ describe('withDeleteTable', () => { - 11 - 12 - + 11 + + + + 12 + + @@ -63,8 +71,12 @@ describe('withDeleteTable', () => { - 11 - 12 + + 11 + + + 12 + @@ -76,10 +88,14 @@ describe('withDeleteTable', () => { - - 11 + + + 11 + + + + 12 - 12 @@ -113,14 +129,18 @@ describe('withDeleteTable', () => { 11 - 12 + + 12 + 21 - 22 + + 22 + @@ -131,16 +151,25 @@ describe('withDeleteTable', () => { - - + + + + + + + 12 - 12 - + + + + + + + 22 - 22 diff --git a/packages/nodes/table/src/withDeleteTable.ts b/packages/nodes/table/src/withDeleteTable.ts index d67917997c..a3e959ad50 100644 --- a/packages/nodes/table/src/withDeleteTable.ts +++ b/packages/nodes/table/src/withDeleteTable.ts @@ -1,15 +1,17 @@ import { + ELEMENT_DEFAULT, getBlockAbove, getEndPoint, - getNodeChildren, + getPluginType, getPointAfter, getPointBefore, getStartPoint, isCollapsed, moveSelection, PlateEditor, - removeNodes, + replaceNodeChildren, select, + TElement, Value, withoutNormalizing, } from '@udecode/plate-core'; @@ -95,15 +97,21 @@ export const withDeleteTable = < if (cellEntries.length > 1) { withoutNormalizing(editor, () => { cellEntries.forEach(([, cellPath]) => { - for (const [, childPath] of getNodeChildren(editor, cellPath, { - reverse: true, - })) { - removeNodes(editor, { at: childPath }); - } + replaceNodeChildren(editor, { + at: cellPath, + nodes: { + type: getPluginType(editor, ELEMENT_DEFAULT), + children: [{ text: '' }], + }, + }); }); - }); - select(editor, cellEntries[0][1]); + // set back the selection + select(editor, { + anchor: getStartPoint(editor, cellEntries[0][1]), + focus: getEndPoint(editor, cellEntries[cellEntries.length - 1][1]), + }); + }); return; } diff --git a/packages/nodes/table/src/withGetFragmentTable.spec.tsx b/packages/nodes/table/src/withGetFragmentTable.spec.tsx index 3400c8be9d..bb894a2bc3 100644 --- a/packages/nodes/table/src/withGetFragmentTable.spec.tsx +++ b/packages/nodes/table/src/withGetFragmentTable.spec.tsx @@ -43,4 +43,42 @@ describe('withGetFragmentTable', () => { expect(fragment).toEqual([getTableGridAbove(editor)[0][0]]); }); }); + + // https://github.com/udecode/editor-protocol/issues/63 + describe('when copying a single cell with 2 blocks', () => { + it('should copy only the 2 blocks', () => { + const blocks = ( + + + + 11 + + + 12 + + + + ); + + const input = (( + + + + {blocks} + + + + ) as any) as PlateEditor; + + let editor = createPlateEditor({ + editor: input, + }); + + editor = withGetFragmentTable(editor); + + const fragment = editor.getFragment(); + + expect(fragment).toEqual(blocks); + }); + }); }); diff --git a/packages/nodes/table/src/withGetFragmentTable.ts b/packages/nodes/table/src/withGetFragmentTable.ts index 818046f6b9..022a5d49b9 100644 --- a/packages/nodes/table/src/withGetFragmentTable.ts +++ b/packages/nodes/table/src/withGetFragmentTable.ts @@ -1,4 +1,10 @@ -import { getPluginType, PlateEditor, Value } from '@udecode/plate-core'; +import { + getPluginType, + PlateEditor, + TDescendant, + TElement, + Value, +} from '@udecode/plate-core'; import { getTableGridAbove } from './queries/getTableGridAbove'; import { ELEMENT_TABLE } from './createTablePlugin'; @@ -14,20 +20,36 @@ export const withGetFragmentTable = < const { getFragment } = editor; editor.getFragment = (): any[] => { - let fragment = getFragment(); + const fragment = getFragment(); - fragment = fragment.map((node) => { + const newFragment: TDescendant[] = []; + + fragment.forEach((node) => { if (node.type === getPluginType(editor, ELEMENT_TABLE)) { - const subTable = getTableGridAbove(editor); - if (subTable.length) { - return subTable[0][0]; + const rows = node.children as TElement[]; + + const rowCount = rows.length; + if (!rowCount) return; + + const colCount = rows[0].children.length; + const hasOneCell = rowCount <= 1 && colCount <= 1; + + if (!hasOneCell) { + const subTable = getTableGridAbove(editor); + if (subTable.length) { + newFragment.push(subTable[0][0]); + return; + } + } else { + newFragment.push(...(rows[0].children[0].children as TElement[])); + return; } } - return node; + newFragment.push(node); }); - return fragment; + return newFragment; }; return editor; diff --git a/packages/nodes/table/src/withInsertFragmentTable.spec.tsx b/packages/nodes/table/src/withInsertFragmentTable.spec.tsx index fd9dbe4969..fc56b4ba6b 100644 --- a/packages/nodes/table/src/withInsertFragmentTable.spec.tsx +++ b/packages/nodes/table/src/withInsertFragmentTable.spec.tsx @@ -16,14 +16,22 @@ describe('withInsertFragmentTable', () => { - 11 - + + 11 + + + + + 12 - 12 - 21 - 22 + + 21 + + + 22 + @@ -33,10 +41,14 @@ describe('withInsertFragmentTable', () => { - a + + a + - b + + b + @@ -46,12 +58,20 @@ describe('withInsertFragmentTable', () => { - a - 12 + + a + + + 12 + - b - 22 + + b + + + 22 + @@ -79,11 +99,17 @@ describe('withInsertFragmentTable', () => { 11 - 12 + + 12 + - 21 - 22 + + 21 + + + 22 + @@ -93,8 +119,12 @@ describe('withInsertFragmentTable', () => { - a - b + + a + + + b + @@ -104,12 +134,20 @@ describe('withInsertFragmentTable', () => { - a - b + + a + + + b + - 21 - 22 + + 21 + + + 22 + @@ -137,10 +175,14 @@ describe('withInsertFragmentTable', () => { 11 - 12 + + 12 + - 21 + + 21 + 22 @@ -154,8 +196,12 @@ describe('withInsertFragmentTable', () => { - a - b + + a + + + b + @@ -165,15 +211,23 @@ describe('withInsertFragmentTable', () => { - a - b + + a + + + b + - + + + - + + + @@ -199,15 +253,23 @@ describe('withInsertFragmentTable', () => { - 11 - 12 - + 11 + + + + 12 + + - 21 - 22 + + 21 + + + 22 + @@ -217,10 +279,14 @@ describe('withInsertFragmentTable', () => { - a + + a + - b + + b + @@ -230,15 +296,23 @@ describe('withInsertFragmentTable', () => { - 11 - a + 11 + + + + a + - 21 - b + 21 + + + + b + @@ -254,9 +328,6 @@ describe('withInsertFragmentTable', () => { expect(editor.children).toEqual(output.children); - const selection = output.selection!; - selection.anchor.path.pop(); - selection.focus.path.pop(); expect(editor.selection).toEqual(output.selection); }); }); @@ -268,14 +339,22 @@ describe('withInsertFragmentTable', () => { - 11 - 12 + + 11 + + + 12 + - 21 - 22 - + 21 + + + + 22 + + @@ -286,12 +365,20 @@ describe('withInsertFragmentTable', () => { - aa - ab + + aa + + + ab + - ba - bb + + ba + + + bb + @@ -301,28 +388,46 @@ describe('withInsertFragmentTable', () => { - 11 - 12 - + 11 + + + 12 + + + + + - 21 - - aa + 21 + + + + + aa + + + + ab - ab - + + + - ba - - bb + ba + + + + bb + + @@ -342,9 +447,6 @@ describe('withInsertFragmentTable', () => { expect(editor.children).toEqual(output.children); - const selection = output.selection!; - selection.anchor.path.pop(); - selection.focus.path.pop(); expect(editor.selection).toEqual(output.selection); }); }); @@ -355,14 +457,22 @@ describe('withInsertFragmentTable', () => { - 11 - 12 + + 11 + + + 12 + - 21 - 22 - + 21 + + + + 22 + + @@ -373,12 +483,20 @@ describe('withInsertFragmentTable', () => { - aa - ab + + aa + + + ab + - ba - bb + + ba + + + bb + @@ -388,14 +506,23 @@ describe('withInsertFragmentTable', () => { - 11 - 12 + + 11 + + + 12 + - 21 - - aa + 21 + + + + + aa + + @@ -418,9 +545,131 @@ describe('withInsertFragmentTable', () => { expect(editor.children).toEqual(output.children); - const selection = output.selection!; - selection.anchor.path.pop(); expect(editor.selection).toEqual(output.selection); }); }); + + // https://github.com/udecode/editor-protocol/issues/63 + describe('when inserting table cells with multiple p', () => { + it('should paste', () => { + const input = (( + + + + + 11a + + 11b + + + + + 12 + + + + + ) as any) as PlateEditor; + + const fragment = (( + + + + + o11a + o11b + + + o12 + + + + + ) as any) as TElement[]; + + const output = (( + + + + + o11a + o11b + + + o12 + + + + + ) as any) as PlateEditor; + + const editor = createPlateEditor({ + editor: input, + plugins: [createTablePlugin()], + }); + + editor.insertFragment(fragment); + + expect(editor.children).toEqual(output.children); + }); + }); + + // https://github.com/udecode/editor-protocol/issues/64 + describe('when inserting blocks inside a table', () => { + it('should insert the blocks without removing the cells', () => { + const input = (( + + + + + + + 11 + + + + + 12 + + + + + + + ) as any) as PlateEditor; + + const fragment = (( + + o11a + o11b + + ) as any) as TElement[]; + + const output = (( + + + + + o11a + o11b + + + o11a + o11b + + + + + ) as any) as PlateEditor; + + const editor = createPlateEditor({ + editor: input, + plugins: [createTablePlugin()], + }); + + editor.insertFragment(fragment); + + expect(editor.children).toEqual(output.children); + }); + }); }); diff --git a/packages/nodes/table/src/withInsertFragmentTable.ts b/packages/nodes/table/src/withInsertFragmentTable.ts index a39fd225cc..d5dc561bb7 100644 --- a/packages/nodes/table/src/withInsertFragmentTable.ts +++ b/packages/nodes/table/src/withInsertFragmentTable.ts @@ -1,10 +1,11 @@ import { - getBlockAbove, + getEndPoint, getPluginType, + getStartPoint, + getTEditor, hasNode, - insertNodes, PlateEditor, - removeNodes, + replaceNodeChildren, select, TElement, Value, @@ -13,6 +14,7 @@ import { } from '@udecode/plate-core'; import { cloneDeep } from 'lodash'; import { Path } from 'slate'; +import { getTableAbove } from './queries/getTableAbove'; import { getTableGridAbove } from './queries/getTableGridAbove'; import { ELEMENT_TABLE } from './createTablePlugin'; import { TablePlugin } from './types'; @@ -33,22 +35,53 @@ export const withInsertFragmentTable = < const { insertFragment } = editor; const { disableExpandOnInsert, insertColumn, insertRow } = options; - editor.insertFragment = (fragment) => { + const myEditor = getTEditor(editor); + + myEditor.insertFragment = (fragment) => { const insertedTable = fragment.find( (n) => n.type === getPluginType(editor, ELEMENT_TABLE) ); + if (!insertedTable) { + const tableEntry = getTableAbove(editor, { + at: editor.selection?.anchor, + }); + + if (tableEntry) { + const cellEntries = getTableGridAbove(editor, { + format: 'cell', + }); + + if (cellEntries.length > 1) { + cellEntries.forEach((cellEntry) => { + if (cellEntry) { + const [, cellPath] = cellEntry; + + replaceNodeChildren(editor, { + at: cellPath, + nodes: cloneDeep(fragment), + }); + } + }); + + select(editor, { + anchor: getStartPoint(editor, cellEntries[0][1]), + focus: getEndPoint(editor, cellEntries[cellEntries.length - 1][1]), + }); + return; + } + } + } + if (insertedTable) { - const tableEntry = getBlockAbove(editor, { + const tableEntry = getTableAbove(editor, { at: editor.selection?.anchor, - match: { - type: getPluginType(editor, ELEMENT_TABLE), - }, }); // inserting inside table if (tableEntry) { const [cellEntry] = getTableGridAbove(editor, { + at: editor.selection?.anchor, format: 'cell', }); @@ -102,11 +135,9 @@ export const withInsertFragmentTable = < } initCell = false; - removeNodes(editor, { - at: cellPath, - }); - insertNodes(editor, cloneDeep(cell), { + replaceNodeChildren(editor, { at: cellPath, + nodes: cloneDeep(cell.children as any), }); lastCellPath = [...cellPath]; @@ -115,14 +146,8 @@ export const withInsertFragmentTable = < if (lastCellPath) { select(editor, { - anchor: { - offset: 0, - path: startCellPath, - }, - focus: { - offset: 0, - path: lastCellPath, - }, + anchor: getStartPoint(editor, startCellPath), + focus: getEndPoint(editor, lastCellPath), }); } }); diff --git a/packages/nodes/table/src/withInsertTextTable.spec.tsx b/packages/nodes/table/src/withInsertTextTable.spec.tsx new file mode 100644 index 0000000000..fba7e31cf0 --- /dev/null +++ b/packages/nodes/table/src/withInsertTextTable.spec.tsx @@ -0,0 +1,75 @@ +/** @jsx jsx */ + +import { createPlateEditor, PlateEditor } from '@udecode/plate-core'; +import { jsx } from '@udecode/plate-test-utils'; +import { createTablePlugin } from './createTablePlugin'; + +jsx; + +describe('withInsertTextTable', () => { + // https://github.com/udecode/editor-protocol/issues/65 + describe('cell child is a text', () => { + it('should wrap the children into a p', async () => { + const input = (( + + + + + + a + + + + b + + + + + + c + + + + d + + + + + ) as any) as PlateEditor; + + const output = (( + + + + + + + + + + b + + + + + e + + + d + + + + + ) as any) as PlateEditor; + + const editor = createPlateEditor({ + editor: input, + plugins: [createTablePlugin()], + }); + + editor.deleteFragment(); + editor.insertText('e'); + expect(editor.children).toEqual(output.children); + }); + }); +}); diff --git a/packages/nodes/table/src/withInsertTextTable.ts b/packages/nodes/table/src/withInsertTextTable.ts new file mode 100644 index 0000000000..45088ac501 --- /dev/null +++ b/packages/nodes/table/src/withInsertTextTable.ts @@ -0,0 +1,44 @@ +import { + collapseSelection, + isExpanded, + PlateEditor, + Value, + WithPlatePlugin, +} from '@udecode/plate-core'; +import { getTableAbove, getTableGridAbove } from './queries/index'; +import { TablePlugin } from './types'; + +export const withInsertTextTable = < + V extends Value = Value, + E extends PlateEditor = PlateEditor +>( + editor: E, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + plugin: WithPlatePlugin, V, E> +) => { + const { insertText } = editor; + + editor.insertText = (text) => { + if (isExpanded(editor.selection)) { + const entry = getTableAbove(editor, { + at: editor.selection?.anchor, + }); + + if (entry) { + const cellEntries = getTableGridAbove(editor, { + format: 'cell', + }); + + if (cellEntries.length > 1) { + collapseSelection(editor, { + edge: 'focus', + }); + } + } + } + + insertText(text); + }; + + return editor; +}; diff --git a/packages/nodes/table/src/withNormalizeTable.spec.tsx b/packages/nodes/table/src/withNormalizeTable.spec.tsx new file mode 100644 index 0000000000..1e416efc53 --- /dev/null +++ b/packages/nodes/table/src/withNormalizeTable.spec.tsx @@ -0,0 +1,58 @@ +/** @jsx jsx */ + +import { + createPlateEditor, + normalizeEditor, + PlateEditor, +} from '@udecode/plate-core'; +import { jsx } from '@udecode/plate-test-utils'; +import { createTablePlugin } from './createTablePlugin'; + +jsx; + +describe('withNormalizeTable', () => { + // https://github.com/udecode/editor-protocol/issues/65 + describe('cell child is a text', () => { + it('should wrap the children into a p', async () => { + const input = (( + + + + + a + b + c + + + + + ) as any) as PlateEditor; + + const output = (( + + + + + + a + b + c + + + + + + ) as any) as PlateEditor; + + const editor = createPlateEditor({ + editor: input, + plugins: [createTablePlugin()], + }); + + normalizeEditor(editor, { + force: true, + }); + expect(editor.children).toEqual(output.children); + }); + }); +}); diff --git a/packages/nodes/table/src/withNormalizeTable.ts b/packages/nodes/table/src/withNormalizeTable.ts new file mode 100644 index 0000000000..1fc480d8e4 --- /dev/null +++ b/packages/nodes/table/src/withNormalizeTable.ts @@ -0,0 +1,52 @@ +import { + ELEMENT_DEFAULT, + getPluginType, + getTEditor, + isElement, + isText, + PlateEditor, + TElement, + Value, + wrapNodeChildren, +} from '@udecode/plate-core'; +import { getCellTypes } from './utils/index'; + +/** + * Normalize table: + * - Wrap cell children in a paragraph if they are texts. + */ +export const withNormalizeTable = < + V extends Value = Value, + E extends PlateEditor = PlateEditor +>( + editor: E +) => { + const { normalizeNode } = editor; + + const myEditor = getTEditor(editor); + + myEditor.normalizeNode = ([node, path]) => { + if (isElement(node) && getCellTypes(editor).includes(node.type)) { + const { children } = node; + + if (isText(children[0])) { + wrapNodeChildren( + editor, + { + type: getPluginType(editor, ELEMENT_DEFAULT), + children: [{ text: '' }], + }, + { + at: path, + } + ); + + return; + } + } + + return normalizeNode([node, path]); + }; + + return editor; +}; diff --git a/packages/nodes/table/src/withSelectionTable.ts b/packages/nodes/table/src/withSelectionTable.ts new file mode 100644 index 0000000000..09706c4d13 --- /dev/null +++ b/packages/nodes/table/src/withSelectionTable.ts @@ -0,0 +1,74 @@ +import { + getBlockAbove, + getEndPoint, + getPluginType, + getPointBefore, + getStartPoint, + isRangeAcrossBlocks, + PlateEditor, + Value, +} from '@udecode/plate-core'; +import { Range } from 'slate'; +import { ELEMENT_TABLE } from './createTablePlugin'; + +// TODO: tests + +/** + * Selection table: + * - If anchor is in table, focus in a block before: set focus to start of table + * - If anchor is in table, focus in a block after: set focus to end of table + * - If focus is in table, anchor in a block before: set focus to end of table + * - If focus is in table, anchor in a block after: set focus to the point before start of table + */ +export const withSelectionTable = < + V extends Value = Value, + E extends PlateEditor = PlateEditor +>( + editor: E +) => { + const { apply } = editor; + + editor.apply = (op) => { + if (op.type === 'set_selection') { + const selection = { + ...editor.selection, + ...op.newProperties, + } as Range | null; + + if ( + op.newProperties && + Range.isRange(selection) && + isRangeAcrossBlocks(editor, { + at: selection, + match: (n) => n.type === getPluginType(editor, ELEMENT_TABLE), + }) + ) { + const anchorEntry = getBlockAbove(editor, { + at: selection.anchor, + match: (n) => n.type === getPluginType(editor, ELEMENT_TABLE), + }); + + if (!anchorEntry) { + const focusEntry = getBlockAbove(editor, { + at: selection.focus, + match: (n) => n.type === getPluginType(editor, ELEMENT_TABLE), + }); + + if (focusEntry) { + op.newProperties.focus = Range.isBackward(selection) + ? getPointBefore(editor, getStartPoint(editor, focusEntry[1])) + : getEndPoint(editor, focusEntry[1]); + } + } else { + op.newProperties.focus = Range.isBackward(selection) + ? getStartPoint(editor, anchorEntry[1]) + : getEndPoint(editor, anchorEntry[1]); + } + } + } + + apply(op); + }; + + return editor; +}; diff --git a/packages/nodes/table/src/withTable.ts b/packages/nodes/table/src/withTable.ts index 2e46293704..2227724d0e 100644 --- a/packages/nodes/table/src/withTable.ts +++ b/packages/nodes/table/src/withTable.ts @@ -3,6 +3,9 @@ import { TablePlugin } from './types'; import { withDeleteTable } from './withDeleteTable'; import { withGetFragmentTable } from './withGetFragmentTable'; import { withInsertFragmentTable } from './withInsertFragmentTable'; +import { withInsertTextTable } from './withInsertTextTable'; +import { withNormalizeTable } from './withNormalizeTable'; +import { withSelectionTable } from './withSelectionTable'; export const withTable = < V extends Value = Value, @@ -11,9 +14,12 @@ export const withTable = < editor: E, plugin: WithPlatePlugin, V, E> ) => { + editor = withNormalizeTable(editor); editor = withDeleteTable(editor); editor = withGetFragmentTable(editor); editor = withInsertFragmentTable(editor, plugin); + editor = withInsertTextTable(editor, plugin); + editor = withSelectionTable(editor); return editor; }; diff --git a/packages/ui/combobox/src/Combobox.tsx b/packages/ui/combobox/src/Combobox.tsx index dffb3fe645..5b8574cd0c 100644 --- a/packages/ui/combobox/src/Combobox.tsx +++ b/packages/ui/combobox/src/Combobox.tsx @@ -88,11 +88,9 @@ const ComboboxContent = ( ? combobox.getMenuProps({}, { suppressRefError: true }) : { ref: null }; - const { - root, - item: styleItem, - highlightedItem, - } = getComboboxStyles(props as any); + const { root, item: styleItem, highlightedItem } = getComboboxStyles( + props as any + ); return ( diff --git a/packages/ui/dnd/src/components/withDraggable.tsx b/packages/ui/dnd/src/components/withDraggable.tsx index 3d6d87b0f8..dc5a92621e 100644 --- a/packages/ui/dnd/src/components/withDraggable.tsx +++ b/packages/ui/dnd/src/components/withDraggable.tsx @@ -22,7 +22,7 @@ export const withDraggable = ( Component: any, { styles, - level, + level = 0, filter, allowReadOnly = false, onRenderDragHandle, diff --git a/packages/ui/nodes/table/src/TableCellElement/TableCellElement.styles.ts b/packages/ui/nodes/table/src/TableCellElement/TableCellElement.styles.ts index 12b0beabb4..41bceb30a2 100644 --- a/packages/ui/nodes/table/src/TableCellElement/TableCellElement.styles.ts +++ b/packages/ui/nodes/table/src/TableCellElement/TableCellElement.styles.ts @@ -22,7 +22,7 @@ export const getTableCellElementStyles = ( min-width: 48px; `, ], - content: tw`relative px-3 py-2 z-10`, + content: tw`relative px-3 py-2 z-10 h-full box-border`, resizableWrapper: [ tw`absolute w-full h-full top-0`, selected && tw`hidden`, diff --git a/packages/ui/nodes/table/src/TableElement/TableElement.styles.ts b/packages/ui/nodes/table/src/TableElement/TableElement.styles.ts index 3974427dc8..5e1877c37a 100644 --- a/packages/ui/nodes/table/src/TableElement/TableElement.styles.ts +++ b/packages/ui/nodes/table/src/TableElement/TableElement.styles.ts @@ -11,7 +11,7 @@ export const getTableElementStyles = ( { prefixClassNames: 'TableElement', ...props }, { root: [ - tw`table table-fixed w-full my-4 mx-0 border-collapse border border-solid border-gray-300`, + tw`table table-fixed h-px w-full my-4 mx-0 border-collapse border border-solid border-gray-300`, props.isSelectingCell && css` *::selection {