@37signals/lexxy 0.1.12-beta → 0.1.14-beta

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
@@ -87,7 +87,11 @@ For the CSS, you can include it with the standard Rails helper:
87
87
  <%= stylesheet_link_tag "lexxy" %>
88
88
  ```
89
89
 
90
- Of course, you can copy the CSS to your project and adapt it to your needs.
90
+ You can copy the CSS to your project and adapt it to your needs.
91
+
92
+ #### Custom styles and dark mode
93
+
94
+ All of Lexxy's color styles are defiend as CSS variables in `app/stylesheets/lexxy-variables.css`. This enables a straightforward way to customize Lexxy to match your application's theme. You can see an example implementation of a custom dark mode style in the Sandbox's stylesheet at `test/dummy/app/assets/stylesheets/sandbox.css`.
91
95
 
92
96
  #### Rendered Action Text content
93
97
 
@@ -104,6 +108,8 @@ To apply syntax highlighting to rendered Action Text content, you need to call t
104
108
  ```javascript
105
109
  import { Controller } from "@hotwired/stimulus"
106
110
  import { highlightAll } from "lexxy"
111
+ // Or if you installed via a javascript bundler:
112
+ // import { highlightAll } from "@37signals/lexxy"
107
113
 
108
114
  export default class extends Controller {
109
115
  connect() {
package/dist/lexxy.esm.js CHANGED
@@ -1,26 +1,26 @@
1
1
  import DOMPurify from 'dompurify';
2
- import { $getSelection, $isRangeSelection, $isTextNode, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, SELECTION_CHANGE_COMMAND, $createNodeSelection, $setSelection, $createParagraphNode, $createTextNode, $isParagraphNode, $insertNodes, $createLineBreakNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, SKIP_DOM_SELECTION_TAG, createEditor, KEY_ENTER_COMMAND, COMMAND_PRIORITY_NORMAL, COMMAND_PRIORITY_HIGH, KEY_TAB_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
2
+ import { $getSelection, $isRangeSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, $isNodeSelection, $getRoot, $isLineBreakNode, $isTextNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, SELECTION_CHANGE_COMMAND, $createNodeSelection, $setSelection, $createParagraphNode, $createTextNode, $insertNodes, $isParagraphNode, $createLineBreakNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, SKIP_DOM_SELECTION_TAG, createEditor, KEY_ENTER_COMMAND, COMMAND_PRIORITY_NORMAL, COMMAND_PRIORITY_HIGH, KEY_TAB_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
3
3
  import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListNode, ListItemNode, registerList } from '@lexical/list';
4
4
  import { $isQuoteNode, $isHeadingNode, $createQuoteNode, $createHeadingNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
5
- import { $isCodeNode, $isCodeHighlightNode, CodeNode, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP, normalizeCodeLang } from '@lexical/code';
5
+ import { $isCodeNode, CodeNode, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP, normalizeCodeLang } from '@lexical/code';
6
6
  import { $isLinkNode, $toggleLink, $createLinkNode, LinkNode, AutoLinkNode } from '@lexical/link';
7
7
  import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
8
8
  import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
9
- import { registerHistory, createEmptyHistoryState } from '@lexical/history';
9
+ import { createEmptyHistoryState, registerHistory } from '@lexical/history';
10
10
  import { DirectUpload } from '@rails/activestorage';
11
11
  import { marked } from 'marked';
12
12
  import 'prismjs/components/prism-ruby';
13
13
 
14
14
  DOMPurify.addHook("uponSanitizeElement", (node, data) => {
15
15
  if (data.tagName === "strong" || data.tagName === "em") {
16
- node.removeAttribute('class');
16
+ node.removeAttribute("class");
17
17
  }
18
18
  });
19
19
 
20
- const getNonce = () => {
20
+ function getNonce() {
21
21
  const element = document.head.querySelector("meta[name=csp-nonce]");
22
22
  return element?.content
23
- };
23
+ }
24
24
 
25
25
  function getNearestListItemNode(node) {
26
26
  let current = node;
@@ -47,7 +47,7 @@ function isPrintableCharacter(event) {
47
47
  if (event.ctrlKey || event.metaKey || event.altKey) return false
48
48
 
49
49
  // Ignore special keys
50
- if (event.key.length > 1 && event.key !== 'Enter' && event.key !== 'Space') return false
50
+ if (event.key.length > 1 && event.key !== "Enter" && event.key !== "Space") return false
51
51
 
52
52
  // Accept single character keys (letters, numbers, punctuation)
53
53
  return event.key.length === 1
@@ -61,12 +61,17 @@ class LexicalToolbarElement extends HTMLElement {
61
61
  }
62
62
 
63
63
  connectedCallback() {
64
- this.#refreshToolbarOverflow();
65
- window.addEventListener("resize", this.#refreshToolbarOverflow);
64
+ requestAnimationFrame(() => this.#refreshToolbarOverflow());
65
+
66
+ this._resizeObserver = new ResizeObserver(() => this.#refreshToolbarOverflow());
67
+ this._resizeObserver.observe(this);
66
68
  }
67
69
 
68
70
  disconnectedCallback() {
69
- window.removeEventListener("resize", this.#refreshToolbarOverflow);
71
+ if (this._resizeObserver) {
72
+ this._resizeObserver.disconnect();
73
+ this._resizeObserver = null;
74
+ }
70
75
  }
71
76
 
72
77
  setEditor(editorElement) {
@@ -76,6 +81,8 @@ class LexicalToolbarElement extends HTMLElement {
76
81
  this.#bindHotkeys();
77
82
  this.#assignButtonTabindex();
78
83
  this.#monitorSelectionChanges();
84
+ this.#monitorHistoryChanges();
85
+ this.#refreshToolbarOverflow();
79
86
  }
80
87
 
81
88
  #bindButtons() {
@@ -101,7 +108,7 @@ class LexicalToolbarElement extends HTMLElement {
101
108
 
102
109
  // Not using popover because of CSS anchoring still not widely available.
103
110
  #toggleDialog(button) {
104
- const dialog = document.getElementById(button.dataset.dialogTarget).parentNode;
111
+ const dialog = this.querySelector("lexxy-link-dialog .link-dialog").parentNode;
105
112
 
106
113
  if (dialog.open) {
107
114
  dialog.close();
@@ -111,7 +118,7 @@ class LexicalToolbarElement extends HTMLElement {
111
118
  }
112
119
 
113
120
  #bindHotkeys() {
114
- this.editorElement.addEventListener('keydown', (event) => {
121
+ this.editorElement.addEventListener("keydown", (event) => {
115
122
  const buttons = this.querySelectorAll("[data-hotkey]");
116
123
  buttons.forEach((button) => {
117
124
  const hotkeys = button.dataset.hotkey.toLowerCase().split(/\s+/);
@@ -127,13 +134,13 @@ class LexicalToolbarElement extends HTMLElement {
127
134
  #keyCombinationFor(event) {
128
135
  const pressedKey = event.key.toLowerCase();
129
136
  const modifiers = [
130
- event.ctrlKey ? 'ctrl' : null,
131
- event.metaKey ? 'cmd' : null,
132
- event.altKey ? 'alt' : null,
133
- event.shiftKey ? 'shift' : null,
137
+ event.ctrlKey ? "ctrl" : null,
138
+ event.metaKey ? "cmd" : null,
139
+ event.altKey ? "alt" : null,
140
+ event.shiftKey ? "shift" : null,
134
141
  ].filter(Boolean);
135
142
 
136
- return [ ...modifiers, pressedKey ].join('+')
143
+ return [ ...modifiers, pressedKey ].join("+")
137
144
  }
138
145
 
139
146
  #assignButtonTabindex() {
@@ -151,6 +158,30 @@ class LexicalToolbarElement extends HTMLElement {
151
158
  });
152
159
  }
153
160
 
161
+ #monitorHistoryChanges() {
162
+ this.editor.registerUpdateListener(() => {
163
+ this.#updateUndoRedoButtonStates();
164
+ });
165
+ }
166
+
167
+ #updateUndoRedoButtonStates() {
168
+ this.editor.getEditorState().read(() => {
169
+ const historyState = this.editorElement.historyState;
170
+ if (historyState) {
171
+ this.#setButtonDisabled("undo", historyState.undoStack.length === 0);
172
+ this.#setButtonDisabled("redo", historyState.redoStack.length === 0);
173
+ }
174
+ });
175
+ }
176
+
177
+ #setButtonDisabled(name, isDisabled) {
178
+ const button = this.querySelector(`[name="${name}"]`);
179
+ if (button) {
180
+ button.disabled = isDisabled;
181
+ button.setAttribute("aria-disabled", isDisabled.toString());
182
+ }
183
+ }
184
+
154
185
  #updateButtonStates() {
155
186
  const selection = $getSelection();
156
187
  if (!$isRangeSelection(selection)) return
@@ -162,6 +193,7 @@ class LexicalToolbarElement extends HTMLElement {
162
193
 
163
194
  const isBold = selection.hasFormat("bold");
164
195
  const isItalic = selection.hasFormat("italic");
196
+ const isStrikethrough = selection.hasFormat("strikethrough");
165
197
  const isInCode = $isCodeNode(topLevelElement) || selection.hasFormat("code");
166
198
  const isInList = this.#isInList(anchorNode);
167
199
  const listType = getListType(anchorNode);
@@ -171,25 +203,15 @@ class LexicalToolbarElement extends HTMLElement {
171
203
 
172
204
  this.#setButtonPressed("bold", isBold);
173
205
  this.#setButtonPressed("italic", isItalic);
206
+ this.#setButtonPressed("strikethrough", isStrikethrough);
174
207
  this.#setButtonPressed("code", isInCode);
175
208
  this.#setButtonPressed("unordered-list", isInList && listType === "bullet");
176
209
  this.#setButtonPressed("ordered-list", isInList && listType === "number");
177
210
  this.#setButtonPressed("quote", isInQuote);
178
211
  this.#setButtonPressed("heading", isInHeading);
179
212
  this.#setButtonPressed("link", isInLink);
180
- }
181
213
 
182
- #isSelectionInInlineCode(selection) {
183
- const nodes = selection.getNodes();
184
- return nodes.some(node => {
185
- if ($isCodeHighlightNode(node)) return true
186
- // Check parent for text nodes inside code highlight
187
- if ($isTextNode(node)) {
188
- const parent = node.getParent();
189
- if (parent && $isCodeHighlightNode(parent)) return true
190
- }
191
- return false
192
- })
214
+ this.#updateUndoRedoButtonStates();
193
215
  }
194
216
 
195
217
  #isInList(node) {
@@ -249,17 +271,17 @@ class LexicalToolbarElement extends HTMLElement {
249
271
 
250
272
  for (const button of buttons) {
251
273
  if (this.#toolbarIsOverflowing()) {
252
- this.#overflowMenu.appendChild(button);
274
+ this.#overflowMenu.prepend(button);
253
275
  movedToOverflow = true;
254
276
  } else {
255
- if (movedToOverflow) this.#overflowMenu.appendChild(button);
277
+ if (movedToOverflow) this.#overflowMenu.prepend(button);
256
278
  break
257
279
  }
258
280
  }
259
281
  }
260
282
 
261
283
  get #buttons() {
262
- return Array.from(this.querySelectorAll(":scope > button"))
284
+ return Array.from(this.querySelectorAll(":scope > button, :scope > [role=separator]"))
263
285
  }
264
286
 
265
287
  static get defaultTemplate() {
@@ -272,12 +294,18 @@ class LexicalToolbarElement extends HTMLElement {
272
294
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.1 4h-1.5l-3.2 16h1.5l-.4 2h-7l.4-2h1.5l3.2-16h-1.5l.4-2h7l-.4 2z"/></svg>
273
295
  </button>
274
296
 
297
+ <button class="lexxy-editor__toolbar-button" type="button" name="strikethrough" data-command="strikethrough" title="Strikethrough">
298
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
299
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M4.70588 16.1591C4.81459 19.7901 7.48035 22 11.6668 22C15.9854 22 18.724 19.6296 18.724 15.8779C18.724 15.5007 18.6993 15.1427 18.6474 14.8066H14.3721C14.8637 15.2085 15.0799 15.7037 15.0799 16.3471C15.0799 17.7668 13.7532 18.7984 11.8113 18.7984C9.88053 18.7984 8.38582 17.7531 8.21659 16.1591H4.70588ZM5.23953 9.31962H9.88794C9.10723 8.88889 8.75888 8.33882 8.75888 7.57339C8.75888 6.13992 9.96576 5.18793 11.7631 5.18793C13.5852 5.18793 14.8761 6.1797 14.9959 7.81344H18.4102C18.3485 4.31824 15.8038 2 11.752 2C7.867 2 5.09129 4.35802 5.09129 7.92044C5.09129 8.41838 5.14071 8.88477 5.23953 9.31962ZM2.23529 10.6914C1.90767 10.6914 1.59347 10.8359 1.36181 11.0931C1.13015 11.3504 1 11.6993 1 12.0631C1 12.4269 1.13015 12.7758 1.36181 13.0331C1.59347 13.2903 1.90767 13.4348 2.23529 13.4348H20.7647C21.0923 13.4348 21.4065 13.2903 21.6382 13.0331C21.8699 12.7758 22 12.4269 22 12.0631C22 11.6993 21.8699 11.3504 21.6382 11.0931C21.4065 10.8359 21.0923 10.6914 20.7647 10.6914H2.23529Z"/>
300
+ </svg>
301
+ </button>
302
+
275
303
  <button class="lexxy-editor__toolbar-button" type="button" name="link" title="Link" data-dialog-target="link-dialog" data-hotkey="cmd+k ctrl+k">
276
304
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.111 9.546a1.5 1.5 0 012.121 0 5.5 5.5 0 010 7.778l-2.828 2.828a5.5 5.5 0 01-7.778 0 5.498 5.498 0 010-7.777l2.828-2.83a1.5 1.5 0 01.355-.262 6.52 6.52 0 00.351 3.799l-1.413 1.414a2.499 2.499 0 000 3.535 2.499 2.499 0 003.535 0l2.83-2.828a2.5 2.5 0 000-3.536 1.5 1.5 0 010-2.121z"/><path d="M12.111 3.89a5.5 5.5 0 117.778 7.777l-2.828 2.829a1.496 1.496 0 01-.355.262 6.522 6.522 0 00-.351-3.8l1.413-1.412a2.5 2.5 0 10-3.536-3.535l-2.828 2.828a2.5 2.5 0 000 3.536 1.5 1.5 0 01-2.122 2.12 5.5 5.5 0 010-7.777l2.83-2.829z"/></svg>
277
305
  </button>
278
306
 
279
307
  <lexxy-link-dialog class="lexxy-link-dialog">
280
- <dialog id="link-dialog" closedby="any">
308
+ <dialog class="link-dialog" closedby="any">
281
309
  <form method="dialog">
282
310
  <input type="url" placeholder="Enter a URL…" class="input" required>
283
311
  <div class="lexxy-dialog-actions">
@@ -312,6 +340,20 @@ class LexicalToolbarElement extends HTMLElement {
312
340
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M16 8a2 2 0 110 4 2 2 0 010-4z""/><path d="M22 2a1 1 0 011 1v18a1 1 0 01-1 1H2a1 1 0 01-1-1V3a1 1 0 011-1h20zM3 18.714L9 11l5.25 6.75L17 15l4 4V4H3v14.714z"/></svg>
313
341
  </button>
314
342
 
343
+ <button class="lexxy-editor__toolbar-button" type="button" name="divider" data-command="insertHorizontalDivider" title="Insert a divider">
344
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 12C0 11.4477 0.447715 11 1 11H23C23.5523 11 24 11.4477 24 12C24 12.5523 23.5523 13 23 13H1C0.447716 13 0 12.5523 0 12Z"/><path d="M4 5C4 3.89543 4.89543 3 6 3H18C19.1046 3 20 3.89543 20 5C20 6.10457 19.1046 7 18 7H6C4.89543 7 4 6.10457 4 5Z"/><path d="M4 19C4 17.8954 4.89543 17 6 17H18C19.1046 17 20 17.8954 20 19C20 20.1046 19.1046 21 18 21H6C4.89543 21 4 20.1046 4 19Z"/></svg>
345
+ </button>
346
+
347
+ <div class="lexxy-editor__toolbar-spacer" role="separator"></div>
348
+
349
+ <button class="lexxy-editor__toolbar-button" type="button" name="undo" data-command="undo" title="Undo" data-hotkey="cmd+z ctrl+z">
350
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M5.64648 8.26531C7.93911 6.56386 10.7827 5.77629 13.624 6.05535C16.4655 6.33452 19.1018 7.66079 21.0195 9.77605C22.5839 11.5016 23.5799 13.6516 23.8936 15.9352C24.0115 16.7939 23.2974 17.4997 22.4307 17.4997C21.5641 17.4997 20.8766 16.7915 20.7148 15.9401C20.4295 14.4379 19.7348 13.0321 18.6943 11.8844C17.3 10.3464 15.3835 9.38139 13.3174 9.17839C11.2514 8.97546 9.18359 9.54856 7.5166 10.7858C6.38259 11.6275 5.48981 12.7361 4.90723 13.9997H8.5C9.3283 13.9997 9.99979 14.6714 10 15.4997C10 16.3281 9.32843 16.9997 8.5 16.9997H1.5C0.671573 16.9997 0 16.3281 0 15.4997V8.49968C0.000213656 7.67144 0.671705 6.99968 1.5 6.99968C2.3283 6.99968 2.99979 7.67144 3 8.49968V11.0212C3.7166 9.9704 4.60793 9.03613 5.64648 8.26531Z"/></svg>
351
+ </button>
352
+
353
+ <button class="lexxy-editor__toolbar-button" type="button" name="redo" data-command="redo" title="Redo" data-hotkey="cmd+shift+z ctrl+shift+z ctrl+y">
354
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M18.2599 8.26531C15.9672 6.56386 13.1237 5.77629 10.2823 6.05535C7.4408 6.33452 4.80455 7.66079 2.88681 9.77605C1.32245 11.5016 0.326407 13.6516 0.0127834 15.9352C-0.105117 16.7939 0.608975 17.4997 1.47567 17.4997C2.34228 17.4997 3.02969 16.7915 3.19149 15.9401C3.47682 14.4379 4.17156 13.0321 5.212 11.8844C6.60637 10.3464 8.52287 9.38139 10.589 9.17839C12.655 8.97546 14.7227 9.54856 16.3897 10.7858C17.5237 11.6275 18.4165 12.7361 18.9991 13.9997H15.4063C14.578 13.9997 13.9066 14.6714 13.9063 15.4997C13.9063 16.3281 14.5779 16.9997 15.4063 16.9997H22.4063C23.2348 16.9997 23.9063 16.3281 23.9063 15.4997V8.49968C23.9061 7.67144 23.2346 6.99968 22.4063 6.99968C21.578 6.99968 20.9066 7.67144 20.9063 8.49968V11.0212C20.1897 9.9704 19.2984 9.03613 18.2599 8.26531Z"/></svg>
355
+ </button>
356
+
315
357
  <details class="lexxy-editor__toolbar-overflow">
316
358
  <summary class="lexxy-editor__toolbar-button" aria-label="Show more toolbar buttons">•••</summary>
317
359
  <div class="lexxy-editor__toolbar-overflow-menu" aria-label="More toolbar buttons"></div>
@@ -326,69 +368,62 @@ var theme = {
326
368
  text: {
327
369
  bold: "lexxy-content__bold",
328
370
  italic: "lexxy-content__italic",
371
+ strikethrough: "lexxy-content__strikethrough",
329
372
  underline: "lexxy-content__underline",
330
373
  },
331
374
  codeHighlight: {
332
- atrule: 'code-token__attr',
333
- attr: 'code-token__attr',
334
- 'attr-name': 'code-token__attr',
335
- 'attr-value': 'code-token__selector',
336
- boolean: 'code-token__property',
337
- bold: 'code-token__variable',
338
- builtin: 'code-token__selector',
339
- cdata: 'code-token__comment',
340
- char: 'code-token__selector',
341
- class: 'code-token__function',
342
- 'class-name': 'code-token__function',
343
- color: 'code-token__property',
344
- comment: 'code-token__comment',
345
- constant: 'code-token__property',
346
- coord: 'code-token__property',
347
- decorator: 'code-token__function',
348
- deleted: 'code-token__property',
349
- doctype: 'code-token__comment',
350
- entity: 'code-token__operator',
351
- function: 'code-token__function',
352
- hexcode: 'code-token__property',
353
- important: 'code-token__variable',
354
- inserted: 'code-token__selector',
355
- italic: 'code-token__comment',
356
- keyword: 'code-token__attr',
357
- namespace: 'code-token__variable',
358
- number: 'code-token__property',
359
- operator: 'code-token__operator',
360
- parameter: 'code-token__variable',
361
- prolog: 'code-token__comment',
362
- property: 'code-token__property',
363
- punctuation: 'code-token__punctuation',
364
- regex: 'code-token__variable',
365
- script: 'code-token__function',
366
- selector: 'code-token__selector',
367
- string: 'code-token__selector',
368
- style: 'code-token__function',
369
- symbol: 'code-token__property',
370
- tag: 'code-token__property',
371
- title: 'code-token__function',
372
- url: 'code-token__operator',
373
- variable: 'code-token__variable',
375
+ atrule: "code-token__attr",
376
+ attr: "code-token__attr",
377
+ "attr-name": "code-token__attr",
378
+ "attr-value": "code-token__selector",
379
+ boolean: "code-token__property",
380
+ bold: "code-token__variable",
381
+ builtin: "code-token__selector",
382
+ cdata: "code-token__comment",
383
+ char: "code-token__selector",
384
+ class: "code-token__function",
385
+ "class-name": "code-token__function",
386
+ color: "code-token__property",
387
+ comment: "code-token__comment",
388
+ constant: "code-token__property",
389
+ coord: "code-token__property",
390
+ decorator: "code-token__function",
391
+ deleted: "code-token__property",
392
+ doctype: "code-token__comment",
393
+ entity: "code-token__operator",
394
+ function: "code-token__function",
395
+ hexcode: "code-token__property",
396
+ important: "code-token__variable",
397
+ inserted: "code-token__selector",
398
+ italic: "code-token__comment",
399
+ keyword: "code-token__attr",
400
+ namespace: "code-token__variable",
401
+ number: "code-token__property",
402
+ operator: "code-token__operator",
403
+ parameter: "code-token__variable",
404
+ prolog: "code-token__comment",
405
+ property: "code-token__property",
406
+ punctuation: "code-token__punctuation",
407
+ regex: "code-token__variable",
408
+ script: "code-token__function",
409
+ selector: "code-token__selector",
410
+ string: "code-token__selector",
411
+ style: "code-token__function",
412
+ symbol: "code-token__property",
413
+ tag: "code-token__property",
414
+ title: "code-token__function",
415
+ url: "code-token__operator",
416
+ variable: "code-token__variable",
374
417
  }
375
418
  };
376
419
 
377
- function bytesToHumanSize(bytes) {
378
- if (bytes === 0) return "0 B"
379
- const sizes = [ "B", "KB", "MB", "GB", "TB", "PB" ];
380
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
381
- const value = bytes / Math.pow(1024, i);
382
- return `${ value.toFixed(2) } ${ sizes[i] }`
383
- }
384
-
385
420
  const VISUALLY_RELEVANT_ELEMENTS_SELECTOR = [
386
421
  "img", "video", "audio", "iframe", "embed", "object", "picture", "source", "canvas", "svg", "math",
387
- "form", "input", "textarea", "select", "button", "code", "blockquote"
422
+ "form", "input", "textarea", "select", "button", "code", "blockquote", "hr"
388
423
  ].join(",");
389
424
 
390
425
  const ALLOWED_HTML_TAGS = [ "a", "action-text-attachment", "b", "blockquote", "br", "code", "em",
391
- "figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "i", "img", "li", "ol", "p", "pre", "q", "strong", "ul" ];
426
+ "figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "ol", "p", "pre", "q", "s", "strong", "ul" ];
392
427
 
393
428
  const ALLOWED_HTML_ATTRIBUTES = [ "alt", "caption", "class", "content", "content-type", "contenteditable",
394
429
  "data-direct-upload-id", "data-sgid", "filename", "filesize", "height", "href", "presentation",
@@ -412,9 +447,9 @@ function parseHtml(html) {
412
447
  }
413
448
 
414
449
  function createAttachmentFigure(contentType, isPreviewable, fileName) {
415
- const extension = fileName ? fileName.split('.').pop().toLowerCase() : "unknown";
450
+ const extension = fileName ? fileName.split(".").pop().toLowerCase() : "unknown";
416
451
  return createElement("figure", {
417
- className: `attachment attachment--${isPreviewable ? 'preview' : 'file'} attachment--${extension}`,
452
+ className: `attachment attachment--${isPreviewable ? "preview" : "file"} attachment--${extension}`,
418
453
  "data-content-type": contentType
419
454
  })
420
455
  }
@@ -454,13 +489,21 @@ function generateDomId(prefix) {
454
489
  return `${prefix}-${randomPart}`
455
490
  }
456
491
 
492
+ function bytesToHumanSize(bytes) {
493
+ if (bytes === 0) return "0 B"
494
+ const sizes = [ "B", "KB", "MB", "GB", "TB", "PB" ];
495
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
496
+ const value = bytes / Math.pow(1024, i);
497
+ return `${ value.toFixed(2) } ${ sizes[i] }`
498
+ }
499
+
457
500
  class ActionTextAttachmentNode extends DecoratorNode {
458
501
  static getType() {
459
502
  return "action_text_attachment"
460
503
  }
461
504
 
462
505
  static clone(node) {
463
- return new ActionTextAttachmentNode({ ...node }, node.__key);
506
+ return new ActionTextAttachmentNode({ ...node }, node.__key)
464
507
  }
465
508
 
466
509
  static importJSON(serializedNode) {
@@ -501,6 +544,22 @@ class ActionTextAttachmentNode extends DecoratorNode {
501
544
  }),
502
545
  priority: 1
503
546
  }
547
+ },
548
+ "video": (video) => {
549
+ const videoSource = video.getAttribute("src") || video.querySelector("source")?.src;
550
+ const fileName = videoSource?.split("/")?.pop();
551
+ const contentType = video.querySelector("source")?.getAttribute("content-type") || "video/*";
552
+
553
+ return {
554
+ conversion: () => ({
555
+ node: new ActionTextAttachmentNode({
556
+ src: videoSource,
557
+ fileName: fileName,
558
+ contentType: contentType
559
+ })
560
+ }),
561
+ priority: 1
562
+ }
504
563
  }
505
564
  }
506
565
  }
@@ -514,7 +573,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
514
573
  this.altText = altText || "";
515
574
  this.caption = caption || "";
516
575
  this.contentType = contentType || "";
517
- this.fileName = fileName;
576
+ this.fileName = fileName || "";
518
577
  this.fileSize = fileSize;
519
578
  this.width = width;
520
579
  this.height = height;
@@ -598,7 +657,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
598
657
  }
599
658
 
600
659
  #createDOMForImage() {
601
- return createElement("img", { src: this.src, alt: this.altText, ...this.#imageDimensions})
660
+ return createElement("img", { src: this.src, alt: this.altText, ...this.#imageDimensions })
602
661
  }
603
662
 
604
663
  get #imageDimensions() {
@@ -610,18 +669,21 @@ class ActionTextAttachmentNode extends DecoratorNode {
610
669
  }
611
670
 
612
671
  #createDOMForFile() {
613
- const extension = this.fileName ? this.fileName.split('.').pop().toLowerCase() : 'unknown';
614
- return createElement("span", { className: "attachment__icon", textContent: `${extension}`})
672
+ const extension = this.fileName ? this.fileName.split(".").pop().toLowerCase() : "unknown";
673
+ return createElement("span", { className: "attachment__icon", textContent: `${extension}` })
615
674
  }
616
675
 
617
676
  #createDOMForNotImage() {
618
677
  const figcaption = createElement("figcaption", { className: "attachment__caption" });
619
678
 
620
679
  const nameTag = createElement("strong", { className: "attachment__name", textContent: this.caption || this.fileName });
621
- const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.fileSize) });
622
680
 
623
681
  figcaption.appendChild(nameTag);
624
- figcaption.appendChild(sizeSpan);
682
+
683
+ if (this.fileSize) {
684
+ const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.fileSize) });
685
+ figcaption.appendChild(sizeSpan);
686
+ }
625
687
 
626
688
  return figcaption
627
689
  }
@@ -648,9 +710,6 @@ class ActionTextAttachmentNode extends DecoratorNode {
648
710
  return caption
649
711
  }
650
712
 
651
- #updateCaption(input) {
652
- }
653
-
654
713
  #handleCaptionInputBlurred(event) {
655
714
  const input = event.target;
656
715
 
@@ -694,7 +753,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
694
753
  }
695
754
 
696
755
  static clone(node) {
697
- return new ActionTextAttachmentUploadNode({ ...node }, node.__key);
756
+ return new ActionTextAttachmentUploadNode({ ...node }, node.__key)
698
757
  }
699
758
 
700
759
  constructor({ file, uploadUrl, blobUrlTemplate, editor, progress }, key) {
@@ -747,7 +806,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
747
806
  }
748
807
 
749
808
  #getFileExtension() {
750
- return this.file.name.split('.').pop().toLowerCase()
809
+ return this.file.name.split(".").pop().toLowerCase()
751
810
  }
752
811
 
753
812
  #createCaption() {
@@ -777,7 +836,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
777
836
  directUploadWillStoreFileWithXHR: (request) => {
778
837
  request.upload.addEventListener("progress", (event) => {
779
838
  this.editor.update(() => {
780
- progressBar.value = Math.round((event.loaded / event.total) * 100);
839
+ progressBar.value = Math.round(event.loaded / event.total * 100);
781
840
  });
782
841
  });
783
842
  }
@@ -842,17 +901,89 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
842
901
  }
843
902
  }
844
903
 
904
+ class HorizontalDividerNode extends DecoratorNode {
905
+ static getType() {
906
+ return "horizontal_divider"
907
+ }
908
+
909
+ static clone(node) {
910
+ return new HorizontalDividerNode(node.__key)
911
+ }
912
+
913
+ static importJSON(serializedNode) {
914
+ return new HorizontalDividerNode()
915
+ }
916
+
917
+ static importDOM() {
918
+ return {
919
+ "hr": (hr) => {
920
+ return {
921
+ conversion: () => ({
922
+ node: new HorizontalDividerNode()
923
+ }),
924
+ priority: 1
925
+ }
926
+ }
927
+ }
928
+ }
929
+
930
+ constructor(key) {
931
+ super(key);
932
+ }
933
+
934
+ createDOM() {
935
+ const figure = createElement("figure", { className: "horizontal-divider" });
936
+ const hr = createElement("hr");
937
+
938
+ figure.addEventListener("click", (event) => {
939
+ dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
940
+ });
941
+
942
+ figure.appendChild(hr);
943
+
944
+ return figure
945
+ }
946
+
947
+ updateDOM() {
948
+ return true
949
+ }
950
+
951
+ isInline() {
952
+ return false
953
+ }
954
+
955
+ exportDOM() {
956
+ const hr = createElement("hr");
957
+ return { element: hr }
958
+ }
959
+
960
+ exportJSON() {
961
+ return {
962
+ type: "horizontal_divider",
963
+ version: 1
964
+ }
965
+ }
966
+
967
+ decorate() {
968
+ return null
969
+ }
970
+ }
971
+
845
972
  const COMMANDS = [
846
973
  "bold",
847
- "rotateHeadingFormat",
848
974
  "italic",
975
+ "strikethrough",
849
976
  "link",
850
977
  "unlink",
978
+ "rotateHeadingFormat",
851
979
  "insertUnorderedList",
852
980
  "insertOrderedList",
853
981
  "insertQuoteBlock",
854
982
  "insertCodeBlock",
855
- "uploadAttachments"
983
+ "insertHorizontalDivider",
984
+ "uploadAttachments",
985
+ "undo",
986
+ "redo"
856
987
  ];
857
988
 
858
989
  class CommandDispatcher {
@@ -883,6 +1014,10 @@ class CommandDispatcher {
883
1014
  this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic");
884
1015
  }
885
1016
 
1017
+ dispatchStrikethrough() {
1018
+ this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough");
1019
+ }
1020
+
886
1021
  dispatchLink(url) {
887
1022
  this.#toggleLink(url);
888
1023
  }
@@ -893,7 +1028,7 @@ class CommandDispatcher {
893
1028
 
894
1029
  dispatchInsertUnorderedList() {
895
1030
  const selection = $getSelection();
896
- if (!selection) return;
1031
+ if (!selection) return
897
1032
 
898
1033
  const anchorNode = selection.anchor.getNode();
899
1034
 
@@ -906,7 +1041,7 @@ class CommandDispatcher {
906
1041
 
907
1042
  dispatchInsertOrderedList() {
908
1043
  const selection = $getSelection();
909
- if (!selection) return;
1044
+ if (!selection) return
910
1045
 
911
1046
  const anchorNode = selection.anchor.getNode();
912
1047
 
@@ -931,6 +1066,12 @@ class CommandDispatcher {
931
1066
  });
932
1067
  }
933
1068
 
1069
+ dispatchInsertHorizontalDivider() {
1070
+ this.editor.update(() => {
1071
+ this.contents.insertAtCursor(new HorizontalDividerNode());
1072
+ });
1073
+ }
1074
+
934
1075
  dispatchRotateHeadingFormat() {
935
1076
  this.editor.update(() => {
936
1077
  const selection = $getSelection();
@@ -978,6 +1119,14 @@ class CommandDispatcher {
978
1119
  setTimeout(() => input.remove(), 1000);
979
1120
  }
980
1121
 
1122
+ dispatchUndo() {
1123
+ this.editor.dispatchCommand(UNDO_COMMAND, undefined);
1124
+ }
1125
+
1126
+ dispatchRedo() {
1127
+ this.editor.dispatchCommand(REDO_COMMAND, undefined);
1128
+ }
1129
+
981
1130
  #registerCommands() {
982
1131
  for (const command of COMMANDS) {
983
1132
  const methodName = `dispatch${capitalize(command)}`;
@@ -1335,11 +1484,11 @@ class Selection {
1335
1484
  // above when navigating UP/DOWN when Lexical shows its fake cursor on custom decorator nodes.
1336
1485
  this.editorContentElement.addEventListener("keydown", (event) => {
1337
1486
  if (event.key === "ArrowUp") {
1338
- const lexicalCursor = this.editor.getRootElement().querySelector('[data-lexical-cursor]');
1487
+ const lexicalCursor = this.editor.getRootElement().querySelector("[data-lexical-cursor]");
1339
1488
 
1340
1489
  if (lexicalCursor) {
1341
1490
  let currentElement = lexicalCursor.previousElementSibling;
1342
- while (currentElement && currentElement.hasAttribute('data-lexical-cursor')) {
1491
+ while (currentElement && currentElement.hasAttribute("data-lexical-cursor")) {
1343
1492
  currentElement = currentElement.previousElementSibling;
1344
1493
  }
1345
1494
 
@@ -1350,11 +1499,11 @@ class Selection {
1350
1499
  }
1351
1500
 
1352
1501
  if (event.key === "ArrowDown") {
1353
- const lexicalCursor = this.editor.getRootElement().querySelector('[data-lexical-cursor]');
1502
+ const lexicalCursor = this.editor.getRootElement().querySelector("[data-lexical-cursor]");
1354
1503
 
1355
1504
  if (lexicalCursor) {
1356
1505
  let currentElement = lexicalCursor.nextElementSibling;
1357
- while (currentElement && currentElement.hasAttribute('data-lexical-cursor')) {
1506
+ while (currentElement && currentElement.hasAttribute("data-lexical-cursor")) {
1358
1507
  currentElement = currentElement.nextElementSibling;
1359
1508
  }
1360
1509
 
@@ -1542,7 +1691,7 @@ class Selection {
1542
1691
  }
1543
1692
 
1544
1693
  #isRectUnreliable(rect) {
1545
- return (rect.width === 0 && rect.height === 0) || (rect.top === 0 && rect.left === 0)
1694
+ return rect.width === 0 && rect.height === 0 || rect.top === 0 && rect.left === 0
1546
1695
  }
1547
1696
 
1548
1697
  #createAndInsertMarker(range) {
@@ -1573,7 +1722,7 @@ class Selection {
1573
1722
 
1574
1723
  #calculateCursorPosition(rect, range) {
1575
1724
  const rootRect = this.editor.getRootElement().getBoundingClientRect();
1576
- let x = rect.left - rootRect.left;
1725
+ const x = rect.left - rootRect.left;
1577
1726
  let y = rect.top - rootRect.top;
1578
1727
 
1579
1728
  const fontSize = this.#getFontSizeForCursor(range);
@@ -1788,6 +1937,23 @@ class Contents {
1788
1937
  });
1789
1938
  }
1790
1939
 
1940
+ insertAtCursor(node) {
1941
+ this.editor.update(() => {
1942
+ const selection = $getSelection();
1943
+ const selectedNodes = selection?.getNodes();
1944
+
1945
+ if ($isRangeSelection(selection)) {
1946
+ $insertNodes([ node ]);
1947
+ } else if ($isNodeSelection(selection) && selectedNodes && selectedNodes.length > 0) {
1948
+ const lastNode = selectedNodes[selectedNodes.length - 1];
1949
+ lastNode.insertAfter(node);
1950
+ } else {
1951
+ const root = $getRoot();
1952
+ root.append(node);
1953
+ }
1954
+ });
1955
+ }
1956
+
1791
1957
  insertNodeWrappingEachSelectedLine(newNodeFn) {
1792
1958
  this.editor.update(() => {
1793
1959
  const selection = $getSelection();
@@ -1859,22 +2025,6 @@ class Contents {
1859
2025
  return result
1860
2026
  }
1861
2027
 
1862
- hasSelectedWords() {
1863
- let result = false;
1864
-
1865
- this.editor.update(() => {
1866
- const selection = $getSelection();
1867
- if (!$isRangeSelection(selection)) return
1868
-
1869
- // Check if we have selected text within a line (not entire lines)
1870
- result = !selection.isCollapsed() &&
1871
- selection.anchor.getNode().getTopLevelElement() ===
1872
- selection.focus.getNode().getTopLevelElement();
1873
- });
1874
-
1875
- return result
1876
- }
1877
-
1878
2028
  unwrapSelectedListItems() {
1879
2029
  this.editor.update(() => {
1880
2030
  const selection = $getSelection();
@@ -1899,7 +2049,7 @@ class Contents {
1899
2049
 
1900
2050
  const selection = $getSelection();
1901
2051
  if ($isRangeSelection(selection)) {
1902
- selection.insertNodes([linkNode]);
2052
+ selection.insertNodes([ linkNode ]);
1903
2053
  linkNodeKey = linkNode.getKey();
1904
2054
  }
1905
2055
  });
@@ -1965,7 +2115,7 @@ class Contents {
1965
2115
  }
1966
2116
 
1967
2117
  replaceTextBackUntil(stringToReplace, replacementNodes) {
1968
- replacementNodes = Array.isArray(replacementNodes) ? replacementNodes : [replacementNodes];
2118
+ replacementNodes = Array.isArray(replacementNodes) ? replacementNodes : [ replacementNodes ];
1969
2119
 
1970
2120
  this.editor.update(() => {
1971
2121
  const { anchorNode, offset } = this.#getTextAnchorData();
@@ -2016,55 +2166,30 @@ class Contents {
2016
2166
  const blobUrlTemplate = this.editorElement.blobUrlTemplate;
2017
2167
 
2018
2168
  this.editor.update(() => {
2019
- const selection = $getSelection();
2020
- const anchorNode = selection?.anchor.getNode();
2021
- const currentParagraph = anchorNode?.getTopLevelElement();
2022
-
2023
2169
  const uploadedImageNode = new ActionTextAttachmentUploadNode({ file: file, uploadUrl: uploadUrl, blobUrlTemplate: blobUrlTemplate, editor: this.editor });
2024
-
2025
- if (currentParagraph && $isParagraphNode(currentParagraph) && currentParagraph.getChildrenSize() === 0) {
2026
- // If we're inside an empty paragraph, replace it
2027
- currentParagraph.replace(uploadedImageNode);
2028
- } else if (currentParagraph && $isElementNode(currentParagraph)) {
2029
- currentParagraph.insertAfter(uploadedImageNode);
2030
- } else {
2031
- $insertNodes([uploadedImageNode]);
2032
- }
2170
+ this.insertAtCursor(uploadedImageNode);
2033
2171
  }, { tag: HISTORY_MERGE_TAG });
2034
2172
  }
2035
2173
 
2036
- deleteSelectedNodes() {
2174
+ async deleteSelectedNodes() {
2175
+ let focusNode = null;
2176
+
2037
2177
  this.editor.update(() => {
2038
2178
  if ($isNodeSelection(this.#selection.current)) {
2039
2179
  const nodesToRemove = this.#selection.current.getNodes();
2040
2180
  if (nodesToRemove.length === 0) return
2041
2181
 
2042
- // Use splice() instead of node.remove() for proper removal and
2043
- // reconciliation. Would have issues with removing unintended decorator nodes
2044
- // with node.remove()
2045
- nodesToRemove.forEach((node) => {
2046
- const parent = node.getParent();
2047
- if (!$isElementNode(parent)) return
2048
-
2049
- const children = parent.getChildren();
2050
- const index = children.indexOf(node);
2051
-
2052
- if (index >= 0) {
2053
- parent.splice(index, 1, []);
2054
- }
2055
- });
2056
-
2057
- // Check if root is empty after all removals
2058
- const root = $getRoot();
2059
- if (root.getChildrenSize() === 0) {
2060
- root.append($createParagraphNode());
2061
- }
2182
+ focusNode = this.#findAdjacentNodeTo(nodesToRemove);
2183
+ this.#deleteNodes(nodesToRemove);
2184
+ }
2185
+ });
2062
2186
 
2063
- this.#selection.clear();
2064
- this.editor.focus();
2187
+ await nextFrame();
2065
2188
 
2066
- return true
2067
- }
2189
+ this.editor.update(() => {
2190
+ this.#selectAfterDeletion(focusNode);
2191
+ this.#selection.clear();
2192
+ this.editor.focus();
2068
2193
  });
2069
2194
  }
2070
2195
 
@@ -2122,7 +2247,7 @@ class Contents {
2122
2247
  wrappingNode.append(...topLevelElement.getChildren());
2123
2248
  topLevelElement.replace(wrappingNode);
2124
2249
  } else {
2125
- $insertNodes([newNodeFn()]);
2250
+ $insertNodes([ newNodeFn() ]);
2126
2251
  }
2127
2252
  }
2128
2253
 
@@ -2199,6 +2324,45 @@ class Contents {
2199
2324
  nodesToDelete.forEach((node) => node.remove());
2200
2325
  }
2201
2326
 
2327
+ #deleteNodes(nodes) {
2328
+ // Use splice() instead of node.remove() for proper removal and
2329
+ // reconciliation. Would have issues with removing unintended decorator nodes
2330
+ // with node.remove()
2331
+ nodes.forEach((node) => {
2332
+ const parent = node.getParent();
2333
+ if (!$isElementNode(parent)) return
2334
+
2335
+ const children = parent.getChildren();
2336
+ const index = children.indexOf(node);
2337
+
2338
+ if (index >= 0) {
2339
+ parent.splice(index, 1, []);
2340
+ }
2341
+ });
2342
+ }
2343
+
2344
+ #findAdjacentNodeTo(nodes) {
2345
+ const firstNode = nodes[0];
2346
+ const lastNode = nodes[nodes.length - 1];
2347
+
2348
+ return firstNode?.getPreviousSibling() || lastNode?.getNextSibling()
2349
+ }
2350
+
2351
+ #selectAfterDeletion(focusNode) {
2352
+ const root = $getRoot();
2353
+ if (root.getChildrenSize() === 0) {
2354
+ const newParagraph = $createParagraphNode();
2355
+ root.append(newParagraph);
2356
+ newParagraph.selectStart();
2357
+ } else if (focusNode) {
2358
+ if ($isTextNode(focusNode) || $isParagraphNode(focusNode)) {
2359
+ focusNode.selectEnd();
2360
+ } else {
2361
+ focusNode.selectNext(0, 0);
2362
+ }
2363
+ }
2364
+ }
2365
+
2202
2366
  #collectSelectedListItems(selection) {
2203
2367
  const nodes = selection.getNodes();
2204
2368
  const listItems = new Set();
@@ -2290,8 +2454,8 @@ class Contents {
2290
2454
  firstParagraph.selectStart();
2291
2455
  const currentSelection = $getSelection();
2292
2456
  if (currentSelection && $isRangeSelection(currentSelection)) {
2293
- currentSelection.anchor.set(firstParagraph.getKey(), 0, 'element');
2294
- currentSelection.focus.set(lastParagraph.getKey(), lastParagraph.getChildrenSize(), 'element');
2457
+ currentSelection.anchor.set(firstParagraph.getKey(), 0, "element");
2458
+ currentSelection.focus.set(lastParagraph.getKey(), lastParagraph.getChildrenSize(), "element");
2295
2459
  }
2296
2460
  }
2297
2461
 
@@ -2345,14 +2509,14 @@ class Contents {
2345
2509
  const last = children[children.length - 1];
2346
2510
  const beforeLast = children[children.length - 2];
2347
2511
 
2348
- if (($isTextNode(last) && last.getTextContent() === "") && (beforeLast && !$isTextNode(beforeLast))) {
2512
+ if ($isTextNode(last) && last.getTextContent() === "" && (beforeLast && !$isTextNode(beforeLast))) {
2349
2513
  paragraph.append($createLineBreakNode());
2350
2514
  }
2351
2515
  }
2352
2516
  }
2353
2517
 
2354
2518
  #createCustomAttachmentNodeWithHtml(html, options = {}) {
2355
- const attachmentConfig = typeof options === 'object' ? options : {};
2519
+ const attachmentConfig = typeof options === "object" ? options : {};
2356
2520
 
2357
2521
  return new CustomActionTextAttachmentNode({
2358
2522
  sgid: attachmentConfig.sgid || null,
@@ -2367,7 +2531,7 @@ class Contents {
2367
2531
  }
2368
2532
 
2369
2533
  #shouldUploadFile(file) {
2370
- return dispatch(this.editorElement, 'lexxy:file-accept', { file }, true)
2534
+ return dispatch(this.editorElement, "lexxy:file-accept", { file }, true)
2371
2535
  }
2372
2536
  }
2373
2537
 
@@ -2375,7 +2539,7 @@ function isUrl(string) {
2375
2539
  try {
2376
2540
  new URL(string);
2377
2541
  return true
2378
- } catch (_) {
2542
+ } catch {
2379
2543
  return false
2380
2544
  }
2381
2545
  }
@@ -2450,7 +2614,7 @@ class Clipboard {
2450
2614
  #handlePastedFiles(clipboardData) {
2451
2615
  if (!this.editorElement.supportsAttachments) return
2452
2616
 
2453
- const html = clipboardData.getData('text/html');
2617
+ const html = clipboardData.getData("text/html");
2454
2618
  if (html) return // Ignore if image copied from browser since we will load it as a remote image
2455
2619
 
2456
2620
  this.#preservingScrollPosition(() => {
@@ -2480,7 +2644,7 @@ class Clipboard {
2480
2644
  class LexicalEditorElement extends HTMLElement {
2481
2645
  static formAssociated = true
2482
2646
  static debug = true
2483
- static commands = [ "bold", "italic", "" ]
2647
+ static commands = [ "bold", "italic", "strikethrough" ]
2484
2648
 
2485
2649
  static observedAttributes = [ "connected", "required" ]
2486
2650
 
@@ -2642,6 +2806,7 @@ class LexicalEditorElement extends HTMLElement {
2642
2806
  CodeHighlightNode,
2643
2807
  LinkNode,
2644
2808
  AutoLinkNode,
2809
+ HorizontalDividerNode,
2645
2810
 
2646
2811
  CustomActionTextAttachmentNode,
2647
2812
  ];
@@ -2736,7 +2901,8 @@ class LexicalEditorElement extends HTMLElement {
2736
2901
 
2737
2902
  #registerComponents() {
2738
2903
  registerRichText(this.editor);
2739
- registerHistory(this.editor, createEmptyHistoryState(), 20);
2904
+ this.historyState = createEmptyHistoryState();
2905
+ registerHistory(this.editor, this.historyState, 20);
2740
2906
  registerList(this.editor);
2741
2907
  this.#registerCodeHiglightingComponents();
2742
2908
  registerMarkdownShortcuts(this.editor, TRANSFORMERS);
@@ -2813,6 +2979,7 @@ class LexicalEditorElement extends HTMLElement {
2813
2979
  #createDefaultToolbar() {
2814
2980
  const toolbar = createElement("lexxy-toolbar");
2815
2981
  toolbar.innerHTML = LexicalToolbarElement.defaultTemplate;
2982
+ toolbar.setAttribute("data-attachments", this.supportsAttachments); // Drives toolbar CSS styles
2816
2983
  this.prepend(toolbar);
2817
2984
  return toolbar
2818
2985
  }
@@ -3366,7 +3533,7 @@ class LexicalPromptElement extends HTMLElement {
3366
3533
  popoverContainer.classList.add("lexxy-prompt-menu");
3367
3534
  popoverContainer.style.position = "absolute";
3368
3535
  popoverContainer.setAttribute("nonce", getNonce());
3369
- popoverContainer.append(...(await this.source.buildListItems()));
3536
+ popoverContainer.append(...await this.source.buildListItems());
3370
3537
  popoverContainer.addEventListener("click", this.#handlePopoverClick);
3371
3538
  this.#editorElement.appendChild(popoverContainer);
3372
3539
  return popoverContainer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.1.12-beta",
3
+ "version": "0.1.14-beta",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",
@@ -12,8 +12,10 @@
12
12
  "author": "Jorge Manrubia <jorge@37signals.com>",
13
13
  "license": "MIT",
14
14
  "devDependencies": {
15
+ "@eslint/js": "^9.15.0",
15
16
  "@rollup/plugin-node-resolve": "^16.0.1",
16
17
  "@rollup/plugin-terser": "^0.4.4",
18
+ "eslint": "^9.15.0",
17
19
  "rollup": "^4.44.1",
18
20
  "rollup-plugin-gzip": "^4.1.1"
19
21
  },
@@ -21,6 +23,7 @@
21
23
  "build": "rollup -c",
22
24
  "build:npm": "rollup -c rollup.config.npm.mjs",
23
25
  "watch": "rollup -wc",
26
+ "lint": "eslint",
24
27
  "prerelease": "yarn build:npm",
25
28
  "release": "yarn build:npm && yarn publish"
26
29
  },