@37signals/lexxy 0.1.24-beta → 0.1.26-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,15 +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_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
19
  import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
19
20
  import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
20
21
  import { createEmptyHistoryState, registerHistory } from '@lexical/history';
21
22
  import { DirectUpload } from '@rails/activestorage';
23
+ import { $getNearestNodeOfType } from '@lexical/utils';
22
24
  import { marked } from 'marked';
23
25
 
24
26
  // Configure Prism for manual highlighting mode
@@ -27,7 +29,7 @@ window.Prism = window.Prism || {};
27
29
  window.Prism.manual = true;
28
30
 
29
31
  const ALLOWED_HTML_TAGS = [ "a", "action-text-attachment", "b", "blockquote", "br", "code", "em",
30
- "figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "mark", "ol", "p", "pre", "q", "s", "strong", "ul" ];
32
+ "figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "mark", "ol", "p", "pre", "q", "s", "strong", "ul", "table", "tbody", "tr", "th", "td" ];
31
33
 
32
34
  const ALLOWED_HTML_ATTRIBUTES = [ "alt", "caption", "class", "content", "content-type", "contenteditable",
33
35
  "data-direct-upload-id", "data-sgid", "filename", "filesize", "height", "href", "presentation",
@@ -144,6 +146,93 @@ function hasHighlightStyles(cssOrStyles) {
144
146
  return !!(styles.color || styles["background-color"])
145
147
  }
146
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
+
147
236
  class LexicalToolbarElement extends HTMLElement {
148
237
  static observedAttributes = [ "connected" ]
149
238
 
@@ -155,17 +244,14 @@ class LexicalToolbarElement extends HTMLElement {
155
244
 
156
245
  connectedCallback() {
157
246
  requestAnimationFrame(() => this.#refreshToolbarOverflow());
158
-
159
- this._resizeObserver = new ResizeObserver(() => this.#refreshToolbarOverflow());
160
- this._resizeObserver.observe(this);
247
+ this.setAttribute("role", "toolbar");
248
+ this.#installResizeObserver();
161
249
  }
162
250
 
163
251
  disconnectedCallback() {
164
- if (this._resizeObserver) {
165
- this._resizeObserver.disconnect();
166
- this._resizeObserver = null;
167
- }
252
+ this.#uninstallResizeObserver();
168
253
  this.#unbindHotkeys();
254
+ this.#unbindFocusListeners();
169
255
  }
170
256
 
171
257
  attributeChangedCallback(name, oldValue, newValue) {
@@ -179,11 +265,12 @@ class LexicalToolbarElement extends HTMLElement {
179
265
  this.editor = editorElement.editor;
180
266
  this.#bindButtons();
181
267
  this.#bindHotkeys();
182
- this.#setTabIndexValues();
268
+ this.#resetTabIndexValues();
183
269
  this.#setItemPositionValues();
184
270
  this.#monitorSelectionChanges();
185
271
  this.#monitorHistoryChanges();
186
272
  this.#refreshToolbarOverflow();
273
+ this.#bindFocusListeners();
187
274
 
188
275
  this.toggleAttribute("connected", true);
189
276
  }
@@ -193,24 +280,39 @@ class LexicalToolbarElement extends HTMLElement {
193
280
  this.connectedCallback();
194
281
  }
195
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
+
196
295
  #bindButtons() {
197
296
  this.addEventListener("click", this.#handleButtonClicked.bind(this));
198
297
  }
199
298
 
200
- #handleButtonClicked({ target }) {
201
- this.#handleTargetClicked(target, "[data-command]", this.#dispatchButtonCommand.bind(this));
299
+ #handleButtonClicked(event) {
300
+ this.#handleTargetClicked(event, "[data-command]", this.#dispatchButtonCommand.bind(this));
202
301
  }
203
302
 
204
- #handleTargetClicked(target, selector, callback) {
205
- const button = target.closest(selector);
303
+ #handleTargetClicked(event, selector, callback) {
304
+ const button = event.target.closest(selector);
206
305
  if (button) {
207
- callback(button);
306
+ callback(event, button);
208
307
  }
209
308
  }
210
309
 
211
- #dispatchButtonCommand(button) {
212
- const { command, payload } = button.dataset;
213
- 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 } );
214
316
  }
215
317
 
216
318
  #bindHotkeys() {
@@ -245,9 +347,38 @@ class LexicalToolbarElement extends HTMLElement {
245
347
  return [ ...modifiers, pressedKey ].join("+")
246
348
  }
247
349
 
248
- #setTabIndexValues() {
249
- this.#buttons.forEach((button) => {
250
- 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;
251
382
  });
252
383
  }
253
384
 
@@ -294,6 +425,7 @@ class LexicalToolbarElement extends HTMLElement {
294
425
  const isInCode = $isCodeNode(topLevelElement) || selection.hasFormat("code");
295
426
  const isInList = this.#isInList(anchorNode);
296
427
  const listType = getListType(anchorNode);
428
+ const isInTable = $getTableCellNodeFromLexicalNode(anchorNode) !== null;
297
429
 
298
430
  this.#setButtonPressed("bold", isBold);
299
431
  this.#setButtonPressed("italic", isItalic);
@@ -305,6 +437,7 @@ class LexicalToolbarElement extends HTMLElement {
305
437
  this.#setButtonPressed("code", isInCode);
306
438
  this.#setButtonPressed("unordered-list", isInList && listType === "bullet");
307
439
  this.#setButtonPressed("ordered-list", isInList && listType === "number");
440
+ this.#setButtonPressed("table", isInTable);
308
441
 
309
442
  this.#updateUndoRedoButtonStates();
310
443
  }
@@ -355,6 +488,7 @@ class LexicalToolbarElement extends HTMLElement {
355
488
 
356
489
  const isOverflowing = this.#overflowMenu.children.length > 0;
357
490
  this.toggleAttribute("overflowing", isOverflowing);
491
+ this.#overflowMenu.toggleAttribute("disabled", !isOverflowing);
358
492
  }
359
493
 
360
494
  #compactMenu() {
@@ -406,6 +540,10 @@ class LexicalToolbarElement extends HTMLElement {
406
540
  return Array.from(this.querySelectorAll(":scope > button"))
407
541
  }
408
542
 
543
+ get #focusableItems() {
544
+ return Array.from(this.querySelectorAll(":scope button, :scope > details > summary"))
545
+ }
546
+
409
547
  get #toolbarItems() {
410
548
  return Array.from(this.querySelectorAll(":scope > *:not(.lexxy-editor__toolbar-overflow)"))
411
549
  }
@@ -476,6 +614,10 @@ class LexicalToolbarElement extends HTMLElement {
476
614
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M16 8a2 2 0 110 4 2 2 0 010-4z""/><path d="M22 2a1 1 0 011 1v18a1 1 0 01-1 1H2a1 1 0 01-1-1V3a1 1 0 011-1h20zM3 18.714L9 11l5.25 6.75L17 15l4 4V4H3v14.714z"/></svg>
477
615
  </button>
478
616
 
617
+ <button class="lexxy-editor__toolbar-button" type="button" name="table" data-command="insertTable" title="Insert a table">
618
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M20.2041 2.01074C21.2128 2.113 22 2.96435 22 4V20L21.9893 20.2041C21.8938 21.1457 21.1457 21.8938 20.2041 21.9893L20 22H4C2.96435 22 2.113 21.2128 2.01074 20.2041L2 20V4C2 2.89543 2.89543 2 4 2H20L20.2041 2.01074ZM4 13V20H11V13H4ZM13 13V20H20V13H13ZM4 11H11V4H4V11ZM13 11H20V4H13V11Z"/></svg>
619
+ </button>
620
+
479
621
  <button class="lexxy-editor__toolbar-button" type="button" name="divider" data-command="insertHorizontalDivider" title="Insert a divider">
480
622
  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 12C0 11.4477 0.447715 11 1 11H23C23.5523 11 24 11.4477 24 12C24 12.5523 23.5523 13 23 13H1C0.447716 13 0 12.5523 0 12Z"/><path d="M4 5C4 3.89543 4.89543 3 6 3H18C19.1046 3 20 3.89543 20 5C20 6.10457 19.1046 7 18 7H6C4.89543 7 4 6.10457 4 5Z"/><path d="M4 19C4 17.8954 4.89543 17 6 17H18C19.1046 17 20 17.8954 20 19C20 20.1046 19.1046 21 18 21H6C4.89543 21 4 20.1046 4 19Z"/></svg>
481
623
  </button>
@@ -508,6 +650,10 @@ var theme = {
508
650
  underline: "lexxy-content__underline",
509
651
  highlight: "lexxy-content__highlight"
510
652
  },
653
+ tableCellHeader: "lexxy-content__table-cell--header",
654
+ tableCellSelected: "lexxy-content__table-cell--selected",
655
+ tableSelection: "lexxy-content__table--selection",
656
+ tableScrollableWrapper: "lexxy-content__table-wrapper",
511
657
  list: {
512
658
  nested: {
513
659
  listitem: "lexxy-nested-listitem",
@@ -567,7 +713,7 @@ var theme = {
567
713
  }
568
714
  };
569
715
 
570
- function createElement(name, properties) {
716
+ function createElement(name, properties, content = "") {
571
717
  const element = document.createElement(name);
572
718
  for (const [ key, value ] of Object.entries(properties || {})) {
573
719
  if (key in element) {
@@ -576,6 +722,9 @@ function createElement(name, properties) {
576
722
  element.setAttribute(key, value);
577
723
  }
578
724
  }
725
+ if (content) {
726
+ element.innerHTML = content;
727
+ }
579
728
  return element
580
729
  }
581
730
 
@@ -729,6 +878,10 @@ class ActionTextAttachmentNode extends DecoratorNode {
729
878
  return true
730
879
  }
731
880
 
881
+ getTextContent() {
882
+ return `[${ this.caption || this.fileName }]\n\n`
883
+ }
884
+
732
885
  isInline() {
733
886
  return false
734
887
  }
@@ -1095,6 +1248,10 @@ class HorizontalDividerNode extends DecoratorNode {
1095
1248
  return true
1096
1249
  }
1097
1250
 
1251
+ getTextContent() {
1252
+ return "┄\n\n"
1253
+ }
1254
+
1098
1255
  isInline() {
1099
1256
  return false
1100
1257
  }
@@ -1131,6 +1288,16 @@ const COMMANDS = [
1131
1288
  "insertCodeBlock",
1132
1289
  "insertHorizontalDivider",
1133
1290
  "uploadAttachments",
1291
+
1292
+ "insertTable",
1293
+ "insertTableRowAbove",
1294
+ "insertTableRowBelow",
1295
+ "insertTableColumnAfter",
1296
+ "insertTableColumnBefore",
1297
+ "deleteTableRow",
1298
+ "deleteTableColumn",
1299
+ "deleteTable",
1300
+
1134
1301
  "undo",
1135
1302
  "redo"
1136
1303
  ];
@@ -1293,6 +1460,45 @@ class CommandDispatcher {
1293
1460
  setTimeout(() => input.remove(), 1000);
1294
1461
  }
1295
1462
 
1463
+ dispatchInsertTable() {
1464
+ this.editor.dispatchCommand(INSERT_TABLE_COMMAND, { "rows": 3, "columns": 3, "includeHeaders": true });
1465
+ }
1466
+
1467
+ dispatchInsertTableRowBelow() {
1468
+ $insertTableRowAtSelection(true);
1469
+ }
1470
+
1471
+ dispatchInsertTableRowAbove() {
1472
+ $insertTableRowAtSelection(false);
1473
+ }
1474
+
1475
+ dispatchInsertTableColumnAfter() {
1476
+ $insertTableColumnAtSelection(true);
1477
+ }
1478
+
1479
+ dispatchInsertTableColumnBefore() {
1480
+ $insertTableColumnAtSelection(false);
1481
+ }
1482
+
1483
+ dispatchDeleteTableRow() {
1484
+ $deleteTableRowAtSelection();
1485
+ }
1486
+
1487
+ dispatchDeleteTableColumn() {
1488
+ $deleteTableColumnAtSelection();
1489
+ }
1490
+
1491
+ dispatchDeleteTable() {
1492
+ this.editor.update(() => {
1493
+ const selection = $getSelection();
1494
+ if (!$isRangeSelection(selection)) return
1495
+
1496
+ const anchorNode = selection.anchor.getNode();
1497
+ const tableNode = $findTableNode(anchorNode);
1498
+ tableNode.remove();
1499
+ });
1500
+ }
1501
+
1296
1502
  dispatchUndo() {
1297
1503
  this.editor.dispatchCommand(UNDO_COMMAND, undefined);
1298
1504
  }
@@ -1315,7 +1521,7 @@ class CommandDispatcher {
1315
1521
  }
1316
1522
 
1317
1523
  #registerKeyboardCommands() {
1318
- 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);
1319
1525
  }
1320
1526
 
1321
1527
  #registerDragAndDropHandlers() {
@@ -1365,18 +1571,28 @@ class CommandDispatcher {
1365
1571
  this.editor.focus();
1366
1572
  }
1367
1573
 
1368
- #handleListIndentation(event) {
1574
+ #handleTabKey(event) {
1369
1575
  if (this.selection.isInsideList) {
1370
- event.preventDefault();
1371
- if (event.shiftKey) {
1372
- return this.editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined)
1373
- } else {
1374
- return this.editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)
1375
- }
1576
+ return this.#handleTabForList(event)
1577
+ } else if (this.selection.isInsideCodeBlock) {
1578
+ return this.#handleTabForCode()
1376
1579
  }
1377
1580
  return false
1378
1581
  }
1379
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
+
1380
1596
  // Not using TOGGLE_LINK_COMMAND because it's not handled unless you use React/LinkPlugin
1381
1597
  #toggleLink(url) {
1382
1598
  this.editor.update(() => {
@@ -1553,6 +1769,29 @@ class Selection {
1553
1769
  return getNearestListItemNode(anchorNode) !== null
1554
1770
  }
1555
1771
 
1772
+ get isIndentedList() {
1773
+ const selection = $getSelection();
1774
+ if (!$isRangeSelection(selection)) return false
1775
+
1776
+ const nodes = selection.getNodes();
1777
+ for (const node of nodes) {
1778
+ const closestListNode = $getNearestNodeOfType(node, ListNode);
1779
+ if (closestListNode && $getListDepth(closestListNode) > 1) {
1780
+ return true
1781
+ }
1782
+ }
1783
+
1784
+ return false
1785
+ }
1786
+
1787
+ get isInsideCodeBlock() {
1788
+ const selection = $getSelection();
1789
+ if (!$isRangeSelection(selection)) return false
1790
+
1791
+ const anchorNode = selection.anchor.getNode();
1792
+ return $getNearestNodeOfType(anchorNode, CodeNode) !== null
1793
+ }
1794
+
1556
1795
  get nodeAfterCursor() {
1557
1796
  const { anchorNode, offset } = this.#getCollapsedSelectionData();
1558
1797
  if (!anchorNode) return null
@@ -2132,6 +2371,10 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
2132
2371
  return true
2133
2372
  }
2134
2373
 
2374
+ getTextContent() {
2375
+ return this.createDOM().textContent.trim() || `[${this.contentType}]`
2376
+ }
2377
+
2135
2378
  isInline() {
2136
2379
  return true
2137
2380
  }
@@ -3432,7 +3675,7 @@ function applyLanguage(conversionOutput, element) {
3432
3675
 
3433
3676
  class LexicalEditorElement extends HTMLElement {
3434
3677
  static formAssociated = true
3435
- static debug = true
3678
+ static debug = false
3436
3679
  static commands = [ "bold", "italic", "strikethrough" ]
3437
3680
 
3438
3681
  static observedAttributes = [ "connected", "required" ]
@@ -3507,6 +3750,18 @@ class LexicalEditorElement extends HTMLElement {
3507
3750
  return this.dataset.blobUrlTemplate
3508
3751
  }
3509
3752
 
3753
+ get isEmpty() {
3754
+ return [ "<p><br></p>", "<p></p>", "" ].includes(this.value.trim())
3755
+ }
3756
+
3757
+ get isBlank() {
3758
+ return this.isEmpty || this.toString().match(/^\s*$/g) !== null
3759
+ }
3760
+
3761
+ get hasOpenPrompt() {
3762
+ return this.querySelector(".lexxy-prompt-menu.lexxy-prompt-menu--visible") !== null
3763
+ }
3764
+
3510
3765
  get isSingleLineMode() {
3511
3766
  return this.hasAttribute("single-line")
3512
3767
  }
@@ -3550,6 +3805,16 @@ class LexicalEditorElement extends HTMLElement {
3550
3805
  });
3551
3806
  }
3552
3807
 
3808
+ toString() {
3809
+ if (!this.cachedStringValue) {
3810
+ this.editor?.getEditorState().read(() => {
3811
+ this.cachedStringValue = $getRoot().getTextContent();
3812
+ });
3813
+ }
3814
+
3815
+ return this.cachedStringValue
3816
+ }
3817
+
3553
3818
  #parseHtmlIntoLexicalNodes(html) {
3554
3819
  if (!html) html = "<p></p>";
3555
3820
  const nodes = $generateNodesFromDOM(this.editor, parseHtml(`<div>${html}</div>`));
@@ -3572,6 +3837,7 @@ class LexicalEditorElement extends HTMLElement {
3572
3837
  this.#listenForInvalidatedNodes();
3573
3838
  this.#handleEnter();
3574
3839
  this.#handleFocus();
3840
+ this.#handleTables();
3575
3841
  this.#attachDebugHooks();
3576
3842
  this.#attachToolbar();
3577
3843
  this.#loadInitialValue();
@@ -3608,6 +3874,9 @@ class LexicalEditorElement extends HTMLElement {
3608
3874
  LinkNode,
3609
3875
  AutoLinkNode,
3610
3876
  HorizontalDividerNode,
3877
+ TableNode,
3878
+ TableCellNode,
3879
+ TableRowNode,
3611
3880
 
3612
3881
  CustomActionTextAttachmentNode,
3613
3882
  ];
@@ -3655,7 +3924,7 @@ class LexicalEditorElement extends HTMLElement {
3655
3924
 
3656
3925
  this.internals.setFormValue(html);
3657
3926
  this._internalFormValue = html;
3658
- this.#validationTextArea.value = this.#isEmpty ? "" : html;
3927
+ this.#validationTextArea.value = this.isEmpty ? "" : html;
3659
3928
 
3660
3929
  if (changed) {
3661
3930
  dispatch(this, "lexxy:change");
@@ -3681,13 +3950,18 @@ class LexicalEditorElement extends HTMLElement {
3681
3950
 
3682
3951
  #synchronizeWithChanges() {
3683
3952
  this.#addUnregisterHandler(this.editor.registerUpdateListener(({ editorState }) => {
3684
- this.cachedValue = null;
3953
+ this.#clearCachedValues();
3685
3954
  this.#internalFormValue = this.value;
3686
3955
  this.#toggleEmptyStatus();
3687
3956
  this.#setValidity();
3688
3957
  }));
3689
3958
  }
3690
3959
 
3960
+ #clearCachedValues() {
3961
+ this.cachedValue = null;
3962
+ this.cachedStringValue = null;
3963
+ }
3964
+
3691
3965
  #addUnregisterHandler(handler) {
3692
3966
  this.unregisterHandlers = this.unregisterHandlers || [];
3693
3967
  this.unregisterHandlers.push(handler);
@@ -3705,13 +3979,21 @@ class LexicalEditorElement extends HTMLElement {
3705
3979
  this.historyState = createEmptyHistoryState();
3706
3980
  registerHistory(this.editor, this.historyState, 20);
3707
3981
  registerList(this.editor);
3982
+ this.#registerTableComponents();
3708
3983
  this.#registerCodeHiglightingComponents();
3709
3984
  registerMarkdownShortcuts(this.editor, TRANSFORMERS);
3710
3985
  }
3711
3986
 
3987
+ #registerTableComponents() {
3988
+ registerTablePlugin(this.editor);
3989
+ this.tableHandler = createElement("lexxy-table-handler");
3990
+ this.append(this.tableHandler);
3991
+ }
3992
+
3712
3993
  #registerCodeHiglightingComponents() {
3713
3994
  registerCodeHighlighting(this.editor);
3714
- this.append(createElement("lexxy-code-language-picker"));
3995
+ this.codeLanguagePicker = createElement("lexxy-code-language-picker");
3996
+ this.append(this.codeLanguagePicker);
3715
3997
  }
3716
3998
 
3717
3999
  #listenForInvalidatedNodes() {
@@ -3760,12 +4042,18 @@ class LexicalEditorElement extends HTMLElement {
3760
4042
  this.editor.registerCommand(FOCUS_COMMAND, () => { dispatch(this, "lexxy:focus"); }, COMMAND_PRIORITY_NORMAL);
3761
4043
  }
3762
4044
 
4045
+ #handleTables() {
4046
+ this.removeTableSelectionObserver = registerTableSelectionObserver(this.editor, true);
4047
+ setScrollableTablesActive(this.editor, true);
4048
+ }
4049
+
3763
4050
  #attachDebugHooks() {
3764
4051
  if (!LexicalEditorElement.debug) return
3765
4052
 
3766
4053
  this.#addUnregisterHandler(this.editor.registerUpdateListener(({ editorState }) => {
3767
4054
  editorState.read(() => {
3768
- console.debug("HTML: ", this.value);
4055
+ console.debug("HTML: ", this.value, "String:", this.toString());
4056
+ console.debug("empty", this.isEmpty, "blank", this.isBlank);
3769
4057
  });
3770
4058
  }));
3771
4059
  }
@@ -3794,11 +4082,7 @@ class LexicalEditorElement extends HTMLElement {
3794
4082
  }
3795
4083
 
3796
4084
  #toggleEmptyStatus() {
3797
- this.classList.toggle("lexxy-editor--empty", this.#isEmpty);
3798
- }
3799
-
3800
- get #isEmpty() {
3801
- return [ "<p><br></p>", "<p></p>", "" ].includes(this.value.trim())
4085
+ this.classList.toggle("lexxy-editor--empty", this.isEmpty);
3802
4086
  }
3803
4087
 
3804
4088
  #setValidity() {
@@ -3825,6 +4109,16 @@ class LexicalEditorElement extends HTMLElement {
3825
4109
  this.toolbar = null;
3826
4110
  }
3827
4111
 
4112
+ if (this.codeLanguagePicker) {
4113
+ this.codeLanguagePicker.remove();
4114
+ this.codeLanguagePicker = null;
4115
+ }
4116
+
4117
+ if (this.tableHandler) {
4118
+ this.tableHandler.remove();
4119
+ this.tableHandler = null;
4120
+ }
4121
+
3828
4122
  this.selection = null;
3829
4123
 
3830
4124
  document.removeEventListener("turbo:before-cache", this.#handleTurboBeforeCache);
@@ -3845,8 +4139,6 @@ class ToolbarDropdown extends HTMLElement {
3845
4139
 
3846
4140
  this.container.addEventListener("toggle", this.#handleToggle.bind(this));
3847
4141
  this.container.addEventListener("keydown", this.#handleKeyDown.bind(this));
3848
-
3849
- this.#setTabIndexValues();
3850
4142
  }
3851
4143
 
3852
4144
  disconnectedCallback() {
@@ -3874,14 +4166,14 @@ class ToolbarDropdown extends HTMLElement {
3874
4166
  }
3875
4167
  }
3876
4168
 
3877
- #handleOpen(trigger) {
3878
- this.trigger = trigger;
4169
+ #handleOpen() {
3879
4170
  this.#interactiveElements[0].focus();
3880
4171
  this.#setupClickOutsideHandler();
4172
+
4173
+ this.#resetTabIndexValues();
3881
4174
  }
3882
4175
 
3883
4176
  #handleClose() {
3884
- this.trigger = null;
3885
4177
  this.#removeClickOutsideHandler();
3886
4178
  this.editor.focus();
3887
4179
  }
@@ -3911,16 +4203,20 @@ class ToolbarDropdown extends HTMLElement {
3911
4203
  }
3912
4204
  }
3913
4205
 
3914
- async #setTabIndexValues() {
4206
+ async #resetTabIndexValues() {
3915
4207
  await nextFrame();
3916
- this.#interactiveElements.forEach((element) => {
3917
- element.setAttribute("tabindex", 0);
4208
+ this.#buttons.forEach((element, index) => {
4209
+ element.setAttribute("tabindex", index === 0 ? 0 : "-1");
3918
4210
  });
3919
4211
  }
3920
4212
 
3921
4213
  get #interactiveElements() {
3922
4214
  return Array.from(this.querySelectorAll("button, input"))
3923
4215
  }
4216
+
4217
+ get #buttons() {
4218
+ return Array.from(this.querySelectorAll("button"))
4219
+ }
3924
4220
  }
3925
4221
 
3926
4222
  class LinkDropdown extends ToolbarDropdown {
@@ -4077,6 +4373,507 @@ class HighlightDropdown extends ToolbarDropdown {
4077
4373
 
4078
4374
  customElements.define("lexxy-highlight-dropdown", HighlightDropdown);
4079
4375
 
4376
+ class TableHandler extends HTMLElement {
4377
+ connectedCallback() {
4378
+ this.#setUpButtons();
4379
+ this.#monitorForTableSelection();
4380
+ this.#registerKeyboardShortcuts();
4381
+ }
4382
+
4383
+ disconnectedCallback() {
4384
+ this.#unregisterKeyboardShortcuts();
4385
+ }
4386
+
4387
+ get #editor() {
4388
+ return this.#editorElement.editor
4389
+ }
4390
+
4391
+ get #editorElement() {
4392
+ return this.closest("lexxy-editor")
4393
+ }
4394
+
4395
+ get #currentCell() {
4396
+ const selection = $getSelection();
4397
+ if (!$isRangeSelection(selection)) return null
4398
+
4399
+ const anchorNode = selection.anchor.getNode();
4400
+ return $getTableCellNodeFromLexicalNode(anchorNode)
4401
+ }
4402
+
4403
+ get #currentRow() {
4404
+ const currentCell = this.#currentCell;
4405
+ if (!currentCell) return 0
4406
+ return $getTableRowIndexFromTableCellNode(currentCell)
4407
+ }
4408
+
4409
+ get #currentColumn() {
4410
+ const currentCell = this.#currentCell;
4411
+ if (!currentCell) return 0
4412
+ return $getTableColumnIndexFromTableCellNode(currentCell)
4413
+ }
4414
+
4415
+ get #tableHandlerButtons() {
4416
+ return Array.from(this.buttonsContainer.querySelectorAll("button, details > summary"))
4417
+ }
4418
+
4419
+ #registerKeyboardShortcuts() {
4420
+ this.unregisterKeyboardShortcuts = this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleKeyDown, COMMAND_PRIORITY_HIGH);
4421
+ }
4422
+
4423
+ #unregisterKeyboardShortcuts() {
4424
+ this.unregisterKeyboardShortcuts();
4425
+ }
4426
+
4427
+ #handleKeyDown = (event) => {
4428
+ if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === "F10") {
4429
+ const firstButton = this.buttonsContainer?.querySelector("button, [tabindex]:not([tabindex='-1'])");
4430
+ this.#setFocusStateOnSelectedCell();
4431
+ firstButton?.focus();
4432
+ } else if (event.key === "Escape") {
4433
+ this.#editor.getEditorState().read(() => {
4434
+ const cell = this.#currentCell;
4435
+ if (!cell) return
4436
+
4437
+ this.#editor.update(() => {
4438
+ cell.select();
4439
+ });
4440
+ });
4441
+ this.#closeMoreMenu();
4442
+ }
4443
+ }
4444
+
4445
+ #handleTableHandlerKeydown = (event) => {
4446
+ if (event.key === "Escape") {
4447
+ this.#editor.focus();
4448
+ } else {
4449
+ handleRollingTabIndex(this.#tableHandlerButtons, event);
4450
+ }
4451
+ }
4452
+
4453
+ #setUpButtons() {
4454
+ this.buttonsContainer = createElement("div", {
4455
+ className: "lexxy-table-handle-buttons"
4456
+ });
4457
+
4458
+ this.buttonsContainer.appendChild(this.#createRowButtonsContainer());
4459
+ this.buttonsContainer.appendChild(this.#createColumnButtonsContainer());
4460
+
4461
+ this.moreMenu = this.#createMoreMenu();
4462
+ this.buttonsContainer.appendChild(this.moreMenu);
4463
+ this.buttonsContainer.addEventListener("keydown", this.#handleTableHandlerKeydown);
4464
+
4465
+ this.#editorElement.appendChild(this.buttonsContainer);
4466
+ }
4467
+
4468
+ #showTableHandlerButtons() {
4469
+ this.buttonsContainer.style.display = "flex";
4470
+ this.#closeMoreMenu();
4471
+
4472
+ this.#updateRowColumnCount();
4473
+ this.#setTableFocusState(true);
4474
+ }
4475
+
4476
+ #hideTableHandlerButtons() {
4477
+ this.buttonsContainer.style.display = "none";
4478
+ this.#closeMoreMenu();
4479
+
4480
+ this.#setTableFocusState(false);
4481
+ this.currentTableNode = null;
4482
+ }
4483
+
4484
+ #updateButtonsPosition(tableNode) {
4485
+ const tableElement = this.#editor.getElementByKey(tableNode.getKey());
4486
+ if (!tableElement) return
4487
+
4488
+ const tableRect = tableElement.getBoundingClientRect();
4489
+ const editorRect = this.#editorElement.getBoundingClientRect();
4490
+
4491
+ const relativeTop = tableRect.top - editorRect.top;
4492
+ const relativeCenter = (tableRect.left + tableRect.right) / 2 - editorRect.left;
4493
+ this.buttonsContainer.style.top = `${relativeTop}px`;
4494
+ this.buttonsContainer.style.left = `${relativeCenter}px`;
4495
+ }
4496
+
4497
+ #updateRowColumnCount() {
4498
+ if (!this.currentTableNode) return
4499
+
4500
+ const tableElement = $getElementForTableNode(this.#editor, this.currentTableNode);
4501
+ if (!tableElement) return
4502
+
4503
+ const rowCount = tableElement.rows;
4504
+ const columnCount = tableElement.columns;
4505
+
4506
+ this.rowCount.textContent = `${rowCount} row${rowCount === 1 ? "" : "s"}`;
4507
+ this.columnCount.textContent = `${columnCount} column${columnCount === 1 ? "" : "s"}`;
4508
+ }
4509
+
4510
+ #createButton(icon, label, onClick) {
4511
+ const button = createElement("button", {
4512
+ className: "lexxy-table-control__button",
4513
+ "aria-label": label,
4514
+ type: "button"
4515
+ });
4516
+ button.tabIndex = -1;
4517
+ button.innerHTML = `${icon} <span>${label}</span>`;
4518
+ button.addEventListener("click", onClick.bind(this));
4519
+
4520
+ return button
4521
+ }
4522
+
4523
+ #createRowButtonsContainer() {
4524
+ const container = createElement("div", { className: "lexxy-table-control" });
4525
+
4526
+ const plusButton = this.#createButton("+", "Add row", () => this.#insertTableRow("end"));
4527
+ const minusButton = this.#createButton("−", "Remove row", () => this.#deleteTableRow("end"));
4528
+
4529
+ this.rowCount = createElement("span");
4530
+ this.rowCount.textContent = "_ rows";
4531
+
4532
+ container.appendChild(minusButton);
4533
+ container.appendChild(this.rowCount);
4534
+ container.appendChild(plusButton);
4535
+
4536
+ return container
4537
+ }
4538
+
4539
+ #createColumnButtonsContainer() {
4540
+ const container = createElement("div", { className: "lexxy-table-control" });
4541
+
4542
+ const plusButton = this.#createButton("+", "Add column", () => this.#insertTableColumn("end"));
4543
+ const minusButton = this.#createButton("−", "Remove column", () => this.#deleteTableColumn("end"));
4544
+
4545
+ this.columnCount = createElement("span");
4546
+ this.columnCount.textContent = "_ columns";
4547
+
4548
+ container.appendChild(minusButton);
4549
+ container.appendChild(this.columnCount);
4550
+ container.appendChild(plusButton);
4551
+
4552
+ return container
4553
+ }
4554
+
4555
+ #createMoreMenu() {
4556
+ const container = createElement("details", {
4557
+ className: "lexxy-table-control lexxy-table-control__more-menu"
4558
+ });
4559
+
4560
+ container.tabIndex = -1;
4561
+
4562
+ const summary = createElement("summary", {}, "•••");
4563
+ container.appendChild(summary);
4564
+
4565
+ const details = createElement("div", { className: "lexxy-table-control__more-menu-details" });
4566
+ container.appendChild(details);
4567
+
4568
+ details.appendChild(this.#createRowSection());
4569
+ details.appendChild(this.#createColumnSection());
4570
+ details.appendChild(this.#createDeleteTableSection());
4571
+
4572
+ container.addEventListener("toggle", this.#handleMoreMenuToggle.bind(this));
4573
+
4574
+ return container
4575
+ }
4576
+
4577
+ #createColumnSection() {
4578
+ const columnSection = createElement("section", { className: "lexxy-table-control__more-menu-section" });
4579
+
4580
+ const columnButtons = [
4581
+ { icon: this.#icon("add-column-before"), label: "Add column before", onClick: () => this.#insertTableColumn("left") },
4582
+ { icon: this.#icon("add-column-after"), label: "Add column after", onClick: () => this.#insertTableColumn("right") },
4583
+ { icon: this.#icon("remove-column"), label: "Remove column", onClick: this.#deleteTableColumn },
4584
+ { icon: this.#icon("toggle-column-style"), label: "Toggle column style", onClick: this.#toggleColumnHeaderStyle },
4585
+ ];
4586
+
4587
+ columnButtons.forEach(button => {
4588
+ const buttonElement = this.#createButton(button.icon, button.label, button.onClick);
4589
+ columnSection.appendChild(buttonElement);
4590
+ });
4591
+
4592
+ return columnSection
4593
+ }
4594
+
4595
+ #createRowSection() {
4596
+ const rowSection = createElement("section", { className: "lexxy-table-control__more-menu-section" });
4597
+
4598
+ const rowButtons = [
4599
+ { icon: this.#icon("add-row-above"), label: "Add row above", onClick: () => this.#insertTableRow("above") },
4600
+ { icon: this.#icon("add-row-below"), label: "Add row below", onClick: () => this.#insertTableRow("below") },
4601
+ { icon: this.#icon("remove-row"), label: "Remove row", onClick: this.#deleteTableRow },
4602
+ { icon: this.#icon("toggle-row-style"), label: "Toggle row style", onClick: this.#toggleRowHeaderStyle }
4603
+ ];
4604
+
4605
+ rowButtons.forEach(button => {
4606
+ const buttonElement = this.#createButton(button.icon, button.label, button.onClick);
4607
+ rowSection.appendChild(buttonElement);
4608
+ });
4609
+
4610
+ return rowSection
4611
+ }
4612
+
4613
+ #createDeleteTableSection() {
4614
+ const deleteSection = createElement("section", { className: "lexxy-table-control__more-menu-section" });
4615
+
4616
+ const deleteButton = { icon: this.#icon("delete-table"), label: "Delete table", onClick: this.#deleteTable };
4617
+
4618
+ const buttonElement = this.#createButton(deleteButton.icon, deleteButton.label, deleteButton.onClick);
4619
+ deleteSection.appendChild(buttonElement);
4620
+
4621
+ return deleteSection
4622
+ }
4623
+
4624
+ #handleMoreMenuToggle() {
4625
+ if (this.moreMenu.open) {
4626
+ this.#setFocusStateOnSelectedCell();
4627
+ } else {
4628
+ this.#removeFocusStateFromSelectedCell();
4629
+ }
4630
+ }
4631
+
4632
+ #closeMoreMenu() {
4633
+ this.#removeFocusStateFromSelectedCell();
4634
+ this.moreMenu.removeAttribute("open");
4635
+ }
4636
+
4637
+ #monitorForTableSelection() {
4638
+ this.#editor.registerUpdateListener(() => {
4639
+ this.#editor.getEditorState().read(() => {
4640
+ const selection = $getSelection();
4641
+ if (!$isRangeSelection(selection)) return
4642
+
4643
+ const anchorNode = selection.anchor.getNode();
4644
+ const tableNode = $findTableNode(anchorNode);
4645
+
4646
+ if (tableNode) {
4647
+ this.#tableCellWasSelected(tableNode);
4648
+ } else {
4649
+ this.#hideTableHandlerButtons();
4650
+ }
4651
+ });
4652
+ });
4653
+ }
4654
+
4655
+ #setTableFocusState(focused) {
4656
+ this.#editorElement.querySelector("div.node--selected:has(table)")?.classList.remove("node--selected");
4657
+
4658
+ if (focused && this.currentTableNode) {
4659
+ const tableParent = this.#editor.getElementByKey(this.currentTableNode.getKey());
4660
+ if (!tableParent) return
4661
+ tableParent.classList.add("node--selected");
4662
+ }
4663
+ }
4664
+
4665
+ #tableCellWasSelected(tableNode) {
4666
+ this.currentTableNode = tableNode;
4667
+ this.#updateButtonsPosition(tableNode);
4668
+ this.#showTableHandlerButtons();
4669
+ }
4670
+
4671
+ #setFocusStateOnSelectedCell() {
4672
+ this.#editor.getEditorState().read(() => {
4673
+ const currentCell = this.#currentCell;
4674
+ if (!currentCell) return
4675
+
4676
+ const cellElement = this.#editor.getElementByKey(currentCell.getKey());
4677
+ if (!cellElement) return
4678
+
4679
+ cellElement.classList.add("table-cell--selected");
4680
+ });
4681
+ }
4682
+
4683
+ #removeFocusStateFromSelectedCell() {
4684
+ this.#editorElement.querySelector(".table-cell--selected")?.classList.remove("table-cell--selected");
4685
+ }
4686
+
4687
+ #selectLastTableCell() {
4688
+ if (!this.currentTableNode) return
4689
+
4690
+ const last = this.currentTableNode.getLastChild().getLastChild();
4691
+ if (!$isTableCellNode(last)) return
4692
+
4693
+ last.selectEnd();
4694
+ }
4695
+
4696
+ #deleteTable() {
4697
+ this.#editor.dispatchCommand("deleteTable");
4698
+
4699
+ this.#closeMoreMenu();
4700
+ this.#updateRowColumnCount();
4701
+ }
4702
+
4703
+ #insertTableRow(direction) {
4704
+ this.#executeTableCommand("insert", "row", direction);
4705
+ }
4706
+
4707
+ #insertTableColumn(direction) {
4708
+ this.#executeTableCommand("insert", "column", direction);
4709
+ }
4710
+
4711
+ #deleteTableRow(direction) {
4712
+ this.#executeTableCommand("delete", "row", direction);
4713
+ }
4714
+
4715
+ #deleteTableColumn(direction) {
4716
+ this.#executeTableCommand("delete", "column", direction);
4717
+ }
4718
+
4719
+ #executeTableCommand(action = "insert", childType = "row", direction) {
4720
+ this.#editor.update(() => {
4721
+ const currentCell = this.#currentCell;
4722
+ if (!currentCell) return
4723
+
4724
+ if (direction === "end") {
4725
+ this.#selectLastTableCell();
4726
+ }
4727
+
4728
+ this.#dispatchTableCommand(action, childType, direction);
4729
+
4730
+ if (currentCell.isAttached()) {
4731
+ currentCell.selectEnd();
4732
+ }
4733
+ });
4734
+
4735
+ this.#closeMoreMenu();
4736
+ this.#updateRowColumnCount();
4737
+ }
4738
+
4739
+ #dispatchTableCommand(action, childType, direction) {
4740
+ switch (action) {
4741
+ case "insert":
4742
+ switch (childType) {
4743
+ case "row":
4744
+ if (direction === "above") {
4745
+ this.#editor.dispatchCommand("insertTableRowAbove");
4746
+ } else {
4747
+ this.#editor.dispatchCommand("insertTableRowBelow");
4748
+ }
4749
+ break
4750
+ case "column":
4751
+ if (direction === "left") {
4752
+ this.#editor.dispatchCommand("insertTableColumnBefore");
4753
+ } else {
4754
+ this.#editor.dispatchCommand("insertTableColumnAfter");
4755
+ }
4756
+ break
4757
+ }
4758
+ break
4759
+ case "delete":
4760
+ switch (childType) {
4761
+ case "row":
4762
+ this.#editor.dispatchCommand("deleteTableRow");
4763
+ break
4764
+ case "column":
4765
+ this.#editor.dispatchCommand("deleteTableColumn");
4766
+ break
4767
+ }
4768
+ break
4769
+ }
4770
+ }
4771
+
4772
+ #toggleRowHeaderStyle() {
4773
+ this.#editor.update(() => {
4774
+ const rows = this.currentTableNode.getChildren();
4775
+
4776
+ const row = rows[this.#currentRow];
4777
+ if (!row) return
4778
+
4779
+ const cells = row.getChildren();
4780
+ const firstCell = $getTableCellNodeFromLexicalNode(cells[0]);
4781
+ if (!firstCell) return
4782
+
4783
+ const currentStyle = firstCell.getHeaderStyles();
4784
+ const newStyle = currentStyle ^ TableCellHeaderStates.ROW;
4785
+
4786
+ cells.forEach(cell => {
4787
+ this.#setHeaderStyle(cell, newStyle, TableCellHeaderStates.ROW);
4788
+ });
4789
+ });
4790
+ }
4791
+
4792
+ #toggleColumnHeaderStyle() {
4793
+ this.#editor.update(() => {
4794
+ const rows = this.currentTableNode.getChildren();
4795
+
4796
+ const row = rows[this.#currentRow];
4797
+ if (!row) return
4798
+
4799
+ const cells = row.getChildren();
4800
+ const selectedCell = $getTableCellNodeFromLexicalNode(cells[this.#currentColumn]);
4801
+ if (!selectedCell) return
4802
+
4803
+ const currentStyle = selectedCell.getHeaderStyles();
4804
+ const newStyle = currentStyle ^ TableCellHeaderStates.COLUMN;
4805
+
4806
+ rows.forEach(row => {
4807
+ const cell = row.getChildren()[this.#currentColumn];
4808
+ if (!cell) return
4809
+ this.#setHeaderStyle(cell, newStyle, TableCellHeaderStates.COLUMN);
4810
+ });
4811
+ });
4812
+ }
4813
+
4814
+ #setHeaderStyle(cell, newStyle, headerState) {
4815
+ const tableCellNode = $getTableCellNodeFromLexicalNode(cell);
4816
+
4817
+ if (tableCellNode) {
4818
+ tableCellNode.setHeaderStyles(newStyle, headerState);
4819
+ }
4820
+ }
4821
+
4822
+ #icon(name) {
4823
+ const icons =
4824
+ {
4825
+ "add-row-above":
4826
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4827
+ <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"/>
4828
+ </svg>`,
4829
+
4830
+ "add-row-below":
4831
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4832
+ <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"/>
4833
+ </svg>`,
4834
+
4835
+ "remove-row":
4836
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4837
+ <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"/>
4838
+ </svg>`,
4839
+
4840
+ "toggle-row-style":
4841
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4842
+ <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"/>
4843
+ </svg>`,
4844
+
4845
+ "add-column-before":
4846
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4847
+ <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"/>
4848
+ </svg>`,
4849
+
4850
+ "add-column-after":
4851
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4852
+ <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"/>
4853
+ </svg>`,
4854
+
4855
+ "remove-column":
4856
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4857
+ <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"/>
4858
+ </svg>`,
4859
+
4860
+ "toggle-column-style":
4861
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4862
+ <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"/>
4863
+ </svg>`,
4864
+
4865
+ "delete-table":
4866
+ `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
4867
+ <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"/>
4868
+ </svg>`
4869
+ };
4870
+
4871
+ return icons[name]
4872
+ }
4873
+ }
4874
+
4875
+ customElements.define("lexxy-table-handler", TableHandler);
4876
+
4080
4877
  class BaseSource {
4081
4878
  // Template method to override
4082
4879
  async buildListItems(filter = "") {