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/element.tsx
1698 views
1
import React from "react";
2
import { useRef } from "react";
3
const getDirection = require("direction");
4
import { Editor, Node, Range, Element as SlateElement } from "slate";
5
6
import Text from "./text";
7
import Children from "./children";
8
import { ReactEditor, useSlateStatic, useReadOnly } from "..";
9
import { SelectedContext } from "../hooks/use-selected";
10
import { useIsomorphicLayoutEffect } from "../hooks/use-isomorphic-layout-effect";
11
import {
12
NODE_TO_ELEMENT,
13
ELEMENT_TO_NODE,
14
NODE_TO_PARENT,
15
NODE_TO_INDEX,
16
KEY_TO_ELEMENT,
17
} from "../utils/weak-maps";
18
import { RenderElementProps, RenderLeafProps } from "./editable";
19
20
/**
21
* Element.
22
*/
23
24
const Element = (props: {
25
decorations: Range[];
26
element: SlateElement;
27
renderElement?: React.FC<RenderElementProps>;
28
renderLeaf?: React.FC<RenderLeafProps>;
29
selection: Range | null;
30
}) => {
31
const {
32
decorations,
33
element,
34
renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
35
renderLeaf,
36
selection,
37
} = props;
38
// console.log("renderElement", element);
39
40
const ref = useRef<HTMLElement>(null);
41
const editor = useSlateStatic();
42
const readOnly = useReadOnly();
43
const key = ReactEditor.findKey(editor, element);
44
45
// Update element-related weak maps with the DOM element ref.
46
useIsomorphicLayoutEffect(() => {
47
if (ref.current) {
48
KEY_TO_ELEMENT.set(key, ref.current);
49
NODE_TO_ELEMENT.set(element, ref.current);
50
ELEMENT_TO_NODE.set(ref.current, element);
51
} else {
52
KEY_TO_ELEMENT.delete(key);
53
NODE_TO_ELEMENT.delete(element);
54
}
55
});
56
57
const isInline = editor.isInline(element);
58
59
let children: React.JSX.Element | null = (
60
<Children
61
decorations={decorations}
62
node={element}
63
renderElement={renderElement}
64
renderLeaf={renderLeaf}
65
selection={selection}
66
/>
67
);
68
69
// Attributes that the developer must mix into the element in their
70
// custom node renderer component.
71
const attributes: {
72
"data-slate-node": "element";
73
"data-slate-void"?: true;
74
"data-slate-inline"?: true;
75
contentEditable?: false;
76
dir?: "rtl";
77
ref: any;
78
} = {
79
"data-slate-node": "element",
80
ref,
81
};
82
83
if (isInline) {
84
attributes["data-slate-inline"] = true;
85
}
86
87
// If it's a block node with inline children, add the proper `dir` attribute
88
// for text direction.
89
if (!isInline && Editor.hasInlines(editor, element)) {
90
const text = Node.string(element);
91
const dir = getDirection(text);
92
93
if (dir === "rtl") {
94
attributes.dir = dir;
95
}
96
}
97
98
// If it's a void node, wrap the children in extra void-specific elements.
99
if (Editor.isVoid(editor, element)) {
100
attributes["data-slate-void"] = true;
101
102
if (!readOnly && isInline) {
103
attributes.contentEditable = false;
104
}
105
106
const Tag = isInline ? "span" : "div";
107
const [[text]] = Node.texts(element);
108
109
children = readOnly ? null : (
110
<Tag
111
data-slate-spacer
112
style={{
113
height: "0",
114
color: "transparent",
115
outline: "none",
116
position: "absolute",
117
}}
118
>
119
<Text decorations={[]} isLast={false} parent={element} text={text} />
120
</Tag>
121
);
122
123
NODE_TO_INDEX.set(text, 0);
124
NODE_TO_PARENT.set(text, element);
125
}
126
127
return (
128
<SelectedContext.Provider value={!!selection}>
129
{React.createElement(renderElement, { attributes, children, element })}
130
</SelectedContext.Provider>
131
);
132
};
133
134
const MemoizedElement = React.memo(Element, (prev, next) => {
135
const is_equal =
136
prev.element === next.element &&
137
prev.renderElement === next.renderElement &&
138
prev.renderLeaf === next.renderLeaf &&
139
isRangeListEqual(prev.decorations, next.decorations) &&
140
(prev.selection === next.selection ||
141
(!!prev.selection &&
142
!!next.selection &&
143
Range.equals(prev.selection, next.selection)));
144
// console.log("MemoizedElement", { is_equal, prev, next });
145
return is_equal;
146
});
147
148
/**
149
* The default element renderer.
150
*/
151
152
export const DefaultElement = (props: RenderElementProps) => {
153
const { attributes, children, element } = props;
154
const editor = useSlateStatic();
155
const Tag = editor.isInline(element) ? "span" : "div";
156
return (
157
<Tag {...attributes} style={{ position: "relative" }}>
158
{children}
159
</Tag>
160
);
161
};
162
163
/**
164
* Check if a list of ranges is equal to another.
165
*
166
* PERF: this requires the two lists to also have the ranges inside them in the
167
* same order, but this is an okay constraint for us since decorations are
168
* kept in order, and the odd case where they aren't is okay to re-render for.
169
*/
170
171
const isRangeListEqual = (list?: Range[], another?: Range[]): boolean => {
172
if (list == null || another == null) {
173
return list === another;
174
}
175
if (list.length !== another.length) {
176
return false;
177
}
178
179
for (let i = 0; i < list.length; i++) {
180
const range = list[i];
181
const other = another[i];
182
183
if (!Range.equals(range, other)) {
184
return false;
185
}
186
}
187
188
return true;
189
};
190
191
export default MemoizedElement;
192
193