@37signals/lexxy 0.9.9-beta.preview4 → 0.9.9-beta.preview5

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.
package/dist/lexxy.esm.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { isActiveAndVisible, createElement, extractPlainTextFromHtml, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
2
2
  export { highlightCode } from './lexxy_helpers.esm.js';
3
3
  import DOMPurify from 'dompurify';
4
- import { getStyleObjectFromCSS, getCSSFromStyleObject, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $ensureForwardRangeSelection, $setBlocksType, $forEachSelectedTextNode } from '@lexical/selection';
5
- import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createParagraphNode, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, $createTextNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, COMMAND_PRIORITY_EDITOR, $getEditor, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $isNodeSelection, $getRoot, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, ElementNode, $splitNode, $getChildCaretAtIndex, $createLineBreakNode, $isParagraphNode, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, mergeRegister as mergeRegister$1, $findMatchingParent, CLEAR_HISTORY_COMMAND, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
4
+ import { getStyleObjectFromCSS, getCSSFromStyleObject, $ensureForwardRangeSelection, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType, $forEachSelectedTextNode } from '@lexical/selection';
5
+ import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createParagraphNode, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, $createTextNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, $isParagraphNode, $splitNode, $getSiblingCaret, LineBreakNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, COMMAND_PRIORITY_EDITOR, $getEditor, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $isNodeSelection, $getRoot, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, ElementNode, $getChildCaretAtIndex, $createLineBreakNode, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, mergeRegister as mergeRegister$1, $findMatchingParent, CLEAR_HISTORY_COMMAND, $onUpdate, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
6
6
  import { buildEditorFromExtensions } from '@lexical/extension';
7
7
  import { ListNode, ListItemNode, $getListDepth, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $isListItemNode, $isListNode, registerList } from '@lexical/list';
8
8
  import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
9
9
  import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, $descendantsMatching, IS_APPLE } from '@lexical/utils';
10
10
  import { registerPlainText } from '@lexical/plain-text';
11
- import { RichTextExtension, $isQuoteNode, $isHeadingNode, QuoteNode, $createHeadingNode, $createQuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
11
+ import { RichTextExtension, $isQuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
12
12
  import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
13
13
  import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, normalizeCodeLang, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
14
14
  import { TRANSFORMERS, registerMarkdownShortcuts } from '@lexical/markdown';
@@ -1460,6 +1460,40 @@ function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
1460
1460
  && previousNode instanceof CustomActionTextAttachmentNode
1461
1461
  }
1462
1462
 
1463
+ function $splitParagraphsAtLineBreakBoundaries(selection) {
1464
+ $ensureForwardRangeSelection(selection);
1465
+
1466
+ // Split focus first so the anchor split position stays valid.
1467
+ $splitAtNearestLineBreak(selection.focus, "next");
1468
+ $splitAtNearestLineBreak(selection.anchor, "previous");
1469
+ }
1470
+
1471
+ function $splitAtNearestLineBreak(point, direction) {
1472
+ const paragraph = point.getNode().getTopLevelElement();
1473
+ if (!paragraph || !$isParagraphNode(paragraph)) return
1474
+
1475
+ const pointNode = point.getNode();
1476
+ const selectionChild = pointNode.getParent().is(paragraph) ? pointNode : pointNode.getParentOrThrow();
1477
+ const lineBreakCaret = $caretAtNearestNodeOfType(selectionChild, LineBreakNode, direction);
1478
+ if (!lineBreakCaret) return
1479
+
1480
+ const lineBreak = lineBreakCaret.origin;
1481
+ const isEdge = lineBreakCaret.getNodeAtCaret() === null;
1482
+
1483
+ if (!isEdge) {
1484
+ $splitNode(paragraph, lineBreak.getIndexWithinParent());
1485
+ }
1486
+
1487
+ lineBreak.remove();
1488
+ }
1489
+
1490
+ function $caretAtNearestNodeOfType(node, klass, direction) {
1491
+ for (const caret of $getSiblingCaret(node, direction)) {
1492
+ if (caret.origin instanceof klass) return caret
1493
+ }
1494
+ return null
1495
+ }
1496
+
1463
1497
  // Shared, strictly-contained element used to attach ephemeral nodes when we
1464
1498
  // need to read computed styles (e.g. canonicalizing style values, resolving
1465
1499
  // CSS custom properties). The container is created once and attached to
@@ -4646,7 +4680,6 @@ class NodeInserter {
4646
4680
  static for(selection) {
4647
4681
  const INSERTERS = [
4648
4682
  CodeNodeInserter,
4649
- QuoteNodeInserter,
4650
4683
  ShadowRootNodeInserter,
4651
4684
  NodeSelectionNodeInserter
4652
4685
  ];
@@ -4695,25 +4728,6 @@ class CodeNodeInserter extends NodeInserter {
4695
4728
 
4696
4729
  }
4697
4730
 
4698
- // Lexical will split a QuoteNode when inserting other Elements - we want them simply inserted as-is
4699
- class QuoteNodeInserter extends NodeInserter {
4700
- static handles(selection) {
4701
- return $getNearestNodeOfType(selection.anchor?.getNode(), QuoteNode)
4702
- }
4703
-
4704
- insertNodes(nodes) {
4705
- if (!this.selection.isCollapsed()) { this.selection.removeText(); }
4706
-
4707
- $ensureForwardRangeSelection(this.selection);
4708
- let lastNode = this.selection.focus.getNode();
4709
- for (const node of nodes) {
4710
- lastNode = lastNode.insertAfter(node);
4711
- }
4712
-
4713
- lastNode.selectEnd();
4714
- }
4715
- }
4716
-
4717
4731
  class ShadowRootNodeInserter extends NodeInserter {
4718
4732
  static handles(selection) {
4719
4733
  return $isShadowRoot(selection?.anchor.getNode())
@@ -4861,7 +4875,7 @@ class Contents {
4861
4875
  } else {
4862
4876
  topLevelElements.filter($isQuoteNode).forEach(node => this.#unwrap(node));
4863
4877
 
4864
- this.#splitParagraphsAtLineBreaks(selection);
4878
+ $splitParagraphsAtLineBreakBoundaries(selection);
4865
4879
 
4866
4880
  const elements = this.#topLevelElementsInSelection(selection);
4867
4881
  if (elements.length === 0) return
@@ -5364,7 +5378,13 @@ class Clipboard {
5364
5378
  paste(event) {
5365
5379
  const clipboardData = event.clipboardData;
5366
5380
 
5367
- if (!clipboardData || this.#isPastingIntoCodeBlock()) return false
5381
+ if (!clipboardData) return false
5382
+
5383
+ if (this.#isPastingIntoCodeBlock()) {
5384
+ this.#pastePlainTextIntoCodeBlock(clipboardData);
5385
+ event.preventDefault();
5386
+ return true
5387
+ }
5368
5388
 
5369
5389
  if (this.#isPlainTextOrURLPasted(clipboardData)) {
5370
5390
  this.#pastePlainText(clipboardData);
@@ -5413,6 +5433,16 @@ class Clipboard {
5413
5433
  return result
5414
5434
  }
5415
5435
 
5436
+ #pastePlainTextIntoCodeBlock(clipboardData) {
5437
+ const text = clipboardData.getData("text/plain");
5438
+ if (!text) return
5439
+
5440
+ this.editor.update(() => {
5441
+ const selection = $getSelection();
5442
+ if ($isRangeSelection(selection)) selection.insertRawText(text);
5443
+ }, { tag: PASTE_TAG });
5444
+ }
5445
+
5416
5446
  #pastePlainText(clipboardData) {
5417
5447
  const item = clipboardData.items[0];
5418
5448
  item.getAsString((text) => {
@@ -6510,39 +6540,16 @@ class EarlyEscapeCodeNode extends CodeNode {
6510
6540
  }
6511
6541
 
6512
6542
  insertNewAfter(selection, restoreSelection) {
6513
- if (!selection.isCollapsed()) return super.insertNewAfter(selection, restoreSelection)
6514
-
6515
- // Clamp element-type selection offsets that may have been invalidated
6516
- // by the code retokenizer. The retokenizer's $updateAndRetainSelection
6517
- // restores the element offset verbatim after re-tokenizing, but when
6518
- // highlight splits changed the child count before retokenization, the
6519
- // restored offset can exceed the current child count. Without clamping,
6520
- // CodeNode.insertNewAfter passes the stale offset to splice(), which
6521
- // throws "start + deleteCount > oldSize".
6522
- this.#clampSelectionOffset(selection);
6523
-
6524
- if (this.#isCursorAtStart(selection)) {
6525
- this.insertBefore($createParagraphNode());
6526
- return null
6527
- }
6528
-
6529
- if (this.#isCursorOnEmptyLastLine(selection)) {
6530
- $trimTrailingBlankNodes(this);
6531
-
6532
- const paragraph = $createParagraphNode();
6533
- this.insertAfter(paragraph);
6534
- return paragraph
6535
- }
6536
-
6537
- return super.insertNewAfter(selection, restoreSelection)
6538
- }
6539
-
6540
- #clampSelectionOffset(selection) {
6541
- const childrenSize = this.getChildrenSize();
6542
- for (const point of [ selection.anchor, selection.focus ]) {
6543
- if (point.type === "element" && point.key === this.__key && point.offset > childrenSize) {
6544
- point.set(this.__key, childrenSize, "element");
6545
- }
6543
+ if ($hasUpdateTag(PASTE_TAG) || !selection.isCollapsed()) {
6544
+ return super.insertNewAfter(selection, restoreSelection)
6545
+ } else if (this.#isCursorAtStart(selection)) {
6546
+ return this.#insertParagraphBefore()
6547
+ } else if (this.#isCursorOnWhitespaceOnlyLastLine(selection)) {
6548
+ return this.#insertBlankLineBelow(selection, restoreSelection)
6549
+ } else if (this.#isCursorOnEmptyLastLine(selection)) {
6550
+ return this.#escapeToNewParagraphAfter()
6551
+ } else {
6552
+ return super.insertNewAfter(selection, restoreSelection)
6546
6553
  }
6547
6554
  }
6548
6555
 
@@ -6561,6 +6568,32 @@ class EarlyEscapeCodeNode extends CodeNode {
6561
6568
  return textContent === "" || textContent.endsWith("\n")
6562
6569
  }
6563
6570
 
6571
+ #isCursorOnWhitespaceOnlyLastLine(selection) {
6572
+ if (!$isCursorOnLastLine(selection)) return false
6573
+
6574
+ const textContent = this.getTextContent();
6575
+ const lastNewlineIndex = textContent.lastIndexOf("\n");
6576
+ const lastLine = lastNewlineIndex === -1 ? textContent : textContent.slice(lastNewlineIndex + 1);
6577
+ return lastLine.length > 0 && lastLine.trim() === ""
6578
+ }
6579
+
6580
+ #insertParagraphBefore() {
6581
+ this.insertBefore($createParagraphNode());
6582
+ return null
6583
+ }
6584
+
6585
+ #insertBlankLineBelow(selection, restoreSelection) {
6586
+ super.insertNewAfter(selection, restoreSelection);
6587
+ this.getLastChild().remove();
6588
+ return null
6589
+ }
6590
+
6591
+ #escapeToNewParagraphAfter() {
6592
+ $trimTrailingBlankNodes(this);
6593
+ const paragraph = $createParagraphNode();
6594
+ this.insertAfter(paragraph);
6595
+ return paragraph
6596
+ }
6564
6597
  }
6565
6598
 
6566
6599
  class EarlyEscapeListItemNode extends ListItemNode {
@@ -6965,7 +6998,7 @@ class LexicalEditorElement extends HTMLElement {
6965
6998
  }
6966
6999
 
6967
7000
  get #isContentFocused() {
6968
- return !!this.editorContentElement && this.editorContentElement.contains(document.activeElement)
7001
+ return !!this.editor && isEditorFocused(this.editor)
6969
7002
  }
6970
7003
 
6971
7004
  get value() {
@@ -6979,14 +7012,24 @@ class LexicalEditorElement extends HTMLElement {
6979
7012
  }
6980
7013
 
6981
7014
  set value(html) {
7015
+ const editorHasFocus = this.#isContentFocused;
7016
+
6982
7017
  this.editor.update(() => {
7018
+ if (editorHasFocus) {
7019
+ // Address Safari inconsistently placing the cursor in the contenteditable by forcing focus back onto the editor
7020
+ // Use direct `editor.focus` to bypass the pre-existing focus optimization and skip the callback
7021
+ $onUpdate(() => this.editor.focus());
7022
+ } else {
7023
+ $addUpdateTag(SKIP_DOM_SELECTION_TAG);
7024
+ }
7025
+
6983
7026
  $getRoot()
6984
7027
  .clear()
6985
7028
  .selectEnd()
6986
7029
  .insertNodes(this.#parseHtmlIntoLexicalNodes(html));
6987
7030
 
6988
7031
  this.#toggleEmptyStatus();
6989
- }, { discrete: true, tag: SKIP_DOM_SELECTION_TAG });
7032
+ }, { discrete: true });
6990
7033
  }
6991
7034
 
6992
7035
  get canUndo() {
@@ -114,7 +114,7 @@ function highlightElement(preElement) {
114
114
  applyHighlightRanges(codeElement, highlights);
115
115
  }
116
116
 
117
- preElement.replaceWith(codeElement);
117
+ preElement.replaceChildren(codeElement);
118
118
  }
119
119
 
120
120
  // Walk the DOM tree inside a <pre> element and build a list of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.9-beta.preview4",
3
+ "version": "0.9.9-beta.preview5",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",