@37signals/lexxy 0.9.15-alpha.4 → 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.
@@ -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,7 +1,7 @@
1
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, $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, PASTE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, mergeRegister as mergeRegister$1, CLEAR_HISTORY_COMMAND, $onUpdate, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, INPUT_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
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';
@@ -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(`(?:^|\\b)${escapeForRegExp(normalizedMatch)}`));
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
 
@@ -5441,14 +5441,13 @@ class Contents {
5441
5441
  }
5442
5442
 
5443
5443
  insertDOM(doc, { tag } = {}) {
5444
- this.#unwrapPlaceholderAnchors(doc);
5445
-
5446
5444
  this.editor.update(() => {
5447
- if ($hasUpdateTag(PASTE_TAG)) this.#stripTableCellColorStyles(doc);
5445
+ if ($hasUpdateTag(PASTE_TAG)) this.#formatPastedDOM(doc);
5448
5446
 
5449
5447
  const nodes = this.editorElement.$generateNodesFromDOM(doc);
5450
- if (!this.#insertUploadNodes(nodes)) {
5451
- this.insertAtCursor(...nodes);
5448
+
5449
+ if (!$hasUpdateTag(PASTE_TAG) || !this.#dispatchPastedNodesCommand(nodes)) {
5450
+ this.#insertUploadNodes(nodes) || this.insertAtCursor(...nodes);
5452
5451
  }
5453
5452
  }, { tag });
5454
5453
  }
@@ -5768,6 +5767,17 @@ class Contents {
5768
5767
  });
5769
5768
  }
5770
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
+
5771
5781
  #insertNodeIfRoot(node) {
5772
5782
  const selection = $getSelection();
5773
5783
  if (!$isRangeSelection(selection)) return false
@@ -6062,10 +6072,10 @@ class Clipboard {
6062
6072
 
6063
6073
  #handleParsedClipboardNodes({ nodes, selection }) {
6064
6074
  const url = $bareUrlFromSingleLink(nodes);
6065
- if (!url) return false
6066
-
6067
- this.#insertSingleLinkAt(selection, url);
6068
- return true
6075
+ if (url && $isRangeSelection(selection)) {
6076
+ this.#insertSingleLinkAt(selection, url);
6077
+ return true
6078
+ }
6069
6079
  }
6070
6080
 
6071
6081
  #isPlainTextOrURLPasted(clipboardData) {
@@ -6146,10 +6156,7 @@ class Clipboard {
6146
6156
  const linkNode = $createLinkNode(url).append($createTextNode(url));
6147
6157
  selection.insertNodes([ linkNode ]);
6148
6158
 
6149
- // Defer the lexxy:insert-link event until after the active update commits;
6150
- // listeners may run editor mutations of their own.
6151
- const nodeKey = linkNode.getKey();
6152
- Promise.resolve().then(() => this.#dispatchLinkInsertEvent(nodeKey, { url }));
6159
+ $onUpdate(() => this.#dispatchLinkInsertEvent(linkNode.getKey(), { url }));
6153
6160
  }
6154
6161
 
6155
6162
  #dispatchLinkInsertEvent(nodeKey, payload) {
@@ -7605,9 +7612,10 @@ class LexicalEditorElement extends HTMLElement {
7605
7612
  static observedAttributes = [ "connected", "required" ]
7606
7613
 
7607
7614
  #initialValue = ""
7615
+ #previousInternalFormValue = null
7616
+
7608
7617
  #initializeEventDispatched = false
7609
7618
  #editorInitializedDispatched = false
7610
- #valueLoaded = false
7611
7619
  #listeners = new ListenerBin()
7612
7620
  #disposables = []
7613
7621
  #historyState = { undo: false, redo: false }
@@ -7659,12 +7667,11 @@ class LexicalEditorElement extends HTMLElement {
7659
7667
  disconnectedCallback() {
7660
7668
  this.#initializeEventDispatched = false;
7661
7669
  this.#editorInitializedDispatched = false;
7662
- if (this.#valueLoaded) {
7663
- this.valueBeforeDisconnect = this.value;
7664
- } else {
7665
- this.valueBeforeDisconnect = null;
7666
- }
7667
- this.#valueLoaded = false;
7670
+
7671
+ this.#previousInternalFormValue = null;
7672
+ this.valueBeforeDisconnect = this.value;
7673
+
7674
+ this.#clearCachedValues();
7668
7675
  this.#reset(); // Prevent hangs with Safari when morphing
7669
7676
  }
7670
7677
 
@@ -7689,13 +7696,9 @@ class LexicalEditorElement extends HTMLElement {
7689
7696
  }
7690
7697
 
7691
7698
  toString() {
7692
- if (this.cachedStringValue == null) {
7693
- this.editor?.getEditorState().read(() => {
7694
- this.cachedStringValue = $getReadableTextContent($getRoot());
7695
- });
7696
- }
7697
-
7698
- return this.cachedStringValue
7699
+ return this.cachedStringValue ??= this.editor?.read(() => {
7700
+ return $getReadableTextContent($getRoot())
7701
+ })
7699
7702
  }
7700
7703
 
7701
7704
  get form() {
@@ -7779,8 +7782,8 @@ class LexicalEditorElement extends HTMLElement {
7779
7782
  return dispatch(this, "lexxy:file-accept", { file }, true)
7780
7783
  }
7781
7784
 
7782
- $generateNodesFromDOM(doc) {
7783
- let nodes = $generateNodesFromDOM(this.editor, doc);
7785
+ $generateNodesFromDOM(doc, { editor = this.editor } = {}) {
7786
+ let nodes = $generateNodesFromDOM(editor, doc);
7784
7787
  if ($hasUpdateTag(PASTE_TAG)) nodes = $convertInlineImageDataURIs(nodes, this);
7785
7788
  return filterDisallowedAttachmentNodes(nodes, this)
7786
7789
  }
@@ -7822,7 +7825,6 @@ class LexicalEditorElement extends HTMLElement {
7822
7825
 
7823
7826
  if (!this.editor) return
7824
7827
 
7825
- this.#editorInitializedDispatched = true;
7826
7828
  this.#dispatchEditorInitialized();
7827
7829
  this.#dispatchAttributesChange();
7828
7830
  }
@@ -7867,17 +7869,12 @@ class LexicalEditorElement extends HTMLElement {
7867
7869
  }
7868
7870
 
7869
7871
  get value() {
7870
- if (!this.cachedValue) {
7871
- this.editor?.getEditorState().read(() => {
7872
- this.cachedValue = sanitize($generateHtmlFromNodes(this.editor, null));
7873
- });
7874
- }
7875
-
7876
- return this.cachedValue
7872
+ return this.cachedValue ??= this.editor?.read(() => {
7873
+ return sanitize($generateHtmlFromNodes(this.editor, null))
7874
+ }) ?? null
7877
7875
  }
7878
7876
 
7879
7877
  set value(html) {
7880
- this.#valueLoaded = true;
7881
7878
  const editorHasFocus = this.#isContentFocused;
7882
7879
 
7883
7880
  this.editor.update(() => {
@@ -7889,11 +7886,8 @@ class LexicalEditorElement extends HTMLElement {
7889
7886
  $addUpdateTag(SKIP_DOM_SELECTION_TAG);
7890
7887
  }
7891
7888
 
7892
- $getRoot()
7893
- .clear()
7894
- .selectEnd()
7895
- .insertNodes(this.#parseHtmlIntoLexicalNodes(html));
7896
7889
 
7890
+ this.#setEditorHtml(html);
7897
7891
  this.#toggleEmptyStatus();
7898
7892
  }, { discrete: true });
7899
7893
  }
@@ -7906,9 +7900,9 @@ class LexicalEditorElement extends HTMLElement {
7906
7900
  return this.#historyState.redo
7907
7901
  }
7908
7902
 
7909
- #parseHtmlIntoLexicalNodes(html) {
7903
+ #parseHtmlIntoLexicalNodes(html, { editor = this.editor } = {}) {
7910
7904
  if (!html) html = "<p></p>";
7911
- const nodes = this.$generateNodesFromDOM(parseHtml(`${html}`));
7905
+ const nodes = this.$generateNodesFromDOM(parseHtml(`${html}`), { editor });
7912
7906
 
7913
7907
  return nodes
7914
7908
  .filter(this.#isNotWhitespaceOnlyNode)
@@ -7944,7 +7938,6 @@ class LexicalEditorElement extends HTMLElement {
7944
7938
  this.#attachDebugHooks();
7945
7939
  this.#attachToolbar();
7946
7940
  this.#configureSanitizer();
7947
- this.#loadInitialValue();
7948
7941
  this.#resetBeforeTurboCaches();
7949
7942
  }
7950
7943
 
@@ -7969,7 +7962,8 @@ class LexicalEditorElement extends HTMLElement {
7969
7962
  nodes: this.#lexicalNodes,
7970
7963
  html: {
7971
7964
  export: new Map([ [ TextNode, exportTextNodeDOM ], [ CodeHighlightNode, exportTextNodeDOM ] ])
7972
- }
7965
+ },
7966
+ $initialEditorState: (editor) => this.#loadInitialValue(editor)
7973
7967
  },
7974
7968
  ...this.extensions.lexicalExtensions
7975
7969
  );
@@ -8036,28 +8030,30 @@ class LexicalEditorElement extends HTMLElement {
8036
8030
  return Array.from(this.attributes).filter(attribute => attribute.name.startsWith("aria-"))
8037
8031
  }
8038
8032
 
8039
- set #internalFormValue(html) {
8040
- const changed = this.#internalFormValue !== undefined && this.#internalFormValue !== this.value;
8033
+ #setInternalFormValue(html) {
8034
+ const changed = this.#previousInternalFormValue !== null && html !== this.#previousInternalFormValue;
8041
8035
 
8042
8036
  this.internals.setFormValue(html);
8043
- this._internalFormValue = html;
8037
+ this.#previousInternalFormValue = html;
8044
8038
 
8045
8039
  if (changed) {
8046
8040
  dispatch(this, "lexxy:change");
8047
8041
  }
8048
8042
  }
8049
8043
 
8050
- get #internalFormValue() {
8051
- return this._internalFormValue
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 });
8052
8050
  }
8053
8051
 
8054
- #loadInitialValue() {
8055
- if (!this.#valueLoaded) {
8056
- const initialHtml = this.valueBeforeDisconnect || this.getAttribute("value") || "<p><br></p>";
8057
- this.editor.update(() => {
8058
- this.value = this.#initialValue = initialHtml;
8059
- }, { tag: HISTORY_MERGE_TAG });
8060
- }
8052
+ #setEditorHtml(html, { editor = this.editor } = { }) {
8053
+ $getRoot()
8054
+ .clear()
8055
+ .selectEnd()
8056
+ .insertNodes(this.#parseHtmlIntoLexicalNodes(html, { editor }));
8061
8057
  }
8062
8058
 
8063
8059
  #resetBeforeTurboCaches() {
@@ -8075,7 +8071,7 @@ class LexicalEditorElement extends HTMLElement {
8075
8071
  #synchronizeWithChanges() {
8076
8072
  this.#listeners.track(this.editor.registerUpdateListener(({ editorState }) => {
8077
8073
  this.#clearCachedValues();
8078
- this.#internalFormValue = this.value;
8074
+ this.#setInternalFormValue(this.value);
8079
8075
  this.#toggleEmptyStatus();
8080
8076
  this.#requestValidityRefresh();
8081
8077
  this.#dispatchAttributesChange();
@@ -8333,6 +8329,8 @@ class LexicalEditorElement extends HTMLElement {
8333
8329
  #dispatchEditorInitialized() {
8334
8330
  if (!this.adapter) return
8335
8331
 
8332
+ this.#editorInitializedDispatched = true;
8333
+
8336
8334
  this.adapter.dispatchEditorInitialized({
8337
8335
  highlightColors: this.#resolvedHighlightColors,
8338
8336
  headingFormats: this.#supportedHeadingFormats
@@ -8347,7 +8345,6 @@ class LexicalEditorElement extends HTMLElement {
8347
8345
  }
8348
8346
 
8349
8347
  if (!this.#editorInitializedDispatched) {
8350
- this.#editorInitializedDispatched = true;
8351
8348
  this.#dispatchEditorInitialized();
8352
8349
  }
8353
8350
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.15-alpha.4",
3
+ "version": "0.9.16",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",