@37signals/lexxy 0.9.13-beta → 0.9.15-alpha.1

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/README.md CHANGED
@@ -2,9 +2,6 @@
2
2
 
3
3
  A modern rich text editor for Rails.
4
4
 
5
- > [!IMPORTANT]
6
- > This is a beta. It hasn't been battle-tested yet. Please try it out and report any issues you find.
7
-
8
5
  **[Try it out!](https://basecamp.github.io/lexxy/try-it)**
9
6
 
10
7
  ## Features
package/dist/lexxy.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { highlightCode } 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, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isTextNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $getCommonAncestor, $findMatchingParent, TextNode, createCommand, defineExtension, 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, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $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, PASTE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, mergeRegister as mergeRegister$1, CLEAR_HISTORY_COMMAND, $onUpdate, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, 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, $getRoot, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isTextNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $getCommonAncestor, $findMatchingParent, TextNode, createCommand, defineExtension, 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, 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, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, ElementNode, $splitNode, $getChildCaretAtIndex, $createLineBreakNode, PASTE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, mergeRegister as mergeRegister$1, CLEAR_HISTORY_COMMAND, $onUpdate, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, INPUT_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
5
5
  import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
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';
@@ -619,14 +619,14 @@ class LexicalToolbarElement extends HTMLElement {
619
619
 
620
620
  this.#setButtonPressed("bold", isBold);
621
621
  this.#setButtonPressed("italic", isItalic);
622
+ this.#setButtonPressed("strikethrough", isStrikethrough);
623
+ this.#setButtonPressed("underline", isUnderline);
622
624
 
623
- this.#setButtonPressed("format", isInHeading || isStrikethrough || isUnderline);
625
+ this.#setButtonPressed("format", isInHeading);
624
626
  this.#setButtonPressed("paragraph", !isInHeading);
625
627
  this.#setButtonPressed("heading-large", headingTag === "h2");
626
628
  this.#setButtonPressed("heading-medium", headingTag === "h3");
627
629
  this.#setButtonPressed("heading-small", headingTag === "h4");
628
- this.#setButtonPressed("strikethrough", isStrikethrough);
629
- this.#setButtonPressed("underline", isUnderline);
630
630
 
631
631
  this.#setButtonPressed("lists", isInList);
632
632
  this.#setButtonPressed("unordered-list", isInList && listType === "bullet");
@@ -789,6 +789,14 @@ class LexicalToolbarElement extends HTMLElement {
789
789
  ${ToolbarIcons.italic}
790
790
  </button>
791
791
 
792
+ <button class="lexxy-editor__toolbar-button" type="button" name="strikethrough" data-command="strikethrough" title="Strikethrough">
793
+ ${ToolbarIcons.strikethrough}
794
+ </button>
795
+
796
+ <button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" type="button" name="underline" data-command="underline" title="Underline">
797
+ ${ToolbarIcons.underline}
798
+ </button>
799
+
792
800
  <lexxy-toolbar-dropdown class="lexxy-editor__toolbar-dropdown">
793
801
  <button data-dropdown-trigger class="lexxy-editor__toolbar-button lexxy-editor__toolbar-button--chevron" type="button" name="format" title="Text formatting" aria-haspopup="menu" aria-expanded="false">
794
802
  ${ToolbarIcons.heading}
@@ -807,13 +815,6 @@ class LexicalToolbarElement extends HTMLElement {
807
815
  ${ToolbarIcons.h4} <span>Small Heading</span>
808
816
  </button>
809
817
  <div class="lexxy-editor__toolbar-separator" role="separator"></div>
810
- <button type="button" name="strikethrough" data-command="strikethrough" title="Strikethrough" role="menuitem">
811
- ${ToolbarIcons.strikethrough} <span>Strikethrough</span>
812
- </button>
813
- <button type="button" name="underline" data-command="underline" title="Underline" role="menuitem">
814
- ${ToolbarIcons.underline} <span>Underline</span>
815
- </button>
816
- <div class="lexxy-editor__toolbar-separator" role="separator"></div>
817
818
  <button type="button" name="clear-formatting" data-command="clearFormatting" title="Clear formatting" role="menuitem">
818
819
  ${ToolbarIcons.clearFormatting} <span>Clear formatting</span>
819
820
  </button>
@@ -1526,14 +1527,15 @@ function $isShadowRoot(node) {
1526
1527
  return $isElementNode(node) && $isRootOrShadowRoot(node) && !$isRootNode(node)
1527
1528
  }
1528
1529
 
1530
+ function $isSafeForRoot(node) {
1531
+ return ($isElementNode(node) || $isDecoratorNode(node)) && !node.isParentRequired()
1532
+ }
1533
+
1529
1534
  function $makeSafeForRoot(node) {
1530
- if ($isTextNode(node)) {
1531
- return $wrapNodeInElement(node, $createParagraphNode)
1532
- } else if (node.isParentRequired()) {
1533
- const parent = node.createRequiredParent();
1534
- return $wrapNodeInElement(node, parent)
1535
- } else {
1535
+ if ($isSafeForRoot(node)) {
1536
1536
  return node
1537
+ } else {
1538
+ return $wrapNodeInElement(node, () => node.createParentElementNode())
1537
1539
  }
1538
1540
  }
1539
1541
 
@@ -1648,6 +1650,39 @@ function $isListItemStructurallyEmpty(listItem) {
1648
1650
  return true
1649
1651
  }
1650
1652
 
1653
+ // Returns the document text up to `offset` inside `targetNode`. Non-inline
1654
+ // element siblings are joined with `\n\n`, matching Lexical's own
1655
+ // ElementNode.getTextContent behavior.
1656
+ function $textBeforeOffset(targetNode, offset) {
1657
+ const parts = [];
1658
+ let done = false;
1659
+
1660
+ function visit(node) {
1661
+ if (done) return
1662
+ if (node === targetNode) {
1663
+ parts.push(node.getTextContent().slice(0, offset));
1664
+ done = true;
1665
+ return
1666
+ }
1667
+ if ($isElementNode(node)) {
1668
+ const children = node.getChildren();
1669
+ for (let i = 0; i < children.length; i++) {
1670
+ visit(children[i]);
1671
+ if (done) return
1672
+ const child = children[i];
1673
+ if ($isElementNode(child) && !child.isInline() && i < children.length - 1) {
1674
+ parts.push("\n\n");
1675
+ }
1676
+ }
1677
+ } else {
1678
+ parts.push(node.getTextContent());
1679
+ }
1680
+ }
1681
+
1682
+ visit($getRoot());
1683
+ return parts.join("")
1684
+ }
1685
+
1651
1686
  function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
1652
1687
  return $isTextNode(node)
1653
1688
  && node.getTextContent() === " "
@@ -8551,6 +8586,9 @@ class RemoteFilterSource extends BaseSource {
8551
8586
  const NOTHING_FOUND_DEFAULT_MESSAGE = "Nothing found";
8552
8587
  const FILTER_DEBOUNCE_INTERVAL = 50;
8553
8588
 
8589
+ // Start of line, or after a space or newline.
8590
+ const DEFAULT_ONLY_AT_PATTERN = "^|[ \\n]";
8591
+
8554
8592
  class LexicalPromptElement extends HTMLElement {
8555
8593
  #globalListeners = new ListenerBin()
8556
8594
  #popoverListeners = new ListenerBin()
@@ -8596,6 +8634,10 @@ class LexicalPromptElement extends HTMLElement {
8596
8634
  return this.hasAttribute("supports-space-in-searches")
8597
8635
  }
8598
8636
 
8637
+ get onlyAt() {
8638
+ return this.getAttribute("only-at")
8639
+ }
8640
+
8599
8641
  get open() {
8600
8642
  return this.popoverElement?.classList?.contains("lexxy-prompt-menu--visible")
8601
8643
  }
@@ -8639,14 +8681,10 @@ class LexicalPromptElement extends HTMLElement {
8639
8681
  if (offset >= triggerLength) {
8640
8682
  const textBeforeCursor = fullText.slice(offset - triggerLength, offset);
8641
8683
 
8642
- // Check if trigger is at the start of the text node (new line case) or preceded by space or newline
8643
8684
  if (textBeforeCursor === this.trigger) {
8644
- const isAtStart = offset === triggerLength;
8645
-
8646
- const charBeforeTrigger = offset > triggerLength ? fullText[offset - triggerLength - 1] : null;
8647
- const isPrecededBySpaceOrNewline = charBeforeTrigger === " " || charBeforeTrigger === "\n";
8685
+ const textBeforeTrigger = $textBeforeOffset(node, offset - triggerLength);
8648
8686
 
8649
- if (isAtStart || isPrecededBySpaceOrNewline) {
8687
+ if (this.#onlyAtRegExp.test(textBeforeTrigger)) {
8650
8688
  this.#popoverListeners.dispose();
8651
8689
  this.#showPopover();
8652
8690
  }
@@ -8657,7 +8695,15 @@ class LexicalPromptElement extends HTMLElement {
8657
8695
  }));
8658
8696
  }
8659
8697
 
8698
+ get #onlyAtRegExp() {
8699
+ return new RegExp(`(?:${this.onlyAt ?? DEFAULT_ONLY_AT_PATTERN})$`)
8700
+ }
8701
+
8660
8702
  get #promptContentTypePermitted() {
8703
+ // `insert-editable-text` prompts never create attachments, so the
8704
+ // editor's attachment support and content-type allowlist don't apply.
8705
+ if (this.hasAttribute("insert-editable-text")) return true
8706
+
8661
8707
  const el = this.#editorElement;
8662
8708
  if (!el.supportsAttachments) {
8663
8709
  return false
@@ -9360,6 +9406,8 @@ class TableController {
9360
9406
 
9361
9407
  this.currentCellKey = cellNode?.getKey() ?? null;
9362
9408
  this.currentTableNodeKey = tableNode?.getKey() ?? null;
9409
+
9410
+ return tableNode
9363
9411
  }
9364
9412
 
9365
9413
  executeTableCommand(command, customIndex = null) {
@@ -9862,9 +9910,8 @@ class TableTools extends HTMLElement {
9862
9910
 
9863
9911
  #monitorForTableSelection() {
9864
9912
  this.#listeners.track(this.#editor.registerUpdateListener(() => {
9865
- this.tableController.updateSelectedTable();
9913
+ const tableNode = this.#editor.getRootElement() && this.tableController.updateSelectedTable();
9866
9914
 
9867
- const tableNode = this.tableController.currentTableNode;
9868
9915
  if (tableNode) {
9869
9916
  this.#show();
9870
9917
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.13-beta",
3
+ "version": "0.9.15-alpha.1",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",