@37signals/lexxy 0.9.28 → 0.9.30

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 +182 -37
  2. package/package.json +1 -1
package/dist/lexxy.esm.js CHANGED
@@ -1,7 +1,7 @@
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, defineExtension, $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, 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, COMMAND_PRIORITY_CRITICAL, $addUpdateTag, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, ElementNode, $splitNode, $getChildCaretAtIndex, $createLineBreakNode, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, PASTE_COMMAND, $onUpdate, ParagraphNode, RootNode, DRAGSTART_COMMAND, DROP_COMMAND, mergeRegister as mergeRegister$1, $createRangeSelectionFromDom, CLEAR_HISTORY_COMMAND, KEY_ENTER_COMMAND, CONTROLLED_TEXT_INSERTION_COMMAND, KEY_SPACE_COMMAND, INPUT_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
4
+ import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, defineExtension, $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, 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, COMMAND_PRIORITY_CRITICAL, $addUpdateTag, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, ElementNode, $splitNode, $getChildCaretAtIndex, $createLineBreakNode, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, PASTE_COMMAND, $onUpdate, ParagraphNode, RootNode, DRAGSTART_COMMAND, DROP_COMMAND, mergeRegister as mergeRegister$1, $createRangeSelectionFromDom, CLEAR_HISTORY_COMMAND, KEY_ENTER_COMMAND, KEY_SPACE_COMMAND, INPUT_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
5
5
  import * as lexical from 'lexical';
6
6
  export { lexical as Lexical };
7
7
  import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
@@ -15,7 +15,7 @@ import { HistoryExtension } from '@lexical/history';
15
15
  import { $isCodeNode, CodeHighlightNode, PrismTokenizer, $isCodeHighlightNode, $createCodeNode, $createCodeHighlightNode, CodeNode, registerCodeHighlighting, normalizeCodeLang, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
16
16
  import { TRANSFORMERS, registerMarkdownShortcuts } from '@lexical/markdown';
17
17
  import { INSERT_TABLE_COMMAND, $getTableCellNodeFromLexicalNode, TableCellNode, TableNode, TableRowNode, setScrollableTablesActive, registerTablePlugin, registerTableSelectionObserver, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
18
- import { marked } from 'marked';
18
+ import { Marked } from 'marked';
19
19
  import { $insertDataTransferForRichText } from '@lexical/clipboard';
20
20
  import 'prismjs';
21
21
  import 'prismjs/components/prism-clike';
@@ -67,6 +67,22 @@ function styleFilterHook(_currentNode, hookEvent) {
67
67
 
68
68
  DOMPurify.addHook("uponSanitizeAttribute", styleFilterHook);
69
69
 
70
+ const FORBIDDEN_STIMULUS_ATTRIBUTES = [ "data-controller", "data-action" ];
71
+
72
+ // Stimulus behavior attributes must never survive sanitization, whatever an
73
+ // extension's allowedElements declares. FORBID_ATTR alone isn't enough: in
74
+ // DOMPurify 3.x the functional ADD_ATTR — which Lexxy builds from the public
75
+ // allowedElements API — is evaluated ahead of FORBID_ATTR, so an extension that
76
+ // listed one of these on a tag would otherwise reinstate it. This hook drops
77
+ // them unconditionally, keeping the class-level prohibition config-independent.
78
+ function stimulusAttributeFilterHook(_currentNode, hookEvent) {
79
+ if (FORBIDDEN_STIMULUS_ATTRIBUTES.includes(hookEvent.attrName)) {
80
+ hookEvent.keepAttr = false;
81
+ }
82
+ }
83
+
84
+ DOMPurify.addHook("uponSanitizeAttribute", stimulusAttributeFilterHook);
85
+
70
86
  DOMPurify.addHook("uponSanitizeElement", (node, data) => {
71
87
  if (data.tagName === "strong" || data.tagName === "em") {
72
88
  node.removeAttribute("class");
@@ -90,7 +106,12 @@ function buildConfig(allowedElements ) {
90
106
  ALLOWED_ATTR: ALLOWED_HTML_ATTRIBUTES,
91
107
  ADD_ATTR: (attribute, tag) => tagAttributes[tag]?.includes(attribute),
92
108
  ADD_URI_SAFE_ATTR: [ "caption", "filename" ],
93
- SAFE_FOR_XML: false // So that it does not strip attributes that contains serialized HTML (like content)
109
+ SAFE_FOR_XML: false, // So that it does not strip attributes that contains serialized HTML (like content)
110
+ // Stimulus behavior attributes must never survive sanitization: they let stored content
111
+ // wire up arbitrary controllers/actions in the viewer's session. FORBID_ATTR wins over
112
+ // ALLOWED_ATTR/ADD_ATTR/ALLOW_DATA_ATTR in DOMPurify, so this holds even though other
113
+ // data-* attributes (data-language, data-trix-*, etc.) are otherwise allowed through.
114
+ FORBID_ATTR: [ "data-controller", "data-action" ]
94
115
  }
95
116
  }
96
117
 
@@ -1527,7 +1548,7 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
1527
1548
  return Lexxy.global.get("attachmentTagName")
1528
1549
  }
1529
1550
 
1530
- constructor({ tagName, sgid, contentType, innerHtml, plainText }, key) {
1551
+ constructor({ tagName, sgid, contentType, innerHtml, plainText } = {}, key) {
1531
1552
  super(key);
1532
1553
 
1533
1554
  const contentTypeNamespace = Lexxy.global.get("attachmentContentTypeNamespace");
@@ -2328,7 +2349,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
2328
2349
  return Lexxy.global.get("attachmentTagName")
2329
2350
  }
2330
2351
 
2331
- constructor({ tagName, sgid, src, previewSrc, previewable, previewStatusUrl, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError }, key) {
2352
+ constructor({ tagName, sgid, src, previewSrc, previewable, previewStatusUrl, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError } = {}, key) {
2332
2353
  super(key);
2333
2354
 
2334
2355
  this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
@@ -5472,7 +5493,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
5472
5493
  return null
5473
5494
  }
5474
5495
 
5475
- constructor(node, key) {
5496
+ constructor(node = {}, key) {
5476
5497
  const { file, uploadUrl, blobUrlTemplate, progress, width, height, uploadError, fileName, contentType } = node;
5477
5498
  super({ ...node, contentType: file?.type ?? contentType }, key);
5478
5499
  this.file = file ?? null;
@@ -5493,9 +5514,10 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
5493
5514
  // node is reloaded from saved state such as from history.
5494
5515
  this.#startUploadIfNeeded();
5495
5516
 
5496
- // Bridge-managed uploads (uploadUrl is null) don't have file data to show
5497
- // an image preview, so always show the file icon during upload.
5498
- const canPreviewFile = this.isPreviewableAttachment && this.uploadUrl != null;
5517
+ // Bridge-managed uploads (uploadUrl is null) and restored or remote
5518
+ // instances (no local File) don't have file data to show an image
5519
+ // preview, so always show the file icon during upload.
5520
+ const canPreviewFile = this.isPreviewableAttachment && this.uploadUrl != null && this.file != null;
5499
5521
  const figure = this.createAttachmentFigure(canPreviewFile);
5500
5522
 
5501
5523
  if (canPreviewFile) {
@@ -5566,9 +5588,12 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
5566
5588
  const figcaption = createElement("figcaption", { className: "attachment__caption" });
5567
5589
 
5568
5590
  const nameSpan = createElement("span", { className: "attachment__name", textContent: this.caption || this.fileName || "" });
5569
- const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.file?.size) });
5570
5591
  figcaption.appendChild(nameSpan);
5571
- figcaption.appendChild(sizeSpan);
5592
+
5593
+ if (this.file) {
5594
+ const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.file.size) });
5595
+ figcaption.appendChild(sizeSpan);
5596
+ }
5572
5597
 
5573
5598
  return figcaption
5574
5599
  }
@@ -5590,6 +5615,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
5590
5615
  async #startUploadIfNeeded() {
5591
5616
  if (this.#uploadStarted) return
5592
5617
  if (!this.uploadUrl) return // Bridge-managed upload — skip DirectUpload
5618
+ if (!this.file) return // Restored or remote instance — no local File to upload
5593
5619
 
5594
5620
  this.#setUploadStarted();
5595
5621
 
@@ -6081,12 +6107,24 @@ class OfficeFormatter {
6081
6107
  }
6082
6108
  }
6083
6109
 
6110
+ // Properties worth rescuing from a foreign style sheet before #stripStyleElements
6111
+ // drops it, keyed by the elements that carry them. Word and Outlook declare
6112
+ // paragraph spacing in the sheet (p.MsoNormal { margin: 0cm }) and never inline,
6113
+ // so once the sheet goes there is nothing left to say how tightly the author's
6114
+ // paragraphs sat, and hosts that turn pasted paragraph margins into explicit
6115
+ // blank lines read tight Office paragraphs as spaced ones and multiply every
6116
+ // line break.
6117
+ const PRESERVABLE_STYLES = {
6118
+ p: [ "margin-top", "margin-bottom" ]
6119
+ };
6120
+
6084
6121
  class PastedContentFormatter {
6085
6122
  constructor(doc) {
6086
6123
  this.doc = doc;
6087
6124
  }
6088
6125
 
6089
6126
  format() {
6127
+ this.#inlinePreservableStyles();
6090
6128
  this.#stripStyleElements();
6091
6129
  this.#unwrapPlaceholderAnchors();
6092
6130
  this.#stripTableCellColorStyles();
@@ -6094,9 +6132,23 @@ class PastedContentFormatter {
6094
6132
  this.#unwrapWrappedListChildren();
6095
6133
  this.#nestStrayListChildren();
6096
6134
  this.#stripStrayListChildren();
6135
+ this.#replaceGmailEmojiImgTags();
6097
6136
  return this.doc
6098
6137
  }
6099
6138
 
6139
+ // Runs before #stripStyleElements, to inline anything we still need
6140
+ // from an external style before we remove it
6141
+ #inlinePreservableStyles() {
6142
+ const rules = this.#styleRules();
6143
+ if (rules.length === 0) return
6144
+
6145
+ for (const [ selector, properties ] of Object.entries(PRESERVABLE_STYLES)) {
6146
+ for (const element of this.doc.querySelectorAll(selector)) {
6147
+ this.#inlineDeclaredStyles(element, properties, rules);
6148
+ }
6149
+ }
6150
+ }
6151
+
6100
6152
  // Spreadsheets (e.g. Excel) copy a <style> block whose rules (td { color:
6101
6153
  // black }, .xlNN { ... }) cascade onto the imported text. That color rides
6102
6154
  // in through the cascade rather than an inline style, so it slips past both
@@ -6191,6 +6243,72 @@ class PastedContentFormatter {
6191
6243
  }
6192
6244
  }
6193
6245
 
6246
+ // Read the rules from a sheet we build rather than from this.doc.styleSheets: the
6247
+ // document came from DOMParser, so it has no browsing context, and WebKit leaves
6248
+ // its styleSheets empty. Parsing the text ourselves still expands the shorthands
6249
+ // Word writes (margin: 0cm) into the longhands we ask for.
6250
+ #styleRules() {
6251
+ const css = Array.from(this.doc.querySelectorAll("style"), (style) => style.textContent).join("\n");
6252
+ if (css.trim() === "") return []
6253
+
6254
+ const sheet = new CSSStyleSheet();
6255
+ sheet.replaceSync(css);
6256
+
6257
+ const rules = [];
6258
+ for (const rule of sheet.cssRules) {
6259
+ if (rule instanceof CSSStyleRule) {
6260
+ rules.push(rule);
6261
+ }
6262
+ }
6263
+ return rules
6264
+ }
6265
+
6266
+ // A declaration the element already carries inline is the author's own and wins.
6267
+ #inlineDeclaredStyles(element, properties, rules) {
6268
+ for (const property of properties) {
6269
+ if (element.style.getPropertyValue(property) === "") {
6270
+ const value = this.#declaredValue(rules, element, property);
6271
+ if (value) {
6272
+ this.#appendStyleProperty(element, property, value);
6273
+ }
6274
+ }
6275
+ }
6276
+ }
6277
+
6278
+ // Source order alone doesn't decide the cascade, so when the matching rules
6279
+ // disagree we can't say which value wins without resolving specificity. Skip
6280
+ // the property rather than guess: guessing is permanent, since the sheet is
6281
+ // about to go, and skipping only leaves the host on its own default.
6282
+ #declaredValue(rules, element, property) {
6283
+ const declared = new Set();
6284
+ for (const rule of rules) {
6285
+ const value = rule.style.getPropertyValue(property);
6286
+ if (value !== "" && element.matches(rule.selectorText)) {
6287
+ declared.add(value);
6288
+ }
6289
+ }
6290
+
6291
+ if (declared.size === 1) {
6292
+ return declared.values().next().value
6293
+ } else {
6294
+ return null
6295
+ }
6296
+ }
6297
+
6298
+ // Assigning through element.style would reserialize the style attribute and
6299
+ // drop the mso-* declarations Word leaves inline (they aren't valid CSS),
6300
+ // and OfficeFormatter still needs to read mso-list from them. Append to the
6301
+ // attribute instead so the original declarations survive untouched.
6302
+ #appendStyleProperty(element, property, value) {
6303
+ const style = element.getAttribute("style");
6304
+ const declaration = `${property}:${value}`;
6305
+ if (style === null || style.trim() === "") {
6306
+ element.setAttribute("style", declaration);
6307
+ } else {
6308
+ element.setAttribute("style", `${style.replace(/;\s*$/, "")};${declaration}`);
6309
+ }
6310
+ }
6311
+
6194
6312
  #firstStrayListChild(list) {
6195
6313
  for (const child of list.childNodes) {
6196
6314
  if (child.nodeType !== Node.ELEMENT_NODE || child.tagName !== "LI") {
@@ -6207,6 +6325,16 @@ class PastedContentFormatter {
6207
6325
  #containsListItems(node) {
6208
6326
  return node.nodeType === Node.ELEMENT_NODE && node.querySelector("li") !== null
6209
6327
  }
6328
+
6329
+ // Emoji in gmail: <img data-emoji="😂" class="an1" alt="😂" aria-label="😂" draggable="false" src="https://fonts.gstatic.com/s/e/notoemoji/17.0/1f602/32.png" loading="lazy" style="height: 1.2em; width: 1.2em; vertical-align: middle;">
6330
+ #replaceGmailEmojiImgTags() {
6331
+ for (const emojiImg of this.doc.querySelectorAll("img[data-emoji]")) {
6332
+ const emoji = emojiImg.dataset.emoji;
6333
+ if (emoji === emojiImg.alt) {
6334
+ emojiImg.replaceWith(emoji);
6335
+ }
6336
+ }
6337
+ }
6210
6338
  }
6211
6339
 
6212
6340
  class Contents {
@@ -6241,7 +6369,7 @@ class Contents {
6241
6369
  insertText(text, { tag } = {}) {
6242
6370
  this.editor.update(() => {
6243
6371
  const paragraph = $createParagraphNode();
6244
- text.split("\n").forEach((line, index) => {
6372
+ text.split(/\r\n|\r|\n/).forEach((line, index) => {
6245
6373
  if (index > 0) paragraph.append($createLineBreakNode());
6246
6374
  paragraph.append($createTextNode(line));
6247
6375
  });
@@ -6249,20 +6377,6 @@ class Contents {
6249
6377
  }, { tag });
6250
6378
  }
6251
6379
 
6252
- // A literal "\n" stays visible in the editor, which Lexical renders with white-space: pre-wrap,
6253
- // but collapses to a space once serialized to HTML, so it has to become line break nodes.
6254
- insertTextWithLineBreaks(text) {
6255
- const normalizedText = text?.replace(/\r\n?/g, "\n");
6256
- const selection = $getSelection();
6257
-
6258
- if (normalizedText?.includes("\n") && $isRangeSelection(selection)) {
6259
- selection.insertRawText(normalizedText);
6260
- return true
6261
- } else {
6262
- return false
6263
- }
6264
- }
6265
-
6266
6380
  insertAtCursor(...nodes) {
6267
6381
  const selection = this.#insertableSelection();
6268
6382
  const inserter = BaseNodeInserter.for(selection);
@@ -6897,6 +7011,12 @@ class Contents {
6897
7011
  }
6898
7012
  }
6899
7013
 
7014
+ // Markdown reads any line indented by a tab or four spaces as a code block. Pasted
7015
+ // plain text is full of incidental indentation — outlines, notes, logs — and nobody
7016
+ // indents a note meaning "this is code"; they fence it. Keep fences, drop the
7017
+ // indentation rule.
7018
+ const pastedMarkdown = new Marked({ breaks: true }).use({ tokenizer: { code: () => undefined } });
7019
+
6900
7020
  class Clipboard {
6901
7021
  #listeners = new ListenerBin()
6902
7022
 
@@ -7083,7 +7203,7 @@ class Clipboard {
7083
7203
  }
7084
7204
 
7085
7205
  #pasteMarkdown(text) {
7086
- const html = marked(text, { breaks: true });
7206
+ const html = pastedMarkdown.parse(text);
7087
7207
  const doc = parseHtml(html);
7088
7208
 
7089
7209
  if (this.#isPlainTextWithoutMarkdown(doc)) {
@@ -8908,6 +9028,39 @@ class CustomAttachmentDragAndDropExtension extends LexxyExtension {
8908
9028
  }
8909
9029
  }
8910
9030
 
9031
+ const LINE_SEPARATORS = /\r\n|[\n\r\u2028\u2029]/;
9032
+
9033
+ // A literal line separator inside a text node renders as a break in the editor, or not at all,
9034
+ // but never survives serialization to HTML. macOS text replacements deliver U+2028 LINE SEPARATOR,
9035
+ // and other insertions can carry "\n", so however the text arrives, a transform converts whatever
9036
+ // lands in a text node into line break nodes.
9037
+ class LineSeparatorsExtension extends LexxyExtension {
9038
+ get lexicalExtension() {
9039
+ return this.defineExtension({
9040
+ name: "lexxy/line-separators",
9041
+ register(editor) {
9042
+ return editor.registerNodeTransform(TextNode, $replaceLineSeparatorWithLineBreak)
9043
+ }
9044
+ })
9045
+ }
9046
+ }
9047
+
9048
+ // Replaces the first separator only: the split leaves dirty text nodes behind, so Lexical re-runs
9049
+ // the transform until none remain.
9050
+ function $replaceLineSeparatorWithLineBreak(textNode) {
9051
+ const match = textNode.getTextContent().match(LINE_SEPARATORS);
9052
+
9053
+ if (match) {
9054
+ let separatorNode;
9055
+ if (match.index === 0) {
9056
+ [ separatorNode ] = textNode.splitText(match[0].length);
9057
+ } else {
9058
+ [ , separatorNode ] = textNode.splitText(match.index, match.index + match[0].length);
9059
+ }
9060
+ separatorNode.replace($createLineBreakNode());
9061
+ }
9062
+ }
9063
+
8911
9064
  class LexicalEditorElement extends HTMLElement {
8912
9065
  static formAssociated = true
8913
9066
  static debug = false
@@ -9066,7 +9219,8 @@ class LexicalEditorElement extends HTMLElement {
9066
9219
  FormatEscapeExtension,
9067
9220
  LinkOpenerExtension,
9068
9221
  PreventLexicalTripleClickExtension,
9069
- CustomAttachmentDragAndDropExtension
9222
+ CustomAttachmentDragAndDropExtension,
9223
+ LineSeparatorsExtension
9070
9224
  ]
9071
9225
  }
9072
9226
 
@@ -9254,7 +9408,6 @@ class LexicalEditorElement extends HTMLElement {
9254
9408
  #initialize() {
9255
9409
  this.#registerComponents();
9256
9410
  this.#handleEnter();
9257
- this.#handleReplacementText();
9258
9411
  this.#registerFocusEvents();
9259
9412
  this.#registerHistoryEvents();
9260
9413
  this.#registerFileAcceptFilter();
@@ -9518,14 +9671,6 @@ class LexicalEditorElement extends HTMLElement {
9518
9671
  ));
9519
9672
  }
9520
9673
 
9521
- #handleReplacementText() {
9522
- this.#listeners.track(this.editor.registerCommand(
9523
- CONTROLLED_TEXT_INSERTION_COMMAND,
9524
- (event) => event instanceof InputEvent && this.contents.insertTextWithLineBreaks(event.data),
9525
- COMMAND_PRIORITY_NORMAL
9526
- ));
9527
- }
9528
-
9529
9674
  #registerFocusEvents() {
9530
9675
  this.#listeners.track(
9531
9676
  registerEventListener(this, "focusin", this.#handleFocusIn),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.28",
3
+ "version": "0.9.30",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",