@37signals/lexxy 0.1.24-beta → 0.1.25-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,11 +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, $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';
13
+ import { $isTextNode, TextNode, $isRangeSelection, $getSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, SELECTION_CHANGE_COMMAND, $createNodeSelection, $setSelection, $createParagraphNode, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, $insertNodes, $createLineBreakNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, SKIP_DOM_SELECTION_TAG, createEditor, BLUR_COMMAND, FOCUS_COMMAND, KEY_DOWN_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
14
14
  import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $createListNode, ListNode, 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';
@@ -27,7 +28,7 @@ window.Prism = window.Prism || {};
27
28
  window.Prism.manual = true;
28
29
 
29
30
  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" ];
31
+ "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
32
 
32
33
  const ALLOWED_HTML_ATTRIBUTES = [ "alt", "caption", "class", "content", "content-type", "contenteditable",
33
34
  "data-direct-upload-id", "data-sgid", "filename", "filesize", "height", "href", "presentation",
@@ -294,6 +295,7 @@ class LexicalToolbarElement extends HTMLElement {
294
295
  const isInCode = $isCodeNode(topLevelElement) || selection.hasFormat("code");
295
296
  const isInList = this.#isInList(anchorNode);
296
297
  const listType = getListType(anchorNode);
298
+ const isInTable = $getTableCellNodeFromLexicalNode(anchorNode) !== null;
297
299
 
298
300
  this.#setButtonPressed("bold", isBold);
299
301
  this.#setButtonPressed("italic", isItalic);
@@ -305,6 +307,7 @@ class LexicalToolbarElement extends HTMLElement {
305
307
  this.#setButtonPressed("code", isInCode);
306
308
  this.#setButtonPressed("unordered-list", isInList && listType === "bullet");
307
309
  this.#setButtonPressed("ordered-list", isInList && listType === "number");
310
+ this.#setButtonPressed("table", isInTable);
308
311
 
309
312
  this.#updateUndoRedoButtonStates();
310
313
  }
@@ -476,6 +479,10 @@ class LexicalToolbarElement extends HTMLElement {
476
479
  <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
480
  </button>
478
481
 
482
+ <button class="lexxy-editor__toolbar-button" type="button" name="table" data-command="insertTable" title="Insert a table">
483
+ <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>
484
+ </button>
485
+
479
486
  <button class="lexxy-editor__toolbar-button" type="button" name="divider" data-command="insertHorizontalDivider" title="Insert a divider">
480
487
  <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
488
  </button>
@@ -508,6 +515,10 @@ var theme = {
508
515
  underline: "lexxy-content__underline",
509
516
  highlight: "lexxy-content__highlight"
510
517
  },
518
+ tableCellHeader: "lexxy-content__table-cell--header",
519
+ tableCellSelected: "lexxy-content__table-cell--selected",
520
+ tableSelection: "lexxy-content__table--selection",
521
+ tableScrollableWrapper: "lexxy-content__table-wrapper",
511
522
  list: {
512
523
  nested: {
513
524
  listitem: "lexxy-nested-listitem",
@@ -567,7 +578,7 @@ var theme = {
567
578
  }
568
579
  };
569
580
 
570
- function createElement(name, properties) {
581
+ function createElement(name, properties, content = "") {
571
582
  const element = document.createElement(name);
572
583
  for (const [ key, value ] of Object.entries(properties || {})) {
573
584
  if (key in element) {
@@ -576,6 +587,9 @@ function createElement(name, properties) {
576
587
  element.setAttribute(key, value);
577
588
  }
578
589
  }
590
+ if (content) {
591
+ element.innerHTML = content;
592
+ }
579
593
  return element
580
594
  }
581
595
 
@@ -729,6 +743,10 @@ class ActionTextAttachmentNode extends DecoratorNode {
729
743
  return true
730
744
  }
731
745
 
746
+ getTextContent() {
747
+ return `[${ this.caption || this.fileName }]\n\n`
748
+ }
749
+
732
750
  isInline() {
733
751
  return false
734
752
  }
@@ -1095,6 +1113,10 @@ class HorizontalDividerNode extends DecoratorNode {
1095
1113
  return true
1096
1114
  }
1097
1115
 
1116
+ getTextContent() {
1117
+ return "┄\n\n"
1118
+ }
1119
+
1098
1120
  isInline() {
1099
1121
  return false
1100
1122
  }
@@ -1131,6 +1153,16 @@ const COMMANDS = [
1131
1153
  "insertCodeBlock",
1132
1154
  "insertHorizontalDivider",
1133
1155
  "uploadAttachments",
1156
+
1157
+ "insertTable",
1158
+ "insertTableRowAbove",
1159
+ "insertTableRowBelow",
1160
+ "insertTableColumnAfter",
1161
+ "insertTableColumnBefore",
1162
+ "deleteTableRow",
1163
+ "deleteTableColumn",
1164
+ "deleteTable",
1165
+
1134
1166
  "undo",
1135
1167
  "redo"
1136
1168
  ];
@@ -1293,6 +1325,45 @@ class CommandDispatcher {
1293
1325
  setTimeout(() => input.remove(), 1000);
1294
1326
  }
1295
1327
 
1328
+ dispatchInsertTable() {
1329
+ this.editor.dispatchCommand(INSERT_TABLE_COMMAND, { "rows": 3, "columns": 3, "includeHeaders": true });
1330
+ }
1331
+
1332
+ dispatchInsertTableRowBelow() {
1333
+ $insertTableRowAtSelection(true);
1334
+ }
1335
+
1336
+ dispatchInsertTableRowAbove() {
1337
+ $insertTableRowAtSelection(false);
1338
+ }
1339
+
1340
+ dispatchInsertTableColumnAfter() {
1341
+ $insertTableColumnAtSelection(true);
1342
+ }
1343
+
1344
+ dispatchInsertTableColumnBefore() {
1345
+ $insertTableColumnAtSelection(false);
1346
+ }
1347
+
1348
+ dispatchDeleteTableRow() {
1349
+ $deleteTableRowAtSelection();
1350
+ }
1351
+
1352
+ dispatchDeleteTableColumn() {
1353
+ $deleteTableColumnAtSelection();
1354
+ }
1355
+
1356
+ dispatchDeleteTable() {
1357
+ this.editor.update(() => {
1358
+ const selection = $getSelection();
1359
+ if (!$isRangeSelection(selection)) return
1360
+
1361
+ const anchorNode = selection.anchor.getNode();
1362
+ const tableNode = $findTableNode(anchorNode);
1363
+ tableNode.remove();
1364
+ });
1365
+ }
1366
+
1296
1367
  dispatchUndo() {
1297
1368
  this.editor.dispatchCommand(UNDO_COMMAND, undefined);
1298
1369
  }
@@ -2132,6 +2203,10 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
2132
2203
  return true
2133
2204
  }
2134
2205
 
2206
+ getTextContent() {
2207
+ return this.createDOM().textContent.trim() || `[${this.contentType}]`
2208
+ }
2209
+
2135
2210
  isInline() {
2136
2211
  return true
2137
2212
  }
@@ -3432,7 +3507,7 @@ function applyLanguage(conversionOutput, element) {
3432
3507
 
3433
3508
  class LexicalEditorElement extends HTMLElement {
3434
3509
  static formAssociated = true
3435
- static debug = true
3510
+ static debug = false
3436
3511
  static commands = [ "bold", "italic", "strikethrough" ]
3437
3512
 
3438
3513
  static observedAttributes = [ "connected", "required" ]
@@ -3507,6 +3582,18 @@ class LexicalEditorElement extends HTMLElement {
3507
3582
  return this.dataset.blobUrlTemplate
3508
3583
  }
3509
3584
 
3585
+ get isEmpty() {
3586
+ return [ "<p><br></p>", "<p></p>", "" ].includes(this.value.trim())
3587
+ }
3588
+
3589
+ get isBlank() {
3590
+ return this.isEmpty || this.toString().match(/^\s*$/g) !== null
3591
+ }
3592
+
3593
+ get hasOpenPrompt() {
3594
+ return this.querySelector(".lexxy-prompt-menu.lexxy-prompt-menu--visible") !== null
3595
+ }
3596
+
3510
3597
  get isSingleLineMode() {
3511
3598
  return this.hasAttribute("single-line")
3512
3599
  }
@@ -3550,6 +3637,16 @@ class LexicalEditorElement extends HTMLElement {
3550
3637
  });
3551
3638
  }
3552
3639
 
3640
+ toString() {
3641
+ if (!this.cachedStringValue) {
3642
+ this.editor?.getEditorState().read(() => {
3643
+ this.cachedStringValue = $getRoot().getTextContent();
3644
+ });
3645
+ }
3646
+
3647
+ return this.cachedStringValue
3648
+ }
3649
+
3553
3650
  #parseHtmlIntoLexicalNodes(html) {
3554
3651
  if (!html) html = "<p></p>";
3555
3652
  const nodes = $generateNodesFromDOM(this.editor, parseHtml(`<div>${html}</div>`));
@@ -3572,6 +3669,7 @@ class LexicalEditorElement extends HTMLElement {
3572
3669
  this.#listenForInvalidatedNodes();
3573
3670
  this.#handleEnter();
3574
3671
  this.#handleFocus();
3672
+ this.#handleTables();
3575
3673
  this.#attachDebugHooks();
3576
3674
  this.#attachToolbar();
3577
3675
  this.#loadInitialValue();
@@ -3608,6 +3706,9 @@ class LexicalEditorElement extends HTMLElement {
3608
3706
  LinkNode,
3609
3707
  AutoLinkNode,
3610
3708
  HorizontalDividerNode,
3709
+ TableNode,
3710
+ TableCellNode,
3711
+ TableRowNode,
3611
3712
 
3612
3713
  CustomActionTextAttachmentNode,
3613
3714
  ];
@@ -3655,7 +3756,7 @@ class LexicalEditorElement extends HTMLElement {
3655
3756
 
3656
3757
  this.internals.setFormValue(html);
3657
3758
  this._internalFormValue = html;
3658
- this.#validationTextArea.value = this.#isEmpty ? "" : html;
3759
+ this.#validationTextArea.value = this.isEmpty ? "" : html;
3659
3760
 
3660
3761
  if (changed) {
3661
3762
  dispatch(this, "lexxy:change");
@@ -3681,13 +3782,18 @@ class LexicalEditorElement extends HTMLElement {
3681
3782
 
3682
3783
  #synchronizeWithChanges() {
3683
3784
  this.#addUnregisterHandler(this.editor.registerUpdateListener(({ editorState }) => {
3684
- this.cachedValue = null;
3785
+ this.#clearCachedValues();
3685
3786
  this.#internalFormValue = this.value;
3686
3787
  this.#toggleEmptyStatus();
3687
3788
  this.#setValidity();
3688
3789
  }));
3689
3790
  }
3690
3791
 
3792
+ #clearCachedValues() {
3793
+ this.cachedValue = null;
3794
+ this.cachedStringValue = null;
3795
+ }
3796
+
3691
3797
  #addUnregisterHandler(handler) {
3692
3798
  this.unregisterHandlers = this.unregisterHandlers || [];
3693
3799
  this.unregisterHandlers.push(handler);
@@ -3705,13 +3811,21 @@ class LexicalEditorElement extends HTMLElement {
3705
3811
  this.historyState = createEmptyHistoryState();
3706
3812
  registerHistory(this.editor, this.historyState, 20);
3707
3813
  registerList(this.editor);
3814
+ this.#registerTableComponents();
3708
3815
  this.#registerCodeHiglightingComponents();
3709
3816
  registerMarkdownShortcuts(this.editor, TRANSFORMERS);
3710
3817
  }
3711
3818
 
3819
+ #registerTableComponents() {
3820
+ registerTablePlugin(this.editor);
3821
+ this.tableHandler = createElement("lexxy-table-handler");
3822
+ this.append(this.tableHandler);
3823
+ }
3824
+
3712
3825
  #registerCodeHiglightingComponents() {
3713
3826
  registerCodeHighlighting(this.editor);
3714
- this.append(createElement("lexxy-code-language-picker"));
3827
+ this.codeLanguagePicker = createElement("lexxy-code-language-picker");
3828
+ this.append(this.codeLanguagePicker);
3715
3829
  }
3716
3830
 
3717
3831
  #listenForInvalidatedNodes() {
@@ -3760,12 +3874,18 @@ class LexicalEditorElement extends HTMLElement {
3760
3874
  this.editor.registerCommand(FOCUS_COMMAND, () => { dispatch(this, "lexxy:focus"); }, COMMAND_PRIORITY_NORMAL);
3761
3875
  }
3762
3876
 
3877
+ #handleTables() {
3878
+ this.removeTableSelectionObserver = registerTableSelectionObserver(this.editor, true);
3879
+ setScrollableTablesActive(this.editor, true);
3880
+ }
3881
+
3763
3882
  #attachDebugHooks() {
3764
3883
  if (!LexicalEditorElement.debug) return
3765
3884
 
3766
3885
  this.#addUnregisterHandler(this.editor.registerUpdateListener(({ editorState }) => {
3767
3886
  editorState.read(() => {
3768
- console.debug("HTML: ", this.value);
3887
+ console.debug("HTML: ", this.value, "String:", this.toString());
3888
+ console.debug("empty", this.isEmpty, "blank", this.isBlank);
3769
3889
  });
3770
3890
  }));
3771
3891
  }
@@ -3794,11 +3914,7 @@ class LexicalEditorElement extends HTMLElement {
3794
3914
  }
3795
3915
 
3796
3916
  #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())
3917
+ this.classList.toggle("lexxy-editor--empty", this.isEmpty);
3802
3918
  }
3803
3919
 
3804
3920
  #setValidity() {
@@ -3825,6 +3941,16 @@ class LexicalEditorElement extends HTMLElement {
3825
3941
  this.toolbar = null;
3826
3942
  }
3827
3943
 
3944
+ if (this.codeLanguagePicker) {
3945
+ this.codeLanguagePicker.remove();
3946
+ this.codeLanguagePicker = null;
3947
+ }
3948
+
3949
+ if (this.tableHandler) {
3950
+ this.tableHandler.remove();
3951
+ this.tableHandler = null;
3952
+ }
3953
+
3828
3954
  this.selection = null;
3829
3955
 
3830
3956
  document.removeEventListener("turbo:before-cache", this.#handleTurboBeforeCache);
@@ -4077,6 +4203,494 @@ class HighlightDropdown extends ToolbarDropdown {
4077
4203
 
4078
4204
  customElements.define("lexxy-highlight-dropdown", HighlightDropdown);
4079
4205
 
4206
+ class TableHandler extends HTMLElement {
4207
+ connectedCallback() {
4208
+ this.#setUpButtons();
4209
+ this.#monitorForTableSelection();
4210
+ this.#registerKeyboardShortcuts();
4211
+ }
4212
+
4213
+ disconnectedCallback() {
4214
+ this.#unregisterKeyboardShortcuts();
4215
+ }
4216
+
4217
+ get #editor() {
4218
+ return this.#editorElement.editor
4219
+ }
4220
+
4221
+ get #editorElement() {
4222
+ return this.closest("lexxy-editor")
4223
+ }
4224
+
4225
+ get #currentCell() {
4226
+ const selection = $getSelection();
4227
+ if (!$isRangeSelection(selection)) return null
4228
+
4229
+ const anchorNode = selection.anchor.getNode();
4230
+ return $getTableCellNodeFromLexicalNode(anchorNode)
4231
+ }
4232
+
4233
+ get #currentRow() {
4234
+ const currentCell = this.#currentCell;
4235
+ if (!currentCell) return 0
4236
+ return $getTableRowIndexFromTableCellNode(currentCell)
4237
+ }
4238
+
4239
+ get #currentColumn() {
4240
+ const currentCell = this.#currentCell;
4241
+ if (!currentCell) return 0
4242
+ return $getTableColumnIndexFromTableCellNode(currentCell)
4243
+ }
4244
+
4245
+ #registerKeyboardShortcuts() {
4246
+ this.unregisterKeyboardShortcuts = this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleKeyDown.bind(this), COMMAND_PRIORITY_HIGH);
4247
+ }
4248
+
4249
+ #unregisterKeyboardShortcuts() {
4250
+ this.unregisterKeyboardShortcuts();
4251
+ }
4252
+
4253
+ #handleKeyDown(event) {
4254
+ if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === "F10") {
4255
+ const firstButton = this.buttonsContainer?.querySelector("button, [tabindex]:not([tabindex='-1'])");
4256
+ this.#setFocusStateOnSelectedCell();
4257
+ firstButton?.focus();
4258
+ } else if (event.key === "Escape") {
4259
+ this.#editor.getEditorState().read(() => {
4260
+ const cell = this.#currentCell;
4261
+ if (!cell) return
4262
+
4263
+ this.#editor.update(() => {
4264
+ cell.select();
4265
+ });
4266
+ });
4267
+ this.#closeMoreMenu();
4268
+ }
4269
+ }
4270
+
4271
+ #setUpButtons() {
4272
+ this.buttonsContainer = createElement("div", {
4273
+ className: "lexxy-table-handle-buttons"
4274
+ });
4275
+
4276
+ this.buttonsContainer.appendChild(this.#createRowButtonsContainer());
4277
+ this.buttonsContainer.appendChild(this.#createColumnButtonsContainer());
4278
+
4279
+ this.moreMenu = this.#createMoreMenu();
4280
+ this.buttonsContainer.appendChild(this.moreMenu);
4281
+
4282
+ this.#editorElement.appendChild(this.buttonsContainer);
4283
+ }
4284
+
4285
+ #showTableHandlerButtons() {
4286
+ this.buttonsContainer.style.display = "flex";
4287
+ this.#closeMoreMenu();
4288
+
4289
+ this.#updateRowColumnCount();
4290
+ this.#setTableFocusState(true);
4291
+ }
4292
+
4293
+ #hideTableHandlerButtons() {
4294
+ this.buttonsContainer.style.display = "none";
4295
+ this.#closeMoreMenu();
4296
+
4297
+ this.#setTableFocusState(false);
4298
+ this.currentTableNode = null;
4299
+ }
4300
+
4301
+ #updateButtonsPosition(tableNode) {
4302
+ const tableElement = this.#editor.getElementByKey(tableNode.getKey());
4303
+ if (!tableElement) return
4304
+
4305
+ const tableRect = tableElement.getBoundingClientRect();
4306
+ const editorRect = this.#editorElement.getBoundingClientRect();
4307
+
4308
+ const relativeTop = tableRect.top - editorRect.top;
4309
+ const relativeCenter = (tableRect.left + tableRect.right) / 2 - editorRect.left;
4310
+ this.buttonsContainer.style.top = `${relativeTop}px`;
4311
+ this.buttonsContainer.style.left = `${relativeCenter}px`;
4312
+ }
4313
+
4314
+ #updateRowColumnCount() {
4315
+ if (!this.currentTableNode) return
4316
+
4317
+ const tableElement = $getElementForTableNode(this.#editor, this.currentTableNode);
4318
+ if (!tableElement) return
4319
+
4320
+ const rowCount = tableElement.rows;
4321
+ const columnCount = tableElement.columns;
4322
+
4323
+ this.rowCount.textContent = `${rowCount} row${rowCount === 1 ? "" : "s"}`;
4324
+ this.columnCount.textContent = `${columnCount} column${columnCount === 1 ? "" : "s"}`;
4325
+ }
4326
+
4327
+ #createButton(icon, label, onClick) {
4328
+ const button = createElement("button", {
4329
+ className: "lexxy-table-control__button",
4330
+ "aria-label": label,
4331
+ type: "button"
4332
+ });
4333
+ button.tabIndex = -1;
4334
+ button.innerHTML = `${icon} <span>${label}</span>`;
4335
+ button.addEventListener("click", onClick.bind(this));
4336
+
4337
+ return button
4338
+ }
4339
+
4340
+ #createRowButtonsContainer() {
4341
+ const container = createElement("div", { className: "lexxy-table-control" });
4342
+
4343
+ const plusButton = this.#createButton("+", "Add row", () => this.#insertTableRow("end"));
4344
+ const minusButton = this.#createButton("−", "Remove row", () => this.#deleteTableRow("end"));
4345
+
4346
+ this.rowCount = createElement("span");
4347
+ this.rowCount.textContent = "_ rows";
4348
+
4349
+ container.appendChild(minusButton);
4350
+ container.appendChild(this.rowCount);
4351
+ container.appendChild(plusButton);
4352
+
4353
+ return container
4354
+ }
4355
+
4356
+ #createColumnButtonsContainer() {
4357
+ const container = createElement("div", { className: "lexxy-table-control" });
4358
+
4359
+ const plusButton = this.#createButton("+", "Add column", () => this.#insertTableColumn("end"));
4360
+ const minusButton = this.#createButton("−", "Remove column", () => this.#deleteTableColumn("end"));
4361
+
4362
+ this.columnCount = createElement("span");
4363
+ this.columnCount.textContent = "_ columns";
4364
+
4365
+ container.appendChild(minusButton);
4366
+ container.appendChild(this.columnCount);
4367
+ container.appendChild(plusButton);
4368
+
4369
+ return container
4370
+ }
4371
+
4372
+ #createMoreMenu() {
4373
+ const container = createElement("details", {
4374
+ className: "lexxy-table-control lexxy-table-control__more-menu"
4375
+ });
4376
+
4377
+ container.tabIndex = -1;
4378
+
4379
+ const summary = createElement("summary", {}, "•••");
4380
+ container.appendChild(summary);
4381
+
4382
+ const details = createElement("div", { className: "lexxy-table-control__more-menu-details" });
4383
+ container.appendChild(details);
4384
+
4385
+ details.appendChild(this.#createRowSection());
4386
+ details.appendChild(this.#createColumnSection());
4387
+ details.appendChild(this.#createDeleteTableSection());
4388
+
4389
+ container.addEventListener("toggle", this.#handleMoreMenuToggle.bind(this));
4390
+
4391
+ return container
4392
+ }
4393
+
4394
+ #createColumnSection() {
4395
+ const columnSection = createElement("section", { className: "lexxy-table-control__more-menu-section" });
4396
+
4397
+ const columnButtons = [
4398
+ { icon: this.#icon("add-column-before"), label: "Add column before", onClick: () => this.#insertTableColumn("left") },
4399
+ { icon: this.#icon("add-column-after"), label: "Add column after", onClick: () => this.#insertTableColumn("right") },
4400
+ { icon: this.#icon("remove-column"), label: "Remove column", onClick: this.#deleteTableColumn },
4401
+ { icon: this.#icon("toggle-column-style"), label: "Toggle column style", onClick: this.#toggleColumnHeaderStyle },
4402
+ ];
4403
+
4404
+ columnButtons.forEach(button => {
4405
+ const buttonElement = this.#createButton(button.icon, button.label, button.onClick);
4406
+ columnSection.appendChild(buttonElement);
4407
+ });
4408
+
4409
+ return columnSection
4410
+ }
4411
+
4412
+ #createRowSection() {
4413
+ const rowSection = createElement("section", { className: "lexxy-table-control__more-menu-section" });
4414
+
4415
+ const rowButtons = [
4416
+ { icon: this.#icon("add-row-above"), label: "Add row above", onClick: () => this.#insertTableRow("above") },
4417
+ { icon: this.#icon("add-row-below"), label: "Add row below", onClick: () => this.#insertTableRow("below") },
4418
+ { icon: this.#icon("remove-row"), label: "Remove row", onClick: this.#deleteTableRow },
4419
+ { icon: this.#icon("toggle-row-style"), label: "Toggle row style", onClick: this.#toggleRowHeaderStyle }
4420
+ ];
4421
+
4422
+ rowButtons.forEach(button => {
4423
+ const buttonElement = this.#createButton(button.icon, button.label, button.onClick);
4424
+ rowSection.appendChild(buttonElement);
4425
+ });
4426
+
4427
+ return rowSection
4428
+ }
4429
+
4430
+ #createDeleteTableSection() {
4431
+ const deleteSection = createElement("section", { className: "lexxy-table-control__more-menu-section" });
4432
+
4433
+ const deleteButton = { icon: this.#icon("delete-table"), label: "Delete table", onClick: this.#deleteTable };
4434
+
4435
+ const buttonElement = this.#createButton(deleteButton.icon, deleteButton.label, deleteButton.onClick);
4436
+ deleteSection.appendChild(buttonElement);
4437
+
4438
+ return deleteSection
4439
+ }
4440
+
4441
+ #handleMoreMenuToggle() {
4442
+ if (this.moreMenu.open) {
4443
+ this.#setFocusStateOnSelectedCell();
4444
+ } else {
4445
+ this.#removeFocusStateFromSelectedCell();
4446
+ }
4447
+ }
4448
+
4449
+ #closeMoreMenu() {
4450
+ this.#removeFocusStateFromSelectedCell();
4451
+ this.moreMenu.removeAttribute("open");
4452
+ }
4453
+
4454
+ #monitorForTableSelection() {
4455
+ this.#editor.registerUpdateListener(() => {
4456
+ this.#editor.getEditorState().read(() => {
4457
+ const selection = $getSelection();
4458
+ if (!$isRangeSelection(selection)) return
4459
+
4460
+ const anchorNode = selection.anchor.getNode();
4461
+ const tableNode = $findTableNode(anchorNode);
4462
+
4463
+ if (tableNode) {
4464
+ this.#tableCellWasSelected(tableNode);
4465
+ } else {
4466
+ this.#hideTableHandlerButtons();
4467
+ }
4468
+ });
4469
+ });
4470
+ }
4471
+
4472
+ #setTableFocusState(focused) {
4473
+ this.#editorElement.querySelector("div.node--selected:has(table)")?.classList.remove("node--selected");
4474
+
4475
+ if (focused && this.currentTableNode) {
4476
+ const tableParent = this.#editor.getElementByKey(this.currentTableNode.getKey());
4477
+ if (!tableParent) return
4478
+ tableParent.classList.add("node--selected");
4479
+ }
4480
+ }
4481
+
4482
+ #tableCellWasSelected(tableNode) {
4483
+ this.currentTableNode = tableNode;
4484
+ this.#updateButtonsPosition(tableNode);
4485
+ this.#showTableHandlerButtons();
4486
+ }
4487
+
4488
+ #setFocusStateOnSelectedCell() {
4489
+ this.#editor.getEditorState().read(() => {
4490
+ const currentCell = this.#currentCell;
4491
+ if (!currentCell) return
4492
+
4493
+ const cellElement = this.#editor.getElementByKey(currentCell.getKey());
4494
+ if (!cellElement) return
4495
+
4496
+ cellElement.classList.add("table-cell--selected");
4497
+ });
4498
+ }
4499
+
4500
+ #removeFocusStateFromSelectedCell() {
4501
+ this.#editorElement.querySelector(".table-cell--selected")?.classList.remove("table-cell--selected");
4502
+ }
4503
+
4504
+ #selectLastTableCell() {
4505
+ if (!this.currentTableNode) return
4506
+
4507
+ const last = this.currentTableNode.getLastChild().getLastChild();
4508
+ if (!$isTableCellNode(last)) return
4509
+
4510
+ last.selectEnd();
4511
+ }
4512
+
4513
+ #deleteTable() {
4514
+ this.#editor.dispatchCommand("deleteTable");
4515
+
4516
+ this.#closeMoreMenu();
4517
+ this.#updateRowColumnCount();
4518
+ }
4519
+
4520
+ #insertTableRow(direction) {
4521
+ this.#executeTableCommand("insert", "row", direction);
4522
+ }
4523
+
4524
+ #insertTableColumn(direction) {
4525
+ this.#executeTableCommand("insert", "column", direction);
4526
+ }
4527
+
4528
+ #deleteTableRow(direction) {
4529
+ this.#executeTableCommand("delete", "row", direction);
4530
+ }
4531
+
4532
+ #deleteTableColumn(direction) {
4533
+ this.#executeTableCommand("delete", "column", direction);
4534
+ }
4535
+
4536
+ #executeTableCommand(action = "insert", childType = "row", direction) {
4537
+ this.#editor.update(() => {
4538
+ const currentCell = this.#currentCell;
4539
+ if (!currentCell) return
4540
+
4541
+ if (direction === "end") {
4542
+ this.#selectLastTableCell();
4543
+ }
4544
+
4545
+ this.#dispatchTableCommand(action, childType, direction);
4546
+
4547
+ if (currentCell.isAttached()) {
4548
+ currentCell.selectEnd();
4549
+ }
4550
+ });
4551
+
4552
+ this.#closeMoreMenu();
4553
+ this.#updateRowColumnCount();
4554
+ }
4555
+
4556
+ #dispatchTableCommand(action, childType, direction) {
4557
+ switch (action) {
4558
+ case "insert":
4559
+ switch (childType) {
4560
+ case "row":
4561
+ if (direction === "above") {
4562
+ this.#editor.dispatchCommand("insertTableRowAbove");
4563
+ } else {
4564
+ this.#editor.dispatchCommand("insertTableRowBelow");
4565
+ }
4566
+ break
4567
+ case "column":
4568
+ if (direction === "left") {
4569
+ this.#editor.dispatchCommand("insertTableColumnBefore");
4570
+ } else {
4571
+ this.#editor.dispatchCommand("insertTableColumnAfter");
4572
+ }
4573
+ break
4574
+ }
4575
+ break
4576
+ case "delete":
4577
+ switch (childType) {
4578
+ case "row":
4579
+ this.#editor.dispatchCommand("deleteTableRow");
4580
+ break
4581
+ case "column":
4582
+ this.#editor.dispatchCommand("deleteTableColumn");
4583
+ break
4584
+ }
4585
+ break
4586
+ }
4587
+ }
4588
+
4589
+ #toggleRowHeaderStyle() {
4590
+ this.#editor.update(() => {
4591
+ const rows = this.currentTableNode.getChildren();
4592
+
4593
+ const row = rows[this.#currentRow];
4594
+ if (!row) return
4595
+
4596
+ const cells = row.getChildren();
4597
+ const firstCell = $getTableCellNodeFromLexicalNode(cells[0]);
4598
+ if (!firstCell) return
4599
+
4600
+ const currentStyle = firstCell.getHeaderStyles();
4601
+ const newStyle = currentStyle ^ TableCellHeaderStates.ROW;
4602
+
4603
+ cells.forEach(cell => {
4604
+ this.#setHeaderStyle(cell, newStyle, TableCellHeaderStates.ROW);
4605
+ });
4606
+ });
4607
+ }
4608
+
4609
+ #toggleColumnHeaderStyle() {
4610
+ this.#editor.update(() => {
4611
+ const rows = this.currentTableNode.getChildren();
4612
+
4613
+ const row = rows[this.#currentRow];
4614
+ if (!row) return
4615
+
4616
+ const cells = row.getChildren();
4617
+ const selectedCell = $getTableCellNodeFromLexicalNode(cells[this.#currentColumn]);
4618
+ if (!selectedCell) return
4619
+
4620
+ const currentStyle = selectedCell.getHeaderStyles();
4621
+ const newStyle = currentStyle ^ TableCellHeaderStates.COLUMN;
4622
+
4623
+ rows.forEach(row => {
4624
+ const cell = row.getChildren()[this.#currentColumn];
4625
+ if (!cell) return
4626
+ this.#setHeaderStyle(cell, newStyle, TableCellHeaderStates.COLUMN);
4627
+ });
4628
+ });
4629
+ }
4630
+
4631
+ #setHeaderStyle(cell, newStyle, headerState) {
4632
+ const tableCellNode = $getTableCellNodeFromLexicalNode(cell);
4633
+
4634
+ if (tableCellNode) {
4635
+ tableCellNode.setHeaderStyles(newStyle, headerState);
4636
+ }
4637
+ }
4638
+
4639
+ #icon(name) {
4640
+ const icons =
4641
+ {
4642
+ "add-row-above":
4643
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4644
+ <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"/>
4645
+ </svg>`,
4646
+
4647
+ "add-row-below":
4648
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4649
+ <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"/>
4650
+ </svg>`,
4651
+
4652
+ "remove-row":
4653
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4654
+ <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"/>
4655
+ </svg>`,
4656
+
4657
+ "toggle-row-style":
4658
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4659
+ <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"/>
4660
+ </svg>`,
4661
+
4662
+ "add-column-before":
4663
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4664
+ <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"/>
4665
+ </svg>`,
4666
+
4667
+ "add-column-after":
4668
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4669
+ <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"/>
4670
+ </svg>`,
4671
+
4672
+ "remove-column":
4673
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4674
+ <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"/>
4675
+ </svg>`,
4676
+
4677
+ "toggle-column-style":
4678
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
4679
+ <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"/>
4680
+ </svg>`,
4681
+
4682
+ "delete-table":
4683
+ `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
4684
+ <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"/>
4685
+ </svg>`
4686
+ };
4687
+
4688
+ return icons[name]
4689
+ }
4690
+ }
4691
+
4692
+ customElements.define("lexxy-table-handler", TableHandler);
4693
+
4080
4694
  class BaseSource {
4081
4695
  // Template method to override
4082
4696
  async buildListItems(filter = "") {
@@ -195,6 +195,42 @@
195
195
  .token.punctuation {
196
196
  color: var(--lexxy-color-code-token-punctuation);
197
197
  }
198
+
199
+ /* Tables */
200
+ :where(.lexxy-content__table-wrapper) {
201
+ margin-block: 1ch;
202
+ overflow-x: auto;
203
+ }
204
+
205
+ table {
206
+ border-collapse: collapse;
207
+ border-spacing: 0;
208
+ inline-size: calc(100% - 0.5ch);
209
+ margin: 0.25ch;
210
+
211
+ th,
212
+ td {
213
+ border: 1px solid var(--lexxy-color-ink-lighter);
214
+ padding: 1ch;
215
+ text-align: start;
216
+ word-break: normal;
217
+
218
+ *:last-child {
219
+ margin-block-end: 0;
220
+ }
221
+
222
+ &.lexxy-content__table-cell--header {
223
+ background-color: var(--lexxy-color-ink-lightest);
224
+ font-weight: bold;
225
+ }
226
+
227
+ *:is(code, pre) {
228
+ hyphens: auto;
229
+ text-wrap: wrap;
230
+ white-space: pre-wrap;
231
+ }
232
+ }
233
+ }
198
234
  }
199
235
 
200
236
  :where([data-lexical-cursor]) {
@@ -36,6 +36,7 @@
36
36
  cursor: pointer;
37
37
  font-size: inherit;
38
38
  inline-size: auto;
39
+ padding: 0;
39
40
 
40
41
  @media(any-hover: hover) {
41
42
  &:hover:not([aria-disabled="true"]) {
@@ -52,6 +53,23 @@
52
53
  }
53
54
  }
54
55
 
56
+ table {
57
+ .table-cell--selected {
58
+ background-color: var(--lexxy-color-table-cell-selected-bg) !important;
59
+ }
60
+
61
+ .lexxy-content__table-cell--selected {
62
+ background-color: var(--lexxy-color-table-cell-selected-bg) !important;
63
+ border-color: var(--lexxy-color-table-cell-selected-border) !important;
64
+ }
65
+
66
+ &.lexxy-content__table--selection {
67
+ ::selection {
68
+ background: transparent;
69
+ }
70
+ }
71
+ }
72
+
55
73
  action-text-attachment {
56
74
  cursor: pointer;
57
75
  }
@@ -189,7 +207,7 @@
189
207
  user-select: none;
190
208
  -webkit-user-select: none;
191
209
 
192
- .lexxy-editor__toolbar-dropdown-content {
210
+ :where(.lexxy-editor__toolbar-dropdown-content) {
193
211
  --dropdown-padding: 1ch;
194
212
  --dropdown-gap: calc(var(--dropdown-padding) / 2);
195
213
 
@@ -358,6 +376,212 @@
358
376
  }
359
377
  }
360
378
 
379
+ /* Table dropdown
380
+ /* -------------------------------------------------------------------------- */
381
+
382
+ :where(lexxy-table-dropdown) {
383
+ display: flex;
384
+ flex-direction: column;
385
+ gap: 1ch;
386
+
387
+ .lexxy-editor__table-create {
388
+ display: flex;
389
+ flex-direction: column;
390
+ flex-wrap: wrap;
391
+ gap: 0;
392
+
393
+ .lexxy-editor__table-buttons {
394
+ background-color: var(--lexxy-color-ink-lighter);
395
+ display: flex;
396
+ flex-direction: column;
397
+ gap: 1px;
398
+ padding: 1px;
399
+
400
+ div {
401
+ display: flex;
402
+ flex-direction: row;
403
+ gap: 1px;
404
+ }
405
+
406
+ button {
407
+ aspect-ratio: 1.5 / 1;
408
+ border: 0;
409
+ border-radius: 0;
410
+ color: var(--lexxy-color-ink);
411
+ font-family: var(--lexxy-font-base);
412
+ font-size: var(--lexxy-text-small);
413
+ font-weight: normal;
414
+ inline-size: 4ch;
415
+ margin: 0;
416
+
417
+ &.active {
418
+ background-color: var(--lexxy-color-ink-lightest);
419
+ }
420
+ }
421
+ }
422
+
423
+ label {
424
+ align-items: center;
425
+ display: flex;
426
+ gap: 0.5ch;
427
+ padding: 0.5ch 0;
428
+ margin-block-start: 1ch;
429
+ }
430
+
431
+ &:has(input[type="checkbox"]:checked) {
432
+ .lexxy-editor__table-buttons {
433
+ div:first-child button,
434
+ button:first-child {
435
+ filter: brightness(0.95);
436
+ }
437
+ }
438
+ }
439
+ }
440
+
441
+ .lexxy-editor__table-edit {
442
+ display: flex;
443
+ flex-direction: column;
444
+ flex-wrap: wrap;
445
+ gap: 0;
446
+
447
+ button {
448
+
449
+ }
450
+ }
451
+ }
452
+
453
+ /* Table handle buttons
454
+ /* -------------------------------------------------------------------------- */
455
+
456
+ :where(.lexxy-table-handle-buttons) {
457
+ --button-size: 2.5lh;
458
+ color: var(--lexxy-color-ink-inverted);
459
+ display: none;
460
+ flex-direction: row;
461
+ font-size: var(--lexxy-text-small);
462
+ gap: 0.25ch;
463
+ line-height: 1;
464
+ position: absolute;
465
+ transform: translate(-50%, -120%);
466
+ z-index: 10;
467
+
468
+ .lexxy-table-control {
469
+ align-items: center;
470
+ background-color: var(--lexxy-color-ink);
471
+ border-radius: 0.75ch;
472
+ display: flex;
473
+ flex-direction: row;
474
+ gap: 1ch;
475
+ padding: 2px;
476
+ white-space: nowrap;
477
+
478
+ button {
479
+ aspect-ratio: 1 / 1;
480
+ align-items: center;
481
+ background-color: transparent;
482
+ border-radius: var(--lexxy-radius);
483
+ border: 0;
484
+ color: var(--lexxy-color-ink-inverted);
485
+ cursor: pointer;
486
+ display: flex;
487
+ font-weight: bold;
488
+ justify-content: center;
489
+ line-height: 1;
490
+ min-block-size: var(--button-size);
491
+ min-inline-size: var(--button-size);
492
+ padding: 0;
493
+
494
+ &:hover,
495
+ &:focus-visible {
496
+ background-color: var(--lexxy-color-ink-medium);
497
+ }
498
+
499
+ svg {
500
+ block-size: 1em;
501
+ inline-size: 1em;
502
+ fill: currentColor;
503
+ }
504
+
505
+ span {
506
+ display: none;
507
+ }
508
+ }
509
+ }
510
+
511
+ .lexxy-table-control__more-menu {
512
+ gap: 0;
513
+ padding: 2px;
514
+ position: relative;
515
+
516
+ summary {
517
+ aspect-ratio: 1 / 1;
518
+ align-items: center;
519
+ background: transparent;
520
+ border-radius: var(--lexxy-radius);
521
+ border: 0;
522
+ box-sizing: border-box;
523
+ display: flex;
524
+ font-size: inherit;
525
+ justify-content: center;
526
+ list-style: none;
527
+ min-block-size: var(--button-size);
528
+ min-inline-size: var(--button-size);
529
+ padding: 0;
530
+ user-select: none;
531
+ -webkit-user-select: none;
532
+
533
+ &::-webkit-details-marker {
534
+ display: none;
535
+ }
536
+
537
+ &:hover {
538
+ background: var(--lexxy-color-ink-medium);
539
+ }
540
+ }
541
+
542
+ .lexxy-table-control__more-menu-details {
543
+ display: flex;
544
+ flex-direction: column;
545
+ gap: 0.25ch;
546
+ inset-block-start: 105%;
547
+ inset-inline-start: 0;
548
+ padding: 0;
549
+ position: absolute;
550
+
551
+ .lexxy-table-control__more-menu-section {
552
+ background: var(--lexxy-color-ink);
553
+ border-radius: 0.75ch;
554
+ display: flex;
555
+ flex-direction: column;
556
+ padding: 2px;
557
+ }
558
+
559
+ button {
560
+ aspect-ratio: unset;
561
+ align-items: center;
562
+ display: flex;
563
+ flex-direction: row;
564
+ font-weight: normal;
565
+ gap: 1ch;
566
+ justify-content: flex-start;
567
+ padding: 0.5ch 2ch;
568
+ padding-inline-start: 1ch;
569
+ white-space: nowrap;
570
+
571
+ span {
572
+ display: inline-block;
573
+ }
574
+
575
+ svg {
576
+ block-size: 1.3lh;
577
+ inline-size: 1.3lh;
578
+ fill: currentColor;
579
+ }
580
+ }
581
+ }
582
+ }
583
+ }
584
+
361
585
 
362
586
  /* Language picker
363
587
  /* -------------------------------------------------------------------------- */
@@ -35,6 +35,7 @@
35
35
  --lexxy-color-selected-dark: var(--lexxy-color-blue);
36
36
  --lexxy-color-code-bg: var(--lexxy-color-ink-lightest);
37
37
 
38
+ /* Text color highlights */
38
39
  --highlight-1: rgb(136, 118, 38);
39
40
  --highlight-2: rgb(185, 94, 6);
40
41
  --highlight-3: rgb(207, 0, 0);
@@ -55,6 +56,13 @@
55
56
  --highlight-bg-8: rgba(221, 170, 123, 0.3);
56
57
  --highlight-bg-9: rgba(200, 200, 200, 0.3);
57
58
 
59
+ /* Tables */
60
+ --lexxy-color-table-header-bg: var(--lexxy-color-ink-lightest);
61
+ --lexxy-color-table-cell-border: var(--lexxy-color-ink-lighter);
62
+ --lexxy-color-table-cell-selected: var(--lexxy-color-selected);
63
+ --lexxy-color-table-cell-selected-border: highlight;
64
+ --lexxy-color-table-cell-selected-bg: highlight;
65
+
58
66
  /* Typography */
59
67
  --lexxy-font-base: system-ui, sans-serif;
60
68
  --lexxy-font-mono: ui-monospace, "Menlo", "Monaco", Consolas, monospace;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.1.24-beta",
3
+ "version": "0.1.25-beta",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",