Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
tablesInsertCellAdapter,
tablesSetBorderAdapter,
tablesSetShadingAdapter,
tablesSplitCellAdapter,
tablesSplitAdapter,
} from './tables-adapter.js';

Expand Down Expand Up @@ -38,6 +39,11 @@ type NodeOptions = {
nodeSize?: number;
};

type TableEditorOptions = {
firstRowAsHeaders?: boolean;
firstRowBorders?: Record<string, unknown> | null;
};

function createNode(typeName: string, children: ProseMirrorNode[] = [], options: NodeOptions = {}): ProseMirrorNode {
const attrs = options.attrs ?? {};
const text = options.text ?? '';
Expand Down Expand Up @@ -122,7 +128,15 @@ function createNode(typeName: string, children: ProseMirrorNode[] = [], options:
return node as unknown as ProseMirrorNode;
}

function makeTableEditor(): Editor {
function makeTableEditor(options: TableEditorOptions = {}): Editor {
const firstRowAsHeaders = options.firstRowAsHeaders ?? false;
const firstRowType = firstRowAsHeaders ? 'tableHeader' : 'tableCell';
const firstRowAttrs =
options.firstRowBorders === undefined
? {}
: {
borders: options.firstRowBorders,
};
const paragraph1 = createNode('paragraph', [createNode('text', [], { text: 'Hello' })], {
attrs: { sdBlockId: 'p1', paraId: 'p1', paragraphProperties: {} },
isBlock: true,
Expand All @@ -144,13 +158,13 @@ function makeTableEditor(): Editor {
inlineContent: true,
});

const cell1 = createNode('tableCell', [paragraph1], {
attrs: { sdBlockId: 'cell-1', colspan: 1, rowspan: 1, colwidth: [100] },
const cell1 = createNode(firstRowType, [paragraph1], {
attrs: { sdBlockId: 'cell-1', colspan: 1, rowspan: 1, colwidth: [100], ...firstRowAttrs },
isBlock: true,
inlineContent: false,
});
const cell2 = createNode('tableCell', [paragraph2], {
attrs: { sdBlockId: 'cell-2', colspan: 1, rowspan: 1, colwidth: [200] },
const cell2 = createNode(firstRowType, [paragraph2], {
attrs: { sdBlockId: 'cell-2', colspan: 1, rowspan: 1, colwidth: [200], ...firstRowAttrs },
isBlock: true,
inlineContent: false,
});
Expand Down Expand Up @@ -199,6 +213,8 @@ function makeTableEditor(): Editor {
insert: vi.fn().mockReturnThis(),
replaceWith: vi.fn().mockReturnThis(),
setNodeMarkup: vi.fn().mockReturnThis(),
setSelection: vi.fn().mockReturnThis(),
setStoredMarks: vi.fn().mockReturnThis(),
setMeta: vi.fn().mockReturnThis(),
mapping: {
maps: [] as unknown[],
Expand All @@ -213,6 +229,7 @@ function makeTableEditor(): Editor {
doc,
tr,
schema: {
text: (text: string) => createNode('text', [], { text }),
nodes: {
paragraph: {
createAndFill: vi.fn((attrs: Record<string, unknown> = {}, content?: unknown) => {
Expand Down Expand Up @@ -554,6 +571,75 @@ describe('tables-adapter regressions', () => {
});
});

it('splits a cell by structural row/column expansion without deleting neighboring cells', () => {
const editor = makeTableEditor();
const tr = editor.state.tr as unknown as {
delete: ReturnType<typeof vi.fn>;
insert: ReturnType<typeof vi.fn>;
setNodeMarkup: ReturnType<typeof vi.fn>;
};

const result = tablesSplitCellAdapter(editor, {
nodeId: 'cell-1',
rows: 2,
columns: 2,
});

expect(result.success).toBe(true);
expect(tr.delete).not.toHaveBeenCalled();
expect(tr.insert).toHaveBeenCalled();
expect(getTableGridUpdateAttrs(tr)).toMatchObject({
userEdited: true,
grid: [{ col: 1200 }, { col: 3000 }, { col: 3000 }],
});
});

it('does not copy header-only null borders when split inserts a body row from a header source row', () => {
const editor = makeTableEditor({ firstRowAsHeaders: true, firstRowBorders: null });
const tr = editor.state.tr as unknown as { insert: ReturnType<typeof vi.fn> };

const result = tablesSplitCellAdapter(editor, {
nodeId: 'cell-1',
rows: 2,
columns: 1,
});

expect(result.success).toBe(true);

const insertedRow = tr.insert.mock.calls.find(([, node]) => node?.type?.name === 'tableRow')?.[1] as
| ProseMirrorNode
| undefined;
expect(insertedRow).toBeDefined();

const insertedCells = ((insertedRow as unknown as { _children?: ProseMirrorNode[] })._children ?? []).filter(
(node) => node.type.name === 'tableCell',
);
expect(insertedCells.length).toBeGreaterThan(0);
for (const cell of insertedCells) {
expect((cell.attrs as Record<string, unknown>).borders).toBeUndefined();
}
});

it('preserves non-target rows when split inserts columns by widening adjacent cells', () => {
const editor = makeTableEditor();
const tr = editor.state.tr as unknown as { setNodeMarkup: ReturnType<typeof vi.fn> };

const result = tablesSplitCellAdapter(editor, {
nodeId: 'cell-1',
rows: 1,
columns: 2,
});

expect(result.success).toBe(true);
expect(tr.setNodeMarkup).toHaveBeenCalledWith(
expect.any(Number),
null,
expect.objectContaining({
colspan: 2,
}),
);
});

it('rejects paragraph targets for tables.setBorder', () => {
const editor = makeTableEditor();
const result = tablesSetBorderAdapter(editor, {
Expand Down
Loading
Loading