Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/editors/slate/keyboard/arrow-keys.ts
1697 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
What happens when you hit arrow keys. This defines arrow key behavior for our
8
Slate editor, including moving the cursor up and down, scrolling the window,
9
moving to the beginning or end of the document, and handling cases where
10
selections are not in the DOM.
11
*/
12
13
import { register } from "./register";
14
import {
15
blocksCursor,
16
moveCursorUp,
17
moveCursorDown,
18
moveCursorToBeginningOfBlock,
19
moveCursorToBeginningOfLine,
20
moveCursorToEndOfLine,
21
isAtBeginningOfBlock,
22
isAtEndOfBlock,
23
} from "../control";
24
import { SlateEditor } from "../types";
25
import { ReactEditor } from "../slate-react";
26
import { Transforms } from "slate";
27
28
const down = ({ editor }: { editor: SlateEditor }) => {
29
const { selection } = editor;
30
setTimeout(() => {
31
// We have to do this via a timeout, because we don't control the cursor.
32
// Instead the selection in contenteditable changes via the browser and
33
// we react to that. Thus this is the only way with our current "sync with
34
// contenteditable approach". Here we just ensure that a move happens, rather
35
// than having the cursor be totally stuck, which is super annoying..
36
if (editor.selection === selection) {
37
Transforms.move(editor, { unit: "line" });
38
}
39
}, 1);
40
41
const cur = editor.selection?.focus;
42
43
if (
44
cur != null &&
45
editor.onCursorBottom != null &&
46
cur.path[0] >= editor.children.length - 1 &&
47
isAtEndOfBlock(editor, { mode: "highest" })
48
) {
49
editor.onCursorBottom();
50
}
51
const index = cur?.path[0];
52
if (
53
editor.windowedListRef.current != null &&
54
cur != null &&
55
index != null &&
56
cur.path[1] == editor.children[cur.path[0]]["children"]?.length - 1
57
) {
58
// moving to the next block:
59
if (editor.scrollIntoDOM(index + 1)) {
60
// we did actually have to scroll the block below current one into the dom.
61
setTimeout(() => {
62
// did cursor move? -- if not, we manually move it.
63
if (cur == editor.selection?.focus) {
64
moveCursorDown(editor, true);
65
moveCursorToBeginningOfBlock(editor);
66
}
67
}, 0);
68
}
69
}
70
if (ReactEditor.selectionIsInDOM(editor)) {
71
// just work in the usual way
72
if (!blocksCursor(editor, false)) {
73
// built in cursor movement works fine
74
return false;
75
}
76
moveCursorDown(editor, true);
77
moveCursorToBeginningOfBlock(editor);
78
return true;
79
} else {
80
// in case of windowing when actual selection is not even
81
// in the DOM, it's much better to just scroll it into view
82
// and not move the cursor at all than to have it be all
83
// wrong (which is what happens with contenteditable and
84
// selection change). I absolutely don't know how to
85
// subsequently move the cursor down programatically in
86
// contenteditable, and it makes no sense to do so in slate
87
// since the semantics of moving down depend on the exact rendering.
88
return true;
89
}
90
};
91
92
register({ key: "ArrowDown" }, down);
93
94
const up = ({ editor }: { editor: SlateEditor }) => {
95
const { selection } = editor;
96
setTimeout(() => {
97
// We have to do this via a timeout, because we don't control the cursor.
98
// Instead the selection in contenteditable changes via the browser and
99
// we react to that. Thus this is the only way with our current "sync with
100
// contenteditable approach".
101
if (editor.selection === selection) {
102
Transforms.move(editor, { unit: "line", reverse: true });
103
}
104
}, 1);
105
106
const cur = editor.selection?.focus;
107
if (
108
cur != null &&
109
editor.onCursorTop != null &&
110
cur?.path[0] == 0 &&
111
isAtBeginningOfBlock(editor, { mode: "highest" })
112
) {
113
editor.onCursorTop();
114
}
115
const index = cur?.path[0];
116
if (editor.windowedListRef.current != null && index && cur.path[1] == 0) {
117
if (editor.scrollIntoDOM(index - 1)) {
118
setTimeout(() => {
119
if (cur == editor.selection?.focus) {
120
moveCursorUp(editor, true);
121
moveCursorToBeginningOfBlock(editor);
122
}
123
}, 0);
124
}
125
}
126
if (ReactEditor.selectionIsInDOM(editor)) {
127
if (!blocksCursor(editor, true)) {
128
// built in cursor movement works fine
129
return false;
130
}
131
moveCursorUp(editor, true);
132
moveCursorToBeginningOfBlock(editor);
133
return true;
134
} else {
135
return true;
136
}
137
};
138
139
register({ key: "ArrowUp" }, up);
140
141
/*
142
The following functions are needed when using windowing, since
143
otherwise page up/page down get stuck when the rendered window
144
is at the edge. This is unavoidable, even if we were to
145
render a big overscan. If scrolling doesn't move, the code below
146
forces a manual move by one page.
147
148
NOTE/TODO: none of the code below moves the *cursor*; it only
149
moves the scroll position on the page. In contrast, word,
150
google docs and codemirror all move the cursor when you page up/down,
151
so maybe that should be implemented...?
152
*/
153
154
function pageWindowed(sign) {
155
return ({ editor }) => {
156
const scroller = editor.windowedListRef.current?.getScrollerRef();
157
if (scroller == null) return false;
158
const { scrollTop } = scroller;
159
160
setTimeout(() => {
161
if (scrollTop == scroller.scrollTop) {
162
scroller.scrollTop += sign * scroller.getBoundingClientRect().height;
163
}
164
}, 0);
165
166
return false;
167
};
168
}
169
170
const pageUp = pageWindowed(-1);
171
register({ key: "PageUp" }, pageUp);
172
173
const pageDown = pageWindowed(1);
174
register({ key: "PageDown" }, pageDown);
175
176
function beginningOfDoc({ editor }) {
177
const scroller = editor.windowedListRef.current?.getScrollerRef();
178
if (scroller == null) return false;
179
scroller.scrollTop = 0;
180
return true;
181
}
182
function endOfDoc({ editor }) {
183
const scroller = editor.windowedListRef.current?.getScrollerRef();
184
if (scroller == null) return false;
185
scroller.scrollTop = 1e20; // basically infinity
186
// might have to do it again do to measuring size of rows...
187
setTimeout(() => {
188
scroller.scrollTop = 1e20;
189
}, 1);
190
return true;
191
}
192
register({ key: "ArrowUp", meta: true }, beginningOfDoc); // mac
193
register({ key: "Home", ctrl: true }, beginningOfDoc); // windows
194
register({ key: "ArrowDown", meta: true }, endOfDoc); // mac
195
register({ key: "End", ctrl: true }, endOfDoc); // windows
196
197
function endOfLine({ editor }) {
198
const { selection } = editor;
199
setTimeout(() => {
200
// We have to do this via a timeout, because we don't control the cursor.
201
// Instead the selection in contenteditable changes via the browser and
202
// we react to that. Thus this is the only way with our current "sync with
203
// contenteditable approach".
204
if (editor.selection === selection) {
205
// stuck!
206
moveCursorToEndOfLine(editor);
207
}
208
}, 1);
209
return false;
210
}
211
212
function beginningOfLine({ editor }) {
213
const { selection } = editor;
214
setTimeout(() => {
215
// We have to do this via a timeout, because we don't control the cursor.
216
// Instead the selection in contenteditable changes via the browser and
217
// we react to that. Thus this is the only way with our current "sync with
218
// contenteditable approach".
219
if (editor.selection === selection) {
220
// stuck!
221
moveCursorToBeginningOfLine(editor);
222
}
223
}, 1);
224
return false;
225
}
226
227
register({ key: "ArrowRight", meta: true }, endOfLine);
228
register({ key: "ArrowRight", ctrl: true }, endOfLine);
229
register({ key: "ArrowLeft", meta: true }, beginningOfLine);
230
register({ key: "ArrowLeft", ctrl: true }, beginningOfLine);
231
232