lexxy 0.9.24 → 0.9.26

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: '08d5395bf49ba2c13a16064a9bae40bb6dd6e7d17a00235732374b66967d5532'
4
- data.tar.gz: 0446fe6c91c279e2b198ee2d42191e4a3d3ec80882945b20f85fc3f9522fe180
3
+ metadata.gz: f5c63b2cb8499843f516d8404a2a7573f93452f753b54672b53d80a60242d0ef
4
+ data.tar.gz: 13d59b10d606353cbc379ea5e940b40647fbe17a497f11a565c633ca1ae51519
5
5
  SHA512:
6
- metadata.gz: 6ce53b76db8039e8658761a8083ce5e6713cb7afb17c64838fb0561d15b25f2901ed22ed3bb715874659accbebbee78ac1797a5613659bdaeeabe40f034994f5
7
- data.tar.gz: ee2bb48df2a24e532d9f69b16cc205cf5a19fb22d3d2add102d8ec2f26406ca59bcd7dbcae4ea577ce423ca09eaaceb9d4939b19a673a4efabf5734e68775fd8
6
+ metadata.gz: 0e0b6c3b8763c773fbc0ebe6a4d17cc5c8ee00813c48e66151975640daf3866b9a9558128584812aba21de840982ff0273200158b529343fd98b0992641622b8
7
+ data.tar.gz: 72db851395aea2ad408a62dd78afd2e0cea0843b1484f39a19755f58d6a8a65c482c029bb876d5f109018820bee4b8141365976b35508bfd83adc0c9754dd543
data/README.md CHANGED
@@ -21,6 +21,7 @@ A modern rich text editor for Rails.
21
21
 
22
22
  - Bug reports and pull requests are welcome on [GitHub Issues](https://github.com/basecamp/lexxy/issues). Help is especially welcome with [those tagged as "Help Wanted"](https://github.com/basecamp/lexxy/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22).
23
23
  - For questions and general Lexxy discussion, please use the [Discussions section](https://github.com/basecamp/lexxy/discussions)
24
+ - To build with a development version of Lexical, use `bin/link-to-local-lexical`.
24
25
 
25
26
  ## License
26
27
 
@@ -7473,9 +7473,9 @@ class ToolbarDropdown extends HTMLElement {
7473
7473
  const HEADING_BUTTON_SELECTOR = "button.lexxy-heading-button";
7474
7474
 
7475
7475
  const HEADING_PRESETS = [
7476
- { label: "Large Heading", name: "heading-large" },
7477
- { label: "Medium Heading", name: "heading-medium" },
7478
- { label: "Small Heading", name: "heading-small" }
7476
+ { label: "Large Heading", name: "heading-large", command: "setFormatHeadingLarge" },
7477
+ { label: "Medium Heading", name: "heading-medium", command: "setFormatHeadingMedium" },
7478
+ { label: "Small Heading", name: "heading-small", command: "setFormatHeadingSmall" }
7479
7479
  ];
7480
7480
 
7481
7481
  class HeadingDropdown extends HTMLElement {
@@ -7505,6 +7505,14 @@ class HeadingDropdown extends HTMLElement {
7505
7505
  }
7506
7506
  }
7507
7507
 
7508
+ static commandFor(index) {
7509
+ if (index < HEADING_PRESETS.length) {
7510
+ return HEADING_PRESETS[index].command
7511
+ } else {
7512
+ return "applyHeadingFormat"
7513
+ }
7514
+ }
7515
+
7508
7516
  #listeners = new ListenerBin()
7509
7517
 
7510
7518
  connectedCallback() {
@@ -7567,6 +7575,7 @@ class HeadingDropdown extends HTMLElement {
7567
7575
  const button = document.createElement("button");
7568
7576
  button.type = "button";
7569
7577
  button.dataset.heading = tag;
7578
+ button.dataset.command = HeadingDropdown.commandFor(index);
7570
7579
  button.classList.add("lexxy-heading-button");
7571
7580
  button.name = name;
7572
7581
  button.title = label;
@@ -7581,7 +7590,7 @@ class HeadingDropdown extends HTMLElement {
7581
7590
  if (!this.#editor) return
7582
7591
 
7583
7592
  event.preventDefault();
7584
- this.#editor.dispatchCommand("applyHeadingFormat", button.dataset.heading);
7593
+ this.#editor.dispatchCommand(button.dataset.command, button.dataset.heading);
7585
7594
  this.#editor.focus();
7586
7595
  }));
7587
7596
  });
@@ -8450,14 +8459,29 @@ function $shrinkSelectionPastBlockEdges(selection) {
8450
8459
  if (!anchorBlock || !focusBlock || anchorBlock.is(focusBlock)) return
8451
8460
 
8452
8461
  if ($isAtBlockEnd(selection.anchor, anchorBlock)) {
8453
- const nextBlock = anchorBlock.getNextSibling();
8462
+ const nextBlock = $nearestElementSibling(anchorBlock, "next");
8454
8463
  if (nextBlock) selection.anchor.set(nextBlock.getKey(), 0, "element");
8455
8464
  }
8456
8465
 
8457
8466
  if ($isAtBlockStart(selection.focus, focusBlock)) {
8458
- const previousBlock = focusBlock.getPreviousSibling();
8467
+ const previousBlock = $nearestElementSibling(focusBlock, "previous");
8459
8468
  if (previousBlock) selection.focus.set(previousBlock.getKey(), previousBlock.getChildrenSize(), "element");
8460
8469
  }
8470
+
8471
+ // Shrinking moves the endpoints toward each other, so they can cross when
8472
+ // both were flush against block edges; callers assume a forward selection.
8473
+ j$6(selection);
8474
+ }
8475
+
8476
+ // Top-level decorator blocks (attachments, horizontal dividers) can't hold a
8477
+ // selection point, so walk past them to the nearest element sibling. Returns
8478
+ // null when no element sibling exists in that direction.
8479
+ function $nearestElementSibling(block, direction) {
8480
+ let sibling = block;
8481
+ do {
8482
+ sibling = direction === "next" ? sibling.getNextSibling() : sibling.getPreviousSibling();
8483
+ } while (sibling && !Wi(sibling))
8484
+ return sibling
8461
8485
  }
8462
8486
 
8463
8487
  function $isAtBlockEnd(point, block) {
@@ -10309,6 +10333,9 @@ const COMMANDS = [
10309
10333
  "unlink",
10310
10334
  "toggleHighlight",
10311
10335
  "removeHighlight",
10336
+ "setFormatHeadingLarge",
10337
+ "setFormatHeadingMedium",
10338
+ "setFormatHeadingSmall",
10312
10339
  "setFormatParagraph",
10313
10340
  "applyHeadingFormat",
10314
10341
  "clearFormatting",
@@ -10502,6 +10529,18 @@ class CommandDispatcher {
10502
10529
  Pt$3(new HorizontalDividerNode);
10503
10530
  }
10504
10531
 
10532
+ dispatchSetFormatHeadingLarge() {
10533
+ this.#applyConfiguredHeadingFormat(0);
10534
+ }
10535
+
10536
+ dispatchSetFormatHeadingMedium() {
10537
+ this.#applyConfiguredHeadingFormat(1);
10538
+ }
10539
+
10540
+ dispatchSetFormatHeadingSmall() {
10541
+ this.#applyConfiguredHeadingFormat(2);
10542
+ }
10543
+
10505
10544
  dispatchSetFormatParagraph() {
10506
10545
  this.contents.applyParagraphFormat();
10507
10546
  }
@@ -10558,6 +10597,11 @@ class CommandDispatcher {
10558
10597
  this.#listeners.dispose();
10559
10598
  }
10560
10599
 
10600
+ #applyConfiguredHeadingFormat(index) {
10601
+ const tag = this.editorElement.config.get("headings")[index];
10602
+ if (tag) this.contents.applyHeadingFormat(tag);
10603
+ }
10604
+
10561
10605
  #registerCommands() {
10562
10606
  for (const command of COMMANDS) {
10563
10607
  const methodName = `dispatch${capitalize(command)}`;
@@ -12128,11 +12172,11 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
12128
12172
  }
12129
12173
 
12130
12174
  #forgetUploadRequest() {
12131
- this.#editorElement.uploadRequests.forget(this.getKey());
12175
+ this.#editorElement?.uploadRequests.forget(this.getKey());
12132
12176
  }
12133
12177
 
12134
12178
  #rememberUploadRequest(request) {
12135
- this.#editorElement.uploadRequests.track(this.getKey(), request);
12179
+ this.#editorElement?.uploadRequests.track(this.getKey(), request);
12136
12180
  }
12137
12181
 
12138
12182
  get #editorElement() {
@@ -12438,13 +12482,27 @@ class PastedContentFormatter {
12438
12482
  }
12439
12483
 
12440
12484
  format() {
12485
+ this.#stripStyleElements();
12441
12486
  this.#unwrapPlaceholderAnchors();
12442
12487
  this.#stripTableCellColorStyles();
12488
+ this.#unwrapWrappedListChildren();
12443
12489
  this.#nestStrayListChildren();
12444
12490
  this.#stripStrayListChildren();
12445
12491
  return this.doc
12446
12492
  }
12447
12493
 
12494
+ // Spreadsheets (e.g. Excel) copy a <style> block whose rules (td { color:
12495
+ // black }, .xlNN { ... }) cascade onto the imported text. That color rides
12496
+ // in through the cascade rather than an inline style, so it slips past both
12497
+ // the cell-level stripping below and the paste style canonicalizer, leaving
12498
+ // pasted tables with foreign text colors that don't adapt to the theme. Drop
12499
+ // foreign style sheets so nothing cascades into imported content.
12500
+ #stripStyleElements() {
12501
+ for (const style of this.doc.querySelectorAll("style")) {
12502
+ style.remove();
12503
+ }
12504
+ }
12505
+
12448
12506
  // Anchors with non-meaningful hrefs (e.g. "#", "") appear in content copied
12449
12507
  // from rendered views where mentions and interactive elements are wrapped in
12450
12508
  // <a href="#"> tags. Unwrap them so their text content pastes as plain text
@@ -12469,6 +12527,29 @@ class PastedContentFormatter {
12469
12527
  }
12470
12528
  }
12471
12529
 
12530
+ // Some sources wrap runs of <li>s in a stray element (e.g. a <div> directly
12531
+ // inside the list). Dissolve such wrappers so their items become direct
12532
+ // list children and any nested list they hide becomes visible to
12533
+ // #nestStrayListChildren below.
12534
+ #unwrapWrappedListChildren() {
12535
+ for (const list of this.doc.querySelectorAll("ol, ul")) {
12536
+ let wrapper = this.#wrappedListChild(list);
12537
+ while (wrapper) {
12538
+ wrapper.replaceWith(...wrapper.childNodes);
12539
+ wrapper = this.#wrappedListChild(list);
12540
+ }
12541
+ }
12542
+ }
12543
+
12544
+ #wrappedListChild(list) {
12545
+ for (const child of list.children) {
12546
+ if (child.tagName !== "LI" && !this.#isNestedList(child) && this.#containsListItems(child)) {
12547
+ return child
12548
+ }
12549
+ }
12550
+ return null
12551
+ }
12552
+
12472
12553
  // Some sources (e.g. Gmail) nest a sublist as a direct child of the parent
12473
12554
  // <ol>/<ul> instead of inside a <li>. Move each nested list into its
12474
12555
  // preceding <li> so the import preserves the nesting instead of dropping it.
@@ -12485,15 +12566,40 @@ class PastedContentFormatter {
12485
12566
  }
12486
12567
  }
12487
12568
 
12488
- // Only <li> is a valid child of a list; drop stray <br>/whitespace so the
12489
- // import doesn't wrap them into an empty leading item.
12569
+ // Only <li> is a valid child of a list. Unwrap remaining stray children
12570
+ // that still hold list items (a nested list with no preceding <li> to nest
12571
+ // under) so the items survive, and drop stray <br>/whitespace so the import
12572
+ // doesn't wrap them into an empty leading item.
12490
12573
  #stripStrayListChildren() {
12491
12574
  for (const list of this.doc.querySelectorAll("ol, ul")) {
12492
- for (const child of Array.from(list.childNodes)) {
12493
- if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "LI") continue
12494
- list.removeChild(child);
12575
+ let stray = this.#firstStrayListChild(list);
12576
+ while (stray) {
12577
+ if (this.#containsListItems(stray)) {
12578
+ stray.replaceWith(...stray.childNodes);
12579
+ } else {
12580
+ stray.remove();
12581
+ }
12582
+
12583
+ stray = this.#firstStrayListChild(list);
12584
+ }
12585
+ }
12586
+ }
12587
+
12588
+ #firstStrayListChild(list) {
12589
+ for (const child of list.childNodes) {
12590
+ if (child.nodeType !== Node.ELEMENT_NODE || child.tagName !== "LI") {
12591
+ return child
12495
12592
  }
12496
12593
  }
12594
+ return null
12595
+ }
12596
+
12597
+ #isNestedList(node) {
12598
+ return node.nodeType === Node.ELEMENT_NODE && (node.tagName === "OL" || node.tagName === "UL")
12599
+ }
12600
+
12601
+ #containsListItems(node) {
12602
+ return node.nodeType === Node.ELEMENT_NODE && node.querySelector("li") !== null
12497
12603
  }
12498
12604
  }
12499
12605
 
@@ -16046,7 +16152,7 @@ class LexicalEditorElement extends HTMLElement {
16046
16152
  { label: "Normal", command: "setFormatParagraph", tag: null },
16047
16153
  ...headings.map((tag, index) => ({
16048
16154
  label: HeadingDropdown.labelFor(tag, index),
16049
- command: "applyHeadingFormat",
16155
+ command: HeadingDropdown.commandFor(index),
16050
16156
  tag
16051
16157
  }))
16052
16158
  ]
Binary file
Binary file