Skip to content

Commit 65e4327

Browse files
authored
feat: support colspan and rowspan in table actions (#356)
1 parent a3d7538 commit 65e4327

File tree

7 files changed

+340
-147
lines changed

7 files changed

+340
-147
lines changed

src/extensions/yfm/YfmTable/plugins/YfmTableControls/actions.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
import {Node} from 'prosemirror-model';
22

33
import {CommandWithAttrs} from '../../../../../core';
4-
import {
5-
addColumnAfter,
6-
addRowAfter,
7-
appendColumn,
8-
appendRow,
9-
removeColumn,
10-
removeRow,
11-
} from '../../../../../table-utils';
4+
import {appendColumn, appendRow, removeColumn, removeRow} from '../../../../../table-utils';
125
import {defineActions} from '../../../../../utils/actions';
136
import {removeNode} from '../../../../../utils/remove-node';
147

@@ -36,18 +29,10 @@ const removeYfmTable: CommandWithAttrs<{
3629
return true;
3730
};
3831
export const controlActions = defineActions({
39-
addRow: {
40-
isEnable: addRowAfter,
41-
run: addRowAfter,
42-
},
4332
deleteRow: {
4433
isEnable: removeRow,
4534
run: removeRow,
4635
},
47-
addColumn: {
48-
isEnable: addColumnAfter,
49-
run: addColumnAfter,
50-
},
5136
deleteColumn: {
5237
isEnable: removeColumn,
5338
run: removeColumn,
Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,70 @@
1-
import {findChildren, findParentNodeClosestToPos} from 'prosemirror-utils';
1+
import {findParentNodeClosestToPos} from 'prosemirror-utils';
22

3-
import {isTableNode, isTableRowNode} from '..';
3+
import {isTableNode} from '..';
44
import type {CommandWithAttrs} from '../../core';
5-
import {findChildIndex} from '../helpers';
6-
import {findChildTableCells, isTableBodyNode, isTableCellNode} from '../utils';
5+
import {CellPos, TableDesc} from '../table-desc';
76

87
export const appendColumn: CommandWithAttrs<{
98
tablePos: number;
109
columnNumber?: number;
1110
direction?: 'before' | 'after';
11+
// eslint-disable-next-line complexity
1212
}> = (state, dispatch, _, attrs) => {
1313
if (!attrs) return false;
14-
const {tablePos, columnNumber, direction} = attrs;
15-
const parentTable = findParentNodeClosestToPos(state.doc.resolve(tablePos + 1), isTableNode)
16-
?.node;
1714

18-
if (!parentTable) return false;
19-
let parentCell;
20-
let parentRow;
15+
const {tablePos, columnNumber, direction = 'after'} = attrs;
16+
const res = findParentNodeClosestToPos(state.doc.resolve(tablePos + 1), isTableNode);
17+
if (!res) return false;
2118

22-
const tableBody = findChildren(parentTable, isTableBodyNode, false).pop();
23-
if (!tableBody) return false;
19+
const tableNode = res.node;
20+
const tableDesc = TableDesc.create(tableNode);
21+
if (!tableDesc) return false;
2422

25-
if (columnNumber !== undefined) {
26-
parentCell = findChildTableCells(parentTable)[columnNumber];
27-
parentRow = findParentNodeClosestToPos(
28-
state.doc.resolve(tablePos + parentCell.pos + 1),
29-
isTableRowNode,
30-
);
31-
} else {
32-
parentRow = findChildren(tableBody.node, isTableRowNode, false).pop();
33-
if (!parentRow) return false;
23+
const columnIndex = columnNumber ?? tableDesc.cols - 1; // if columnNumber is not defined, that means last row
24+
const isFirstColumn = columnIndex === 0;
25+
const isLastColumn = columnIndex === tableDesc.cols - 1;
3426

35-
parentCell = findChildren(parentRow.node, isTableCellNode, false).pop();
36-
}
37-
38-
if (!parentCell || !parentRow || !parentTable) {
39-
return false;
40-
}
27+
let pos: number[] | null = null;
28+
if (isFirstColumn && direction === 'before')
29+
pos = tableDesc.getRelativePosForColumn(0).map(fromOrClosest);
30+
if (isLastColumn && direction === 'after')
31+
pos = tableDesc.getRelativePosForColumn(tableDesc.cols - 1).map(toOrClosest);
4132

42-
const parentCellIndex = columnNumber || findChildIndex(parentRow.node, parentCell.node);
33+
if (!pos) {
34+
if (tableDesc.cols <= columnIndex) return false;
4335

44-
if (parentCellIndex < 0) {
45-
return false;
36+
if (tableDesc.isSafeColumn(columnIndex)) {
37+
const columnPos = tableDesc.getRelativePosForColumn(columnIndex);
38+
if (direction === 'before') pos = columnPos.map(fromOrClosest);
39+
if (direction === 'after') pos = columnPos.map(toOrClosest);
40+
} else {
41+
if (direction === 'before' && tableDesc.isSafeColumn(columnIndex - 1))
42+
pos = tableDesc.getRelativePosForColumn(columnIndex - 1).map(toOrClosest);
43+
if (direction === 'after' && tableDesc.isSafeColumn(columnIndex + 1))
44+
pos = tableDesc.getRelativePosForColumn(columnIndex + 1).map(fromOrClosest);
45+
}
4646
}
4747

48-
if (dispatch) {
49-
const allRows = findChildren(tableBody.node, isTableRowNode, false);
48+
if (!pos) return false;
5049

51-
let tr = state.tr;
52-
for (const row of allRows) {
53-
const rowCells = findChildren(row.node, isTableCellNode, false);
54-
const cell = rowCells[parentCellIndex];
55-
56-
let position = tablePos + row.pos + cell.pos + 3;
57-
position += direction === 'before' ? 0 : cell.node.nodeSize;
50+
if (dispatch) {
51+
const cellType = tableDesc.getCellNodeType();
52+
const {tr} = state;
5853

59-
tr = tr.insert(
60-
tr.mapping.map(position),
61-
cell.node.type.createAndFill(cell.node.attrs)!,
62-
);
54+
for (const p of pos) {
55+
tr.insert(tr.mapping.map(res.pos + p), cellType.createAndFill()!);
6356
}
6457

65-
dispatch(tr.scrollIntoView());
58+
dispatch(tr);
6659
}
6760

6861
return true;
6962
};
63+
64+
function fromOrClosest(pos: CellPos): number {
65+
return pos.type === 'real' ? pos.from : pos.closestPos;
66+
}
67+
68+
function toOrClosest(pos: CellPos): number {
69+
return pos.type === 'real' ? pos.to : pos.closestPos;
70+
}
Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,62 @@
1-
import {Fragment, Node} from 'prosemirror-model';
2-
import {findChildren, findParentNodeClosestToPos} from 'prosemirror-utils';
1+
import {Node, NodeType} from 'prosemirror-model';
2+
import {findParentNodeClosestToPos} from 'prosemirror-utils';
33

4-
import {findChildTableRows, isTableBodyNode, isTableNode, isTableRowNode} from '..';
4+
import {isTableNode} from '..';
55
import type {CommandWithAttrs} from '../../core';
6+
import {TableDesc} from '../table-desc';
67

78
export const appendRow: CommandWithAttrs<{
89
tablePos: number;
910
rowNumber?: number;
1011
direction?: 'before' | 'after';
12+
// eslint-disable-next-line complexity
1113
}> = (state, dispatch, _, attrs) => {
1214
if (!attrs) return false;
13-
const {tablePos, rowNumber, direction} = attrs;
1415

15-
const tableNode = findParentNodeClosestToPos(state.doc.resolve(tablePos + 1), isTableNode)
16-
?.node;
17-
18-
if (!tableNode) return false;
19-
20-
const parentBody = findChildren(tableNode, isTableBodyNode, false).pop();
21-
if (!parentBody) return false;
22-
23-
let parentRow;
24-
if (rowNumber !== undefined) {
25-
parentRow = findChildTableRows(tableNode)[rowNumber];
26-
} else {
27-
parentRow = findChildren(parentBody.node, isTableRowNode, false).pop();
16+
const {tablePos, rowNumber, direction = 'after'} = attrs;
17+
const res = findParentNodeClosestToPos(state.doc.resolve(tablePos + 1), isTableNode);
18+
if (!res) return false;
19+
20+
const tableNode = res.node;
21+
const tableDesc = TableDesc.create(tableNode);
22+
if (!tableDesc) return false;
23+
24+
const rowIndex = rowNumber ?? tableDesc.rows - 1; // if rowNumber is not defined, that means last row
25+
const isFirstRow = rowIndex === 0;
26+
const isLastRow = rowIndex === tableDesc.rows - 1;
27+
28+
let pos = -1;
29+
if (isFirstRow && direction === 'before') pos = tableDesc.getRelativePosForRow(0).from;
30+
if (isLastRow && direction === 'after')
31+
pos = tableDesc.getRelativePosForRow(tableDesc.rows - 1).to;
32+
33+
if (pos === -1) {
34+
if (tableDesc.rows <= rowIndex) return false;
35+
36+
if (tableDesc.isSafeRow(rowIndex)) {
37+
const rowPos = tableDesc.getRelativePosForRow(rowIndex);
38+
if (direction === 'before') pos = rowPos.from;
39+
if (direction === 'after') pos = rowPos.to;
40+
} else {
41+
if (direction === 'before' && tableDesc.isSafeRow(rowIndex - 1))
42+
pos = tableDesc.getRelativePosForRow(rowIndex - 1).to;
43+
if (direction === 'after' && tableDesc.isSafeRow(rowIndex + 1))
44+
pos = tableDesc.getRelativePosForRow(rowIndex + 1).from;
45+
}
2846
}
2947

30-
if (!parentRow) {
31-
return false;
32-
}
48+
if (pos === -1) return false;
3349

3450
if (dispatch) {
35-
const newCellNodes: Node[] = [];
36-
parentRow.node.forEach((node) => {
37-
newCellNodes.push(node.type.createAndFill(node.attrs)!);
38-
});
39-
40-
let position = tablePos + parentRow.pos;
41-
position += direction === 'before' ? 1 : parentRow.node.nodeSize + 1;
42-
43-
dispatch(state.tr.insert(position, parentRow.node.copy(Fragment.from(newCellNodes))));
51+
const cells = getNodes(tableDesc.getCellNodeType(), tableDesc.cols);
52+
dispatch(state.tr.insert(res.pos + pos, tableDesc.getRowNodeType().create(null, cells)));
4453
}
4554

4655
return true;
4756
};
57+
58+
function getNodes(type: NodeType, count: number) {
59+
const nodes: Node[] = [];
60+
for (let i = 0; i < count; i++) nodes.push(type.createAndFill()!);
61+
return nodes;
62+
}

src/table-utils/commands/removeColumn.ts

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {Command} from 'prosemirror-state';
22

3-
import {isTableNode} from '..';
4-
import {isTableBodyNode, isTableCellNode, isTableRowNode} from '../utils';
3+
import {TableDesc} from '../table-desc';
4+
import {isTableNode} from '../utils';
55

66
export const removeColumn: Command = (
77
state,
@@ -18,30 +18,29 @@ export const removeColumn: Command = (
1818
const tableNode = state.doc.nodeAt(tablePos);
1919
if (!tableNode || tableNode.nodeSize <= 2 || !isTableNode(tableNode)) return false;
2020

21-
const tableBodyNode = tableNode.firstChild;
22-
if (!tableBodyNode || tableBodyNode.nodeSize <= 2 || !isTableBodyNode(tableBodyNode))
23-
return false;
21+
const tableDesc = TableDesc.create(tableNode);
22+
if (!tableDesc) return false;
2423

25-
// there is one column left
26-
if (tableBodyNode.firstChild && tableBodyNode.firstChild.childCount < 2) return false;
24+
if (
25+
!tableDesc ||
26+
// there is one column left
27+
tableDesc.cols < 2 ||
28+
tableDesc.cols <= columnNumber ||
29+
!tableDesc.isSafeColumn(columnNumber)
30+
)
31+
return false;
2732

2833
if (dispatch) {
2934
const {tr} = state;
30-
31-
tableBodyNode.forEach((rowNode, rowOffset) => {
32-
if (!isTableRowNode(rowNode)) return;
33-
34-
rowNode.forEach((cellNode, cellOffset, cellIndex) => {
35-
if (!isTableCellNode(cellNode)) return;
36-
37-
if (cellIndex === columnNumber) {
38-
// table -> tbody -> tr -> td
39-
const from = tablePos + 2 + rowOffset + 1 + cellOffset;
40-
const to = from + cellNode.nodeSize;
41-
tr.delete(tr.mapping.map(from), tr.mapping.map(to));
42-
}
43-
});
44-
});
35+
const pos = tableDesc.getRelativePosForColumn(columnNumber);
36+
for (const item of pos) {
37+
if (item.type === 'real') {
38+
let {from, to} = item;
39+
from += tablePos;
40+
to += tablePos;
41+
tr.delete(tr.mapping.map(from), tr.mapping.map(to));
42+
}
43+
}
4544

4645
dispatch(tr);
4746
}
Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {Fragment} from 'prosemirror-model';
22
import type {Command} from 'prosemirror-state';
3-
import {findChildren, findParentNodeClosestToPos} from 'prosemirror-utils';
3+
import {findParentNodeClosestToPos} from 'prosemirror-utils';
44

5-
import {findChildTableBody, isTableNode, isTableRowNode} from '..';
5+
import {isTableNode} from '..';
66
import {trackTransactionMetrics} from '../../core';
7-
import {findChildTableRows} from '../utils';
7+
import {TableDesc} from '../table-desc';
88

99
export const removeRow: Command = (
1010
state,
@@ -20,31 +20,22 @@ export const removeRow: Command = (
2020
?.node;
2121

2222
if (!tableNode) return false;
23-
const parentRows = findChildren(tableNode, isTableRowNode);
2423

25-
const parentBody = findChildTableBody(tableNode)[0];
26-
const parentRow = parentRows[rowNumber];
27-
28-
if (!parentRows.length || !parentBody) {
29-
return false;
30-
}
31-
32-
if (findChildTableRows(parentBody.node).length < 2) {
33-
// there is one row left
34-
return false;
24+
const tableDesc = TableDesc.create(tableNode);
25+
if (!tableDesc || rowNumber >= tableDesc.rows) return false;
26+
if (!tableDesc.isSafeRow(rowNumber)) return false;
27+
28+
if (dispatch) {
29+
let {from, to} = tableDesc.getRelativePosForRow(rowNumber);
30+
from += tablePos;
31+
to += tablePos;
32+
dispatch(
33+
trackTransactionMetrics(state.tr.replaceWith(from, to, Fragment.empty), 'removeRow', {
34+
rows: tableDesc.rows,
35+
cols: tableDesc.cols,
36+
}),
37+
);
3538
}
3639

37-
dispatch?.(
38-
trackTransactionMetrics(
39-
state.tr.replaceWith(
40-
tablePos + parentRow.pos,
41-
tablePos + parentRow.pos + parentRow.node.nodeSize + 1,
42-
Fragment.empty,
43-
),
44-
'removeRow',
45-
{rows: parentRows.length, cols: parentRow.node.childCount},
46-
),
47-
);
48-
4940
return true;
5041
};

0 commit comments

Comments
 (0)