@37signals/lexxy 0.9.24 → 0.9.25

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.
Files changed (2) hide show
  1. package/dist/lexxy.esm.js +66 -9
  2. package/package.json +1 -1
package/dist/lexxy.esm.js CHANGED
@@ -1061,9 +1061,9 @@ class ToolbarDropdown extends HTMLElement {
1061
1061
  const HEADING_BUTTON_SELECTOR = "button.lexxy-heading-button";
1062
1062
 
1063
1063
  const HEADING_PRESETS = [
1064
- { label: "Large Heading", name: "heading-large" },
1065
- { label: "Medium Heading", name: "heading-medium" },
1066
- { label: "Small Heading", name: "heading-small" }
1064
+ { label: "Large Heading", name: "heading-large", command: "setFormatHeadingLarge" },
1065
+ { label: "Medium Heading", name: "heading-medium", command: "setFormatHeadingMedium" },
1066
+ { label: "Small Heading", name: "heading-small", command: "setFormatHeadingSmall" }
1067
1067
  ];
1068
1068
 
1069
1069
  class HeadingDropdown extends HTMLElement {
@@ -1093,6 +1093,14 @@ class HeadingDropdown extends HTMLElement {
1093
1093
  }
1094
1094
  }
1095
1095
 
1096
+ static commandFor(index) {
1097
+ if (index < HEADING_PRESETS.length) {
1098
+ return HEADING_PRESETS[index].command
1099
+ } else {
1100
+ return "applyHeadingFormat"
1101
+ }
1102
+ }
1103
+
1096
1104
  #listeners = new ListenerBin()
1097
1105
 
1098
1106
  connectedCallback() {
@@ -1155,6 +1163,7 @@ class HeadingDropdown extends HTMLElement {
1155
1163
  const button = document.createElement("button");
1156
1164
  button.type = "button";
1157
1165
  button.dataset.heading = tag;
1166
+ button.dataset.command = HeadingDropdown.commandFor(index);
1158
1167
  button.classList.add("lexxy-heading-button");
1159
1168
  button.name = name;
1160
1169
  button.title = label;
@@ -1169,7 +1178,7 @@ class HeadingDropdown extends HTMLElement {
1169
1178
  if (!this.#editor) return
1170
1179
 
1171
1180
  event.preventDefault();
1172
- this.#editor.dispatchCommand("applyHeadingFormat", button.dataset.heading);
1181
+ this.#editor.dispatchCommand(button.dataset.command, button.dataset.heading);
1173
1182
  this.#editor.focus();
1174
1183
  }));
1175
1184
  });
@@ -1938,14 +1947,29 @@ function $shrinkSelectionPastBlockEdges(selection) {
1938
1947
  if (!anchorBlock || !focusBlock || anchorBlock.is(focusBlock)) return
1939
1948
 
1940
1949
  if ($isAtBlockEnd(selection.anchor, anchorBlock)) {
1941
- const nextBlock = anchorBlock.getNextSibling();
1950
+ const nextBlock = $nearestElementSibling(anchorBlock, "next");
1942
1951
  if (nextBlock) selection.anchor.set(nextBlock.getKey(), 0, "element");
1943
1952
  }
1944
1953
 
1945
1954
  if ($isAtBlockStart(selection.focus, focusBlock)) {
1946
- const previousBlock = focusBlock.getPreviousSibling();
1955
+ const previousBlock = $nearestElementSibling(focusBlock, "previous");
1947
1956
  if (previousBlock) selection.focus.set(previousBlock.getKey(), previousBlock.getChildrenSize(), "element");
1948
1957
  }
1958
+
1959
+ // Shrinking moves the endpoints toward each other, so they can cross when
1960
+ // both were flush against block edges; callers assume a forward selection.
1961
+ $ensureForwardRangeSelection(selection);
1962
+ }
1963
+
1964
+ // Top-level decorator blocks (attachments, horizontal dividers) can't hold a
1965
+ // selection point, so walk past them to the nearest element sibling. Returns
1966
+ // null when no element sibling exists in that direction.
1967
+ function $nearestElementSibling(block, direction) {
1968
+ let sibling = block;
1969
+ do {
1970
+ sibling = direction === "next" ? sibling.getNextSibling() : sibling.getPreviousSibling();
1971
+ } while (sibling && !$isElementNode(sibling))
1972
+ return sibling
1949
1973
  }
1950
1974
 
1951
1975
  function $isAtBlockEnd(point, block) {
@@ -3747,6 +3771,9 @@ const COMMANDS = [
3747
3771
  "unlink",
3748
3772
  "toggleHighlight",
3749
3773
  "removeHighlight",
3774
+ "setFormatHeadingLarge",
3775
+ "setFormatHeadingMedium",
3776
+ "setFormatHeadingSmall",
3750
3777
  "setFormatParagraph",
3751
3778
  "applyHeadingFormat",
3752
3779
  "clearFormatting",
@@ -3940,6 +3967,18 @@ class CommandDispatcher {
3940
3967
  $insertNodeToNearestRoot(new HorizontalDividerNode);
3941
3968
  }
3942
3969
 
3970
+ dispatchSetFormatHeadingLarge() {
3971
+ this.#applyConfiguredHeadingFormat(0);
3972
+ }
3973
+
3974
+ dispatchSetFormatHeadingMedium() {
3975
+ this.#applyConfiguredHeadingFormat(1);
3976
+ }
3977
+
3978
+ dispatchSetFormatHeadingSmall() {
3979
+ this.#applyConfiguredHeadingFormat(2);
3980
+ }
3981
+
3943
3982
  dispatchSetFormatParagraph() {
3944
3983
  this.contents.applyParagraphFormat();
3945
3984
  }
@@ -3996,6 +4035,11 @@ class CommandDispatcher {
3996
4035
  this.#listeners.dispose();
3997
4036
  }
3998
4037
 
4038
+ #applyConfiguredHeadingFormat(index) {
4039
+ const tag = this.editorElement.config.get("headings")[index];
4040
+ if (tag) this.contents.applyHeadingFormat(tag);
4041
+ }
4042
+
3999
4043
  #registerCommands() {
4000
4044
  for (const command of COMMANDS) {
4001
4045
  const methodName = `dispatch${capitalize(command)}`;
@@ -5566,11 +5610,11 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
5566
5610
  }
5567
5611
 
5568
5612
  #forgetUploadRequest() {
5569
- this.#editorElement.uploadRequests.forget(this.getKey());
5613
+ this.#editorElement?.uploadRequests.forget(this.getKey());
5570
5614
  }
5571
5615
 
5572
5616
  #rememberUploadRequest(request) {
5573
- this.#editorElement.uploadRequests.track(this.getKey(), request);
5617
+ this.#editorElement?.uploadRequests.track(this.getKey(), request);
5574
5618
  }
5575
5619
 
5576
5620
  get #editorElement() {
@@ -5876,6 +5920,7 @@ class PastedContentFormatter {
5876
5920
  }
5877
5921
 
5878
5922
  format() {
5923
+ this.#stripStyleElements();
5879
5924
  this.#unwrapPlaceholderAnchors();
5880
5925
  this.#stripTableCellColorStyles();
5881
5926
  this.#nestStrayListChildren();
@@ -5883,6 +5928,18 @@ class PastedContentFormatter {
5883
5928
  return this.doc
5884
5929
  }
5885
5930
 
5931
+ // Spreadsheets (e.g. Excel) copy a <style> block whose rules (td { color:
5932
+ // black }, .xlNN { ... }) cascade onto the imported text. That color rides
5933
+ // in through the cascade rather than an inline style, so it slips past both
5934
+ // the cell-level stripping below and the paste style canonicalizer, leaving
5935
+ // pasted tables with foreign text colors that don't adapt to the theme. Drop
5936
+ // foreign style sheets so nothing cascades into imported content.
5937
+ #stripStyleElements() {
5938
+ for (const style of this.doc.querySelectorAll("style")) {
5939
+ style.remove();
5940
+ }
5941
+ }
5942
+
5886
5943
  // Anchors with non-meaningful hrefs (e.g. "#", "") appear in content copied
5887
5944
  // from rendered views where mentions and interactive elements are wrapped in
5888
5945
  // <a href="#"> tags. Unwrap them so their text content pastes as plain text
@@ -9412,7 +9469,7 @@ class LexicalEditorElement extends HTMLElement {
9412
9469
  { label: "Normal", command: "setFormatParagraph", tag: null },
9413
9470
  ...headings.map((tag, index) => ({
9414
9471
  label: HeadingDropdown.labelFor(tag, index),
9415
- command: "applyHeadingFormat",
9472
+ command: HeadingDropdown.commandFor(index),
9416
9473
  tag
9417
9474
  }))
9418
9475
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.24",
3
+ "version": "0.9.25",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",