@37signals/lexxy 0.9.18 → 0.9.19-alpha.2
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 +403 -93
- package/package.json +4 -3
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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,
|
|
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, $normalizeSelection__EXPERIMENTAL, $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, INSERT_LINE_BREAK_COMMAND, COMMAND_PRIORITY_HIGH, INSERT_PARAGRAPH_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, DRAGSTART_COMMAND, DROP_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
|
-
import { ListNode, ListItemNode, $getListDepth, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $
|
|
7
|
+
import { ListNode, ListItemNode, $getListDepth, $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $createListNode, $createListItemNode, registerList } from '@lexical/list';
|
|
8
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
|
-
import { RichTextExtension, $isQuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode,
|
|
10
|
+
import { RichTextExtension, $isQuoteNode, QuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
11
11
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
12
12
|
import { HistoryExtension } from '@lexical/history';
|
|
13
13
|
import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, normalizeCodeLang, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
@@ -1534,8 +1534,12 @@ function $isSafeForRoot(node) {
|
|
|
1534
1534
|
function $makeSafeForRoot(node) {
|
|
1535
1535
|
if ($isSafeForRoot(node)) {
|
|
1536
1536
|
return node
|
|
1537
|
-
} else {
|
|
1537
|
+
} else if (node.getParent()) {
|
|
1538
1538
|
return $wrapNodeInElement(node, () => node.createParentElementNode())
|
|
1539
|
+
} else {
|
|
1540
|
+
// Detached nodes (e.g. clipboard nodes being inserted) can't be `replace`d in place,
|
|
1541
|
+
// so append them into a fresh required parent instead.
|
|
1542
|
+
return node.createParentElementNode().append(node)
|
|
1539
1543
|
}
|
|
1540
1544
|
}
|
|
1541
1545
|
|
|
@@ -1723,7 +1727,7 @@ function $splitSelectedParagraphsAtInnerLineBreaks(selection) {
|
|
|
1723
1727
|
}
|
|
1724
1728
|
}
|
|
1725
1729
|
|
|
1726
|
-
function $expandSelectionToLineBreaksAndSplitAtEdges(selection) {
|
|
1730
|
+
function $expandSelectionToLineBreaksAndSplitAtEdges(selection, fallbackAncestor = (node) => node.getTopLevelElement()) {
|
|
1727
1731
|
$ensureForwardRangeSelection(selection);
|
|
1728
1732
|
|
|
1729
1733
|
const focusCaret = $caretFromPoint(selection.focus, "next");
|
|
@@ -1743,8 +1747,8 @@ function $expandSelectionToLineBreaksAndSplitAtEdges(selection) {
|
|
|
1743
1747
|
const focusOuter = focusBrCaret && $splitAroundLineBreak(focusBrCaret);
|
|
1744
1748
|
const anchorOuter = anchorBrCaret && $splitAroundLineBreak(anchorBrCaret);
|
|
1745
1749
|
|
|
1746
|
-
const innerStart = anchorOuter?.getNextSibling() ?? selection.anchor.getNode()
|
|
1747
|
-
const innerEnd = focusOuter?.getPreviousSibling() ?? selection.focus.getNode()
|
|
1750
|
+
const innerStart = anchorOuter?.getNextSibling() ?? fallbackAncestor(selection.anchor.getNode());
|
|
1751
|
+
const innerEnd = focusOuter?.getPreviousSibling() ?? fallbackAncestor(selection.focus.getNode());
|
|
1748
1752
|
if (!innerStart || !innerEnd) return
|
|
1749
1753
|
|
|
1750
1754
|
$setSelectionFromCaretRange($getCaretRange(
|
|
@@ -1850,6 +1854,49 @@ function $splitAroundLineBreak(lineBreakCaret) {
|
|
|
1850
1854
|
return outer
|
|
1851
1855
|
}
|
|
1852
1856
|
|
|
1857
|
+
// Lexical's RangeSelection.insertNodes/insertLineBreak require every selection point to have a
|
|
1858
|
+
// block ancestor with inline children. An element point on a container of block nodes — e.g. a
|
|
1859
|
+
// quote holding paragraphs — has none, so Lexical throws invariant #211 or #212. This detects
|
|
1860
|
+
// such a point so callers can descend it to a leaf before inserting.
|
|
1861
|
+
function $isPointOnBlockContainer(point) {
|
|
1862
|
+
if (point.type !== "element") return false
|
|
1863
|
+
|
|
1864
|
+
const firstChild = point.getNode().getFirstChild();
|
|
1865
|
+
return ($isElementNode(firstChild) || $isDecoratorNode(firstChild)) && !firstChild.isInline()
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function $hasPointOnBlockContainer(selection) {
|
|
1869
|
+
return $isRangeSelection(selection) &&
|
|
1870
|
+
[ selection.anchor, selection.focus ].some($isPointOnBlockContainer)
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// Descend any block-container element point in the selection to a leaf position, so a subsequent
|
|
1874
|
+
// Lexical insert (insertNodes, insertLineBreak, INSERT_PARAGRAPH) doesn't throw invariant #211/#212.
|
|
1875
|
+
function $normalizeBlockContainerSelection(selection = $getSelection()) {
|
|
1876
|
+
if (!$hasPointOnBlockContainer(selection)) return false
|
|
1877
|
+
|
|
1878
|
+
$normalizeSelection__EXPERIMENTAL(selection);
|
|
1879
|
+
return true
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function $consecutiveSiblingGroups(blocks) {
|
|
1883
|
+
const ordered = [ ...blocks ].sort((a, b) => a.getIndexWithinParent() - b.getIndexWithinParent());
|
|
1884
|
+
const groups = [];
|
|
1885
|
+
|
|
1886
|
+
for (const block of ordered) {
|
|
1887
|
+
const lastGroup = groups.at(-1);
|
|
1888
|
+
const previous = lastGroup?.at(-1);
|
|
1889
|
+
|
|
1890
|
+
if (previous && previous.getParent().is(block.getParent()) && previous.getNextSibling()?.is(block)) {
|
|
1891
|
+
lastGroup.push(block);
|
|
1892
|
+
} else {
|
|
1893
|
+
groups.push([ block ]);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
return groups
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1853
1900
|
// Payload: Record<nodeKey, { patch?, replace? }>
|
|
1854
1901
|
// - patch: plain object, shallow-merged into the existing node's properties
|
|
1855
1902
|
// - replace: a LexicalNode instance that replaces the node
|
|
@@ -3741,6 +3788,16 @@ class CommandDispatcher {
|
|
|
3741
3788
|
#registerKeyboardCommands() {
|
|
3742
3789
|
this.#registerCommandHandler(KEY_ARROW_RIGHT_COMMAND, COMMAND_PRIORITY_NORMAL, this.#handleArrowRightKey.bind(this));
|
|
3743
3790
|
this.#registerCommandHandler(KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, this.#handleTabKey.bind(this));
|
|
3791
|
+
|
|
3792
|
+
// Run before Lexical's built-in insert handlers to descend an element point on a
|
|
3793
|
+
// block container to a leaf, avoiding error #211 on Enter / Shift+Enter in a quote.
|
|
3794
|
+
this.#registerCommandHandler(INSERT_LINE_BREAK_COMMAND, COMMAND_PRIORITY_HIGH, this.#normalizeBlockContainerSelection.bind(this));
|
|
3795
|
+
this.#registerCommandHandler(INSERT_PARAGRAPH_COMMAND, COMMAND_PRIORITY_HIGH, this.#normalizeBlockContainerSelection.bind(this));
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
#normalizeBlockContainerSelection() {
|
|
3799
|
+
$normalizeBlockContainerSelection();
|
|
3800
|
+
return false
|
|
3744
3801
|
}
|
|
3745
3802
|
|
|
3746
3803
|
#handleArrowRightKey(event) {
|
|
@@ -4013,6 +4070,10 @@ class Selection {
|
|
|
4013
4070
|
return this.nearestNodeOfType(ListItemNode)
|
|
4014
4071
|
}
|
|
4015
4072
|
|
|
4073
|
+
get isInsideBlockQuote() {
|
|
4074
|
+
return this.nearestNodeOfType(QuoteNode)
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4016
4077
|
get isIndentedList() {
|
|
4017
4078
|
const closestListNode = this.nearestNodeOfType(ListNode);
|
|
4018
4079
|
return closestListNode && ($getListDepth(closestListNode) > 1)
|
|
@@ -4031,9 +4092,12 @@ class Selection {
|
|
|
4031
4092
|
}
|
|
4032
4093
|
|
|
4033
4094
|
get isOnPreviewableImage() {
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4095
|
+
return this.previewableImageNode != null
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
get previewableImageNode() {
|
|
4099
|
+
const firstNode = $getSelection()?.getNodes().at(0);
|
|
4100
|
+
return $isActionTextAttachmentNode(firstNode) && firstNode.isPreviewableImage ? firstNode : null
|
|
4037
4101
|
}
|
|
4038
4102
|
|
|
4039
4103
|
get isAtNodeStart() {
|
|
@@ -4437,6 +4501,9 @@ class Selection {
|
|
|
4437
4501
|
// - First item (no previous sibling): convert to a paragraph above the
|
|
4438
4502
|
// list, matching the standard "unwrap list formatting" behavior that
|
|
4439
4503
|
// users expect from pressing backspace at the start of a list item.
|
|
4504
|
+
// Inside a blockquote we instead just remove the empty item and move
|
|
4505
|
+
// the cursor into the next one — stranding a paragraph there would
|
|
4506
|
+
// leave the blank line the user is trying to close.
|
|
4440
4507
|
//
|
|
4441
4508
|
// When the empty item is the last/only one in the list, we return false
|
|
4442
4509
|
// and let Lexical's default (convert to paragraph) provide the standard
|
|
@@ -4455,19 +4522,22 @@ class Selection {
|
|
|
4455
4522
|
if (!nextSibling) return false
|
|
4456
4523
|
|
|
4457
4524
|
const previousSibling = listItem.getPreviousSibling();
|
|
4525
|
+
const listNode = $getNearestNodeOfType(listItem, ListNode);
|
|
4526
|
+
if (!listNode) return false
|
|
4527
|
+
|
|
4458
4528
|
if (previousSibling) {
|
|
4459
4529
|
previousSibling.selectEnd();
|
|
4460
4530
|
listItem.remove();
|
|
4461
|
-
|
|
4531
|
+
} else if ($isQuoteNode(listNode.getParent())) {
|
|
4532
|
+
nextSibling.selectStart();
|
|
4533
|
+
listItem.remove();
|
|
4534
|
+
} else {
|
|
4535
|
+
const paragraph = $createParagraphNode();
|
|
4536
|
+
listNode.insertBefore(paragraph);
|
|
4537
|
+
listItem.remove();
|
|
4538
|
+
paragraph.selectStart();
|
|
4462
4539
|
}
|
|
4463
4540
|
|
|
4464
|
-
const listNode = $getNearestNodeOfType(listItem, ListNode);
|
|
4465
|
-
if (!listNode) return false
|
|
4466
|
-
|
|
4467
|
-
const paragraph = $createParagraphNode();
|
|
4468
|
-
listNode.insertBefore(paragraph);
|
|
4469
|
-
listItem.remove();
|
|
4470
|
-
paragraph.selectStart();
|
|
4471
4541
|
return true
|
|
4472
4542
|
}
|
|
4473
4543
|
|
|
@@ -5008,7 +5078,10 @@ class GalleryUploader extends Uploader {
|
|
|
5008
5078
|
|
|
5009
5079
|
#findOrCreateGallery() {
|
|
5010
5080
|
if (this.selection.isOnPreviewableImage) {
|
|
5011
|
-
|
|
5081
|
+
// Resolve from the previewable image itself (the selection's first node), not from
|
|
5082
|
+
// #selectedNode (the anchor) — those differ when the selection runs from an image
|
|
5083
|
+
// into following text, and the anchor text node can't join a gallery (returns null).
|
|
5084
|
+
this.#gallery = $findOrCreateGalleryForImage(this.selection.previewableImageNode);
|
|
5012
5085
|
} else if (this.#selectionIsAfterGalleryEdge) {
|
|
5013
5086
|
this.#gallery = $findOrCreateGalleryForImage(this.selection.nodeBeforeCursor);
|
|
5014
5087
|
} else {
|
|
@@ -5340,23 +5413,13 @@ function $createActionTextAttachmentUploadNode(...args) {
|
|
|
5340
5413
|
return new ActionTextAttachmentUploadNode(...args)
|
|
5341
5414
|
}
|
|
5342
5415
|
|
|
5343
|
-
class
|
|
5344
|
-
static for(selection) {
|
|
5345
|
-
const INSERTERS = [
|
|
5346
|
-
CodeNodeInserter,
|
|
5347
|
-
ShadowRootNodeInserter,
|
|
5348
|
-
NodeSelectionNodeInserter
|
|
5349
|
-
];
|
|
5350
|
-
const Inserter = INSERTERS.find(inserter => inserter.handles(selection));
|
|
5351
|
-
return Inserter ? new Inserter(selection) : selection
|
|
5352
|
-
}
|
|
5353
|
-
|
|
5416
|
+
class BaseNodeInserter {
|
|
5354
5417
|
constructor(selection) {
|
|
5355
5418
|
this.selection = selection;
|
|
5356
5419
|
}
|
|
5357
5420
|
}
|
|
5358
5421
|
|
|
5359
|
-
class CodeNodeInserter extends
|
|
5422
|
+
class CodeNodeInserter extends BaseNodeInserter {
|
|
5360
5423
|
static handles(selection) {
|
|
5361
5424
|
return $getNearestNodeOfType(selection.anchor?.getNode(), CodeNode)
|
|
5362
5425
|
}
|
|
@@ -5371,14 +5434,40 @@ class CodeNodeInserter extends NodeInserter {
|
|
|
5371
5434
|
|
|
5372
5435
|
const caret = $getChildCaretAtIndex(codeNode, insertionIndex + 1, "previous");
|
|
5373
5436
|
|
|
5437
|
+
// Nodes that are already in the document come from the format-toggle path (existing
|
|
5438
|
+
// content converted into this code block). Brand-new nodes (dropped/pasted content)
|
|
5439
|
+
// were never attached.
|
|
5440
|
+
const existingNodes = new Set(nodes.filter(node => node.isAttached()));
|
|
5441
|
+
const trailingNodes = [];
|
|
5442
|
+
|
|
5374
5443
|
for (const node of nodes) {
|
|
5375
|
-
if (
|
|
5376
|
-
|
|
5444
|
+
if (existingNodes.has(node)) {
|
|
5445
|
+
if (!node.isAttached()) continue // already pulled in when a converted ancestor was removed
|
|
5446
|
+
} else if (!this.#canJoinCodeBlock(node)) {
|
|
5447
|
+
trailingNodes.push(node); // e.g. a dropped attachment, which a code block can't hold
|
|
5448
|
+
continue
|
|
5449
|
+
}
|
|
5377
5450
|
|
|
5451
|
+
if (caret.getNodeAtCaret() && $isElementNode(node)) { caret.insert($createLineBreakNode()); }
|
|
5378
5452
|
caret.insert(this.#convertNodeToCodeChild(node));
|
|
5379
5453
|
}
|
|
5380
5454
|
|
|
5381
|
-
|
|
5455
|
+
const lastTrailingNode = this.#insertAfterCodeBlock(codeNode, trailingNodes);
|
|
5456
|
+
const nodeToSelect = lastTrailingNode ?? caret.getNodeAtCaret();
|
|
5457
|
+
nodeToSelect?.selectEnd();
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5460
|
+
#canJoinCodeBlock(node) {
|
|
5461
|
+
return $isTextNode(node) || $isLineBreakNode(node)
|
|
5462
|
+
}
|
|
5463
|
+
|
|
5464
|
+
#insertAfterCodeBlock(codeNode, nodes) {
|
|
5465
|
+
let previousNode = codeNode;
|
|
5466
|
+
for (const node of nodes) {
|
|
5467
|
+
previousNode.insertAfter(node);
|
|
5468
|
+
previousNode = node;
|
|
5469
|
+
}
|
|
5470
|
+
return nodes.at(-1)
|
|
5382
5471
|
}
|
|
5383
5472
|
|
|
5384
5473
|
#convertNodeToCodeChild(node) {
|
|
@@ -5389,12 +5478,11 @@ class CodeNodeInserter extends NodeInserter {
|
|
|
5389
5478
|
return $createTextNode(node.getTextContent())
|
|
5390
5479
|
}
|
|
5391
5480
|
}
|
|
5392
|
-
|
|
5393
5481
|
}
|
|
5394
5482
|
|
|
5395
|
-
class ShadowRootNodeInserter extends
|
|
5483
|
+
class ShadowRootNodeInserter extends BaseNodeInserter {
|
|
5396
5484
|
static handles(selection) {
|
|
5397
|
-
return $isShadowRoot(selection?.anchor
|
|
5485
|
+
return $isShadowRoot(selection?.anchor?.getNode())
|
|
5398
5486
|
}
|
|
5399
5487
|
|
|
5400
5488
|
insertNodes(nodes) {
|
|
@@ -5406,19 +5494,173 @@ class ShadowRootNodeInserter extends NodeInserter {
|
|
|
5406
5494
|
}
|
|
5407
5495
|
}
|
|
5408
5496
|
|
|
5409
|
-
class NodeSelectionNodeInserter extends
|
|
5497
|
+
class NodeSelectionNodeInserter extends BaseNodeInserter {
|
|
5410
5498
|
static handles(selection) {
|
|
5411
5499
|
return $isNodeSelection(selection)
|
|
5412
5500
|
}
|
|
5413
5501
|
|
|
5414
5502
|
insertNodes(nodes) {
|
|
5415
|
-
const selectedNodes = this.selection.getNodes();
|
|
5416
|
-
|
|
5417
5503
|
// Overrides Lexical's default behavior of _removing_ the currently selected nodes
|
|
5418
5504
|
// https://github.com/facebook/lexical/blob/v0.38.2/packages/lexical/src/LexicalSelection.ts#L412
|
|
5419
|
-
let lastNode =
|
|
5505
|
+
let lastNode = this.selection.getNodes().at(-1);
|
|
5506
|
+
|
|
5420
5507
|
for (const node of nodes) {
|
|
5421
|
-
|
|
5508
|
+
// Inserting after a top-level node would make this one a root child. Inline nodes
|
|
5509
|
+
// can't live there (Lexical error #99), so wrap them in their required parent first.
|
|
5510
|
+
const nodeToInsert = this.#insertsIntoRoot(lastNode) ? $makeSafeForRoot(node) : node;
|
|
5511
|
+
lastNode = lastNode.insertAfter(nodeToInsert);
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5515
|
+
#insertsIntoRoot(node) {
|
|
5516
|
+
return node.is(node.getTopLevelElement())
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
|
|
5520
|
+
// A list item can only hold inline content, so a block node (such as an image
|
|
5521
|
+
// attachment) dropped into one corrupts the list: Lexical lifts it into the
|
|
5522
|
+
// wrong list item and orphans an empty bullet. Block nodes belong at the top
|
|
5523
|
+
// level instead, splitting the list around the cursor. Inline content keeps
|
|
5524
|
+
// Lexical's default behavior and stays within the list item.
|
|
5525
|
+
class ListItemNodeInserter extends BaseNodeInserter {
|
|
5526
|
+
static handles(selection) {
|
|
5527
|
+
return $isRangeSelection(selection) &&
|
|
5528
|
+
$getNearestNodeOfType(selection.anchor.getNode(), ListItemNode)
|
|
5529
|
+
}
|
|
5530
|
+
|
|
5531
|
+
insertNodes(nodes) {
|
|
5532
|
+
if (nodes.some(node => this.#isBlockDecorator(node))) {
|
|
5533
|
+
this.#insertAroundList(nodes);
|
|
5534
|
+
} else {
|
|
5535
|
+
this.selection.insertNodes(nodes);
|
|
5536
|
+
}
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
#insertAroundList(nodes) {
|
|
5540
|
+
if (!this.selection.isCollapsed()) { this.selection.removeText(); }
|
|
5541
|
+
|
|
5542
|
+
// Break out of any nesting to the outermost list. Splitting an inner list
|
|
5543
|
+
// would leave the block stranded inside an ancestor list item, which is the
|
|
5544
|
+
// very corruption we are avoiding. The block must land at the list's level.
|
|
5545
|
+
const anchorNode = this.selection.anchor.getNode();
|
|
5546
|
+
const outerList = this.#outermostList(anchorNode);
|
|
5547
|
+
const topItem = this.#topLevelItemFor(anchorNode, outerList);
|
|
5548
|
+
|
|
5549
|
+
// A blank top-level bullet is just the insertion point (e.g. the user pressed
|
|
5550
|
+
// Enter to leave the list); break out of it entirely. A bullet with content —
|
|
5551
|
+
// including one wrapping a nested list — splits so its content stays in the list.
|
|
5552
|
+
const splitAfterItem = $isBlankNode(topItem) ? topItem.getPreviousSibling() : topItem;
|
|
5553
|
+
const splitIndex = splitAfterItem ? splitAfterItem.getIndexWithinParent() + 1 : 0;
|
|
5554
|
+
const [ listBefore, listAfter ] = $splitNode(outerList, splitIndex);
|
|
5555
|
+
if ($isBlankNode(topItem)) { topItem.remove(); }
|
|
5556
|
+
|
|
5557
|
+
let anchor = listBefore ?? listAfter;
|
|
5558
|
+
for (const node of nodes) {
|
|
5559
|
+
anchor.insertAfter(node);
|
|
5560
|
+
anchor = node;
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
this.#removeEmptyList(listBefore);
|
|
5564
|
+
this.#removeEmptyList(listAfter);
|
|
5565
|
+
nodes.at(-1).selectNext();
|
|
5566
|
+
}
|
|
5567
|
+
|
|
5568
|
+
#outermostList(node) {
|
|
5569
|
+
return [ node, ...node.getParents() ].reverse().find($isListNode)
|
|
5570
|
+
}
|
|
5571
|
+
|
|
5572
|
+
#topLevelItemFor(node, outerList) {
|
|
5573
|
+
return [ node, ...node.getParents() ].find(ancestor =>
|
|
5574
|
+
$isListItemNode(ancestor) && ancestor.getParent()?.is(outerList)
|
|
5575
|
+
)
|
|
5576
|
+
}
|
|
5577
|
+
|
|
5578
|
+
#removeEmptyList(list) {
|
|
5579
|
+
if ($isListNode(list) && list.isEmpty()) list.remove();
|
|
5580
|
+
}
|
|
5581
|
+
|
|
5582
|
+
// Only block decorator nodes (image/file attachments) are intercepted. A list
|
|
5583
|
+
// item cannot hold them, so they must break out. Pasted element blocks
|
|
5584
|
+
// (paragraphs, quotes) keep Lexical's own list-escape semantics.
|
|
5585
|
+
#isBlockDecorator(node) {
|
|
5586
|
+
return $isDecoratorNode(node) && !node.isInline()
|
|
5587
|
+
}
|
|
5588
|
+
}
|
|
5589
|
+
|
|
5590
|
+
// Lexical's RangeSelection.insertNodes requires every selection point to have a block
|
|
5591
|
+
// ancestor with inline children. An element point on a container of block nodes — e.g.
|
|
5592
|
+
// a quote holding paragraphs — has none, so Lexical throws invariant #211 or #212.
|
|
5593
|
+
// Descend such points to a leaf position before inserting.
|
|
5594
|
+
class BlockContainerNodeInserter extends BaseNodeInserter {
|
|
5595
|
+
static handles(selection) {
|
|
5596
|
+
return $hasPointOnBlockContainer(selection)
|
|
5597
|
+
}
|
|
5598
|
+
|
|
5599
|
+
insertNodes(nodes) {
|
|
5600
|
+
$normalizeSelection__EXPERIMENTAL(this.selection);
|
|
5601
|
+
this.selection.insertNodes(nodes);
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
|
|
5605
|
+
const INSERTERS = [
|
|
5606
|
+
CodeNodeInserter,
|
|
5607
|
+
ShadowRootNodeInserter,
|
|
5608
|
+
NodeSelectionNodeInserter,
|
|
5609
|
+
ListItemNodeInserter,
|
|
5610
|
+
BlockContainerNodeInserter
|
|
5611
|
+
];
|
|
5612
|
+
|
|
5613
|
+
// Defined here rather than on the base class so the base can stay free of any
|
|
5614
|
+
// dependency on its subclasses (they import the base), avoiding an import cycle.
|
|
5615
|
+
BaseNodeInserter.for = (selection) => {
|
|
5616
|
+
const inserterClass = INSERTERS.find(inserter => inserter.handles(selection));
|
|
5617
|
+
return inserterClass ? new inserterClass(selection) : selection
|
|
5618
|
+
};
|
|
5619
|
+
|
|
5620
|
+
class PastedContentFormatter {
|
|
5621
|
+
constructor(doc) {
|
|
5622
|
+
this.doc = doc;
|
|
5623
|
+
}
|
|
5624
|
+
|
|
5625
|
+
format() {
|
|
5626
|
+
this.#unwrapPlaceholderAnchors();
|
|
5627
|
+
this.#stripTableCellColorStyles();
|
|
5628
|
+
this.#stripStrayListChildren();
|
|
5629
|
+
return this.doc
|
|
5630
|
+
}
|
|
5631
|
+
|
|
5632
|
+
// Anchors with non-meaningful hrefs (e.g. "#", "") appear in content copied
|
|
5633
|
+
// from rendered views where mentions and interactive elements are wrapped in
|
|
5634
|
+
// <a href="#"> tags. Unwrap them so their text content pastes as plain text
|
|
5635
|
+
// and real links are preserved.
|
|
5636
|
+
#unwrapPlaceholderAnchors() {
|
|
5637
|
+
for (const anchor of this.doc.querySelectorAll("a")) {
|
|
5638
|
+
const href = anchor.getAttribute("href") || "";
|
|
5639
|
+
if (href === "" || href === "#") {
|
|
5640
|
+
anchor.replaceWith(...anchor.childNodes);
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
|
|
5645
|
+
// Table cells copied from a page inherit the source theme's inline color
|
|
5646
|
+
// styles (e.g. dark-mode backgrounds). Strip them so pasted tables adopt
|
|
5647
|
+
// the current theme instead of carrying stale colors.
|
|
5648
|
+
#stripTableCellColorStyles() {
|
|
5649
|
+
for (const cell of this.doc.querySelectorAll("td, th")) {
|
|
5650
|
+
cell.style.removeProperty("background-color");
|
|
5651
|
+
cell.style.removeProperty("background");
|
|
5652
|
+
cell.style.removeProperty("color");
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
|
|
5656
|
+
// Only <li> is a valid child of a list; drop stray <br>/whitespace so the
|
|
5657
|
+
// import doesn't wrap them into an empty leading item.
|
|
5658
|
+
#stripStrayListChildren() {
|
|
5659
|
+
for (const list of this.doc.querySelectorAll("ol, ul")) {
|
|
5660
|
+
for (const child of Array.from(list.childNodes)) {
|
|
5661
|
+
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "LI") continue
|
|
5662
|
+
list.removeChild(child);
|
|
5663
|
+
}
|
|
5422
5664
|
}
|
|
5423
5665
|
}
|
|
5424
5666
|
}
|
|
@@ -5454,7 +5696,7 @@ class Contents {
|
|
|
5454
5696
|
|
|
5455
5697
|
insertAtCursor(...nodes) {
|
|
5456
5698
|
const selection = $getSelection() ?? $getRoot().selectEnd();
|
|
5457
|
-
const inserter =
|
|
5699
|
+
const inserter = BaseNodeInserter.for(selection);
|
|
5458
5700
|
|
|
5459
5701
|
inserter.insertNodes(nodes);
|
|
5460
5702
|
}
|
|
@@ -5468,7 +5710,7 @@ class Contents {
|
|
|
5468
5710
|
const selection = $getSelection();
|
|
5469
5711
|
if (!$isRangeSelection(selection)) return
|
|
5470
5712
|
|
|
5471
|
-
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5713
|
+
$expandSelectionToLineBreaksAndSplitAtEdges(selection, (node) => $getNearestBlockElementAncestorOrThrow(node));
|
|
5472
5714
|
$setBlocksType(selection, () => $createParagraphNode());
|
|
5473
5715
|
}
|
|
5474
5716
|
|
|
@@ -5481,13 +5723,11 @@ class Contents {
|
|
|
5481
5723
|
}
|
|
5482
5724
|
|
|
5483
5725
|
applyUnorderedListFormat() {
|
|
5484
|
-
this.#
|
|
5485
|
-
this.editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
5726
|
+
this.#applyListFormat("bullet", INSERT_UNORDERED_LIST_COMMAND);
|
|
5486
5727
|
}
|
|
5487
5728
|
|
|
5488
5729
|
applyOrderedListFormat() {
|
|
5489
|
-
this.#
|
|
5490
|
-
this.editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
5730
|
+
this.#applyListFormat("number", INSERT_ORDERED_LIST_COMMAND);
|
|
5491
5731
|
}
|
|
5492
5732
|
|
|
5493
5733
|
clearFormatting() {
|
|
@@ -5519,7 +5759,7 @@ class Contents {
|
|
|
5519
5759
|
blockElements.forEach(node => this.#unwrapCodeBlock(node));
|
|
5520
5760
|
} else {
|
|
5521
5761
|
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5522
|
-
const elements = this.#blockLevelElementsInSelection(selection);
|
|
5762
|
+
const elements = this.#outermostElements(this.#blockLevelElementsInSelection(selection));
|
|
5523
5763
|
if (elements.length === 0) return
|
|
5524
5764
|
|
|
5525
5765
|
const codeNode = $createCodeNode("plain");
|
|
@@ -5575,7 +5815,7 @@ class Contents {
|
|
|
5575
5815
|
|
|
5576
5816
|
const selection = $getSelection();
|
|
5577
5817
|
if ($isRangeSelection(selection)) {
|
|
5578
|
-
selection.insertNodes([ linkNode ]);
|
|
5818
|
+
BaseNodeInserter.for(selection).insertNodes([ linkNode ]);
|
|
5579
5819
|
linkNodeKey = linkNode.getKey();
|
|
5580
5820
|
}
|
|
5581
5821
|
});
|
|
@@ -5655,6 +5895,8 @@ class Contents {
|
|
|
5655
5895
|
}
|
|
5656
5896
|
|
|
5657
5897
|
uploadFiles(files, { selectLast } = {}) {
|
|
5898
|
+
if (!this.editorElement) return // Disposed (e.g. on turbo:before-cache); a late drop can still land here
|
|
5899
|
+
|
|
5658
5900
|
if (!this.editorElement.supportsAttachments) {
|
|
5659
5901
|
console.warn("This editor does not supports attachments (it's configured with [attachments=false])");
|
|
5660
5902
|
return
|
|
@@ -5768,8 +6010,7 @@ class Contents {
|
|
|
5768
6010
|
}
|
|
5769
6011
|
|
|
5770
6012
|
#formatPastedDOM(doc) {
|
|
5771
|
-
|
|
5772
|
-
this.#stripTableCellColorStyles(doc);
|
|
6013
|
+
new PastedContentFormatter(doc).format();
|
|
5773
6014
|
}
|
|
5774
6015
|
|
|
5775
6016
|
#dispatchPastedNodesCommand(nodes) {
|
|
@@ -5817,6 +6058,45 @@ class Contents {
|
|
|
5817
6058
|
codeNode.remove();
|
|
5818
6059
|
}
|
|
5819
6060
|
|
|
6061
|
+
#applyListFormat(listType, command) {
|
|
6062
|
+
if (this.selection.isInsideBlockQuote) {
|
|
6063
|
+
this.#insertListInsideQuote(listType);
|
|
6064
|
+
} else {
|
|
6065
|
+
this.#splitParagraphsAtLineBreaksUnlessInsideList();
|
|
6066
|
+
this.editor.dispatchCommand(command);
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
6069
|
+
|
|
6070
|
+
#insertListInsideQuote(listType) {
|
|
6071
|
+
for (const group of $consecutiveSiblingGroups(this.#quotedBlocksInSelection())) {
|
|
6072
|
+
this.#wrapBlocksInList(group, listType);
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
|
|
6076
|
+
#quotedBlocksInSelection() {
|
|
6077
|
+
const selection = $getSelection();
|
|
6078
|
+
if (!$isRangeSelection(selection)) return []
|
|
6079
|
+
|
|
6080
|
+
const blocks = this.#outermostElements(this.#blockLevelElementsInSelection(selection));
|
|
6081
|
+
return blocks.filter((block) => $isQuoteNode(block.getParent()))
|
|
6082
|
+
}
|
|
6083
|
+
|
|
6084
|
+
#wrapBlocksInList(blocks, listType) {
|
|
6085
|
+
const list = $createListNode(listType);
|
|
6086
|
+
blocks[0].insertBefore(list);
|
|
6087
|
+
|
|
6088
|
+
for (const block of blocks) {
|
|
6089
|
+
const listItem = $createListItemNode();
|
|
6090
|
+
if ($isListNode(block)) {
|
|
6091
|
+
listItem.append(...block.getChildren().flatMap((item) => item.getChildren()));
|
|
6092
|
+
} else {
|
|
6093
|
+
listItem.append(...block.getChildren());
|
|
6094
|
+
}
|
|
6095
|
+
list.append(listItem);
|
|
6096
|
+
block.remove();
|
|
6097
|
+
}
|
|
6098
|
+
}
|
|
6099
|
+
|
|
5820
6100
|
#splitParagraphsAtLineBreaksUnlessInsideList() {
|
|
5821
6101
|
if (this.selection.isInsideList) return
|
|
5822
6102
|
|
|
@@ -5845,6 +6125,18 @@ class Contents {
|
|
|
5845
6125
|
return Array.from(elements)
|
|
5846
6126
|
}
|
|
5847
6127
|
|
|
6128
|
+
// Selections spanning nested structures (a quote and its inner paragraphs,
|
|
6129
|
+
// nested list items) yield both an element and its ancestor. Converting the
|
|
6130
|
+
// ancestor detaches its whole subtree — including a node freshly inserted
|
|
6131
|
+
// inside it — which can leave the selection on removed nodes (Lexical
|
|
6132
|
+
// invariant #19). The outermost elements already cover their descendants'
|
|
6133
|
+
// text content, so keep only those.
|
|
6134
|
+
#outermostElements(elements) {
|
|
6135
|
+
return elements.filter((element) => {
|
|
6136
|
+
return elements.every((other) => other === element || !element.getParents().includes(other))
|
|
6137
|
+
})
|
|
6138
|
+
}
|
|
6139
|
+
|
|
5848
6140
|
#insertUploadNodes(nodes) {
|
|
5849
6141
|
if (nodes.every($isActionTextAttachmentNode)) {
|
|
5850
6142
|
const uploader = Uploader.for(this.editorElement, []);
|
|
@@ -5885,30 +6177,6 @@ class Contents {
|
|
|
5885
6177
|
node.remove();
|
|
5886
6178
|
}
|
|
5887
6179
|
|
|
5888
|
-
// Anchors with non-meaningful hrefs (e.g. "#", "") appear in content copied
|
|
5889
|
-
// from rendered views where mentions and interactive elements are wrapped in
|
|
5890
|
-
// <a href="#"> tags. Unwrap them so their text content pastes as plain text
|
|
5891
|
-
// and real links are preserved.
|
|
5892
|
-
#unwrapPlaceholderAnchors(doc) {
|
|
5893
|
-
for (const anchor of doc.querySelectorAll("a")) {
|
|
5894
|
-
const href = anchor.getAttribute("href") || "";
|
|
5895
|
-
if (href === "" || href === "#") {
|
|
5896
|
-
anchor.replaceWith(...anchor.childNodes);
|
|
5897
|
-
}
|
|
5898
|
-
}
|
|
5899
|
-
}
|
|
5900
|
-
|
|
5901
|
-
// Table cells copied from a page inherit the source theme's inline color
|
|
5902
|
-
// styles (e.g. dark-mode backgrounds). Strip them so pasted tables adopt
|
|
5903
|
-
// the current theme instead of carrying stale colors.
|
|
5904
|
-
#stripTableCellColorStyles(doc) {
|
|
5905
|
-
for (const cell of doc.querySelectorAll("td, th")) {
|
|
5906
|
-
cell.style.removeProperty("background-color");
|
|
5907
|
-
cell.style.removeProperty("background");
|
|
5908
|
-
cell.style.removeProperty("color");
|
|
5909
|
-
}
|
|
5910
|
-
}
|
|
5911
|
-
|
|
5912
6180
|
#getTextAnchorData() {
|
|
5913
6181
|
const selection = $getSelection();
|
|
5914
6182
|
if (!selection || !selection.isCollapsed()) return { anchorNode: null, offset: 0 }
|
|
@@ -6154,7 +6422,7 @@ class Clipboard {
|
|
|
6154
6422
|
}
|
|
6155
6423
|
|
|
6156
6424
|
const linkNode = $createLinkNode(url).append($createTextNode(url));
|
|
6157
|
-
selection.insertNodes([ linkNode ]);
|
|
6425
|
+
BaseNodeInserter.for(selection).insertNodes([ linkNode ]);
|
|
6158
6426
|
|
|
6159
6427
|
$onUpdate(() => this.#dispatchLinkInsertEvent(linkNode.getKey(), { url }));
|
|
6160
6428
|
}
|
|
@@ -7372,12 +7640,23 @@ class EarlyEscapeListItemNode extends ListItemNode {
|
|
|
7372
7640
|
return super.insertNewAfter(selection, restoreSelection)
|
|
7373
7641
|
}
|
|
7374
7642
|
|
|
7643
|
+
get #isInBlockquote() {
|
|
7644
|
+
return Boolean($getNearestNodeOfType(this, QuoteNode))
|
|
7645
|
+
}
|
|
7646
|
+
|
|
7375
7647
|
#shouldEscape(selection) {
|
|
7376
|
-
if (
|
|
7377
|
-
|
|
7648
|
+
if (this.#isInPasteOperation() || !this.#isInBlockquote) {
|
|
7649
|
+
return false
|
|
7650
|
+
} else if ($isBlankNode(this)) {
|
|
7651
|
+
return true
|
|
7652
|
+
} else {
|
|
7653
|
+
const paragraph = $getNearestNodeOfType(selection.anchor.getNode(), ParagraphNode);
|
|
7654
|
+
return paragraph && $isBlankNode(paragraph) && $isListItemNode(paragraph.getParent())
|
|
7655
|
+
}
|
|
7656
|
+
}
|
|
7378
7657
|
|
|
7379
|
-
|
|
7380
|
-
return
|
|
7658
|
+
#isInPasteOperation() {
|
|
7659
|
+
return $hasUpdateTag(PASTE_TAG)
|
|
7381
7660
|
}
|
|
7382
7661
|
|
|
7383
7662
|
#escapeFromList() {
|
|
@@ -7425,7 +7704,7 @@ class FormatEscapeExtension extends LexxyExtension {
|
|
|
7425
7704
|
}
|
|
7426
7705
|
|
|
7427
7706
|
get allowedElements() {
|
|
7428
|
-
return [ { tag: "li", attributes: [ "value" ] } ]
|
|
7707
|
+
return [ { tag: "ol", attributes: [ "start" ] }, { tag: "li", attributes: [ "value" ] } ]
|
|
7429
7708
|
}
|
|
7430
7709
|
|
|
7431
7710
|
get lexicalExtension() {
|
|
@@ -7609,7 +7888,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7609
7888
|
static debug = false
|
|
7610
7889
|
static commands = [ "bold", "italic", "strikethrough" ]
|
|
7611
7890
|
|
|
7612
|
-
static observedAttributes = [ "connected", "required" ]
|
|
7891
|
+
static observedAttributes = [ "autocapitalize", "connected", "required" ]
|
|
7613
7892
|
|
|
7614
7893
|
#initialValue = ""
|
|
7615
7894
|
#previousInternalFormValue = null
|
|
@@ -7676,8 +7955,15 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7676
7955
|
}
|
|
7677
7956
|
|
|
7678
7957
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
7679
|
-
if (name === "
|
|
7680
|
-
|
|
7958
|
+
if (typeof this[`${name}ChangedCallback`] === "function") {
|
|
7959
|
+
this[`${name}ChangedCallback`](oldValue, newValue);
|
|
7960
|
+
}
|
|
7961
|
+
}
|
|
7962
|
+
|
|
7963
|
+
autocapitalizeChangedCallback() {
|
|
7964
|
+
if (this.editorContentElement) {
|
|
7965
|
+
this.#transferAttributeToContentEditable(this.editorContentElement, "autocapitalize");
|
|
7966
|
+
}
|
|
7681
7967
|
}
|
|
7682
7968
|
|
|
7683
7969
|
connectedChangedCallback(oldValue, newValue) {
|
|
@@ -8009,25 +8295,33 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8009
8295
|
|
|
8010
8296
|
#createEditorContentElement() {
|
|
8011
8297
|
const editorContentElement = createElement("div", {
|
|
8298
|
+
id: `${this.id}-content`,
|
|
8012
8299
|
classList: "lexxy-editor__content",
|
|
8013
8300
|
contenteditable: true,
|
|
8014
|
-
autocapitalize: "none",
|
|
8015
8301
|
role: "textbox",
|
|
8016
8302
|
"aria-multiline": true,
|
|
8017
8303
|
"aria-label": this.#labelText,
|
|
8018
8304
|
placeholder: this.getAttribute("placeholder")
|
|
8019
8305
|
});
|
|
8020
|
-
|
|
8306
|
+
|
|
8021
8307
|
this.#ariaAttributes.forEach(attribute => editorContentElement.setAttribute(attribute.name, attribute.value));
|
|
8022
8308
|
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8309
|
+
this.#transferAttributeToContentEditable(editorContentElement, "autocapitalize");
|
|
8310
|
+
this.#transferAttributeToContentEditable(editorContentElement, "tabindex", { defaultValue: 0, removeSource: true });
|
|
8311
|
+
|
|
8312
|
+
return editorContentElement
|
|
8313
|
+
}
|
|
8314
|
+
|
|
8315
|
+
#transferAttributeToContentEditable(element, qualifiedName, { defaultValue = null, removeSource = false } = {}) {
|
|
8316
|
+
if (this.hasAttribute(qualifiedName)) {
|
|
8317
|
+
element.setAttribute(qualifiedName, this.getAttribute(qualifiedName));
|
|
8318
|
+
} else if (defaultValue !== null) {
|
|
8319
|
+
element.setAttribute(qualifiedName, defaultValue);
|
|
8026
8320
|
} else {
|
|
8027
|
-
|
|
8321
|
+
element.removeAttribute(qualifiedName);
|
|
8028
8322
|
}
|
|
8029
8323
|
|
|
8030
|
-
|
|
8324
|
+
if (removeSource) this.removeAttribute(qualifiedName);
|
|
8031
8325
|
}
|
|
8032
8326
|
|
|
8033
8327
|
get #labelText() {
|
|
@@ -8644,6 +8938,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8644
8938
|
this.source = this.#createSource();
|
|
8645
8939
|
|
|
8646
8940
|
this.#addTriggerListener();
|
|
8941
|
+
this.#removePopoverBeforeTurboCaches();
|
|
8647
8942
|
this.toggleAttribute("connected", true);
|
|
8648
8943
|
}
|
|
8649
8944
|
|
|
@@ -8651,7 +8946,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8651
8946
|
this.#popoverListeners.dispose();
|
|
8652
8947
|
this.#globalListeners.dispose();
|
|
8653
8948
|
this.source = null;
|
|
8654
|
-
this
|
|
8949
|
+
this.#removePopover();
|
|
8655
8950
|
}
|
|
8656
8951
|
|
|
8657
8952
|
|
|
@@ -8947,6 +9242,21 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8947
9242
|
this.#addTriggerListener();
|
|
8948
9243
|
}
|
|
8949
9244
|
|
|
9245
|
+
// The popover is appended to the <lexxy-editor> subtree, so Turbo serializes it
|
|
9246
|
+
// into the page cache. Removing it before caching prevents an orphaned, unmanaged
|
|
9247
|
+
// popover from being restored on history back/forward.
|
|
9248
|
+
#removePopoverBeforeTurboCaches() {
|
|
9249
|
+
this.#globalListeners.track(
|
|
9250
|
+
registerEventListener(document, "turbo:before-cache", () => this.#removePopover())
|
|
9251
|
+
);
|
|
9252
|
+
}
|
|
9253
|
+
|
|
9254
|
+
#removePopover() {
|
|
9255
|
+
this.#popoverListeners.dispose();
|
|
9256
|
+
this.popoverElement?.remove();
|
|
9257
|
+
this.popoverElement = null;
|
|
9258
|
+
}
|
|
9259
|
+
|
|
8950
9260
|
#filterOptions = async () => {
|
|
8951
9261
|
if (this.initialPrompt) {
|
|
8952
9262
|
this.initialPrompt = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@37signals/lexxy",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.19-alpha.2",
|
|
4
4
|
"description": "Lexxy - A modern rich text editor for Rails.",
|
|
5
5
|
"module": "dist/lexxy.esm.js",
|
|
6
6
|
"type": "module",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
29
29
|
"@rollup/plugin-terser": "^0.4.4",
|
|
30
30
|
"eslint": "^10.3.0",
|
|
31
|
-
"globals": "^17.6.0",
|
|
32
31
|
"eslint-plugin-compat": "^7.0.2",
|
|
32
|
+
"globals": "^17.6.0",
|
|
33
33
|
"jsdom": "^27.3.0",
|
|
34
34
|
"rollup": "^4.44.1",
|
|
35
35
|
"rollup-plugin-copy": "^3.5.0",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"test:browser:headed": "npx playwright test --config test/browser/playwright.config.js --headed",
|
|
53
53
|
"test:browser:debug": "npx playwright test --config test/browser/playwright.config.js --debug",
|
|
54
54
|
"prerelease": "yarn build:npm",
|
|
55
|
-
"release": "yarn build:npm && yarn publish"
|
|
55
|
+
"release": "yarn build:npm && yarn publish",
|
|
56
|
+
"release:alpha": "yarn build:npm && yarn publish --tag alpha"
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
58
59
|
"@lexical/clipboard": "^0.44.0",
|