@37signals/lexxy 0.9.15-beta.next-0 → 0.9.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lexical.esm.js +344 -0
- package/dist/lexxy.esm.js +177 -93
- package/dist/lexxy_helpers.esm.js +7 -3
- package/package.json +1 -1
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { $getRoot, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isTextNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $getSelection, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $isRangeSelection, $getCommonAncestor, $findMatchingParent, TextNode } from 'lexical';
|
|
2
|
+
export * from 'lexical';
|
|
3
|
+
import { ListNode } from '@lexical/list';
|
|
4
|
+
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator } from '@lexical/utils';
|
|
5
|
+
import { $ensureForwardRangeSelection, $isAtNodeEnd } from '@lexical/selection';
|
|
6
|
+
|
|
7
|
+
/*** Only import from lexical packages in this file to prevent breaking npm package export chunking ***/
|
|
8
|
+
|
|
9
|
+
function $containsRangeSelection(node, selection = $getSelection()) {
|
|
10
|
+
if ($isRangeSelection(selection)) {
|
|
11
|
+
const { commonAncestor } = $getCommonAncestor(selection.focus.getNode(), selection.anchor.getNode());
|
|
12
|
+
return $findMatchingParent(commonAncestor, parent => parent.is(node))
|
|
13
|
+
} else {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function $createNodeSelectionWith(...nodes) {
|
|
19
|
+
const selection = $createNodeSelection();
|
|
20
|
+
nodes.forEach(node => selection.add(node.getKey()));
|
|
21
|
+
return selection
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function $isShadowRoot(node) {
|
|
25
|
+
return $isElementNode(node) && $isRootOrShadowRoot(node) && !$isRootNode(node)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function $isSafeForRoot(node) {
|
|
29
|
+
return ($isElementNode(node) || $isDecoratorNode(node)) && !node.isParentRequired()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function $makeSafeForRoot(node) {
|
|
33
|
+
if ($isSafeForRoot(node)) {
|
|
34
|
+
return node
|
|
35
|
+
} else {
|
|
36
|
+
return $wrapNodeInElement(node, () => node.createParentElementNode())
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getListType(node) {
|
|
41
|
+
const list = $getNearestNodeOfType(node, ListNode);
|
|
42
|
+
return list?.getListType() ?? null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isEditorFocused(editor) {
|
|
46
|
+
const rootElement = editor.getRootElement();
|
|
47
|
+
return rootElement !== null && rootElement.contains(document.activeElement)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function $isAtNodeEdge(point, atStart = null) {
|
|
51
|
+
if (atStart === null) {
|
|
52
|
+
return $isAtNodeEdge(point, true) || $isAtNodeEdge(point, false)
|
|
53
|
+
} else {
|
|
54
|
+
return atStart ? $isAtNodeStart(point) : $isAtNodeEnd(point)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function $isAtNodeStart(point) {
|
|
59
|
+
return point.offset === 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extendTextNodeConversion(conversionName, ...callbacks) {
|
|
63
|
+
return extendConversion(TextNode, conversionName, (conversionOutput, element) => ({
|
|
64
|
+
...conversionOutput,
|
|
65
|
+
forChild: (lexicalNode, parentNode) => {
|
|
66
|
+
const originalForChild = conversionOutput?.forChild ?? (x => x);
|
|
67
|
+
let childNode = originalForChild(lexicalNode, parentNode);
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if ($isTextNode(childNode)) {
|
|
71
|
+
childNode = callbacks.reduce(
|
|
72
|
+
(childNode, callback) => callback(childNode, element) ?? childNode,
|
|
73
|
+
childNode
|
|
74
|
+
);
|
|
75
|
+
return childNode
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extendConversion(nodeKlass, conversionName, callback = (output => output)) {
|
|
82
|
+
return (element) => {
|
|
83
|
+
const converter = nodeKlass.importDOM()?.[conversionName]?.(element);
|
|
84
|
+
if (!converter) return null
|
|
85
|
+
|
|
86
|
+
const conversionOutput = converter.conversion(element);
|
|
87
|
+
if (!conversionOutput) return conversionOutput
|
|
88
|
+
|
|
89
|
+
return callback(conversionOutput, element) ?? conversionOutput
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function $isCursorOnLastLine(selection) {
|
|
94
|
+
const anchorNode = selection.anchor.getNode();
|
|
95
|
+
const elementNode = $isElementNode(anchorNode) ? anchorNode : anchorNode.getParentOrThrow();
|
|
96
|
+
const children = elementNode.getChildren();
|
|
97
|
+
if (children.length === 0) return true
|
|
98
|
+
|
|
99
|
+
const lastChild = children[children.length - 1];
|
|
100
|
+
|
|
101
|
+
if (anchorNode === elementNode.getLatest() && selection.anchor.offset === children.length) return true
|
|
102
|
+
if (anchorNode === lastChild) return true
|
|
103
|
+
|
|
104
|
+
const lastLineBreakIndex = children.findLastIndex(child => $isLineBreakNode(child));
|
|
105
|
+
if (lastLineBreakIndex === -1) return true
|
|
106
|
+
|
|
107
|
+
const anchorIndex = children.indexOf(anchorNode);
|
|
108
|
+
return anchorIndex > lastLineBreakIndex
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function $isBlankNode(node) {
|
|
112
|
+
if (node.getTextContent().trim() !== "") return false
|
|
113
|
+
|
|
114
|
+
const children = node.getChildren?.();
|
|
115
|
+
if (!children || children.length === 0) return true
|
|
116
|
+
|
|
117
|
+
return children.every(child => {
|
|
118
|
+
if ($isLineBreakNode(child)) return true
|
|
119
|
+
return $isBlankNode(child)
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function $trimTrailingBlankNodes(parent) {
|
|
124
|
+
for (const child of $lastToFirstIterator(parent)) {
|
|
125
|
+
if ($isBlankNode(child)) {
|
|
126
|
+
child.remove();
|
|
127
|
+
} else {
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// A list item is structurally empty if it contains no meaningful content.
|
|
134
|
+
// Unlike getTextContent().trim() === "", this walks descendants to ensure
|
|
135
|
+
// decorator nodes (mentions, attachments whose getTextContent() may return
|
|
136
|
+
// invisible characters like \ufeff) are treated as non-empty content.
|
|
137
|
+
function $isListItemStructurallyEmpty(listItem) {
|
|
138
|
+
const children = listItem.getChildren();
|
|
139
|
+
for (const child of children) {
|
|
140
|
+
if ($isDecoratorNode(child)) return false
|
|
141
|
+
if ($isLineBreakNode(child)) continue
|
|
142
|
+
if ($isTextNode(child)) {
|
|
143
|
+
if (child.getTextContent().trim() !== "") return false
|
|
144
|
+
} else if ($isElementNode(child)) {
|
|
145
|
+
if (child.getTextContent().trim() !== "") return false
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Returns the document text up to `offset` inside `targetNode`. Non-inline
|
|
152
|
+
// element siblings are joined with `\n\n`, matching Lexical's own
|
|
153
|
+
// ElementNode.getTextContent behavior.
|
|
154
|
+
function $textBeforeOffset(targetNode, offset) {
|
|
155
|
+
const parts = [];
|
|
156
|
+
let done = false;
|
|
157
|
+
|
|
158
|
+
function visit(node) {
|
|
159
|
+
if (done) return
|
|
160
|
+
if (node === targetNode) {
|
|
161
|
+
parts.push(node.getTextContent().slice(0, offset));
|
|
162
|
+
done = true;
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
if ($isElementNode(node)) {
|
|
166
|
+
const children = node.getChildren();
|
|
167
|
+
for (let i = 0; i < children.length; i++) {
|
|
168
|
+
visit(children[i]);
|
|
169
|
+
if (done) return
|
|
170
|
+
const child = children[i];
|
|
171
|
+
if ($isElementNode(child) && !child.isInline() && i < children.length - 1) {
|
|
172
|
+
parts.push("\n\n");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
parts.push(node.getTextContent());
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
visit($getRoot());
|
|
181
|
+
return parts.join("")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function $splitSelectedParagraphsAtInnerLineBreaks(selection) {
|
|
185
|
+
const topLevelElements = new Set();
|
|
186
|
+
for (const node of selection.getNodes()) {
|
|
187
|
+
const topLevel = node.getTopLevelElement();
|
|
188
|
+
if (topLevel) topLevelElements.add(topLevel);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const element of topLevelElements) {
|
|
192
|
+
if (!$isParagraphNode(element)) continue
|
|
193
|
+
|
|
194
|
+
const children = element.getChildren();
|
|
195
|
+
if (!children.some($isLineBreakNode)) continue
|
|
196
|
+
|
|
197
|
+
const groups = [ [] ];
|
|
198
|
+
for (const child of children) {
|
|
199
|
+
if ($isLineBreakNode(child)) {
|
|
200
|
+
groups.push([]);
|
|
201
|
+
child.remove();
|
|
202
|
+
} else {
|
|
203
|
+
groups[groups.length - 1].push(child);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const group of groups) {
|
|
208
|
+
if (group.length === 0) continue
|
|
209
|
+
const paragraph = $createParagraphNode();
|
|
210
|
+
group.forEach(child => paragraph.append(child));
|
|
211
|
+
element.insertBefore(paragraph);
|
|
212
|
+
}
|
|
213
|
+
if (groups.some(group => group.length > 0)) element.remove();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function $expandSelectionToLineBreaksAndSplitAtEdges(selection) {
|
|
218
|
+
$ensureForwardRangeSelection(selection);
|
|
219
|
+
|
|
220
|
+
const focusCaret = $caretFromPoint(selection.focus, "next");
|
|
221
|
+
const anchorCaret = $caretFromPoint(selection.anchor, "previous");
|
|
222
|
+
|
|
223
|
+
// A collapsed cursor adjacent to a <br> would claim it from both sides via
|
|
224
|
+
// inward-edge; force outward-only walks so each side finds its own boundary.
|
|
225
|
+
const skipInwardEdge = selection.isCollapsed();
|
|
226
|
+
const focusBrCaret = $getCaretAtLineBreakBoundary(focusCaret, skipInwardEdge);
|
|
227
|
+
let anchorBrCaret = $getCaretAtLineBreakBoundary(anchorCaret, skipInwardEdge);
|
|
228
|
+
|
|
229
|
+
if (focusBrCaret?.origin.is(anchorBrCaret?.origin)) {
|
|
230
|
+
anchorBrCaret = null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Splitting focus first keeps the anchor <br>'s position stable.
|
|
234
|
+
const focusOuter = focusBrCaret && $splitAroundLineBreak(focusBrCaret);
|
|
235
|
+
const anchorOuter = anchorBrCaret && $splitAroundLineBreak(anchorBrCaret);
|
|
236
|
+
|
|
237
|
+
const innerStart = anchorOuter?.getNextSibling() ?? selection.anchor.getNode().getTopLevelElement();
|
|
238
|
+
const innerEnd = focusOuter?.getPreviousSibling() ?? selection.focus.getNode().getTopLevelElement();
|
|
239
|
+
if (!innerStart || !innerEnd) return
|
|
240
|
+
|
|
241
|
+
$setSelectionFromCaretRange($getCaretRange(
|
|
242
|
+
$normalizeCaret($getChildCaret(innerStart, "next")),
|
|
243
|
+
$getCaretInDirection(
|
|
244
|
+
$normalizeCaret($getChildCaret(innerEnd, "previous")),
|
|
245
|
+
"next",
|
|
246
|
+
),
|
|
247
|
+
));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function $getCaretAtLineBreakBoundary(caret, skipInwardEdge = false) {
|
|
251
|
+
const paragraph = caret.origin.getTopLevelElement();
|
|
252
|
+
if (!paragraph || !$isParagraphNode(paragraph)) return null
|
|
253
|
+
|
|
254
|
+
const lineBreak = (skipInwardEdge ? null : $inwardEdgeLineBreak(caret, paragraph))
|
|
255
|
+
?? $outwardLineBreak(caret, paragraph);
|
|
256
|
+
|
|
257
|
+
return lineBreak ? $getSiblingCaret(lineBreak, caret.direction) : null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Prefer a <br> the cursor is sitting flush against, except when a further <br>
|
|
261
|
+
// also exists outward — that one is the real paragraph break for this side.
|
|
262
|
+
function $inwardEdgeLineBreak(caret, paragraph) {
|
|
263
|
+
let candidateCaret;
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
($isChildCaret(caret) && caret.origin.is(paragraph)) ||
|
|
267
|
+
($isTextPointCaret(caret) && $isExtendableTextPointCaret(caret.getFlipped()))
|
|
268
|
+
) {
|
|
269
|
+
candidateCaret = null;
|
|
270
|
+
} else if ($isSiblingCaret(caret) && caret.getParentAtCaret().is(paragraph)) {
|
|
271
|
+
candidateCaret = caret;
|
|
272
|
+
} else {
|
|
273
|
+
const childCaret = $paragraphChildCaretAtInwardEdge(caret, paragraph);
|
|
274
|
+
candidateCaret = childCaret ? $rewindSiblingCaret(childCaret) : null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (candidateCaret && $isLineBreakNode(candidateCaret.origin)) {
|
|
278
|
+
return $candidateUnlessShadowed(candidateCaret)
|
|
279
|
+
} else {
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function $candidateUnlessShadowed(candidateCaret) {
|
|
285
|
+
const outward = candidateCaret.getNodeAtCaret();
|
|
286
|
+
return $isLineBreakNode(outward) ? null : candidateCaret.origin
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function $outwardLineBreak(caret, paragraph) {
|
|
290
|
+
const startCaret = $outwardWalkStartCaret(caret, paragraph);
|
|
291
|
+
if (!startCaret) return null
|
|
292
|
+
|
|
293
|
+
for (const { origin } of startCaret) {
|
|
294
|
+
if (!origin.getParent().is(paragraph)) break
|
|
295
|
+
if ($isLineBreakNode(origin)) return origin
|
|
296
|
+
}
|
|
297
|
+
return null
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function $outwardWalkStartCaret(caret, paragraph) {
|
|
301
|
+
if (caret.getParentAtCaret().is(paragraph)) {
|
|
302
|
+
return caret
|
|
303
|
+
} else {
|
|
304
|
+
return $paragraphChildCaretContaining(caret, paragraph)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function $paragraphChildCaretContaining(caret, paragraph) {
|
|
309
|
+
let cursor = caret.getSiblingCaret();
|
|
310
|
+
while (cursor && !cursor.origin.getParent()?.is(paragraph)) {
|
|
311
|
+
cursor = cursor.getParentCaret();
|
|
312
|
+
}
|
|
313
|
+
return cursor?.origin.getParent()?.is(paragraph) ? cursor : null
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Only succeeds when the cursor is flush against the inward edge of every
|
|
317
|
+
// ancestor between itself and the paragraph child.
|
|
318
|
+
function $paragraphChildCaretAtInwardEdge(caret, paragraph) {
|
|
319
|
+
let cursor = caret.getSiblingCaret();
|
|
320
|
+
while (cursor && !cursor.origin.getParent()?.is(paragraph)) {
|
|
321
|
+
if (cursor.getNodeAtCaret()) return null
|
|
322
|
+
cursor = cursor.getParentCaret();
|
|
323
|
+
}
|
|
324
|
+
return cursor?.origin.getParent()?.is(paragraph) ? cursor : null
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function $splitAroundLineBreak(lineBreakCaret) {
|
|
328
|
+
let outer = null;
|
|
329
|
+
|
|
330
|
+
if (lineBreakCaret.getNodeAtCaret() === null) {
|
|
331
|
+
lineBreakCaret.origin.remove();
|
|
332
|
+
} else {
|
|
333
|
+
const lineBreak = lineBreakCaret.origin;
|
|
334
|
+
const splitCaret = $getCaretInDirection($rewindSiblingCaret(lineBreakCaret), "next");
|
|
335
|
+
|
|
336
|
+
$splitAtPointCaretNext(splitCaret);
|
|
337
|
+
outer = lineBreak.getTopLevelElement();
|
|
338
|
+
lineBreak.remove();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return outer
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export { $containsRangeSelection, $createNodeSelectionWith, $expandSelectionToLineBreaksAndSplitAtEdges, $isAtNodeEdge, $isAtNodeStart, $isBlankNode, $isCursorOnLastLine, $isListItemStructurallyEmpty, $isSafeForRoot, $isShadowRoot, $makeSafeForRoot, $splitSelectedParagraphsAtInnerLineBreaks, $textBeforeOffset, $trimTrailingBlankNodes, extendConversion, extendTextNodeConversion, getListType, isEditorFocused };
|
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export { highlightCode } from './lexxy_helpers.esm.js';
|
|
1
|
+
export { highlightCode, highlightElement } from './lexxy_helpers.esm.js';
|
|
2
2
|
import DOMPurify from 'dompurify';
|
|
3
3
|
import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $ensureForwardRangeSelection, $isAtNodeEnd, $patchStyleText, $setBlocksType, $forEachSelectedTextNode } from '@lexical/selection';
|
|
4
|
-
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isTextNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $getCommonAncestor, $findMatchingParent, TextNode, createCommand, defineExtension, COMMAND_PRIORITY_EDITOR, $getEditor, $getNodeByKey, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $createRangeSelection, $setSelection, createState, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection,
|
|
4
|
+
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, $getRoot, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isTextNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $getCommonAncestor, $findMatchingParent, TextNode, createCommand, defineExtension, COMMAND_PRIORITY_EDITOR, $getEditor, $getNodeByKey, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $createRangeSelection, $setSelection, createState, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, ElementNode, $splitNode, $getChildCaretAtIndex, $createLineBreakNode, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, PASTE_COMMAND, $onUpdate, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, mergeRegister as mergeRegister$1, CLEAR_HISTORY_COMMAND, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, INPUT_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
5
5
|
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
|
|
6
6
|
import { buildEditorFromExtensions } from '@lexical/extension';
|
|
7
7
|
import { ListNode, ListItemNode, $getListDepth, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $isListItemNode, $isListNode, registerList } from '@lexical/list';
|
|
8
|
-
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, $descendantsMatching, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, IS_APPLE } from '@lexical/utils';
|
|
8
|
+
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, $descendantsMatching, mergeRegister, $insertNodeToNearestRoot, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, IS_APPLE } from '@lexical/utils';
|
|
9
9
|
import { registerPlainText } from '@lexical/plain-text';
|
|
10
10
|
import { RichTextExtension, $isQuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
11
11
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
@@ -1449,7 +1449,7 @@ function filterMatchPosition(text, potentialMatch) {
|
|
|
1449
1449
|
|
|
1450
1450
|
if (!normalizedMatch) return 0
|
|
1451
1451
|
|
|
1452
|
-
const match = normalizedText.match(new RegExp(`(
|
|
1452
|
+
const match = normalizedText.match(new RegExp(`(?<![\\p{L}\\p{N}])${escapeForRegExp(normalizedMatch)}`, "u"));
|
|
1453
1453
|
return match ? match.index : -1
|
|
1454
1454
|
}
|
|
1455
1455
|
|
|
@@ -1650,6 +1650,39 @@ function $isListItemStructurallyEmpty(listItem) {
|
|
|
1650
1650
|
return true
|
|
1651
1651
|
}
|
|
1652
1652
|
|
|
1653
|
+
// Returns the document text up to `offset` inside `targetNode`. Non-inline
|
|
1654
|
+
// element siblings are joined with `\n\n`, matching Lexical's own
|
|
1655
|
+
// ElementNode.getTextContent behavior.
|
|
1656
|
+
function $textBeforeOffset(targetNode, offset) {
|
|
1657
|
+
const parts = [];
|
|
1658
|
+
let done = false;
|
|
1659
|
+
|
|
1660
|
+
function visit(node) {
|
|
1661
|
+
if (done) return
|
|
1662
|
+
if (node === targetNode) {
|
|
1663
|
+
parts.push(node.getTextContent().slice(0, offset));
|
|
1664
|
+
done = true;
|
|
1665
|
+
return
|
|
1666
|
+
}
|
|
1667
|
+
if ($isElementNode(node)) {
|
|
1668
|
+
const children = node.getChildren();
|
|
1669
|
+
for (let i = 0; i < children.length; i++) {
|
|
1670
|
+
visit(children[i]);
|
|
1671
|
+
if (done) return
|
|
1672
|
+
const child = children[i];
|
|
1673
|
+
if ($isElementNode(child) && !child.isInline() && i < children.length - 1) {
|
|
1674
|
+
parts.push("\n\n");
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
} else {
|
|
1678
|
+
parts.push(node.getTextContent());
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
visit($getRoot());
|
|
1683
|
+
return parts.join("")
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1653
1686
|
function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
|
|
1654
1687
|
return $isTextNode(node)
|
|
1655
1688
|
&& node.getTextContent() === " "
|
|
@@ -1935,6 +1968,11 @@ function safeCloneEditorState(editorState) {
|
|
|
1935
1968
|
return clone
|
|
1936
1969
|
}
|
|
1937
1970
|
|
|
1971
|
+
const INITIAL_PREVIEW_POLL_DELAY_MS = 3000;
|
|
1972
|
+
const MAX_PREVIEW_POLL_DELAY_MS = 120000;
|
|
1973
|
+
const MAX_PREVIEW_POLL_ATTEMPTS = 20;
|
|
1974
|
+
|
|
1975
|
+
|
|
1938
1976
|
class ActionTextAttachmentNode extends DecoratorNode {
|
|
1939
1977
|
static getType() {
|
|
1940
1978
|
return "action_text_attachment"
|
|
@@ -2009,7 +2047,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2009
2047
|
return Lexxy.global.get("attachmentTagName")
|
|
2010
2048
|
}
|
|
2011
2049
|
|
|
2012
|
-
constructor({ tagName, sgid, src, previewSrc, previewable, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError }, key) {
|
|
2050
|
+
constructor({ tagName, sgid, src, previewSrc, previewable, previewStatusUrl, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError }, key) {
|
|
2013
2051
|
super(key);
|
|
2014
2052
|
|
|
2015
2053
|
this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
|
|
@@ -2017,6 +2055,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2017
2055
|
this.src = src;
|
|
2018
2056
|
this.previewSrc = previewSrc;
|
|
2019
2057
|
this.previewable = parseBoolean(previewable);
|
|
2058
|
+
this.previewStatusUrl = previewStatusUrl;
|
|
2020
2059
|
this.pendingPreview = pendingPreview;
|
|
2021
2060
|
this.altText = altText || "";
|
|
2022
2061
|
this.caption = caption || "";
|
|
@@ -2095,6 +2134,8 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2095
2134
|
sgid: this.sgid,
|
|
2096
2135
|
src: this.src,
|
|
2097
2136
|
previewable: this.previewable,
|
|
2137
|
+
previewStatusUrl: this.previewStatusUrl,
|
|
2138
|
+
pendingPreview: this.pendingPreview,
|
|
2098
2139
|
altText: this.altText,
|
|
2099
2140
|
caption: this.caption,
|
|
2100
2141
|
contentType: this.contentType,
|
|
@@ -2213,41 +2254,68 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2213
2254
|
});
|
|
2214
2255
|
}
|
|
2215
2256
|
|
|
2257
|
+
// While the file-icon is shown, watch for the preview to become ready.
|
|
2258
|
+
// With a status URL, poll it (2xx = processing, anything else = ready).
|
|
2259
|
+
// Without one, preload the preview URL once and swap on load.
|
|
2216
2260
|
#pollForPreview(figure) {
|
|
2261
|
+
if (this.previewStatusUrl) {
|
|
2262
|
+
this.#waitForPreviewByPollingStatus(figure);
|
|
2263
|
+
} else {
|
|
2264
|
+
this.#waitForPreviewByPreloadingImage(figure);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
#waitForPreviewByPollingStatus(figure) {
|
|
2217
2269
|
let attempt = 0;
|
|
2218
|
-
const maxAttempts = 10;
|
|
2219
2270
|
|
|
2220
|
-
const
|
|
2271
|
+
const tryStatus = async () => {
|
|
2221
2272
|
if (!this.editor.read(() => this.isAttached())) return
|
|
2222
2273
|
|
|
2223
|
-
|
|
2224
|
-
|
|
2274
|
+
try {
|
|
2275
|
+
// redirect: "manual" prevents fetch from transparently following a
|
|
2276
|
+
// 3xx response — without it, a status endpoint that redirected to,
|
|
2277
|
+
// say, the preview URL would resolve to a 200 and look like
|
|
2278
|
+
// "still processing." The contract is "any non-2xx means done."
|
|
2279
|
+
const response = await fetch(this.previewStatusUrl, { credentials: "include", redirect: "manual" });
|
|
2225
2280
|
|
|
2226
|
-
img.onload = () => {
|
|
2227
2281
|
if (!this.editor.read(() => this.isAttached())) return
|
|
2228
2282
|
|
|
2229
|
-
|
|
2230
|
-
// generated from PDF/video content is significantly larger.
|
|
2231
|
-
if (img.naturalWidth > 150 && img.naturalHeight > 150) {
|
|
2232
|
-
this.#swapToPreviewDOM(figure, cacheBustedSrc);
|
|
2233
|
-
} else {
|
|
2283
|
+
if (response.ok) {
|
|
2234
2284
|
retry();
|
|
2285
|
+
} else {
|
|
2286
|
+
this.#swapToPreviewDOM(figure, this.src);
|
|
2235
2287
|
}
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2288
|
+
} catch {
|
|
2289
|
+
retry();
|
|
2290
|
+
}
|
|
2239
2291
|
};
|
|
2240
2292
|
|
|
2241
2293
|
const retry = () => {
|
|
2242
2294
|
attempt++;
|
|
2243
|
-
if (attempt <
|
|
2244
|
-
const delay = Math.min(2000 * Math.pow(1.5, attempt),
|
|
2245
|
-
setTimeout(
|
|
2295
|
+
if (attempt < MAX_PREVIEW_POLL_ATTEMPTS && this.editor.read(() => this.isAttached())) {
|
|
2296
|
+
const delay = Math.min(2000 * Math.pow(1.5, attempt), MAX_PREVIEW_POLL_DELAY_MS);
|
|
2297
|
+
setTimeout(tryStatus, delay);
|
|
2246
2298
|
}
|
|
2247
2299
|
};
|
|
2248
2300
|
|
|
2249
2301
|
// Give the server time to start processing before the first attempt
|
|
2250
|
-
setTimeout(
|
|
2302
|
+
setTimeout(tryStatus, INITIAL_PREVIEW_POLL_DELAY_MS);
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
#waitForPreviewByPreloadingImage(figure) {
|
|
2306
|
+
const img = new Image();
|
|
2307
|
+
img.onload = () => {
|
|
2308
|
+
if (!this.editor.read(() => this.isAttached())) return
|
|
2309
|
+
this.#swapToPreviewDOM(figure, this.src);
|
|
2310
|
+
};
|
|
2311
|
+
img.onerror = () => {
|
|
2312
|
+
// Clear pendingPreview so undo/redo or any JSON round-trip doesn't
|
|
2313
|
+
// re-enter the pending flow and issue another fetch. The file icon
|
|
2314
|
+
// stays as the stable fallback.
|
|
2315
|
+
if (!this.editor.read(() => this.isAttached())) return
|
|
2316
|
+
this.patchAndRewriteHistory({ pendingPreview: false });
|
|
2317
|
+
};
|
|
2318
|
+
img.src = this.src;
|
|
2251
2319
|
}
|
|
2252
2320
|
|
|
2253
2321
|
#swapToPreviewDOM(figure, previewSrc) {
|
|
@@ -3592,8 +3660,7 @@ class CommandDispatcher {
|
|
|
3592
3660
|
}
|
|
3593
3661
|
|
|
3594
3662
|
dispatchInsertHorizontalDivider() {
|
|
3595
|
-
|
|
3596
|
-
this.editor.focus();
|
|
3663
|
+
$insertNodeToNearestRoot(new HorizontalDividerNode);
|
|
3597
3664
|
}
|
|
3598
3665
|
|
|
3599
3666
|
dispatchSetFormatHeadingLarge() {
|
|
@@ -4826,7 +4893,7 @@ class ImageGalleryNode extends ElementNode {
|
|
|
4826
4893
|
replaceWithSingularChild() {
|
|
4827
4894
|
if (this.#hasSingularChild) {
|
|
4828
4895
|
const child = this.getFirstChild();
|
|
4829
|
-
return this.replace(child)
|
|
4896
|
+
return this.replace($makeSafeForRoot(child))
|
|
4830
4897
|
}
|
|
4831
4898
|
}
|
|
4832
4899
|
|
|
@@ -5254,6 +5321,7 @@ class AttachmentNodeConversion {
|
|
|
5254
5321
|
fileName: blob.filename,
|
|
5255
5322
|
fileSize: blob.byte_size,
|
|
5256
5323
|
previewable: blob.previewable,
|
|
5324
|
+
previewStatusUrl: blob.preview_status_url
|
|
5257
5325
|
}
|
|
5258
5326
|
}
|
|
5259
5327
|
|
|
@@ -5373,14 +5441,13 @@ class Contents {
|
|
|
5373
5441
|
}
|
|
5374
5442
|
|
|
5375
5443
|
insertDOM(doc, { tag } = {}) {
|
|
5376
|
-
this.#unwrapPlaceholderAnchors(doc);
|
|
5377
|
-
|
|
5378
5444
|
this.editor.update(() => {
|
|
5379
|
-
if ($hasUpdateTag(PASTE_TAG)) this.#
|
|
5445
|
+
if ($hasUpdateTag(PASTE_TAG)) this.#formatPastedDOM(doc);
|
|
5380
5446
|
|
|
5381
5447
|
const nodes = this.editorElement.$generateNodesFromDOM(doc);
|
|
5382
|
-
|
|
5383
|
-
|
|
5448
|
+
|
|
5449
|
+
if (!$hasUpdateTag(PASTE_TAG) || !this.#dispatchPastedNodesCommand(nodes)) {
|
|
5450
|
+
this.#insertUploadNodes(nodes) || this.insertAtCursor(...nodes);
|
|
5384
5451
|
}
|
|
5385
5452
|
}, { tag });
|
|
5386
5453
|
}
|
|
@@ -5700,6 +5767,17 @@ class Contents {
|
|
|
5700
5767
|
});
|
|
5701
5768
|
}
|
|
5702
5769
|
|
|
5770
|
+
#formatPastedDOM(doc) {
|
|
5771
|
+
this.#unwrapPlaceholderAnchors(doc);
|
|
5772
|
+
this.#stripTableCellColorStyles(doc);
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5775
|
+
#dispatchPastedNodesCommand(nodes) {
|
|
5776
|
+
return this.editor.dispatchCommand(SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, {
|
|
5777
|
+
nodes, selection: $getSelection()
|
|
5778
|
+
})
|
|
5779
|
+
}
|
|
5780
|
+
|
|
5703
5781
|
#insertNodeIfRoot(node) {
|
|
5704
5782
|
const selection = $getSelection();
|
|
5705
5783
|
if (!$isRangeSelection(selection)) return false
|
|
@@ -5994,10 +6072,10 @@ class Clipboard {
|
|
|
5994
6072
|
|
|
5995
6073
|
#handleParsedClipboardNodes({ nodes, selection }) {
|
|
5996
6074
|
const url = $bareUrlFromSingleLink(nodes);
|
|
5997
|
-
if (
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6075
|
+
if (url && $isRangeSelection(selection)) {
|
|
6076
|
+
this.#insertSingleLinkAt(selection, url);
|
|
6077
|
+
return true
|
|
6078
|
+
}
|
|
6001
6079
|
}
|
|
6002
6080
|
|
|
6003
6081
|
#isPlainTextOrURLPasted(clipboardData) {
|
|
@@ -6078,10 +6156,7 @@ class Clipboard {
|
|
|
6078
6156
|
const linkNode = $createLinkNode(url).append($createTextNode(url));
|
|
6079
6157
|
selection.insertNodes([ linkNode ]);
|
|
6080
6158
|
|
|
6081
|
-
|
|
6082
|
-
// listeners may run editor mutations of their own.
|
|
6083
|
-
const nodeKey = linkNode.getKey();
|
|
6084
|
-
Promise.resolve().then(() => this.#dispatchLinkInsertEvent(nodeKey, { url }));
|
|
6159
|
+
$onUpdate(() => this.#dispatchLinkInsertEvent(linkNode.getKey(), { url }));
|
|
6085
6160
|
}
|
|
6086
6161
|
|
|
6087
6162
|
#dispatchLinkInsertEvent(nodeKey, payload) {
|
|
@@ -7537,9 +7612,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7537
7612
|
static observedAttributes = [ "connected", "required" ]
|
|
7538
7613
|
|
|
7539
7614
|
#initialValue = ""
|
|
7615
|
+
#previousInternalFormValue = null
|
|
7616
|
+
|
|
7540
7617
|
#initializeEventDispatched = false
|
|
7541
7618
|
#editorInitializedDispatched = false
|
|
7542
|
-
#valueLoaded = false
|
|
7543
7619
|
#listeners = new ListenerBin()
|
|
7544
7620
|
#disposables = []
|
|
7545
7621
|
#historyState = { undo: false, redo: false }
|
|
@@ -7591,12 +7667,11 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7591
7667
|
disconnectedCallback() {
|
|
7592
7668
|
this.#initializeEventDispatched = false;
|
|
7593
7669
|
this.#editorInitializedDispatched = false;
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
this.#valueLoaded = false;
|
|
7670
|
+
|
|
7671
|
+
this.#previousInternalFormValue = null;
|
|
7672
|
+
this.valueBeforeDisconnect = this.value;
|
|
7673
|
+
|
|
7674
|
+
this.#clearCachedValues();
|
|
7600
7675
|
this.#reset(); // Prevent hangs with Safari when morphing
|
|
7601
7676
|
}
|
|
7602
7677
|
|
|
@@ -7621,13 +7696,9 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7621
7696
|
}
|
|
7622
7697
|
|
|
7623
7698
|
toString() {
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
});
|
|
7628
|
-
}
|
|
7629
|
-
|
|
7630
|
-
return this.cachedStringValue
|
|
7699
|
+
return this.cachedStringValue ??= this.editor?.read(() => {
|
|
7700
|
+
return $getReadableTextContent($getRoot())
|
|
7701
|
+
})
|
|
7631
7702
|
}
|
|
7632
7703
|
|
|
7633
7704
|
get form() {
|
|
@@ -7711,8 +7782,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7711
7782
|
return dispatch(this, "lexxy:file-accept", { file }, true)
|
|
7712
7783
|
}
|
|
7713
7784
|
|
|
7714
|
-
$generateNodesFromDOM(doc) {
|
|
7715
|
-
let nodes = $generateNodesFromDOM(
|
|
7785
|
+
$generateNodesFromDOM(doc, { editor = this.editor } = {}) {
|
|
7786
|
+
let nodes = $generateNodesFromDOM(editor, doc);
|
|
7716
7787
|
if ($hasUpdateTag(PASTE_TAG)) nodes = $convertInlineImageDataURIs(nodes, this);
|
|
7717
7788
|
return filterDisallowedAttachmentNodes(nodes, this)
|
|
7718
7789
|
}
|
|
@@ -7754,7 +7825,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7754
7825
|
|
|
7755
7826
|
if (!this.editor) return
|
|
7756
7827
|
|
|
7757
|
-
this.#editorInitializedDispatched = true;
|
|
7758
7828
|
this.#dispatchEditorInitialized();
|
|
7759
7829
|
this.#dispatchAttributesChange();
|
|
7760
7830
|
}
|
|
@@ -7799,17 +7869,12 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7799
7869
|
}
|
|
7800
7870
|
|
|
7801
7871
|
get value() {
|
|
7802
|
-
|
|
7803
|
-
this.editor
|
|
7804
|
-
|
|
7805
|
-
});
|
|
7806
|
-
}
|
|
7807
|
-
|
|
7808
|
-
return this.cachedValue
|
|
7872
|
+
return this.cachedValue ??= this.editor?.read(() => {
|
|
7873
|
+
return sanitize($generateHtmlFromNodes(this.editor, null))
|
|
7874
|
+
}) ?? null
|
|
7809
7875
|
}
|
|
7810
7876
|
|
|
7811
7877
|
set value(html) {
|
|
7812
|
-
this.#valueLoaded = true;
|
|
7813
7878
|
const editorHasFocus = this.#isContentFocused;
|
|
7814
7879
|
|
|
7815
7880
|
this.editor.update(() => {
|
|
@@ -7821,11 +7886,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7821
7886
|
$addUpdateTag(SKIP_DOM_SELECTION_TAG);
|
|
7822
7887
|
}
|
|
7823
7888
|
|
|
7824
|
-
$getRoot()
|
|
7825
|
-
.clear()
|
|
7826
|
-
.selectEnd()
|
|
7827
|
-
.insertNodes(this.#parseHtmlIntoLexicalNodes(html));
|
|
7828
7889
|
|
|
7890
|
+
this.#setEditorHtml(html);
|
|
7829
7891
|
this.#toggleEmptyStatus();
|
|
7830
7892
|
}, { discrete: true });
|
|
7831
7893
|
}
|
|
@@ -7838,9 +7900,9 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7838
7900
|
return this.#historyState.redo
|
|
7839
7901
|
}
|
|
7840
7902
|
|
|
7841
|
-
#parseHtmlIntoLexicalNodes(html) {
|
|
7903
|
+
#parseHtmlIntoLexicalNodes(html, { editor = this.editor } = {}) {
|
|
7842
7904
|
if (!html) html = "<p></p>";
|
|
7843
|
-
const nodes = this.$generateNodesFromDOM(parseHtml(`${html}`));
|
|
7905
|
+
const nodes = this.$generateNodesFromDOM(parseHtml(`${html}`), { editor });
|
|
7844
7906
|
|
|
7845
7907
|
return nodes
|
|
7846
7908
|
.filter(this.#isNotWhitespaceOnlyNode)
|
|
@@ -7876,7 +7938,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7876
7938
|
this.#attachDebugHooks();
|
|
7877
7939
|
this.#attachToolbar();
|
|
7878
7940
|
this.#configureSanitizer();
|
|
7879
|
-
this.#loadInitialValue();
|
|
7880
7941
|
this.#resetBeforeTurboCaches();
|
|
7881
7942
|
}
|
|
7882
7943
|
|
|
@@ -7901,7 +7962,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7901
7962
|
nodes: this.#lexicalNodes,
|
|
7902
7963
|
html: {
|
|
7903
7964
|
export: new Map([ [ TextNode, exportTextNodeDOM ], [ CodeHighlightNode, exportTextNodeDOM ] ])
|
|
7904
|
-
}
|
|
7965
|
+
},
|
|
7966
|
+
$initialEditorState: (editor) => this.#loadInitialValue(editor)
|
|
7905
7967
|
},
|
|
7906
7968
|
...this.extensions.lexicalExtensions
|
|
7907
7969
|
);
|
|
@@ -7968,28 +8030,30 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7968
8030
|
return Array.from(this.attributes).filter(attribute => attribute.name.startsWith("aria-"))
|
|
7969
8031
|
}
|
|
7970
8032
|
|
|
7971
|
-
|
|
7972
|
-
const changed = this.#
|
|
8033
|
+
#setInternalFormValue(html) {
|
|
8034
|
+
const changed = this.#previousInternalFormValue !== null && html !== this.#previousInternalFormValue;
|
|
7973
8035
|
|
|
7974
8036
|
this.internals.setFormValue(html);
|
|
7975
|
-
this
|
|
8037
|
+
this.#previousInternalFormValue = html;
|
|
7976
8038
|
|
|
7977
8039
|
if (changed) {
|
|
7978
8040
|
dispatch(this, "lexxy:change");
|
|
7979
8041
|
}
|
|
7980
8042
|
}
|
|
7981
8043
|
|
|
7982
|
-
|
|
7983
|
-
|
|
8044
|
+
#loadInitialValue(editor) {
|
|
8045
|
+
const initialHtml = this.valueBeforeDisconnect || this.getAttribute("value") || "<p><br></p>";
|
|
8046
|
+
|
|
8047
|
+
this.#initialValue = initialHtml;
|
|
8048
|
+
this.#setInternalFormValue(initialHtml);
|
|
8049
|
+
this.#setEditorHtml(initialHtml, { editor });
|
|
7984
8050
|
}
|
|
7985
8051
|
|
|
7986
|
-
#
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
}, { tag: HISTORY_MERGE_TAG });
|
|
7992
|
-
}
|
|
8052
|
+
#setEditorHtml(html, { editor = this.editor } = { }) {
|
|
8053
|
+
$getRoot()
|
|
8054
|
+
.clear()
|
|
8055
|
+
.selectEnd()
|
|
8056
|
+
.insertNodes(this.#parseHtmlIntoLexicalNodes(html, { editor }));
|
|
7993
8057
|
}
|
|
7994
8058
|
|
|
7995
8059
|
#resetBeforeTurboCaches() {
|
|
@@ -8007,7 +8071,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8007
8071
|
#synchronizeWithChanges() {
|
|
8008
8072
|
this.#listeners.track(this.editor.registerUpdateListener(({ editorState }) => {
|
|
8009
8073
|
this.#clearCachedValues();
|
|
8010
|
-
this.#
|
|
8074
|
+
this.#setInternalFormValue(this.value);
|
|
8011
8075
|
this.#toggleEmptyStatus();
|
|
8012
8076
|
this.#requestValidityRefresh();
|
|
8013
8077
|
this.#dispatchAttributesChange();
|
|
@@ -8265,6 +8329,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8265
8329
|
#dispatchEditorInitialized() {
|
|
8266
8330
|
if (!this.adapter) return
|
|
8267
8331
|
|
|
8332
|
+
this.#editorInitializedDispatched = true;
|
|
8333
|
+
|
|
8268
8334
|
this.adapter.dispatchEditorInitialized({
|
|
8269
8335
|
highlightColors: this.#resolvedHighlightColors,
|
|
8270
8336
|
headingFormats: this.#supportedHeadingFormats
|
|
@@ -8279,7 +8345,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8279
8345
|
}
|
|
8280
8346
|
|
|
8281
8347
|
if (!this.#editorInitializedDispatched) {
|
|
8282
|
-
this.#editorInitializedDispatched = true;
|
|
8283
8348
|
this.#dispatchEditorInitialized();
|
|
8284
8349
|
}
|
|
8285
8350
|
}
|
|
@@ -8553,6 +8618,9 @@ class RemoteFilterSource extends BaseSource {
|
|
|
8553
8618
|
const NOTHING_FOUND_DEFAULT_MESSAGE = "Nothing found";
|
|
8554
8619
|
const FILTER_DEBOUNCE_INTERVAL = 50;
|
|
8555
8620
|
|
|
8621
|
+
// Start of line, or after a space or newline.
|
|
8622
|
+
const DEFAULT_ONLY_AT_PATTERN = "^|[ \\n]";
|
|
8623
|
+
|
|
8556
8624
|
class LexicalPromptElement extends HTMLElement {
|
|
8557
8625
|
#globalListeners = new ListenerBin()
|
|
8558
8626
|
#popoverListeners = new ListenerBin()
|
|
@@ -8598,6 +8666,14 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8598
8666
|
return this.hasAttribute("supports-space-in-searches")
|
|
8599
8667
|
}
|
|
8600
8668
|
|
|
8669
|
+
get onlyAt() {
|
|
8670
|
+
return this.getAttribute("only-at")
|
|
8671
|
+
}
|
|
8672
|
+
|
|
8673
|
+
get verticalDirection() {
|
|
8674
|
+
return this.getAttribute("vertical-direction")
|
|
8675
|
+
}
|
|
8676
|
+
|
|
8601
8677
|
get open() {
|
|
8602
8678
|
return this.popoverElement?.classList?.contains("lexxy-prompt-menu--visible")
|
|
8603
8679
|
}
|
|
@@ -8641,14 +8717,10 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8641
8717
|
if (offset >= triggerLength) {
|
|
8642
8718
|
const textBeforeCursor = fullText.slice(offset - triggerLength, offset);
|
|
8643
8719
|
|
|
8644
|
-
// Check if trigger is at the start of the text node (new line case) or preceded by space or newline
|
|
8645
8720
|
if (textBeforeCursor === this.trigger) {
|
|
8646
|
-
const
|
|
8647
|
-
|
|
8648
|
-
const charBeforeTrigger = offset > triggerLength ? fullText[offset - triggerLength - 1] : null;
|
|
8649
|
-
const isPrecededBySpaceOrNewline = charBeforeTrigger === " " || charBeforeTrigger === "\n";
|
|
8721
|
+
const textBeforeTrigger = $textBeforeOffset(node, offset - triggerLength);
|
|
8650
8722
|
|
|
8651
|
-
if (
|
|
8723
|
+
if (this.#onlyAtRegExp.test(textBeforeTrigger)) {
|
|
8652
8724
|
this.#popoverListeners.dispose();
|
|
8653
8725
|
this.#showPopover();
|
|
8654
8726
|
}
|
|
@@ -8659,7 +8731,15 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8659
8731
|
}));
|
|
8660
8732
|
}
|
|
8661
8733
|
|
|
8734
|
+
get #onlyAtRegExp() {
|
|
8735
|
+
return new RegExp(`(?:${this.onlyAt ?? DEFAULT_ONLY_AT_PATTERN})$`)
|
|
8736
|
+
}
|
|
8737
|
+
|
|
8662
8738
|
get #promptContentTypePermitted() {
|
|
8739
|
+
// `insert-editable-text` prompts never create attachments, so the
|
|
8740
|
+
// editor's attachment support and content-type allowlist don't apply.
|
|
8741
|
+
if (this.hasAttribute("insert-editable-text")) return true
|
|
8742
|
+
|
|
8663
8743
|
const el = this.#editorElement;
|
|
8664
8744
|
if (!el.supportsAttachments) {
|
|
8665
8745
|
return false
|
|
@@ -8822,11 +8902,15 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8822
8902
|
|
|
8823
8903
|
const popoverRect = this.popoverElement.getBoundingClientRect();
|
|
8824
8904
|
|
|
8825
|
-
if (popoverRect.right >
|
|
8905
|
+
if (popoverRect.right > editorRect.right) {
|
|
8826
8906
|
this.popoverElement.toggleAttribute("data-clipped-at-right", true);
|
|
8827
8907
|
}
|
|
8828
8908
|
|
|
8829
|
-
|
|
8909
|
+
const forceTop = this.verticalDirection === "top";
|
|
8910
|
+
const forceBottom = this.verticalDirection === "bottom";
|
|
8911
|
+
const overflowsWindow = popoverRect.bottom > window.innerHeight;
|
|
8912
|
+
|
|
8913
|
+
if (!forceBottom && (forceTop || overflowsWindow)) {
|
|
8830
8914
|
this.#setPopoverOffsetY(contentRect.height - y + fontSize);
|
|
8831
8915
|
this.popoverElement.toggleAttribute("data-clipped-at-bottom", true);
|
|
8832
8916
|
}
|
|
@@ -28,8 +28,8 @@ import 'prismjs/components/prism-kotlin';
|
|
|
28
28
|
window.Prism ||= {};
|
|
29
29
|
window.Prism.manual = true;
|
|
30
30
|
|
|
31
|
-
function highlightCode() {
|
|
32
|
-
const elements =
|
|
31
|
+
function highlightCode(root = document) {
|
|
32
|
+
const elements = root.querySelectorAll("pre[data-language]:not([data-highlighted])");
|
|
33
33
|
|
|
34
34
|
elements.forEach(preElement => {
|
|
35
35
|
highlightElement(preElement);
|
|
@@ -37,6 +37,8 @@ function highlightCode() {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function highlightElement(preElement) {
|
|
40
|
+
if (preElement.dataset.highlighted === "true") return
|
|
41
|
+
|
|
40
42
|
const language = preElement.getAttribute("data-language");
|
|
41
43
|
let code = preElement.innerHTML.replace(/<br\s*\/?>/gi, "\n");
|
|
42
44
|
|
|
@@ -55,6 +57,8 @@ function highlightElement(preElement) {
|
|
|
55
57
|
if (highlights.length > 0) {
|
|
56
58
|
applyHighlightRanges(preElement, highlights);
|
|
57
59
|
}
|
|
60
|
+
|
|
61
|
+
preElement.dataset.highlighted = "true";
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
// Walk the DOM tree inside a <pre> element and build a list of
|
|
@@ -160,4 +164,4 @@ function collectTextNodes(root) {
|
|
|
160
164
|
return nodes
|
|
161
165
|
}
|
|
162
166
|
|
|
163
|
-
export { highlightCode };
|
|
167
|
+
export { highlightCode, highlightElement };
|