@37signals/lexxy 0.7.2-beta → 0.7.3-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/dist/lexxy.esm.js CHANGED
@@ -10,12 +10,12 @@ 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, SKIP_DOM_SELECTION_TAG, $getSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, $isRootOrShadowRoot, 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, createCommand, createState, defineExtension, $setState, $getState, $hasUpdateTag, PASTE_TAG, CLEAR_HISTORY_COMMAND, $addUpdateTag, BLUR_COMMAND, FOCUS_COMMAND, KEY_DOWN_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
13
+ import { $isTextNode, TextNode, $isRangeSelection, SKIP_DOM_SELECTION_TAG, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_HIGH, $getSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, $isRootOrShadowRoot, 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, $createNodeSelection, $setSelection, $createParagraphNode, KEY_ENTER_COMMAND, $isParagraphNode, $insertNodes, $createLineBreakNode, PASTE_TAG, createCommand, createState, defineExtension, $setState, $getState, $hasUpdateTag, CLEAR_HISTORY_COMMAND, $addUpdateTag, KEY_DOWN_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
14
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, RichTextExtension, 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, TableNode, INSERT_TABLE_COMMAND, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, TableCellNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $getElementForTableNode, $isTableCellNode, TableCellHeaderStates } from '@lexical/table';
18
+ import { $getTableCellNodeFromLexicalNode, INSERT_TABLE_COMMAND, TableCellNode, TableNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
19
19
  import { createElement, createAttachmentFigure, isPreviewableImage, dispatchCustomEvent, parseHtml, dispatch, generateDomId } from './lexxy_helpers.esm.js';
20
20
  export { highlightCode as highlightAll, highlightCode } from './lexxy_helpers.esm.js';
21
21
  import { buildEditorFromExtensions } from '@lexical/extension';
@@ -146,6 +146,7 @@ function buildConfig() {
146
146
  return {
147
147
  ALLOWED_TAGS: ALLOWED_HTML_TAGS.concat(Lexxy.global.get("attachmentTagName")),
148
148
  ALLOWED_ATTR: ALLOWED_HTML_ATTRIBUTES,
149
+ ADD_URI_SAFE_ATTR: [ "caption", "filename" ],
149
150
  SAFE_FOR_XML: false // So that it does not strip attributes that contains serialized HTML (like content)
150
151
  }
151
152
  }
@@ -378,6 +379,8 @@ class LexicalToolbarElement extends HTMLElement {
378
379
  super();
379
380
  this.internals = this.attachInternals();
380
381
  this.internals.role = "toolbar";
382
+
383
+ this.#createEditorPromise();
381
384
  }
382
385
 
383
386
  connectedCallback() {
@@ -410,14 +413,26 @@ class LexicalToolbarElement extends HTMLElement {
410
413
  this.#refreshToolbarOverflow();
411
414
  this.#bindFocusListeners();
412
415
 
416
+ this.resolveEditorPromise(editorElement);
417
+
413
418
  this.toggleAttribute("connected", true);
414
419
  }
415
420
 
421
+ async getEditorElement() {
422
+ return this.editorElement || await this.editorPromise
423
+ }
424
+
416
425
  #reconnect() {
417
426
  this.disconnectedCallback();
418
427
  this.connectedCallback();
419
428
  }
420
429
 
430
+ #createEditorPromise() {
431
+ this.editorPromise = new Promise((resolve) => {
432
+ this.resolveEditorPromise = resolve;
433
+ });
434
+ }
435
+
421
436
  #installResizeObserver() {
422
437
  this.resizeObserver = new ResizeObserver(() => this.#refreshToolbarOverflow());
423
438
  this.resizeObserver.observe(this);
@@ -486,28 +501,24 @@ class LexicalToolbarElement extends HTMLElement {
486
501
  }
487
502
 
488
503
  #bindFocusListeners() {
489
- this.editorElement.addEventListener("lexxy:focus", this.#handleFocus);
490
- this.editorElement.addEventListener("lexxy:blur", this.#handleFocusOut);
491
- this.addEventListener("focusout", this.#handleFocusOut);
504
+ this.editorElement.addEventListener("lexxy:focus", this.#handleEditorFocus);
505
+ this.editorElement.addEventListener("lexxy:blur", this.#handleEditorBlur);
492
506
  this.addEventListener("keydown", this.#handleKeydown);
493
507
  }
494
508
 
495
509
  #unbindFocusListeners() {
496
- this.editorElement.removeEventListener("lexxy:focus", this.#handleFocus);
497
- this.editorElement.removeEventListener("lexxy:blur", this.#handleFocusOut);
498
- this.removeEventListener("focusout", this.#handleFocusOut);
510
+ this.editorElement.removeEventListener("lexxy:focus", this.#handleEditorFocus);
511
+ this.editorElement.removeEventListener("lexxy:blur", this.#handleEditorBlur);
499
512
  this.removeEventListener("keydown", this.#handleKeydown);
500
513
  }
501
514
 
502
- #handleFocus = () => {
503
- this.#resetTabIndexValues();
515
+ #handleEditorFocus = () => {
504
516
  this.#focusableItems[0].tabIndex = 0;
505
517
  }
506
518
 
507
- #handleFocusOut = () => {
508
- if (!this.contains(document.activeElement)) {
509
- this.#resetTabIndexValues();
510
- }
519
+ #handleEditorBlur = () => {
520
+ this.#resetTabIndexValues();
521
+ this.#closeDropdowns();
511
522
  }
512
523
 
513
524
  #handleKeydown = (event) => {
@@ -521,11 +532,13 @@ class LexicalToolbarElement extends HTMLElement {
521
532
  }
522
533
 
523
534
  #monitorSelectionChanges() {
524
- this.editor.registerUpdateListener(() => {
525
- this.editor.getEditorState().read(() => {
535
+ this.editor.registerCommand(
536
+ SELECTION_CHANGE_COMMAND,
537
+ () => {
538
+ this.#closeDropdowns();
526
539
  this.#updateButtonStates();
527
- });
528
- });
540
+ return false
541
+ }, COMMAND_PRIORITY_HIGH);
529
542
  }
530
543
 
531
544
  #monitorHistoryChanges() {
@@ -614,11 +627,13 @@ class LexicalToolbarElement extends HTMLElement {
614
627
  }
615
628
 
616
629
  #toolbarIsOverflowing() {
617
- return this.scrollWidth > this.clientWidth
630
+ // Safari can report inconsistent clientWidth values on more than 100% window zoom level,
631
+ // that was affecting the toolbar overflow calculation. We're adding +1 to get around this issue.
632
+ return (this.scrollWidth - this.#overflow.clientWidth) > this.clientWidth + 1
618
633
  }
619
634
 
620
635
  #refreshToolbarOverflow = () => {
621
- this.#resetToolbar();
636
+ this.#resetToolbarOverflow();
622
637
  this.#compactMenu();
623
638
 
624
639
  this.#overflow.style.display = this.#overflowMenu.children.length ? "block" : "none";
@@ -644,7 +659,7 @@ class LexicalToolbarElement extends HTMLElement {
644
659
  }
645
660
  }
646
661
 
647
- #resetToolbar() {
662
+ #resetToolbarOverflow() {
648
663
  const items = Array.from(this.#overflowMenu.children);
649
664
  items.sort((a, b) => this.#itemPosition(b) - this.#itemPosition(a));
650
665
 
@@ -666,6 +681,16 @@ class LexicalToolbarElement extends HTMLElement {
666
681
  });
667
682
  }
668
683
 
684
+ #closeDropdowns() {
685
+ this.#dropdowns.forEach((details) => {
686
+ details.open = false;
687
+ });
688
+ }
689
+
690
+ get #dropdowns() {
691
+ return this.querySelectorAll("details")
692
+ }
693
+
669
694
  get #overflow() {
670
695
  return this.querySelector(".lexxy-editor__toolbar-overflow")
671
696
  }
@@ -707,9 +732,8 @@ class LexicalToolbarElement extends HTMLElement {
707
732
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M7.65422 0.711575C7.1856 0.242951 6.42579 0.242951 5.95717 0.711575C5.48853 1.18021 5.48853 1.94 5.95717 2.40864L8.70864 5.16011L2.85422 11.0145C1.44834 12.4204 1.44833 14.6998 2.85422 16.1057L7.86011 21.1115C9.26599 22.5174 11.5454 22.5174 12.9513 21.1115L19.6542 14.4087C20.1228 13.94 20.1228 13.1802 19.6542 12.7115L11.8544 4.91171L11.2542 4.31158L7.65422 0.711575ZM4.55127 12.7115L10.4057 6.85716L17.1087 13.56H4.19981C4.19981 13.253 4.31696 12.9459 4.55127 12.7115ZM23.6057 20.76C23.6057 22.0856 22.5311 23.16 21.2057 23.16C19.8802 23.16 18.8057 22.0856 18.8057 20.76C18.8057 19.5408 19.8212 18.5339 20.918 17.4462C21.0135 17.3516 21.1096 17.2563 21.2057 17.16C21.3018 17.2563 21.398 17.3516 21.4935 17.4462C22.5903 18.5339 23.6057 19.5408 23.6057 20.76Z"/></svg>
708
733
  </summary>
709
734
  <lexxy-highlight-dropdown class="lexxy-editor__toolbar-dropdown-content">
710
- <div data-button-group="color"></div>
711
- <div data-button-group="background-color"></div>
712
- <button data-command="removeHighlight" class="lexxy-editor__toolbar-dropdown-reset">Remove all coloring</button>
735
+ <div class="lexxy-highlight-colors"></div>
736
+ <button data-command="removeHighlight" class="lexxy-editor__toolbar-button lexxy-editor__toolbar-dropdown-reset">Remove all coloring</button>
713
737
  </lexxy-highlight-dropdown>
714
738
  </details>
715
739
 
@@ -721,8 +745,8 @@ class LexicalToolbarElement extends HTMLElement {
721
745
  <form method="dialog">
722
746
  <input type="url" placeholder="Enter a URL…" class="input">
723
747
  <div class="lexxy-editor__toolbar-dropdown-actions">
724
- <button type="submit" class="btn" value="link">Link</button>
725
- <button type="button" class="btn" value="unlink">Unlink</button>
748
+ <button type="submit" class="lexxy-editor__toolbar-button" value="link">Link</button>
749
+ <button type="button" class="lexxy-editor__toolbar-button" value="unlink">Unlink</button>
726
750
  </div>
727
751
  </form>
728
752
  </lexxy-link-dropdown>
@@ -770,9 +794,9 @@ class LexicalToolbarElement extends HTMLElement {
770
794
  <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>
771
795
  </button>
772
796
 
773
- <details class="lexxy-editor__toolbar-overflow">
797
+ <details class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-overflow" name="lexxy-dropdown">
774
798
  <summary class="lexxy-editor__toolbar-button" aria-label="Show more toolbar buttons">•••</summary>
775
- <div class="lexxy-editor__toolbar-overflow-menu" aria-label="More toolbar buttons"></div>
799
+ <div class="lexxy-editor__toolbar-dropdown-content lexxy-editor__toolbar-overflow-menu" aria-label="More toolbar buttons"></div>
776
800
  </details>
777
801
  `
778
802
  }
@@ -792,6 +816,8 @@ var theme = {
792
816
  tableCellSelected: "lexxy-content__table-cell--selected",
793
817
  tableSelection: "lexxy-content__table--selection",
794
818
  tableScrollableWrapper: "lexxy-content__table-wrapper",
819
+ tableCellHighlight: "lexxy-content__table-cell--highlight",
820
+ tableCellFocus: "lexxy-content__table-cell--focus",
795
821
  list: {
796
822
  nested: {
797
823
  listitem: "lexxy-nested-listitem",
@@ -1373,30 +1399,6 @@ class HorizontalDividerNode extends DecoratorNode {
1373
1399
  }
1374
1400
  }
1375
1401
 
1376
- class WrappedTableNode extends TableNode {
1377
- static clone(node) {
1378
- return new WrappedTableNode(node.__key)
1379
- }
1380
-
1381
- exportDOM(editor) {
1382
- const superExport = super.exportDOM(editor);
1383
-
1384
- return {
1385
- ...superExport,
1386
- after: (tableElement) => {
1387
- if (superExport.after) {
1388
- tableElement = superExport.after(tableElement);
1389
- const clonedTable = tableElement.cloneNode(true);
1390
- const wrappedTable = createElement("figure", { className: "lexxy-content__table-wrapper" }, clonedTable.outerHTML);
1391
- return wrappedTable
1392
- }
1393
-
1394
- return tableElement
1395
- }
1396
- }
1397
- }
1398
- }
1399
-
1400
1402
  const COMMANDS = [
1401
1403
  "bold",
1402
1404
  "italic",
@@ -1414,13 +1416,6 @@ const COMMANDS = [
1414
1416
  "uploadAttachments",
1415
1417
 
1416
1418
  "insertTable",
1417
- "insertTableRowAbove",
1418
- "insertTableRowBelow",
1419
- "insertTableColumnAfter",
1420
- "insertTableColumnBefore",
1421
- "deleteTableRow",
1422
- "deleteTableColumn",
1423
- "deleteTable",
1424
1419
 
1425
1420
  "undo",
1426
1421
  "redo"
@@ -1529,9 +1524,7 @@ class CommandDispatcher {
1529
1524
  }
1530
1525
 
1531
1526
  dispatchInsertHorizontalDivider() {
1532
- this.editor.update(() => {
1533
- this.contents.insertAtCursorEnsuringLineBelow(new HorizontalDividerNode());
1534
- });
1527
+ this.contents.insertAtCursorEnsuringLineBelow(new HorizontalDividerNode());
1535
1528
 
1536
1529
  this.editor.focus();
1537
1530
  }
@@ -1593,41 +1586,6 @@ class CommandDispatcher {
1593
1586
  this.editor.dispatchCommand(INSERT_TABLE_COMMAND, { "rows": 3, "columns": 3, "includeHeaders": true });
1594
1587
  }
1595
1588
 
1596
- dispatchInsertTableRowBelow() {
1597
- $insertTableRowAtSelection(true);
1598
- }
1599
-
1600
- dispatchInsertTableRowAbove() {
1601
- $insertTableRowAtSelection(false);
1602
- }
1603
-
1604
- dispatchInsertTableColumnAfter() {
1605
- $insertTableColumnAtSelection(true);
1606
- }
1607
-
1608
- dispatchInsertTableColumnBefore() {
1609
- $insertTableColumnAtSelection(false);
1610
- }
1611
-
1612
- dispatchDeleteTableRow() {
1613
- $deleteTableRowAtSelection();
1614
- }
1615
-
1616
- dispatchDeleteTableColumn() {
1617
- $deleteTableColumnAtSelection();
1618
- }
1619
-
1620
- dispatchDeleteTable() {
1621
- this.editor.update(() => {
1622
- const selection = $getSelection();
1623
- if (!$isRangeSelection(selection)) return
1624
-
1625
- const anchorNode = selection.anchor.getNode();
1626
- const tableNode = $findTableNode(anchorNode);
1627
- tableNode.remove();
1628
- });
1629
- }
1630
-
1631
1589
  dispatchUndo() {
1632
1590
  this.editor.dispatchCommand(UNDO_COMMAND, undefined);
1633
1591
  }
@@ -1921,6 +1879,14 @@ class Selection {
1921
1879
  return $getNearestNodeOfType(anchorNode, CodeNode) !== null
1922
1880
  }
1923
1881
 
1882
+ get isTableCellSelected() {
1883
+ const selection = $getSelection();
1884
+ if (!$isRangeSelection(selection)) return false
1885
+
1886
+ const anchorNode = selection.anchor.getNode();
1887
+ return $getNearestNodeOfType(anchorNode, TableCellNode) !== null
1888
+ }
1889
+
1924
1890
  get nodeAfterCursor() {
1925
1891
  const { anchorNode, offset } = this.#getCollapsedSelectionData();
1926
1892
  if (!anchorNode) return null
@@ -2433,17 +2399,6 @@ function sanitize(html) {
2433
2399
  return DOMPurify.sanitize(html, buildConfig())
2434
2400
  }
2435
2401
 
2436
- // Prevent the hardcoded background color
2437
- // A background color value is set by Lexical if background is null:
2438
- // https://github.com/facebook/lexical/blob/5bbbe849bd229e1db0e7b536e6a919520ada7bb2/packages/lexical-table/src/LexicalTableCellNode.ts#L187
2439
- function registerHeaderBackgroundTransform(editor) {
2440
- return editor.registerNodeTransform(TableCellNode, (node) => {
2441
- if (node.getBackgroundColor() === null) {
2442
- node.setBackgroundColor("");
2443
- }
2444
- })
2445
- }
2446
-
2447
2402
  function dasherize(value) {
2448
2403
  return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)
2449
2404
  }
@@ -2467,6 +2422,10 @@ function filterMatches(text, potentialMatch) {
2467
2422
  return normalizeFilteredText(text).includes(normalizeFilteredText(potentialMatch))
2468
2423
  }
2469
2424
 
2425
+ function upcaseFirst(string) {
2426
+ return string.charAt(0).toUpperCase() + string.slice(1)
2427
+ }
2428
+
2470
2429
  class EditorConfiguration {
2471
2430
  #editorElement
2472
2431
  #config
@@ -2908,15 +2867,14 @@ class Contents {
2908
2867
  new FormatEscaper(editorElement).monitor();
2909
2868
  }
2910
2869
 
2911
- insertHtml(html) {
2870
+ insertHtml(html, { tag } = {}) {
2912
2871
  this.editor.update(() => {
2913
2872
  const selection = $getSelection();
2914
-
2915
2873
  if (!$isRangeSelection(selection)) return
2916
2874
 
2917
2875
  const nodes = $generateNodesFromDOM(this.editor, parseHtml(html));
2918
2876
  selection.insertNodes(nodes);
2919
- });
2877
+ }, { tag });
2920
2878
  }
2921
2879
 
2922
2880
  insertAtCursor(node) {
@@ -3719,14 +3677,14 @@ class Clipboard {
3719
3677
 
3720
3678
  #pasteMarkdown(text) {
3721
3679
  const html = marked(text);
3722
- this.contents.insertHtml(html);
3680
+ this.contents.insertHtml(html, { tag: [ PASTE_TAG ] });
3723
3681
  }
3724
3682
 
3725
3683
  #pasteRichText(clipboardData) {
3726
3684
  this.editor.update(() => {
3727
3685
  const selection = $getSelection();
3728
3686
  $insertDataTransferForRichText(clipboardData, selection, this.editor);
3729
- });
3687
+ }, { tag: PASTE_TAG });
3730
3688
  }
3731
3689
 
3732
3690
  #handlePastedFiles(clipboardData) {
@@ -3960,6 +3918,124 @@ function $applyLanguage(conversionOutput, element) {
3960
3918
  conversionOutput.node.setLanguage(language);
3961
3919
  }
3962
3920
 
3921
+ class WrappedTableNode extends TableNode {
3922
+ static clone(node) {
3923
+ return new WrappedTableNode(node.__key)
3924
+ }
3925
+
3926
+ exportDOM(editor) {
3927
+ const superExport = super.exportDOM(editor);
3928
+
3929
+ return {
3930
+ ...superExport,
3931
+ after: (tableElement) => {
3932
+ if (superExport.after) {
3933
+ tableElement = superExport.after(tableElement);
3934
+ const clonedTable = tableElement.cloneNode(true);
3935
+ const wrappedTable = createElement("figure", { className: "lexxy-content__table-wrapper" }, clonedTable.outerHTML);
3936
+ return wrappedTable
3937
+ }
3938
+
3939
+ return tableElement
3940
+ }
3941
+ }
3942
+ }
3943
+ }
3944
+
3945
+ const TablesLexicalExtension = defineExtension({
3946
+ name: "lexxy/tables",
3947
+ nodes: [
3948
+ WrappedTableNode,
3949
+ {
3950
+ replace: TableNode,
3951
+ with: () => new WrappedTableNode()
3952
+ },
3953
+ TableCellNode,
3954
+ TableRowNode
3955
+ ],
3956
+ register(editor) {
3957
+ // Register Lexical table plugins
3958
+ registerTablePlugin(editor);
3959
+ registerTableSelectionObserver(editor, true);
3960
+ setScrollableTablesActive(editor, true);
3961
+
3962
+ // Bug fix: Prevent hardcoded background color (Lexical #8089)
3963
+ editor.registerNodeTransform(TableCellNode, (node) => {
3964
+ if (node.getBackgroundColor() === null) {
3965
+ node.setBackgroundColor("");
3966
+ }
3967
+ });
3968
+
3969
+ // Bug fix: Fix column header states (Lexical #8090)
3970
+ editor.registerNodeTransform(TableCellNode, (node) => {
3971
+ const headerState = node.getHeaderStyles();
3972
+
3973
+ if (headerState !== TableCellHeaderStates.ROW) return
3974
+
3975
+ const rowParent = node.getParent();
3976
+ const tableNode = rowParent?.getParent();
3977
+ if (!tableNode) return
3978
+
3979
+ const rows = tableNode.getChildren();
3980
+ const cellIndex = rowParent.getChildren().indexOf(node);
3981
+
3982
+ const cellsInRow = rowParent.getChildren();
3983
+ const isHeaderRow = cellsInRow.every(cell =>
3984
+ cell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS
3985
+ );
3986
+
3987
+ const isHeaderColumn = rows.every(row => {
3988
+ const cell = row.getChildren()[cellIndex];
3989
+ return cell && cell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS
3990
+ });
3991
+
3992
+ let newHeaderState = TableCellHeaderStates.NO_STATUS;
3993
+
3994
+ if (isHeaderRow) {
3995
+ newHeaderState |= TableCellHeaderStates.ROW;
3996
+ }
3997
+
3998
+ if (isHeaderColumn) {
3999
+ newHeaderState |= TableCellHeaderStates.COLUMN;
4000
+ }
4001
+
4002
+ if (newHeaderState !== headerState) {
4003
+ node.setHeaderStyles(newHeaderState, TableCellHeaderStates.BOTH);
4004
+ }
4005
+ });
4006
+
4007
+ editor.registerCommand("insertTableRowAfter", () => {
4008
+ $insertTableRowAtSelection(true);
4009
+ }, COMMAND_PRIORITY_NORMAL);
4010
+
4011
+ editor.registerCommand("insertTableRowBefore", () => {
4012
+ $insertTableRowAtSelection(false);
4013
+ }, COMMAND_PRIORITY_NORMAL);
4014
+
4015
+ editor.registerCommand("insertTableColumnAfter", () => {
4016
+ $insertTableColumnAtSelection(true);
4017
+ }, COMMAND_PRIORITY_NORMAL);
4018
+
4019
+ editor.registerCommand("insertTableColumnBefore", () => {
4020
+ $insertTableColumnAtSelection(false);
4021
+ }, COMMAND_PRIORITY_NORMAL);
4022
+
4023
+ editor.registerCommand("deleteTableRow", () => {
4024
+ $deleteTableRowAtSelection();
4025
+ }, COMMAND_PRIORITY_NORMAL);
4026
+
4027
+ editor.registerCommand("deleteTableColumn", () => {
4028
+ $deleteTableColumnAtSelection();
4029
+ }, COMMAND_PRIORITY_NORMAL);
4030
+
4031
+ editor.registerCommand("deleteTable", () => {
4032
+ const selection = $getSelection();
4033
+ if (!$isRangeSelection(selection)) return false
4034
+ $findTableNode(selection.anchor.getNode())?.remove();
4035
+ }, COMMAND_PRIORITY_NORMAL);
4036
+ }
4037
+ });
4038
+
3963
4039
  class LexicalEditorElement extends HTMLElement {
3964
4040
  static formAssociated = true
3965
4041
  static debug = false
@@ -4151,8 +4227,7 @@ class LexicalEditorElement extends HTMLElement {
4151
4227
  this.#registerComponents();
4152
4228
  this.#listenForInvalidatedNodes();
4153
4229
  this.#handleEnter();
4154
- this.#handleFocus();
4155
- this.#handleTables();
4230
+ this.#registerFocusEvents();
4156
4231
  this.#attachDebugHooks();
4157
4232
  this.#attachToolbar();
4158
4233
  this.#loadInitialValue();
@@ -4180,7 +4255,8 @@ class LexicalEditorElement extends HTMLElement {
4180
4255
  const extensions = [ ];
4181
4256
  const richTextExtensions = [
4182
4257
  this.highlighter.lexicalExtension,
4183
- TrixContentExtension
4258
+ TrixContentExtension,
4259
+ TablesLexicalExtension
4184
4260
  ];
4185
4261
 
4186
4262
  if (this.supportsRichText) {
@@ -4205,14 +4281,7 @@ class LexicalEditorElement extends HTMLElement {
4205
4281
  CodeHighlightNode,
4206
4282
  LinkNode,
4207
4283
  AutoLinkNode,
4208
- HorizontalDividerNode,
4209
- WrappedTableNode,
4210
- {
4211
- replace: TableNode,
4212
- with: () => { return new WrappedTableNode() }
4213
- },
4214
- TableCellNode,
4215
- TableRowNode,
4284
+ HorizontalDividerNode
4216
4285
  );
4217
4286
  }
4218
4287
 
@@ -4326,11 +4395,8 @@ class LexicalEditorElement extends HTMLElement {
4326
4395
  }
4327
4396
 
4328
4397
  #registerTableComponents() {
4329
- registerTablePlugin(this.editor);
4330
- this.tableHandler = createElement("lexxy-table-handler");
4331
- this.append(this.tableHandler);
4332
-
4333
- this.#addUnregisterHandler(registerHeaderBackgroundTransform(this.editor));
4398
+ this.tableTools = createElement("lexxy-table-tools");
4399
+ this.append(this.tableTools);
4334
4400
  }
4335
4401
 
4336
4402
  #registerCodeHiglightingComponents() {
@@ -4377,12 +4443,27 @@ class LexicalEditorElement extends HTMLElement {
4377
4443
  );
4378
4444
  }
4379
4445
 
4380
- #handleFocus() {
4381
- // Lexxy handles focus and blur as commands
4382
- // see https://github.com/facebook/lexical/blob/d1a8e84fe9063a4f817655b346b6ff373aa107f0/packages/lexical/src/LexicalEvents.ts#L35
4383
- // and https://stackoverflow.com/a/72212077
4384
- this.editor.registerCommand(BLUR_COMMAND, () => { dispatch(this, "lexxy:blur"); }, COMMAND_PRIORITY_NORMAL);
4385
- this.editor.registerCommand(FOCUS_COMMAND, () => { dispatch(this, "lexxy:focus"); }, COMMAND_PRIORITY_NORMAL);
4446
+ #registerFocusEvents() {
4447
+ this.addEventListener("focusin", this.#handleFocusIn);
4448
+ this.addEventListener("focusout", this.#handleFocusOut);
4449
+ }
4450
+
4451
+ #handleFocusIn(event) {
4452
+ if (this.#elementInEditorOrToolbar(event.target) && !this.currentlyFocused) {
4453
+ dispatch(this, "lexxy:focus");
4454
+ this.currentlyFocused = true;
4455
+ }
4456
+ }
4457
+
4458
+ #handleFocusOut(event) {
4459
+ if (!this.#elementInEditorOrToolbar(event.relatedTarget)) {
4460
+ dispatch(this, "lexxy:blur");
4461
+ this.currentlyFocused = false;
4462
+ }
4463
+ }
4464
+
4465
+ #elementInEditorOrToolbar(element) {
4466
+ return this.contains(element) || this.toolbarElement?.contains(element)
4386
4467
  }
4387
4468
 
4388
4469
  #onFocus() {
@@ -4399,12 +4480,6 @@ class LexicalEditorElement extends HTMLElement {
4399
4480
  }
4400
4481
  }
4401
4482
 
4402
- #handleTables() {
4403
- if (this.supportsRichText) {
4404
- this.removeTableSelectionObserver = registerTableSelectionObserver(this.editor, true);
4405
- setScrollableTablesActive(this.editor, true);
4406
- }
4407
- }
4408
4483
 
4409
4484
  #attachDebugHooks() {
4410
4485
  if (!LexicalEditorElement.debug) return
@@ -4502,10 +4577,11 @@ class ToolbarDropdown extends HTMLElement {
4502
4577
 
4503
4578
  this.container.addEventListener("toggle", this.#handleToggle.bind(this));
4504
4579
  this.container.addEventListener("keydown", this.#handleKeyDown.bind(this));
4580
+
4581
+ this.#onToolbarEditor(this.initialize.bind(this));
4505
4582
  }
4506
4583
 
4507
4584
  disconnectedCallback() {
4508
- this.#removeClickOutsideHandler();
4509
4585
  this.container.removeEventListener("keydown", this.#handleKeyDown.bind(this));
4510
4586
  }
4511
4587
 
@@ -4521,46 +4597,29 @@ class ToolbarDropdown extends HTMLElement {
4521
4597
  return this.toolbar.editor
4522
4598
  }
4523
4599
 
4524
- close() {
4525
- this.container.removeAttribute("open");
4526
- }
4527
-
4528
- #handleToggle(event) {
4529
- if (this.container.open) {
4530
- this.#handleOpen(event.target);
4531
- } else {
4532
- this.#handleClose();
4533
- }
4600
+ initialize() {
4601
+ // Any post-editor initialization
4534
4602
  }
4535
4603
 
4536
- #handleOpen() {
4537
- this.#interactiveElements[0].focus();
4538
- this.#setupClickOutsideHandler();
4539
-
4540
- this.#resetTabIndexValues();
4541
- }
4542
-
4543
- #handleClose() {
4544
- this.#removeClickOutsideHandler();
4604
+ close() {
4545
4605
  this.editor.focus();
4606
+ this.container.open = false;
4546
4607
  }
4547
4608
 
4548
- #setupClickOutsideHandler() {
4549
- if (this.clickOutsideHandler) return
4550
-
4551
- this.clickOutsideHandler = this.#handleClickOutside.bind(this);
4552
- document.addEventListener("click", this.clickOutsideHandler, true);
4609
+ async #onToolbarEditor(callback) {
4610
+ await this.toolbar.editorConnected;
4611
+ callback();
4553
4612
  }
4554
4613
 
4555
- #removeClickOutsideHandler() {
4556
- if (!this.clickOutsideHandler) return
4557
-
4558
- document.removeEventListener("click", this.clickOutsideHandler, true);
4559
- this.clickOutsideHandler = null;
4614
+ #handleToggle() {
4615
+ if (this.container.open) {
4616
+ this.#handleOpen();
4617
+ }
4560
4618
  }
4561
4619
 
4562
- #handleClickOutside({ target }) {
4563
- if (this.container.open && !this.container.contains(target)) this.close();
4620
+ async #handleOpen() {
4621
+ this.#interactiveElements[0].focus();
4622
+ this.#resetTabIndexValues();
4564
4623
  }
4565
4624
 
4566
4625
  #handleKeyDown(event) {
@@ -4648,19 +4707,14 @@ const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
4648
4707
  const NO_STYLE = Symbol("no_style");
4649
4708
 
4650
4709
  class HighlightDropdown extends ToolbarDropdown {
4651
- #initialized = false
4652
-
4653
4710
  connectedCallback() {
4654
4711
  super.connectedCallback();
4655
4712
  this.#registerToggleHandler();
4656
4713
  }
4657
4714
 
4658
- #ensureInitialized() {
4659
- if (this.#initialized) return
4660
-
4715
+ initialize() {
4661
4716
  this.#setUpButtons();
4662
4717
  this.#registerButtonHandlers();
4663
- this.#initialized = true;
4664
4718
  }
4665
4719
 
4666
4720
  #registerToggleHandler() {
@@ -4673,16 +4727,18 @@ class HighlightDropdown extends ToolbarDropdown {
4673
4727
  }
4674
4728
 
4675
4729
  #setUpButtons() {
4676
- this.#buttonGroups.forEach(buttonGroup => {
4677
- this.#populateButtonGroup(buttonGroup);
4678
- });
4730
+ const colorGroups = this.editorElement.config.get("highlight.buttons");
4731
+
4732
+ this.#populateButtonGroup("color", colorGroups.color);
4733
+ this.#populateButtonGroup("background-color", colorGroups["background-color"]);
4734
+
4735
+ const maxNumberOfColors = Math.max(colorGroups.color.length, colorGroups["background-color"].length);
4736
+ this.style.setProperty("--max-colors", maxNumberOfColors);
4679
4737
  }
4680
4738
 
4681
- #populateButtonGroup(buttonGroup) {
4682
- const attribute = buttonGroup.dataset.buttonGroup;
4683
- const values = this.editorElement.config.get(`highlight.buttons.${attribute}`) || [];
4739
+ #populateButtonGroup(attribute, values) {
4684
4740
  values.forEach((value, index) => {
4685
- buttonGroup.appendChild(this.#createButton(attribute, value, index));
4741
+ this.#buttonContainer.appendChild(this.#createButton(attribute, value, index));
4686
4742
  });
4687
4743
  }
4688
4744
 
@@ -4691,15 +4747,13 @@ class HighlightDropdown extends ToolbarDropdown {
4691
4747
  button.dataset.style = attribute;
4692
4748
  button.style.setProperty(attribute, value);
4693
4749
  button.dataset.value = value;
4694
- button.classList.add("lexxy-highlight-button");
4750
+ button.classList.add("lexxy-editor__toolbar-button", "lexxy-highlight-button");
4695
4751
  button.name = attribute + "-" + index;
4696
4752
  return button
4697
4753
  }
4698
4754
 
4699
4755
  #handleToggle({ newState }) {
4700
4756
  if (newState === "open") {
4701
- this.#ensureInitialized();
4702
-
4703
4757
  this.editor.getEditorState().read(() => {
4704
4758
  this.#updateColorButtonStates($getSelection());
4705
4759
  });
@@ -4742,8 +4796,8 @@ class HighlightDropdown extends ToolbarDropdown {
4742
4796
  this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).disabled = !hasHighlight;
4743
4797
  }
4744
4798
 
4745
- get #buttonGroups() {
4746
- return this.querySelectorAll("[data-button-group]")
4799
+ get #buttonContainer() {
4800
+ return this.querySelector(".lexxy-highlight-colors")
4747
4801
  }
4748
4802
 
4749
4803
  get #colorButtons() {
@@ -4753,501 +4807,714 @@ class HighlightDropdown extends ToolbarDropdown {
4753
4807
 
4754
4808
  customElements.define("lexxy-highlight-dropdown", HighlightDropdown);
4755
4809
 
4756
- class TableHandler extends HTMLElement {
4757
- connectedCallback() {
4758
- this.#setUpButtons();
4759
- this.#monitorForTableSelection();
4760
- this.#registerKeyboardShortcuts();
4810
+ class TableController {
4811
+ constructor(editorElement) {
4812
+ this.editor = editorElement.editor;
4813
+ this.contents = editorElement.contents;
4814
+ this.selection = editorElement.selection;
4815
+
4816
+ this.currentTableNodeKey = null;
4817
+ this.currentCellKey = null;
4818
+
4819
+ this.#registerKeyHandlers();
4761
4820
  }
4762
4821
 
4763
- disconnectedCallback() {
4764
- this.#unregisterKeyboardShortcuts();
4822
+ destroy() {
4823
+ this.currentTableNodeKey = null;
4824
+ this.currentCellKey = null;
4825
+
4826
+ this.#unregisterKeyHandlers();
4765
4827
  }
4766
4828
 
4767
- get #editor() {
4768
- return this.#editorElement.editor
4829
+ get currentCell() {
4830
+ if (!this.currentCellKey) return null
4831
+
4832
+ return this.editor.getEditorState().read(() => {
4833
+ const cell = $getNodeByKey(this.currentCellKey);
4834
+ return (cell instanceof TableCellNode) ? cell : null
4835
+ })
4769
4836
  }
4770
4837
 
4771
- get #editorElement() {
4772
- return this.closest("lexxy-editor")
4838
+ get currentTableNode() {
4839
+ if (!this.currentTableNodeKey) return null
4840
+
4841
+ return this.editor.getEditorState().read(() => {
4842
+ const tableNode = $getNodeByKey(this.currentTableNodeKey);
4843
+ return (tableNode instanceof TableNode) ? tableNode : null
4844
+ })
4773
4845
  }
4774
4846
 
4775
- get #currentCell() {
4776
- const selection = $getSelection();
4777
- if (!$isRangeSelection(selection)) return null
4847
+ get currentRowCells() {
4848
+ const currentRowIndex = this.currentRowIndex;
4778
4849
 
4779
- const anchorNode = selection.anchor.getNode();
4780
- return $getTableCellNodeFromLexicalNode(anchorNode)
4850
+ const rows = this.tableRows;
4851
+ if (!rows) return null
4852
+
4853
+ return this.editor.getEditorState().read(() => {
4854
+ return rows[currentRowIndex]?.getChildren() ?? null
4855
+ }) ?? null
4781
4856
  }
4782
4857
 
4783
- get #currentRow() {
4784
- const currentCell = this.#currentCell;
4858
+ get currentRowIndex() {
4859
+ const currentCell = this.currentCell;
4785
4860
  if (!currentCell) return 0
4786
- return $getTableRowIndexFromTableCellNode(currentCell)
4861
+
4862
+ return this.editor.getEditorState().read(() => {
4863
+ return $getTableRowIndexFromTableCellNode(currentCell)
4864
+ }) ?? 0
4787
4865
  }
4788
4866
 
4789
- get #currentColumn() {
4790
- const currentCell = this.#currentCell;
4791
- if (!currentCell) return 0
4792
- return $getTableColumnIndexFromTableCellNode(currentCell)
4867
+ get currentColumnCells() {
4868
+ const columnIndex = this.currentColumnIndex;
4869
+
4870
+ const rows = this.tableRows;
4871
+ if (!rows) return null
4872
+
4873
+ return this.editor.getEditorState().read(() => {
4874
+ return rows.map(row => row.getChildAtIndex(columnIndex))
4875
+ }) ?? null
4793
4876
  }
4794
4877
 
4795
- get #tableHandlerButtons() {
4796
- return Array.from(this.querySelectorAll("button, details > summary"))
4878
+ get currentColumnIndex() {
4879
+ const currentCell = this.currentCell;
4880
+ if (!currentCell) return 0
4881
+
4882
+ return this.editor.getEditorState().read(() => {
4883
+ return $getTableColumnIndexFromTableCellNode(currentCell)
4884
+ }) ?? 0
4797
4885
  }
4798
4886
 
4799
- #registerKeyboardShortcuts() {
4800
- this.unregisterKeyboardShortcuts = this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleKeyDown, COMMAND_PRIORITY_HIGH);
4887
+ get tableRows() {
4888
+ return this.editor.getEditorState().read(() => {
4889
+ return this.currentTableNode?.getChildren()
4890
+ }) ?? null
4801
4891
  }
4802
4892
 
4803
- #unregisterKeyboardShortcuts() {
4804
- this.unregisterKeyboardShortcuts();
4893
+ updateSelectedTable() {
4894
+ let cellNode = null;
4895
+ let tableNode = null;
4896
+
4897
+ this.editor.getEditorState().read(() => {
4898
+ const selection = $getSelection();
4899
+ if (!selection || !this.selection.isTableCellSelected) return
4900
+
4901
+ const node = selection.getNodes()[0];
4902
+
4903
+ cellNode = $findCellNode(node);
4904
+ tableNode = $findTableNode(node);
4905
+ });
4906
+
4907
+ this.currentCellKey = cellNode?.getKey() ?? null;
4908
+ this.currentTableNodeKey = tableNode?.getKey() ?? null;
4805
4909
  }
4806
4910
 
4807
- #handleKeyDown = (event) => {
4808
- if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === "F10") {
4809
- const firstButton = this.querySelector("button, [tabindex]:not([tabindex='-1'])");
4810
- this.#setFocusStateOnSelectedCell();
4811
- firstButton?.focus();
4812
- } else if (event.key === "Escape") {
4813
- this.#editor.getEditorState().read(() => {
4814
- const cell = this.#currentCell;
4815
- if (!cell) return
4911
+ executeTableCommand(command, customIndex = null) {
4912
+ if (command.action === "delete" && command.childType === "table") {
4913
+ this.#deleteTable();
4914
+ return
4915
+ }
4816
4916
 
4817
- this.#editor.update(() => {
4818
- cell.select();
4819
- });
4820
- });
4821
- this.#closeMoreMenu();
4917
+ if (command.action === "toggle") {
4918
+ this.#executeToggleStyle(command);
4919
+ return
4822
4920
  }
4921
+
4922
+ this.#executeCommand(command, customIndex);
4823
4923
  }
4824
4924
 
4825
- #handleTableHandlerKeydown = (event) => {
4826
- if (event.key === "Escape") {
4827
- this.#editor.focus();
4828
- } else {
4829
- handleRollingTabIndex(this.#tableHandlerButtons, event);
4830
- }
4925
+ #executeCommand(command, customIndex = null) {
4926
+ this.#selectCellAtSelection();
4927
+ this.editor.dispatchCommand(this.#commandName(command));
4928
+ this.#selectNextBestCell(command, customIndex);
4831
4929
  }
4832
4930
 
4833
- #setUpButtons() {
4834
- this.appendChild(this.#createRowButtonsContainer());
4835
- this.appendChild(this.#createColumnButtonsContainer());
4931
+ #executeToggleStyle(command) {
4932
+ const childType = command.childType;
4836
4933
 
4837
- this.moreMenu = this.#createMoreMenu();
4838
- this.appendChild(this.moreMenu);
4839
- this.addEventListener("keydown", this.#handleTableHandlerKeydown);
4840
- }
4934
+ let cells = null;
4935
+ let headerState = null;
4841
4936
 
4842
- #showTableHandlerButtons() {
4843
- this.style.display = "flex";
4844
- this.#closeMoreMenu();
4937
+ if (childType === "row") {
4938
+ cells = this.currentRowCells;
4939
+ headerState = TableCellHeaderStates.ROW;
4940
+ } else if (childType === "column") {
4941
+ cells = this.currentColumnCells;
4942
+ headerState = TableCellHeaderStates.COLUMN;
4943
+ }
4845
4944
 
4846
- this.#updateRowColumnCount();
4847
- this.#setTableFocusState(true);
4945
+ if (!cells || cells.length === 0) return
4946
+
4947
+ this.editor.update(() => {
4948
+ const firstCell = $getTableCellNodeFromLexicalNode(cells[0]);
4949
+ if (!firstCell) return
4950
+
4951
+ const currentStyle = firstCell.getHeaderStyles();
4952
+ const newStyle = currentStyle ^ headerState;
4953
+
4954
+ cells.forEach(cell => {
4955
+ this.#setHeaderStyle(cell, newStyle, headerState);
4956
+ });
4957
+ });
4848
4958
  }
4849
4959
 
4850
- #hideTableHandlerButtons() {
4851
- this.style.display = "none";
4852
- this.#closeMoreMenu();
4960
+ #deleteTable() {
4961
+ this.#selectCellAtSelection();
4962
+ this.editor.dispatchCommand("deleteTable");
4963
+ }
4964
+
4965
+ #selectCellAtSelection() {
4966
+ this.editor.update(() => {
4967
+ const selection = $getSelection();
4968
+ if (!selection) return
4969
+
4970
+ const node = selection.getNodes()[0];
4853
4971
 
4854
- this.#setTableFocusState(false);
4855
- this.currentTableNode = null;
4972
+ $findCellNode(node)?.selectEnd();
4973
+ });
4856
4974
  }
4857
4975
 
4858
- #updateButtonsPosition(tableNode) {
4859
- const tableElement = this.#editor.getElementByKey(tableNode.getKey());
4860
- if (!tableElement) return
4976
+ #commandName(command) {
4977
+ const { action, childType, direction } = command;
4861
4978
 
4862
- const tableRect = tableElement.getBoundingClientRect();
4863
- const editorRect = this.#editorElement.getBoundingClientRect();
4979
+ const childTypeSuffix = upcaseFirst(childType);
4980
+ const directionSuffix = action == "insert" ? upcaseFirst(direction) : "";
4981
+ return `${action}Table${childTypeSuffix}${directionSuffix}`
4982
+ }
4864
4983
 
4865
- const relativeTop = tableRect.top - editorRect.top;
4866
- const relativeCenter = (tableRect.left + tableRect.right) / 2 - editorRect.left;
4867
- this.style.top = `${relativeTop}px`;
4868
- this.style.left = `${relativeCenter}px`;
4984
+ #setHeaderStyle(cell, newStyle, headerState) {
4985
+ const tableCellNode = $getTableCellNodeFromLexicalNode(cell);
4986
+ tableCellNode?.setHeaderStyles(newStyle, headerState);
4869
4987
  }
4870
4988
 
4871
- #updateRowColumnCount() {
4872
- if (!this.currentTableNode) return
4989
+ async #selectCellAtIndex(rowIndex, columnIndex) {
4990
+ // We wait for next frame, otherwise table operations might not have completed yet.
4991
+ await nextFrame();
4873
4992
 
4874
- const tableElement = $getElementForTableNode(this.#editor, this.currentTableNode);
4875
- if (!tableElement) return
4993
+ if (!this.currentTableNode) return
4876
4994
 
4877
- const rowCount = tableElement.rows;
4878
- const columnCount = tableElement.columns;
4995
+ const rows = this.tableRows;
4996
+ if (!rows) return
4879
4997
 
4880
- this.rowCount.textContent = `${rowCount} row${rowCount === 1 ? "" : "s"}`;
4881
- this.columnCount.textContent = `${columnCount} column${columnCount === 1 ? "" : "s"}`;
4882
- }
4998
+ const row = rows[rowIndex];
4999
+ if (!row) return
4883
5000
 
4884
- #createButton(icon, label, onClick) {
4885
- const button = createElement("button", {
4886
- className: "lexxy-table-control__button",
4887
- "aria-label": label,
4888
- type: "button"
5001
+ this.editor.update(() => {
5002
+ const cell = $getTableCellNodeFromLexicalNode(row.getChildAtIndex(columnIndex));
5003
+ cell?.selectEnd();
4889
5004
  });
4890
- button.tabIndex = -1;
4891
- button.innerHTML = `${icon} <span>${label}</span>`;
4892
- button.addEventListener("click", onClick.bind(this));
4893
-
4894
- return button
4895
5005
  }
4896
5006
 
4897
- #createRowButtonsContainer() {
4898
- const container = createElement("div", { className: "lexxy-table-control" });
5007
+ #selectNextBestCell(command, customIndex = null) {
5008
+ const { childType, direction } = command;
4899
5009
 
4900
- const plusButton = this.#createButton("+", "Add row", () => this.#insertTableRow("end"));
4901
- const minusButton = this.#createButton("−", "Remove row", () => this.#deleteTableRow("end"));
5010
+ let rowIndex = this.currentRowIndex;
5011
+ let columnIndex = customIndex !== null ? customIndex : this.currentColumnIndex;
4902
5012
 
4903
- this.rowCount = createElement("span");
4904
- this.rowCount.textContent = "_ rows";
5013
+ const deleteOffset = command.action === "delete" ? -1 : 0;
5014
+ const offset = direction === "after" ? 1 : deleteOffset;
4905
5015
 
4906
- container.appendChild(minusButton);
4907
- container.appendChild(this.rowCount);
4908
- container.appendChild(plusButton);
5016
+ if (childType === "row") {
5017
+ rowIndex += offset;
5018
+ } else if (childType === "column") {
5019
+ columnIndex += offset;
5020
+ }
4909
5021
 
4910
- return container
5022
+ this.#selectCellAtIndex(rowIndex, columnIndex);
4911
5023
  }
4912
5024
 
4913
- #createColumnButtonsContainer() {
4914
- const container = createElement("div", { className: "lexxy-table-control" });
5025
+ #selectNextRow() {
5026
+ const rows = this.tableRows;
5027
+ if (!rows) return
4915
5028
 
4916
- const plusButton = this.#createButton("+", "Add column", () => this.#insertTableColumn("end"));
4917
- const minusButton = this.#createButton("−", "Remove column", () => this.#deleteTableColumn("end"));
5029
+ const nextRow = rows.at(this.currentRowIndex + 1);
5030
+ if (!nextRow) return
4918
5031
 
4919
- this.columnCount = createElement("span");
4920
- this.columnCount.textContent = "_ columns";
5032
+ this.editor.update(() => {
5033
+ nextRow.getChildAtIndex(this.currentColumnIndex)?.selectEnd();
5034
+ });
5035
+ }
4921
5036
 
4922
- container.appendChild(minusButton);
4923
- container.appendChild(this.columnCount);
4924
- container.appendChild(plusButton);
5037
+ #selectPreviousCell() {
5038
+ const cell = this.currentCell;
5039
+ if (!cell) return
4925
5040
 
4926
- return container
5041
+ this.editor.update(() => {
5042
+ cell.selectPrevious();
5043
+ });
4927
5044
  }
4928
5045
 
4929
- #createMoreMenu() {
4930
- const container = createElement("details", {
4931
- className: "lexxy-table-control lexxy-table-control__more-menu"
4932
- });
4933
- container.setAttribute("name", "lexxy-dropdown");
5046
+ #insertRowAndSelectFirstCell() {
5047
+ this.executeTableCommand({ action: "insert", childType: "row", direction: "after" }, 0);
5048
+ }
4934
5049
 
4935
- container.tabIndex = -1;
5050
+ #deleteRowAndSelectLastCell() {
5051
+ this.executeTableCommand({ action: "delete", childType: "row" }, -1);
5052
+ }
4936
5053
 
4937
- const summary = createElement("summary", {}, "•••");
4938
- container.appendChild(summary);
5054
+ #deleteRowAndSelectNextNode() {
5055
+ const tableNode = this.currentTableNode;
5056
+ this.executeTableCommand({ action: "delete", childType: "row" });
4939
5057
 
4940
- const details = createElement("div", { className: "lexxy-table-control__more-menu-details" });
4941
- container.appendChild(details);
5058
+ this.editor.update(() => {
5059
+ const next = tableNode?.getNextSibling();
5060
+ if ($isParagraphNode(next)) {
5061
+ next.selectStart();
5062
+ } else {
5063
+ const newParagraph = $createParagraphNode();
5064
+ this.currentTableNode.insertAfter(newParagraph);
5065
+ newParagraph.selectStart();
5066
+ }
5067
+ });
5068
+ }
4942
5069
 
4943
- details.appendChild(this.#createRowSection());
4944
- details.appendChild(this.#createColumnSection());
4945
- details.appendChild(this.#createDeleteTableSection());
5070
+ #isCurrentCellEmpty() {
5071
+ if (!this.currentTableNode) return false
4946
5072
 
4947
- container.addEventListener("toggle", this.#handleMoreMenuToggle.bind(this));
5073
+ const cell = this.currentCell;
5074
+ if (!cell) return false
4948
5075
 
4949
- return container
5076
+ return cell.getTextContent().trim() === ""
4950
5077
  }
4951
5078
 
4952
- #createColumnSection() {
4953
- const columnSection = createElement("section", { className: "lexxy-table-control__more-menu-section" });
5079
+ #isCurrentRowLast() {
5080
+ if (!this.currentTableNode) return false
4954
5081
 
4955
- const columnButtons = [
4956
- { icon: this.#icon("add-column-before"), label: "Add column before", onClick: () => this.#insertTableColumn("left") },
4957
- { icon: this.#icon("add-column-after"), label: "Add column after", onClick: () => this.#insertTableColumn("right") },
4958
- { icon: this.#icon("remove-column"), label: "Remove column", onClick: this.#deleteTableColumn },
4959
- { icon: this.#icon("toggle-column-style"), label: "Toggle column style", onClick: this.#toggleColumnHeaderStyle },
4960
- ];
5082
+ const rows = this.tableRows;
5083
+ if (!rows) return false
4961
5084
 
4962
- columnButtons.forEach(button => {
4963
- const buttonElement = this.#createButton(button.icon, button.label, button.onClick);
4964
- columnSection.appendChild(buttonElement);
4965
- });
5085
+ return rows.length === this.currentRowIndex + 1
5086
+ }
4966
5087
 
4967
- return columnSection
5088
+ #isCurrentRowEmpty() {
5089
+ if (!this.currentTableNode) return false
5090
+
5091
+ const cells = this.currentRowCells;
5092
+ if (!cells) return false
5093
+
5094
+ return cells.every(cell => cell.getTextContent().trim() === "")
4968
5095
  }
4969
5096
 
4970
- #createRowSection() {
4971
- const rowSection = createElement("section", { className: "lexxy-table-control__more-menu-section" });
5097
+ #isFirstCellInRow() {
5098
+ if (!this.currentTableNode) return false
4972
5099
 
4973
- const rowButtons = [
4974
- { icon: this.#icon("add-row-above"), label: "Add row above", onClick: () => this.#insertTableRow("above") },
4975
- { icon: this.#icon("add-row-below"), label: "Add row below", onClick: () => this.#insertTableRow("below") },
4976
- { icon: this.#icon("remove-row"), label: "Remove row", onClick: this.#deleteTableRow },
4977
- { icon: this.#icon("toggle-row-style"), label: "Toggle row style", onClick: this.#toggleRowHeaderStyle }
4978
- ];
5100
+ const cells = this.currentRowCells;
5101
+ if (!cells) return false
4979
5102
 
4980
- rowButtons.forEach(button => {
4981
- const buttonElement = this.#createButton(button.icon, button.label, button.onClick);
4982
- rowSection.appendChild(buttonElement);
4983
- });
5103
+ return cells.indexOf(this.currentCell) === 0
5104
+ }
4984
5105
 
4985
- return rowSection
5106
+ #registerKeyHandlers() {
5107
+ // We can't prevent these externally using regular keydown because Lexical handles it first.
5108
+ this.unregisterBackspaceKeyHandler = this.editor.registerCommand(KEY_BACKSPACE_COMMAND, (event) => this.#handleBackspaceKey(event), COMMAND_PRIORITY_HIGH);
5109
+ this.unregisterEnterKeyHandler = this.editor.registerCommand(KEY_ENTER_COMMAND, (event) => this.#handleEnterKey(event), COMMAND_PRIORITY_HIGH);
4986
5110
  }
4987
5111
 
4988
- #createDeleteTableSection() {
4989
- const deleteSection = createElement("section", { className: "lexxy-table-control__more-menu-section" });
5112
+ #unregisterKeyHandlers() {
5113
+ this.unregisterBackspaceKeyHandler?.();
5114
+ this.unregisterEnterKeyHandler?.();
4990
5115
 
4991
- const deleteButton = { icon: this.#icon("delete-table"), label: "Delete table", onClick: this.#deleteTable };
5116
+ this.unregisterBackspaceKeyHandler = null;
5117
+ this.unregisterEnterKeyHandler = null;
5118
+ }
4992
5119
 
4993
- const buttonElement = this.#createButton(deleteButton.icon, deleteButton.label, deleteButton.onClick);
4994
- deleteSection.appendChild(buttonElement);
5120
+ #handleBackspaceKey(event) {
5121
+ if (!this.currentTableNode) return false
4995
5122
 
4996
- return deleteSection
5123
+ if (this.#isCurrentRowEmpty() && this.#isFirstCellInRow()) {
5124
+ event.preventDefault();
5125
+ this.#deleteRowAndSelectLastCell();
5126
+ return true
5127
+ }
5128
+
5129
+ if (this.#isCurrentCellEmpty() && !this.#isFirstCellInRow()) {
5130
+ event.preventDefault();
5131
+ this.#selectPreviousCell();
5132
+ return true
5133
+ }
5134
+
5135
+ return false
4997
5136
  }
4998
5137
 
4999
- #handleMoreMenuToggle() {
5000
- if (this.moreMenu.open) {
5001
- this.#setFocusStateOnSelectedCell();
5138
+ #handleEnterKey(event) {
5139
+ if ((event.ctrlKey || event.metaKey) || event.shiftKey || !this.currentTableNode) return false
5140
+
5141
+ if (this.selection.isInsideList || this.selection.isInsideCodeBlock) return false
5142
+
5143
+ event.preventDefault();
5144
+
5145
+ if (this.#isCurrentRowLast() && this.#isCurrentRowEmpty()) {
5146
+ this.#deleteRowAndSelectNextNode();
5147
+ } else if (this.#isCurrentRowLast()) {
5148
+ this.#insertRowAndSelectFirstCell();
5002
5149
  } else {
5003
- this.#removeFocusStateFromSelectedCell();
5150
+ this.#selectNextRow();
5004
5151
  }
5152
+
5153
+ return true
5005
5154
  }
5155
+ }
5006
5156
 
5007
- #closeMoreMenu() {
5008
- this.#removeFocusStateFromSelectedCell();
5009
- this.moreMenu.removeAttribute("open");
5157
+ var TableIcons = {
5158
+ "insert-row-before":
5159
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5160
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M7.86804e-07 15C8.29055e-07 15.8284 0.671574 16.5 1.5 16.5H15L15.1533 16.4922C15.8593 16.4205 16.4205 15.8593 16.4922 15.1533L16.5 15V4.5L16.4922 4.34668C16.4154 3.59028 15.7767 3 15 3H13.5L13.5 4.5H15V9H1.5L1.5 4.5L3 4.5V3H1.5C0.671574 3 1.20956e-06 3.67157 1.24577e-06 4.5L7.86804e-07 15ZM15 10.5V15H1.5L1.5 10.5H15Z"/>
5161
+ <path d="M4.5 4.5H7.5V7.5H9V4.5H12L12 3L9 3V6.55671e-08L7.5 0V3L4.5 3V4.5Z"/>
5162
+ </svg>`,
5163
+
5164
+ "insert-row-after":
5165
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5166
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M7.86804e-07 13.5C7.50592e-07 14.3284 0.671574 15 1.5 15H3V13.5H1.5L1.5 9L15 9V13.5H13.5V15H15C15.7767 15 16.4154 14.4097 16.4922 13.6533L16.5 13.5V3L16.4922 2.84668C16.4205 2.14069 15.8593 1.57949 15.1533 1.50781L15 1.5L1.5 1.5C0.671574 1.5 1.28803e-06 2.17157 1.24577e-06 3L7.86804e-07 13.5ZM15 3V7.5L1.5 7.5L1.5 3L15 3Z"/>
5167
+ <path d="M7.5 15V18H9V15H12V13.5H9V10.5H7.5V13.5H4.5V15H7.5Z"/>
5168
+ </svg>`,
5169
+
5170
+ "delete-row":
5171
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5172
+ <path d="M16.4922 12.1533C16.4154 12.9097 15.7767 13.5 15 13.5L12 13.5V12H15V6L1.5 6L1.5 12H4.5V13.5H1.5C0.723337 13.5 0.0846104 12.9097 0.00781328 12.1533L7.86804e-07 12L1.04907e-06 6C1.17362e-06 5.22334 0.590278 4.58461 1.34668 4.50781L1.5 4.5L15 4.5C15.8284 4.5 16.5 5.17157 16.5 6V12L16.4922 12.1533Z"/>
5173
+ <path d="M10.3711 15.9316L8.25 13.8096L6.12793 15.9316L5.06738 14.8711L7.18945 12.75L5.06738 10.6289L6.12793 9.56836L8.25 11.6895L10.3711 9.56836L11.4316 10.6289L9.31055 12.75L11.4316 14.8711L10.3711 15.9316Z"/>
5174
+ </svg>`,
5175
+
5176
+ "toggle-row":
5177
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5178
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M0.00781328 13.6533C0.0846108 14.4097 0.723337 15 1.5 15L15 15L15.1533 14.9922C15.8593 14.9205 16.4205 14.3593 16.4922 13.6533L16.5 13.5V4.5L16.4922 4.34668C16.4205 3.64069 15.8593 3.07949 15.1533 3.00781L15 3L1.5 3C0.671574 3 1.24863e-06 3.67157 1.18021e-06 4.5L7.86804e-07 13.5L0.00781328 13.6533ZM15 9V13.5L1.5 13.5L1.5 9L15 9Z"/>
5179
+ </svg>`,
5180
+
5181
+ "insert-column-before":
5182
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5183
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 0C3.67157 0 3 0.671573 3 1.5V3H4.5V1.5H9V15H4.5V13.5H3V15C3 15.7767 3.59028 16.4154 4.34668 16.4922L4.5 16.5H15L15.1533 16.4922C15.8593 16.4205 16.4205 15.8593 16.4922 15.1533L16.5 15V1.5C16.5 0.671573 15.8284 6.03989e-09 15 0H4.5ZM15 15H10.5V1.5H15V15Z"/>
5184
+ <path d="M3 7.5H0V9H3V12H4.5V9H7.5V7.5H4.5V4.5H3V7.5Z"/>
5185
+ </svg>`,
5186
+
5187
+ "insert-column-after":
5188
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5189
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M13.5 0C14.3284 0 15 0.671573 15 1.5V3H13.5V1.5H9V15H13.5V13.5H15V15C15 15.7767 14.4097 16.4154 13.6533 16.4922L13.5 16.5H3L2.84668 16.4922C2.14069 16.4205 1.57949 15.8593 1.50781 15.1533L1.5 15V1.5C1.5 0.671573 2.17157 6.03989e-09 3 0H13.5ZM3 15H7.5V1.5H3V15Z"/>
5190
+ <path d="M15 7.5H18V9H15V12H13.5V9H10.5V7.5H13.5V4.5H15V7.5Z"/>
5191
+ </svg>`,
5192
+
5193
+ "delete-column":
5194
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5195
+ <path d="M12.1533 0.0078125C12.9097 0.0846097 13.5 0.723336 13.5 1.5V4.5H12V1.5H6V15H12V12H13.5V15C13.5 15.7767 12.9097 16.4154 12.1533 16.4922L12 16.5H6C5.22334 16.5 4.58461 15.9097 4.50781 15.1533L4.5 15V1.5C4.5 0.671573 5.17157 2.41596e-08 6 0H12L12.1533 0.0078125Z"/>
5196
+ <path d="M15.9316 6.12891L13.8105 8.24902L15.9326 10.3711L14.8711 11.4316L12.75 9.31055L10.6289 11.4316L9.56738 10.3711L11.6885 8.24902L9.56836 6.12891L10.6289 5.06836L12.75 7.18848L14.8711 5.06836L15.9316 6.12891Z"/>
5197
+ </svg>`,
5198
+
5199
+ "toggle-column":
5200
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5201
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M13.6533 17.9922C14.4097 17.9154 15 17.2767 15 16.5L15 3L14.9922 2.84668C14.9205 2.14069 14.3593 1.57949 13.6533 1.50781L13.5 1.5L4.5 1.5L4.34668 1.50781C3.59028 1.58461 3 2.22334 3 3L3 16.5C3 17.2767 3.59028 17.9154 4.34668 17.9922L4.5 18L13.5 18L13.6533 17.9922ZM9 3L13.5 3L13.5 16.5L9 16.5L9 3Z" />
5202
+ </svg>`,
5203
+
5204
+ "delete-table":
5205
+ `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
5206
+ <path d="M18.2129 19.2305C18.0925 20.7933 16.7892 22 15.2217 22H7.77832C6.21084 22 4.90753 20.7933 4.78711 19.2305L4 9H19L18.2129 19.2305Z"/><path d="M13 2C14.1046 2 15 2.89543 15 4H19C19.5523 4 20 4.44772 20 5V6C20 6.55228 19.5523 7 19 7H4C3.44772 7 3 6.55228 3 6V5C3 4.44772 3.44772 4 4 4H8C8 2.89543 8.89543 2 10 2H13Z"/>
5207
+ </svg>`
5208
+ };
5209
+
5210
+ class TableTools extends HTMLElement {
5211
+ connectedCallback() {
5212
+ this.tableController = new TableController(this.#editorElement);
5213
+
5214
+ this.#setUpButtons();
5215
+ this.#monitorForTableSelection();
5216
+ this.#registerKeyboardShortcuts();
5010
5217
  }
5011
5218
 
5012
- #monitorForTableSelection() {
5013
- this.#editor.registerUpdateListener(() => {
5014
- this.#editor.getEditorState().read(() => {
5015
- const selection = $getSelection();
5016
- if (!$isRangeSelection(selection)) return
5219
+ disconnectedCallback() {
5220
+ this.#unregisterKeyboardShortcuts();
5017
5221
 
5018
- const anchorNode = selection.anchor.getNode();
5019
- const tableNode = $findTableNode(anchorNode);
5222
+ this.unregisterUpdateListener?.();
5223
+ this.unregisterUpdateListener = null;
5020
5224
 
5021
- if (tableNode) {
5022
- this.#tableCellWasSelected(tableNode);
5023
- } else {
5024
- this.#hideTableHandlerButtons();
5025
- }
5026
- });
5027
- });
5225
+ this.removeEventListener("keydown", this.#handleToolsKeydown);
5226
+
5227
+ this.tableController?.destroy();
5228
+ this.tableController = null;
5028
5229
  }
5029
5230
 
5030
- #setTableFocusState(focused) {
5031
- this.#editorElement.querySelector("div.node--selected:has(table)")?.classList.remove("node--selected");
5231
+ get #editor() {
5232
+ return this.#editorElement.editor
5233
+ }
5032
5234
 
5033
- if (focused && this.currentTableNode) {
5034
- const tableParent = this.#editor.getElementByKey(this.currentTableNode.getKey());
5035
- if (!tableParent) return
5036
- tableParent.classList.add("node--selected");
5037
- }
5235
+ get #editorElement() {
5236
+ return this.closest("lexxy-editor")
5038
5237
  }
5039
5238
 
5040
- #tableCellWasSelected(tableNode) {
5041
- this.currentTableNode = tableNode;
5042
- this.#updateButtonsPosition(tableNode);
5043
- this.#showTableHandlerButtons();
5239
+ get #tableToolsButtons() {
5240
+ return Array.from(this.querySelectorAll("button, details > summary"))
5044
5241
  }
5045
5242
 
5046
- #setFocusStateOnSelectedCell() {
5047
- this.#editor.getEditorState().read(() => {
5048
- const currentCell = this.#currentCell;
5049
- if (!currentCell) return
5243
+ #setUpButtons() {
5244
+ this.appendChild(this.#createRowButtonsContainer());
5245
+ this.appendChild(this.#createColumnButtonsContainer());
5050
5246
 
5051
- const cellElement = this.#editor.getElementByKey(currentCell.getKey());
5052
- if (!cellElement) return
5247
+ this.appendChild(this.#createDeleteTableButton());
5248
+ this.addEventListener("keydown", this.#handleToolsKeydown);
5249
+ }
5053
5250
 
5054
- cellElement.classList.add("table-cell--selected");
5055
- });
5251
+ #createButtonsContainer(childType, setCountProperty, moreMenu) {
5252
+ const container = createElement("div", { className: `lexxy-table-control lexxy-table-control--${childType}` });
5253
+
5254
+ const plusButton = this.#createButton(`Add ${childType}`, { action: "insert", childType, direction: "after" }, "+");
5255
+ const minusButton = this.#createButton(`Remove ${childType}`, { action: "delete", childType }, "−");
5256
+
5257
+ const dropdown = createElement("details", { className: "lexxy-table-control__more-menu" });
5258
+ dropdown.setAttribute("name", "lexxy-dropdown");
5259
+ dropdown.tabIndex = -1;
5260
+
5261
+ const count = createElement("summary", {}, `_ ${childType}s`);
5262
+ setCountProperty(count);
5263
+ dropdown.appendChild(count);
5264
+
5265
+ dropdown.appendChild(moreMenu);
5266
+
5267
+ container.appendChild(minusButton);
5268
+ container.appendChild(dropdown);
5269
+ container.appendChild(plusButton);
5270
+
5271
+ return container
5056
5272
  }
5057
5273
 
5058
- #removeFocusStateFromSelectedCell() {
5059
- this.#editorElement.querySelector(".table-cell--selected")?.classList.remove("table-cell--selected");
5274
+ #createRowButtonsContainer() {
5275
+ return this.#createButtonsContainer(
5276
+ "row",
5277
+ (count) => { this.rowCount = count; },
5278
+ this.#createMoreMenuSection("row")
5279
+ )
5060
5280
  }
5061
5281
 
5062
- #selectLastTableCell() {
5063
- if (!this.currentTableNode) return
5282
+ #createColumnButtonsContainer() {
5283
+ return this.#createButtonsContainer(
5284
+ "column",
5285
+ (count) => { this.columnCount = count; },
5286
+ this.#createMoreMenuSection("column")
5287
+ )
5288
+ }
5064
5289
 
5065
- const last = this.currentTableNode.getLastChild().getLastChild();
5066
- if (!$isTableCellNode(last)) return
5290
+ #createMoreMenuSection(childType) {
5291
+ const section = createElement("div", { className: "lexxy-table-control__more-menu-details" });
5292
+ const addBeforeButton = this.#createButton(`Add ${childType} before`, { action: "insert", childType, direction: "before" });
5293
+ const addAfterButton = this.#createButton(`Add ${childType} after`, { action: "insert", childType, direction: "after" });
5294
+ const toggleStyleButton = this.#createButton(`Toggle ${childType} style`, { action: "toggle", childType });
5295
+ const deleteButton = this.#createButton(`Remove ${childType}`, { action: "delete", childType });
5067
5296
 
5068
- last.selectEnd();
5297
+ section.appendChild(addBeforeButton);
5298
+ section.appendChild(addAfterButton);
5299
+ section.appendChild(toggleStyleButton);
5300
+ section.appendChild(deleteButton);
5301
+
5302
+ return section
5069
5303
  }
5070
5304
 
5071
- #deleteTable() {
5072
- this.#editor.dispatchCommand("deleteTable");
5305
+ #createDeleteTableButton() {
5306
+ const container = createElement("div", { className: "lexxy-table-control" });
5073
5307
 
5074
- this.#closeMoreMenu();
5075
- this.#updateRowColumnCount();
5076
- }
5308
+ const deleteTableButton = this.#createButton("Delete this table?", { action: "delete", childType: "table" });
5309
+ deleteTableButton.classList.add("lexxy-table-control__button--delete-table");
5310
+
5311
+ container.appendChild(deleteTableButton);
5312
+
5313
+ this.deleteContainer = container;
5077
5314
 
5078
- #insertTableRow(direction) {
5079
- this.#executeTableCommand("insert", "row", direction);
5315
+ return container
5080
5316
  }
5081
5317
 
5082
- #insertTableColumn(direction) {
5083
- this.#executeTableCommand("insert", "column", direction);
5318
+ #createButton(label, command = {}, icon = this.#icon(command)) {
5319
+ const button = createElement("button", {
5320
+ className: "lexxy-table-control__button",
5321
+ "aria-label": label,
5322
+ type: "button"
5323
+ });
5324
+ button.tabIndex = -1;
5325
+ button.innerHTML = `${icon} <span>${label}</span>`;
5326
+
5327
+ button.dataset.action = command.action;
5328
+ button.dataset.childType = command.childType;
5329
+ button.dataset.direction = command.direction;
5330
+
5331
+ button.addEventListener("click", () => this.#executeTableCommand(command));
5332
+
5333
+ button.addEventListener("mouseover", () => this.#handleCommandButtonHover());
5334
+ button.addEventListener("focus", () => this.#handleCommandButtonHover());
5335
+ button.addEventListener("mouseout", () => this.#handleCommandButtonHover());
5336
+
5337
+ return button
5084
5338
  }
5085
5339
 
5086
- #deleteTableRow(direction) {
5087
- this.#executeTableCommand("delete", "row", direction);
5340
+ #registerKeyboardShortcuts() {
5341
+ this.unregisterKeyboardShortcuts = this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleAccessibilityShortcutKey, COMMAND_PRIORITY_HIGH);
5088
5342
  }
5089
5343
 
5090
- #deleteTableColumn(direction) {
5091
- this.#executeTableCommand("delete", "column", direction);
5344
+ #unregisterKeyboardShortcuts() {
5345
+ this.unregisterKeyboardShortcuts?.();
5346
+ this.unregisterKeyboardShortcuts = null;
5092
5347
  }
5093
5348
 
5094
- #executeTableCommand(action = "insert", childType = "row", direction) {
5095
- this.#editor.update(() => {
5096
- const currentCell = this.#currentCell;
5097
- if (!currentCell) return
5349
+ #handleAccessibilityShortcutKey = (event) => {
5350
+ if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === "F10") {
5351
+ const firstButton = this.querySelector("button, [tabindex]:not([tabindex='-1'])");
5352
+ firstButton?.focus();
5353
+ }
5354
+ }
5098
5355
 
5099
- if (direction === "end") {
5100
- this.#selectLastTableCell();
5101
- }
5356
+ #handleToolsKeydown = (event) => {
5357
+ if (event.key === "Escape") {
5358
+ this.#handleEscapeKey();
5359
+ } else {
5360
+ handleRollingTabIndex(this.#tableToolsButtons, event);
5361
+ }
5362
+ }
5102
5363
 
5103
- this.#dispatchTableCommand(action, childType, direction);
5364
+ #handleEscapeKey() {
5365
+ const cell = this.tableController.currentCell;
5366
+ if (!cell) return
5104
5367
 
5105
- if (currentCell.isAttached()) {
5106
- currentCell.selectEnd();
5107
- }
5368
+ this.#editor.update(() => {
5369
+ cell.select();
5370
+ this.#editor.focus();
5108
5371
  });
5109
5372
 
5110
- this.#closeMoreMenu();
5111
- this.#updateRowColumnCount();
5373
+ this.#update();
5112
5374
  }
5113
5375
 
5114
- #dispatchTableCommand(action, childType, direction) {
5115
- switch (action) {
5116
- case "insert":
5117
- switch (childType) {
5118
- case "row":
5119
- if (direction === "above") {
5120
- this.#editor.dispatchCommand("insertTableRowAbove");
5121
- } else {
5122
- this.#editor.dispatchCommand("insertTableRowBelow");
5123
- }
5124
- break
5125
- case "column":
5126
- if (direction === "left") {
5127
- this.#editor.dispatchCommand("insertTableColumnBefore");
5128
- } else {
5129
- this.#editor.dispatchCommand("insertTableColumnAfter");
5130
- }
5131
- break
5132
- }
5376
+ async #handleCommandButtonHover() {
5377
+ await nextFrame();
5378
+
5379
+ this.#clearCellStyles();
5380
+
5381
+ const activeElement = this.querySelector("button:hover, button:focus");
5382
+ if (!activeElement) return
5383
+
5384
+ const command = {
5385
+ action: activeElement.dataset.action,
5386
+ childType: activeElement.dataset.childType,
5387
+ direction: activeElement.dataset.direction
5388
+ };
5389
+
5390
+ let cellsToHighlight = null;
5391
+
5392
+ switch (command.childType) {
5393
+ case "row":
5394
+ cellsToHighlight = this.tableController.currentRowCells;
5133
5395
  break
5134
- case "delete":
5135
- switch (childType) {
5136
- case "row":
5137
- this.#editor.dispatchCommand("deleteTableRow");
5138
- break
5139
- case "column":
5140
- this.#editor.dispatchCommand("deleteTableColumn");
5141
- break
5142
- }
5396
+ case "column":
5397
+ cellsToHighlight = this.tableController.currentColumnCells;
5398
+ break
5399
+ case "table":
5400
+ cellsToHighlight = this.tableController.tableRows;
5143
5401
  break
5144
5402
  }
5145
- }
5146
5403
 
5147
- #toggleRowHeaderStyle() {
5148
- this.#editor.update(() => {
5149
- const rows = this.currentTableNode.getChildren();
5404
+ if (!cellsToHighlight) return
5150
5405
 
5151
- const row = rows[this.#currentRow];
5152
- if (!row) return
5406
+ cellsToHighlight.forEach(cell => {
5407
+ const cellElement = this.#editor.getElementByKey(cell.getKey());
5408
+ if (!cellElement) return
5153
5409
 
5154
- const cells = row.getChildren();
5155
- const firstCell = $getTableCellNodeFromLexicalNode(cells[0]);
5156
- if (!firstCell) return
5410
+ cellElement.classList.toggle(theme.tableCellHighlight, true);
5411
+ Object.assign(cellElement.dataset, command);
5412
+ });
5413
+ }
5157
5414
 
5158
- const currentStyle = firstCell.getHeaderStyles();
5159
- const newStyle = currentStyle ^ TableCellHeaderStates.ROW;
5415
+ #monitorForTableSelection() {
5416
+ this.unregisterUpdateListener = this.#editor.registerUpdateListener(() => {
5417
+ this.tableController.updateSelectedTable();
5160
5418
 
5161
- cells.forEach(cell => {
5162
- this.#setHeaderStyle(cell, newStyle, TableCellHeaderStates.ROW);
5163
- });
5419
+ const tableNode = this.tableController.currentTableNode;
5420
+ if (tableNode) {
5421
+ this.#show();
5422
+ } else {
5423
+ this.#hide();
5424
+ }
5164
5425
  });
5165
5426
  }
5166
5427
 
5167
- #toggleColumnHeaderStyle() {
5168
- this.#editor.update(() => {
5169
- const rows = this.currentTableNode.getChildren();
5428
+ #executeTableCommand(command) {
5429
+ this.tableController.executeTableCommand(command);
5430
+ this.#update();
5431
+ }
5170
5432
 
5171
- const row = rows[this.#currentRow];
5172
- if (!row) return
5433
+ #show() {
5434
+ this.style.display = "flex";
5435
+ this.#update();
5436
+ }
5173
5437
 
5174
- const cells = row.getChildren();
5175
- const selectedCell = $getTableCellNodeFromLexicalNode(cells[this.#currentColumn]);
5176
- if (!selectedCell) return
5438
+ #hide() {
5439
+ this.style.display = "none";
5440
+ this.#clearCellStyles();
5441
+ }
5177
5442
 
5178
- const currentStyle = selectedCell.getHeaderStyles();
5179
- const newStyle = currentStyle ^ TableCellHeaderStates.COLUMN;
5443
+ #update() {
5444
+ this.#updateButtonsPosition();
5445
+ this.#updateRowColumnCount();
5446
+ this.#closeMoreMenu();
5447
+ this.#handleCommandButtonHover();
5448
+ }
5180
5449
 
5181
- rows.forEach(row => {
5182
- const cell = row.getChildren()[this.#currentColumn];
5183
- if (!cell) return
5184
- this.#setHeaderStyle(cell, newStyle, TableCellHeaderStates.COLUMN);
5185
- });
5186
- });
5450
+ #closeMoreMenu() {
5451
+ this.querySelector("details[open]")?.removeAttribute("open");
5187
5452
  }
5188
5453
 
5189
- #setHeaderStyle(cell, newStyle, headerState) {
5190
- const tableCellNode = $getTableCellNodeFromLexicalNode(cell);
5454
+ #updateButtonsPosition() {
5455
+ const tableNode = this.tableController.currentTableNode;
5456
+ if (!tableNode) return
5191
5457
 
5192
- if (tableCellNode) {
5193
- tableCellNode.setHeaderStyles(newStyle, headerState);
5194
- }
5458
+ const tableElement = this.#editor.getElementByKey(tableNode.getKey());
5459
+ if (!tableElement) return
5460
+
5461
+ const tableRect = tableElement.getBoundingClientRect();
5462
+ const editorRect = this.#editorElement.getBoundingClientRect();
5463
+
5464
+ const relativeTop = tableRect.top - editorRect.top;
5465
+ const relativeCenter = (tableRect.left + tableRect.right) / 2 - editorRect.left;
5466
+ this.style.top = `${relativeTop}px`;
5467
+ this.style.left = `${relativeCenter}px`;
5195
5468
  }
5196
5469
 
5197
- #icon(name) {
5198
- const icons =
5199
- {
5200
- "add-row-above":
5201
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5202
- <path d="M4 7L0 10V4L4 7ZM6.5 7.5H16.5V6.5H6.5V7.5ZM18 8C18 8.55228 17.5523 9 17 9H6C5.44772 9 5 8.55228 5 8V6C5 5.44772 5.44772 5 6 5H17C17.5523 5 18 5.44772 18 6V8Z"/><path d="M2 2C2 1.44772 2.44772 1 3 1H15C15.5523 1 16 1.44772 16 2C16 2.55228 15.5523 3 15 3H3C2.44772 3 2 2.55228 2 2Z"/><path d="M2 12C2 11.4477 2.44772 11 3 11H15C15.5523 11 16 11.4477 16 12C16 12.5523 15.5523 13 15 13H3C2.44772 13 2 12.5523 2 12Z"/><path d="M2 16C2 15.4477 2.44772 15 3 15H15C15.5523 15 16 15.4477 16 16C16 16.5523 15.5523 17 15 17H3C2.44772 17 2 16.5523 2 16Z"/>
5203
- </svg>`,
5470
+ #updateRowColumnCount() {
5471
+ const tableNode = this.tableController.currentTableNode;
5472
+ if (!tableNode) return
5473
+
5474
+ const tableElement = $getElementForTableNode(this.#editor, tableNode);
5475
+ if (!tableElement) return
5204
5476
 
5205
- "add-row-below":
5206
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5207
- <path d="M4 11L0 8V14L4 11ZM6.5 10.5H16.5V11.5H6.5V10.5ZM18 10C18 9.44772 17.5523 9 17 9H6C5.44772 9 5 9.44772 5 10V12C5 12.5523 5.44772 13 6 13H17C17.5523 13 18 12.5523 18 12V10Z"/><path d="M2 16C2 16.5523 2.44772 17 3 17H15C15.5523 17 16 16.5523 16 16C16 15.4477 15.5523 15 15 15H3C2.44772 15 2 15.4477 2 16Z"/><path d="M2 6C2 6.55228 2.44772 7 3 7H15C15.5523 7 16 6.55228 16 6C16 5.44772 15.5523 5 15 5H3C2.44772 5 2 5.44772 2 6Z"/><path d="M2 2C2 2.55228 2.44772 3 3 3H15C15.5523 3 16 2.55228 16 2C16 1.44772 15.5523 1 15 1H3C2.44772 1 2 1.44772 2 2Z"/>
5208
- </svg>`,
5477
+ const rowCount = tableElement.rows;
5478
+ const columnCount = tableElement.columns;
5209
5479
 
5210
- "remove-row":
5211
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5212
- <path d="M17.9951 10.1025C17.9438 10.6067 17.5177 11 17 11H12.4922L13.9922 9.5H16.5V5.5L1.5 5.5L1.5 9.5H4.00586L5.50586 11H1L0.897461 10.9951C0.427034 10.9472 0.0527828 10.573 0.00488281 10.1025L0 10L1.78814e-07 5C2.61831e-07 4.48232 0.393332 4.05621 0.897461 4.00488L1 4L17 4C17.5523 4 18 4.44772 18 5V10L17.9951 10.1025Z"/><path d="M11.2969 15.0146L8.99902 12.7168L6.7002 15.0146L5.63965 13.9541L7.93848 11.6562L5.63965 9.3584L6.7002 8.29785L8.99902 10.5957L11.2969 8.29785L12.3574 9.3584L10.0596 11.6562L12.3574 13.9541L11.2969 15.0146Z"/>
5213
- </svg>`,
5480
+ this.rowCount.textContent = `${rowCount} row${rowCount === 1 ? "" : "s"}`;
5481
+ this.columnCount.textContent = `${columnCount} column${columnCount === 1 ? "" : "s"}`;
5482
+ }
5214
5483
 
5215
- "toggle-row-style":
5216
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5217
- <path d="M1 2C1 1.44772 1.44772 1 2 1H7C7.55228 1 8 1.44772 8 2V7C8 7.55228 7.55228 8 7 8H2C1.44772 8 1 7.55228 1 7V2Z"/><path d="M2.5 15.5H6.5V11.5H2.5V15.5ZM8 16C8 16.5177 7.60667 16.9438 7.10254 16.9951L7 17H2L1.89746 16.9951C1.42703 16.9472 1.05278 16.573 1.00488 16.1025L1 16V11C1 10.4477 1.44772 10 2 10H7C7.55228 10 8 10.4477 8 11V16Z"/><path d="M10 2C10 1.44772 10.4477 1 11 1H16C16.5523 1 17 1.44772 17 2V7C17 7.55228 16.5523 8 16 8H11C10.4477 8 10 7.55228 10 7V2Z"/><path d="M11.5 15.5H15.5V11.5H11.5V15.5ZM17 16C17 16.5177 16.6067 16.9438 16.1025 16.9951L16 17H11L10.8975 16.9951C10.427 16.9472 10.0528 16.573 10.0049 16.1025L10 16V11C10 10.4477 10.4477 10 11 10H16C16.5523 10 17 10.4477 17 11V16Z"/>
5218
- </svg>`,
5484
+ #setTableCellFocus() {
5485
+ const cell = this.tableController.currentCell;
5486
+ if (!cell) return
5219
5487
 
5220
- "add-column-before":
5221
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5222
- <path d="M7 4L10 2.62268e-07L4 0L7 4ZM7.5 6.5L7.5 16.5H6.5L6.5 6.5H7.5ZM8 18C8.55228 18 9 17.5523 9 17V6C9 5.44772 8.55229 5 8 5H6C5.44772 5 5 5.44772 5 6L5 17C5 17.5523 5.44772 18 6 18H8Z"/><path d="M2 2C1.44772 2 1 2.44772 1 3L1 15C1 15.5523 1.44772 16 2 16C2.55228 16 3 15.5523 3 15L3 3C3 2.44772 2.55229 2 2 2Z"/><path d="M12 2C11.4477 2 11 2.44772 11 3L11 15C11 15.5523 11.4477 16 12 16C12.5523 16 13 15.5523 13 15L13 3C13 2.44772 12.5523 2 12 2Z"/><path d="M16 2C15.4477 2 15 2.44772 15 3L15 15C15 15.5523 15.4477 16 16 16C16.5523 16 17 15.5523 17 15V3C17 2.44772 16.5523 2 16 2Z"/>
5223
- </svg>`,
5488
+ const cellElement = this.#editor.getElementByKey(cell.getKey());
5489
+ if (!cellElement) return
5224
5490
 
5225
- "add-column-after":
5226
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5227
- <path d="M11 4L8 2.62268e-07L14 0L11 4ZM10.5 6.5V16.5H11.5V6.5H10.5ZM10 18C9.44772 18 9 17.5523 9 17V6C9 5.44772 9.44772 5 10 5H12C12.5523 5 13 5.44772 13 6V17C13 17.5523 12.5523 18 12 18H10Z"/><path d="M16 2C16.5523 2 17 2.44772 17 3L17 15C17 15.5523 16.5523 16 16 16C15.4477 16 15 15.5523 15 15V3C15 2.44772 15.4477 2 16 2Z"/><path d="M6 2C6.55228 2 7 2.44772 7 3L7 15C7 15.5523 6.55228 16 6 16C5.44772 16 5 15.5523 5 15L5 3C5 2.44772 5.44771 2 6 2Z"/><path d="M2 2C2.55228 2 3 2.44772 3 3L3 15C3 15.5523 2.55228 16 2 16C1.44772 16 1 15.5523 1 15V3C1 2.44772 1.44771 2 2 2Z"/>
5228
- </svg>`,
5491
+ cellElement.classList.add(theme.tableCellFocus);
5492
+ }
5229
5493
 
5230
- "remove-column":
5231
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5232
- <path d="M10.1025 0.00488281C10.6067 0.0562145 11 0.482323 11 1V5.50781L9.5 4.00781V1.5H5.5V16.5H9.5V13.9941L11 12.4941V17L10.9951 17.1025C10.9472 17.573 10.573 17.9472 10.1025 17.9951L10 18H5C4.48232 18 4.05621 17.6067 4.00488 17.1025L4 17V1C4 0.447715 4.44772 1.61064e-08 5 0H10L10.1025 0.00488281Z"/><path d="M12.7169 8.99999L15.015 11.2981L13.9543 12.3588L11.6562 10.0607L9.35815 12.3588L8.29749 11.2981L10.5956 8.99999L8.29749 6.7019L9.35815 5.64124L11.6562 7.93933L13.9543 5.64124L15.015 6.7019L12.7169 8.99999Z"/>
5233
- </svg>`,
5494
+ #clearCellStyles() {
5495
+ this.#editorElement.querySelectorAll(`.${theme.tableCellFocus}`)?.forEach(cell => {
5496
+ cell.classList.remove(theme.tableCellFocus);
5497
+ });
5234
5498
 
5235
- "toggle-column-style":
5236
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5237
- <path d="M1 2C1 1.44772 1.44772 1 2 1H7C7.55228 1 8 1.44772 8 2V7C8 7.55228 7.55228 8 7 8H2C1.44772 8 1 7.55228 1 7V2Z"/><path d="M1 11C1 10.4477 1.44772 10 2 10H7C7.55228 10 8 10.4477 8 11V16C8 16.5523 7.55228 17 7 17H2C1.44772 17 1 16.5523 1 16V11Z"/><path d="M11.5 6.5H15.5V2.5H11.5V6.5ZM17 7C17 7.51768 16.6067 7.94379 16.1025 7.99512L16 8H11L10.8975 7.99512C10.427 7.94722 10.0528 7.57297 10.0049 7.10254L10 7V2C10 1.44772 10.4477 1 11 1H16C16.5523 1 17 1.44772 17 2V7Z"/><path d="M11.5 15.5H15.5V11.5H11.5V15.5ZM17 16C17 16.5177 16.6067 16.9438 16.1025 16.9951L16 17H11L10.8975 16.9951C10.427 16.9472 10.0528 16.573 10.0049 16.1025L10 16V11C10 10.4477 10.4477 10 11 10H16C16.5523 10 17 10.4477 17 11V16Z"/>
5238
- </svg>`,
5499
+ this.#editorElement.querySelectorAll(`.${theme.tableCellHighlight}`)?.forEach(cell => {
5500
+ cell.classList.remove(theme.tableCellHighlight);
5501
+ cell.removeAttribute("data-action");
5502
+ cell.removeAttribute("data-child-type");
5503
+ cell.removeAttribute("data-direction");
5504
+ });
5239
5505
 
5240
- "delete-table":
5241
- `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
5242
- <path d="M18.2129 19.2305C18.0925 20.7933 16.7892 22 15.2217 22H7.77832C6.21084 22 4.90753 20.7933 4.78711 19.2305L4 9H19L18.2129 19.2305Z"/><path d="M13 2C14.1046 2 15 2.89543 15 4H19C19.5523 4 20 4.44772 20 5V6C20 6.55228 19.5523 7 19 7H4C3.44772 7 3 6.55228 3 6V5C3 4.44772 3.44772 4 4 4H8C8 2.89543 8.89543 2 10 2H13Z"/>
5243
- </svg>`
5244
- };
5506
+ this.#setTableCellFocus();
5507
+ }
5245
5508
 
5246
- return icons[name]
5509
+ #icon(command) {
5510
+ const { action, childType } = command;
5511
+ const direction = (action == "insert" ? command.direction : null);
5512
+ const iconId = [ action, childType, direction ].filter(Boolean).join("-");
5513
+ return TableIcons[iconId]
5247
5514
  }
5248
5515
  }
5249
5516
 
5250
- customElements.define("lexxy-table-handler", TableHandler);
5517
+ customElements.define("lexxy-table-tools", TableTools);
5251
5518
 
5252
5519
  class BaseSource {
5253
5520
  // Template method to override
@@ -5451,25 +5718,30 @@ class LexicalPromptElement extends HTMLElement {
5451
5718
  }
5452
5719
 
5453
5720
  #addTriggerListener() {
5454
- const unregister = this.#editor.registerUpdateListener(() => {
5455
- this.#editor.read(() => {
5721
+ const unregister = this.#editor.registerUpdateListener(({ editorState }) => {
5722
+ editorState.read(() => {
5456
5723
  const { node, offset } = this.#selection.selectedNodeWithOffset();
5457
5724
  if (!node) return
5458
5725
 
5459
- if ($isTextNode(node) && offset > 0) {
5726
+ if ($isTextNode(node)) {
5460
5727
  const fullText = node.getTextContent();
5461
- const charBeforeCursor = fullText[offset - 1];
5728
+ const triggerLength = this.trigger.length;
5729
+
5730
+ // Check if we have enough characters for the trigger
5731
+ if (offset >= triggerLength) {
5732
+ const textBeforeCursor = fullText.slice(offset - triggerLength, offset);
5462
5733
 
5463
- // Check if trigger is at the start of the text node (new line case) or preceded by space or newline
5464
- if (charBeforeCursor === this.trigger) {
5465
- const isAtStart = offset === 1;
5734
+ // Check if trigger is at the start of the text node (new line case) or preceded by space or newline
5735
+ if (textBeforeCursor === this.trigger) {
5736
+ const isAtStart = offset === triggerLength;
5466
5737
 
5467
- const charBeforeTrigger = offset > 1 ? fullText[offset - 2] : null;
5468
- const isPrecededBySpaceOrNewline = charBeforeTrigger === " " || charBeforeTrigger === "\n";
5738
+ const charBeforeTrigger = offset > triggerLength ? fullText[offset - triggerLength - 1] : null;
5739
+ const isPrecededBySpaceOrNewline = charBeforeTrigger === " " || charBeforeTrigger === "\n";
5469
5740
 
5470
- if (isAtStart || isPrecededBySpaceOrNewline) {
5471
- unregister();
5472
- this.#showPopover();
5741
+ if (isAtStart || isPrecededBySpaceOrNewline) {
5742
+ unregister();
5743
+ this.#showPopover();
5744
+ }
5473
5745
  }
5474
5746
  }
5475
5747
  }
@@ -5489,9 +5761,10 @@ class LexicalPromptElement extends HTMLElement {
5489
5761
  const fullText = node.getTextContent();
5490
5762
  const textBeforeCursor = fullText.slice(0, offset);
5491
5763
  const lastTriggerIndex = textBeforeCursor.lastIndexOf(this.trigger);
5764
+ const triggerEndIndex = lastTriggerIndex + this.trigger.length - 1;
5492
5765
 
5493
- // If trigger is not found, or cursor is at or before the trigger position, hide popover
5494
- if (lastTriggerIndex === -1 || offset <= lastTriggerIndex) {
5766
+ // If trigger is not found, or cursor is at or before the trigger end position, hide popover
5767
+ if (lastTriggerIndex === -1 || offset <= triggerEndIndex) {
5495
5768
  this.#hidePopover();
5496
5769
  }
5497
5770
  } else {
@@ -5935,8 +6208,10 @@ class CodeLanguagePicker extends HTMLElement {
5935
6208
  const codeRect = codeElement.getBoundingClientRect();
5936
6209
  const editorRect = this.editorElement.getBoundingClientRect();
5937
6210
  const relativeTop = codeRect.top - editorRect.top;
6211
+ const relativeRight = editorRect.right - codeRect.right;
5938
6212
 
5939
6213
  this.style.top = `${relativeTop}px`;
6214
+ this.style.right = `${relativeRight}px`;
5940
6215
  }
5941
6216
 
5942
6217
  #showLanguagePicker() {