lexxy 0.9.19.alpha.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6b15ddc26ce7b82fd871ed3f5dfa96598b84278da3af81c7c8e7ed1c7d8135b
4
- data.tar.gz: eedb6f7b9cca87c16c1737c6db392b2fb8a38f560ca20f3c615dbd865afafbfc
3
+ metadata.gz: 3f4e64b061d290f12430a666006c5845f242f1776474b8b8691c53e83a7a5674
4
+ data.tar.gz: 8f539bac381f8273f97d18c9d40093c917272d89e74a36ef3d8dee9888ab149d
5
5
  SHA512:
6
- metadata.gz: 6945b89f20390bc1f407d95de1f365ef97508b8e7c8ac4c8960d48df6a8d53e39cfcc39795988e8e4e77b37d2d4e60819ffc0faf0a8f1035b3acedaede71d978
7
- data.tar.gz: 791d8ac7ffebbe6987fc70571bb04450e17e93571f89bd10abdcbf7586b0b3deed48208a0e035b66eceeb5cb42db373399894b2aead182c19139ab3cc0f11d5b
6
+ metadata.gz: 2245d2ea20bae119bda7464502ef6c83390f08425ea998e9e2e4a84b4ac20d389fc967ae4d4aab1d821d462050c3358cf617d3170cf11a4fcad526d368d59453
7
+ data.tar.gz: db3cef1b72fcc42d00f66160292ce2f13b2c1b82a57bbe44fa53a7100a31e65143a52c68674ada67d20901be052c26a037516b5a13b72994673e1391d8c24883
@@ -7810,8 +7810,12 @@ function $isSafeForRoot(node) {
7810
7810
  function $makeSafeForRoot(node) {
7811
7811
  if ($isSafeForRoot(node)) {
7812
7812
  return node
7813
- } else {
7813
+ } else if (node.getParent()) {
7814
7814
  return Tt$4(node, () => node.createParentElementNode())
7815
+ } else {
7816
+ // Detached nodes (e.g. clipboard nodes being inserted) can't be `replace`d in place,
7817
+ // so append them into a fresh required parent instead.
7818
+ return node.createParentElementNode().append(node)
7815
7819
  }
7816
7820
  }
7817
7821
 
@@ -7999,7 +8003,7 @@ function $splitSelectedParagraphsAtInnerLineBreaks(selection) {
7999
8003
  }
8000
8004
  }
8001
8005
 
8002
- function $expandSelectionToLineBreaksAndSplitAtEdges(selection) {
8006
+ function $expandSelectionToLineBreaksAndSplitAtEdges(selection, fallbackAncestor = (node) => node.getTopLevelElement()) {
8003
8007
  j$6(selection);
8004
8008
 
8005
8009
  const focusCaret = ql(selection.focus, "next");
@@ -8019,8 +8023,8 @@ function $expandSelectionToLineBreaksAndSplitAtEdges(selection) {
8019
8023
  const focusOuter = focusBrCaret && $splitAroundLineBreak(focusBrCaret);
8020
8024
  const anchorOuter = anchorBrCaret && $splitAroundLineBreak(anchorBrCaret);
8021
8025
 
8022
- const innerStart = anchorOuter?.getNextSibling() ?? selection.anchor.getNode().getTopLevelElement();
8023
- const innerEnd = focusOuter?.getPreviousSibling() ?? selection.focus.getNode().getTopLevelElement();
8026
+ const innerStart = anchorOuter?.getNextSibling() ?? fallbackAncestor(selection.anchor.getNode());
8027
+ const innerEnd = focusOuter?.getPreviousSibling() ?? fallbackAncestor(selection.focus.getNode());
8024
8028
  if (!innerStart || !innerEnd) return
8025
8029
 
8026
8030
  Gl(Wl(
@@ -8126,6 +8130,49 @@ function $splitAroundLineBreak(lineBreakCaret) {
8126
8130
  return outer
8127
8131
  }
8128
8132
 
8133
+ // Lexical's RangeSelection.insertNodes/insertLineBreak require every selection point to have a
8134
+ // block ancestor with inline children. An element point on a container of block nodes — e.g. a
8135
+ // quote holding paragraphs — has none, so Lexical throws invariant #211 or #212. This detects
8136
+ // such a point so callers can descend it to a leaf before inserting.
8137
+ function $isPointOnBlockContainer(point) {
8138
+ if (point.type !== "element") return false
8139
+
8140
+ const firstChild = point.getNode().getFirstChild();
8141
+ return (Wi(firstChild) || ji(firstChild)) && !firstChild.isInline()
8142
+ }
8143
+
8144
+ function $hasPointOnBlockContainer(selection) {
8145
+ return Fr(selection) &&
8146
+ [ selection.anchor, selection.focus ].some($isPointOnBlockContainer)
8147
+ }
8148
+
8149
+ // Descend any block-container element point in the selection to a leaf position, so a subsequent
8150
+ // Lexical insert (insertNodes, insertLineBreak, INSERT_PARAGRAPH) doesn't throw invariant #211/#212.
8151
+ function $normalizeBlockContainerSelection(selection = Qr()) {
8152
+ if (!$hasPointOnBlockContainer(selection)) return false
8153
+
8154
+ St$3(selection);
8155
+ return true
8156
+ }
8157
+
8158
+ function $consecutiveSiblingGroups(blocks) {
8159
+ const ordered = [ ...blocks ].sort((a, b) => a.getIndexWithinParent() - b.getIndexWithinParent());
8160
+ const groups = [];
8161
+
8162
+ for (const block of ordered) {
8163
+ const lastGroup = groups.at(-1);
8164
+ const previous = lastGroup?.at(-1);
8165
+
8166
+ if (previous && previous.getParent().is(block.getParent()) && previous.getNextSibling()?.is(block)) {
8167
+ lastGroup.push(block);
8168
+ } else {
8169
+ groups.push([ block ]);
8170
+ }
8171
+ }
8172
+
8173
+ return groups
8174
+ }
8175
+
8129
8176
  // Payload: Record<nodeKey, { patch?, replace? }>
8130
8177
  // - patch: plain object, shallow-merged into the existing node's properties
8131
8178
  // - replace: a LexicalNode instance that replaces the node
@@ -10067,6 +10114,16 @@ class CommandDispatcher {
10067
10114
  #registerKeyboardCommands() {
10068
10115
  this.#registerCommandHandler(ke$2, oo, this.#handleArrowRightKey.bind(this));
10069
10116
  this.#registerCommandHandler(Fe$2, oo, this.#handleTabKey.bind(this));
10117
+
10118
+ // Run before Lexical's built-in insert handlers to descend an element point on a
10119
+ // block container to a leaf, avoiding error #211 on Enter / Shift+Enter in a quote.
10120
+ this.#registerCommandHandler(de$4, so, this.#normalizeBlockContainerSelection.bind(this));
10121
+ this.#registerCommandHandler(he$3, so, this.#normalizeBlockContainerSelection.bind(this));
10122
+ }
10123
+
10124
+ #normalizeBlockContainerSelection() {
10125
+ $normalizeBlockContainerSelection();
10126
+ return false
10070
10127
  }
10071
10128
 
10072
10129
  #handleArrowRightKey(event) {
@@ -10339,6 +10396,10 @@ class Selection {
10339
10396
  return this.nearestNodeOfType(ge$2)
10340
10397
  }
10341
10398
 
10399
+ get isInsideBlockQuote() {
10400
+ return this.nearestNodeOfType(At$1)
10401
+ }
10402
+
10342
10403
  get isIndentedList() {
10343
10404
  const closestListNode = this.nearestNodeOfType(me$2);
10344
10405
  return closestListNode && (G$7(closestListNode) > 1)
@@ -10766,6 +10827,9 @@ class Selection {
10766
10827
  // - First item (no previous sibling): convert to a paragraph above the
10767
10828
  // list, matching the standard "unwrap list formatting" behavior that
10768
10829
  // users expect from pressing backspace at the start of a list item.
10830
+ // Inside a blockquote we instead just remove the empty item and move
10831
+ // the cursor into the next one — stranding a paragraph there would
10832
+ // leave the blank line the user is trying to close.
10769
10833
  //
10770
10834
  // When the empty item is the last/only one in the list, we return false
10771
10835
  // and let Lexical's default (convert to paragraph) provide the standard
@@ -10784,19 +10848,22 @@ class Selection {
10784
10848
  if (!nextSibling) return false
10785
10849
 
10786
10850
  const previousSibling = listItem.getPreviousSibling();
10851
+ const listNode = At$2(listItem, me$2);
10852
+ if (!listNode) return false
10853
+
10787
10854
  if (previousSibling) {
10788
10855
  previousSibling.selectEnd();
10789
10856
  listItem.remove();
10790
- return true
10857
+ } else if (Mt$2(listNode.getParent())) {
10858
+ nextSibling.selectStart();
10859
+ listItem.remove();
10860
+ } else {
10861
+ const paragraph = eo();
10862
+ listNode.insertBefore(paragraph);
10863
+ listItem.remove();
10864
+ paragraph.selectStart();
10791
10865
  }
10792
10866
 
10793
- const listNode = At$2(listItem, me$2);
10794
- if (!listNode) return false
10795
-
10796
- const paragraph = eo();
10797
- listNode.insertBefore(paragraph);
10798
- listItem.remove();
10799
- paragraph.selectStart();
10800
10867
  return true
10801
10868
  }
10802
10869
 
@@ -11672,24 +11739,13 @@ function $createActionTextAttachmentUploadNode(...args) {
11672
11739
  return new ActionTextAttachmentUploadNode(...args)
11673
11740
  }
11674
11741
 
11675
- class NodeInserter {
11676
- static for(selection) {
11677
- const INSERTERS = [
11678
- CodeNodeInserter,
11679
- ShadowRootNodeInserter,
11680
- NodeSelectionNodeInserter,
11681
- BlockContainerNodeInserter
11682
- ];
11683
- const Inserter = INSERTERS.find(inserter => inserter.handles(selection));
11684
- return Inserter ? new Inserter(selection) : selection
11685
- }
11686
-
11742
+ class BaseNodeInserter {
11687
11743
  constructor(selection) {
11688
11744
  this.selection = selection;
11689
11745
  }
11690
11746
  }
11691
11747
 
11692
- class CodeNodeInserter extends NodeInserter {
11748
+ class CodeNodeInserter extends BaseNodeInserter {
11693
11749
  static handles(selection) {
11694
11750
  return At$2(selection.anchor?.getNode(), z$1)
11695
11751
  }
@@ -11748,10 +11804,9 @@ class CodeNodeInserter extends NodeInserter {
11748
11804
  return kr(node.getTextContent())
11749
11805
  }
11750
11806
  }
11751
-
11752
11807
  }
11753
11808
 
11754
- class ShadowRootNodeInserter extends NodeInserter {
11809
+ class ShadowRootNodeInserter extends BaseNodeInserter {
11755
11810
  static handles(selection) {
11756
11811
  return $isShadowRoot(selection?.anchor?.getNode())
11757
11812
  }
@@ -11765,31 +11820,106 @@ class ShadowRootNodeInserter extends NodeInserter {
11765
11820
  }
11766
11821
  }
11767
11822
 
11768
- class NodeSelectionNodeInserter extends NodeInserter {
11823
+ class NodeSelectionNodeInserter extends BaseNodeInserter {
11769
11824
  static handles(selection) {
11770
11825
  return Ir(selection)
11771
11826
  }
11772
11827
 
11773
11828
  insertNodes(nodes) {
11774
- const selectedNodes = this.selection.getNodes();
11775
-
11776
11829
  // Overrides Lexical's default behavior of _removing_ the currently selected nodes
11777
11830
  // https://github.com/facebook/lexical/blob/v0.38.2/packages/lexical/src/LexicalSelection.ts#L412
11778
- let lastNode = selectedNodes.at(-1);
11831
+ let lastNode = this.selection.getNodes().at(-1);
11832
+
11779
11833
  for (const node of nodes) {
11780
- lastNode = lastNode.insertAfter(node);
11834
+ // Inserting after a top-level node would make this one a root child. Inline nodes
11835
+ // can't live there (Lexical error #99), so wrap them in their required parent first.
11836
+ const nodeToInsert = this.#insertsIntoRoot(lastNode) ? $makeSafeForRoot(node) : node;
11837
+ lastNode = lastNode.insertAfter(nodeToInsert);
11781
11838
  }
11782
11839
  }
11840
+
11841
+ #insertsIntoRoot(node) {
11842
+ return node.is(node.getTopLevelElement())
11843
+ }
11844
+ }
11845
+
11846
+ // A list item can only hold inline content, so a block node (such as an image
11847
+ // attachment) dropped into one corrupts the list: Lexical lifts it into the
11848
+ // wrong list item and orphans an empty bullet. Block nodes belong at the top
11849
+ // level instead, splitting the list around the cursor. Inline content keeps
11850
+ // Lexical's default behavior and stays within the list item.
11851
+ class ListItemNodeInserter extends BaseNodeInserter {
11852
+ static handles(selection) {
11853
+ return Fr(selection) &&
11854
+ At$2(selection.anchor.getNode(), ge$2)
11855
+ }
11856
+
11857
+ insertNodes(nodes) {
11858
+ if (nodes.some(node => this.#isBlockDecorator(node))) {
11859
+ this.#insertAroundList(nodes);
11860
+ } else {
11861
+ this.selection.insertNodes(nodes);
11862
+ }
11863
+ }
11864
+
11865
+ #insertAroundList(nodes) {
11866
+ if (!this.selection.isCollapsed()) { this.selection.removeText(); }
11867
+
11868
+ // Break out of any nesting to the outermost list. Splitting an inner list
11869
+ // would leave the block stranded inside an ancestor list item, which is the
11870
+ // very corruption we are avoiding. The block must land at the list's level.
11871
+ const anchorNode = this.selection.anchor.getNode();
11872
+ const outerList = this.#outermostList(anchorNode);
11873
+ const topItem = this.#topLevelItemFor(anchorNode, outerList);
11874
+
11875
+ // A blank top-level bullet is just the insertion point (e.g. the user pressed
11876
+ // Enter to leave the list); break out of it entirely. A bullet with content —
11877
+ // including one wrapping a nested list — splits so its content stays in the list.
11878
+ const splitAfterItem = $isBlankNode(topItem) ? topItem.getPreviousSibling() : topItem;
11879
+ const splitIndex = splitAfterItem ? splitAfterItem.getIndexWithinParent() + 1 : 0;
11880
+ const [ listBefore, listAfter ] = Us(outerList, splitIndex);
11881
+ if ($isBlankNode(topItem)) { topItem.remove(); }
11882
+
11883
+ let anchor = listBefore ?? listAfter;
11884
+ for (const node of nodes) {
11885
+ anchor.insertAfter(node);
11886
+ anchor = node;
11887
+ }
11888
+
11889
+ this.#removeEmptyList(listBefore);
11890
+ this.#removeEmptyList(listAfter);
11891
+ nodes.at(-1).selectNext();
11892
+ }
11893
+
11894
+ #outermostList(node) {
11895
+ return [ node, ...node.getParents() ].reverse().find(Se$1)
11896
+ }
11897
+
11898
+ #topLevelItemFor(node, outerList) {
11899
+ return [ node, ...node.getParents() ].find(ancestor =>
11900
+ pe$2(ancestor) && ancestor.getParent()?.is(outerList)
11901
+ )
11902
+ }
11903
+
11904
+ #removeEmptyList(list) {
11905
+ if (Se$1(list) && list.isEmpty()) list.remove();
11906
+ }
11907
+
11908
+ // Only block decorator nodes (image/file attachments) are intercepted. A list
11909
+ // item cannot hold them, so they must break out. Pasted element blocks
11910
+ // (paragraphs, quotes) keep Lexical's own list-escape semantics.
11911
+ #isBlockDecorator(node) {
11912
+ return ji(node) && !node.isInline()
11913
+ }
11783
11914
  }
11784
11915
 
11785
11916
  // Lexical's RangeSelection.insertNodes requires every selection point to have a block
11786
11917
  // ancestor with inline children. An element point on a container of block nodes — e.g.
11787
11918
  // a quote holding paragraphs — has none, so Lexical throws invariant #211 or #212.
11788
11919
  // Descend such points to a leaf position before inserting.
11789
- class BlockContainerNodeInserter extends NodeInserter {
11920
+ class BlockContainerNodeInserter extends BaseNodeInserter {
11790
11921
  static handles(selection) {
11791
- return Fr(selection) &&
11792
- [ selection.anchor, selection.focus ].some($isPointOnBlockContainer)
11922
+ return $hasPointOnBlockContainer(selection)
11793
11923
  }
11794
11924
 
11795
11925
  insertNodes(nodes) {
@@ -11798,12 +11928,66 @@ class BlockContainerNodeInserter extends NodeInserter {
11798
11928
  }
11799
11929
  }
11800
11930
 
11801
- function $isPointOnBlockContainer(point) {
11802
- if (point.type === "element") {
11803
- const firstChild = point.getNode().getFirstChild();
11804
- return (Wi(firstChild) || ji(firstChild)) && !firstChild.isInline()
11805
- } else {
11806
- return false
11931
+ const INSERTERS = [
11932
+ CodeNodeInserter,
11933
+ ShadowRootNodeInserter,
11934
+ NodeSelectionNodeInserter,
11935
+ ListItemNodeInserter,
11936
+ BlockContainerNodeInserter
11937
+ ];
11938
+
11939
+ // Defined here rather than on the base class so the base can stay free of any
11940
+ // dependency on its subclasses (they import the base), avoiding an import cycle.
11941
+ BaseNodeInserter.for = (selection) => {
11942
+ const inserterClass = INSERTERS.find(inserter => inserter.handles(selection));
11943
+ return inserterClass ? new inserterClass(selection) : selection
11944
+ };
11945
+
11946
+ class PastedContentFormatter {
11947
+ constructor(doc) {
11948
+ this.doc = doc;
11949
+ }
11950
+
11951
+ format() {
11952
+ this.#unwrapPlaceholderAnchors();
11953
+ this.#stripTableCellColorStyles();
11954
+ this.#stripStrayListChildren();
11955
+ return this.doc
11956
+ }
11957
+
11958
+ // Anchors with non-meaningful hrefs (e.g. "#", "") appear in content copied
11959
+ // from rendered views where mentions and interactive elements are wrapped in
11960
+ // <a href="#"> tags. Unwrap them so their text content pastes as plain text
11961
+ // and real links are preserved.
11962
+ #unwrapPlaceholderAnchors() {
11963
+ for (const anchor of this.doc.querySelectorAll("a")) {
11964
+ const href = anchor.getAttribute("href") || "";
11965
+ if (href === "" || href === "#") {
11966
+ anchor.replaceWith(...anchor.childNodes);
11967
+ }
11968
+ }
11969
+ }
11970
+
11971
+ // Table cells copied from a page inherit the source theme's inline color
11972
+ // styles (e.g. dark-mode backgrounds). Strip them so pasted tables adopt
11973
+ // the current theme instead of carrying stale colors.
11974
+ #stripTableCellColorStyles() {
11975
+ for (const cell of this.doc.querySelectorAll("td, th")) {
11976
+ cell.style.removeProperty("background-color");
11977
+ cell.style.removeProperty("background");
11978
+ cell.style.removeProperty("color");
11979
+ }
11980
+ }
11981
+
11982
+ // Only <li> is a valid child of a list; drop stray <br>/whitespace so the
11983
+ // import doesn't wrap them into an empty leading item.
11984
+ #stripStrayListChildren() {
11985
+ for (const list of this.doc.querySelectorAll("ol, ul")) {
11986
+ for (const child of Array.from(list.childNodes)) {
11987
+ if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "LI") continue
11988
+ list.removeChild(child);
11989
+ }
11990
+ }
11807
11991
  }
11808
11992
  }
11809
11993
 
@@ -11838,7 +12022,7 @@ class Contents {
11838
12022
 
11839
12023
  insertAtCursor(...nodes) {
11840
12024
  const selection = Qr() ?? Zo().selectEnd();
11841
- const inserter = NodeInserter.for(selection);
12025
+ const inserter = BaseNodeInserter.for(selection);
11842
12026
 
11843
12027
  inserter.insertNodes(nodes);
11844
12028
  }
@@ -11852,7 +12036,7 @@ class Contents {
11852
12036
  const selection = Qr();
11853
12037
  if (!Fr(selection)) return
11854
12038
 
11855
- $expandSelectionToLineBreaksAndSplitAtEdges(selection);
12039
+ $expandSelectionToLineBreaksAndSplitAtEdges(selection, (node) => Nt$2(node));
11856
12040
  H$6(selection, () => eo());
11857
12041
  }
11858
12042
 
@@ -11865,13 +12049,11 @@ class Contents {
11865
12049
  }
11866
12050
 
11867
12051
  applyUnorderedListFormat() {
11868
- this.#splitParagraphsAtLineBreaksUnlessInsideList();
11869
- this.editor.dispatchCommand(Ee$2, undefined);
12052
+ this.#applyListFormat("bullet", Ee$2);
11870
12053
  }
11871
12054
 
11872
12055
  applyOrderedListFormat() {
11873
- this.#splitParagraphsAtLineBreaksUnlessInsideList();
11874
- this.editor.dispatchCommand(Oe$1, undefined);
12056
+ this.#applyListFormat("number", Oe$1);
11875
12057
  }
11876
12058
 
11877
12059
  clearFormatting() {
@@ -11959,7 +12141,7 @@ class Contents {
11959
12141
 
11960
12142
  const selection = Qr();
11961
12143
  if (Fr(selection)) {
11962
- selection.insertNodes([ linkNode ]);
12144
+ BaseNodeInserter.for(selection).insertNodes([ linkNode ]);
11963
12145
  linkNodeKey = linkNode.getKey();
11964
12146
  }
11965
12147
  });
@@ -12154,8 +12336,7 @@ class Contents {
12154
12336
  }
12155
12337
 
12156
12338
  #formatPastedDOM(doc) {
12157
- this.#unwrapPlaceholderAnchors(doc);
12158
- this.#stripTableCellColorStyles(doc);
12339
+ new PastedContentFormatter(doc).format();
12159
12340
  }
12160
12341
 
12161
12342
  #dispatchPastedNodesCommand(nodes) {
@@ -12203,6 +12384,45 @@ class Contents {
12203
12384
  codeNode.remove();
12204
12385
  }
12205
12386
 
12387
+ #applyListFormat(listType, command) {
12388
+ if (this.selection.isInsideBlockQuote) {
12389
+ this.#insertListInsideQuote(listType);
12390
+ } else {
12391
+ this.#splitParagraphsAtLineBreaksUnlessInsideList();
12392
+ this.editor.dispatchCommand(command);
12393
+ }
12394
+ }
12395
+
12396
+ #insertListInsideQuote(listType) {
12397
+ for (const group of $consecutiveSiblingGroups(this.#quotedBlocksInSelection())) {
12398
+ this.#wrapBlocksInList(group, listType);
12399
+ }
12400
+ }
12401
+
12402
+ #quotedBlocksInSelection() {
12403
+ const selection = Qr();
12404
+ if (!Fr(selection)) return []
12405
+
12406
+ const blocks = this.#outermostElements(this.#blockLevelElementsInSelection(selection));
12407
+ return blocks.filter((block) => Mt$2(block.getParent()))
12408
+ }
12409
+
12410
+ #wrapBlocksInList(blocks, listType) {
12411
+ const list = Te$1(listType);
12412
+ blocks[0].insertBefore(list);
12413
+
12414
+ for (const block of blocks) {
12415
+ const listItem = fe$2();
12416
+ if (Se$1(block)) {
12417
+ listItem.append(...block.getChildren().flatMap((item) => item.getChildren()));
12418
+ } else {
12419
+ listItem.append(...block.getChildren());
12420
+ }
12421
+ list.append(listItem);
12422
+ block.remove();
12423
+ }
12424
+ }
12425
+
12206
12426
  #splitParagraphsAtLineBreaksUnlessInsideList() {
12207
12427
  if (this.selection.isInsideList) return
12208
12428
 
@@ -12283,30 +12503,6 @@ class Contents {
12283
12503
  node.remove();
12284
12504
  }
12285
12505
 
12286
- // Anchors with non-meaningful hrefs (e.g. "#", "") appear in content copied
12287
- // from rendered views where mentions and interactive elements are wrapped in
12288
- // <a href="#"> tags. Unwrap them so their text content pastes as plain text
12289
- // and real links are preserved.
12290
- #unwrapPlaceholderAnchors(doc) {
12291
- for (const anchor of doc.querySelectorAll("a")) {
12292
- const href = anchor.getAttribute("href") || "";
12293
- if (href === "" || href === "#") {
12294
- anchor.replaceWith(...anchor.childNodes);
12295
- }
12296
- }
12297
- }
12298
-
12299
- // Table cells copied from a page inherit the source theme's inline color
12300
- // styles (e.g. dark-mode backgrounds). Strip them so pasted tables adopt
12301
- // the current theme instead of carrying stale colors.
12302
- #stripTableCellColorStyles(doc) {
12303
- for (const cell of doc.querySelectorAll("td, th")) {
12304
- cell.style.removeProperty("background-color");
12305
- cell.style.removeProperty("background");
12306
- cell.style.removeProperty("color");
12307
- }
12308
- }
12309
-
12310
12506
  #getTextAnchorData() {
12311
12507
  const selection = Qr();
12312
12508
  if (!selection || !selection.isCollapsed()) return { anchorNode: null, offset: 0 }
@@ -12624,7 +12820,7 @@ class Clipboard {
12624
12820
  }
12625
12821
 
12626
12822
  const linkNode = $$2(url).append(kr(url));
12627
- selection.insertNodes([ linkNode ]);
12823
+ BaseNodeInserter.for(selection).insertNodes([ linkNode ]);
12628
12824
 
12629
12825
  Ms(() => this.#dispatchLinkInsertEvent(linkNode.getKey(), { url }));
12630
12826
  }
@@ -14090,7 +14286,7 @@ class LexicalEditorElement extends HTMLElement {
14090
14286
  static debug = false
14091
14287
  static commands = [ "bold", "italic", "strikethrough" ]
14092
14288
 
14093
- static observedAttributes = [ "connected", "required" ]
14289
+ static observedAttributes = [ "autocapitalize", "connected", "required" ]
14094
14290
 
14095
14291
  #initialValue = ""
14096
14292
  #previousInternalFormValue = null
@@ -14157,8 +14353,15 @@ class LexicalEditorElement extends HTMLElement {
14157
14353
  }
14158
14354
 
14159
14355
  attributeChangedCallback(name, oldValue, newValue) {
14160
- if (name === "connected") this.connectedChangedCallback(oldValue, newValue);
14161
- if (name === "required") this.requiredChangedCallback(oldValue, newValue);
14356
+ if (typeof this[`${name}ChangedCallback`] === "function") {
14357
+ this[`${name}ChangedCallback`](oldValue, newValue);
14358
+ }
14359
+ }
14360
+
14361
+ autocapitalizeChangedCallback() {
14362
+ if (this.editorContentElement) {
14363
+ this.#transferAttributeToContentEditable(this.editorContentElement, "autocapitalize");
14364
+ }
14162
14365
  }
14163
14366
 
14164
14367
  connectedChangedCallback(oldValue, newValue) {
@@ -14490,25 +14693,33 @@ class LexicalEditorElement extends HTMLElement {
14490
14693
 
14491
14694
  #createEditorContentElement() {
14492
14695
  const editorContentElement = createElement("div", {
14696
+ id: `${this.id}-content`,
14493
14697
  classList: "lexxy-editor__content",
14494
14698
  contenteditable: true,
14495
- autocapitalize: "none",
14496
14699
  role: "textbox",
14497
14700
  "aria-multiline": true,
14498
14701
  "aria-label": this.#labelText,
14499
14702
  placeholder: this.getAttribute("placeholder")
14500
14703
  });
14501
- editorContentElement.id = `${this.id}-content`;
14704
+
14502
14705
  this.#ariaAttributes.forEach(attribute => editorContentElement.setAttribute(attribute.name, attribute.value));
14503
14706
 
14504
- if (this.getAttribute("tabindex")) {
14505
- editorContentElement.setAttribute("tabindex", this.getAttribute("tabindex"));
14506
- this.removeAttribute("tabindex");
14707
+ this.#transferAttributeToContentEditable(editorContentElement, "autocapitalize");
14708
+ this.#transferAttributeToContentEditable(editorContentElement, "tabindex", { defaultValue: 0, removeSource: true });
14709
+
14710
+ return editorContentElement
14711
+ }
14712
+
14713
+ #transferAttributeToContentEditable(element, qualifiedName, { defaultValue = null, removeSource = false } = {}) {
14714
+ if (this.hasAttribute(qualifiedName)) {
14715
+ element.setAttribute(qualifiedName, this.getAttribute(qualifiedName));
14716
+ } else if (defaultValue !== null) {
14717
+ element.setAttribute(qualifiedName, defaultValue);
14507
14718
  } else {
14508
- editorContentElement.setAttribute("tabindex", 0);
14719
+ element.removeAttribute(qualifiedName);
14509
14720
  }
14510
14721
 
14511
- return editorContentElement
14722
+ if (removeSource) this.removeAttribute(qualifiedName);
14512
14723
  }
14513
14724
 
14514
14725
  get #labelText() {
@@ -15125,6 +15336,7 @@ class LexicalPromptElement extends HTMLElement {
15125
15336
  this.source = this.#createSource();
15126
15337
 
15127
15338
  this.#addTriggerListener();
15339
+ this.#removePopoverBeforeTurboCaches();
15128
15340
  this.toggleAttribute("connected", true);
15129
15341
  }
15130
15342
 
@@ -15132,7 +15344,7 @@ class LexicalPromptElement extends HTMLElement {
15132
15344
  this.#popoverListeners.dispose();
15133
15345
  this.#globalListeners.dispose();
15134
15346
  this.source = null;
15135
- this.popoverElement = null;
15347
+ this.#removePopover();
15136
15348
  }
15137
15349
 
15138
15350
 
@@ -15428,6 +15640,21 @@ class LexicalPromptElement extends HTMLElement {
15428
15640
  this.#addTriggerListener();
15429
15641
  }
15430
15642
 
15643
+ // The popover is appended to the <lexxy-editor> subtree, so Turbo serializes it
15644
+ // into the page cache. Removing it before caching prevents an orphaned, unmanaged
15645
+ // popover from being restored on history back/forward.
15646
+ #removePopoverBeforeTurboCaches() {
15647
+ this.#globalListeners.track(
15648
+ registerEventListener(document, "turbo:before-cache", () => this.#removePopover())
15649
+ );
15650
+ }
15651
+
15652
+ #removePopover() {
15653
+ this.#popoverListeners.dispose();
15654
+ this.popoverElement?.remove();
15655
+ this.popoverElement = null;
15656
+ }
15657
+
15431
15658
  #filterOptions = async () => {
15432
15659
  if (this.initialPrompt) {
15433
15660
  this.initialPrompt = false;
Binary file
Binary file