Skip to content

Commit 0b3e2b4

Browse files
authored
fix(document-api): split table cell command (#2217)
* fix(document-api): split table cell command * chore: fix doc-api-stories tests * fix(document-api): split cell logic
1 parent 456f60e commit 0b3e2b4

4 files changed

Lines changed: 667 additions & 98 deletions

File tree

packages/super-editor/src/document-api-adapters/tables-adapter.regressions.test.ts

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
tablesInsertCellAdapter,
1111
tablesSetBorderAdapter,
1212
tablesSetShadingAdapter,
13+
tablesSplitCellAdapter,
1314
tablesSplitAdapter,
1415
} from './tables-adapter.js';
1516

@@ -38,6 +39,11 @@ type NodeOptions = {
3839
nodeSize?: number;
3940
};
4041

42+
type TableEditorOptions = {
43+
firstRowAsHeaders?: boolean;
44+
firstRowBorders?: Record<string, unknown> | null;
45+
};
46+
4147
function createNode(typeName: string, children: ProseMirrorNode[] = [], options: NodeOptions = {}): ProseMirrorNode {
4248
const attrs = options.attrs ?? {};
4349
const text = options.text ?? '';
@@ -122,7 +128,15 @@ function createNode(typeName: string, children: ProseMirrorNode[] = [], options:
122128
return node as unknown as ProseMirrorNode;
123129
}
124130

125-
function makeTableEditor(): Editor {
131+
function makeTableEditor(options: TableEditorOptions = {}): Editor {
132+
const firstRowAsHeaders = options.firstRowAsHeaders ?? false;
133+
const firstRowType = firstRowAsHeaders ? 'tableHeader' : 'tableCell';
134+
const firstRowAttrs =
135+
options.firstRowBorders === undefined
136+
? {}
137+
: {
138+
borders: options.firstRowBorders,
139+
};
126140
const paragraph1 = createNode('paragraph', [createNode('text', [], { text: 'Hello' })], {
127141
attrs: { sdBlockId: 'p1', paraId: 'p1', paragraphProperties: {} },
128142
isBlock: true,
@@ -144,13 +158,13 @@ function makeTableEditor(): Editor {
144158
inlineContent: true,
145159
});
146160

147-
const cell1 = createNode('tableCell', [paragraph1], {
148-
attrs: { sdBlockId: 'cell-1', colspan: 1, rowspan: 1, colwidth: [100] },
161+
const cell1 = createNode(firstRowType, [paragraph1], {
162+
attrs: { sdBlockId: 'cell-1', colspan: 1, rowspan: 1, colwidth: [100], ...firstRowAttrs },
149163
isBlock: true,
150164
inlineContent: false,
151165
});
152-
const cell2 = createNode('tableCell', [paragraph2], {
153-
attrs: { sdBlockId: 'cell-2', colspan: 1, rowspan: 1, colwidth: [200] },
166+
const cell2 = createNode(firstRowType, [paragraph2], {
167+
attrs: { sdBlockId: 'cell-2', colspan: 1, rowspan: 1, colwidth: [200], ...firstRowAttrs },
154168
isBlock: true,
155169
inlineContent: false,
156170
});
@@ -199,6 +213,8 @@ function makeTableEditor(): Editor {
199213
insert: vi.fn().mockReturnThis(),
200214
replaceWith: vi.fn().mockReturnThis(),
201215
setNodeMarkup: vi.fn().mockReturnThis(),
216+
setSelection: vi.fn().mockReturnThis(),
217+
setStoredMarks: vi.fn().mockReturnThis(),
202218
setMeta: vi.fn().mockReturnThis(),
203219
mapping: {
204220
maps: [] as unknown[],
@@ -213,6 +229,7 @@ function makeTableEditor(): Editor {
213229
doc,
214230
tr,
215231
schema: {
232+
text: (text: string) => createNode('text', [], { text }),
216233
nodes: {
217234
paragraph: {
218235
createAndFill: vi.fn((attrs: Record<string, unknown> = {}, content?: unknown) => {
@@ -554,6 +571,75 @@ describe('tables-adapter regressions', () => {
554571
});
555572
});
556573

574+
it('splits a cell by structural row/column expansion without deleting neighboring cells', () => {
575+
const editor = makeTableEditor();
576+
const tr = editor.state.tr as unknown as {
577+
delete: ReturnType<typeof vi.fn>;
578+
insert: ReturnType<typeof vi.fn>;
579+
setNodeMarkup: ReturnType<typeof vi.fn>;
580+
};
581+
582+
const result = tablesSplitCellAdapter(editor, {
583+
nodeId: 'cell-1',
584+
rows: 2,
585+
columns: 2,
586+
});
587+
588+
expect(result.success).toBe(true);
589+
expect(tr.delete).not.toHaveBeenCalled();
590+
expect(tr.insert).toHaveBeenCalled();
591+
expect(getTableGridUpdateAttrs(tr)).toMatchObject({
592+
userEdited: true,
593+
grid: [{ col: 1200 }, { col: 3000 }, { col: 3000 }],
594+
});
595+
});
596+
597+
it('does not copy header-only null borders when split inserts a body row from a header source row', () => {
598+
const editor = makeTableEditor({ firstRowAsHeaders: true, firstRowBorders: null });
599+
const tr = editor.state.tr as unknown as { insert: ReturnType<typeof vi.fn> };
600+
601+
const result = tablesSplitCellAdapter(editor, {
602+
nodeId: 'cell-1',
603+
rows: 2,
604+
columns: 1,
605+
});
606+
607+
expect(result.success).toBe(true);
608+
609+
const insertedRow = tr.insert.mock.calls.find(([, node]) => node?.type?.name === 'tableRow')?.[1] as
610+
| ProseMirrorNode
611+
| undefined;
612+
expect(insertedRow).toBeDefined();
613+
614+
const insertedCells = ((insertedRow as unknown as { _children?: ProseMirrorNode[] })._children ?? []).filter(
615+
(node) => node.type.name === 'tableCell',
616+
);
617+
expect(insertedCells.length).toBeGreaterThan(0);
618+
for (const cell of insertedCells) {
619+
expect((cell.attrs as Record<string, unknown>).borders).toBeUndefined();
620+
}
621+
});
622+
623+
it('preserves non-target rows when split inserts columns by widening adjacent cells', () => {
624+
const editor = makeTableEditor();
625+
const tr = editor.state.tr as unknown as { setNodeMarkup: ReturnType<typeof vi.fn> };
626+
627+
const result = tablesSplitCellAdapter(editor, {
628+
nodeId: 'cell-1',
629+
rows: 1,
630+
columns: 2,
631+
});
632+
633+
expect(result.success).toBe(true);
634+
expect(tr.setNodeMarkup).toHaveBeenCalledWith(
635+
expect.any(Number),
636+
null,
637+
expect.objectContaining({
638+
colspan: 2,
639+
}),
640+
);
641+
});
642+
557643
it('rejects paragraph targets for tables.setBorder', () => {
558644
const editor = makeTableEditor();
559645
const result = tablesSetBorderAdapter(editor, {

0 commit comments

Comments
 (0)