@37signals/lexxy 0.9.17 → 0.9.18

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/lexxy.esm.js CHANGED
@@ -7898,8 +7898,8 @@ class LexicalEditorElement extends HTMLElement {
7898
7898
  return this.#historyState.redo
7899
7899
  }
7900
7900
 
7901
- #readSanitizedEditorValue(editor = this.editor) {
7902
- return editor?.read(() => {
7901
+ #readSanitizedEditorValue() {
7902
+ return this.editor?.read(() => {
7903
7903
  return sanitize($generateHtmlFromNodes(this.editor, null))
7904
7904
  }) ?? null
7905
7905
  }
@@ -7933,7 +7933,6 @@ class LexicalEditorElement extends HTMLElement {
7933
7933
  }
7934
7934
 
7935
7935
  #initialize() {
7936
- this.#synchronizeWithChanges();
7937
7936
  this.#registerComponents();
7938
7937
  this.#handleEnter();
7939
7938
  this.#registerFocusEvents();
@@ -7942,6 +7941,9 @@ class LexicalEditorElement extends HTMLElement {
7942
7941
  this.#attachDebugHooks();
7943
7942
  this.#attachToolbar();
7944
7943
  this.#resetBeforeTurboCaches();
7944
+
7945
+ this.#setInternalFormValue(this.value, { suppressEvent: true });
7946
+ this.#synchronizeWithChanges();
7945
7947
  }
7946
7948
 
7947
7949
  #registerFileAcceptFilter() {
@@ -7969,7 +7971,6 @@ class LexicalEditorElement extends HTMLElement {
7969
7971
  $initialEditorState: (editor) => {
7970
7972
  this.#configureSanitizer(editor);
7971
7973
  this.#loadInitialValue(editor);
7972
- this.#setInternalFormValue(this.#readSanitizedEditorValue(editor));
7973
7974
  },
7974
7975
  },
7975
7976
  ...this.extensions.lexicalExtensions
@@ -8037,13 +8038,13 @@ class LexicalEditorElement extends HTMLElement {
8037
8038
  return Array.from(this.attributes).filter(attribute => attribute.name.startsWith("aria-"))
8038
8039
  }
8039
8040
 
8040
- #setInternalFormValue(html) {
8041
- const changed = this.#previousInternalFormValue !== null && html !== this.#previousInternalFormValue;
8041
+ #setInternalFormValue(html, { suppressEvent = false } = {}) {
8042
+ const changed = html !== this.#previousInternalFormValue;
8042
8043
 
8043
8044
  this.internals.setFormValue(html);
8044
8045
  this.#previousInternalFormValue = html;
8045
8046
 
8046
- if (changed) {
8047
+ if (changed && !suppressEvent) {
8047
8048
  dispatch(this, "lexxy:change");
8048
8049
  }
8049
8050
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.17",
3
+ "version": "0.9.18",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",
@@ -1,344 +0,0 @@
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 };