@37signals/lexxy 0.9.21 → 0.9.23

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,7 +2,7 @@
2
2
 
3
3
  A modern rich text editor for Rails.
4
4
 
5
- **[Try it out!](https://basecamp.github.io/lexxy/try-it)**
5
+ [Documentation](https://basecamp.github.io/lexxy/docs/) | <mark>[Try it out!](https://basecamp.github.io/lexxy/sandbox/)</mark>
6
6
 
7
7
  ## Features
8
8
 
@@ -15,24 +15,7 @@ A modern rich text editor for Rails.
15
15
  - Preview attachments like PDFs and Videos in the editor.
16
16
  - Works seamlessly with Action Text, generating the same canonical HTML format it expects for attachments.
17
17
 
18
- ![Lexxy editor screenshot](docs/images/home.screenshot.png)
19
-
20
- ## Documentation
21
-
22
- Visit the **[documentation site](https://basecamp.github.io/lexxy)**.
23
-
24
- ## Roadmap
25
-
26
- This is a beta. Here's what's coming next:
27
-
28
- - [x] Configurable editors in Action Text: Choose your editor like you choose your database.
29
- - [x] More editing features:
30
- - [x] Tables
31
- - [x] Text highlighting
32
- - [x] Configuration hooks.
33
- - [x] Standalone JS package: to use in non-Rails environments.
34
- - [x] Image galleries: The only remaining feature for full Action Text compatibility
35
- - [ ] Install task that generates the necessary JS and adds stylesheets.
18
+ ![Lexxy editor screenshot](home/docs/images/home.screenshot.png)
36
19
 
37
20
  ## Contributing
38
21
 
package/dist/lexxy.esm.js CHANGED
@@ -1,7 +1,9 @@
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, $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, 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, 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, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, 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, 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, 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, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, 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, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, INPUT_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
5
+ import * as lexical from 'lexical';
6
+ export { lexical as Lexical };
5
7
  import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
6
8
  import { buildEditorFromExtensions } from '@lexical/extension';
7
9
  import { ListNode, ListItemNode, $getListDepth, $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $createListNode, $createListItemNode, registerList } from '@lexical/list';
@@ -10,7 +12,7 @@ import { registerPlainText } from '@lexical/plain-text';
10
12
  import { RichTextExtension, $isQuoteNode, QuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
11
13
  import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
12
14
  import { HistoryExtension } from '@lexical/history';
13
- import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, normalizeCodeLang, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
15
+ import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, registerCodeHighlighting, normalizeCodeLang, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
14
16
  import { TRANSFORMERS, registerMarkdownShortcuts } from '@lexical/markdown';
15
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';
16
18
  import { marked } from 'marked';
@@ -1509,8 +1511,11 @@ class LexxyExtension {
1509
1511
  return []
1510
1512
  }
1511
1513
 
1512
- initializeToolbar(_lexxyToolbar) {
1514
+ defineExtension(...args) {
1515
+ return defineExtension(...args)
1516
+ }
1513
1517
 
1518
+ initializeToolbar(_lexxyToolbar) {
1514
1519
  }
1515
1520
 
1516
1521
  setEditorValidity(flags, message) {
@@ -5994,16 +5999,34 @@ class Contents {
5994
5999
  return result
5995
6000
  }
5996
6001
 
5997
- // The query runs from the trigger up to the next whitespace, even when the
6002
+ // The query runs from the trigger up to the next boundary, even when the
5998
6003
  // cursor sits inside an existing word — inserting "@" before "Jack" must
5999
6004
  // filter by "Jack" rather than treating the prompt as empty.
6000
6005
  #endOffsetAt(fullText, cursorOffset) {
6001
- const whitespaceOffset = fullText.slice(cursorOffset).search(/\s/);
6002
- if (whitespaceOffset === -1) {
6003
- return fullText.length
6004
- } else {
6005
- return cursorOffset + whitespaceOffset
6006
+ for (let offset = cursorOffset; offset < fullText.length; offset++) {
6007
+ if (this.#isQueryBoundary(fullText, offset)) {
6008
+ return offset
6009
+ }
6006
6010
  }
6011
+
6012
+ return fullText.length
6013
+ }
6014
+
6015
+ // Whitespace always ends the query. Punctuation ends it too: with "@" typed
6016
+ // right before a period, extending across it would glue "." onto everything
6017
+ // typed ("B" would query "B."), matching the wrong names or nothing at all.
6018
+ // But punctuation joining two word characters is part of a name — "@" before
6019
+ // "Anne-Marie" or "O'Connor" must keep the whole name as the query.
6020
+ #isQueryBoundary(fullText, offset) {
6021
+ const character = fullText[offset];
6022
+ if (/\s/.test(character)) return true
6023
+ if (!/\p{P}/u.test(character)) return false
6024
+
6025
+ return !(this.#isWordCharacter(fullText[offset - 1]) && this.#isWordCharacter(fullText[offset + 1]))
6026
+ }
6027
+
6028
+ #isWordCharacter(character) {
6029
+ return character != null && /[\p{L}\p{N}]/u.test(character)
6007
6030
  }
6008
6031
 
6009
6032
  containsTextBackUntil(string) {
@@ -7071,6 +7094,26 @@ function $getAllProvisionalParagraphs(rootNode = $getRoot()) {
7071
7094
  return $descendantsMatching(rootNode.getChildren(), $isProvisionalParagraphNode)
7072
7095
  }
7073
7096
 
7097
+ // Registers code highlighting through the extension system so its node
7098
+ // transforms exist before the initial editor state is applied. Registering
7099
+ // after editor creation misses code blocks loaded from the initial value:
7100
+ // their transform pass has already run, and Lexical's catch-up dirtying
7101
+ // only sees the committed (still empty) state.
7102
+ class CodeHighlightingExtension extends LexxyExtension {
7103
+ get enabled() {
7104
+ return this.editorElement.supportsRichText
7105
+ }
7106
+
7107
+ get lexicalExtension() {
7108
+ return defineExtension({
7109
+ name: "lexxy/code-highlighting",
7110
+ register(editor) {
7111
+ return registerCodeHighlighting(editor)
7112
+ }
7113
+ })
7114
+ }
7115
+ }
7116
+
7074
7117
  const TRIX_LANGUAGE_ATTR = "language";
7075
7118
 
7076
7119
  class TrixContentExtension extends LexxyExtension {
@@ -7202,9 +7245,13 @@ class TablesExtension extends LexxyExtension {
7202
7245
  }
7203
7246
  }),
7204
7247
 
7205
- // Bug fix: Prevent hardcoded background color (Lexical #8089)
7248
+ // A table in Lexxy is a Lexxy table: cell shading can't be set in the
7249
+ // editor, so any cell background only ever comes from foreign content
7250
+ // (pasted spreadsheets, loaded documents). Normalize every cell to no
7251
+ // background so it adopts the current theme. This also clears
7252
+ // Lexical's hardcoded default header background (Lexical #8089).
7206
7253
  editor.registerNodeTransform(TableCellNode, (node) => {
7207
- if (node.getBackgroundColor() === null) {
7254
+ if (node.getBackgroundColor() !== "") {
7208
7255
  node.setBackgroundColor("");
7209
7256
  }
7210
7257
  }),
@@ -8591,6 +8638,7 @@ class LexicalEditorElement extends HTMLElement {
8591
8638
  get baseExtensions() {
8592
8639
  return [
8593
8640
  ProvisionalParagraphExtension,
8641
+ CodeHighlightingExtension,
8594
8642
  HighlightExtension,
8595
8643
  TrixContentExtension,
8596
8644
  TablesExtension,
@@ -8998,7 +9046,7 @@ class LexicalEditorElement extends HTMLElement {
8998
9046
  registerList(this.editor)
8999
9047
  );
9000
9048
  this.#registerTableComponents();
9001
- this.#registerCodeHiglightingComponents();
9049
+ this.#registerCodeLanguagePicker();
9002
9050
  if (this.supportsMarkdown) {
9003
9051
  const transformers = [ ...TRANSFORMERS, HORIZONTAL_DIVIDER ];
9004
9052
  registered.push(
@@ -9020,8 +9068,7 @@ class LexicalEditorElement extends HTMLElement {
9020
9068
  this.#disposables.push(tableTools);
9021
9069
  }
9022
9070
 
9023
- #registerCodeHiglightingComponents() {
9024
- registerCodeHighlighting(this.editor);
9071
+ #registerCodeLanguagePicker() {
9025
9072
  let codeLanguagePicker = this.querySelector("lexxy-code-language-picker");
9026
9073
  codeLanguagePicker ??= createElement("lexxy-code-language-picker");
9027
9074
  this.append(codeLanguagePicker);
@@ -10652,6 +10699,7 @@ class TableTools extends HTMLElement {
10652
10699
  connectedCallback() {
10653
10700
  this.tableController = new TableController(this.#editorElement);
10654
10701
  this.classList.add("lexxy-floating-controls");
10702
+ this.role = "toolbar";
10655
10703
 
10656
10704
  this.#setUpButtons();
10657
10705
  this.#hide();
@@ -10782,16 +10830,28 @@ class TableTools extends HTMLElement {
10782
10830
  }
10783
10831
 
10784
10832
  #registerKeyboardShortcuts() {
10785
- this.#listeners.track(this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleAccessibilityShortcutKey, COMMAND_PRIORITY_HIGH));
10833
+ this.#listeners.track(this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#focusToolbarOnShortcut, COMMAND_PRIORITY_HIGH));
10786
10834
  }
10787
10835
 
10788
- #handleAccessibilityShortcutKey = (event) => {
10789
- if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === "F10") {
10790
- const firstButton = this.querySelector("button, [tabindex]:not([tabindex='-1'])");
10791
- firstButton?.focus();
10836
+ #focusToolbarOnShortcut = (event) => {
10837
+ if (this.#hasSelectedTable && this.#isFocusToolbarShortcut(event)) {
10838
+ event.preventDefault();
10839
+ this.#tableToolsButtons[0]?.focus();
10840
+ return true
10841
+ } else {
10842
+ return false
10792
10843
  }
10793
10844
  }
10794
10845
 
10846
+ get #hasSelectedTable() {
10847
+ return this.tableController?.currentTableNodeKey != null
10848
+ }
10849
+
10850
+ #isFocusToolbarShortcut(event) {
10851
+ if (event.key !== "F10") return false
10852
+ return event.altKey || ((event.ctrlKey || event.metaKey) && event.shiftKey)
10853
+ }
10854
+
10795
10855
  #handleToolsKeydown = (event) => {
10796
10856
  if (event.key === "Escape") {
10797
10857
  this.#handleEscapeKey();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",