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