@37signals/lexxy 0.7.2-beta → 0.7.4-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lexxy.esm.js CHANGED
@@ -10,12 +10,12 @@ import 'prismjs/components/prism-json';
10
10
  import 'prismjs/components/prism-diff';
11
11
  import DOMPurify from 'dompurify';
12
12
  import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
13
- import { $isTextNode, TextNode, $isRangeSelection, SKIP_DOM_SELECTION_TAG, $getSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, $isRootOrShadowRoot, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, SELECTION_CHANGE_COMMAND, $createNodeSelection, $setSelection, $createParagraphNode, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, $insertNodes, $createLineBreakNode, createCommand, createState, defineExtension, $setState, $getState, $hasUpdateTag, PASTE_TAG, CLEAR_HISTORY_COMMAND, $addUpdateTag, BLUR_COMMAND, FOCUS_COMMAND, KEY_DOWN_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
14
- import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListNode, $getListDepth, $createListNode, ListItemNode, registerList } from '@lexical/list';
13
+ import { HISTORY_MERGE_TAG, SKIP_DOM_SELECTION_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $isTextNode, TextNode, $isRangeSelection, $getSelection, DecoratorNode, $getEditor, FORMAT_TEXT_COMMAND, $createTextNode, $isRootOrShadowRoot, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, SELECTION_CHANGE_COMMAND, $getNodeByKey, $createNodeSelection, $setSelection, $createParagraphNode, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, $insertNodes, $createLineBreakNode, PASTE_TAG, createCommand, createState, defineExtension, $setState, $getState, $hasUpdateTag, CLEAR_HISTORY_COMMAND, $addUpdateTag, KEY_SPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
14
+ import { $isListItemNode, $isListNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListNode, $getListDepth, $createListNode, ListItemNode, registerList } from '@lexical/list';
15
15
  import { $isQuoteNode, $isHeadingNode, $createQuoteNode, $createHeadingNode, RichTextExtension, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
16
16
  import { $isCodeNode, CodeNode, normalizeCodeLang, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
17
17
  import { $isLinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, LinkNode, AutoLinkNode } from '@lexical/link';
18
- import { $getTableCellNodeFromLexicalNode, TableNode, INSERT_TABLE_COMMAND, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, TableCellNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $getElementForTableNode, $isTableCellNode, TableCellHeaderStates } from '@lexical/table';
18
+ import { $getTableCellNodeFromLexicalNode, INSERT_TABLE_COMMAND, TableCellNode, TableNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
19
19
  import { createElement, createAttachmentFigure, isPreviewableImage, dispatchCustomEvent, parseHtml, dispatch, generateDomId } from './lexxy_helpers.esm.js';
20
20
  export { highlightCode as highlightAll, highlightCode } from './lexxy_helpers.esm.js';
21
21
  import { buildEditorFromExtensions } from '@lexical/extension';
@@ -146,6 +146,7 @@ function buildConfig() {
146
146
  return {
147
147
  ALLOWED_TAGS: ALLOWED_HTML_TAGS.concat(Lexxy.global.get("attachmentTagName")),
148
148
  ALLOWED_ATTR: ALLOWED_HTML_ATTRIBUTES,
149
+ ADD_URI_SAFE_ATTR: [ "caption", "filename" ],
149
150
  SAFE_FOR_XML: false // So that it does not strip attributes that contains serialized HTML (like content)
150
151
  }
151
152
  }
@@ -155,6 +156,8 @@ function getNonce() {
155
156
  return element?.content
156
157
  }
157
158
 
159
+ const SILENT_UPDATE_TAGS = [ HISTORY_MERGE_TAG, SKIP_DOM_SELECTION_TAG, SKIP_SCROLL_INTO_VIEW_TAG ];
160
+
158
161
  function getNearestListItemNode(node) {
159
162
  let current = node;
160
163
  while (current !== null) {
@@ -378,6 +381,8 @@ class LexicalToolbarElement extends HTMLElement {
378
381
  super();
379
382
  this.internals = this.attachInternals();
380
383
  this.internals.role = "toolbar";
384
+
385
+ this.#createEditorPromise();
381
386
  }
382
387
 
383
388
  connectedCallback() {
@@ -410,14 +415,26 @@ class LexicalToolbarElement extends HTMLElement {
410
415
  this.#refreshToolbarOverflow();
411
416
  this.#bindFocusListeners();
412
417
 
418
+ this.resolveEditorPromise(editorElement);
419
+
413
420
  this.toggleAttribute("connected", true);
414
421
  }
415
422
 
423
+ async getEditorElement() {
424
+ return this.editorElement || await this.editorPromise
425
+ }
426
+
416
427
  #reconnect() {
417
428
  this.disconnectedCallback();
418
429
  this.connectedCallback();
419
430
  }
420
431
 
432
+ #createEditorPromise() {
433
+ this.editorPromise = new Promise((resolve) => {
434
+ this.resolveEditorPromise = resolve;
435
+ });
436
+ }
437
+
421
438
  #installResizeObserver() {
422
439
  this.resizeObserver = new ResizeObserver(() => this.#refreshToolbarOverflow());
423
440
  this.resizeObserver.observe(this);
@@ -450,7 +467,7 @@ class LexicalToolbarElement extends HTMLElement {
450
467
 
451
468
  this.editor.update(() => {
452
469
  this.editor.dispatchCommand(command, payload);
453
- }, { tag: isKeyboard ? SKIP_DOM_SELECTION_TAG : undefined } );
470
+ }, { tag: isKeyboard ? SKIP_DOM_SELECTION_TAG : undefined });
454
471
  }
455
472
 
456
473
  #bindHotkeys() {
@@ -486,28 +503,24 @@ class LexicalToolbarElement extends HTMLElement {
486
503
  }
487
504
 
488
505
  #bindFocusListeners() {
489
- this.editorElement.addEventListener("lexxy:focus", this.#handleFocus);
490
- this.editorElement.addEventListener("lexxy:blur", this.#handleFocusOut);
491
- this.addEventListener("focusout", this.#handleFocusOut);
506
+ this.editorElement.addEventListener("lexxy:focus", this.#handleEditorFocus);
507
+ this.editorElement.addEventListener("lexxy:blur", this.#handleEditorBlur);
492
508
  this.addEventListener("keydown", this.#handleKeydown);
493
509
  }
494
510
 
495
511
  #unbindFocusListeners() {
496
- this.editorElement.removeEventListener("lexxy:focus", this.#handleFocus);
497
- this.editorElement.removeEventListener("lexxy:blur", this.#handleFocusOut);
498
- this.removeEventListener("focusout", this.#handleFocusOut);
512
+ this.editorElement.removeEventListener("lexxy:focus", this.#handleEditorFocus);
513
+ this.editorElement.removeEventListener("lexxy:blur", this.#handleEditorBlur);
499
514
  this.removeEventListener("keydown", this.#handleKeydown);
500
515
  }
501
516
 
502
- #handleFocus = () => {
503
- this.#resetTabIndexValues();
517
+ #handleEditorFocus = () => {
504
518
  this.#focusableItems[0].tabIndex = 0;
505
519
  }
506
520
 
507
- #handleFocusOut = () => {
508
- if (!this.contains(document.activeElement)) {
509
- this.#resetTabIndexValues();
510
- }
521
+ #handleEditorBlur = () => {
522
+ this.#resetTabIndexValues();
523
+ this.#closeDropdowns();
511
524
  }
512
525
 
513
526
  #handleKeydown = (event) => {
@@ -524,6 +537,7 @@ class LexicalToolbarElement extends HTMLElement {
524
537
  this.editor.registerUpdateListener(() => {
525
538
  this.editor.getEditorState().read(() => {
526
539
  this.#updateButtonStates();
540
+ this.#closeDropdowns();
527
541
  });
528
542
  });
529
543
  }
@@ -614,11 +628,13 @@ class LexicalToolbarElement extends HTMLElement {
614
628
  }
615
629
 
616
630
  #toolbarIsOverflowing() {
617
- return this.scrollWidth > this.clientWidth
631
+ // Safari can report inconsistent clientWidth values on more than 100% window zoom level,
632
+ // that was affecting the toolbar overflow calculation. We're adding +1 to get around this issue.
633
+ return (this.scrollWidth - this.#overflow.clientWidth) > this.clientWidth + 1
618
634
  }
619
635
 
620
636
  #refreshToolbarOverflow = () => {
621
- this.#resetToolbar();
637
+ this.#resetToolbarOverflow();
622
638
  this.#compactMenu();
623
639
 
624
640
  this.#overflow.style.display = this.#overflowMenu.children.length ? "block" : "none";
@@ -644,7 +660,7 @@ class LexicalToolbarElement extends HTMLElement {
644
660
  }
645
661
  }
646
662
 
647
- #resetToolbar() {
663
+ #resetToolbarOverflow() {
648
664
  const items = Array.from(this.#overflowMenu.children);
649
665
  items.sort((a, b) => this.#itemPosition(b) - this.#itemPosition(a));
650
666
 
@@ -666,6 +682,16 @@ class LexicalToolbarElement extends HTMLElement {
666
682
  });
667
683
  }
668
684
 
685
+ #closeDropdowns() {
686
+ this.#dropdowns.forEach((details) => {
687
+ details.open = false;
688
+ });
689
+ }
690
+
691
+ get #dropdowns() {
692
+ return this.querySelectorAll("details")
693
+ }
694
+
669
695
  get #overflow() {
670
696
  return this.querySelector(".lexxy-editor__toolbar-overflow")
671
697
  }
@@ -707,9 +733,8 @@ class LexicalToolbarElement extends HTMLElement {
707
733
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M7.65422 0.711575C7.1856 0.242951 6.42579 0.242951 5.95717 0.711575C5.48853 1.18021 5.48853 1.94 5.95717 2.40864L8.70864 5.16011L2.85422 11.0145C1.44834 12.4204 1.44833 14.6998 2.85422 16.1057L7.86011 21.1115C9.26599 22.5174 11.5454 22.5174 12.9513 21.1115L19.6542 14.4087C20.1228 13.94 20.1228 13.1802 19.6542 12.7115L11.8544 4.91171L11.2542 4.31158L7.65422 0.711575ZM4.55127 12.7115L10.4057 6.85716L17.1087 13.56H4.19981C4.19981 13.253 4.31696 12.9459 4.55127 12.7115ZM23.6057 20.76C23.6057 22.0856 22.5311 23.16 21.2057 23.16C19.8802 23.16 18.8057 22.0856 18.8057 20.76C18.8057 19.5408 19.8212 18.5339 20.918 17.4462C21.0135 17.3516 21.1096 17.2563 21.2057 17.16C21.3018 17.2563 21.398 17.3516 21.4935 17.4462C22.5903 18.5339 23.6057 19.5408 23.6057 20.76Z"/></svg>
708
734
  </summary>
709
735
  <lexxy-highlight-dropdown class="lexxy-editor__toolbar-dropdown-content">
710
- <div data-button-group="color"></div>
711
- <div data-button-group="background-color"></div>
712
- <button data-command="removeHighlight" class="lexxy-editor__toolbar-dropdown-reset">Remove all coloring</button>
736
+ <div class="lexxy-highlight-colors"></div>
737
+ <button data-command="removeHighlight" class="lexxy-editor__toolbar-button lexxy-editor__toolbar-dropdown-reset">Remove all coloring</button>
713
738
  </lexxy-highlight-dropdown>
714
739
  </details>
715
740
 
@@ -721,8 +746,8 @@ class LexicalToolbarElement extends HTMLElement {
721
746
  <form method="dialog">
722
747
  <input type="url" placeholder="Enter a URL…" class="input">
723
748
  <div class="lexxy-editor__toolbar-dropdown-actions">
724
- <button type="submit" class="btn" value="link">Link</button>
725
- <button type="button" class="btn" value="unlink">Unlink</button>
749
+ <button type="submit" class="lexxy-editor__toolbar-button" value="link">Link</button>
750
+ <button type="button" class="lexxy-editor__toolbar-button" value="unlink">Unlink</button>
726
751
  </div>
727
752
  </form>
728
753
  </lexxy-link-dropdown>
@@ -770,16 +795,14 @@ class LexicalToolbarElement extends HTMLElement {
770
795
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M18.2599 8.26531C15.9672 6.56386 13.1237 5.77629 10.2823 6.05535C7.4408 6.33452 4.80455 7.66079 2.88681 9.77605C1.32245 11.5016 0.326407 13.6516 0.0127834 15.9352C-0.105117 16.7939 0.608975 17.4997 1.47567 17.4997C2.34228 17.4997 3.02969 16.7915 3.19149 15.9401C3.47682 14.4379 4.17156 13.0321 5.212 11.8844C6.60637 10.3464 8.52287 9.38139 10.589 9.17839C12.655 8.97546 14.7227 9.54856 16.3897 10.7858C17.5237 11.6275 18.4165 12.7361 18.9991 13.9997H15.4063C14.578 13.9997 13.9066 14.6714 13.9063 15.4997C13.9063 16.3281 14.5779 16.9997 15.4063 16.9997H22.4063C23.2348 16.9997 23.9063 16.3281 23.9063 15.4997V8.49968C23.9061 7.67144 23.2346 6.99968 22.4063 6.99968C21.578 6.99968 20.9066 7.67144 20.9063 8.49968V11.0212C20.1897 9.9704 19.2984 9.03613 18.2599 8.26531Z"/></svg>
771
796
  </button>
772
797
 
773
- <details class="lexxy-editor__toolbar-overflow">
798
+ <details class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-overflow" name="lexxy-dropdown">
774
799
  <summary class="lexxy-editor__toolbar-button" aria-label="Show more toolbar buttons">•••</summary>
775
- <div class="lexxy-editor__toolbar-overflow-menu" aria-label="More toolbar buttons"></div>
800
+ <div class="lexxy-editor__toolbar-dropdown-content lexxy-editor__toolbar-overflow-menu" aria-label="More toolbar buttons"></div>
776
801
  </details>
777
802
  `
778
803
  }
779
804
  }
780
805
 
781
- customElements.define("lexxy-toolbar", LexicalToolbarElement);
782
-
783
806
  var theme = {
784
807
  text: {
785
808
  bold: "lexxy-content__bold",
@@ -792,6 +815,8 @@ var theme = {
792
815
  tableCellSelected: "lexxy-content__table-cell--selected",
793
816
  tableSelection: "lexxy-content__table--selection",
794
817
  tableScrollableWrapper: "lexxy-content__table-wrapper",
818
+ tableCellHighlight: "lexxy-content__table-cell--highlight",
819
+ tableCellFocus: "lexxy-content__table-cell--focus",
795
820
  list: {
796
821
  nested: {
797
822
  listitem: "lexxy-nested-listitem",
@@ -943,6 +968,8 @@ class ActionTextAttachmentNode extends DecoratorNode {
943
968
  this.fileSize = fileSize;
944
969
  this.width = width;
945
970
  this.height = height;
971
+
972
+ this.editor = $getEditor();
946
973
  }
947
974
 
948
975
  createDOM() {
@@ -963,8 +990,13 @@ class ActionTextAttachmentNode extends DecoratorNode {
963
990
  return figure
964
991
  }
965
992
 
966
- updateDOM() {
967
- return true
993
+ updateDOM(_prevNode, dom) {
994
+ const caption = dom.querySelector("figcaption textarea");
995
+ if (caption && this.caption) {
996
+ caption.value = this.caption;
997
+ }
998
+
999
+ return false
968
1000
  }
969
1001
 
970
1002
  getTextContent() {
@@ -1072,8 +1104,8 @@ class ActionTextAttachmentNode extends DecoratorNode {
1072
1104
  });
1073
1105
 
1074
1106
  input.addEventListener("focusin", () => input.placeholder = "Add caption...");
1075
- input.addEventListener("blur", this.#handleCaptionInputBlurred.bind(this));
1076
- input.addEventListener("keydown", this.#handleCaptionInputKeydown.bind(this));
1107
+ input.addEventListener("blur", (event) => this.#handleCaptionInputBlurred(event));
1108
+ input.addEventListener("keydown", (event) => this.#handleCaptionInputKeydown(event));
1077
1109
 
1078
1110
  caption.appendChild(input);
1079
1111
 
@@ -1081,21 +1113,24 @@ class ActionTextAttachmentNode extends DecoratorNode {
1081
1113
  }
1082
1114
 
1083
1115
  #handleCaptionInputBlurred(event) {
1084
- const input = event.target;
1085
-
1086
- input.placeholder = this.fileName;
1087
- this.#updateCaptionValueFromInput(input);
1116
+ this.#updateCaptionValueFromInput(event.target);
1088
1117
  }
1089
1118
 
1090
1119
  #updateCaptionValueFromInput(input) {
1091
- dispatchCustomEvent(input, "lexxy:internal:invalidate-node", { key: this.getKey(), values: { caption: input.value } });
1120
+ input.placeholder = this.fileName;
1121
+ this.editor.update(() => {
1122
+ this.getWritable().caption = input.value;
1123
+ });
1092
1124
  }
1093
1125
 
1094
1126
  #handleCaptionInputKeydown(event) {
1095
1127
  if (event.key === "Enter") {
1096
1128
  this.#updateCaptionValueFromInput(event.target);
1097
- dispatchCustomEvent(event.target, "lexxy:internal:move-to-next-line");
1098
1129
  event.preventDefault();
1130
+
1131
+ this.editor.update(() => {
1132
+ this.selectNext();
1133
+ }, { tag: HISTORY_MERGE_TAG });
1099
1134
  }
1100
1135
  event.stopPropagation();
1101
1136
  }
@@ -1136,56 +1171,83 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
1136
1171
  }
1137
1172
 
1138
1173
  constructor(node, key) {
1139
- const { file, uploadUrl, blobUrlTemplate, editor, progress } = node;
1174
+ const { file, uploadUrl, blobUrlTemplate, progress, width, height, uploadError } = node;
1140
1175
  super({ ...node, contentType: file.type }, key);
1141
1176
  this.file = file;
1142
1177
  this.uploadUrl = uploadUrl;
1143
1178
  this.blobUrlTemplate = blobUrlTemplate;
1144
- this.src = null;
1145
- this.editor = editor;
1146
- this.progress = progress || 0;
1179
+ this.progress = progress ?? null;
1180
+ this.width = width;
1181
+ this.height = height;
1182
+ this.uploadError = uploadError;
1147
1183
  }
1148
1184
 
1149
1185
  createDOM() {
1186
+ if (this.uploadError) return this.#createDOMForError()
1187
+
1188
+ // This side-effect is trigged on DOM load to fire only once and avoid multiple
1189
+ // uploads through cloning. The upload is guarded from restarting in case the
1190
+ // node is reloaded from saved state such as from history.
1191
+ this.#startUploadIfNeeded();
1192
+
1150
1193
  const figure = this.createAttachmentFigure();
1151
1194
 
1152
1195
  if (this.isPreviewableAttachment) {
1153
- figure.appendChild(this.#createDOMForImage());
1196
+ const img = figure.appendChild(this.#createDOMForImage());
1197
+
1198
+ // load file locally to set dimensions and prevent vertical shifting
1199
+ loadFileIntoImage(this.file, img).then(img => this.#setDimensionsFromImage(img));
1154
1200
  } else {
1155
1201
  figure.appendChild(this.#createDOMForFile());
1156
1202
  }
1157
1203
 
1158
1204
  figure.appendChild(this.#createCaption());
1205
+ figure.appendChild(this.#createProgressBar());
1206
+
1207
+ return figure
1208
+ }
1159
1209
 
1160
- const progressBar = createElement("progress", { value: this.progress, max: 100 });
1161
- figure.appendChild(progressBar);
1210
+ updateDOM(prevNode, dom) {
1211
+ if (this.uploadError !== prevNode.uploadError) return true
1162
1212
 
1163
- // We wait for images to download so that we can pass the dimensions down to the attachment. We do this
1164
- // so that we can render images in edit mode with the dimensions set, which prevent vertical layout shifts.
1165
- this.#loadFigure(figure).then(() => this.#startUpload(progressBar, figure));
1213
+ if (prevNode.progress !== this.progress) {
1214
+ const progress = dom.querySelector("progress");
1215
+ progress.value = this.progress ?? 0;
1216
+ }
1166
1217
 
1167
- return figure
1218
+ return false
1168
1219
  }
1169
1220
 
1170
1221
  exportDOM() {
1171
1222
  const img = document.createElement("img");
1172
- if (this.src) {
1173
- img.src = this.src;
1174
- }
1175
1223
  return { element: img }
1176
1224
  }
1177
1225
 
1178
1226
  exportJSON() {
1179
1227
  return {
1228
+ ...super.exportJSON(),
1180
1229
  type: "action_text_attachment_upload",
1181
1230
  version: 1,
1182
- progress: this.progress,
1183
1231
  uploadUrl: this.uploadUrl,
1184
1232
  blobUrlTemplate: this.blobUrlTemplate,
1185
- ...super.exportJSON()
1233
+ progress: this.progress,
1234
+ width: this.width,
1235
+ height: this.height,
1236
+ uploadError: this.uploadError
1186
1237
  }
1187
1238
  }
1188
1239
 
1240
+ get #uploadStarted() {
1241
+ return this.progress !== null
1242
+ }
1243
+
1244
+ #createDOMForError() {
1245
+ const figure = this.createAttachmentFigure();
1246
+ figure.classList.add("attachment--error");
1247
+ figure.appendChild(createElement("div", { innerText: `Error uploading ${this.file?.name ?? "file"}` }));
1248
+ return figure
1249
+ }
1250
+
1189
1251
  #createDOMForImage() {
1190
1252
  return createElement("img")
1191
1253
  }
@@ -1211,94 +1273,126 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
1211
1273
  return figcaption
1212
1274
  }
1213
1275
 
1214
- #loadFigure(figure) {
1215
- const image = figure.querySelector("img");
1216
- if (!image) {
1217
- return Promise.resolve()
1218
- } else {
1219
- return loadFileIntoImage(this.file, image)
1220
- }
1276
+ #createProgressBar() {
1277
+ return createElement("progress", { value: this.progress ?? 0, max: 100 })
1278
+ }
1279
+
1280
+ #setDimensionsFromImage({ width, height }) {
1281
+ if (this.#hasDimensions) return
1282
+
1283
+ this.editor.update(() => {
1284
+ const writable = this.getWritable();
1285
+ writable.width = width;
1286
+ writable.height = height;
1287
+ }, { tag: SILENT_UPDATE_TAGS });
1288
+ }
1289
+
1290
+ get #hasDimensions() {
1291
+ return Boolean(this.width && this.height)
1221
1292
  }
1222
1293
 
1223
- async #startUpload(progressBar, figure) {
1294
+ async #startUploadIfNeeded() {
1295
+ if (this.#uploadStarted) return
1296
+
1297
+ this.#setUploadStarted();
1298
+
1224
1299
  const { DirectUpload } = await import('@rails/activestorage');
1225
- const shouldAuthenticateUploads = Lexxy.global.get("authenticatedUploads");
1226
1300
 
1227
1301
  const upload = new DirectUpload(this.file, this.uploadUrl, this);
1302
+ upload.delegate = this.#createUploadDelegate();
1303
+ upload.create((error, blob) => {
1304
+ if (error) {
1305
+ this.#handleUploadError(error);
1306
+ } else {
1307
+ this.#showUploadedAttachment(blob);
1308
+ }
1309
+ });
1310
+ }
1311
+
1312
+ #createUploadDelegate() {
1313
+ const shouldAuthenticateUploads = Lexxy.global.get("authenticatedUploads");
1228
1314
 
1229
- upload.delegate = {
1315
+ return {
1230
1316
  directUploadWillCreateBlobWithXHR: (request) => {
1231
1317
  if (shouldAuthenticateUploads) request.withCredentials = true;
1232
1318
  },
1233
1319
  directUploadWillStoreFileWithXHR: (request) => {
1234
1320
  if (shouldAuthenticateUploads) request.withCredentials = true;
1235
1321
 
1236
- request.upload.addEventListener("progress", (event) => {
1237
- this.editor.update(() => {
1238
- progressBar.value = Math.round(event.loaded / event.total * 100);
1239
- });
1240
- });
1322
+ const uploadProgressHandler = (event) => this.#handleUploadProgress(event);
1323
+ request.upload.addEventListener("progress", uploadProgressHandler);
1241
1324
  }
1242
- };
1325
+ }
1326
+ }
1243
1327
 
1244
- upload.create((error, blob) => {
1245
- if (error) {
1246
- this.#handleUploadError(figure);
1247
- } else {
1248
- this.#loadFigurePreviewFromBlob(blob, figure).then(() => {
1249
- this.#showUploadedAttachment(figure, blob);
1250
- });
1251
- }
1252
- });
1328
+ #setUploadStarted() {
1329
+ this.#setProgress(1);
1253
1330
  }
1254
1331
 
1255
- #handleUploadError(figure) {
1256
- figure.innerHTML = "";
1257
- figure.classList.add("attachment--error");
1258
- figure.appendChild(createElement("div", { innerText: `Error uploading ${this.file?.name ?? "image"}` }));
1332
+ #handleUploadProgress(event) {
1333
+ this.#setProgress(Math.round(event.loaded / event.total * 100));
1259
1334
  }
1260
1335
 
1261
- async #showUploadedAttachment(figure, blob) {
1336
+ #setProgress(progress) {
1262
1337
  this.editor.update(() => {
1263
- const image = figure.querySelector("img");
1264
-
1265
- const src = this.blobUrlTemplate
1266
- .replace(":signed_id", blob.signed_id)
1267
- .replace(":filename", encodeURIComponent(blob.filename));
1268
- const latest = $getNodeByKey(this.getKey());
1269
- if (latest) {
1270
- latest.replace(new ActionTextAttachmentNode({
1271
- tagName: this.tagName,
1272
- sgid: blob.attachable_sgid,
1273
- src: blob.previewable ? blob.url : src,
1274
- altText: blob.filename,
1275
- contentType: blob.content_type,
1276
- fileName: blob.filename,
1277
- fileSize: blob.byte_size,
1278
- width: image?.naturalWidth,
1279
- previewable: blob.previewable,
1280
- height: image?.naturalHeight
1281
- }));
1282
- }
1283
- }, { tag: HISTORY_MERGE_TAG });
1338
+ this.getWritable().progress = progress;
1339
+ }, { tag: SILENT_UPDATE_TAGS });
1284
1340
  }
1285
1341
 
1286
- async #loadFigurePreviewFromBlob(blob, figure) {
1287
- if (blob.previewable) {
1288
- return new Promise((resolve) => {
1289
- this.editor.update(() => {
1290
- const image = this.#createDOMForImage();
1291
- image.addEventListener("load", () => {
1292
- resolve();
1293
- });
1294
- image.src = blob.url;
1295
- figure.insertBefore(image, figure.firstChild);
1296
- });
1297
- })
1298
- } else {
1299
- return Promise.resolve()
1342
+ #handleUploadError(error) {
1343
+ console.warn(`Upload error for ${this.file?.name ?? "file"}: ${error}`);
1344
+ this.editor.update(() => {
1345
+ this.getWritable().uploadError = true;
1346
+ }, { tag: SILENT_UPDATE_TAGS });
1347
+ }
1348
+
1349
+ async #showUploadedAttachment(blob) {
1350
+ this.editor.update(() => {
1351
+ this.replace(this.#toActionTextAttachmentNodeWith(blob));
1352
+ }, { tag: SILENT_UPDATE_TAGS });
1353
+ }
1354
+
1355
+ #toActionTextAttachmentNodeWith(blob) {
1356
+ const conversion = new AttachmentNodeConversion(this, blob);
1357
+ return conversion.toAttachmentNode()
1358
+ }
1359
+ }
1360
+
1361
+ class AttachmentNodeConversion {
1362
+ constructor(uploadNode, blob) {
1363
+ this.uploadNode = uploadNode;
1364
+ this.blob = blob;
1365
+ }
1366
+
1367
+ toAttachmentNode() {
1368
+ return new ActionTextAttachmentNode({
1369
+ ...this.uploadNode,
1370
+ ...this.#propertiesFromBlob,
1371
+ src: this.#src
1372
+ })
1373
+ }
1374
+
1375
+ get #propertiesFromBlob() {
1376
+ const { blob } = this;
1377
+ return {
1378
+ sgid: blob.attachable_sgid,
1379
+ altText: blob.filename,
1380
+ contentType: blob.content_type,
1381
+ fileName: blob.filename,
1382
+ fileSize: blob.byte_size,
1383
+ previewable: blob.previewable,
1300
1384
  }
1301
1385
  }
1386
+
1387
+ get #src() {
1388
+ return this.blob.previewable ? this.blob.url : this.#blobSrc
1389
+ }
1390
+
1391
+ get #blobSrc() {
1392
+ return this.uploadNode.blobUrlTemplate
1393
+ .replace(":signed_id", this.blob.signed_id)
1394
+ .replace(":filename", encodeURIComponent(this.blob.filename))
1395
+ }
1302
1396
  }
1303
1397
 
1304
1398
  class HorizontalDividerNode extends DecoratorNode {
@@ -1373,30 +1467,6 @@ class HorizontalDividerNode extends DecoratorNode {
1373
1467
  }
1374
1468
  }
1375
1469
 
1376
- class WrappedTableNode extends TableNode {
1377
- static clone(node) {
1378
- return new WrappedTableNode(node.__key)
1379
- }
1380
-
1381
- exportDOM(editor) {
1382
- const superExport = super.exportDOM(editor);
1383
-
1384
- return {
1385
- ...superExport,
1386
- after: (tableElement) => {
1387
- if (superExport.after) {
1388
- tableElement = superExport.after(tableElement);
1389
- const clonedTable = tableElement.cloneNode(true);
1390
- const wrappedTable = createElement("figure", { className: "lexxy-content__table-wrapper" }, clonedTable.outerHTML);
1391
- return wrappedTable
1392
- }
1393
-
1394
- return tableElement
1395
- }
1396
- }
1397
- }
1398
- }
1399
-
1400
1470
  const COMMANDS = [
1401
1471
  "bold",
1402
1472
  "italic",
@@ -1414,13 +1484,6 @@ const COMMANDS = [
1414
1484
  "uploadAttachments",
1415
1485
 
1416
1486
  "insertTable",
1417
- "insertTableRowAbove",
1418
- "insertTableRowBelow",
1419
- "insertTableColumnAfter",
1420
- "insertTableColumnBefore",
1421
- "deleteTableRow",
1422
- "deleteTableColumn",
1423
- "deleteTable",
1424
1487
 
1425
1488
  "undo",
1426
1489
  "redo"
@@ -1529,9 +1592,7 @@ class CommandDispatcher {
1529
1592
  }
1530
1593
 
1531
1594
  dispatchInsertHorizontalDivider() {
1532
- this.editor.update(() => {
1533
- this.contents.insertAtCursorEnsuringLineBelow(new HorizontalDividerNode());
1534
- });
1595
+ this.contents.insertAtCursorEnsuringLineBelow(new HorizontalDividerNode());
1535
1596
 
1536
1597
  this.editor.focus();
1537
1598
  }
@@ -1593,41 +1654,6 @@ class CommandDispatcher {
1593
1654
  this.editor.dispatchCommand(INSERT_TABLE_COMMAND, { "rows": 3, "columns": 3, "includeHeaders": true });
1594
1655
  }
1595
1656
 
1596
- dispatchInsertTableRowBelow() {
1597
- $insertTableRowAtSelection(true);
1598
- }
1599
-
1600
- dispatchInsertTableRowAbove() {
1601
- $insertTableRowAtSelection(false);
1602
- }
1603
-
1604
- dispatchInsertTableColumnAfter() {
1605
- $insertTableColumnAtSelection(true);
1606
- }
1607
-
1608
- dispatchInsertTableColumnBefore() {
1609
- $insertTableColumnAtSelection(false);
1610
- }
1611
-
1612
- dispatchDeleteTableRow() {
1613
- $deleteTableRowAtSelection();
1614
- }
1615
-
1616
- dispatchDeleteTableColumn() {
1617
- $deleteTableColumnAtSelection();
1618
- }
1619
-
1620
- dispatchDeleteTable() {
1621
- this.editor.update(() => {
1622
- const selection = $getSelection();
1623
- if (!$isRangeSelection(selection)) return
1624
-
1625
- const anchorNode = selection.anchor.getNode();
1626
- const tableNode = $findTableNode(anchorNode);
1627
- tableNode.remove();
1628
- });
1629
- }
1630
-
1631
1657
  dispatchUndo() {
1632
1658
  this.editor.dispatchCommand(UNDO_COMMAND, undefined);
1633
1659
  }
@@ -1921,6 +1947,14 @@ class Selection {
1921
1947
  return $getNearestNodeOfType(anchorNode, CodeNode) !== null
1922
1948
  }
1923
1949
 
1950
+ get isTableCellSelected() {
1951
+ const selection = $getSelection();
1952
+ if (!$isRangeSelection(selection)) return false
1953
+
1954
+ const anchorNode = selection.anchor.getNode();
1955
+ return $getNearestNodeOfType(anchorNode, TableCellNode) !== null
1956
+ }
1957
+
1924
1958
  get nodeAfterCursor() {
1925
1959
  const { anchorNode, offset } = this.#getCollapsedSelectionData();
1926
1960
  if (!anchorNode) return null
@@ -2433,17 +2467,6 @@ function sanitize(html) {
2433
2467
  return DOMPurify.sanitize(html, buildConfig())
2434
2468
  }
2435
2469
 
2436
- // Prevent the hardcoded background color
2437
- // A background color value is set by Lexical if background is null:
2438
- // https://github.com/facebook/lexical/blob/5bbbe849bd229e1db0e7b536e6a919520ada7bb2/packages/lexical-table/src/LexicalTableCellNode.ts#L187
2439
- function registerHeaderBackgroundTransform(editor) {
2440
- return editor.registerNodeTransform(TableCellNode, (node) => {
2441
- if (node.getBackgroundColor() === null) {
2442
- node.setBackgroundColor("");
2443
- }
2444
- })
2445
- }
2446
-
2447
2470
  function dasherize(value) {
2448
2471
  return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)
2449
2472
  }
@@ -2467,6 +2490,10 @@ function filterMatches(text, potentialMatch) {
2467
2490
  return normalizeFilteredText(text).includes(normalizeFilteredText(potentialMatch))
2468
2491
  }
2469
2492
 
2493
+ function upcaseFirst(string) {
2494
+ return string.charAt(0).toUpperCase() + string.slice(1)
2495
+ }
2496
+
2470
2497
  class EditorConfiguration {
2471
2498
  #editorElement
2472
2499
  #config
@@ -2908,15 +2935,14 @@ class Contents {
2908
2935
  new FormatEscaper(editorElement).monitor();
2909
2936
  }
2910
2937
 
2911
- insertHtml(html) {
2938
+ insertHtml(html, { tag } = {}) {
2912
2939
  this.editor.update(() => {
2913
2940
  const selection = $getSelection();
2914
-
2915
2941
  if (!$isRangeSelection(selection)) return
2916
2942
 
2917
2943
  const nodes = $generateNodesFromDOM(this.editor, parseHtml(html));
2918
2944
  selection.insertNodes(nodes);
2919
- });
2945
+ }, { tag });
2920
2946
  }
2921
2947
 
2922
2948
  insertAtCursor(node) {
@@ -3156,7 +3182,7 @@ class Contents {
3156
3182
  const blobUrlTemplate = this.editorElement.blobUrlTemplate;
3157
3183
 
3158
3184
  this.editor.update(() => {
3159
- const uploadedImageNode = new ActionTextAttachmentUploadNode({ file: file, uploadUrl: uploadUrl, blobUrlTemplate: blobUrlTemplate, editor: this.editor });
3185
+ const uploadedImageNode = new ActionTextAttachmentUploadNode({ file: file, uploadUrl: uploadUrl, blobUrlTemplate: blobUrlTemplate });
3160
3186
  this.insertAtCursor(uploadedImageNode);
3161
3187
  }, { tag: HISTORY_MERGE_TAG });
3162
3188
  }
@@ -3719,14 +3745,14 @@ class Clipboard {
3719
3745
 
3720
3746
  #pasteMarkdown(text) {
3721
3747
  const html = marked(text);
3722
- this.contents.insertHtml(html);
3748
+ this.contents.insertHtml(html, { tag: [ PASTE_TAG ] });
3723
3749
  }
3724
3750
 
3725
3751
  #pasteRichText(clipboardData) {
3726
3752
  this.editor.update(() => {
3727
3753
  const selection = $getSelection();
3728
3754
  $insertDataTransferForRichText(clipboardData, selection, this.editor);
3729
- });
3755
+ }, { tag: PASTE_TAG });
3730
3756
  }
3731
3757
 
3732
3758
  #handlePastedFiles(clipboardData) {
@@ -3773,7 +3799,7 @@ class Extensions {
3773
3799
 
3774
3800
  initializeToolbars() {
3775
3801
  if (this.#lexxyToolbar) {
3776
- this.enabledExtensions.forEach(ext => ext.initializeToobar(this.#lexxyToolbar));
3802
+ this.enabledExtensions.forEach(ext => ext.initializeToolbar(this.#lexxyToolbar));
3777
3803
  }
3778
3804
  }
3779
3805
 
@@ -3960,6 +3986,124 @@ function $applyLanguage(conversionOutput, element) {
3960
3986
  conversionOutput.node.setLanguage(language);
3961
3987
  }
3962
3988
 
3989
+ class WrappedTableNode extends TableNode {
3990
+ static clone(node) {
3991
+ return new WrappedTableNode(node.__key)
3992
+ }
3993
+
3994
+ exportDOM(editor) {
3995
+ const superExport = super.exportDOM(editor);
3996
+
3997
+ return {
3998
+ ...superExport,
3999
+ after: (tableElement) => {
4000
+ if (superExport.after) {
4001
+ tableElement = superExport.after(tableElement);
4002
+ const clonedTable = tableElement.cloneNode(true);
4003
+ const wrappedTable = createElement("figure", { className: "lexxy-content__table-wrapper" }, clonedTable.outerHTML);
4004
+ return wrappedTable
4005
+ }
4006
+
4007
+ return tableElement
4008
+ }
4009
+ }
4010
+ }
4011
+ }
4012
+
4013
+ const TablesLexicalExtension = defineExtension({
4014
+ name: "lexxy/tables",
4015
+ nodes: [
4016
+ WrappedTableNode,
4017
+ {
4018
+ replace: TableNode,
4019
+ with: () => new WrappedTableNode()
4020
+ },
4021
+ TableCellNode,
4022
+ TableRowNode
4023
+ ],
4024
+ register(editor) {
4025
+ // Register Lexical table plugins
4026
+ registerTablePlugin(editor);
4027
+ registerTableSelectionObserver(editor, true);
4028
+ setScrollableTablesActive(editor, true);
4029
+
4030
+ // Bug fix: Prevent hardcoded background color (Lexical #8089)
4031
+ editor.registerNodeTransform(TableCellNode, (node) => {
4032
+ if (node.getBackgroundColor() === null) {
4033
+ node.setBackgroundColor("");
4034
+ }
4035
+ });
4036
+
4037
+ // Bug fix: Fix column header states (Lexical #8090)
4038
+ editor.registerNodeTransform(TableCellNode, (node) => {
4039
+ const headerState = node.getHeaderStyles();
4040
+
4041
+ if (headerState !== TableCellHeaderStates.ROW) return
4042
+
4043
+ const rowParent = node.getParent();
4044
+ const tableNode = rowParent?.getParent();
4045
+ if (!tableNode) return
4046
+
4047
+ const rows = tableNode.getChildren();
4048
+ const cellIndex = rowParent.getChildren().indexOf(node);
4049
+
4050
+ const cellsInRow = rowParent.getChildren();
4051
+ const isHeaderRow = cellsInRow.every(cell =>
4052
+ cell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS
4053
+ );
4054
+
4055
+ const isHeaderColumn = rows.every(row => {
4056
+ const cell = row.getChildren()[cellIndex];
4057
+ return cell && cell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS
4058
+ });
4059
+
4060
+ let newHeaderState = TableCellHeaderStates.NO_STATUS;
4061
+
4062
+ if (isHeaderRow) {
4063
+ newHeaderState |= TableCellHeaderStates.ROW;
4064
+ }
4065
+
4066
+ if (isHeaderColumn) {
4067
+ newHeaderState |= TableCellHeaderStates.COLUMN;
4068
+ }
4069
+
4070
+ if (newHeaderState !== headerState) {
4071
+ node.setHeaderStyles(newHeaderState, TableCellHeaderStates.BOTH);
4072
+ }
4073
+ });
4074
+
4075
+ editor.registerCommand("insertTableRowAfter", () => {
4076
+ $insertTableRowAtSelection(true);
4077
+ }, COMMAND_PRIORITY_NORMAL);
4078
+
4079
+ editor.registerCommand("insertTableRowBefore", () => {
4080
+ $insertTableRowAtSelection(false);
4081
+ }, COMMAND_PRIORITY_NORMAL);
4082
+
4083
+ editor.registerCommand("insertTableColumnAfter", () => {
4084
+ $insertTableColumnAtSelection(true);
4085
+ }, COMMAND_PRIORITY_NORMAL);
4086
+
4087
+ editor.registerCommand("insertTableColumnBefore", () => {
4088
+ $insertTableColumnAtSelection(false);
4089
+ }, COMMAND_PRIORITY_NORMAL);
4090
+
4091
+ editor.registerCommand("deleteTableRow", () => {
4092
+ $deleteTableRowAtSelection();
4093
+ }, COMMAND_PRIORITY_NORMAL);
4094
+
4095
+ editor.registerCommand("deleteTableColumn", () => {
4096
+ $deleteTableColumnAtSelection();
4097
+ }, COMMAND_PRIORITY_NORMAL);
4098
+
4099
+ editor.registerCommand("deleteTable", () => {
4100
+ const selection = $getSelection();
4101
+ if (!$isRangeSelection(selection)) return false
4102
+ $findTableNode(selection.anchor.getNode())?.remove();
4103
+ }, COMMAND_PRIORITY_NORMAL);
4104
+ }
4105
+ });
4106
+
3963
4107
  class LexicalEditorElement extends HTMLElement {
3964
4108
  static formAssociated = true
3965
4109
  static debug = false
@@ -4149,10 +4293,8 @@ class LexicalEditorElement extends HTMLElement {
4149
4293
  #initialize() {
4150
4294
  this.#synchronizeWithChanges();
4151
4295
  this.#registerComponents();
4152
- this.#listenForInvalidatedNodes();
4153
4296
  this.#handleEnter();
4154
- this.#handleFocus();
4155
- this.#handleTables();
4297
+ this.#registerFocusEvents();
4156
4298
  this.#attachDebugHooks();
4157
4299
  this.#attachToolbar();
4158
4300
  this.#loadInitialValue();
@@ -4163,11 +4305,11 @@ class LexicalEditorElement extends HTMLElement {
4163
4305
  this.editorContentElement ||= this.#createEditorContentElement();
4164
4306
 
4165
4307
  const editor = buildEditorFromExtensions({
4166
- name: "lexxy/core",
4167
- namespace: "Lexxy",
4168
- theme: theme,
4169
- nodes: this.#lexicalNodes
4170
- },
4308
+ name: "lexxy/core",
4309
+ namespace: "Lexxy",
4310
+ theme: theme,
4311
+ nodes: this.#lexicalNodes
4312
+ },
4171
4313
  ...this.#lexicalExtensions
4172
4314
  );
4173
4315
 
@@ -4177,10 +4319,11 @@ class LexicalEditorElement extends HTMLElement {
4177
4319
  }
4178
4320
 
4179
4321
  get #lexicalExtensions() {
4180
- const extensions = [ ];
4322
+ const extensions = [];
4181
4323
  const richTextExtensions = [
4182
4324
  this.highlighter.lexicalExtension,
4183
- TrixContentExtension
4325
+ TrixContentExtension,
4326
+ TablesLexicalExtension
4184
4327
  ];
4185
4328
 
4186
4329
  if (this.supportsRichText) {
@@ -4205,14 +4348,7 @@ class LexicalEditorElement extends HTMLElement {
4205
4348
  CodeHighlightNode,
4206
4349
  LinkNode,
4207
4350
  AutoLinkNode,
4208
- HorizontalDividerNode,
4209
- WrappedTableNode,
4210
- {
4211
- replace: TableNode,
4212
- with: () => { return new WrappedTableNode() }
4213
- },
4214
- TableCellNode,
4215
- TableRowNode,
4351
+ HorizontalDividerNode
4216
4352
  );
4217
4353
  }
4218
4354
 
@@ -4326,11 +4462,8 @@ class LexicalEditorElement extends HTMLElement {
4326
4462
  }
4327
4463
 
4328
4464
  #registerTableComponents() {
4329
- registerTablePlugin(this.editor);
4330
- this.tableHandler = createElement("lexxy-table-handler");
4331
- this.append(this.tableHandler);
4332
-
4333
- this.#addUnregisterHandler(registerHeaderBackgroundTransform(this.editor));
4465
+ this.tableTools = createElement("lexxy-table-tools");
4466
+ this.append(this.tableTools);
4334
4467
  }
4335
4468
 
4336
4469
  #registerCodeHiglightingComponents() {
@@ -4339,21 +4472,6 @@ class LexicalEditorElement extends HTMLElement {
4339
4472
  this.append(this.codeLanguagePicker);
4340
4473
  }
4341
4474
 
4342
- #listenForInvalidatedNodes() {
4343
- this.editor.getRootElement().addEventListener("lexxy:internal:invalidate-node", (event) => {
4344
- const { key, values } = event.detail;
4345
-
4346
- this.editor.update(() => {
4347
- const node = $getNodeByKey(key);
4348
-
4349
- if (node instanceof ActionTextAttachmentNode) {
4350
- const updatedNode = node.getWritable();
4351
- Object.assign(updatedNode, values);
4352
- }
4353
- });
4354
- });
4355
- }
4356
-
4357
4475
  #handleEnter() {
4358
4476
  // We can't prevent these externally using regular keydown because Lexical handles it first.
4359
4477
  this.editor.registerCommand(
@@ -4377,12 +4495,27 @@ class LexicalEditorElement extends HTMLElement {
4377
4495
  );
4378
4496
  }
4379
4497
 
4380
- #handleFocus() {
4381
- // Lexxy handles focus and blur as commands
4382
- // see https://github.com/facebook/lexical/blob/d1a8e84fe9063a4f817655b346b6ff373aa107f0/packages/lexical/src/LexicalEvents.ts#L35
4383
- // and https://stackoverflow.com/a/72212077
4384
- this.editor.registerCommand(BLUR_COMMAND, () => { dispatch(this, "lexxy:blur"); }, COMMAND_PRIORITY_NORMAL);
4385
- this.editor.registerCommand(FOCUS_COMMAND, () => { dispatch(this, "lexxy:focus"); }, COMMAND_PRIORITY_NORMAL);
4498
+ #registerFocusEvents() {
4499
+ this.addEventListener("focusin", this.#handleFocusIn);
4500
+ this.addEventListener("focusout", this.#handleFocusOut);
4501
+ }
4502
+
4503
+ #handleFocusIn(event) {
4504
+ if (this.#elementInEditorOrToolbar(event.target) && !this.currentlyFocused) {
4505
+ dispatch(this, "lexxy:focus");
4506
+ this.currentlyFocused = true;
4507
+ }
4508
+ }
4509
+
4510
+ #handleFocusOut(event) {
4511
+ if (!this.#elementInEditorOrToolbar(event.relatedTarget)) {
4512
+ dispatch(this, "lexxy:blur");
4513
+ this.currentlyFocused = false;
4514
+ }
4515
+ }
4516
+
4517
+ #elementInEditorOrToolbar(element) {
4518
+ return this.contains(element) || this.toolbarElement?.contains(element)
4386
4519
  }
4387
4520
 
4388
4521
  #onFocus() {
@@ -4399,22 +4532,9 @@ class LexicalEditorElement extends HTMLElement {
4399
4532
  }
4400
4533
  }
4401
4534
 
4402
- #handleTables() {
4403
- if (this.supportsRichText) {
4404
- this.removeTableSelectionObserver = registerTableSelectionObserver(this.editor, true);
4405
- setScrollableTablesActive(this.editor, true);
4406
- }
4407
- }
4408
4535
 
4409
4536
  #attachDebugHooks() {
4410
- if (!LexicalEditorElement.debug) return
4411
-
4412
- this.#addUnregisterHandler(this.editor.registerUpdateListener(({ editorState }) => {
4413
- editorState.read(() => {
4414
- console.debug("HTML: ", this.value, "String:", this.toString());
4415
- console.debug("empty", this.isEmpty, "blank", this.isBlank);
4416
- });
4417
- }));
4537
+ return
4418
4538
  }
4419
4539
 
4420
4540
  #attachToolbar() {
@@ -4494,18 +4614,17 @@ class LexicalEditorElement extends HTMLElement {
4494
4614
  }
4495
4615
  }
4496
4616
 
4497
- customElements.define("lexxy-editor", LexicalEditorElement);
4498
-
4499
4617
  class ToolbarDropdown extends HTMLElement {
4500
4618
  connectedCallback() {
4501
4619
  this.container = this.closest("details");
4502
4620
 
4503
4621
  this.container.addEventListener("toggle", this.#handleToggle.bind(this));
4504
4622
  this.container.addEventListener("keydown", this.#handleKeyDown.bind(this));
4623
+
4624
+ this.#onToolbarEditor(this.initialize.bind(this));
4505
4625
  }
4506
4626
 
4507
4627
  disconnectedCallback() {
4508
- this.#removeClickOutsideHandler();
4509
4628
  this.container.removeEventListener("keydown", this.#handleKeyDown.bind(this));
4510
4629
  }
4511
4630
 
@@ -4521,48 +4640,31 @@ class ToolbarDropdown extends HTMLElement {
4521
4640
  return this.toolbar.editor
4522
4641
  }
4523
4642
 
4643
+ initialize() {
4644
+ // Any post-editor initialization
4645
+ }
4646
+
4524
4647
  close() {
4525
- this.container.removeAttribute("open");
4648
+ this.editor.focus();
4649
+ this.container.open = false;
4526
4650
  }
4527
4651
 
4528
- #handleToggle(event) {
4529
- if (this.container.open) {
4530
- this.#handleOpen(event.target);
4531
- } else {
4532
- this.#handleClose();
4652
+ async #onToolbarEditor(callback) {
4653
+ await this.toolbar.editorConnected;
4654
+ callback();
4655
+ }
4656
+
4657
+ #handleToggle() {
4658
+ if (this.container.open) {
4659
+ this.#handleOpen();
4533
4660
  }
4534
4661
  }
4535
4662
 
4536
- #handleOpen() {
4663
+ async #handleOpen() {
4537
4664
  this.#interactiveElements[0].focus();
4538
- this.#setupClickOutsideHandler();
4539
-
4540
4665
  this.#resetTabIndexValues();
4541
4666
  }
4542
4667
 
4543
- #handleClose() {
4544
- this.#removeClickOutsideHandler();
4545
- this.editor.focus();
4546
- }
4547
-
4548
- #setupClickOutsideHandler() {
4549
- if (this.clickOutsideHandler) return
4550
-
4551
- this.clickOutsideHandler = this.#handleClickOutside.bind(this);
4552
- document.addEventListener("click", this.clickOutsideHandler, true);
4553
- }
4554
-
4555
- #removeClickOutsideHandler() {
4556
- if (!this.clickOutsideHandler) return
4557
-
4558
- document.removeEventListener("click", this.clickOutsideHandler, true);
4559
- this.clickOutsideHandler = null;
4560
- }
4561
-
4562
- #handleClickOutside({ target }) {
4563
- if (this.container.open && !this.container.contains(target)) this.close();
4564
- }
4565
-
4566
4668
  #handleKeyDown(event) {
4567
4669
  if (event.key === "Escape") {
4568
4670
  event.stopPropagation();
@@ -4637,8 +4739,6 @@ class LinkDropdown extends ToolbarDropdown {
4637
4739
  }
4638
4740
  }
4639
4741
 
4640
- customElements.define("lexxy-link-dropdown", LinkDropdown);
4641
-
4642
4742
  const APPLY_HIGHLIGHT_SELECTOR = "button.lexxy-highlight-button";
4643
4743
  const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
4644
4744
 
@@ -4648,19 +4748,14 @@ const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
4648
4748
  const NO_STYLE = Symbol("no_style");
4649
4749
 
4650
4750
  class HighlightDropdown extends ToolbarDropdown {
4651
- #initialized = false
4652
-
4653
4751
  connectedCallback() {
4654
4752
  super.connectedCallback();
4655
4753
  this.#registerToggleHandler();
4656
4754
  }
4657
4755
 
4658
- #ensureInitialized() {
4659
- if (this.#initialized) return
4660
-
4756
+ initialize() {
4661
4757
  this.#setUpButtons();
4662
4758
  this.#registerButtonHandlers();
4663
- this.#initialized = true;
4664
4759
  }
4665
4760
 
4666
4761
  #registerToggleHandler() {
@@ -4673,16 +4768,18 @@ class HighlightDropdown extends ToolbarDropdown {
4673
4768
  }
4674
4769
 
4675
4770
  #setUpButtons() {
4676
- this.#buttonGroups.forEach(buttonGroup => {
4677
- this.#populateButtonGroup(buttonGroup);
4678
- });
4771
+ const colorGroups = this.editorElement.config.get("highlight.buttons");
4772
+
4773
+ this.#populateButtonGroup("color", colorGroups.color);
4774
+ this.#populateButtonGroup("background-color", colorGroups["background-color"]);
4775
+
4776
+ const maxNumberOfColors = Math.max(colorGroups.color.length, colorGroups["background-color"].length);
4777
+ this.style.setProperty("--max-colors", maxNumberOfColors);
4679
4778
  }
4680
4779
 
4681
- #populateButtonGroup(buttonGroup) {
4682
- const attribute = buttonGroup.dataset.buttonGroup;
4683
- const values = this.editorElement.config.get(`highlight.buttons.${attribute}`) || [];
4780
+ #populateButtonGroup(attribute, values) {
4684
4781
  values.forEach((value, index) => {
4685
- buttonGroup.appendChild(this.#createButton(attribute, value, index));
4782
+ this.#buttonContainer.appendChild(this.#createButton(attribute, value, index));
4686
4783
  });
4687
4784
  }
4688
4785
 
@@ -4691,15 +4788,13 @@ class HighlightDropdown extends ToolbarDropdown {
4691
4788
  button.dataset.style = attribute;
4692
4789
  button.style.setProperty(attribute, value);
4693
4790
  button.dataset.value = value;
4694
- button.classList.add("lexxy-highlight-button");
4791
+ button.classList.add("lexxy-editor__toolbar-button", "lexxy-highlight-button");
4695
4792
  button.name = attribute + "-" + index;
4696
4793
  return button
4697
4794
  }
4698
4795
 
4699
4796
  #handleToggle({ newState }) {
4700
4797
  if (newState === "open") {
4701
- this.#ensureInitialized();
4702
-
4703
4798
  this.editor.getEditorState().read(() => {
4704
4799
  this.#updateColorButtonStates($getSelection());
4705
4800
  });
@@ -4742,8 +4837,8 @@ class HighlightDropdown extends ToolbarDropdown {
4742
4837
  this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).disabled = !hasHighlight;
4743
4838
  }
4744
4839
 
4745
- get #buttonGroups() {
4746
- return this.querySelectorAll("[data-button-group]")
4840
+ get #buttonContainer() {
4841
+ return this.querySelector(".lexxy-highlight-colors")
4747
4842
  }
4748
4843
 
4749
4844
  get #colorButtons() {
@@ -4751,1204 +4846,1433 @@ class HighlightDropdown extends ToolbarDropdown {
4751
4846
  }
4752
4847
  }
4753
4848
 
4754
- customElements.define("lexxy-highlight-dropdown", HighlightDropdown);
4755
-
4756
- class TableHandler extends HTMLElement {
4757
- connectedCallback() {
4758
- this.#setUpButtons();
4759
- this.#monitorForTableSelection();
4760
- this.#registerKeyboardShortcuts();
4761
- }
4762
-
4763
- disconnectedCallback() {
4764
- this.#unregisterKeyboardShortcuts();
4765
- }
4766
-
4767
- get #editor() {
4768
- return this.#editorElement.editor
4849
+ class BaseSource {
4850
+ // Template method to override
4851
+ async buildListItems(filter = "") {
4852
+ return Promise.resolve([])
4769
4853
  }
4770
4854
 
4771
- get #editorElement() {
4772
- return this.closest("lexxy-editor")
4855
+ // Template method to override
4856
+ promptItemFor(listItem) {
4857
+ return null
4773
4858
  }
4774
4859
 
4775
- get #currentCell() {
4776
- const selection = $getSelection();
4777
- if (!$isRangeSelection(selection)) return null
4860
+ // Protected
4778
4861
 
4779
- const anchorNode = selection.anchor.getNode();
4780
- return $getTableCellNodeFromLexicalNode(anchorNode)
4862
+ buildListItemElementFor(promptItemElement) {
4863
+ const template = promptItemElement.querySelector("template[type='menu']");
4864
+ const fragment = template.content.cloneNode(true);
4865
+ const listItemElement = createElement("li", { role: "option", id: generateDomId("prompt-item"), tabindex: "0" });
4866
+ listItemElement.classList.add("lexxy-prompt-menu__item");
4867
+ listItemElement.appendChild(fragment);
4868
+ return listItemElement
4781
4869
  }
4782
4870
 
4783
- get #currentRow() {
4784
- const currentCell = this.#currentCell;
4785
- if (!currentCell) return 0
4786
- return $getTableRowIndexFromTableCellNode(currentCell)
4871
+ async loadPromptItemsFromUrl(url) {
4872
+ try {
4873
+ const response = await fetch(url);
4874
+ const html = await response.text();
4875
+ const promptItems = parseHtml(html).querySelectorAll("lexxy-prompt-item");
4876
+ return Promise.resolve(Array.from(promptItems))
4877
+ } catch (error) {
4878
+ return Promise.reject(error)
4879
+ }
4787
4880
  }
4881
+ }
4788
4882
 
4789
- get #currentColumn() {
4790
- const currentCell = this.#currentCell;
4791
- if (!currentCell) return 0
4792
- return $getTableColumnIndexFromTableCellNode(currentCell)
4883
+ class LocalFilterSource extends BaseSource {
4884
+ async buildListItems(filter = "") {
4885
+ const promptItems = await this.fetchPromptItems();
4886
+ return this.#buildListItemsFromPromptItems(promptItems, filter)
4793
4887
  }
4794
4888
 
4795
- get #tableHandlerButtons() {
4796
- return Array.from(this.querySelectorAll("button, details > summary"))
4889
+ // Template method to override
4890
+ async fetchPromptItems(filter) {
4891
+ return Promise.resolve([])
4797
4892
  }
4798
4893
 
4799
- #registerKeyboardShortcuts() {
4800
- this.unregisterKeyboardShortcuts = this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleKeyDown, COMMAND_PRIORITY_HIGH);
4894
+ promptItemFor(listItem) {
4895
+ return this.promptItemByListItem.get(listItem)
4801
4896
  }
4802
4897
 
4803
- #unregisterKeyboardShortcuts() {
4804
- this.unregisterKeyboardShortcuts();
4805
- }
4898
+ #buildListItemsFromPromptItems(promptItems, filter) {
4899
+ const listItems = [];
4900
+ this.promptItemByListItem = new WeakMap();
4901
+ promptItems.forEach((promptItem) => {
4902
+ const searchableText = promptItem.getAttribute("search");
4806
4903
 
4807
- #handleKeyDown = (event) => {
4808
- if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === "F10") {
4809
- const firstButton = this.querySelector("button, [tabindex]:not([tabindex='-1'])");
4810
- this.#setFocusStateOnSelectedCell();
4811
- firstButton?.focus();
4812
- } else if (event.key === "Escape") {
4813
- this.#editor.getEditorState().read(() => {
4814
- const cell = this.#currentCell;
4815
- if (!cell) return
4904
+ if (!filter || filterMatches(searchableText, filter)) {
4905
+ const listItem = this.buildListItemElementFor(promptItem);
4906
+ this.promptItemByListItem.set(listItem, promptItem);
4907
+ listItems.push(listItem);
4908
+ }
4909
+ });
4816
4910
 
4817
- this.#editor.update(() => {
4818
- cell.select();
4819
- });
4820
- });
4821
- this.#closeMoreMenu();
4822
- }
4911
+ return listItems
4823
4912
  }
4913
+ }
4824
4914
 
4825
- #handleTableHandlerKeydown = (event) => {
4826
- if (event.key === "Escape") {
4827
- this.#editor.focus();
4828
- } else {
4829
- handleRollingTabIndex(this.#tableHandlerButtons, event);
4830
- }
4915
+ class InlinePromptSource extends LocalFilterSource {
4916
+ constructor(inlinePromptItems) {
4917
+ super();
4918
+ this.inlinePromptItemElements = Array.from(inlinePromptItems);
4831
4919
  }
4832
4920
 
4833
- #setUpButtons() {
4834
- this.appendChild(this.#createRowButtonsContainer());
4835
- this.appendChild(this.#createColumnButtonsContainer());
4836
-
4837
- this.moreMenu = this.#createMoreMenu();
4838
- this.appendChild(this.moreMenu);
4839
- this.addEventListener("keydown", this.#handleTableHandlerKeydown);
4921
+ async fetchPromptItems() {
4922
+ return Promise.resolve(this.inlinePromptItemElements)
4840
4923
  }
4924
+ }
4841
4925
 
4842
- #showTableHandlerButtons() {
4843
- this.style.display = "flex";
4844
- this.#closeMoreMenu();
4926
+ class DeferredPromptSource extends LocalFilterSource {
4927
+ constructor(url) {
4928
+ super();
4929
+ this.url = url;
4845
4930
 
4846
- this.#updateRowColumnCount();
4847
- this.#setTableFocusState(true);
4931
+ this.fetchPromptItems();
4848
4932
  }
4849
4933
 
4850
- #hideTableHandlerButtons() {
4851
- this.style.display = "none";
4852
- this.#closeMoreMenu();
4934
+ async fetchPromptItems() {
4935
+ this.promptItems ??= await this.loadPromptItemsFromUrl(this.url);
4853
4936
 
4854
- this.#setTableFocusState(false);
4855
- this.currentTableNode = null;
4937
+ return Promise.resolve(this.promptItems)
4856
4938
  }
4939
+ }
4857
4940
 
4858
- #updateButtonsPosition(tableNode) {
4859
- const tableElement = this.#editor.getElementByKey(tableNode.getKey());
4860
- if (!tableElement) return
4941
+ const DEBOUNCE_INTERVAL = 200;
4861
4942
 
4862
- const tableRect = tableElement.getBoundingClientRect();
4863
- const editorRect = this.#editorElement.getBoundingClientRect();
4943
+ class RemoteFilterSource extends BaseSource {
4944
+ constructor(url) {
4945
+ super();
4864
4946
 
4865
- const relativeTop = tableRect.top - editorRect.top;
4866
- const relativeCenter = (tableRect.left + tableRect.right) / 2 - editorRect.left;
4867
- this.style.top = `${relativeTop}px`;
4868
- this.style.left = `${relativeCenter}px`;
4947
+ this.baseURL = url;
4948
+ this.loadAndFilterListItems = debounceAsync(this.fetchFilteredListItems.bind(this), DEBOUNCE_INTERVAL);
4869
4949
  }
4870
4950
 
4871
- #updateRowColumnCount() {
4872
- if (!this.currentTableNode) return
4873
-
4874
- const tableElement = $getElementForTableNode(this.#editor, this.currentTableNode);
4875
- if (!tableElement) return
4876
-
4877
- const rowCount = tableElement.rows;
4878
- const columnCount = tableElement.columns;
4879
-
4880
- this.rowCount.textContent = `${rowCount} row${rowCount === 1 ? "" : "s"}`;
4881
- this.columnCount.textContent = `${columnCount} column${columnCount === 1 ? "" : "s"}`;
4951
+ async buildListItems(filter = "") {
4952
+ return await this.loadAndFilterListItems(filter)
4882
4953
  }
4883
4954
 
4884
- #createButton(icon, label, onClick) {
4885
- const button = createElement("button", {
4886
- className: "lexxy-table-control__button",
4887
- "aria-label": label,
4888
- type: "button"
4889
- });
4890
- button.tabIndex = -1;
4891
- button.innerHTML = `${icon} <span>${label}</span>`;
4892
- button.addEventListener("click", onClick.bind(this));
4893
-
4894
- return button
4955
+ promptItemFor(listItem) {
4956
+ return this.promptItemByListItem.get(listItem)
4895
4957
  }
4896
4958
 
4897
- #createRowButtonsContainer() {
4898
- const container = createElement("div", { className: "lexxy-table-control" });
4899
-
4900
- const plusButton = this.#createButton("+", "Add row", () => this.#insertTableRow("end"));
4901
- const minusButton = this.#createButton("−", "Remove row", () => this.#deleteTableRow("end"));
4902
-
4903
- this.rowCount = createElement("span");
4904
- this.rowCount.textContent = "_ rows";
4905
-
4906
- container.appendChild(minusButton);
4907
- container.appendChild(this.rowCount);
4908
- container.appendChild(plusButton);
4909
-
4910
- return container
4959
+ async fetchFilteredListItems(filter) {
4960
+ const promptItems = await this.loadPromptItemsFromUrl(this.#urlFor(filter));
4961
+ return this.#buildListItemsFromPromptItems(promptItems)
4911
4962
  }
4912
4963
 
4913
- #createColumnButtonsContainer() {
4914
- const container = createElement("div", { className: "lexxy-table-control" });
4915
-
4916
- const plusButton = this.#createButton("+", "Add column", () => this.#insertTableColumn("end"));
4917
- const minusButton = this.#createButton("−", "Remove column", () => this.#deleteTableColumn("end"));
4918
-
4919
- this.columnCount = createElement("span");
4920
- this.columnCount.textContent = "_ columns";
4921
-
4922
- container.appendChild(minusButton);
4923
- container.appendChild(this.columnCount);
4924
- container.appendChild(plusButton);
4925
-
4926
- return container
4964
+ #urlFor(filter) {
4965
+ const url = new URL(this.baseURL, window.location.origin);
4966
+ url.searchParams.append("filter", filter);
4967
+ return url.toString()
4927
4968
  }
4928
4969
 
4929
- #createMoreMenu() {
4930
- const container = createElement("details", {
4931
- className: "lexxy-table-control lexxy-table-control__more-menu"
4932
- });
4933
- container.setAttribute("name", "lexxy-dropdown");
4934
-
4935
- container.tabIndex = -1;
4936
-
4937
- const summary = createElement("summary", {}, "•••");
4938
- container.appendChild(summary);
4939
-
4940
- const details = createElement("div", { className: "lexxy-table-control__more-menu-details" });
4941
- container.appendChild(details);
4942
-
4943
- details.appendChild(this.#createRowSection());
4944
- details.appendChild(this.#createColumnSection());
4945
- details.appendChild(this.#createDeleteTableSection());
4970
+ #buildListItemsFromPromptItems(promptItems) {
4971
+ const listItems = [];
4972
+ this.promptItemByListItem = new WeakMap();
4946
4973
 
4947
- container.addEventListener("toggle", this.#handleMoreMenuToggle.bind(this));
4974
+ for (const promptItem of promptItems) {
4975
+ const listItem = this.buildListItemElementFor(promptItem);
4976
+ this.promptItemByListItem.set(listItem, promptItem);
4977
+ listItems.push(listItem);
4978
+ }
4948
4979
 
4949
- return container
4980
+ return listItems
4950
4981
  }
4982
+ }
4951
4983
 
4952
- #createColumnSection() {
4953
- const columnSection = createElement("section", { className: "lexxy-table-control__more-menu-section" });
4954
-
4955
- const columnButtons = [
4956
- { icon: this.#icon("add-column-before"), label: "Add column before", onClick: () => this.#insertTableColumn("left") },
4957
- { icon: this.#icon("add-column-after"), label: "Add column after", onClick: () => this.#insertTableColumn("right") },
4958
- { icon: this.#icon("remove-column"), label: "Remove column", onClick: this.#deleteTableColumn },
4959
- { icon: this.#icon("toggle-column-style"), label: "Toggle column style", onClick: this.#toggleColumnHeaderStyle },
4960
- ];
4961
-
4962
- columnButtons.forEach(button => {
4963
- const buttonElement = this.#createButton(button.icon, button.label, button.onClick);
4964
- columnSection.appendChild(buttonElement);
4965
- });
4984
+ const NOTHING_FOUND_DEFAULT_MESSAGE = "Nothing found";
4966
4985
 
4967
- return columnSection
4986
+ class LexicalPromptElement extends HTMLElement {
4987
+ constructor() {
4988
+ super();
4989
+ this.keyListeners = [];
4968
4990
  }
4969
4991
 
4970
- #createRowSection() {
4971
- const rowSection = createElement("section", { className: "lexxy-table-control__more-menu-section" });
4972
-
4973
- const rowButtons = [
4974
- { icon: this.#icon("add-row-above"), label: "Add row above", onClick: () => this.#insertTableRow("above") },
4975
- { icon: this.#icon("add-row-below"), label: "Add row below", onClick: () => this.#insertTableRow("below") },
4976
- { icon: this.#icon("remove-row"), label: "Remove row", onClick: this.#deleteTableRow },
4977
- { icon: this.#icon("toggle-row-style"), label: "Toggle row style", onClick: this.#toggleRowHeaderStyle }
4978
- ];
4992
+ static observedAttributes = [ "connected" ]
4979
4993
 
4980
- rowButtons.forEach(button => {
4981
- const buttonElement = this.#createButton(button.icon, button.label, button.onClick);
4982
- rowSection.appendChild(buttonElement);
4983
- });
4994
+ connectedCallback() {
4995
+ this.source = this.#createSource();
4984
4996
 
4985
- return rowSection
4997
+ this.#addTriggerListener();
4998
+ this.toggleAttribute("connected", true);
4986
4999
  }
4987
5000
 
4988
- #createDeleteTableSection() {
4989
- const deleteSection = createElement("section", { className: "lexxy-table-control__more-menu-section" });
4990
-
4991
- const deleteButton = { icon: this.#icon("delete-table"), label: "Delete table", onClick: this.#deleteTable };
4992
-
4993
- const buttonElement = this.#createButton(deleteButton.icon, deleteButton.label, deleteButton.onClick);
4994
- deleteSection.appendChild(buttonElement);
4995
-
4996
- return deleteSection
5001
+ disconnectedCallback() {
5002
+ this.source = null;
5003
+ this.popoverElement = null;
4997
5004
  }
4998
5005
 
4999
- #handleMoreMenuToggle() {
5000
- if (this.moreMenu.open) {
5001
- this.#setFocusStateOnSelectedCell();
5002
- } else {
5003
- this.#removeFocusStateFromSelectedCell();
5006
+
5007
+ attributeChangedCallback(name, oldValue, newValue) {
5008
+ if (name === "connected" && this.isConnected && oldValue != null && oldValue !== newValue) {
5009
+ requestAnimationFrame(() => this.#reconnect());
5004
5010
  }
5005
5011
  }
5006
5012
 
5007
- #closeMoreMenu() {
5008
- this.#removeFocusStateFromSelectedCell();
5009
- this.moreMenu.removeAttribute("open");
5013
+ get name() {
5014
+ return this.getAttribute("name")
5010
5015
  }
5011
5016
 
5012
- #monitorForTableSelection() {
5013
- this.#editor.registerUpdateListener(() => {
5014
- this.#editor.getEditorState().read(() => {
5015
- const selection = $getSelection();
5016
- if (!$isRangeSelection(selection)) return
5017
-
5018
- const anchorNode = selection.anchor.getNode();
5019
- const tableNode = $findTableNode(anchorNode);
5020
-
5021
- if (tableNode) {
5022
- this.#tableCellWasSelected(tableNode);
5023
- } else {
5024
- this.#hideTableHandlerButtons();
5025
- }
5026
- });
5027
- });
5017
+ get trigger() {
5018
+ return this.getAttribute("trigger")
5028
5019
  }
5029
5020
 
5030
- #setTableFocusState(focused) {
5031
- this.#editorElement.querySelector("div.node--selected:has(table)")?.classList.remove("node--selected");
5021
+ get supportsSpaceInSearches() {
5022
+ return this.hasAttribute("supports-space-in-searches")
5023
+ }
5032
5024
 
5033
- if (focused && this.currentTableNode) {
5034
- const tableParent = this.#editor.getElementByKey(this.currentTableNode.getKey());
5035
- if (!tableParent) return
5036
- tableParent.classList.add("node--selected");
5037
- }
5025
+ get open() {
5026
+ return this.popoverElement?.classList?.contains("lexxy-prompt-menu--visible")
5038
5027
  }
5039
5028
 
5040
- #tableCellWasSelected(tableNode) {
5041
- this.currentTableNode = tableNode;
5042
- this.#updateButtonsPosition(tableNode);
5043
- this.#showTableHandlerButtons();
5029
+ get closed() {
5030
+ return !this.open
5044
5031
  }
5045
5032
 
5046
- #setFocusStateOnSelectedCell() {
5047
- this.#editor.getEditorState().read(() => {
5048
- const currentCell = this.#currentCell;
5049
- if (!currentCell) return
5033
+ get #doesSpaceSelect() {
5034
+ return !this.supportsSpaceInSearches
5035
+ }
5050
5036
 
5051
- const cellElement = this.#editor.getElementByKey(currentCell.getKey());
5052
- if (!cellElement) return
5037
+ #createSource() {
5038
+ const src = this.getAttribute("src");
5039
+ if (src) {
5040
+ if (this.hasAttribute("remote-filtering")) {
5041
+ return new RemoteFilterSource(src)
5042
+ } else {
5043
+ return new DeferredPromptSource(src)
5044
+ }
5045
+ } else {
5046
+ return new InlinePromptSource(this.querySelectorAll("lexxy-prompt-item"))
5047
+ }
5048
+ }
5049
+
5050
+ #addTriggerListener() {
5051
+ const unregister = this.#editor.registerUpdateListener(({ editorState }) => {
5052
+ editorState.read(() => {
5053
+ const { node, offset } = this.#selection.selectedNodeWithOffset();
5054
+ if (!node) return
5055
+
5056
+ if ($isTextNode(node)) {
5057
+ const fullText = node.getTextContent();
5058
+ const triggerLength = this.trigger.length;
5053
5059
 
5054
- cellElement.classList.add("table-cell--selected");
5060
+ // Check if we have enough characters for the trigger
5061
+ if (offset >= triggerLength) {
5062
+ const textBeforeCursor = fullText.slice(offset - triggerLength, offset);
5063
+
5064
+ // Check if trigger is at the start of the text node (new line case) or preceded by space or newline
5065
+ if (textBeforeCursor === this.trigger) {
5066
+ const isAtStart = offset === triggerLength;
5067
+
5068
+ const charBeforeTrigger = offset > triggerLength ? fullText[offset - triggerLength - 1] : null;
5069
+ const isPrecededBySpaceOrNewline = charBeforeTrigger === " " || charBeforeTrigger === "\n";
5070
+
5071
+ if (isAtStart || isPrecededBySpaceOrNewline) {
5072
+ unregister();
5073
+ this.#showPopover();
5074
+ }
5075
+ }
5076
+ }
5077
+ }
5078
+ });
5055
5079
  });
5056
5080
  }
5057
5081
 
5058
- #removeFocusStateFromSelectedCell() {
5059
- this.#editorElement.querySelector(".table-cell--selected")?.classList.remove("table-cell--selected");
5082
+ #addCursorPositionListener() {
5083
+ this.cursorPositionListener = this.#editor.registerUpdateListener(() => {
5084
+ if (this.closed) return
5085
+
5086
+ this.#editor.read(() => {
5087
+ const { node, offset } = this.#selection.selectedNodeWithOffset();
5088
+ if (!node) return
5089
+
5090
+ if ($isTextNode(node) && offset > 0) {
5091
+ const fullText = node.getTextContent();
5092
+ const textBeforeCursor = fullText.slice(0, offset);
5093
+ const lastTriggerIndex = textBeforeCursor.lastIndexOf(this.trigger);
5094
+ const triggerEndIndex = lastTriggerIndex + this.trigger.length - 1;
5095
+
5096
+ // If trigger is not found, or cursor is at or before the trigger end position, hide popover
5097
+ if (lastTriggerIndex === -1 || offset <= triggerEndIndex) {
5098
+ this.#hidePopover();
5099
+ }
5100
+ } else {
5101
+ // Cursor is not in a text node or at offset 0, hide popover
5102
+ this.#hidePopover();
5103
+ }
5104
+ });
5105
+ });
5060
5106
  }
5061
5107
 
5062
- #selectLastTableCell() {
5063
- if (!this.currentTableNode) return
5108
+ #removeCursorPositionListener() {
5109
+ if (this.cursorPositionListener) {
5110
+ this.cursorPositionListener();
5111
+ this.cursorPositionListener = null;
5112
+ }
5113
+ }
5064
5114
 
5065
- const last = this.currentTableNode.getLastChild().getLastChild();
5066
- if (!$isTableCellNode(last)) return
5115
+ get #editor() {
5116
+ return this.#editorElement.editor
5117
+ }
5067
5118
 
5068
- last.selectEnd();
5119
+ get #editorElement() {
5120
+ return this.closest("lexxy-editor")
5069
5121
  }
5070
5122
 
5071
- #deleteTable() {
5072
- this.#editor.dispatchCommand("deleteTable");
5123
+ get #selection() {
5124
+ return this.#editorElement.selection
5125
+ }
5073
5126
 
5074
- this.#closeMoreMenu();
5075
- this.#updateRowColumnCount();
5127
+ async #showPopover() {
5128
+ this.popoverElement ??= await this.#buildPopover();
5129
+ this.#resetPopoverPosition();
5130
+ await this.#filterOptions();
5131
+ this.popoverElement.classList.toggle("lexxy-prompt-menu--visible", true);
5132
+ this.#selectFirstOption();
5133
+
5134
+ this.#editorElement.addEventListener("keydown", this.#handleKeydownOnPopover);
5135
+ this.#editorElement.addEventListener("lexxy:change", this.#filterOptions);
5136
+
5137
+ this.#registerKeyListeners();
5138
+ this.#addCursorPositionListener();
5076
5139
  }
5077
5140
 
5078
- #insertTableRow(direction) {
5079
- this.#executeTableCommand("insert", "row", direction);
5141
+ #registerKeyListeners() {
5142
+ // We can't use a regular keydown for Enter as Lexical handles it first
5143
+ this.keyListeners.push(this.#editor.registerCommand(KEY_ENTER_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_HIGH));
5144
+ this.keyListeners.push(this.#editor.registerCommand(KEY_TAB_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_HIGH));
5145
+
5146
+ if (this.#doesSpaceSelect) {
5147
+ this.keyListeners.push(this.#editor.registerCommand(KEY_SPACE_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_HIGH));
5148
+ }
5149
+
5150
+ // Register arrow keys with HIGH priority to prevent Lexical's selection handlers from running
5151
+ this.keyListeners.push(this.#editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#handleArrowUp.bind(this), COMMAND_PRIORITY_HIGH));
5152
+ this.keyListeners.push(this.#editor.registerCommand(KEY_ARROW_DOWN_COMMAND, this.#handleArrowDown.bind(this), COMMAND_PRIORITY_HIGH));
5153
+ }
5154
+
5155
+ #handleArrowUp(event) {
5156
+ this.#moveSelectionUp();
5157
+ event.preventDefault();
5158
+ return true
5159
+ }
5160
+
5161
+ #handleArrowDown(event) {
5162
+ this.#moveSelectionDown();
5163
+ event.preventDefault();
5164
+ return true
5165
+ }
5166
+
5167
+ #selectFirstOption() {
5168
+ const firstOption = this.#listItemElements[0];
5169
+
5170
+ if (firstOption) {
5171
+ this.#selectOption(firstOption);
5172
+ }
5173
+ }
5174
+
5175
+ get #listItemElements() {
5176
+ return Array.from(this.popoverElement.querySelectorAll(".lexxy-prompt-menu__item"))
5177
+ }
5178
+
5179
+ #selectOption(listItem) {
5180
+ this.#clearSelection();
5181
+ listItem.toggleAttribute("aria-selected", true);
5182
+ listItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
5183
+ listItem.focus();
5184
+
5185
+ // Preserve selection to prevent cursor jump
5186
+ this.#selection.preservingSelection(() => {
5187
+ this.#editorElement.focus();
5188
+ });
5189
+
5190
+ this.#editorContentElement.setAttribute("aria-controls", this.popoverElement.id);
5191
+ this.#editorContentElement.setAttribute("aria-activedescendant", listItem.id);
5192
+ this.#editorContentElement.setAttribute("aria-haspopup", "listbox");
5080
5193
  }
5081
5194
 
5082
- #insertTableColumn(direction) {
5083
- this.#executeTableCommand("insert", "column", direction);
5195
+ #clearSelection() {
5196
+ this.#listItemElements.forEach((item) => { item.toggleAttribute("aria-selected", false); });
5197
+ this.#editorContentElement.removeAttribute("aria-controls");
5198
+ this.#editorContentElement.removeAttribute("aria-activedescendant");
5199
+ this.#editorContentElement.removeAttribute("aria-haspopup");
5200
+ }
5201
+
5202
+ #positionPopover() {
5203
+ const { x, y, fontSize } = this.#selection.cursorPosition;
5204
+ const editorRect = this.#editorElement.getBoundingClientRect();
5205
+ const contentRect = this.#editorContentElement.getBoundingClientRect();
5206
+ const verticalOffset = contentRect.top - editorRect.top;
5207
+
5208
+ if (!this.popoverElement.hasAttribute("data-anchored")) {
5209
+ this.popoverElement.style.left = `${x}px`;
5210
+ this.popoverElement.toggleAttribute("data-anchored", true);
5211
+ }
5212
+
5213
+ this.popoverElement.style.top = `${y + verticalOffset}px`;
5214
+ this.popoverElement.style.bottom = "auto";
5215
+
5216
+ const popoverRect = this.popoverElement.getBoundingClientRect();
5217
+ const isClippedAtBottom = popoverRect.bottom > window.innerHeight;
5218
+
5219
+ if (isClippedAtBottom || this.popoverElement.hasAttribute("data-clipped-at-bottom")) {
5220
+ this.popoverElement.style.top = `${y + verticalOffset - popoverRect.height - fontSize}px`;
5221
+ this.popoverElement.style.bottom = "auto";
5222
+ this.popoverElement.toggleAttribute("data-clipped-at-bottom", true);
5223
+ }
5084
5224
  }
5085
5225
 
5086
- #deleteTableRow(direction) {
5087
- this.#executeTableCommand("delete", "row", direction);
5226
+ #resetPopoverPosition() {
5227
+ this.popoverElement.removeAttribute("data-clipped-at-bottom");
5228
+ this.popoverElement.removeAttribute("data-anchored");
5088
5229
  }
5089
5230
 
5090
- #deleteTableColumn(direction) {
5091
- this.#executeTableCommand("delete", "column", direction);
5231
+ async #hidePopover() {
5232
+ this.#clearSelection();
5233
+ this.popoverElement.classList.toggle("lexxy-prompt-menu--visible", false);
5234
+ this.#editorElement.removeEventListener("lexxy:change", this.#filterOptions);
5235
+ this.#editorElement.removeEventListener("keydown", this.#handleKeydownOnPopover);
5236
+
5237
+ this.#unregisterKeyListeners();
5238
+ this.#removeCursorPositionListener();
5239
+
5240
+ await nextFrame();
5241
+ this.#addTriggerListener();
5242
+ }
5243
+
5244
+ #unregisterKeyListeners() {
5245
+ this.keyListeners.forEach((unregister) => unregister());
5246
+ this.keyListeners = [];
5247
+ }
5248
+
5249
+ #filterOptions = async () => {
5250
+ if (this.initialPrompt) {
5251
+ this.initialPrompt = false;
5252
+ return
5253
+ }
5254
+
5255
+ if (this.#editorContents.containsTextBackUntil(this.trigger)) {
5256
+ await this.#showFilteredOptions();
5257
+ await nextFrame();
5258
+ this.#positionPopover();
5259
+ } else {
5260
+ this.#hidePopover();
5261
+ }
5262
+ }
5263
+
5264
+ async #showFilteredOptions() {
5265
+ const filter = this.#editorContents.textBackUntil(this.trigger);
5266
+ const filteredListItems = await this.source.buildListItems(filter);
5267
+ this.popoverElement.innerHTML = "";
5268
+
5269
+ if (filteredListItems.length > 0) {
5270
+ this.#showResults(filteredListItems);
5271
+ } else {
5272
+ this.#showEmptyResults();
5273
+ }
5274
+ this.#selectFirstOption();
5275
+ }
5276
+
5277
+ #showResults(filteredListItems) {
5278
+ this.popoverElement.classList.remove("lexxy-prompt-menu--empty");
5279
+ this.popoverElement.append(...filteredListItems);
5280
+ }
5281
+
5282
+ #showEmptyResults() {
5283
+ this.popoverElement.classList.add("lexxy-prompt-menu--empty");
5284
+ const el = createElement("li", { innerHTML: this.#emptyResultsMessage });
5285
+ el.classList.add("lexxy-prompt-menu__item--empty");
5286
+ this.popoverElement.append(el);
5287
+ }
5288
+
5289
+ get #emptyResultsMessage() {
5290
+ return this.getAttribute("empty-results") || NOTHING_FOUND_DEFAULT_MESSAGE
5291
+ }
5292
+
5293
+ #handleKeydownOnPopover = (event) => {
5294
+ if (event.key === "Escape") {
5295
+ this.#hidePopover();
5296
+ this.#editorElement.focus();
5297
+ event.stopPropagation();
5298
+ }
5299
+ // Arrow keys are now handled via Lexical commands with HIGH priority
5300
+ }
5301
+
5302
+ #moveSelectionDown() {
5303
+ const nextIndex = this.#selectedIndex + 1;
5304
+ if (nextIndex < this.#listItemElements.length) this.#selectOption(this.#listItemElements[nextIndex]);
5305
+ }
5306
+
5307
+ #moveSelectionUp() {
5308
+ const previousIndex = this.#selectedIndex - 1;
5309
+ if (previousIndex >= 0) this.#selectOption(this.#listItemElements[previousIndex]);
5310
+ }
5311
+
5312
+ get #selectedIndex() {
5313
+ return this.#listItemElements.findIndex((item) => item.hasAttribute("aria-selected"))
5314
+ }
5315
+
5316
+ get #selectedListItem() {
5317
+ return this.#listItemElements[this.#selectedIndex]
5318
+ }
5319
+
5320
+ #handleSelectedOption(event) {
5321
+ event.preventDefault();
5322
+ event.stopPropagation();
5323
+ this.#optionWasSelected();
5324
+ return true
5325
+ }
5326
+
5327
+ #optionWasSelected() {
5328
+ this.#replaceTriggerWithSelectedItem();
5329
+ this.#hidePopover();
5330
+ this.#editorElement.focus();
5331
+ }
5332
+
5333
+ #replaceTriggerWithSelectedItem() {
5334
+ const promptItem = this.source.promptItemFor(this.#selectedListItem);
5335
+
5336
+ if (!promptItem) { return }
5337
+
5338
+ const templates = Array.from(promptItem.querySelectorAll("template[type='editor']"));
5339
+ const stringToReplace = `${this.trigger}${this.#editorContents.textBackUntil(this.trigger)}`;
5340
+
5341
+ if (this.hasAttribute("insert-editable-text")) {
5342
+ this.#insertTemplatesAsEditableText(templates, stringToReplace);
5343
+ } else {
5344
+ this.#insertTemplatesAsAttachments(templates, stringToReplace, promptItem.getAttribute("sgid"));
5345
+ }
5346
+ }
5347
+
5348
+ #insertTemplatesAsEditableText(templates, stringToReplace) {
5349
+ this.#editor.update(() => {
5350
+ const nodes = templates.flatMap(template => this.#buildEditableTextNodes(template));
5351
+ this.#editorContents.replaceTextBackUntil(stringToReplace, nodes);
5352
+ });
5353
+ }
5354
+
5355
+ #buildEditableTextNodes(template) {
5356
+ return $generateNodesFromDOM(this.#editor, parseHtml(`${template.innerHTML}`))
5357
+ }
5358
+
5359
+ #insertTemplatesAsAttachments(templates, stringToReplace, fallbackSgid = null) {
5360
+ this.#editor.update(() => {
5361
+ const attachmentNodes = this.#buildAttachmentNodes(templates, fallbackSgid);
5362
+ const spacedAttachmentNodes = attachmentNodes.flatMap(node => [ node, this.#getSpacerTextNode() ]).slice(0, -1);
5363
+ this.#editorContents.replaceTextBackUntil(stringToReplace, spacedAttachmentNodes);
5364
+ });
5365
+ }
5366
+
5367
+ #buildAttachmentNodes(templates, fallbackSgid = null) {
5368
+ return templates.map(
5369
+ template => this.#buildAttachmentNode(
5370
+ template.innerHTML,
5371
+ template.getAttribute("content-type") || this.#defaultPromptContentType,
5372
+ template.getAttribute("sgid") || fallbackSgid
5373
+ ))
5374
+ }
5375
+
5376
+ #getSpacerTextNode() {
5377
+ return $createTextNode(" ")
5378
+ }
5379
+
5380
+ get #defaultPromptContentType() {
5381
+ const attachmentContentTypeNamespace = Lexxy.global.get("attachmentContentTypeNamespace");
5382
+ return `application/vnd.${attachmentContentTypeNamespace}.${this.name}`
5383
+ }
5384
+
5385
+ #buildAttachmentNode(innerHtml, contentType, sgid) {
5386
+ return new CustomActionTextAttachmentNode({ sgid, contentType, innerHtml })
5387
+ }
5388
+
5389
+ get #editorContents() {
5390
+ return this.#editorElement.contents
5391
+ }
5392
+
5393
+ get #editorContentElement() {
5394
+ return this.#editorElement.editorContentElement
5395
+ }
5396
+
5397
+ async #buildPopover() {
5398
+ const popoverContainer = createElement("ul", { role: "listbox", id: generateDomId("prompt-popover") }); // Avoiding [popover] due to not being able to position at an arbitrary X, Y position.
5399
+ popoverContainer.classList.add("lexxy-prompt-menu");
5400
+ popoverContainer.style.position = "absolute";
5401
+ popoverContainer.setAttribute("nonce", getNonce());
5402
+ popoverContainer.append(...await this.source.buildListItems());
5403
+ popoverContainer.addEventListener("click", this.#handlePopoverClick);
5404
+ this.#editorElement.appendChild(popoverContainer);
5405
+ return popoverContainer
5406
+ }
5407
+
5408
+ #handlePopoverClick = (event) => {
5409
+ const listItem = event.target.closest(".lexxy-prompt-menu__item");
5410
+ if (listItem) {
5411
+ this.#selectOption(listItem);
5412
+ this.#optionWasSelected();
5413
+ }
5414
+ }
5415
+
5416
+ #reconnect() {
5417
+ this.disconnectedCallback();
5418
+ this.connectedCallback();
5092
5419
  }
5420
+ }
5093
5421
 
5094
- #executeTableCommand(action = "insert", childType = "row", direction) {
5095
- this.#editor.update(() => {
5096
- const currentCell = this.#currentCell;
5097
- if (!currentCell) return
5422
+ class CodeLanguagePicker extends HTMLElement {
5423
+ connectedCallback() {
5424
+ this.editorElement = this.closest("lexxy-editor");
5425
+ this.editor = this.editorElement.editor;
5098
5426
 
5099
- if (direction === "end") {
5100
- this.#selectLastTableCell();
5101
- }
5427
+ this.#attachLanguagePicker();
5428
+ this.#monitorForCodeBlockSelection();
5429
+ }
5102
5430
 
5103
- this.#dispatchTableCommand(action, childType, direction);
5431
+ #attachLanguagePicker() {
5432
+ this.languagePickerElement = this.#createLanguagePicker();
5104
5433
 
5105
- if (currentCell.isAttached()) {
5106
- currentCell.selectEnd();
5107
- }
5434
+ this.languagePickerElement.addEventListener("change", () => {
5435
+ this.#updateCodeBlockLanguage(this.languagePickerElement.value);
5108
5436
  });
5109
5437
 
5110
- this.#closeMoreMenu();
5111
- this.#updateRowColumnCount();
5438
+ this.languagePickerElement.setAttribute("nonce", getNonce());
5439
+ this.appendChild(this.languagePickerElement);
5112
5440
  }
5113
5441
 
5114
- #dispatchTableCommand(action, childType, direction) {
5115
- switch (action) {
5116
- case "insert":
5117
- switch (childType) {
5118
- case "row":
5119
- if (direction === "above") {
5120
- this.#editor.dispatchCommand("insertTableRowAbove");
5121
- } else {
5122
- this.#editor.dispatchCommand("insertTableRowBelow");
5123
- }
5124
- break
5125
- case "column":
5126
- if (direction === "left") {
5127
- this.#editor.dispatchCommand("insertTableColumnBefore");
5128
- } else {
5129
- this.#editor.dispatchCommand("insertTableColumnAfter");
5130
- }
5131
- break
5132
- }
5133
- break
5134
- case "delete":
5135
- switch (childType) {
5136
- case "row":
5137
- this.#editor.dispatchCommand("deleteTableRow");
5138
- break
5139
- case "column":
5140
- this.#editor.dispatchCommand("deleteTableColumn");
5141
- break
5142
- }
5143
- break
5442
+ #createLanguagePicker() {
5443
+ const selectElement = createElement("select", { className: "lexxy-code-language-picker", "aria-label": "Pick a language…", name: "lexxy-code-language" });
5444
+
5445
+ for (const [ value, label ] of Object.entries(this.#languages)) {
5446
+ const option = document.createElement("option");
5447
+ option.value = value;
5448
+ option.textContent = label;
5449
+ selectElement.appendChild(option);
5144
5450
  }
5145
- }
5146
5451
 
5147
- #toggleRowHeaderStyle() {
5148
- this.#editor.update(() => {
5149
- const rows = this.currentTableNode.getChildren();
5452
+ return selectElement
5453
+ }
5150
5454
 
5151
- const row = rows[this.#currentRow];
5152
- if (!row) return
5455
+ get #languages() {
5456
+ const languages = { ...CODE_LANGUAGE_FRIENDLY_NAME_MAP };
5153
5457
 
5154
- const cells = row.getChildren();
5155
- const firstCell = $getTableCellNodeFromLexicalNode(cells[0]);
5156
- if (!firstCell) return
5458
+ if (!languages.ruby) languages.ruby = "Ruby";
5459
+ if (!languages.php) languages.php = "PHP";
5460
+ if (!languages.go) languages.go = "Go";
5461
+ if (!languages.bash) languages.bash = "Bash";
5462
+ if (!languages.json) languages.json = "JSON";
5463
+ if (!languages.diff) languages.diff = "Diff";
5157
5464
 
5158
- const currentStyle = firstCell.getHeaderStyles();
5159
- const newStyle = currentStyle ^ TableCellHeaderStates.ROW;
5465
+ const sortedEntries = Object.entries(languages)
5466
+ .sort(([ , a ], [ , b ]) => a.localeCompare(b));
5160
5467
 
5161
- cells.forEach(cell => {
5162
- this.#setHeaderStyle(cell, newStyle, TableCellHeaderStates.ROW);
5163
- });
5164
- });
5468
+ // Place the "plain" entry first, then the rest of language sorted alphabetically
5469
+ const plainIndex = sortedEntries.findIndex(([ key ]) => key === "plain");
5470
+ const plainEntry = sortedEntries.splice(plainIndex, 1)[0];
5471
+ return Object.fromEntries([ plainEntry, ...sortedEntries ])
5165
5472
  }
5166
5473
 
5167
- #toggleColumnHeaderStyle() {
5168
- this.#editor.update(() => {
5169
- const rows = this.currentTableNode.getChildren();
5170
-
5171
- const row = rows[this.#currentRow];
5172
- if (!row) return
5474
+ #updateCodeBlockLanguage(language) {
5475
+ this.editor.update(() => {
5476
+ const codeNode = this.#getCurrentCodeNode();
5173
5477
 
5174
- const cells = row.getChildren();
5175
- const selectedCell = $getTableCellNodeFromLexicalNode(cells[this.#currentColumn]);
5176
- if (!selectedCell) return
5478
+ if (codeNode) {
5479
+ codeNode.setLanguage(language);
5480
+ }
5481
+ });
5482
+ }
5177
5483
 
5178
- const currentStyle = selectedCell.getHeaderStyles();
5179
- const newStyle = currentStyle ^ TableCellHeaderStates.COLUMN;
5484
+ #monitorForCodeBlockSelection() {
5485
+ this.editor.registerUpdateListener(() => {
5486
+ this.editor.getEditorState().read(() => {
5487
+ const codeNode = this.#getCurrentCodeNode();
5180
5488
 
5181
- rows.forEach(row => {
5182
- const cell = row.getChildren()[this.#currentColumn];
5183
- if (!cell) return
5184
- this.#setHeaderStyle(cell, newStyle, TableCellHeaderStates.COLUMN);
5489
+ if (codeNode) {
5490
+ this.#codeNodeWasSelected(codeNode);
5491
+ } else {
5492
+ this.#hideLanguagePicker();
5493
+ }
5185
5494
  });
5186
5495
  });
5187
5496
  }
5188
5497
 
5189
- #setHeaderStyle(cell, newStyle, headerState) {
5190
- const tableCellNode = $getTableCellNodeFromLexicalNode(cell);
5498
+ #getCurrentCodeNode() {
5499
+ const selection = $getSelection();
5191
5500
 
5192
- if (tableCellNode) {
5193
- tableCellNode.setHeaderStyles(newStyle, headerState);
5501
+ if (!$isRangeSelection(selection)) {
5502
+ return null
5194
5503
  }
5195
- }
5196
-
5197
- #icon(name) {
5198
- const icons =
5199
- {
5200
- "add-row-above":
5201
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5202
- <path d="M4 7L0 10V4L4 7ZM6.5 7.5H16.5V6.5H6.5V7.5ZM18 8C18 8.55228 17.5523 9 17 9H6C5.44772 9 5 8.55228 5 8V6C5 5.44772 5.44772 5 6 5H17C17.5523 5 18 5.44772 18 6V8Z"/><path d="M2 2C2 1.44772 2.44772 1 3 1H15C15.5523 1 16 1.44772 16 2C16 2.55228 15.5523 3 15 3H3C2.44772 3 2 2.55228 2 2Z"/><path d="M2 12C2 11.4477 2.44772 11 3 11H15C15.5523 11 16 11.4477 16 12C16 12.5523 15.5523 13 15 13H3C2.44772 13 2 12.5523 2 12Z"/><path d="M2 16C2 15.4477 2.44772 15 3 15H15C15.5523 15 16 15.4477 16 16C16 16.5523 15.5523 17 15 17H3C2.44772 17 2 16.5523 2 16Z"/>
5203
- </svg>`,
5204
-
5205
- "add-row-below":
5206
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5207
- <path d="M4 11L0 8V14L4 11ZM6.5 10.5H16.5V11.5H6.5V10.5ZM18 10C18 9.44772 17.5523 9 17 9H6C5.44772 9 5 9.44772 5 10V12C5 12.5523 5.44772 13 6 13H17C17.5523 13 18 12.5523 18 12V10Z"/><path d="M2 16C2 16.5523 2.44772 17 3 17H15C15.5523 17 16 16.5523 16 16C16 15.4477 15.5523 15 15 15H3C2.44772 15 2 15.4477 2 16Z"/><path d="M2 6C2 6.55228 2.44772 7 3 7H15C15.5523 7 16 6.55228 16 6C16 5.44772 15.5523 5 15 5H3C2.44772 5 2 5.44772 2 6Z"/><path d="M2 2C2 2.55228 2.44772 3 3 3H15C15.5523 3 16 2.55228 16 2C16 1.44772 15.5523 1 15 1H3C2.44772 1 2 1.44772 2 2Z"/>
5208
- </svg>`,
5209
-
5210
- "remove-row":
5211
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5212
- <path d="M17.9951 10.1025C17.9438 10.6067 17.5177 11 17 11H12.4922L13.9922 9.5H16.5V5.5L1.5 5.5L1.5 9.5H4.00586L5.50586 11H1L0.897461 10.9951C0.427034 10.9472 0.0527828 10.573 0.00488281 10.1025L0 10L1.78814e-07 5C2.61831e-07 4.48232 0.393332 4.05621 0.897461 4.00488L1 4L17 4C17.5523 4 18 4.44772 18 5V10L17.9951 10.1025Z"/><path d="M11.2969 15.0146L8.99902 12.7168L6.7002 15.0146L5.63965 13.9541L7.93848 11.6562L5.63965 9.3584L6.7002 8.29785L8.99902 10.5957L11.2969 8.29785L12.3574 9.3584L10.0596 11.6562L12.3574 13.9541L11.2969 15.0146Z"/>
5213
- </svg>`,
5214
-
5215
- "toggle-row-style":
5216
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5217
- <path d="M1 2C1 1.44772 1.44772 1 2 1H7C7.55228 1 8 1.44772 8 2V7C8 7.55228 7.55228 8 7 8H2C1.44772 8 1 7.55228 1 7V2Z"/><path d="M2.5 15.5H6.5V11.5H2.5V15.5ZM8 16C8 16.5177 7.60667 16.9438 7.10254 16.9951L7 17H2L1.89746 16.9951C1.42703 16.9472 1.05278 16.573 1.00488 16.1025L1 16V11C1 10.4477 1.44772 10 2 10H7C7.55228 10 8 10.4477 8 11V16Z"/><path d="M10 2C10 1.44772 10.4477 1 11 1H16C16.5523 1 17 1.44772 17 2V7C17 7.55228 16.5523 8 16 8H11C10.4477 8 10 7.55228 10 7V2Z"/><path d="M11.5 15.5H15.5V11.5H11.5V15.5ZM17 16C17 16.5177 16.6067 16.9438 16.1025 16.9951L16 17H11L10.8975 16.9951C10.427 16.9472 10.0528 16.573 10.0049 16.1025L10 16V11C10 10.4477 10.4477 10 11 10H16C16.5523 10 17 10.4477 17 11V16Z"/>
5218
- </svg>`,
5219
-
5220
- "add-column-before":
5221
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5222
- <path d="M7 4L10 2.62268e-07L4 0L7 4ZM7.5 6.5L7.5 16.5H6.5L6.5 6.5H7.5ZM8 18C8.55228 18 9 17.5523 9 17V6C9 5.44772 8.55229 5 8 5H6C5.44772 5 5 5.44772 5 6L5 17C5 17.5523 5.44772 18 6 18H8Z"/><path d="M2 2C1.44772 2 1 2.44772 1 3L1 15C1 15.5523 1.44772 16 2 16C2.55228 16 3 15.5523 3 15L3 3C3 2.44772 2.55229 2 2 2Z"/><path d="M12 2C11.4477 2 11 2.44772 11 3L11 15C11 15.5523 11.4477 16 12 16C12.5523 16 13 15.5523 13 15L13 3C13 2.44772 12.5523 2 12 2Z"/><path d="M16 2C15.4477 2 15 2.44772 15 3L15 15C15 15.5523 15.4477 16 16 16C16.5523 16 17 15.5523 17 15V3C17 2.44772 16.5523 2 16 2Z"/>
5223
- </svg>`,
5224
5504
 
5225
- "add-column-after":
5226
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5227
- <path d="M11 4L8 2.62268e-07L14 0L11 4ZM10.5 6.5V16.5H11.5V6.5H10.5ZM10 18C9.44772 18 9 17.5523 9 17V6C9 5.44772 9.44772 5 10 5H12C12.5523 5 13 5.44772 13 6V17C13 17.5523 12.5523 18 12 18H10Z"/><path d="M16 2C16.5523 2 17 2.44772 17 3L17 15C17 15.5523 16.5523 16 16 16C15.4477 16 15 15.5523 15 15V3C15 2.44772 15.4477 2 16 2Z"/><path d="M6 2C6.55228 2 7 2.44772 7 3L7 15C7 15.5523 6.55228 16 6 16C5.44772 16 5 15.5523 5 15L5 3C5 2.44772 5.44771 2 6 2Z"/><path d="M2 2C2.55228 2 3 2.44772 3 3L3 15C3 15.5523 2.55228 16 2 16C1.44772 16 1 15.5523 1 15V3C1 2.44772 1.44771 2 2 2Z"/>
5228
- </svg>`,
5229
-
5230
- "remove-column":
5231
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5232
- <path d="M10.1025 0.00488281C10.6067 0.0562145 11 0.482323 11 1V5.50781L9.5 4.00781V1.5H5.5V16.5H9.5V13.9941L11 12.4941V17L10.9951 17.1025C10.9472 17.573 10.573 17.9472 10.1025 17.9951L10 18H5C4.48232 18 4.05621 17.6067 4.00488 17.1025L4 17V1C4 0.447715 4.44772 1.61064e-08 5 0H10L10.1025 0.00488281Z"/><path d="M12.7169 8.99999L15.015 11.2981L13.9543 12.3588L11.6562 10.0607L9.35815 12.3588L8.29749 11.2981L10.5956 8.99999L8.29749 6.7019L9.35815 5.64124L11.6562 7.93933L13.9543 5.64124L15.015 6.7019L12.7169 8.99999Z"/>
5233
- </svg>`,
5234
-
5235
- "toggle-column-style":
5236
- `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5237
- <path d="M1 2C1 1.44772 1.44772 1 2 1H7C7.55228 1 8 1.44772 8 2V7C8 7.55228 7.55228 8 7 8H2C1.44772 8 1 7.55228 1 7V2Z"/><path d="M1 11C1 10.4477 1.44772 10 2 10H7C7.55228 10 8 10.4477 8 11V16C8 16.5523 7.55228 17 7 17H2C1.44772 17 1 16.5523 1 16V11Z"/><path d="M11.5 6.5H15.5V2.5H11.5V6.5ZM17 7C17 7.51768 16.6067 7.94379 16.1025 7.99512L16 8H11L10.8975 7.99512C10.427 7.94722 10.0528 7.57297 10.0049 7.10254L10 7V2C10 1.44772 10.4477 1 11 1H16C16.5523 1 17 1.44772 17 2V7Z"/><path d="M11.5 15.5H15.5V11.5H11.5V15.5ZM17 16C17 16.5177 16.6067 16.9438 16.1025 16.9951L16 17H11L10.8975 16.9951C10.427 16.9472 10.0528 16.573 10.0049 16.1025L10 16V11C10 10.4477 10.4477 10 11 10H16C16.5523 10 17 10.4477 17 11V16Z"/>
5238
- </svg>`,
5239
-
5240
- "delete-table":
5241
- `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
5242
- <path d="M18.2129 19.2305C18.0925 20.7933 16.7892 22 15.2217 22H7.77832C6.21084 22 4.90753 20.7933 4.78711 19.2305L4 9H19L18.2129 19.2305Z"/><path d="M13 2C14.1046 2 15 2.89543 15 4H19C19.5523 4 20 4.44772 20 5V6C20 6.55228 19.5523 7 19 7H4C3.44772 7 3 6.55228 3 6V5C3 4.44772 3.44772 4 4 4H8C8 2.89543 8.89543 2 10 2H13Z"/>
5243
- </svg>`
5244
- };
5245
-
5246
- return icons[name]
5247
- }
5248
- }
5249
-
5250
- customElements.define("lexxy-table-handler", TableHandler);
5505
+ const anchorNode = selection.anchor.getNode();
5506
+ const parentNode = anchorNode.getParent();
5251
5507
 
5252
- class BaseSource {
5253
- // Template method to override
5254
- async buildListItems(filter = "") {
5255
- return Promise.resolve([])
5256
- }
5508
+ if ($isCodeNode(anchorNode)) {
5509
+ return anchorNode
5510
+ } else if ($isCodeNode(parentNode)) {
5511
+ return parentNode
5512
+ }
5257
5513
 
5258
- // Template method to override
5259
- promptItemFor(listItem) {
5260
5514
  return null
5261
5515
  }
5262
5516
 
5263
- // Protected
5517
+ #codeNodeWasSelected(codeNode) {
5518
+ const language = codeNode.getLanguage();
5264
5519
 
5265
- buildListItemElementFor(promptItemElement) {
5266
- const template = promptItemElement.querySelector("template[type='menu']");
5267
- const fragment = template.content.cloneNode(true);
5268
- const listItemElement = createElement("li", { role: "option", id: generateDomId("prompt-item"), tabindex: "0" });
5269
- listItemElement.classList.add("lexxy-prompt-menu__item");
5270
- listItemElement.appendChild(fragment);
5271
- return listItemElement
5520
+ this.#updateLanguagePickerWith(language);
5521
+ this.#showLanguagePicker();
5522
+ this.#positionLanguagePicker(codeNode);
5272
5523
  }
5273
5524
 
5274
- async loadPromptItemsFromUrl(url) {
5275
- try {
5276
- const response = await fetch(url);
5277
- const html = await response.text();
5278
- const promptItems = parseHtml(html).querySelectorAll("lexxy-prompt-item");
5279
- return Promise.resolve(Array.from(promptItems))
5280
- } catch (error) {
5281
- return Promise.reject(error)
5525
+ #updateLanguagePickerWith(language) {
5526
+ if (this.languagePickerElement && language) {
5527
+ const normalizedLanguage = normalizeCodeLang(language);
5528
+ this.languagePickerElement.value = normalizedLanguage;
5282
5529
  }
5283
5530
  }
5284
- }
5285
5531
 
5286
- class LocalFilterSource extends BaseSource {
5287
- async buildListItems(filter = "") {
5288
- const promptItems = await this.fetchPromptItems();
5289
- return this.#buildListItemsFromPromptItems(promptItems, filter)
5290
- }
5532
+ #positionLanguagePicker(codeNode) {
5533
+ const codeElement = this.editor.getElementByKey(codeNode.getKey());
5534
+ if (!codeElement) return
5291
5535
 
5292
- // Template method to override
5293
- async fetchPromptItems(filter) {
5294
- return Promise.resolve([])
5295
- }
5536
+ const codeRect = codeElement.getBoundingClientRect();
5537
+ const editorRect = this.editorElement.getBoundingClientRect();
5538
+ const relativeTop = codeRect.top - editorRect.top;
5539
+ const relativeRight = editorRect.right - codeRect.right;
5296
5540
 
5297
- promptItemFor(listItem) {
5298
- return this.promptItemByListItem.get(listItem)
5541
+ this.style.top = `${relativeTop}px`;
5542
+ this.style.right = `${relativeRight}px`;
5299
5543
  }
5300
5544
 
5301
- #buildListItemsFromPromptItems(promptItems, filter) {
5302
- const listItems = [];
5303
- this.promptItemByListItem = new WeakMap();
5304
- promptItems.forEach((promptItem) => {
5305
- const searchableText = promptItem.getAttribute("search");
5306
-
5307
- if (!filter || filterMatches(searchableText, filter)) {
5308
- const listItem = this.buildListItemElementFor(promptItem);
5309
- this.promptItemByListItem.set(listItem, promptItem);
5310
- listItems.push(listItem);
5311
- }
5312
- });
5545
+ #showLanguagePicker() {
5546
+ this.hidden = false;
5547
+ }
5313
5548
 
5314
- return listItems
5549
+ #hideLanguagePicker() {
5550
+ this.hidden = true;
5315
5551
  }
5316
5552
  }
5317
5553
 
5318
- class InlinePromptSource extends LocalFilterSource {
5319
- constructor(inlinePromptItems) {
5320
- super();
5321
- this.inlinePromptItemElements = Array.from(inlinePromptItems);
5322
- }
5554
+ class TableController {
5555
+ constructor(editorElement) {
5556
+ this.editor = editorElement.editor;
5557
+ this.contents = editorElement.contents;
5558
+ this.selection = editorElement.selection;
5323
5559
 
5324
- async fetchPromptItems() {
5325
- return Promise.resolve(this.inlinePromptItemElements)
5560
+ this.currentTableNodeKey = null;
5561
+ this.currentCellKey = null;
5562
+
5563
+ this.#registerKeyHandlers();
5326
5564
  }
5327
- }
5328
5565
 
5329
- class DeferredPromptSource extends LocalFilterSource {
5330
- constructor(url) {
5331
- super();
5332
- this.url = url;
5566
+ destroy() {
5567
+ this.currentTableNodeKey = null;
5568
+ this.currentCellKey = null;
5333
5569
 
5334
- this.fetchPromptItems();
5570
+ this.#unregisterKeyHandlers();
5335
5571
  }
5336
5572
 
5337
- async fetchPromptItems() {
5338
- this.promptItems ??= await this.loadPromptItemsFromUrl(this.url);
5573
+ get currentCell() {
5574
+ if (!this.currentCellKey) return null
5339
5575
 
5340
- return Promise.resolve(this.promptItems)
5576
+ return this.editor.getEditorState().read(() => {
5577
+ const cell = $getNodeByKey(this.currentCellKey);
5578
+ return (cell instanceof TableCellNode) ? cell : null
5579
+ })
5341
5580
  }
5342
- }
5343
-
5344
- const DEBOUNCE_INTERVAL = 200;
5345
5581
 
5346
- class RemoteFilterSource extends BaseSource {
5347
- constructor(url) {
5348
- super();
5582
+ get currentTableNode() {
5583
+ if (!this.currentTableNodeKey) return null
5349
5584
 
5350
- this.baseURL = url;
5351
- this.loadAndFilterListItems = debounceAsync(this.fetchFilteredListItems.bind(this), DEBOUNCE_INTERVAL);
5585
+ return this.editor.getEditorState().read(() => {
5586
+ const tableNode = $getNodeByKey(this.currentTableNodeKey);
5587
+ return (tableNode instanceof TableNode) ? tableNode : null
5588
+ })
5352
5589
  }
5353
5590
 
5354
- async buildListItems(filter = "") {
5355
- return await this.loadAndFilterListItems(filter)
5356
- }
5591
+ get currentRowCells() {
5592
+ const currentRowIndex = this.currentRowIndex;
5357
5593
 
5358
- promptItemFor(listItem) {
5359
- return this.promptItemByListItem.get(listItem)
5360
- }
5594
+ const rows = this.tableRows;
5595
+ if (!rows) return null
5361
5596
 
5362
- async fetchFilteredListItems(filter) {
5363
- const promptItems = await this.loadPromptItemsFromUrl(this.#urlFor(filter));
5364
- return this.#buildListItemsFromPromptItems(promptItems)
5597
+ return this.editor.getEditorState().read(() => {
5598
+ return rows[currentRowIndex]?.getChildren() ?? null
5599
+ }) ?? null
5365
5600
  }
5366
5601
 
5367
- #urlFor(filter) {
5368
- const url = new URL(this.baseURL, window.location.origin);
5369
- url.searchParams.append("filter", filter);
5370
- return url.toString()
5602
+ get currentRowIndex() {
5603
+ const currentCell = this.currentCell;
5604
+ if (!currentCell) return 0
5605
+
5606
+ return this.editor.getEditorState().read(() => {
5607
+ return $getTableRowIndexFromTableCellNode(currentCell)
5608
+ }) ?? 0
5371
5609
  }
5372
5610
 
5373
- #buildListItemsFromPromptItems(promptItems) {
5374
- const listItems = [];
5375
- this.promptItemByListItem = new WeakMap();
5611
+ get currentColumnCells() {
5612
+ const columnIndex = this.currentColumnIndex;
5376
5613
 
5377
- for (const promptItem of promptItems) {
5378
- const listItem = this.buildListItemElementFor(promptItem);
5379
- this.promptItemByListItem.set(listItem, promptItem);
5380
- listItems.push(listItem);
5381
- }
5614
+ const rows = this.tableRows;
5615
+ if (!rows) return null
5382
5616
 
5383
- return listItems
5617
+ return this.editor.getEditorState().read(() => {
5618
+ return rows.map(row => row.getChildAtIndex(columnIndex))
5619
+ }) ?? null
5384
5620
  }
5385
- }
5386
5621
 
5387
- const NOTHING_FOUND_DEFAULT_MESSAGE = "Nothing found";
5622
+ get currentColumnIndex() {
5623
+ const currentCell = this.currentCell;
5624
+ if (!currentCell) return 0
5388
5625
 
5389
- class LexicalPromptElement extends HTMLElement {
5390
- constructor() {
5391
- super();
5392
- this.keyListeners = [];
5626
+ return this.editor.getEditorState().read(() => {
5627
+ return $getTableColumnIndexFromTableCellNode(currentCell)
5628
+ }) ?? 0
5393
5629
  }
5394
5630
 
5395
- static observedAttributes = [ "connected" ]
5631
+ get tableRows() {
5632
+ return this.editor.getEditorState().read(() => {
5633
+ return this.currentTableNode?.getChildren()
5634
+ }) ?? null
5635
+ }
5396
5636
 
5397
- connectedCallback() {
5398
- this.source = this.#createSource();
5637
+ updateSelectedTable() {
5638
+ let cellNode = null;
5639
+ let tableNode = null;
5399
5640
 
5400
- this.#addTriggerListener();
5401
- this.toggleAttribute("connected", true);
5402
- }
5641
+ this.editor.getEditorState().read(() => {
5642
+ const selection = $getSelection();
5643
+ if (!selection || !this.selection.isTableCellSelected) return
5403
5644
 
5404
- disconnectedCallback() {
5405
- this.source = null;
5406
- this.popoverElement = null;
5407
- }
5645
+ const node = selection.getNodes()[0];
5408
5646
 
5647
+ cellNode = $findCellNode(node);
5648
+ tableNode = $findTableNode(node);
5649
+ });
5409
5650
 
5410
- attributeChangedCallback(name, oldValue, newValue) {
5411
- if (name === "connected" && this.isConnected && oldValue != null && oldValue !== newValue) {
5412
- requestAnimationFrame(() => this.#reconnect());
5413
- }
5651
+ this.currentCellKey = cellNode?.getKey() ?? null;
5652
+ this.currentTableNodeKey = tableNode?.getKey() ?? null;
5414
5653
  }
5415
5654
 
5416
- get name() {
5417
- return this.getAttribute("name")
5418
- }
5655
+ executeTableCommand(command, customIndex = null) {
5656
+ if (command.action === "delete" && command.childType === "table") {
5657
+ this.#deleteTable();
5658
+ return
5659
+ }
5419
5660
 
5420
- get trigger() {
5421
- return this.getAttribute("trigger")
5422
- }
5661
+ if (command.action === "toggle") {
5662
+ this.#executeToggleStyle(command);
5663
+ return
5664
+ }
5423
5665
 
5424
- get supportsSpaceInSearches() {
5425
- return this.hasAttribute("supports-space-in-searches")
5666
+ this.#executeCommand(command, customIndex);
5426
5667
  }
5427
5668
 
5428
- get open() {
5429
- return this.popoverElement?.classList?.contains("lexxy-prompt-menu--visible")
5669
+ #executeCommand(command, customIndex = null) {
5670
+ this.#selectCellAtSelection();
5671
+ this.editor.dispatchCommand(this.#commandName(command));
5672
+ this.#selectNextBestCell(command, customIndex);
5430
5673
  }
5431
5674
 
5432
- get closed() {
5433
- return !this.open
5434
- }
5675
+ #executeToggleStyle(command) {
5676
+ const childType = command.childType;
5435
5677
 
5436
- get #doesSpaceSelect() {
5437
- return !this.supportsSpaceInSearches
5438
- }
5678
+ let cells = null;
5679
+ let headerState = null;
5439
5680
 
5440
- #createSource() {
5441
- const src = this.getAttribute("src");
5442
- if (src) {
5443
- if (this.hasAttribute("remote-filtering")) {
5444
- return new RemoteFilterSource(src)
5445
- } else {
5446
- return new DeferredPromptSource(src)
5447
- }
5448
- } else {
5449
- return new InlinePromptSource(this.querySelectorAll("lexxy-prompt-item"))
5681
+ if (childType === "row") {
5682
+ cells = this.currentRowCells;
5683
+ headerState = TableCellHeaderStates.ROW;
5684
+ } else if (childType === "column") {
5685
+ cells = this.currentColumnCells;
5686
+ headerState = TableCellHeaderStates.COLUMN;
5450
5687
  }
5451
- }
5452
-
5453
- #addTriggerListener() {
5454
- const unregister = this.#editor.registerUpdateListener(() => {
5455
- this.#editor.read(() => {
5456
- const { node, offset } = this.#selection.selectedNodeWithOffset();
5457
- if (!node) return
5458
5688
 
5459
- if ($isTextNode(node) && offset > 0) {
5460
- const fullText = node.getTextContent();
5461
- const charBeforeCursor = fullText[offset - 1];
5689
+ if (!cells || cells.length === 0) return
5462
5690
 
5463
- // Check if trigger is at the start of the text node (new line case) or preceded by space or newline
5464
- if (charBeforeCursor === this.trigger) {
5465
- const isAtStart = offset === 1;
5691
+ this.editor.update(() => {
5692
+ const firstCell = $getTableCellNodeFromLexicalNode(cells[0]);
5693
+ if (!firstCell) return
5466
5694
 
5467
- const charBeforeTrigger = offset > 1 ? fullText[offset - 2] : null;
5468
- const isPrecededBySpaceOrNewline = charBeforeTrigger === " " || charBeforeTrigger === "\n";
5695
+ const currentStyle = firstCell.getHeaderStyles();
5696
+ const newStyle = currentStyle ^ headerState;
5469
5697
 
5470
- if (isAtStart || isPrecededBySpaceOrNewline) {
5471
- unregister();
5472
- this.#showPopover();
5473
- }
5474
- }
5475
- }
5698
+ cells.forEach(cell => {
5699
+ this.#setHeaderStyle(cell, newStyle, headerState);
5476
5700
  });
5477
5701
  });
5478
5702
  }
5479
5703
 
5480
- #addCursorPositionListener() {
5481
- this.cursorPositionListener = this.#editor.registerUpdateListener(() => {
5482
- if (this.closed) return
5704
+ #deleteTable() {
5705
+ this.#selectCellAtSelection();
5706
+ this.editor.dispatchCommand("deleteTable");
5707
+ }
5483
5708
 
5484
- this.#editor.read(() => {
5485
- const { node, offset } = this.#selection.selectedNodeWithOffset();
5486
- if (!node) return
5709
+ #selectCellAtSelection() {
5710
+ this.editor.update(() => {
5711
+ const selection = $getSelection();
5712
+ if (!selection) return
5487
5713
 
5488
- if ($isTextNode(node) && offset > 0) {
5489
- const fullText = node.getTextContent();
5490
- const textBeforeCursor = fullText.slice(0, offset);
5491
- const lastTriggerIndex = textBeforeCursor.lastIndexOf(this.trigger);
5714
+ const node = selection.getNodes()[0];
5492
5715
 
5493
- // If trigger is not found, or cursor is at or before the trigger position, hide popover
5494
- if (lastTriggerIndex === -1 || offset <= lastTriggerIndex) {
5495
- this.#hidePopover();
5496
- }
5497
- } else {
5498
- // Cursor is not in a text node or at offset 0, hide popover
5499
- this.#hidePopover();
5500
- }
5501
- });
5716
+ $findCellNode(node)?.selectEnd();
5502
5717
  });
5503
5718
  }
5504
5719
 
5505
- #removeCursorPositionListener() {
5506
- if (this.cursorPositionListener) {
5507
- this.cursorPositionListener();
5508
- this.cursorPositionListener = null;
5509
- }
5510
- }
5720
+ #commandName(command) {
5721
+ const { action, childType, direction } = command;
5511
5722
 
5512
- get #editor() {
5513
- return this.#editorElement.editor
5723
+ const childTypeSuffix = upcaseFirst(childType);
5724
+ const directionSuffix = action == "insert" ? upcaseFirst(direction) : "";
5725
+ return `${action}Table${childTypeSuffix}${directionSuffix}`
5514
5726
  }
5515
5727
 
5516
- get #editorElement() {
5517
- return this.closest("lexxy-editor")
5728
+ #setHeaderStyle(cell, newStyle, headerState) {
5729
+ const tableCellNode = $getTableCellNodeFromLexicalNode(cell);
5730
+ tableCellNode?.setHeaderStyles(newStyle, headerState);
5518
5731
  }
5519
5732
 
5520
- get #selection() {
5521
- return this.#editorElement.selection
5522
- }
5733
+ async #selectCellAtIndex(rowIndex, columnIndex) {
5734
+ // We wait for next frame, otherwise table operations might not have completed yet.
5735
+ await nextFrame();
5523
5736
 
5524
- async #showPopover() {
5525
- this.popoverElement ??= await this.#buildPopover();
5526
- this.#resetPopoverPosition();
5527
- await this.#filterOptions();
5528
- this.popoverElement.classList.toggle("lexxy-prompt-menu--visible", true);
5529
- this.#selectFirstOption();
5737
+ if (!this.currentTableNode) return
5530
5738
 
5531
- this.#editorElement.addEventListener("keydown", this.#handleKeydownOnPopover);
5532
- this.#editorElement.addEventListener("lexxy:change", this.#filterOptions);
5739
+ const rows = this.tableRows;
5740
+ if (!rows) return
5533
5741
 
5534
- this.#registerKeyListeners();
5535
- this.#addCursorPositionListener();
5742
+ const row = rows[rowIndex];
5743
+ if (!row) return
5744
+
5745
+ this.editor.update(() => {
5746
+ const cell = $getTableCellNodeFromLexicalNode(row.getChildAtIndex(columnIndex));
5747
+ cell?.selectEnd();
5748
+ });
5536
5749
  }
5537
5750
 
5538
- #registerKeyListeners() {
5539
- // We can't use a regular keydown for Enter as Lexical handles it first
5540
- this.keyListeners.push(this.#editor.registerCommand(KEY_ENTER_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_HIGH));
5541
- this.keyListeners.push(this.#editor.registerCommand(KEY_TAB_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_HIGH));
5751
+ #selectNextBestCell(command, customIndex = null) {
5752
+ const { childType, direction } = command;
5542
5753
 
5543
- if (this.#doesSpaceSelect) {
5544
- this.keyListeners.push(this.#editor.registerCommand(KEY_SPACE_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_HIGH));
5754
+ let rowIndex = this.currentRowIndex;
5755
+ let columnIndex = customIndex !== null ? customIndex : this.currentColumnIndex;
5756
+
5757
+ const deleteOffset = command.action === "delete" ? -1 : 0;
5758
+ const offset = direction === "after" ? 1 : deleteOffset;
5759
+
5760
+ if (childType === "row") {
5761
+ rowIndex += offset;
5762
+ } else if (childType === "column") {
5763
+ columnIndex += offset;
5545
5764
  }
5546
5765
 
5547
- // Register arrow keys with HIGH priority to prevent Lexical's selection handlers from running
5548
- this.keyListeners.push(this.#editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#handleArrowUp.bind(this), COMMAND_PRIORITY_HIGH));
5549
- this.keyListeners.push(this.#editor.registerCommand(KEY_ARROW_DOWN_COMMAND, this.#handleArrowDown.bind(this), COMMAND_PRIORITY_HIGH));
5766
+ this.#selectCellAtIndex(rowIndex, columnIndex);
5550
5767
  }
5551
5768
 
5552
- #handleArrowUp(event) {
5553
- this.#moveSelectionUp();
5554
- event.preventDefault();
5555
- return true
5556
- }
5769
+ #selectNextRow() {
5770
+ const rows = this.tableRows;
5771
+ if (!rows) return
5557
5772
 
5558
- #handleArrowDown(event) {
5559
- this.#moveSelectionDown();
5560
- event.preventDefault();
5561
- return true
5773
+ const nextRow = rows.at(this.currentRowIndex + 1);
5774
+ if (!nextRow) return
5775
+
5776
+ this.editor.update(() => {
5777
+ nextRow.getChildAtIndex(this.currentColumnIndex)?.selectEnd();
5778
+ });
5562
5779
  }
5563
5780
 
5564
- #selectFirstOption() {
5565
- const firstOption = this.#listItemElements[0];
5781
+ #selectPreviousCell() {
5782
+ const cell = this.currentCell;
5783
+ if (!cell) return
5566
5784
 
5567
- if (firstOption) {
5568
- this.#selectOption(firstOption);
5569
- }
5785
+ this.editor.update(() => {
5786
+ cell.selectPrevious();
5787
+ });
5570
5788
  }
5571
5789
 
5572
- get #listItemElements() {
5573
- return Array.from(this.popoverElement.querySelectorAll(".lexxy-prompt-menu__item"))
5790
+ #insertRowAndSelectFirstCell() {
5791
+ this.executeTableCommand({ action: "insert", childType: "row", direction: "after" }, 0);
5574
5792
  }
5575
5793
 
5576
- #selectOption(listItem) {
5577
- this.#clearSelection();
5578
- listItem.toggleAttribute("aria-selected", true);
5579
- listItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
5580
- listItem.focus();
5794
+ #deleteRowAndSelectLastCell() {
5795
+ this.executeTableCommand({ action: "delete", childType: "row" }, -1);
5796
+ }
5581
5797
 
5582
- // Preserve selection to prevent cursor jump
5583
- this.#selection.preservingSelection(() => {
5584
- this.#editorElement.focus();
5585
- });
5798
+ #deleteRowAndSelectNextNode() {
5799
+ const tableNode = this.currentTableNode;
5800
+ this.executeTableCommand({ action: "delete", childType: "row" });
5586
5801
 
5587
- this.#editorContentElement.setAttribute("aria-controls", this.popoverElement.id);
5588
- this.#editorContentElement.setAttribute("aria-activedescendant", listItem.id);
5589
- this.#editorContentElement.setAttribute("aria-haspopup", "listbox");
5802
+ this.editor.update(() => {
5803
+ const next = tableNode?.getNextSibling();
5804
+ if ($isParagraphNode(next)) {
5805
+ next.selectStart();
5806
+ } else {
5807
+ const newParagraph = $createParagraphNode();
5808
+ this.currentTableNode.insertAfter(newParagraph);
5809
+ newParagraph.selectStart();
5810
+ }
5811
+ });
5590
5812
  }
5591
5813
 
5592
- #clearSelection() {
5593
- this.#listItemElements.forEach((item) => { item.toggleAttribute("aria-selected", false); });
5594
- this.#editorContentElement.removeAttribute("aria-controls");
5595
- this.#editorContentElement.removeAttribute("aria-activedescendant");
5596
- this.#editorContentElement.removeAttribute("aria-haspopup");
5597
- }
5814
+ #isCurrentCellEmpty() {
5815
+ if (!this.currentTableNode) return false
5598
5816
 
5599
- #positionPopover() {
5600
- const { x, y, fontSize } = this.#selection.cursorPosition;
5601
- const editorRect = this.#editorElement.getBoundingClientRect();
5602
- const contentRect = this.#editorContentElement.getBoundingClientRect();
5603
- const verticalOffset = contentRect.top - editorRect.top;
5817
+ const cell = this.currentCell;
5818
+ if (!cell) return false
5604
5819
 
5605
- if (!this.popoverElement.hasAttribute("data-anchored")) {
5606
- this.popoverElement.style.left = `${x}px`;
5607
- this.popoverElement.toggleAttribute("data-anchored", true);
5608
- }
5820
+ return cell.getTextContent().trim() === ""
5821
+ }
5609
5822
 
5610
- this.popoverElement.style.top = `${y + verticalOffset}px`;
5611
- this.popoverElement.style.bottom = "auto";
5823
+ #isCurrentRowLast() {
5824
+ if (!this.currentTableNode) return false
5612
5825
 
5613
- const popoverRect = this.popoverElement.getBoundingClientRect();
5614
- const isClippedAtBottom = popoverRect.bottom > window.innerHeight;
5826
+ const rows = this.tableRows;
5827
+ if (!rows) return false
5615
5828
 
5616
- if (isClippedAtBottom || this.popoverElement.hasAttribute("data-clipped-at-bottom")) {
5617
- this.popoverElement.style.top = `${y + verticalOffset - popoverRect.height - fontSize}px`;
5618
- this.popoverElement.style.bottom = "auto";
5619
- this.popoverElement.toggleAttribute("data-clipped-at-bottom", true);
5620
- }
5829
+ return rows.length === this.currentRowIndex + 1
5621
5830
  }
5622
5831
 
5623
- #resetPopoverPosition() {
5624
- this.popoverElement.removeAttribute("data-clipped-at-bottom");
5625
- this.popoverElement.removeAttribute("data-anchored");
5832
+ #isCurrentRowEmpty() {
5833
+ if (!this.currentTableNode) return false
5834
+
5835
+ const cells = this.currentRowCells;
5836
+ if (!cells) return false
5837
+
5838
+ return cells.every(cell => cell.getTextContent().trim() === "")
5626
5839
  }
5627
5840
 
5628
- async #hidePopover() {
5629
- this.#clearSelection();
5630
- this.popoverElement.classList.toggle("lexxy-prompt-menu--visible", false);
5631
- this.#editorElement.removeEventListener("lexxy:change", this.#filterOptions);
5632
- this.#editorElement.removeEventListener("keydown", this.#handleKeydownOnPopover);
5841
+ #isFirstCellInRow() {
5842
+ if (!this.currentTableNode) return false
5633
5843
 
5634
- this.#unregisterKeyListeners();
5635
- this.#removeCursorPositionListener();
5844
+ const cells = this.currentRowCells;
5845
+ if (!cells) return false
5636
5846
 
5637
- await nextFrame();
5638
- this.#addTriggerListener();
5847
+ return cells.indexOf(this.currentCell) === 0
5639
5848
  }
5640
5849
 
5641
- #unregisterKeyListeners() {
5642
- this.keyListeners.forEach((unregister) => unregister());
5643
- this.keyListeners = [];
5850
+ #registerKeyHandlers() {
5851
+ // We can't prevent these externally using regular keydown because Lexical handles it first.
5852
+ this.unregisterBackspaceKeyHandler = this.editor.registerCommand(KEY_BACKSPACE_COMMAND, (event) => this.#handleBackspaceKey(event), COMMAND_PRIORITY_HIGH);
5853
+ this.unregisterEnterKeyHandler = this.editor.registerCommand(KEY_ENTER_COMMAND, (event) => this.#handleEnterKey(event), COMMAND_PRIORITY_HIGH);
5644
5854
  }
5645
5855
 
5646
- #filterOptions = async () => {
5647
- if (this.initialPrompt) {
5648
- this.initialPrompt = false;
5649
- return
5856
+ #unregisterKeyHandlers() {
5857
+ this.unregisterBackspaceKeyHandler?.();
5858
+ this.unregisterEnterKeyHandler?.();
5859
+
5860
+ this.unregisterBackspaceKeyHandler = null;
5861
+ this.unregisterEnterKeyHandler = null;
5862
+ }
5863
+
5864
+ #handleBackspaceKey(event) {
5865
+ if (!this.currentTableNode) return false
5866
+
5867
+ if (this.#isCurrentRowEmpty() && this.#isFirstCellInRow()) {
5868
+ event.preventDefault();
5869
+ this.#deleteRowAndSelectLastCell();
5870
+ return true
5650
5871
  }
5651
5872
 
5652
- if (this.#editorContents.containsTextBackUntil(this.trigger)) {
5653
- await this.#showFilteredOptions();
5654
- await nextFrame();
5655
- this.#positionPopover();
5656
- } else {
5657
- this.#hidePopover();
5873
+ if (this.#isCurrentCellEmpty() && !this.#isFirstCellInRow()) {
5874
+ event.preventDefault();
5875
+ this.#selectPreviousCell();
5876
+ return true
5658
5877
  }
5878
+
5879
+ return false
5659
5880
  }
5660
5881
 
5661
- async #showFilteredOptions() {
5662
- const filter = this.#editorContents.textBackUntil(this.trigger);
5663
- const filteredListItems = await this.source.buildListItems(filter);
5664
- this.popoverElement.innerHTML = "";
5882
+ #handleEnterKey(event) {
5883
+ if ((event.ctrlKey || event.metaKey) || event.shiftKey || !this.currentTableNode) return false
5665
5884
 
5666
- if (filteredListItems.length > 0) {
5667
- this.#showResults(filteredListItems);
5885
+ if (this.selection.isInsideList || this.selection.isInsideCodeBlock) return false
5886
+
5887
+ event.preventDefault();
5888
+
5889
+ if (this.#isCurrentRowLast() && this.#isCurrentRowEmpty()) {
5890
+ this.#deleteRowAndSelectNextNode();
5891
+ } else if (this.#isCurrentRowLast()) {
5892
+ this.#insertRowAndSelectFirstCell();
5668
5893
  } else {
5669
- this.#showEmptyResults();
5894
+ this.#selectNextRow();
5670
5895
  }
5671
- this.#selectFirstOption();
5672
- }
5673
5896
 
5674
- #showResults(filteredListItems) {
5675
- this.popoverElement.classList.remove("lexxy-prompt-menu--empty");
5676
- this.popoverElement.append(...filteredListItems);
5897
+ return true
5677
5898
  }
5899
+ }
5678
5900
 
5679
- #showEmptyResults() {
5680
- this.popoverElement.classList.add("lexxy-prompt-menu--empty");
5681
- const el = createElement("li", { innerHTML: this.#emptyResultsMessage });
5682
- el.classList.add("lexxy-prompt-menu__item--empty");
5683
- this.popoverElement.append(el);
5684
- }
5901
+ var TableIcons = {
5902
+ "insert-row-before":
5903
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5904
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M7.86804e-07 15C8.29055e-07 15.8284 0.671574 16.5 1.5 16.5H15L15.1533 16.4922C15.8593 16.4205 16.4205 15.8593 16.4922 15.1533L16.5 15V4.5L16.4922 4.34668C16.4154 3.59028 15.7767 3 15 3H13.5L13.5 4.5H15V9H1.5L1.5 4.5L3 4.5V3H1.5C0.671574 3 1.20956e-06 3.67157 1.24577e-06 4.5L7.86804e-07 15ZM15 10.5V15H1.5L1.5 10.5H15Z"/>
5905
+ <path d="M4.5 4.5H7.5V7.5H9V4.5H12L12 3L9 3V6.55671e-08L7.5 0V3L4.5 3V4.5Z"/>
5906
+ </svg>`,
5907
+
5908
+ "insert-row-after":
5909
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5910
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M7.86804e-07 13.5C7.50592e-07 14.3284 0.671574 15 1.5 15H3V13.5H1.5L1.5 9L15 9V13.5H13.5V15H15C15.7767 15 16.4154 14.4097 16.4922 13.6533L16.5 13.5V3L16.4922 2.84668C16.4205 2.14069 15.8593 1.57949 15.1533 1.50781L15 1.5L1.5 1.5C0.671574 1.5 1.28803e-06 2.17157 1.24577e-06 3L7.86804e-07 13.5ZM15 3V7.5L1.5 7.5L1.5 3L15 3Z"/>
5911
+ <path d="M7.5 15V18H9V15H12V13.5H9V10.5H7.5V13.5H4.5V15H7.5Z"/>
5912
+ </svg>`,
5913
+
5914
+ "delete-row":
5915
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5916
+ <path d="M16.4922 12.1533C16.4154 12.9097 15.7767 13.5 15 13.5L12 13.5V12H15V6L1.5 6L1.5 12H4.5V13.5H1.5C0.723337 13.5 0.0846104 12.9097 0.00781328 12.1533L7.86804e-07 12L1.04907e-06 6C1.17362e-06 5.22334 0.590278 4.58461 1.34668 4.50781L1.5 4.5L15 4.5C15.8284 4.5 16.5 5.17157 16.5 6V12L16.4922 12.1533Z"/>
5917
+ <path d="M10.3711 15.9316L8.25 13.8096L6.12793 15.9316L5.06738 14.8711L7.18945 12.75L5.06738 10.6289L6.12793 9.56836L8.25 11.6895L10.3711 9.56836L11.4316 10.6289L9.31055 12.75L11.4316 14.8711L10.3711 15.9316Z"/>
5918
+ </svg>`,
5919
+
5920
+ "toggle-row":
5921
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5922
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M0.00781328 13.6533C0.0846108 14.4097 0.723337 15 1.5 15L15 15L15.1533 14.9922C15.8593 14.9205 16.4205 14.3593 16.4922 13.6533L16.5 13.5V4.5L16.4922 4.34668C16.4205 3.64069 15.8593 3.07949 15.1533 3.00781L15 3L1.5 3C0.671574 3 1.24863e-06 3.67157 1.18021e-06 4.5L7.86804e-07 13.5L0.00781328 13.6533ZM15 9V13.5L1.5 13.5L1.5 9L15 9Z"/>
5923
+ </svg>`,
5924
+
5925
+ "insert-column-before":
5926
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5927
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 0C3.67157 0 3 0.671573 3 1.5V3H4.5V1.5H9V15H4.5V13.5H3V15C3 15.7767 3.59028 16.4154 4.34668 16.4922L4.5 16.5H15L15.1533 16.4922C15.8593 16.4205 16.4205 15.8593 16.4922 15.1533L16.5 15V1.5C16.5 0.671573 15.8284 6.03989e-09 15 0H4.5ZM15 15H10.5V1.5H15V15Z"/>
5928
+ <path d="M3 7.5H0V9H3V12H4.5V9H7.5V7.5H4.5V4.5H3V7.5Z"/>
5929
+ </svg>`,
5930
+
5931
+ "insert-column-after":
5932
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5933
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M13.5 0C14.3284 0 15 0.671573 15 1.5V3H13.5V1.5H9V15H13.5V13.5H15V15C15 15.7767 14.4097 16.4154 13.6533 16.4922L13.5 16.5H3L2.84668 16.4922C2.14069 16.4205 1.57949 15.8593 1.50781 15.1533L1.5 15V1.5C1.5 0.671573 2.17157 6.03989e-09 3 0H13.5ZM3 15H7.5V1.5H3V15Z"/>
5934
+ <path d="M15 7.5H18V9H15V12H13.5V9H10.5V7.5H13.5V4.5H15V7.5Z"/>
5935
+ </svg>`,
5936
+
5937
+ "delete-column":
5938
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5939
+ <path d="M12.1533 0.0078125C12.9097 0.0846097 13.5 0.723336 13.5 1.5V4.5H12V1.5H6V15H12V12H13.5V15C13.5 15.7767 12.9097 16.4154 12.1533 16.4922L12 16.5H6C5.22334 16.5 4.58461 15.9097 4.50781 15.1533L4.5 15V1.5C4.5 0.671573 5.17157 2.41596e-08 6 0H12L12.1533 0.0078125Z"/>
5940
+ <path d="M15.9316 6.12891L13.8105 8.24902L15.9326 10.3711L14.8711 11.4316L12.75 9.31055L10.6289 11.4316L9.56738 10.3711L11.6885 8.24902L9.56836 6.12891L10.6289 5.06836L12.75 7.18848L14.8711 5.06836L15.9316 6.12891Z"/>
5941
+ </svg>`,
5942
+
5943
+ "toggle-column":
5944
+ `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
5945
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M13.6533 17.9922C14.4097 17.9154 15 17.2767 15 16.5L15 3L14.9922 2.84668C14.9205 2.14069 14.3593 1.57949 13.6533 1.50781L13.5 1.5L4.5 1.5L4.34668 1.50781C3.59028 1.58461 3 2.22334 3 3L3 16.5C3 17.2767 3.59028 17.9154 4.34668 17.9922L4.5 18L13.5 18L13.6533 17.9922ZM9 3L13.5 3L13.5 16.5L9 16.5L9 3Z" />
5946
+ </svg>`,
5947
+
5948
+ "delete-table":
5949
+ `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
5950
+ <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"/>
5951
+ </svg>`
5952
+ };
5685
5953
 
5686
- get #emptyResultsMessage() {
5687
- return this.getAttribute("empty-results") || NOTHING_FOUND_DEFAULT_MESSAGE
5688
- }
5954
+ class TableTools extends HTMLElement {
5955
+ connectedCallback() {
5956
+ this.tableController = new TableController(this.#editorElement);
5689
5957
 
5690
- #handleKeydownOnPopover = (event) => {
5691
- if (event.key === "Escape") {
5692
- this.#hidePopover();
5693
- this.#editorElement.focus();
5694
- event.stopPropagation();
5695
- }
5696
- // Arrow keys are now handled via Lexical commands with HIGH priority
5958
+ this.#setUpButtons();
5959
+ this.#monitorForTableSelection();
5960
+ this.#registerKeyboardShortcuts();
5697
5961
  }
5698
5962
 
5699
- #moveSelectionDown() {
5700
- const nextIndex = this.#selectedIndex + 1;
5701
- if (nextIndex < this.#listItemElements.length) this.#selectOption(this.#listItemElements[nextIndex]);
5702
- }
5963
+ disconnectedCallback() {
5964
+ this.#unregisterKeyboardShortcuts();
5703
5965
 
5704
- #moveSelectionUp() {
5705
- const previousIndex = this.#selectedIndex - 1;
5706
- if (previousIndex >= 0) this.#selectOption(this.#listItemElements[previousIndex]);
5966
+ this.unregisterUpdateListener?.();
5967
+ this.unregisterUpdateListener = null;
5968
+
5969
+ this.removeEventListener("keydown", this.#handleToolsKeydown);
5970
+
5971
+ this.tableController?.destroy();
5972
+ this.tableController = null;
5707
5973
  }
5708
5974
 
5709
- get #selectedIndex() {
5710
- return this.#listItemElements.findIndex((item) => item.hasAttribute("aria-selected"))
5975
+ get #editor() {
5976
+ return this.#editorElement.editor
5711
5977
  }
5712
5978
 
5713
- get #selectedListItem() {
5714
- return this.#listItemElements[this.#selectedIndex]
5979
+ get #editorElement() {
5980
+ return this.closest("lexxy-editor")
5715
5981
  }
5716
5982
 
5717
- #handleSelectedOption(event) {
5718
- event.preventDefault();
5719
- event.stopPropagation();
5720
- this.#optionWasSelected();
5721
- return true
5983
+ get #tableToolsButtons() {
5984
+ return Array.from(this.querySelectorAll("button, details > summary"))
5722
5985
  }
5723
5986
 
5724
- #optionWasSelected() {
5725
- this.#replaceTriggerWithSelectedItem();
5726
- this.#hidePopover();
5727
- this.#editorElement.focus();
5987
+ #setUpButtons() {
5988
+ this.appendChild(this.#createRowButtonsContainer());
5989
+ this.appendChild(this.#createColumnButtonsContainer());
5990
+
5991
+ this.appendChild(this.#createDeleteTableButton());
5992
+ this.addEventListener("keydown", this.#handleToolsKeydown);
5728
5993
  }
5729
5994
 
5730
- #replaceTriggerWithSelectedItem() {
5731
- const promptItem = this.source.promptItemFor(this.#selectedListItem);
5995
+ #createButtonsContainer(childType, setCountProperty, moreMenu) {
5996
+ const container = createElement("div", { className: `lexxy-table-control lexxy-table-control--${childType}` });
5732
5997
 
5733
- if (!promptItem) { return }
5998
+ const plusButton = this.#createButton(`Add ${childType}`, { action: "insert", childType, direction: "after" }, "+");
5999
+ const minusButton = this.#createButton(`Remove ${childType}`, { action: "delete", childType }, "−");
5734
6000
 
5735
- const templates = Array.from(promptItem.querySelectorAll("template[type='editor']"));
5736
- const stringToReplace = `${this.trigger}${this.#editorContents.textBackUntil(this.trigger)}`;
6001
+ const dropdown = createElement("details", { className: "lexxy-table-control__more-menu" });
6002
+ dropdown.setAttribute("name", "lexxy-dropdown");
6003
+ dropdown.tabIndex = -1;
5737
6004
 
5738
- if (this.hasAttribute("insert-editable-text")) {
5739
- this.#insertTemplatesAsEditableText(templates, stringToReplace);
5740
- } else {
5741
- this.#insertTemplatesAsAttachments(templates, stringToReplace, promptItem.getAttribute("sgid"));
5742
- }
5743
- }
6005
+ const count = createElement("summary", {}, `_ ${childType}s`);
6006
+ setCountProperty(count);
6007
+ dropdown.appendChild(count);
5744
6008
 
5745
- #insertTemplatesAsEditableText(templates, stringToReplace) {
5746
- this.#editor.update(() => {
5747
- const nodes = templates.flatMap(template => this.#buildEditableTextNodes(template));
5748
- this.#editorContents.replaceTextBackUntil(stringToReplace, nodes);
5749
- });
5750
- }
6009
+ dropdown.appendChild(moreMenu);
5751
6010
 
5752
- #buildEditableTextNodes(template) {
5753
- return $generateNodesFromDOM(this.#editor, parseHtml(`${template.innerHTML}`))
5754
- }
6011
+ container.appendChild(minusButton);
6012
+ container.appendChild(dropdown);
6013
+ container.appendChild(plusButton);
5755
6014
 
5756
- #insertTemplatesAsAttachments(templates, stringToReplace, fallbackSgid = null) {
5757
- this.#editor.update(() => {
5758
- const attachmentNodes = this.#buildAttachmentNodes(templates, fallbackSgid);
5759
- const spacedAttachmentNodes = attachmentNodes.flatMap(node => [ node, this.#getSpacerTextNode() ]).slice(0, -1);
5760
- this.#editorContents.replaceTextBackUntil(stringToReplace, spacedAttachmentNodes);
5761
- });
6015
+ return container
5762
6016
  }
5763
6017
 
5764
- #buildAttachmentNodes(templates, fallbackSgid = null) {
5765
- return templates.map(
5766
- template => this.#buildAttachmentNode(
5767
- template.innerHTML,
5768
- template.getAttribute("content-type") || this.#defaultPromptContentType,
5769
- template.getAttribute("sgid") || fallbackSgid
5770
- ))
6018
+ #createRowButtonsContainer() {
6019
+ return this.#createButtonsContainer(
6020
+ "row",
6021
+ (count) => { this.rowCount = count; },
6022
+ this.#createMoreMenuSection("row")
6023
+ )
5771
6024
  }
5772
6025
 
5773
- #getSpacerTextNode() {
5774
- return $createTextNode(" ")
6026
+ #createColumnButtonsContainer() {
6027
+ return this.#createButtonsContainer(
6028
+ "column",
6029
+ (count) => { this.columnCount = count; },
6030
+ this.#createMoreMenuSection("column")
6031
+ )
5775
6032
  }
5776
6033
 
5777
- get #defaultPromptContentType() {
5778
- const attachmentContentTypeNamespace = Lexxy.global.get("attachmentContentTypeNamespace");
5779
- return `application/vnd.${attachmentContentTypeNamespace}.${this.name}`
6034
+ #createMoreMenuSection(childType) {
6035
+ const section = createElement("div", { className: "lexxy-table-control__more-menu-details" });
6036
+ const addBeforeButton = this.#createButton(`Add ${childType} before`, { action: "insert", childType, direction: "before" });
6037
+ const addAfterButton = this.#createButton(`Add ${childType} after`, { action: "insert", childType, direction: "after" });
6038
+ const toggleStyleButton = this.#createButton(`Toggle ${childType} style`, { action: "toggle", childType });
6039
+ const deleteButton = this.#createButton(`Remove ${childType}`, { action: "delete", childType });
6040
+
6041
+ section.appendChild(addBeforeButton);
6042
+ section.appendChild(addAfterButton);
6043
+ section.appendChild(toggleStyleButton);
6044
+ section.appendChild(deleteButton);
6045
+
6046
+ return section
5780
6047
  }
5781
6048
 
5782
- #buildAttachmentNode(innerHtml, contentType, sgid) {
5783
- return new CustomActionTextAttachmentNode({ sgid, contentType, innerHtml })
6049
+ #createDeleteTableButton() {
6050
+ const container = createElement("div", { className: "lexxy-table-control" });
6051
+
6052
+ const deleteTableButton = this.#createButton("Delete this table?", { action: "delete", childType: "table" });
6053
+ deleteTableButton.classList.add("lexxy-table-control__button--delete-table");
6054
+
6055
+ container.appendChild(deleteTableButton);
6056
+
6057
+ this.deleteContainer = container;
6058
+
6059
+ return container
5784
6060
  }
5785
6061
 
5786
- get #editorContents() {
5787
- return this.#editorElement.contents
6062
+ #createButton(label, command = {}, icon = this.#icon(command)) {
6063
+ const button = createElement("button", {
6064
+ className: "lexxy-table-control__button",
6065
+ "aria-label": label,
6066
+ type: "button"
6067
+ });
6068
+ button.tabIndex = -1;
6069
+ button.innerHTML = `${icon} <span>${label}</span>`;
6070
+
6071
+ button.dataset.action = command.action;
6072
+ button.dataset.childType = command.childType;
6073
+ button.dataset.direction = command.direction;
6074
+
6075
+ button.addEventListener("click", () => this.#executeTableCommand(command));
6076
+
6077
+ button.addEventListener("mouseover", () => this.#handleCommandButtonHover());
6078
+ button.addEventListener("focus", () => this.#handleCommandButtonHover());
6079
+ button.addEventListener("mouseout", () => this.#handleCommandButtonHover());
6080
+
6081
+ return button
5788
6082
  }
5789
6083
 
5790
- get #editorContentElement() {
5791
- return this.#editorElement.editorContentElement
6084
+ #registerKeyboardShortcuts() {
6085
+ this.unregisterKeyboardShortcuts = this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleAccessibilityShortcutKey, COMMAND_PRIORITY_HIGH);
5792
6086
  }
5793
6087
 
5794
- async #buildPopover() {
5795
- const popoverContainer = createElement("ul", { role: "listbox", id: generateDomId("prompt-popover") }); // Avoiding [popover] due to not being able to position at an arbitrary X, Y position.
5796
- popoverContainer.classList.add("lexxy-prompt-menu");
5797
- popoverContainer.style.position = "absolute";
5798
- popoverContainer.setAttribute("nonce", getNonce());
5799
- popoverContainer.append(...await this.source.buildListItems());
5800
- popoverContainer.addEventListener("click", this.#handlePopoverClick);
5801
- this.#editorElement.appendChild(popoverContainer);
5802
- return popoverContainer
6088
+ #unregisterKeyboardShortcuts() {
6089
+ this.unregisterKeyboardShortcuts?.();
6090
+ this.unregisterKeyboardShortcuts = null;
5803
6091
  }
5804
6092
 
5805
- #handlePopoverClick = (event) => {
5806
- const listItem = event.target.closest(".lexxy-prompt-menu__item");
5807
- if (listItem) {
5808
- this.#selectOption(listItem);
5809
- this.#optionWasSelected();
6093
+ #handleAccessibilityShortcutKey = (event) => {
6094
+ if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === "F10") {
6095
+ const firstButton = this.querySelector("button, [tabindex]:not([tabindex='-1'])");
6096
+ firstButton?.focus();
5810
6097
  }
5811
6098
  }
5812
6099
 
5813
- #reconnect() {
5814
- this.disconnectedCallback();
5815
- this.connectedCallback();
6100
+ #handleToolsKeydown = (event) => {
6101
+ if (event.key === "Escape") {
6102
+ this.#handleEscapeKey();
6103
+ } else {
6104
+ handleRollingTabIndex(this.#tableToolsButtons, event);
6105
+ }
5816
6106
  }
5817
- }
5818
6107
 
5819
- customElements.define("lexxy-prompt", LexicalPromptElement);
6108
+ #handleEscapeKey() {
6109
+ const cell = this.tableController.currentCell;
6110
+ if (!cell) return
5820
6111
 
5821
- class CodeLanguagePicker extends HTMLElement {
5822
- connectedCallback() {
5823
- this.editorElement = this.closest("lexxy-editor");
5824
- this.editor = this.editorElement.editor;
6112
+ this.#editor.update(() => {
6113
+ cell.select();
6114
+ this.#editor.focus();
6115
+ });
5825
6116
 
5826
- this.#attachLanguagePicker();
5827
- this.#monitorForCodeBlockSelection();
6117
+ this.#update();
5828
6118
  }
5829
6119
 
5830
- #attachLanguagePicker() {
5831
- this.languagePickerElement = this.#createLanguagePicker();
5832
-
5833
- this.languagePickerElement.addEventListener("change", () => {
5834
- this.#updateCodeBlockLanguage(this.languagePickerElement.value);
5835
- });
6120
+ async #handleCommandButtonHover() {
6121
+ await nextFrame();
5836
6122
 
5837
- this.languagePickerElement.setAttribute("nonce", getNonce());
5838
- this.appendChild(this.languagePickerElement);
5839
- }
6123
+ this.#clearCellStyles();
5840
6124
 
5841
- #createLanguagePicker() {
5842
- const selectElement = createElement("select", { className: "lexxy-code-language-picker", "aria-label": "Pick a language…", name: "lexxy-code-language" });
6125
+ const activeElement = this.querySelector("button:hover, button:focus");
6126
+ if (!activeElement) return
5843
6127
 
5844
- for (const [ value, label ] of Object.entries(this.#languages)) {
5845
- const option = document.createElement("option");
5846
- option.value = value;
5847
- option.textContent = label;
5848
- selectElement.appendChild(option);
5849
- }
6128
+ const command = {
6129
+ action: activeElement.dataset.action,
6130
+ childType: activeElement.dataset.childType,
6131
+ direction: activeElement.dataset.direction
6132
+ };
5850
6133
 
5851
- return selectElement
5852
- }
6134
+ let cellsToHighlight = null;
5853
6135
 
5854
- get #languages() {
5855
- const languages = { ...CODE_LANGUAGE_FRIENDLY_NAME_MAP };
6136
+ switch (command.childType) {
6137
+ case "row":
6138
+ cellsToHighlight = this.tableController.currentRowCells;
6139
+ break
6140
+ case "column":
6141
+ cellsToHighlight = this.tableController.currentColumnCells;
6142
+ break
6143
+ case "table":
6144
+ cellsToHighlight = this.tableController.tableRows;
6145
+ break
6146
+ }
5856
6147
 
5857
- if (!languages.ruby) languages.ruby = "Ruby";
5858
- if (!languages.php) languages.php = "PHP";
5859
- if (!languages.go) languages.go = "Go";
5860
- if (!languages.bash) languages.bash = "Bash";
5861
- if (!languages.json) languages.json = "JSON";
5862
- if (!languages.diff) languages.diff = "Diff";
6148
+ if (!cellsToHighlight) return
5863
6149
 
5864
- const sortedEntries = Object.entries(languages)
5865
- .sort(([ , a ], [ , b ]) => a.localeCompare(b));
6150
+ cellsToHighlight.forEach(cell => {
6151
+ const cellElement = this.#editor.getElementByKey(cell.getKey());
6152
+ if (!cellElement) return
5866
6153
 
5867
- // Place the "plain" entry first, then the rest of language sorted alphabetically
5868
- const plainIndex = sortedEntries.findIndex(([ key ]) => key === "plain");
5869
- const plainEntry = sortedEntries.splice(plainIndex, 1)[0];
5870
- return Object.fromEntries([ plainEntry, ...sortedEntries ])
6154
+ cellElement.classList.toggle(theme.tableCellHighlight, true);
6155
+ Object.assign(cellElement.dataset, command);
6156
+ });
5871
6157
  }
5872
6158
 
5873
- #updateCodeBlockLanguage(language) {
5874
- this.editor.update(() => {
5875
- const codeNode = this.#getCurrentCodeNode();
6159
+ #monitorForTableSelection() {
6160
+ this.unregisterUpdateListener = this.#editor.registerUpdateListener(() => {
6161
+ this.tableController.updateSelectedTable();
5876
6162
 
5877
- if (codeNode) {
5878
- codeNode.setLanguage(language);
6163
+ const tableNode = this.tableController.currentTableNode;
6164
+ if (tableNode) {
6165
+ this.#show();
6166
+ } else {
6167
+ this.#hide();
5879
6168
  }
5880
6169
  });
5881
6170
  }
5882
6171
 
5883
- #monitorForCodeBlockSelection() {
5884
- this.editor.registerUpdateListener(() => {
5885
- this.editor.getEditorState().read(() => {
5886
- const codeNode = this.#getCurrentCodeNode();
6172
+ #executeTableCommand(command) {
6173
+ this.tableController.executeTableCommand(command);
6174
+ this.#update();
6175
+ }
5887
6176
 
5888
- if (codeNode) {
5889
- this.#codeNodeWasSelected(codeNode);
5890
- } else {
5891
- this.#hideLanguagePicker();
5892
- }
5893
- });
5894
- });
6177
+ #show() {
6178
+ this.style.display = "flex";
6179
+ this.#update();
5895
6180
  }
5896
6181
 
5897
- #getCurrentCodeNode() {
5898
- const selection = $getSelection();
6182
+ #hide() {
6183
+ this.style.display = "none";
6184
+ this.#clearCellStyles();
6185
+ }
5899
6186
 
5900
- if (!$isRangeSelection(selection)) {
5901
- return null
5902
- }
6187
+ #update() {
6188
+ this.#updateButtonsPosition();
6189
+ this.#updateRowColumnCount();
6190
+ this.#closeMoreMenu();
6191
+ this.#handleCommandButtonHover();
6192
+ }
5903
6193
 
5904
- const anchorNode = selection.anchor.getNode();
5905
- const parentNode = anchorNode.getParent();
6194
+ #closeMoreMenu() {
6195
+ this.querySelector("details[open]")?.removeAttribute("open");
6196
+ }
5906
6197
 
5907
- if ($isCodeNode(anchorNode)) {
5908
- return anchorNode
5909
- } else if ($isCodeNode(parentNode)) {
5910
- return parentNode
5911
- }
6198
+ #updateButtonsPosition() {
6199
+ const tableNode = this.tableController.currentTableNode;
6200
+ if (!tableNode) return
5912
6201
 
5913
- return null
5914
- }
6202
+ const tableElement = this.#editor.getElementByKey(tableNode.getKey());
6203
+ if (!tableElement) return
5915
6204
 
5916
- #codeNodeWasSelected(codeNode) {
5917
- const language = codeNode.getLanguage();
6205
+ const tableRect = tableElement.getBoundingClientRect();
6206
+ const editorRect = this.#editorElement.getBoundingClientRect();
5918
6207
 
5919
- this.#updateLanguagePickerWith(language);
5920
- this.#showLanguagePicker();
5921
- this.#positionLanguagePicker(codeNode);
6208
+ const relativeTop = tableRect.top - editorRect.top;
6209
+ const relativeCenter = (tableRect.left + tableRect.right) / 2 - editorRect.left;
6210
+ this.style.top = `${relativeTop}px`;
6211
+ this.style.left = `${relativeCenter}px`;
5922
6212
  }
5923
6213
 
5924
- #updateLanguagePickerWith(language) {
5925
- if (this.languagePickerElement && language) {
5926
- const normalizedLanguage = normalizeCodeLang(language);
5927
- this.languagePickerElement.value = normalizedLanguage;
5928
- }
6214
+ #updateRowColumnCount() {
6215
+ const tableNode = this.tableController.currentTableNode;
6216
+ if (!tableNode) return
6217
+
6218
+ const tableElement = $getElementForTableNode(this.#editor, tableNode);
6219
+ if (!tableElement) return
6220
+
6221
+ const rowCount = tableElement.rows;
6222
+ const columnCount = tableElement.columns;
6223
+
6224
+ this.rowCount.textContent = `${rowCount} row${rowCount === 1 ? "" : "s"}`;
6225
+ this.columnCount.textContent = `${columnCount} column${columnCount === 1 ? "" : "s"}`;
5929
6226
  }
5930
6227
 
5931
- #positionLanguagePicker(codeNode) {
5932
- const codeElement = this.editor.getElementByKey(codeNode.getKey());
5933
- if (!codeElement) return
6228
+ #setTableCellFocus() {
6229
+ const cell = this.tableController.currentCell;
6230
+ if (!cell) return
5934
6231
 
5935
- const codeRect = codeElement.getBoundingClientRect();
5936
- const editorRect = this.editorElement.getBoundingClientRect();
5937
- const relativeTop = codeRect.top - editorRect.top;
6232
+ const cellElement = this.#editor.getElementByKey(cell.getKey());
6233
+ if (!cellElement) return
5938
6234
 
5939
- this.style.top = `${relativeTop}px`;
6235
+ cellElement.classList.add(theme.tableCellFocus);
5940
6236
  }
5941
6237
 
5942
- #showLanguagePicker() {
5943
- this.hidden = false;
6238
+ #clearCellStyles() {
6239
+ this.#editorElement.querySelectorAll(`.${theme.tableCellFocus}`)?.forEach(cell => {
6240
+ cell.classList.remove(theme.tableCellFocus);
6241
+ });
6242
+
6243
+ this.#editorElement.querySelectorAll(`.${theme.tableCellHighlight}`)?.forEach(cell => {
6244
+ cell.classList.remove(theme.tableCellHighlight);
6245
+ cell.removeAttribute("data-action");
6246
+ cell.removeAttribute("data-child-type");
6247
+ cell.removeAttribute("data-direction");
6248
+ });
6249
+
6250
+ this.#setTableCellFocus();
5944
6251
  }
5945
6252
 
5946
- #hideLanguagePicker() {
5947
- this.hidden = true;
6253
+ #icon(command) {
6254
+ const { action, childType } = command;
6255
+ const direction = (action == "insert" ? command.direction : null);
6256
+ const iconId = [ action, childType, direction ].filter(Boolean).join("-");
6257
+ return TableIcons[iconId]
5948
6258
  }
5949
6259
  }
5950
6260
 
5951
- customElements.define("lexxy-code-language-picker", CodeLanguagePicker);
6261
+ function defineElements() {
6262
+ const elements = {
6263
+ "lexxy-toolbar": LexicalToolbarElement,
6264
+ "lexxy-editor": LexicalEditorElement,
6265
+ "lexxy-link-dropdown": LinkDropdown,
6266
+ "lexxy-highlight-dropdown": HighlightDropdown,
6267
+ "lexxy-prompt": LexicalPromptElement,
6268
+ "lexxy-code-language-picker": CodeLanguagePicker,
6269
+ "lexxy-table-tools": TableTools,
6270
+ };
6271
+
6272
+ Object.entries(elements).forEach(([ name, element ]) => {
6273
+ customElements.define(name, element);
6274
+ });
6275
+ }
5952
6276
 
5953
6277
  class LexxyExtension {
5954
6278
  #editorElement
@@ -5981,4 +6305,7 @@ class LexxyExtension {
5981
6305
 
5982
6306
  const configure = Lexxy.configure;
5983
6307
 
6308
+ // Pushing elements definition to after the current call stack to allow global configuration to take place first
6309
+ setTimeout(defineElements, 0);
6310
+
5984
6311
  export { ActionTextAttachmentNode, ActionTextAttachmentUploadNode, CustomActionTextAttachmentNode, LexxyExtension as Extension, HorizontalDividerNode, configure };