@37signals/lexxy 0.1.25-beta → 0.1.27-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
@@ -23,7 +23,7 @@ A modern rich text editor for Rails.
23
23
  Add this line to your application's Gemfile:
24
24
 
25
25
  ```ruby
26
- gem 'lexxy', '~> 0.1.23.beta' # Need to specify the version since it's a pre-release
26
+ gem 'lexxy', '~> 0.1.26.beta' # Need to specify the version since it's a pre-release
27
27
  ```
28
28
 
29
29
  And then execute:
package/dist/lexxy.esm.js CHANGED
@@ -10,16 +10,17 @@ import 'prismjs/components/prism-json';
10
10
  import 'prismjs/components/prism-diff';
11
11
  import DOMPurify from 'dompurify';
12
12
  import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
13
- import { $isTextNode, TextNode, $isRangeSelection, $getSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $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, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, $insertNodes, $createLineBreakNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, SKIP_DOM_SELECTION_TAG, createEditor, BLUR_COMMAND, FOCUS_COMMAND, KEY_DOWN_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
14
- import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $createListNode, ListNode, ListItemNode, registerList } from '@lexical/list';
13
+ import { $isTextNode, TextNode, $isRangeSelection, SKIP_DOM_SELECTION_TAG, $getSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $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, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, $insertNodes, $createLineBreakNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, createEditor, BLUR_COMMAND, FOCUS_COMMAND, KEY_DOWN_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
14
+ import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListNode, $getListDepth, $createListNode, ListItemNode, registerList } from '@lexical/list';
15
15
  import { $isQuoteNode, $isHeadingNode, $createQuoteNode, $createHeadingNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
16
16
  import { $isCodeNode, CodeNode, normalizeCodeLang, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
17
17
  import { $isLinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, LinkNode, AutoLinkNode } from '@lexical/link';
18
- import { $getTableCellNodeFromLexicalNode, INSERT_TABLE_COMMAND, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, TableNode, TableCellNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $getElementForTableNode, $isTableCellNode, TableCellHeaderStates } from '@lexical/table';
18
+ import { $getTableCellNodeFromLexicalNode, INSERT_TABLE_COMMAND, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, TableCellNode, TableNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $getElementForTableNode, $isTableCellNode, TableCellHeaderStates } from '@lexical/table';
19
19
  import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
20
20
  import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
21
21
  import { createEmptyHistoryState, registerHistory } from '@lexical/history';
22
22
  import { DirectUpload } from '@rails/activestorage';
23
+ import { $getNearestNodeOfType } from '@lexical/utils';
23
24
  import { marked } from 'marked';
24
25
 
25
26
  // Configure Prism for manual highlighting mode
@@ -145,6 +146,93 @@ function hasHighlightStyles(cssOrStyles) {
145
146
  return !!(styles.color || styles["background-color"])
146
147
  }
147
148
 
149
+ function handleRollingTabIndex(elements, event) {
150
+ const previousActiveElement = document.activeElement;
151
+
152
+ if (elements.includes(previousActiveElement)) {
153
+ const finder = new NextElementFinder(elements, event.key);
154
+
155
+ if (finder.selectNext(previousActiveElement)) {
156
+ event.preventDefault();
157
+ }
158
+ }
159
+ }
160
+
161
+ class NextElementFinder {
162
+ constructor(elements, key) {
163
+ this.elements = elements;
164
+ this.key = key;
165
+ }
166
+
167
+ selectNext(fromElement) {
168
+ const nextElement = this.#findNextElement(fromElement);
169
+
170
+ if (nextElement) {
171
+ const inactiveElements = this.elements.filter(element => element !== nextElement);
172
+ this.#unsetTabIndex(inactiveElements);
173
+ this.#focusWithActiveTabIndex(nextElement);
174
+ return true
175
+ }
176
+
177
+ return false
178
+ }
179
+
180
+ #findNextElement(fromElement) {
181
+ switch (this.key) {
182
+ case "ArrowRight":
183
+ case "ArrowDown":
184
+ return this.#findNextSibling(fromElement)
185
+
186
+ case "ArrowLeft":
187
+ case "ArrowUp":
188
+ return this.#findPreviousSibling(fromElement)
189
+
190
+ case "Home":
191
+ return this.#findFirst()
192
+
193
+ case "End":
194
+ return this.#findLast()
195
+ }
196
+ }
197
+
198
+ #findFirst(elements = this.elements) {
199
+ return elements.find(isActiveAndVisible)
200
+ }
201
+
202
+ #findLast(elements = this.elements) {
203
+ return elements.findLast(isActiveAndVisible)
204
+ }
205
+
206
+ #findNextSibling(element) {
207
+ const afterElements = this.elements.slice(this.#indexOf(element) + 1);
208
+ return this.#findFirst(afterElements)
209
+ }
210
+
211
+ #findPreviousSibling(element) {
212
+ const beforeElements = this.elements.slice(0, this.#indexOf(element));
213
+ return this.#findLast(beforeElements)
214
+ }
215
+
216
+ #indexOf(element) {
217
+ return this.elements.indexOf(element)
218
+ }
219
+
220
+ #focusWithActiveTabIndex(element) {
221
+ if (isActiveAndVisible(element)) {
222
+ element.tabIndex = 0;
223
+ element.focus();
224
+ }
225
+ }
226
+
227
+ #unsetTabIndex(elements) {
228
+ elements.forEach(element => element.tabIndex = -1);
229
+ }
230
+ }
231
+
232
+ function isActiveAndVisible(element) {
233
+ return element && !element.disabled && element.checkVisibility()
234
+ }
235
+
148
236
  class LexicalToolbarElement extends HTMLElement {
149
237
  static observedAttributes = [ "connected" ]
150
238
 
@@ -156,17 +244,14 @@ class LexicalToolbarElement extends HTMLElement {
156
244
 
157
245
  connectedCallback() {
158
246
  requestAnimationFrame(() => this.#refreshToolbarOverflow());
159
-
160
- this._resizeObserver = new ResizeObserver(() => this.#refreshToolbarOverflow());
161
- this._resizeObserver.observe(this);
247
+ this.setAttribute("role", "toolbar");
248
+ this.#installResizeObserver();
162
249
  }
163
250
 
164
251
  disconnectedCallback() {
165
- if (this._resizeObserver) {
166
- this._resizeObserver.disconnect();
167
- this._resizeObserver = null;
168
- }
252
+ this.#uninstallResizeObserver();
169
253
  this.#unbindHotkeys();
254
+ this.#unbindFocusListeners();
170
255
  }
171
256
 
172
257
  attributeChangedCallback(name, oldValue, newValue) {
@@ -180,11 +265,12 @@ class LexicalToolbarElement extends HTMLElement {
180
265
  this.editor = editorElement.editor;
181
266
  this.#bindButtons();
182
267
  this.#bindHotkeys();
183
- this.#setTabIndexValues();
268
+ this.#resetTabIndexValues();
184
269
  this.#setItemPositionValues();
185
270
  this.#monitorSelectionChanges();
186
271
  this.#monitorHistoryChanges();
187
272
  this.#refreshToolbarOverflow();
273
+ this.#bindFocusListeners();
188
274
 
189
275
  this.toggleAttribute("connected", true);
190
276
  }
@@ -194,24 +280,39 @@ class LexicalToolbarElement extends HTMLElement {
194
280
  this.connectedCallback();
195
281
  }
196
282
 
283
+ #installResizeObserver() {
284
+ this.resizeObserver = new ResizeObserver(() => this.#refreshToolbarOverflow());
285
+ this.resizeObserver.observe(this);
286
+ }
287
+
288
+ #uninstallResizeObserver() {
289
+ if (this.resizeObserver) {
290
+ this.resizeObserver.disconnect();
291
+ this.resizeObserver = null;
292
+ }
293
+ }
294
+
197
295
  #bindButtons() {
198
296
  this.addEventListener("click", this.#handleButtonClicked.bind(this));
199
297
  }
200
298
 
201
- #handleButtonClicked({ target }) {
202
- this.#handleTargetClicked(target, "[data-command]", this.#dispatchButtonCommand.bind(this));
299
+ #handleButtonClicked(event) {
300
+ this.#handleTargetClicked(event, "[data-command]", this.#dispatchButtonCommand.bind(this));
203
301
  }
204
302
 
205
- #handleTargetClicked(target, selector, callback) {
206
- const button = target.closest(selector);
303
+ #handleTargetClicked(event, selector, callback) {
304
+ const button = event.target.closest(selector);
207
305
  if (button) {
208
- callback(button);
306
+ callback(event, button);
209
307
  }
210
308
  }
211
309
 
212
- #dispatchButtonCommand(button) {
213
- const { command, payload } = button.dataset;
214
- this.editor.dispatchCommand(command, payload);
310
+ #dispatchButtonCommand(event, { dataset: { command, payload } }) {
311
+ const isKeyboard = event instanceof PointerEvent && event.pointerId === -1;
312
+
313
+ this.editor.update(() => {
314
+ this.editor.dispatchCommand(command, payload);
315
+ }, { tag: isKeyboard ? SKIP_DOM_SELECTION_TAG : undefined } );
215
316
  }
216
317
 
217
318
  #bindHotkeys() {
@@ -246,9 +347,38 @@ class LexicalToolbarElement extends HTMLElement {
246
347
  return [ ...modifiers, pressedKey ].join("+")
247
348
  }
248
349
 
249
- #setTabIndexValues() {
250
- this.#buttons.forEach((button) => {
251
- button.setAttribute("tabindex", 0);
350
+ #bindFocusListeners() {
351
+ this.editorElement.addEventListener("lexxy:focus", this.#handleFocus);
352
+ this.editorElement.addEventListener("lexxy:blur", this.#handleFocusOut);
353
+ this.addEventListener("focusout", this.#handleFocusOut);
354
+ this.addEventListener("keydown", this.#handleKeydown);
355
+ }
356
+
357
+ #unbindFocusListeners() {
358
+ this.editorElement.removeEventListener("lexxy:focus", this.#handleFocus);
359
+ this.editorElement.removeEventListener("lexxy:blur", this.#handleFocusOut);
360
+ this.removeEventListener("focusout", this.#handleFocusOut);
361
+ this.removeEventListener("keydown", this.#handleKeydown);
362
+ }
363
+
364
+ #handleFocus = () => {
365
+ this.#resetTabIndexValues();
366
+ this.#focusableItems[0].tabIndex = 0;
367
+ }
368
+
369
+ #handleFocusOut = () => {
370
+ if (!this.contains(document.activeElement)) {
371
+ this.#resetTabIndexValues();
372
+ }
373
+ }
374
+
375
+ #handleKeydown = (event) => {
376
+ handleRollingTabIndex(this.#focusableItems, event);
377
+ }
378
+
379
+ #resetTabIndexValues() {
380
+ this.#focusableItems.forEach((button) => {
381
+ button.tabIndex = -1;
252
382
  });
253
383
  }
254
384
 
@@ -358,6 +488,7 @@ class LexicalToolbarElement extends HTMLElement {
358
488
 
359
489
  const isOverflowing = this.#overflowMenu.children.length > 0;
360
490
  this.toggleAttribute("overflowing", isOverflowing);
491
+ this.#overflowMenu.toggleAttribute("disabled", !isOverflowing);
361
492
  }
362
493
 
363
494
  #compactMenu() {
@@ -409,6 +540,10 @@ class LexicalToolbarElement extends HTMLElement {
409
540
  return Array.from(this.querySelectorAll(":scope > button"))
410
541
  }
411
542
 
543
+ get #focusableItems() {
544
+ return Array.from(this.querySelectorAll(":scope button, :scope > details > summary"))
545
+ }
546
+
412
547
  get #toolbarItems() {
413
548
  return Array.from(this.querySelectorAll(":scope > *:not(.lexxy-editor__toolbar-overflow)"))
414
549
  }
@@ -1386,7 +1521,7 @@ class CommandDispatcher {
1386
1521
  }
1387
1522
 
1388
1523
  #registerKeyboardCommands() {
1389
- this.editor.registerCommand(KEY_TAB_COMMAND, this.#handleListIndentation.bind(this), COMMAND_PRIORITY_NORMAL);
1524
+ this.editor.registerCommand(KEY_TAB_COMMAND, this.#handleTabKey.bind(this), COMMAND_PRIORITY_NORMAL);
1390
1525
  }
1391
1526
 
1392
1527
  #registerDragAndDropHandlers() {
@@ -1436,18 +1571,28 @@ class CommandDispatcher {
1436
1571
  this.editor.focus();
1437
1572
  }
1438
1573
 
1439
- #handleListIndentation(event) {
1574
+ #handleTabKey(event) {
1440
1575
  if (this.selection.isInsideList) {
1441
- event.preventDefault();
1442
- if (event.shiftKey) {
1443
- return this.editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined)
1444
- } else {
1445
- return this.editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)
1446
- }
1576
+ return this.#handleTabForList(event)
1577
+ } else if (this.selection.isInsideCodeBlock) {
1578
+ return this.#handleTabForCode()
1447
1579
  }
1448
1580
  return false
1449
1581
  }
1450
1582
 
1583
+ #handleTabForList(event) {
1584
+ if (event.shiftKey && !this.selection.isIndentedList) return false
1585
+
1586
+ event.preventDefault();
1587
+ const command = event.shiftKey? OUTDENT_CONTENT_COMMAND : INDENT_CONTENT_COMMAND;
1588
+ return this.editor.dispatchCommand(command)
1589
+ }
1590
+
1591
+ #handleTabForCode() {
1592
+ const selection = $getSelection();
1593
+ return $isRangeSelection(selection) && selection.isCollapsed()
1594
+ }
1595
+
1451
1596
  // Not using TOGGLE_LINK_COMMAND because it's not handled unless you use React/LinkPlugin
1452
1597
  #toggleLink(url) {
1453
1598
  this.editor.update(() => {
@@ -1500,26 +1645,19 @@ class Selection {
1500
1645
  this.#containEditorFocus();
1501
1646
  }
1502
1647
 
1503
- clear() {
1504
- this.current = null;
1505
- }
1506
-
1507
1648
  set current(selection) {
1508
- if ($isNodeSelection(selection)) {
1509
- this.editor.getEditorState().read(() => {
1510
- this._current = $getSelection();
1511
- this.#syncSelectedClasses();
1512
- });
1513
- } else {
1514
- this.editor.update(() => {
1515
- this.#syncSelectedClasses();
1516
- this._current = null;
1517
- });
1518
- }
1649
+ this.editor.update(() => {
1650
+ this.#syncSelectedClasses();
1651
+ });
1519
1652
  }
1520
1653
 
1521
- get current() {
1522
- return this._current
1654
+ get hasNodeSelection() {
1655
+ let result = false;
1656
+ this.editor.getEditorState().read(() => {
1657
+ const selection = $getSelection();
1658
+ result = selection !== null && $isNodeSelection(selection);
1659
+ });
1660
+ return result
1523
1661
  }
1524
1662
 
1525
1663
  get cursorPosition() {
@@ -1624,6 +1762,29 @@ class Selection {
1624
1762
  return getNearestListItemNode(anchorNode) !== null
1625
1763
  }
1626
1764
 
1765
+ get isIndentedList() {
1766
+ const selection = $getSelection();
1767
+ if (!$isRangeSelection(selection)) return false
1768
+
1769
+ const nodes = selection.getNodes();
1770
+ for (const node of nodes) {
1771
+ const closestListNode = $getNearestNodeOfType(node, ListNode);
1772
+ if (closestListNode && $getListDepth(closestListNode) > 1) {
1773
+ return true
1774
+ }
1775
+ }
1776
+
1777
+ return false
1778
+ }
1779
+
1780
+ get isInsideCodeBlock() {
1781
+ const selection = $getSelection();
1782
+ if (!$isRangeSelection(selection)) return false
1783
+
1784
+ const anchorNode = selection.anchor.getNode();
1785
+ return $getNearestNodeOfType(anchorNode, CodeNode) !== null
1786
+ }
1787
+
1627
1788
  get nodeAfterCursor() {
1628
1789
  const { anchorNode, offset } = this.#getCollapsedSelectionData();
1629
1790
  if (!anchorNode) return null
@@ -1689,18 +1850,18 @@ class Selection {
1689
1850
  }
1690
1851
 
1691
1852
  get #currentlySelectedKeys() {
1692
- if (this._currentlySelectedKeys) { return this._currentlySelectedKeys }
1853
+ if (this.currentlySelectedKeys) { return this.currentlySelectedKeys }
1693
1854
 
1694
- this._currentlySelectedKeys = new Set();
1855
+ this.currentlySelectedKeys = new Set();
1695
1856
 
1696
1857
  const selection = $getSelection();
1697
1858
  if (selection && $isNodeSelection(selection)) {
1698
1859
  for (const node of selection.getNodes()) {
1699
- this._currentlySelectedKeys.add(node.getKey());
1860
+ this.currentlySelectedKeys.add(node.getKey());
1700
1861
  }
1701
1862
  }
1702
1863
 
1703
- return this._currentlySelectedKeys
1864
+ return this.currentlySelectedKeys
1704
1865
  }
1705
1866
 
1706
1867
  #processSelectionChangeCommands() {
@@ -1830,7 +1991,7 @@ class Selection {
1830
1991
  this.#highlightNewItems();
1831
1992
 
1832
1993
  this.previouslySelectedKeys = this.#currentlySelectedKeys;
1833
- this._currentlySelectedKeys = null;
1994
+ this.currentlySelectedKeys = null;
1834
1995
  }
1835
1996
 
1836
1997
  #clearPreviouslyHighlightedItems() {
@@ -1852,7 +2013,7 @@ class Selection {
1852
2013
  }
1853
2014
 
1854
2015
  async #selectPreviousNode() {
1855
- if (this.current) {
2016
+ if (this.hasNodeSelection) {
1856
2017
  await this.#withCurrentNode((currentNode) => currentNode.selectPrevious());
1857
2018
  } else {
1858
2019
  this.#selectInLexical(this.nodeBeforeCursor);
@@ -1860,7 +2021,7 @@ class Selection {
1860
2021
  }
1861
2022
 
1862
2023
  async #selectNextNode() {
1863
- if (this.current) {
2024
+ if (this.hasNodeSelection) {
1864
2025
  await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0));
1865
2026
  } else {
1866
2027
  this.#selectInLexical(this.nodeAfterCursor);
@@ -1868,7 +2029,7 @@ class Selection {
1868
2029
  }
1869
2030
 
1870
2031
  async #selectPreviousTopLevelNode() {
1871
- if (this.current) {
2032
+ if (this.hasNodeSelection) {
1872
2033
  await this.#withCurrentNode((currentNode) => currentNode.selectPrevious());
1873
2034
  } else {
1874
2035
  this.#selectInLexical(this.topLevelNodeBeforeCursor);
@@ -1876,7 +2037,7 @@ class Selection {
1876
2037
  }
1877
2038
 
1878
2039
  async #selectNextTopLevelNode() {
1879
- if (this.current) {
2040
+ if (this.hasNodeSelection) {
1880
2041
  await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0));
1881
2042
  } else {
1882
2043
  this.#selectInLexical(this.topLevelNodeAfterCursor);
@@ -1885,10 +2046,9 @@ class Selection {
1885
2046
 
1886
2047
  async #withCurrentNode(fn) {
1887
2048
  await nextFrame();
1888
- if (this.current) {
2049
+ if (this.hasNodeSelection) {
1889
2050
  this.editor.update(() => {
1890
- this.clear();
1891
- fn(this.current.getNodes()[0]);
2051
+ fn($getSelection().getNodes()[0]);
1892
2052
  this.editor.focus();
1893
2053
  });
1894
2054
  }
@@ -2133,6 +2293,17 @@ class Selection {
2133
2293
  }
2134
2294
  }
2135
2295
 
2296
+ // Prevent the hardcoded background color
2297
+ // A background color value is set by Lexical if background is null:
2298
+ // https://github.com/facebook/lexical/blob/5bbbe849bd229e1db0e7b536e6a919520ada7bb2/packages/lexical-table/src/LexicalTableCellNode.ts#L187
2299
+ function registerHeaderBackgroundTransform(editor) {
2300
+ return editor.registerNodeTransform(TableCellNode, (node) => {
2301
+ if (node.getBackgroundColor() === null) {
2302
+ node.setBackgroundColor("");
2303
+ }
2304
+ })
2305
+ }
2306
+
2136
2307
  class CustomActionTextAttachmentNode extends DecoratorNode {
2137
2308
  static getType() {
2138
2309
  return "custom_action_text_attachment"
@@ -2781,8 +2952,8 @@ class Contents {
2781
2952
  let focusNode = null;
2782
2953
 
2783
2954
  this.editor.update(() => {
2784
- if ($isNodeSelection(this.#selection.current)) {
2785
- const nodesToRemove = this.#selection.current.getNodes();
2955
+ if (this.#selection.hasNodeSelection) {
2956
+ const nodesToRemove = $getSelection().getNodes();
2786
2957
  if (nodesToRemove.length === 0) return
2787
2958
 
2788
2959
  focusNode = this.#findAdjacentNodeTo(nodesToRemove);
@@ -2794,7 +2965,6 @@ class Contents {
2794
2965
 
2795
2966
  this.editor.update(() => {
2796
2967
  this.#selectAfterDeletion(focusNode);
2797
- this.#selection.clear();
2798
2968
  this.editor.focus();
2799
2969
  });
2800
2970
  }
@@ -3820,6 +3990,8 @@ class LexicalEditorElement extends HTMLElement {
3820
3990
  registerTablePlugin(this.editor);
3821
3991
  this.tableHandler = createElement("lexxy-table-handler");
3822
3992
  this.append(this.tableHandler);
3993
+
3994
+ this.#addUnregisterHandler(registerHeaderBackgroundTransform(this.editor));
3823
3995
  }
3824
3996
 
3825
3997
  #registerCodeHiglightingComponents() {
@@ -3971,8 +4143,6 @@ class ToolbarDropdown extends HTMLElement {
3971
4143
 
3972
4144
  this.container.addEventListener("toggle", this.#handleToggle.bind(this));
3973
4145
  this.container.addEventListener("keydown", this.#handleKeyDown.bind(this));
3974
-
3975
- this.#setTabIndexValues();
3976
4146
  }
3977
4147
 
3978
4148
  disconnectedCallback() {
@@ -4000,14 +4170,14 @@ class ToolbarDropdown extends HTMLElement {
4000
4170
  }
4001
4171
  }
4002
4172
 
4003
- #handleOpen(trigger) {
4004
- this.trigger = trigger;
4173
+ #handleOpen() {
4005
4174
  this.#interactiveElements[0].focus();
4006
4175
  this.#setupClickOutsideHandler();
4176
+
4177
+ this.#resetTabIndexValues();
4007
4178
  }
4008
4179
 
4009
4180
  #handleClose() {
4010
- this.trigger = null;
4011
4181
  this.#removeClickOutsideHandler();
4012
4182
  this.editor.focus();
4013
4183
  }
@@ -4037,16 +4207,20 @@ class ToolbarDropdown extends HTMLElement {
4037
4207
  }
4038
4208
  }
4039
4209
 
4040
- async #setTabIndexValues() {
4210
+ async #resetTabIndexValues() {
4041
4211
  await nextFrame();
4042
- this.#interactiveElements.forEach((element) => {
4043
- element.setAttribute("tabindex", 0);
4212
+ this.#buttons.forEach((element, index) => {
4213
+ element.setAttribute("tabindex", index === 0 ? 0 : "-1");
4044
4214
  });
4045
4215
  }
4046
4216
 
4047
4217
  get #interactiveElements() {
4048
4218
  return Array.from(this.querySelectorAll("button, input"))
4049
4219
  }
4220
+
4221
+ get #buttons() {
4222
+ return Array.from(this.querySelectorAll("button"))
4223
+ }
4050
4224
  }
4051
4225
 
4052
4226
  class LinkDropdown extends ToolbarDropdown {
@@ -4242,15 +4416,19 @@ class TableHandler extends HTMLElement {
4242
4416
  return $getTableColumnIndexFromTableCellNode(currentCell)
4243
4417
  }
4244
4418
 
4419
+ get #tableHandlerButtons() {
4420
+ return Array.from(this.buttonsContainer.querySelectorAll("button, details > summary"))
4421
+ }
4422
+
4245
4423
  #registerKeyboardShortcuts() {
4246
- this.unregisterKeyboardShortcuts = this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleKeyDown.bind(this), COMMAND_PRIORITY_HIGH);
4424
+ this.unregisterKeyboardShortcuts = this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleKeyDown, COMMAND_PRIORITY_HIGH);
4247
4425
  }
4248
4426
 
4249
4427
  #unregisterKeyboardShortcuts() {
4250
4428
  this.unregisterKeyboardShortcuts();
4251
4429
  }
4252
4430
 
4253
- #handleKeyDown(event) {
4431
+ #handleKeyDown = (event) => {
4254
4432
  if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === "F10") {
4255
4433
  const firstButton = this.buttonsContainer?.querySelector("button, [tabindex]:not([tabindex='-1'])");
4256
4434
  this.#setFocusStateOnSelectedCell();
@@ -4268,6 +4446,14 @@ class TableHandler extends HTMLElement {
4268
4446
  }
4269
4447
  }
4270
4448
 
4449
+ #handleTableHandlerKeydown = (event) => {
4450
+ if (event.key === "Escape") {
4451
+ this.#editor.focus();
4452
+ } else {
4453
+ handleRollingTabIndex(this.#tableHandlerButtons, event);
4454
+ }
4455
+ }
4456
+
4271
4457
  #setUpButtons() {
4272
4458
  this.buttonsContainer = createElement("div", {
4273
4459
  className: "lexxy-table-handle-buttons"
@@ -4278,6 +4464,7 @@ class TableHandler extends HTMLElement {
4278
4464
 
4279
4465
  this.moreMenu = this.#createMoreMenu();
4280
4466
  this.buttonsContainer.appendChild(this.moreMenu);
4467
+ this.buttonsContainer.addEventListener("keydown", this.#handleTableHandlerKeydown);
4281
4468
 
4282
4469
  this.#editorElement.appendChild(this.buttonsContainer);
4283
4470
  }
@@ -4373,6 +4560,7 @@ class TableHandler extends HTMLElement {
4373
4560
  const container = createElement("details", {
4374
4561
  className: "lexxy-table-control lexxy-table-control__more-menu"
4375
4562
  });
4563
+ container.setAttribute("name", "lexxy-dropdown");
4376
4564
 
4377
4565
  container.tabIndex = -1;
4378
4566
 
@@ -54,15 +54,17 @@
54
54
  }
55
55
 
56
56
  table {
57
- .table-cell--selected {
58
- background-color: var(--lexxy-color-table-cell-selected-bg) !important;
59
- }
57
+ th, td {
58
+ &.table-cell--selected {
59
+ background-color: var(--lexxy-color-table-cell-selected-bg);
60
+ }
60
61
 
61
- .lexxy-content__table-cell--selected {
62
- background-color: var(--lexxy-color-table-cell-selected-bg) !important;
63
- border-color: var(--lexxy-color-table-cell-selected-border) !important;
62
+ &.lexxy-content__table-cell--selected {
63
+ background-color: var(--lexxy-color-table-cell-selected-bg);
64
+ border-color: var(--lexxy-color-table-cell-selected-border);
65
+ }
64
66
  }
65
-
67
+
66
68
  &.lexxy-content__table--selection {
67
69
  ::selection {
68
70
  background: transparent;
@@ -376,85 +378,12 @@
376
378
  }
377
379
  }
378
380
 
379
- /* Table dropdown
380
- /* -------------------------------------------------------------------------- */
381
-
382
- :where(lexxy-table-dropdown) {
383
- display: flex;
384
- flex-direction: column;
385
- gap: 1ch;
386
-
387
- .lexxy-editor__table-create {
388
- display: flex;
389
- flex-direction: column;
390
- flex-wrap: wrap;
391
- gap: 0;
392
-
393
- .lexxy-editor__table-buttons {
394
- background-color: var(--lexxy-color-ink-lighter);
395
- display: flex;
396
- flex-direction: column;
397
- gap: 1px;
398
- padding: 1px;
399
-
400
- div {
401
- display: flex;
402
- flex-direction: row;
403
- gap: 1px;
404
- }
405
-
406
- button {
407
- aspect-ratio: 1.5 / 1;
408
- border: 0;
409
- border-radius: 0;
410
- color: var(--lexxy-color-ink);
411
- font-family: var(--lexxy-font-base);
412
- font-size: var(--lexxy-text-small);
413
- font-weight: normal;
414
- inline-size: 4ch;
415
- margin: 0;
416
-
417
- &.active {
418
- background-color: var(--lexxy-color-ink-lightest);
419
- }
420
- }
421
- }
422
-
423
- label {
424
- align-items: center;
425
- display: flex;
426
- gap: 0.5ch;
427
- padding: 0.5ch 0;
428
- margin-block-start: 1ch;
429
- }
430
-
431
- &:has(input[type="checkbox"]:checked) {
432
- .lexxy-editor__table-buttons {
433
- div:first-child button,
434
- button:first-child {
435
- filter: brightness(0.95);
436
- }
437
- }
438
- }
439
- }
440
-
441
- .lexxy-editor__table-edit {
442
- display: flex;
443
- flex-direction: column;
444
- flex-wrap: wrap;
445
- gap: 0;
446
-
447
- button {
448
-
449
- }
450
- }
451
- }
452
-
453
381
  /* Table handle buttons
454
382
  /* -------------------------------------------------------------------------- */
455
383
 
456
384
  :where(.lexxy-table-handle-buttons) {
457
385
  --button-size: 2.5lh;
386
+
458
387
  color: var(--lexxy-color-ink-inverted);
459
388
  display: none;
460
389
  flex-direction: row;
@@ -463,7 +392,7 @@
463
392
  line-height: 1;
464
393
  position: absolute;
465
394
  transform: translate(-50%, -120%);
466
- z-index: 10;
395
+ z-index: 2;
467
396
 
468
397
  .lexxy-table-control {
469
398
  align-items: center;
@@ -475,7 +404,8 @@
475
404
  padding: 2px;
476
405
  white-space: nowrap;
477
406
 
478
- button {
407
+ button,
408
+ summary {
479
409
  aspect-ratio: 1 / 1;
480
410
  align-items: center;
481
411
  background-color: transparent;
@@ -487,13 +417,22 @@
487
417
  font-weight: bold;
488
418
  justify-content: center;
489
419
  line-height: 1;
420
+ list-style: none;
490
421
  min-block-size: var(--button-size);
491
422
  min-inline-size: var(--button-size);
492
423
  padding: 0;
424
+ user-select: none;
425
+ -webkit-user-select: none;
426
+
427
+ &:hover {
428
+ background-color: var(--lexxy-color-ink-medium);
429
+ }
493
430
 
494
- &:hover,
431
+ &:focus,
495
432
  &:focus-visible {
496
433
  background-color: var(--lexxy-color-ink-medium);
434
+ outline: var(--lexxy-focus-ring-size) solid var(--lexxy-focus-ring-color);
435
+ outline-offset: var(--lexxy-focus-ring-offset);
497
436
  }
498
437
 
499
438
  svg {
@@ -510,33 +449,13 @@
510
449
 
511
450
  .lexxy-table-control__more-menu {
512
451
  gap: 0;
513
- padding: 2px;
452
+ padding: 0.25ch;
514
453
  position: relative;
515
454
 
516
455
  summary {
517
- aspect-ratio: 1 / 1;
518
- align-items: center;
519
- background: transparent;
520
- border-radius: var(--lexxy-radius);
521
- border: 0;
522
- box-sizing: border-box;
523
- display: flex;
524
- font-size: inherit;
525
- justify-content: center;
526
- list-style: none;
527
- min-block-size: var(--button-size);
528
- min-inline-size: var(--button-size);
529
- padding: 0;
530
- user-select: none;
531
- -webkit-user-select: none;
532
-
533
456
  &::-webkit-details-marker {
534
457
  display: none;
535
458
  }
536
-
537
- &:hover {
538
- background: var(--lexxy-color-ink-medium);
539
- }
540
459
  }
541
460
 
542
461
  .lexxy-table-control__more-menu-details {
@@ -553,7 +472,7 @@
553
472
  border-radius: 0.75ch;
554
473
  display: flex;
555
474
  flex-direction: column;
556
- padding: 2px;
475
+ padding: 0.25ch;
557
476
  }
558
477
 
559
478
  button {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.1.25-beta",
3
+ "version": "0.1.27-beta",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "scripts": {
26
26
  "build": "rollup -c",
27
27
  "build:npm": "rollup -c rollup.config.npm.mjs",
28
- "watch": "NODE_ENV=development rollup -wc --watch.onEnd=\"rails restart\"",
28
+ "watch": "rollup -wc --watch.onEnd=\"rails restart\"",
29
29
  "lint": "eslint",
30
30
  "prerelease": "yarn build:npm",
31
31
  "release": "yarn build:npm && yarn publish"