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 @@ -7,6 +7,7 @@ import {
tablesClearShadingAdapter,
tablesDeleteCellAdapter,
tablesDistributeColumnsAdapter,
tablesInsertColumnAdapter,
tablesInsertCellAdapter,
tablesSetBorderAdapter,
tablesSetShadingAdapter,
Expand Down Expand Up @@ -42,8 +43,11 @@ type NodeOptions = {
type TableEditorOptions = {
firstRowAsHeaders?: boolean;
firstRowBorders?: Record<string, unknown> | null;
lastColumnAsHeaders?: boolean;
};

const mockSchema: { nodes: Record<string, unknown> } = { nodes: {} };

function createNode(typeName: string, children: ProseMirrorNode[] = [], options: NodeOptions = {}): ProseMirrorNode {
const attrs = options.attrs ?? {};
const text = options.text ?? '';
Expand All @@ -59,6 +63,7 @@ function createNode(typeName: string, children: ProseMirrorNode[] = [], options:
const node = {
type: {
name: typeName,
schema: mockSchema,
create(newAttrs: Record<string, unknown>) {
return createNode(typeName, [], { attrs: newAttrs, isBlock, inlineContent });
},
Expand Down Expand Up @@ -125,12 +130,17 @@ function createNode(typeName: string, children: ProseMirrorNode[] = [], options:
},
};

mockSchema.nodes[typeName] = node.type;

return node as unknown as ProseMirrorNode;
}

function makeTableEditor(options: TableEditorOptions = {}): Editor {
const firstRowAsHeaders = options.firstRowAsHeaders ?? false;
const lastColumnAsHeaders = options.lastColumnAsHeaders ?? false;
const firstRowType = firstRowAsHeaders ? 'tableHeader' : 'tableCell';
const secondColumnType = lastColumnAsHeaders ? 'tableHeader' : firstRowType;
const lastCellType = lastColumnAsHeaders ? 'tableHeader' : 'tableCell';
const firstRowAttrs =
options.firstRowBorders === undefined
? {}
Expand Down Expand Up @@ -163,7 +173,7 @@ function makeTableEditor(options: TableEditorOptions = {}): Editor {
isBlock: true,
inlineContent: false,
});
const cell2 = createNode(firstRowType, [paragraph2], {
const cell2 = createNode(secondColumnType, [paragraph2], {
attrs: { sdBlockId: 'cell-2', colspan: 1, rowspan: 1, colwidth: [200], ...firstRowAttrs },
isBlock: true,
inlineContent: false,
Expand All @@ -173,7 +183,7 @@ function makeTableEditor(options: TableEditorOptions = {}): Editor {
isBlock: true,
inlineContent: false,
});
const cell4 = createNode('tableCell', [paragraph4], {
const cell4 = createNode(lastCellType, [paragraph4], {
attrs: { sdBlockId: 'cell-4', colspan: 1, rowspan: 1, colwidth: [200] },
isBlock: true,
inlineContent: false,
Expand Down Expand Up @@ -383,6 +393,37 @@ describe('tables-adapter regressions', () => {
expect(insertedTable.type.name).toBe('table');
});

it('SD-2127: inserts a new cell in every row when appending a column to the right of the last column', () => {
const editor = makeTableEditor();
const tr = editor.state.tr as unknown as { insert: ReturnType<typeof vi.fn> };

const result = tablesInsertColumnAdapter(
editor,
{ tableNodeId: 'table-1', columnIndex: 1, position: 'right' },
{ changeMode: 'direct' },
);

expect(result.success).toBe(true);
expect(tr.insert).toHaveBeenCalledTimes(2);
});

it('SD-2127: appending right of a header edge inserts body cells, not cloned header cells', () => {
const editor = makeTableEditor({ lastColumnAsHeaders: true });
const tr = editor.state.tr as unknown as { insert: ReturnType<typeof vi.fn> };

const result = tablesInsertColumnAdapter(
editor,
{ tableNodeId: 'table-1', columnIndex: 1, position: 'right' },
{ changeMode: 'direct' },
);

expect(result.success).toBe(true);
expect(tr.insert).toHaveBeenCalledTimes(2);

const insertedTypeNames = tr.insert.mock.calls.map(([, node]) => (node as ProseMirrorNode).type.name);
expect(insertedTypeNames).toEqual(['tableCell', 'tableCell']);
});

it('deletes shiftLeft cells without appending a trailing replacement cell', () => {
const editor = makeTableEditor();
const tr = editor.state.tr as unknown as {
Expand Down
41 changes: 34 additions & 7 deletions packages/super-editor/src/document-api-adapters/tables-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Node as ProseMirrorNode, NodeType } from 'prosemirror-model';
import type { Editor } from '../core/Editor.js';
import { v4 as uuidv4 } from 'uuid';
import type {
Expand Down Expand Up @@ -541,6 +542,33 @@ function addColSpan(attrs: Record<string, unknown>, pos: number, n = 1): Record<
return result;
}

function isHeaderColumn(tableNode: ProseMirrorNode, map: ReturnType<(typeof TableMap)['get']>, col: number): boolean {
for (let row = 0; row < map.height; row++) {
const cell = tableNode.nodeAt(map.map[col + row * map.width]);
if (!cell || cell.type.name !== 'tableHeader') return false;
}
return true;
}

function resolveInsertedColumnCellType(
tableNode: ProseMirrorNode,
map: ReturnType<(typeof TableMap)['get']>,
index: number,
col: number,
): NodeType | null {
let refColumn: number | null = col > 0 ? -1 : 0;
if (isHeaderColumn(tableNode, map, col + refColumn)) {
refColumn = col === 0 || col === map.width ? null : 0;
}

if (refColumn == null) {
return tableNode.type.schema.nodes.tableCell ?? null;
}

const refPos = map.map[index + refColumn];
return refPos != null ? (tableNode.nodeAt(refPos)?.type ?? null) : null;
}

/** Inserts a column at `col` in the table (before that column index). Follows prosemirror-tables addColumn pattern. */
function addColumnToTable(tr: Transaction, tablePos: number, col: number): void {
const tableNode = tr.doc.nodeAt(tablePos);
Expand All @@ -551,12 +579,11 @@ function addColumnToTable(tr: Transaction, tablePos: number, col: number): void

for (let row = 0; row < map.height; row++) {
const index = row * map.width + col;
const pos = map.map[index];
const cell = tableNode.nodeAt(pos);
if (!cell) continue;

if (col > 0 && map.map[index - 1] === pos) {
if (col > 0 && col < map.width && map.map[index - 1] === map.map[index]) {
// Cell spans from the left — expand colspan
const pos = map.map[index];
const cell = tableNode.nodeAt(pos);
if (!cell) continue;
tr.setNodeMarkup(
tr.mapping.slice(mapStart).map(tableStart + pos),
null,
Expand All @@ -565,10 +592,10 @@ function addColumnToTable(tr: Transaction, tablePos: number, col: number): void
row += (((cell.attrs as Record<string, unknown>).rowspan as number) || 1) - 1;
} else {
// Insert a new empty cell
const refType = col > 0 ? (tableNode.nodeAt(map.map[index - 1])?.type ?? cell.type) : cell.type;
const refType = resolveInsertedColumnCellType(tableNode, map, index, col);
if (!refType) continue;
const cellPos = map.positionAt(row, col, tableNode);
tr.insert(tr.mapping.slice(mapStart).map(tableStart + cellPos), refType.createAndFill()!);
row += ((cell.attrs?.rowspan as number) || 1) - 1;
}
}
}
Expand Down
Loading