lexxy 0.9.26 → 0.9.28

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: f5c63b2cb8499843f516d8404a2a7573f93452f753b54672b53d80a60242d0ef
4
- data.tar.gz: 13d59b10d606353cbc379ea5e940b40647fbe17a497f11a565c633ca1ae51519
3
+ metadata.gz: afb455253a99563e4938f9f1634f997fdb3231f52e7b760e53cb590eb4873934
4
+ data.tar.gz: 8334d0fb26282b75e340e5d5c5eeb65000a876700038331d2f58347865ef6232
5
5
  SHA512:
6
- metadata.gz: 0e0b6c3b8763c773fbc0ebe6a4d17cc5c8ee00813c48e66151975640daf3866b9a9558128584812aba21de840982ff0273200158b529343fd98b0992641622b8
7
- data.tar.gz: 72db851395aea2ad408a62dd78afd2e0cea0843b1484f39a19755f58d6a8a65c482c029bb876d5f109018820bee4b8141365976b35508bfd83adc0c9754dd543
6
+ metadata.gz: 46e58bf9d6993735988227cea4c883a8cb8f6e485626c8a7a28cce6e128b2d417faaef93ff404b0d0c64505263df7424ef909c91fe463576f9fe0087a823ef31
7
+ data.tar.gz: 9c5250b597e25d858378e6b219fab8b3c09b6e472f224fff879b22a7b02c85313b3c74a574142566aca9dcc645ceb0691793d30c8b86e1f1cc2c3d5f69382261
@@ -11085,10 +11085,34 @@ class Selection {
11085
11085
 
11086
11086
  this.editor.registerCommand(ie$4, () => {
11087
11087
  this.#syncSelectedClasses();
11088
- }, io)
11088
+ }, io),
11089
+
11090
+ this.editor.registerCommand(ie$4, () => {
11091
+ this.#preserveFocusOfOtherFields();
11092
+ return false
11093
+ }, lo)
11089
11094
  );
11090
11095
  }
11091
11096
 
11097
+ // Firefox (152+) keeps window.getSelection() anchored inside the editor after focus
11098
+ // moves to another field, and fires document selectionchange for every keystroke
11099
+ // typed there. Lexical reads that stale selection, reconciles, and pulls focus back
11100
+ // into the editor — stealing it mid-typing. When another text-entry field owns focus,
11101
+ // tag the update so Lexical leaves the DOM selection (and with it, focus) alone.
11102
+ #preserveFocusOfOtherFields() {
11103
+ if (this.#anotherFieldIsFocused) {
11104
+ Os(qn);
11105
+ }
11106
+ }
11107
+
11108
+ get #anotherFieldIsFocused() {
11109
+ const rootElement = this.editor.getRootElement();
11110
+ const activeElement = rootElement?.ownerDocument.activeElement;
11111
+
11112
+ return activeElement && !rootElement.contains(activeElement) &&
11113
+ (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement.isContentEditable)
11114
+ }
11115
+
11092
11116
  #listenForNodeSelections() {
11093
11117
  this.#listeners.track(this.editor.registerCommand(se$5, ({ target }) => {
11094
11118
  if (!qs(target)) return false
@@ -12476,6 +12500,149 @@ BaseNodeInserter.for = (selection) => {
12476
12500
  return inserterClass ? new inserterClass(selection) : selection
12477
12501
  };
12478
12502
 
12503
+ // Word and Outlook never emit <ul>/<ol>/<li>. They simulate lists with flat
12504
+ // paragraphs that declare their depth in an inline `mso-list:l0 level2 lfo1`
12505
+ // and carry the bullet or number glyph in a <span style="mso-list:Ignore">.
12506
+ // Nothing downstream understands either, so the items land as plain paragraphs
12507
+ // with the marker baked into the text. Rebuild real lists from the level each
12508
+ // paragraph declares.
12509
+ //
12510
+ // Everything this reads lives in inline attributes, so it stays correct after
12511
+ // the pasted content formatter has dropped Office's style sheet.
12512
+ class OfficeFormatter {
12513
+ constructor(doc) {
12514
+ this.doc = doc;
12515
+ }
12516
+
12517
+ format() {
12518
+ for (const run of this.#listRuns()) {
12519
+ this.#replaceWithLists(run);
12520
+ }
12521
+ }
12522
+
12523
+ // A run is a stretch of Office list paragraphs that are siblings with nothing
12524
+ // but whitespace and Word's conditional comments between them. Anything else
12525
+ // in between (a heading, a plain paragraph) ends one list and starts another.
12526
+ #listRuns() {
12527
+ const runs = [];
12528
+ const claimed = new Set();
12529
+
12530
+ for (const paragraph of this.doc.querySelectorAll("p[style*='mso-list']")) {
12531
+ if (claimed.has(paragraph) || this.#listLevel(paragraph) === null) continue
12532
+
12533
+ const run = [];
12534
+ let node = paragraph;
12535
+ while (node) {
12536
+ run.push(node);
12537
+ claimed.add(node);
12538
+ node = this.#nextListParagraph(node);
12539
+ }
12540
+ runs.push(run);
12541
+ }
12542
+
12543
+ return runs
12544
+ }
12545
+
12546
+ #nextListParagraph(paragraph) {
12547
+ let sibling = paragraph.nextSibling;
12548
+ while (sibling && this.#isIgnorableBetweenListParagraphs(sibling)) {
12549
+ sibling = sibling.nextSibling;
12550
+ }
12551
+
12552
+ if (sibling && sibling.nodeType === Node.ELEMENT_NODE && this.#listLevel(sibling) !== null) {
12553
+ return sibling
12554
+ }
12555
+
12556
+ return null
12557
+ }
12558
+
12559
+ #isIgnorableBetweenListParagraphs(node) {
12560
+ if (node.nodeType === Node.COMMENT_NODE) {
12561
+ return true
12562
+ }
12563
+ return node.nodeType === Node.TEXT_NODE && node.textContent.trim() === ""
12564
+ }
12565
+
12566
+ // Word numbers levels from 1, but a copied selection often starts partway down
12567
+ // a list, so every paragraph declares level3 with no level1 above it. Anchoring
12568
+ // the run at its own shallowest level is what keeps the result from arriving
12569
+ // needlessly deep.
12570
+ #replaceWithLists(paragraphs) {
12571
+ const levels = paragraphs.map((paragraph) => this.#listLevel(paragraph));
12572
+ const baseLevel = Math.min(...levels);
12573
+ const roots = [];
12574
+ const stack = [];
12575
+
12576
+ paragraphs.forEach((paragraph, index) => {
12577
+ const depth = levels[index] - baseLevel + 1;
12578
+ const tagName = this.#listTagName(paragraph);
12579
+
12580
+ while (stack.length > depth) {
12581
+ stack.pop();
12582
+ }
12583
+ if (stack.length === depth && stack[depth - 1].tagName !== tagName.toUpperCase()) {
12584
+ stack.pop();
12585
+ }
12586
+
12587
+ while (stack.length < depth) {
12588
+ const list = this.doc.createElement(tagName);
12589
+ if (stack.length === 0) {
12590
+ roots.push(list);
12591
+ } else {
12592
+ this.#lastItemOf(stack[stack.length - 1]).appendChild(list);
12593
+ }
12594
+ stack.push(list);
12595
+ }
12596
+
12597
+ stack[stack.length - 1].appendChild(this.#createListItem(paragraph));
12598
+ });
12599
+
12600
+ paragraphs[0].replaceWith(...roots);
12601
+ for (const paragraph of paragraphs.slice(1)) {
12602
+ paragraph.remove();
12603
+ }
12604
+ }
12605
+
12606
+ #lastItemOf(list) {
12607
+ if (!list.lastElementChild) {
12608
+ list.appendChild(this.doc.createElement("li"));
12609
+ }
12610
+ return list.lastElementChild
12611
+ }
12612
+
12613
+ // Word writes bullets as the raw glyph of the marker font (Symbol's "·",
12614
+ // Wingdings' "§", Courier New's "o") and numbers as the rendered counter
12615
+ // ("1.", "a)", "iv)"). A bare letter with no delimiter after it is Courier
12616
+ // New's bullet, not a counter, so the delimiter is what tells them apart.
12617
+ #listTagName(paragraph) {
12618
+ const marker = paragraph.querySelector("[style*='mso-list:Ignore']");
12619
+ const text = marker?.textContent || "";
12620
+
12621
+ if (/\d/.test(text) || /^\(?[A-Za-z]+[.)]/.test(text)) {
12622
+ return "ol"
12623
+ }
12624
+ return "ul"
12625
+ }
12626
+
12627
+ #createListItem(paragraph) {
12628
+ for (const marker of paragraph.querySelectorAll("[style*='mso-list:Ignore']")) {
12629
+ marker.remove();
12630
+ }
12631
+
12632
+ const item = this.doc.createElement("li");
12633
+ item.append(...paragraph.childNodes);
12634
+ return item
12635
+ }
12636
+
12637
+ #listLevel(element) {
12638
+ const match = /mso-list:[^;"']*\blevel(\d+)/.exec(element.getAttribute("style") || "");
12639
+ if (match) {
12640
+ return parseInt(match[1], 10)
12641
+ }
12642
+ return null
12643
+ }
12644
+ }
12645
+
12479
12646
  class PastedContentFormatter {
12480
12647
  constructor(doc) {
12481
12648
  this.doc = doc;
@@ -12485,6 +12652,7 @@ class PastedContentFormatter {
12485
12652
  this.#stripStyleElements();
12486
12653
  this.#unwrapPlaceholderAnchors();
12487
12654
  this.#stripTableCellColorStyles();
12655
+ new OfficeFormatter(this.doc).format();
12488
12656
  this.#unwrapWrappedListChildren();
12489
12657
  this.#nestStrayListChildren();
12490
12658
  this.#stripStrayListChildren();
@@ -12643,6 +12811,20 @@ class Contents {
12643
12811
  }, { tag });
12644
12812
  }
12645
12813
 
12814
+ // A literal "\n" stays visible in the editor, which Lexical renders with white-space: pre-wrap,
12815
+ // but collapses to a space once serialized to HTML, so it has to become line break nodes.
12816
+ insertTextWithLineBreaks(text) {
12817
+ const normalizedText = text?.replace(/\r\n?/g, "\n");
12818
+ const selection = Qr();
12819
+
12820
+ if (normalizedText?.includes("\n") && Fr(selection)) {
12821
+ selection.insertRawText(normalizedText);
12822
+ return true
12823
+ } else {
12824
+ return false
12825
+ }
12826
+ }
12827
+
12646
12828
  insertAtCursor(...nodes) {
12647
12829
  const selection = this.#insertableSelection();
12648
12830
  const inserter = BaseNodeInserter.for(selection);
@@ -15706,6 +15888,7 @@ class LexicalEditorElement extends HTMLElement {
15706
15888
  #initialize() {
15707
15889
  this.#registerComponents();
15708
15890
  this.#handleEnter();
15891
+ this.#handleReplacementText();
15709
15892
  this.#registerFocusEvents();
15710
15893
  this.#registerHistoryEvents();
15711
15894
  this.#registerFileAcceptFilter();
@@ -15969,6 +16152,14 @@ class LexicalEditorElement extends HTMLElement {
15969
16152
  ));
15970
16153
  }
15971
16154
 
16155
+ #handleReplacementText() {
16156
+ this.#listeners.track(this.editor.registerCommand(
16157
+ ge$3,
16158
+ (event) => event instanceof InputEvent && this.contents.insertTextWithLineBreaks(event.data),
16159
+ oo
16160
+ ));
16161
+ }
16162
+
15972
16163
  #registerFocusEvents() {
15973
16164
  this.#listeners.track(
15974
16165
  registerEventListener(this, "focusin", this.#handleFocusIn),
Binary file
Binary file