Skip to content

Commit 776de5c

Browse files
authored
fix(YfmTable): fix clearing table cells when pressing backspace (#866)
1 parent 1635df8 commit 776de5c

File tree

2 files changed

+85
-112
lines changed

2 files changed

+85
-112
lines changed

src/extensions/yfm/YfmTable/commands/backspace.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe('YfmTable commands', () => {
8989
);
9090
});
9191

92-
it('should delete selected row', () => {
92+
it('should clear cells in selected row', () => {
9393
const state = EditorState.create({
9494
schema,
9595
doc: templateDoc,
@@ -104,14 +104,15 @@ describe('YfmTable commands', () => {
104104
t(
105105
tb(
106106
trow(td(p('qwe')), td(p('rty')), td(p('uio'))),
107+
trow(td(p()), td(p()), td(p())),
107108
trow(td(p('zxc')), td(p('vbn')), td(p('m<>'))),
108109
),
109110
),
110111
),
111112
);
112113
});
113114

114-
it('should clear cells and delete row between', () => {
115+
it('should clear selected cells (selection covers intermediate trow)', () => {
115116
const state = EditorState.create({
116117
schema,
117118
doc: templateDoc,
@@ -126,14 +127,15 @@ describe('YfmTable commands', () => {
126127
t(
127128
tb(
128129
trow(td(p('qwe')), td(p('rty')), td(p(''))),
130+
trow(td(p()), td(p()), td(p())),
129131
trow(td(p('')), td(p('vbn')), td(p('m<>'))),
130132
),
131133
),
132134
),
133135
);
134136
});
135137

136-
it('should clear selected cells in first row and delete other rows', () => {
138+
it('should clear selected cells in first row and clear other rows', () => {
137139
const state = EditorState.create({
138140
schema,
139141
doc: templateDoc,
@@ -143,7 +145,17 @@ describe('YfmTable commands', () => {
143145
const {res, tr} = applyCommand(state, clearSelectedCells);
144146

145147
expect(res).toBe(true);
146-
expect(tr.doc).toMatchNode(doc(t(tb(trow(td(p('qwe')), td(p('')), td(p('')))))));
148+
expect(tr.doc).toMatchNode(
149+
doc(
150+
t(
151+
tb(
152+
trow(td(p('qwe')), td(p('')), td(p(''))),
153+
trow(td(p()), td(p()), td(p())),
154+
trow(td(p()), td(p()), td(p())),
155+
),
156+
),
157+
),
158+
);
147159
});
148160

149161
it('should delete table', () => {
Lines changed: 69 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
1-
import {chainCommands} from 'prosemirror-commands';
2-
import type {Node} from 'prosemirror-model';
3-
import {type Command, TextSelection} from 'prosemirror-state';
4-
5-
import {
6-
findChildTableCells,
7-
findChildTableRows,
8-
findParentTableBodyFromPos,
9-
findParentTableCellFromPos,
10-
findParentTableFromPos,
11-
isTableCellNode,
12-
isTableRowNode,
13-
} from '../../../../table-utils';
14-
import {isNodeSelection, isTextSelection} from '../../../../utils/selection';
1+
import {chainCommands} from '#pm/commands';
2+
import type {Node, NodeType, ResolvedPos} from '#pm/model';
3+
import {type Command, TextSelection} from '#pm/state';
4+
import {pType} from 'src/extensions/base/specs';
5+
import {range} from 'src/lodash';
6+
import {isTableCellNode, isTableNode, isTableRowNode} from 'src/table-utils';
7+
import {TableDesc} from 'src/table-utils/table-desc';
8+
import {isNodeSelection, isTextSelection} from 'src/utils/selection';
9+
10+
import {yfmTableBodyType, yfmTableRowType, yfmTableType} from '../utils';
1511

1612
const removeCellNodeContent: Command = (state, dispatch) => {
1713
const sel = state.selection;
@@ -36,114 +32,79 @@ const removeCellNodeContent: Command = (state, dispatch) => {
3632
return false;
3733
};
3834

39-
// eslint-disable-next-line complexity
4035
export const clearSelectedCells: Command = (state, dispatch) => {
4136
const sel = state.selection;
4237
if (!isTextSelection(sel)) return false;
4338
const {$from, $to} = sel;
4439

45-
const fromCell = findParentTableCellFromPos($from);
46-
const toCell = findParentTableCellFromPos($to);
47-
const fromTBody = findParentTableBodyFromPos($from);
48-
const toTBody = findParentTableBodyFromPos($to);
49-
const fromTable = findParentTableFromPos($to);
50-
51-
if (!fromCell || !toCell || !fromTBody || !toTBody || !fromTable) return false;
52-
53-
if (fromCell.node === toCell.node) {
54-
// selection inside table cell
55-
return false; // should executes default command
56-
}
57-
58-
if (fromTBody && toTBody && fromTBody.pos === toTBody.pos) {
59-
if (dispatch) {
60-
const table = fromTable;
61-
const tBody = fromTBody;
62-
63-
const fromCellIndexInRow = $from.index(fromCell.depth - 1);
64-
const toCellIndexInRow = $to.index(toCell.depth - 1);
65-
66-
const fromRowIndexInBody = $from.index(fromCell.depth - 2);
67-
const toRowIndexInBody = $to.index(toCell.depth - 2);
68-
69-
const bodyRows = findChildTableRows(tBody.node);
70-
71-
let tr = state.tr;
72-
tr = tr.delete(toCell.start, $to.pos);
73-
74-
let rowIndex = toRowIndexInBody;
75-
while (rowIndex >= fromRowIndexInBody) {
76-
const row = bodyRows[rowIndex];
77-
const rowCells = findChildTableCells(row.node);
78-
79-
let cellIndex =
80-
rowIndex === toRowIndexInBody ? toCellIndexInRow - 1 : rowCells.length - 1;
81-
while (cellIndex > (rowIndex === fromRowIndexInBody ? fromCellIndexInRow : -1)) {
82-
const cell = rowCells[cellIndex];
83-
84-
const from = tBody.pos + row.pos + cell.pos + 3;
85-
const to = from + cell.node.nodeSize - 2;
86-
87-
tr = tr.delete(from, to);
88-
89-
cellIndex--;
90-
}
91-
92-
if (rowIndex !== fromRowIndexInBody) {
93-
const rowPos = tBody.pos + row.pos + 1;
94-
const trRow = tr.doc.nodeAt(rowPos);
95-
if (trRow && isEmptyTableRow(trRow)) {
96-
tr = tr.delete(rowPos, rowPos + trRow.nodeSize);
97-
}
98-
}
99-
100-
rowIndex--;
101-
}
102-
103-
tr = tr.delete($from.pos, fromCell.pos + fromCell.node.nodeSize);
104-
105-
const fromRowPos = tBody.pos + bodyRows[fromRowIndexInBody].pos + 1;
106-
const trFromRow = tr.doc.nodeAt(fromRowPos);
107-
if (fromCellIndexInRow === 0 && trFromRow && isEmptyTableRow(trFromRow)) {
108-
const rowsCountBeforeDelete = tr.doc.nodeAt(tBody.pos)!.childCount;
109-
tr = tr.delete(fromRowPos, fromRowPos + tr.doc.nodeAt(fromRowPos)!.nodeSize);
110-
111-
const trTable = tr.doc.nodeAt(table.pos);
112-
if (rowsCountBeforeDelete <= 1) {
113-
if (trTable) {
114-
if (trTable.childCount <= 1) {
115-
tr = tr.delete(table.pos, trTable.nodeSize);
116-
}
117-
} else {
118-
tr = tr.delete(tBody.pos, tBody.pos + tr.doc.nodeAt(tBody.pos)!.nodeSize);
119-
}
120-
}
40+
const sharedDepth = $from.sharedDepth($to.pos);
41+
const commonAncestor = $from.node(sharedDepth);
42+
const {schema} = commonAncestor.type;
43+
44+
if (
45+
!isAnyOfTypes(commonAncestor, [
46+
yfmTableType(schema),
47+
yfmTableBodyType(schema),
48+
yfmTableRowType(schema),
49+
])
50+
)
51+
return false;
52+
53+
const tablePos = findTablePos($from, sharedDepth);
54+
if (typeof tablePos !== 'number') return false;
55+
const tableNode = $from.doc.nodeAt(tablePos);
56+
if (!tableNode) return false;
57+
const tableDesc = TableDesc.create(tableNode)?.bind(tablePos);
58+
if (!tableDesc) return false;
59+
60+
if (dispatch) {
61+
const tr = state.tr;
62+
63+
const cells = range(0, tableDesc.rows)
64+
.flatMap((rowIdx) => tableDesc.getPosForRowCells(rowIdx))
65+
.filter((cell) => cell.type === 'real');
66+
67+
const isAllSelected =
68+
$from.pos <= cells[0].from + 2 && $to.pos >= cells[cells.length - 1].to - 2;
69+
70+
if (isAllSelected) {
71+
// all table content is selected, we should remove table
72+
tr.replaceWith(tablePos, tablePos + tableNode.nodeSize, pType(schema).create());
73+
tr.setSelection(TextSelection.create(tr.doc, tablePos + 1));
74+
} else {
75+
for (const cell of cells) {
76+
if ($from.pos > cell.to) continue;
77+
if ($to.pos < cell.from) break;
78+
79+
const from = Math.max($from.pos, cell.from + 1);
80+
const to = Math.min($to.pos, cell.to - 1);
81+
82+
tr.delete(tr.mapping.map(from), tr.mapping.map(to));
83+
tr.setSelection(TextSelection.near(tr.doc.resolve(tr.mapping.map(to)), -1));
12184
}
122-
tr = tr.setSelection(TextSelection.create(tr.doc, tr.mapping.map(sel.head)));
123-
124-
dispatch(tr);
12585
}
12686

127-
return true;
87+
dispatch(tr.scrollIntoView());
12888
}
12989

130-
return false;
90+
return true;
13191
};
13292

13393
export const backspaceCommand = chainCommands(removeCellNodeContent, clearSelectedCells);
13494

135-
function isEmptyTableRow(node: Node) {
136-
if (!isTableRowNode(node)) return false;
137-
if (node.childCount === 0) return true;
138-
let isRowEmpty = true;
139-
node.forEach((cellNode) => {
140-
isRowEmpty = isEmptyTableCell(cellNode);
141-
});
142-
return isRowEmpty;
95+
function isAnyOfTypes(node: Node, types: NodeType[]): boolean {
96+
return types.some((type) => type === node.type);
14397
}
14498

145-
function isEmptyTableCell(node: Node): boolean {
146-
if (!isTableCellNode(node)) return false;
147-
if (node.childCount === 0) return true;
148-
return node.childCount === 1 && node.child(0).isTextblock && node.child(0).childCount === 0;
99+
function findTablePos($pos: ResolvedPos, startDepth: number): number | null {
100+
const ASCENTS = 5;
101+
let depth = startDepth;
102+
while (depth >= 0 && startDepth - depth <= ASCENTS) {
103+
const node = $pos.node(depth);
104+
if (isTableNode(node)) {
105+
return $pos.before(depth);
106+
}
107+
depth--;
108+
}
109+
return null;
149110
}

0 commit comments

Comments
 (0)