@37signals/lexxy 0.9.17 → 0.9.19-alpha.1
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 +107 -23
- package/package.json +1 -1
- package/dist/lexical.esm.js +0 -344
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, 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';
|
|
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, $normalizeSelection__EXPERIMENTAL, 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';
|
|
@@ -4031,9 +4031,12 @@ class Selection {
|
|
|
4031
4031
|
}
|
|
4032
4032
|
|
|
4033
4033
|
get isOnPreviewableImage() {
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4034
|
+
return this.previewableImageNode != null
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
get previewableImageNode() {
|
|
4038
|
+
const firstNode = $getSelection()?.getNodes().at(0);
|
|
4039
|
+
return $isActionTextAttachmentNode(firstNode) && firstNode.isPreviewableImage ? firstNode : null
|
|
4037
4040
|
}
|
|
4038
4041
|
|
|
4039
4042
|
get isAtNodeStart() {
|
|
@@ -5008,7 +5011,10 @@ class GalleryUploader extends Uploader {
|
|
|
5008
5011
|
|
|
5009
5012
|
#findOrCreateGallery() {
|
|
5010
5013
|
if (this.selection.isOnPreviewableImage) {
|
|
5011
|
-
|
|
5014
|
+
// Resolve from the previewable image itself (the selection's first node), not from
|
|
5015
|
+
// #selectedNode (the anchor) — those differ when the selection runs from an image
|
|
5016
|
+
// into following text, and the anchor text node can't join a gallery (returns null).
|
|
5017
|
+
this.#gallery = $findOrCreateGalleryForImage(this.selection.previewableImageNode);
|
|
5012
5018
|
} else if (this.#selectionIsAfterGalleryEdge) {
|
|
5013
5019
|
this.#gallery = $findOrCreateGalleryForImage(this.selection.nodeBeforeCursor);
|
|
5014
5020
|
} else {
|
|
@@ -5345,7 +5351,8 @@ class NodeInserter {
|
|
|
5345
5351
|
const INSERTERS = [
|
|
5346
5352
|
CodeNodeInserter,
|
|
5347
5353
|
ShadowRootNodeInserter,
|
|
5348
|
-
NodeSelectionNodeInserter
|
|
5354
|
+
NodeSelectionNodeInserter,
|
|
5355
|
+
BlockContainerNodeInserter
|
|
5349
5356
|
];
|
|
5350
5357
|
const Inserter = INSERTERS.find(inserter => inserter.handles(selection));
|
|
5351
5358
|
return Inserter ? new Inserter(selection) : selection
|
|
@@ -5371,14 +5378,40 @@ class CodeNodeInserter extends NodeInserter {
|
|
|
5371
5378
|
|
|
5372
5379
|
const caret = $getChildCaretAtIndex(codeNode, insertionIndex + 1, "previous");
|
|
5373
5380
|
|
|
5381
|
+
// Nodes that are already in the document come from the format-toggle path (existing
|
|
5382
|
+
// content converted into this code block). Brand-new nodes (dropped/pasted content)
|
|
5383
|
+
// were never attached.
|
|
5384
|
+
const existingNodes = new Set(nodes.filter(node => node.isAttached()));
|
|
5385
|
+
const trailingNodes = [];
|
|
5386
|
+
|
|
5374
5387
|
for (const node of nodes) {
|
|
5375
|
-
if (
|
|
5376
|
-
|
|
5388
|
+
if (existingNodes.has(node)) {
|
|
5389
|
+
if (!node.isAttached()) continue // already pulled in when a converted ancestor was removed
|
|
5390
|
+
} else if (!this.#canJoinCodeBlock(node)) {
|
|
5391
|
+
trailingNodes.push(node); // e.g. a dropped attachment, which a code block can't hold
|
|
5392
|
+
continue
|
|
5393
|
+
}
|
|
5377
5394
|
|
|
5395
|
+
if (caret.getNodeAtCaret() && $isElementNode(node)) { caret.insert($createLineBreakNode()); }
|
|
5378
5396
|
caret.insert(this.#convertNodeToCodeChild(node));
|
|
5379
5397
|
}
|
|
5380
5398
|
|
|
5381
|
-
|
|
5399
|
+
const lastTrailingNode = this.#insertAfterCodeBlock(codeNode, trailingNodes);
|
|
5400
|
+
const nodeToSelect = lastTrailingNode ?? caret.getNodeAtCaret();
|
|
5401
|
+
nodeToSelect?.selectEnd();
|
|
5402
|
+
}
|
|
5403
|
+
|
|
5404
|
+
#canJoinCodeBlock(node) {
|
|
5405
|
+
return $isTextNode(node) || $isLineBreakNode(node)
|
|
5406
|
+
}
|
|
5407
|
+
|
|
5408
|
+
#insertAfterCodeBlock(codeNode, nodes) {
|
|
5409
|
+
let previousNode = codeNode;
|
|
5410
|
+
for (const node of nodes) {
|
|
5411
|
+
previousNode.insertAfter(node);
|
|
5412
|
+
previousNode = node;
|
|
5413
|
+
}
|
|
5414
|
+
return nodes.at(-1)
|
|
5382
5415
|
}
|
|
5383
5416
|
|
|
5384
5417
|
#convertNodeToCodeChild(node) {
|
|
@@ -5394,7 +5427,7 @@ class CodeNodeInserter extends NodeInserter {
|
|
|
5394
5427
|
|
|
5395
5428
|
class ShadowRootNodeInserter extends NodeInserter {
|
|
5396
5429
|
static handles(selection) {
|
|
5397
|
-
return $isShadowRoot(selection?.anchor
|
|
5430
|
+
return $isShadowRoot(selection?.anchor?.getNode())
|
|
5398
5431
|
}
|
|
5399
5432
|
|
|
5400
5433
|
insertNodes(nodes) {
|
|
@@ -5423,6 +5456,31 @@ class NodeSelectionNodeInserter extends NodeInserter {
|
|
|
5423
5456
|
}
|
|
5424
5457
|
}
|
|
5425
5458
|
|
|
5459
|
+
// Lexical's RangeSelection.insertNodes requires every selection point to have a block
|
|
5460
|
+
// ancestor with inline children. An element point on a container of block nodes — e.g.
|
|
5461
|
+
// a quote holding paragraphs — has none, so Lexical throws invariant #211 or #212.
|
|
5462
|
+
// Descend such points to a leaf position before inserting.
|
|
5463
|
+
class BlockContainerNodeInserter extends NodeInserter {
|
|
5464
|
+
static handles(selection) {
|
|
5465
|
+
return $isRangeSelection(selection) &&
|
|
5466
|
+
[ selection.anchor, selection.focus ].some($isPointOnBlockContainer)
|
|
5467
|
+
}
|
|
5468
|
+
|
|
5469
|
+
insertNodes(nodes) {
|
|
5470
|
+
$normalizeSelection__EXPERIMENTAL(this.selection);
|
|
5471
|
+
this.selection.insertNodes(nodes);
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5475
|
+
function $isPointOnBlockContainer(point) {
|
|
5476
|
+
if (point.type === "element") {
|
|
5477
|
+
const firstChild = point.getNode().getFirstChild();
|
|
5478
|
+
return ($isElementNode(firstChild) || $isDecoratorNode(firstChild)) && !firstChild.isInline()
|
|
5479
|
+
} else {
|
|
5480
|
+
return false
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
|
|
5426
5484
|
class Contents {
|
|
5427
5485
|
constructor(editorElement) {
|
|
5428
5486
|
this.editorElement = editorElement;
|
|
@@ -5519,7 +5577,7 @@ class Contents {
|
|
|
5519
5577
|
blockElements.forEach(node => this.#unwrapCodeBlock(node));
|
|
5520
5578
|
} else {
|
|
5521
5579
|
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5522
|
-
const elements = this.#blockLevelElementsInSelection(selection);
|
|
5580
|
+
const elements = this.#outermostElements(this.#blockLevelElementsInSelection(selection));
|
|
5523
5581
|
if (elements.length === 0) return
|
|
5524
5582
|
|
|
5525
5583
|
const codeNode = $createCodeNode("plain");
|
|
@@ -5655,6 +5713,8 @@ class Contents {
|
|
|
5655
5713
|
}
|
|
5656
5714
|
|
|
5657
5715
|
uploadFiles(files, { selectLast } = {}) {
|
|
5716
|
+
if (!this.editorElement) return // Disposed (e.g. on turbo:before-cache); a late drop can still land here
|
|
5717
|
+
|
|
5658
5718
|
if (!this.editorElement.supportsAttachments) {
|
|
5659
5719
|
console.warn("This editor does not supports attachments (it's configured with [attachments=false])");
|
|
5660
5720
|
return
|
|
@@ -5845,6 +5905,18 @@ class Contents {
|
|
|
5845
5905
|
return Array.from(elements)
|
|
5846
5906
|
}
|
|
5847
5907
|
|
|
5908
|
+
// Selections spanning nested structures (a quote and its inner paragraphs,
|
|
5909
|
+
// nested list items) yield both an element and its ancestor. Converting the
|
|
5910
|
+
// ancestor detaches its whole subtree — including a node freshly inserted
|
|
5911
|
+
// inside it — which can leave the selection on removed nodes (Lexical
|
|
5912
|
+
// invariant #19). The outermost elements already cover their descendants'
|
|
5913
|
+
// text content, so keep only those.
|
|
5914
|
+
#outermostElements(elements) {
|
|
5915
|
+
return elements.filter((element) => {
|
|
5916
|
+
return elements.every((other) => other === element || !element.getParents().includes(other))
|
|
5917
|
+
})
|
|
5918
|
+
}
|
|
5919
|
+
|
|
5848
5920
|
#insertUploadNodes(nodes) {
|
|
5849
5921
|
if (nodes.every($isActionTextAttachmentNode)) {
|
|
5850
5922
|
const uploader = Uploader.for(this.editorElement, []);
|
|
@@ -7372,12 +7444,23 @@ class EarlyEscapeListItemNode extends ListItemNode {
|
|
|
7372
7444
|
return super.insertNewAfter(selection, restoreSelection)
|
|
7373
7445
|
}
|
|
7374
7446
|
|
|
7447
|
+
get #isInBlockquote() {
|
|
7448
|
+
return Boolean($getNearestNodeOfType(this, QuoteNode))
|
|
7449
|
+
}
|
|
7450
|
+
|
|
7375
7451
|
#shouldEscape(selection) {
|
|
7376
|
-
if (
|
|
7377
|
-
|
|
7452
|
+
if (this.#isInPasteOperation() || !this.#isInBlockquote) {
|
|
7453
|
+
return false
|
|
7454
|
+
} else if ($isBlankNode(this)) {
|
|
7455
|
+
return true
|
|
7456
|
+
} else {
|
|
7457
|
+
const paragraph = $getNearestNodeOfType(selection.anchor.getNode(), ParagraphNode);
|
|
7458
|
+
return paragraph && $isBlankNode(paragraph) && $isListItemNode(paragraph.getParent())
|
|
7459
|
+
}
|
|
7460
|
+
}
|
|
7378
7461
|
|
|
7379
|
-
|
|
7380
|
-
return
|
|
7462
|
+
#isInPasteOperation() {
|
|
7463
|
+
return $hasUpdateTag(PASTE_TAG)
|
|
7381
7464
|
}
|
|
7382
7465
|
|
|
7383
7466
|
#escapeFromList() {
|
|
@@ -7425,7 +7508,7 @@ class FormatEscapeExtension extends LexxyExtension {
|
|
|
7425
7508
|
}
|
|
7426
7509
|
|
|
7427
7510
|
get allowedElements() {
|
|
7428
|
-
return [ { tag: "li", attributes: [ "value" ] } ]
|
|
7511
|
+
return [ { tag: "ol", attributes: [ "start" ] }, { tag: "li", attributes: [ "value" ] } ]
|
|
7429
7512
|
}
|
|
7430
7513
|
|
|
7431
7514
|
get lexicalExtension() {
|
|
@@ -7898,8 +7981,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7898
7981
|
return this.#historyState.redo
|
|
7899
7982
|
}
|
|
7900
7983
|
|
|
7901
|
-
#readSanitizedEditorValue(
|
|
7902
|
-
return editor?.read(() => {
|
|
7984
|
+
#readSanitizedEditorValue() {
|
|
7985
|
+
return this.editor?.read(() => {
|
|
7903
7986
|
return sanitize($generateHtmlFromNodes(this.editor, null))
|
|
7904
7987
|
}) ?? null
|
|
7905
7988
|
}
|
|
@@ -7933,7 +8016,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7933
8016
|
}
|
|
7934
8017
|
|
|
7935
8018
|
#initialize() {
|
|
7936
|
-
this.#synchronizeWithChanges();
|
|
7937
8019
|
this.#registerComponents();
|
|
7938
8020
|
this.#handleEnter();
|
|
7939
8021
|
this.#registerFocusEvents();
|
|
@@ -7942,6 +8024,9 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7942
8024
|
this.#attachDebugHooks();
|
|
7943
8025
|
this.#attachToolbar();
|
|
7944
8026
|
this.#resetBeforeTurboCaches();
|
|
8027
|
+
|
|
8028
|
+
this.#setInternalFormValue(this.value, { suppressEvent: true });
|
|
8029
|
+
this.#synchronizeWithChanges();
|
|
7945
8030
|
}
|
|
7946
8031
|
|
|
7947
8032
|
#registerFileAcceptFilter() {
|
|
@@ -7969,7 +8054,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7969
8054
|
$initialEditorState: (editor) => {
|
|
7970
8055
|
this.#configureSanitizer(editor);
|
|
7971
8056
|
this.#loadInitialValue(editor);
|
|
7972
|
-
this.#setInternalFormValue(this.#readSanitizedEditorValue(editor));
|
|
7973
8057
|
},
|
|
7974
8058
|
},
|
|
7975
8059
|
...this.extensions.lexicalExtensions
|
|
@@ -8037,13 +8121,13 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8037
8121
|
return Array.from(this.attributes).filter(attribute => attribute.name.startsWith("aria-"))
|
|
8038
8122
|
}
|
|
8039
8123
|
|
|
8040
|
-
#setInternalFormValue(html) {
|
|
8041
|
-
const changed =
|
|
8124
|
+
#setInternalFormValue(html, { suppressEvent = false } = {}) {
|
|
8125
|
+
const changed = html !== this.#previousInternalFormValue;
|
|
8042
8126
|
|
|
8043
8127
|
this.internals.setFormValue(html);
|
|
8044
8128
|
this.#previousInternalFormValue = html;
|
|
8045
8129
|
|
|
8046
|
-
if (changed) {
|
|
8130
|
+
if (changed && !suppressEvent) {
|
|
8047
8131
|
dispatch(this, "lexxy:change");
|
|
8048
8132
|
}
|
|
8049
8133
|
}
|
package/package.json
CHANGED
package/dist/lexical.esm.js
DELETED
|
@@ -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 };
|