Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/editors/slate/slate-react/utils/dom.ts
1698 views
1
/**
2
* Types.
3
*/
4
5
// COMPAT: This is required to prevent TypeScript aliases from doing some very
6
// weird things for Slate's types with the same name as globals. (2019/11/27)
7
// https://github.com/microsoft/TypeScript/issues/35002
8
import DOMNode = globalThis.Node;
9
import DOMComment = globalThis.Comment;
10
import DOMElement = globalThis.Element;
11
import DOMText = globalThis.Text;
12
import DOMRange = globalThis.Range;
13
import DOMSelection = globalThis.Selection;
14
import DOMStaticRange = globalThis.StaticRange;
15
16
export {
17
DOMNode,
18
DOMComment,
19
DOMElement,
20
DOMText,
21
DOMRange,
22
DOMSelection,
23
DOMStaticRange,
24
};
25
26
export type DOMPoint = [Node, number];
27
28
/**
29
* Check if a DOM node is a comment node.
30
*/
31
32
export const isDOMComment = (value: any): value is DOMComment => {
33
return isDOMNode(value) && value.nodeType === 8;
34
};
35
36
/**
37
* Check if a DOM node is an element node.
38
*/
39
40
export const isDOMElement = (value: any): value is DOMElement => {
41
return isDOMNode(value) && value.nodeType === 1;
42
};
43
44
/**
45
* Check if a value is a DOM node.
46
*/
47
48
export const isDOMNode = (value: any): value is DOMNode => {
49
return value instanceof Node;
50
};
51
52
/**
53
* Check if a DOM node is an element node.
54
*/
55
56
export const isDOMText = (value: any): value is DOMText => {
57
return isDOMNode(value) && value.nodeType === 3;
58
};
59
60
/**
61
* Checks whether a paste event is a plaintext-only event.
62
*/
63
64
export const isPlainTextOnlyPaste = (event: ClipboardEvent) => {
65
return (
66
event.clipboardData &&
67
event.clipboardData.getData("text/plain") !== "" &&
68
event.clipboardData.types.length === 1
69
);
70
};
71
72
/**
73
* Normalize a DOM point so that it always refers to a text node.
74
*/
75
76
export const normalizeDOMPoint = (domPoint: DOMPoint): DOMPoint => {
77
let [node, offset] = domPoint;
78
79
// If it's an element node, its offset refers to the index of its children
80
// including comment nodes, so try to find the right text child node.
81
if (isDOMElement(node) && node.childNodes.length) {
82
const isLast = offset === node.childNodes.length;
83
const direction = isLast ? "backward" : "forward";
84
const index = isLast ? offset - 1 : offset;
85
node = getEditableChild(node, index, direction);
86
87
// If the node has children, traverse until we have a leaf node. Leaf nodes
88
// can be either text nodes, or other void DOM nodes.
89
while (isDOMElement(node) && node.childNodes.length) {
90
const i = isLast ? node.childNodes.length - 1 : 0;
91
node = getEditableChild(node, i, direction);
92
}
93
94
// Determine the new offset inside the text node.
95
offset = isLast && node.textContent != null ? node.textContent.length : 0;
96
}
97
98
// Return the node and offset.
99
return [node, offset];
100
};
101
102
/**
103
* Get the nearest editable child at `index` in a `parent`, preferring
104
* `direction`.
105
*/
106
107
export const getEditableChild = (
108
parent: DOMElement,
109
index: number,
110
direction: "forward" | "backward"
111
): DOMNode => {
112
const { childNodes } = parent;
113
let child = childNodes[index];
114
let i = index;
115
let triedForward = false;
116
let triedBackward = false;
117
118
// While the child is a comment node, or an element node with no children,
119
// keep iterating to find a sibling non-void, non-comment node.
120
while (
121
isDOMComment(child) ||
122
(isDOMElement(child) && child.childNodes.length === 0) ||
123
(isDOMElement(child) && child.getAttribute("contenteditable") === "false")
124
) {
125
if (triedForward && triedBackward) {
126
break;
127
}
128
129
if (i >= childNodes.length) {
130
triedForward = true;
131
i = index - 1;
132
direction = "backward";
133
continue;
134
}
135
136
if (i < 0) {
137
triedBackward = true;
138
i = index + 1;
139
direction = "forward";
140
continue;
141
}
142
143
child = childNodes[i];
144
i += direction === "forward" ? 1 : -1;
145
}
146
147
return child;
148
};
149
150
/**
151
* Get a plaintext representation of the content of a node, accounting for block
152
* elements which get a newline appended.
153
*
154
* The domNode must be attached to the DOM.
155
*/
156
157
export const getPlainText = (domNode: DOMNode) => {
158
let text = "";
159
160
if (isDOMText(domNode) && domNode.nodeValue) {
161
return domNode.nodeValue;
162
}
163
164
if (isDOMElement(domNode)) {
165
for (const childNode of Array.from(domNode.childNodes)) {
166
text += getPlainText(childNode);
167
}
168
169
const display = getComputedStyle(domNode).getPropertyValue("display");
170
171
if (display === "block" || display === "list" || domNode?.tagName === "BR") {
172
text += "\n";
173
}
174
}
175
176
return text;
177
};
178
179