Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/editors/slate/slate-react/components/children.tsx
1698 views
1
import React, { useCallback, useRef } from "react";
2
import { Editor, Range, Element, Ancestor, Descendant } from "slate";
3
4
import ElementComponent from "./element";
5
import TextComponent from "./text";
6
import { ReactEditor } from "..";
7
import { useSlateStatic } from "../hooks/use-slate-static";
8
import { useDecorate } from "../hooks/use-decorate";
9
import { NODE_TO_INDEX, NODE_TO_PARENT } from "../utils/weak-maps";
10
import { RenderElementProps, RenderLeafProps } from "./editable";
11
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
12
import { shallowCompare } from "@cocalc/util/misc";
13
import { SlateEditor } from "../../editable-markdown";
14
15
export interface WindowingParams {
16
rowStyle?: React.CSSProperties;
17
overscanRowCount?: number;
18
estimatedRowSize?: number;
19
marginTop?;
20
marginBottom?;
21
rowSizeEstimator?: (Node) => number | undefined;
22
}
23
24
/**
25
* Children.
26
*/
27
28
interface Props {
29
decorations: Range[];
30
node: Ancestor;
31
renderElement?: React.FC<RenderElementProps>;
32
renderLeaf?: React.FC<RenderLeafProps>;
33
selection: Range | null;
34
windowing?: WindowingParams;
35
onScroll?: () => void; // called after scrolling when windowing is true.
36
isComposing?: boolean;
37
hiddenChildren?: Set<number>;
38
}
39
40
const Children: React.FC<Props> = React.memo(
41
({
42
decorations,
43
node,
44
renderElement,
45
renderLeaf,
46
selection,
47
windowing,
48
onScroll,
49
hiddenChildren,
50
}) => {
51
const decorate = useDecorate();
52
const editor = useSlateStatic() as SlateEditor;
53
let path;
54
try {
55
path = ReactEditor.findPath(editor, node);
56
} catch (err) {
57
console.warn("WARNING: unable to find path to node", node, err);
58
return <></>;
59
}
60
//console.log("render Children", path);
61
62
const isLeafBlock =
63
Element.isElement(node) &&
64
!editor.isInline(node) &&
65
Editor.hasInlines(editor, node);
66
67
const renderChild = ({ index }: { index: number }) => {
68
//console.log("renderChild", index, JSON.stringify(selection));
69
// When windowing, we put a margin at the top of the first cell
70
// and the bottom of the last cell. This makes sure the scroll
71
// bar looks right, which it would not if we put a margin around
72
// the entire list.
73
let marginTop: string | undefined = undefined;
74
let marginBottom: string | undefined = undefined;
75
if (windowing != null) {
76
if (windowing.marginTop && index === 0) {
77
marginTop = windowing.marginTop;
78
} else if (
79
windowing.marginBottom &&
80
index + 1 === node?.children?.length
81
) {
82
marginBottom = windowing.marginBottom;
83
}
84
}
85
86
if (hiddenChildren?.has(index)) {
87
// TRICK: We use a small positive height since a height of 0 gets ignored, as it often
88
// appears when scrolling and allowing that breaks everything (for now!).
89
return (
90
<div
91
style={{ height: "1px", marginTop, marginBottom }}
92
contentEditable={false}
93
/>
94
);
95
}
96
const n = node.children[index] as Descendant;
97
const key = ReactEditor.findKey(editor, n);
98
let ds, range;
99
if (path != null) {
100
const p = path.concat(index);
101
try {
102
// I had the following crash once when pasting, then undoing in production:
103
range = Editor.range(editor, p);
104
} catch (_) {
105
range = null;
106
}
107
if (range != null) {
108
ds = decorate([n, p]);
109
for (const dec of decorations) {
110
const d = Range.intersection(dec, range);
111
112
if (d) {
113
ds.push(d);
114
}
115
}
116
}
117
} else {
118
ds = [];
119
range = null;
120
}
121
122
if (Element.isElement(n)) {
123
const x = (
124
<ElementComponent
125
decorations={ds}
126
element={n}
127
key={key.id}
128
renderElement={renderElement}
129
renderLeaf={renderLeaf}
130
selection={
131
selection && range && Range.intersection(range, selection)
132
}
133
/>
134
);
135
if (marginTop || marginBottom) {
136
return <div style={{ marginTop, marginBottom }}>{x}</div>;
137
} else {
138
return x;
139
}
140
} else {
141
return (
142
<TextComponent
143
decorations={ds ?? []}
144
key={key.id}
145
isLast={isLeafBlock && index === node.children.length - 1}
146
parent={node as Element}
147
renderLeaf={renderLeaf}
148
text={n}
149
/>
150
);
151
}
152
};
153
154
for (let i = 0; i < node.children.length; i++) {
155
const n = node.children[i];
156
NODE_TO_INDEX.set(n, i);
157
NODE_TO_PARENT.set(n, node);
158
}
159
160
const virtuosoRef = useRef<VirtuosoHandle>(null);
161
const scrollerRef = useRef<HTMLDivElement | null>(null);
162
// see https://github.com/petyosi/react-virtuoso/issues/274
163
const handleScrollerRef = useCallback((ref) => {
164
scrollerRef.current = ref;
165
}, []);
166
if (windowing != null) {
167
// using windowing
168
169
// This is slightly awkward since when splitting frames, the component
170
// gets unmounted and then mounted again, in which case editor.windowedListRef.current
171
// does not get set to null, so we need to write the new virtuosoRef;
172
if (editor.windowedListRef.current == null) {
173
editor.windowedListRef.current = {};
174
}
175
editor.windowedListRef.current.virtuosoRef = virtuosoRef;
176
editor.windowedListRef.current.getScrollerRef = () => scrollerRef.current; // we do this so windowListRef is JSON-able!
177
178
// NOTE: the code for preserving scroll position when editing assumes
179
// the visibleRange really is *visible*. Thus if you mess with overscan
180
// or related properties below, that will likely break.
181
return (
182
<Virtuoso
183
ref={virtuosoRef}
184
scrollerRef={handleScrollerRef}
185
onScroll={onScroll}
186
className="smc-vfill"
187
totalCount={node.children.length}
188
itemContent={(index) => (
189
<div style={windowing.rowStyle}>{renderChild({ index })}</div>
190
)}
191
computeItemKey={(index) =>
192
ReactEditor.findKey(editor, node.children[index])?.id ?? `${index}`
193
}
194
rangeChanged={(visibleRange) => {
195
editor.windowedListRef.current.visibleRange = visibleRange;
196
}}
197
itemsRendered={(items) => {
198
const scrollTop = scrollerRef.current?.scrollTop ?? 0;
199
// need both items, since may use first if there is no second...
200
editor.windowedListRef.current.firstItemOffset =
201
scrollTop - items[0]?.offset;
202
editor.windowedListRef.current.secondItemOffset =
203
scrollTop - items[1]?.offset;
204
}}
205
/>
206
);
207
} else {
208
// anything else -- just render the children
209
const children: React.JSX.Element[] = [];
210
for (let index = 0; index < node.children.length; index++) {
211
try {
212
children.push(renderChild({ index }));
213
} catch (err) {
214
console.warn(
215
"SLATE -- issue in renderChild",
216
node.children[index],
217
err
218
);
219
}
220
}
221
222
return <>{children}</>;
223
}
224
},
225
(prev, next) => {
226
if (next.isComposing) {
227
// IMPORTANT: We prevent render while composing, since rendering
228
// would corrupt the DOM which confuses composition input, thus
229
// breaking input on Android, and many non-US languages. See
230
// https://github.com/ianstormtaylor/slate/issues/4127#issuecomment-803215432
231
return true;
232
}
233
return shallowCompare(prev, next);
234
}
235
);
236
237
export default Children;
238
239
/*
240
function getCursorY(): number | null {
241
const sel = getSelection();
242
if (sel == null || sel.rangeCount == 0) {
243
return null;
244
}
245
return sel.getRangeAt(0)?.getBoundingClientRect().y;
246
}
247
248
function preserveCursorScrollPosition() {
249
const before = getCursorY();
250
if (before === null) return;
251
requestAnimationFrame(() => {
252
const after = getCursorY();
253
if (after === null) return;
254
const elt = $('[data-virtuoso-scroller="true"]');
255
if (elt) {
256
elt.scrollTop((elt.scrollTop() ?? 0) + (after - before));
257
}
258
});
259
}
260
*/
261
262