Skip to content

Commit 2088fe4

Browse files
authored
Merge pull request #21 from yandex-cloud/table-arrow-controls
feat: switching table rows when pressing up and down arrow keys on keyboard
2 parents 22be2fb + 3e00e6a commit 2088fe4

File tree

5 files changed

+105
-24
lines changed

5 files changed

+105
-24
lines changed

src/extensions/behavior/Selection/commands.ts

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type {Node} from 'prosemirror-model';
2-
import {findChildren, findParentNodeClosestToPos, NodeWithPos} from 'prosemirror-utils';
2+
import {findChildren, findParentNodeClosestToPos} from 'prosemirror-utils';
33
import {Command, EditorState, NodeSelection, TextSelection} from 'prosemirror-state';
44

55
import {isCodeBlock} from '../../../utils/nodes';
66
import {GapCursorSelection} from '../../behavior/Cursor/GapCursorSelection';
7+
import {createFakeParagraphNear} from '../../../utils/selection';
78

89
export enum Direction {
910
up = 'up',
@@ -47,28 +48,6 @@ const arrow =
4748
return false;
4849
};
4950

50-
export const createFakeParagraphNear: (direction: 'up' | 'down', parent?: NodeWithPos) => Command =
51-
(direction, parent) => (state, dispatch) => {
52-
const paragraph = state.schema.nodes.paragraph;
53-
54-
if (!paragraph || !parent) {
55-
return false;
56-
}
57-
58-
const insertPos = direction === 'up' ? parent.pos : parent.pos + parent.node.nodeSize;
59-
60-
const tr = state.tr;
61-
const sel = new GapCursorSelection(tr.doc.resolve(insertPos));
62-
63-
tr.setSelection(sel);
64-
65-
if (dispatch) {
66-
dispatch(tr);
67-
}
68-
69-
return true;
70-
};
71-
7251
export const arrowLeft = arrow(Direction.left);
7352
export const arrowDown = arrow(Direction.down);
7453
export const arrowUp = arrow(Direction.up);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {Command, TextSelection} from 'prosemirror-state';
2+
import {atEndOfCell, findChildTableRows, findParentTableRow} from '../../../../table-utils';
3+
import {
4+
findChildTableCells,
5+
findParentTable,
6+
findParentTableCell,
7+
} from '../../../../table-utils/utils';
8+
import {createFakeParagraphNear} from '../../../../utils/selection';
9+
10+
export function goToNextRow(dir: 'up' | 'down'): Command {
11+
return (state, dispatch, view) => {
12+
const parentTable = findParentTable(state.selection);
13+
const parentRow = findParentTableRow(state.selection);
14+
const parentCell = findParentTableCell(state.selection);
15+
16+
if (!view || !atEndOfCell(view, dir === 'up' ? -1 : 1)) {
17+
return false;
18+
}
19+
20+
if (!parentTable || !parentRow || !parentCell) {
21+
return false;
22+
}
23+
24+
const allRows = findChildTableRows(parentTable.node);
25+
const rowIndex = allRows.findIndex((node) => node.node === parentRow.node);
26+
27+
let cellIndex;
28+
29+
for (let i = 0; i < parentRow.node.childCount; i++) {
30+
if (parentRow.node.child(i) === parentCell.node) cellIndex = i;
31+
}
32+
33+
if (cellIndex === undefined) {
34+
return false;
35+
}
36+
37+
const newRowIndex = rowIndex + (dir === 'up' ? -1 : 1);
38+
39+
if (newRowIndex < 0 || newRowIndex >= allRows.length) {
40+
createFakeParagraphNear(dir, parentTable)(state, dispatch);
41+
42+
return true;
43+
}
44+
45+
const newRow = allRows[newRowIndex];
46+
const childCells = findChildTableCells(newRow.node);
47+
const cell = childCells[cellIndex];
48+
49+
const from = parentTable.start + newRow.pos + cell.pos + 3;
50+
51+
dispatch?.(
52+
state.tr.setSelection(new TextSelection(state.doc.resolve(from))).scrollIntoView(),
53+
);
54+
55+
return true;
56+
};
57+
}

src/extensions/yfm/YfmTable/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {getSpec, YfmTableSpecOptions} from './spec';
77
import {createYfmTable} from './actions';
88
import {fromYfm} from './fromYfm';
99
import {toYfm} from './toYfm';
10+
import {goToNextRow} from './commands/goToNextRow';
1011

1112
const action = 'createYfmTable';
1213

@@ -43,6 +44,8 @@ export const YfmTable: ExtensionWithOptions<YfmTableOptions> = (builder, options
4344
.addKeymap(() => ({
4445
Tab: goToNextCell('next'),
4546
'Shift-Tab': goToNextCell('prev'),
47+
ArrowDown: goToNextRow('down'),
48+
ArrowUp: goToNextRow('up'),
4649
}))
4750

4851
.addAction(action, () => createYfmTable);

src/table-utils/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {Node as PmNode} from 'prosemirror-model';
22
import {findChildren, findParentNode, Predicate} from 'prosemirror-utils';
3+
import type {EditorView} from 'prosemirror-view';
4+
import {isTextSelection} from '../utils/selection';
35
import {TableRole} from './const';
46

57
export const isTableNode: Predicate = (node) => node.type.spec.tableRole === TableRole.Table;
@@ -28,3 +30,19 @@ export const getTableDimensions = (node: PmNode | Node) => {
2830

2931
return {rows: rows || 1, cols: cols || 1};
3032
};
33+
34+
export function atEndOfCell(view: EditorView, dir: number) {
35+
if (!isTextSelection(view.state.selection)) return null;
36+
const {$head} = view.state.selection;
37+
for (let d = $head.depth - 1; d >= 0; d--) {
38+
const parent = $head.node(d),
39+
index = dir < 0 ? $head.index(d) : $head.indexAfter(d);
40+
if (index !== (dir < 0 ? 0 : parent.childCount)) return null;
41+
if (parent.type.spec.tableRole === TableRole.Cell) {
42+
const cellPos = $head.before(d);
43+
const dirStr = dir > 0 ? 'down' : 'up';
44+
return view.endOfTextblock(dirStr) ? cellPos : null;
45+
}
46+
}
47+
return null;
48+
}

src/utils/selection.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {Node, NodeType} from 'prosemirror-model';
2-
import {Selection, TextSelection, NodeSelection, AllSelection} from 'prosemirror-state';
2+
import {Selection, TextSelection, NodeSelection, AllSelection, Command} from 'prosemirror-state';
3+
import {NodeWithPos} from 'prosemirror-utils';
4+
import {GapCursorSelection} from '../extensions/behavior/Cursor/GapCursorSelection';
35

46
NodeSelection.prototype.selectionName = 'NodeSelection';
57
TextSelection.prototype.selectionName = 'TextSelection';
@@ -34,3 +36,25 @@ export const findSelectedNodeOfType = (nodeType: NodeType) => {
3436
return null;
3537
};
3638
};
39+
40+
export const createFakeParagraphNear: (direction: 'up' | 'down', parent?: NodeWithPos) => Command =
41+
(direction, parent) => (state, dispatch) => {
42+
const paragraph = state.schema.nodes.paragraph;
43+
44+
if (!paragraph || !parent) {
45+
return false;
46+
}
47+
48+
const insertPos = direction === 'up' ? parent.pos : parent.pos + parent.node.nodeSize;
49+
50+
const tr = state.tr;
51+
const sel = new GapCursorSelection(tr.doc.resolve(insertPos));
52+
53+
tr.setSelection(sel);
54+
55+
if (dispatch) {
56+
dispatch(tr);
57+
}
58+
59+
return true;
60+
};

0 commit comments

Comments
 (0)