@37signals/lexxy 0.1.22-beta → 0.1.23-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ A modern rich text editor for Rails.
23
23
  Add this line to your application's Gemfile:
24
24
 
25
25
  ```ruby
26
- gem 'lexxy', '~> 0.1.21.beta' # Need to specify the version since it's a pre-release
26
+ gem 'lexxy', '~> 0.1.22.beta' # Need to specify the version since it's a pre-release
27
27
  ```
28
28
 
29
29
  And then execute:
package/dist/lexxy.esm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import DOMPurify from 'dompurify';
2
2
  import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
3
- import { $isTextNode, TextNode, $isRangeSelection, $getSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, SELECTION_CHANGE_COMMAND, $createNodeSelection, $setSelection, $createParagraphNode, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, $insertNodes, $createLineBreakNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, SKIP_DOM_SELECTION_TAG, createEditor, COMMAND_PRIORITY_NORMAL, BLUR_COMMAND, FOCUS_COMMAND, KEY_TAB_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
3
+ import { $isTextNode, TextNode, $isRangeSelection, $getSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, SELECTION_CHANGE_COMMAND, $createNodeSelection, $setSelection, $createParagraphNode, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, $insertNodes, $createLineBreakNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, SKIP_DOM_SELECTION_TAG, createEditor, BLUR_COMMAND, FOCUS_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
4
4
  import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $createListNode, ListNode, ListItemNode, registerList } from '@lexical/list';
5
5
  import { $isQuoteNode, $isHeadingNode, $createQuoteNode, $createHeadingNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
6
6
  import { $isCodeNode, CodeNode, normalizeCodeLang, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
@@ -136,6 +136,8 @@ function hasHighlightStyles(cssOrStyles) {
136
136
  }
137
137
 
138
138
  class LexicalToolbarElement extends HTMLElement {
139
+ static observedAttributes = [ "connected" ]
140
+
139
141
  constructor() {
140
142
  super();
141
143
  this.internals = this.attachInternals();
@@ -154,6 +156,13 @@ class LexicalToolbarElement extends HTMLElement {
154
156
  this._resizeObserver.disconnect();
155
157
  this._resizeObserver = null;
156
158
  }
159
+ this.#unbindHotkeys();
160
+ }
161
+
162
+ attributeChangedCallback(name, oldValue, newValue) {
163
+ if (name === "connected" && this.isConnected && oldValue != null && oldValue !== newValue) {
164
+ requestAnimationFrame(() => this.#reconnect());
165
+ }
157
166
  }
158
167
 
159
168
  setEditor(editorElement) {
@@ -161,7 +170,8 @@ class LexicalToolbarElement extends HTMLElement {
161
170
  this.editor = editorElement.editor;
162
171
  this.#bindButtons();
163
172
  this.#bindHotkeys();
164
- this.#assignButtonTabindex();
173
+ this.#setTabIndexValues();
174
+ this.#setItemPositionValues();
165
175
  this.#monitorSelectionChanges();
166
176
  this.#monitorHistoryChanges();
167
177
  this.#refreshToolbarOverflow();
@@ -169,10 +179,9 @@ class LexicalToolbarElement extends HTMLElement {
169
179
  this.toggleAttribute("connected", true);
170
180
  }
171
181
 
172
- get #dialogs() {
173
- const dialogButtons = this.querySelectorAll("[data-dialog-target]");
174
- const dialogTags = Array.from(dialogButtons).map(button => `lexxy-${button.dataset.dialogTarget}`);
175
- return Array.from(this.querySelectorAll(dialogTags))
182
+ #reconnect() {
183
+ this.disconnectedCallback();
184
+ this.connectedCallback();
176
185
  }
177
186
 
178
187
  #bindButtons() {
@@ -181,7 +190,6 @@ class LexicalToolbarElement extends HTMLElement {
181
190
 
182
191
  #handleButtonClicked({ target }) {
183
192
  this.#handleTargetClicked(target, "[data-command]", this.#dispatchButtonCommand.bind(this));
184
- this.#handleTargetClicked(target, "[data-dialog-target]", this.#toggleDialog.bind(this));
185
193
  }
186
194
 
187
195
  #handleTargetClicked(target, selector, callback) {
@@ -196,38 +204,23 @@ class LexicalToolbarElement extends HTMLElement {
196
204
  this.editor.dispatchCommand(command, payload);
197
205
  }
198
206
 
199
- // Not using popover because of CSS anchoring still not widely available.
200
- #toggleDialog(button) {
201
- const dialogTarget = button.dataset.dialogTarget;
202
- const dialog = this.querySelector("lexxy-" + dialogTarget);
203
- if (!dialog) return
204
-
205
- if (dialog.open) {
206
- dialog.close();
207
- } else {
208
- this.#closeOpenDialogs();
209
- dialog.show(button);
210
- }
207
+ #bindHotkeys() {
208
+ this.editorElement.addEventListener("keydown", this.#handleHotkey);
211
209
  }
212
210
 
213
- #closeOpenDialogs() {
214
- const openDialogs = this.querySelectorAll("dialog[open]");
215
- openDialogs.forEach(openDialog => {
216
- openDialog.closest(".lexxy-dialog").close();
217
- });
211
+ #unbindHotkeys() {
212
+ this.editorElement?.removeEventListener("keydown", this.#handleHotkey);
218
213
  }
219
214
 
220
- #bindHotkeys() {
221
- this.editorElement.addEventListener("keydown", (event) => {
222
- const buttons = this.querySelectorAll("[data-hotkey]");
223
- buttons.forEach((button) => {
224
- const hotkeys = button.dataset.hotkey.toLowerCase().split(/\s+/);
225
- if (hotkeys.includes(this.#keyCombinationFor(event))) {
226
- event.preventDefault();
227
- event.stopPropagation();
228
- button.click();
229
- }
230
- });
215
+ #handleHotkey = (event) => {
216
+ const buttons = this.querySelectorAll("[data-hotkey]");
217
+ buttons.forEach((button) => {
218
+ const hotkeys = button.dataset.hotkey.toLowerCase().split(/\s+/);
219
+ if (hotkeys.includes(this.#keyCombinationFor(event))) {
220
+ event.preventDefault();
221
+ event.stopPropagation();
222
+ button.click();
223
+ }
231
224
  });
232
225
  }
233
226
 
@@ -243,10 +236,9 @@ class LexicalToolbarElement extends HTMLElement {
243
236
  return [ ...modifiers, pressedKey ].join("+")
244
237
  }
245
238
 
246
- #assignButtonTabindex() {
247
- const baseTabIndex = parseInt(this.editorElement.editorContentElement.getAttribute("tabindex") ?? "0");
248
- this.#buttons.forEach((button, index) => {
249
- button.setAttribute("tabindex", `${baseTabIndex + index + 1}`);
239
+ #setTabIndexValues() {
240
+ this.#buttons.forEach((button) => {
241
+ button.setAttribute("tabindex", 0);
250
242
  });
251
243
  }
252
244
 
@@ -254,7 +246,6 @@ class LexicalToolbarElement extends HTMLElement {
254
246
  this.editor.registerUpdateListener(() => {
255
247
  this.editor.getEditorState().read(() => {
256
248
  this.#updateButtonStates();
257
- this.#updateDialogStates();
258
249
  });
259
250
  });
260
251
  }
@@ -309,10 +300,6 @@ class LexicalToolbarElement extends HTMLElement {
309
300
  this.#updateUndoRedoButtonStates();
310
301
  }
311
302
 
312
- #updateDialogStates() {
313
- this.#dialogs.forEach(dialog => dialog.updateStateCallback());
314
- }
315
-
316
303
  #isInList(node) {
317
304
  let current = node;
318
305
  while (current) {
@@ -361,22 +348,8 @@ class LexicalToolbarElement extends HTMLElement {
361
348
  this.toggleAttribute("overflowing", isOverflowing);
362
349
  }
363
350
 
364
- get #overflow() {
365
- return this.querySelector(".lexxy-editor__toolbar-overflow")
366
- }
367
-
368
- get #overflowMenu() {
369
- return this.querySelector(".lexxy-editor__toolbar-overflow-menu")
370
- }
371
-
372
- #resetToolbar() {
373
- while (this.#overflowMenu.children.length > 0) {
374
- this.insertBefore(this.#overflowMenu.children[0], this.#overflow);
375
- }
376
- }
377
-
378
351
  #compactMenu() {
379
- const buttons = this.#buttonsWithSeparator.reverse();
352
+ const buttons = this.#buttons.reverse();
380
353
  let movedToOverflow = false;
381
354
 
382
355
  for (const button of buttons) {
@@ -390,12 +363,42 @@ class LexicalToolbarElement extends HTMLElement {
390
363
  }
391
364
  }
392
365
 
366
+ #resetToolbar() {
367
+ const items = Array.from(this.#overflowMenu.children);
368
+ items.sort((a, b) => this.#itemPosition(b) - this.#itemPosition(a));
369
+
370
+ items.forEach((item) => {
371
+ const nextItem = this.querySelector(`[data-position="${this.#itemPosition(item) + 1}"]`) ?? this.#overflow;
372
+ this.insertBefore(item, nextItem);
373
+ });
374
+ }
375
+
376
+ #itemPosition(item) {
377
+ return parseInt(item.dataset.position ?? "999")
378
+ }
379
+
380
+ #setItemPositionValues() {
381
+ this.#toolbarItems.forEach((item, index) => {
382
+ if (item.dataset.position === undefined) {
383
+ item.dataset.position = index;
384
+ }
385
+ });
386
+ }
387
+
388
+ get #overflow() {
389
+ return this.querySelector(".lexxy-editor__toolbar-overflow")
390
+ }
391
+
392
+ get #overflowMenu() {
393
+ return this.querySelector(".lexxy-editor__toolbar-overflow-menu")
394
+ }
395
+
393
396
  get #buttons() {
394
397
  return Array.from(this.querySelectorAll(":scope > button"))
395
398
  }
396
399
 
397
- get #buttonsWithSeparator() {
398
- return Array.from(this.querySelectorAll(":scope > button, :scope > [role=separator]"))
400
+ get #toolbarItems() {
401
+ return Array.from(this.querySelectorAll(":scope > *:not(.lexxy-editor__toolbar-overflow)"))
399
402
  }
400
403
 
401
404
  static get defaultTemplate() {
@@ -414,35 +417,31 @@ class LexicalToolbarElement extends HTMLElement {
414
417
  </svg>
415
418
  </button>
416
419
 
417
- <lexxy-highlight-dialog class="lexxy-dialog lexxy-highlight-dialog">
418
- <dialog class="highlight-dialog">
419
- <div class="lexxy-highlight-dialog-content">
420
- <div data-button-group="color" data-values="var(--highlight-1); var(--highlight-2); var(--highlight-3); var(--highlight-4); var(--highlight-5); var(--highlight-6); var(--highlight-7); var(--highlight-8); var(--highlight-9)"></div>
421
- <div data-button-group="background-color" data-values="var(--highlight-bg-1); var(--highlight-bg-2); var(--highlight-bg-3); var(--highlight-bg-4); var(--highlight-bg-5); var(--highlight-bg-6); var(--highlight-bg-7); var(--highlight-bg-8); var(--highlight-bg-9)"></div>
422
- <button data-command="removeHighlight" class="lexxy-highlight-dialog-reset">Remove all coloring</button>
423
- </div>
424
- </dialog>
425
- </lexxy-highlight-dialog>
426
-
427
- <button class="lexxy-editor__toolbar-button" type="button" name="highlight" title="Color highlight" data-dialog-target="highlight-dialog">
428
- <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>
429
- </button>
420
+ <details class="lexxy-editor__toolbar-dropdown" name="lexxy-dropdown">
421
+ <summary class="lexxy-editor__toolbar-button" name="highlight" title="Color highlight">
422
+ <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>
423
+ </summary>
424
+ <lexxy-highlight-dropdown class="lexxy-editor__toolbar-dropdown-content">
425
+ <div data-button-group="color" data-values="var(--highlight-1); var(--highlight-2); var(--highlight-3); var(--highlight-4); var(--highlight-5); var(--highlight-6); var(--highlight-7); var(--highlight-8); var(--highlight-9)"></div>
426
+ <div data-button-group="background-color" data-values="var(--highlight-bg-1); var(--highlight-bg-2); var(--highlight-bg-3); var(--highlight-bg-4); var(--highlight-bg-5); var(--highlight-bg-6); var(--highlight-bg-7); var(--highlight-bg-8); var(--highlight-bg-9)"></div>
427
+ <button data-command="removeHighlight" class="lexxy-editor__toolbar-dropdown-reset">Remove all coloring</button>
428
+ </lexxy-highlight-dropdown>
429
+ </details>
430
430
 
431
- <lexxy-link-dialog class="lexxy-dialog lexxy-link-dialog">
432
- <dialog class="link-dialog">
431
+ <details class="lexxy-editor__toolbar-dropdown" name="lexxy-dropdown">
432
+ <summary class="lexxy-editor__toolbar-button" name="link" title="Link" data-hotkey="cmd+k ctrl+k">
433
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.111 9.546a1.5 1.5 0 012.121 0 5.5 5.5 0 010 7.778l-2.828 2.828a5.5 5.5 0 01-7.778 0 5.498 5.498 0 010-7.777l2.828-2.83a1.5 1.5 0 01.355-.262 6.52 6.52 0 00.351 3.799l-1.413 1.414a2.499 2.499 0 000 3.535 2.499 2.499 0 003.535 0l2.83-2.828a2.5 2.5 0 000-3.536 1.5 1.5 0 010-2.121z"/><path d="M12.111 3.89a5.5 5.5 0 117.778 7.777l-2.828 2.829a1.496 1.496 0 01-.355.262 6.522 6.522 0 00-.351-3.8l1.413-1.412a2.5 2.5 0 10-3.536-3.535l-2.828 2.828a2.5 2.5 0 000 3.536 1.5 1.5 0 01-2.122 2.12 5.5 5.5 0 010-7.777l2.83-2.829z"/></svg>
434
+ </summary>
435
+ <lexxy-link-dropdown class="lexxy-editor__toolbar-dropdown-content">
433
436
  <form method="dialog">
434
- <input type="url" placeholder="Enter a URL…" class="input" required>
435
- <div class="lexxy-dialog-actions">
437
+ <input type="url" placeholder="Enter a URL…" class="input">
438
+ <div class="lexxy-editor__toolbar-dropdown-actions">
436
439
  <button type="submit" class="btn" value="link">Link</button>
437
440
  <button type="button" class="btn" value="unlink">Unlink</button>
438
441
  </div>
439
442
  </form>
440
- </dialog>
441
- </lexxy-link-dialog>
442
-
443
- <button class="lexxy-editor__toolbar-button" type="button" name="link" title="Link" data-dialog-target="link-dialog" data-hotkey="cmd+k ctrl+k">
444
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.111 9.546a1.5 1.5 0 012.121 0 5.5 5.5 0 010 7.778l-2.828 2.828a5.5 5.5 0 01-7.778 0 5.498 5.498 0 010-7.777l2.828-2.83a1.5 1.5 0 01.355-.262 6.52 6.52 0 00.351 3.799l-1.413 1.414a2.499 2.499 0 000 3.535 2.499 2.499 0 003.535 0l2.83-2.828a2.5 2.5 0 000-3.536 1.5 1.5 0 010-2.121z"/><path d="M12.111 3.89a5.5 5.5 0 117.778 7.777l-2.828 2.829a1.496 1.496 0 01-.355.262 6.522 6.522 0 00-.351-3.8l1.413-1.412a2.5 2.5 0 10-3.536-3.535l-2.828 2.828a2.5 2.5 0 000 3.536 1.5 1.5 0 01-2.122 2.12 5.5 5.5 0 010-7.777l2.83-2.829z"/></svg>
445
- </button>
443
+ </lexxy-link-dropdown>
444
+ </details>
446
445
 
447
446
  <button class="lexxy-editor__toolbar-button" type="button" name="quote" data-command="insertQuoteBlock" title="Quote">
448
447
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.5 5C8.985 5 11 7.09 11 9.667c0 2.694-.962 5.005-2.187 6.644-.613.82-1.3 1.481-1.978 1.943-.668.454-1.375.746-2.022.746a.563.563 0 01-.52-.36.602.602 0 01.067-.57l.055-.066.009-.009.041-.048a4.25 4.25 0 00.168-.21c.143-.188.336-.47.53-.84a6.743 6.743 0 00.75-2.605C3.705 13.994 2 12.038 2 9.667 2 7.089 4.015 5 6.5 5zM17.5 5C19.985 5 22 7.09 22 9.667c0 2.694-.962 5.005-2.187 6.644-.613.82-1.3 1.481-1.978 1.943-.668.454-1.375.746-2.023.746a.563.563 0 01-.52-.36.602.602 0 01.068-.57l.055-.066.009-.009.041-.048c.039-.045.097-.115.168-.21a6.16 6.16 0 00.53-.84 6.745 6.745 0 00.75-2.605C14.705 13.994 13 12.038 13 9.667 13 7.089 15.015 5 17.5 5z"/></svg>
@@ -1136,6 +1135,7 @@ class CommandDispatcher {
1136
1135
  this.highlighter = editorElement.highlighter;
1137
1136
 
1138
1137
  this.#registerCommands();
1138
+ this.#registerKeyboardCommands();
1139
1139
  this.#registerDragAndDropHandlers();
1140
1140
  }
1141
1141
 
@@ -1300,15 +1300,8 @@ class CommandDispatcher {
1300
1300
  this.editor.registerCommand(command, handler, priority);
1301
1301
  }
1302
1302
 
1303
- // Not using TOGGLE_LINK_COMMAND because it's not handled unless you use React/LinkPlugin
1304
- #toggleLink(url) {
1305
- this.editor.update(() => {
1306
- if (url === null) {
1307
- $toggleLink(null);
1308
- } else {
1309
- $toggleLink(url);
1310
- }
1311
- });
1303
+ #registerKeyboardCommands() {
1304
+ this.editor.registerCommand(KEY_TAB_COMMAND, this.#handleListIndentation.bind(this), COMMAND_PRIORITY_NORMAL);
1312
1305
  }
1313
1306
 
1314
1307
  #registerDragAndDropHandlers() {
@@ -1357,6 +1350,29 @@ class CommandDispatcher {
1357
1350
 
1358
1351
  this.editor.focus();
1359
1352
  }
1353
+
1354
+ #handleListIndentation(event) {
1355
+ if (this.selection.isInsideList) {
1356
+ event.preventDefault();
1357
+ if (event.shiftKey) {
1358
+ return this.editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined)
1359
+ } else {
1360
+ return this.editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)
1361
+ }
1362
+ }
1363
+ return false
1364
+ }
1365
+
1366
+ // Not using TOGGLE_LINK_COMMAND because it's not handled unless you use React/LinkPlugin
1367
+ #toggleLink(url) {
1368
+ this.editor.update(() => {
1369
+ if (url === null) {
1370
+ $toggleLink(null);
1371
+ } else {
1372
+ $toggleLink(url);
1373
+ }
1374
+ });
1375
+ }
1360
1376
  }
1361
1377
 
1362
1378
  function capitalize(str) {
@@ -3475,6 +3491,10 @@ class LexicalEditorElement extends HTMLElement {
3475
3491
  return this.getAttribute("attachments") !== "false"
3476
3492
  }
3477
3493
 
3494
+ get contentTabIndex() {
3495
+ return parseInt(this.editorContentElement?.getAttribute("tabindex") ?? "0")
3496
+ }
3497
+
3478
3498
  focus() {
3479
3499
  this.editor.focus();
3480
3500
  }
@@ -3788,39 +3808,26 @@ class LexicalEditorElement extends HTMLElement {
3788
3808
 
3789
3809
  #reconnect() {
3790
3810
  this.disconnectedCallback();
3811
+ this.valueBeforeDisconnect = null;
3791
3812
  this.connectedCallback();
3792
3813
  }
3793
3814
  }
3794
3815
 
3795
3816
  customElements.define("lexxy-editor", LexicalEditorElement);
3796
3817
 
3797
- class ToolbarDialog extends HTMLElement {
3818
+ class ToolbarDropdown extends HTMLElement {
3798
3819
  connectedCallback() {
3799
- this.dialog = this.querySelector("dialog");
3800
- if ("closedBy" in this.dialog.constructor.prototype) {
3801
- this.dialog.closedBy = "any";
3802
- }
3803
- this.#registerHandlers();
3804
- }
3805
-
3806
- disconnectedCallback() {
3807
- this.#removeClickOutsideHandler();
3808
- }
3809
-
3810
- updateStateCallback() { }
3820
+ this.container = this.closest("details");
3811
3821
 
3812
- show(triggerButton) {
3813
- if (this.preventImmediateReopen) { return }
3822
+ this.container.addEventListener("toggle", this.#handleToggle.bind(this));
3823
+ this.container.addEventListener("keydown", this.#handleKeyDown.bind(this));
3814
3824
 
3815
- this.triggerButton = triggerButton;
3816
- this.#positionDialog();
3817
- this.dialog.show();
3818
-
3819
- this.#setupClickOutsideHandler();
3825
+ this.#setTabIndexValues();
3820
3826
  }
3821
3827
 
3822
- close() {
3823
- this.dialog.close();
3828
+ disconnectedCallback() {
3829
+ this.#removeClickOutsideHandler();
3830
+ this.container.removeEventListener("keydown", this.#handleKeyDown.bind(this));
3824
3831
  }
3825
3832
 
3826
3833
  get toolbar() {
@@ -3831,32 +3838,32 @@ class ToolbarDialog extends HTMLElement {
3831
3838
  return this.toolbar.editor
3832
3839
  }
3833
3840
 
3834
- get open() { return this.dialog.open }
3835
-
3836
- #registerHandlers() {
3837
- this.#setupKeydownHandler();
3838
- this.dialog.addEventListener("cancel", this.#handleCancel.bind(this));
3839
- this.dialog.addEventListener("close", this.#handleClose.bind(this));
3841
+ close() {
3842
+ this.container.removeAttribute("open");
3840
3843
  }
3841
3844
 
3842
- #handleClose() {
3843
- this.#removeClickOutsideHandler();
3844
- this.triggerButton = null;
3845
- this.editor.focus();
3845
+ #handleToggle(event) {
3846
+ if (this.container.open) {
3847
+ this.#handleOpen(event.target);
3848
+ } else {
3849
+ this.#handleClose();
3850
+ }
3846
3851
  }
3847
3852
 
3848
- #handleCancel() {
3849
- this.preventImmediateReopen = true;
3850
- requestAnimationFrame(() => this.preventImmediateReopen = undefined);
3853
+ #handleOpen(trigger) {
3854
+ this.trigger = trigger;
3855
+ this.#interactiveElements[0].focus();
3856
+ this.#setupClickOutsideHandler();
3851
3857
  }
3852
3858
 
3853
- #positionDialog() {
3854
- const left = this.triggerButton.offsetLeft;
3855
- this.dialog.style.insetInlineStart = `${left}px`;
3859
+ #handleClose() {
3860
+ this.trigger = null;
3861
+ this.#removeClickOutsideHandler();
3862
+ this.editor.focus();
3856
3863
  }
3857
3864
 
3858
3865
  #setupClickOutsideHandler() {
3859
- if (this.#browserHandlesClose || this.clickOutsideHandler) return
3866
+ if (this.clickOutsideHandler) return
3860
3867
 
3861
3868
  this.clickOutsideHandler = this.#handleClickOutside.bind(this);
3862
3869
  document.addEventListener("click", this.clickOutsideHandler, true);
@@ -3870,22 +3877,7 @@ class ToolbarDialog extends HTMLElement {
3870
3877
  }
3871
3878
 
3872
3879
  #handleClickOutside({ target }) {
3873
- if (!this.dialog.open) return
3874
-
3875
- const isClickInsideDialog = this.dialog.contains(target);
3876
- const isClickOnTrigger = this.triggerButton.contains(target);
3877
-
3878
- if (!isClickInsideDialog && !isClickOnTrigger) {
3879
- this.close();
3880
- }
3881
- }
3882
-
3883
- #setupKeydownHandler() {
3884
- if (!this.#browserHandlesClose) { this.addEventListener("keydown", this.#handleKeyDown.bind(this)); }
3885
- }
3886
-
3887
- get #browserHandlesClose() {
3888
- return this.dialog.closedBy === "any"
3880
+ if (this.container.open && !this.container.contains(target)) this.close();
3889
3881
  }
3890
3882
 
3891
3883
  #handleKeyDown(event) {
@@ -3894,9 +3886,20 @@ class ToolbarDialog extends HTMLElement {
3894
3886
  this.close();
3895
3887
  }
3896
3888
  }
3889
+
3890
+ async #setTabIndexValues() {
3891
+ await nextFrame();
3892
+ this.#interactiveElements.forEach((element) => {
3893
+ element.setAttribute("tabindex", 0);
3894
+ });
3895
+ }
3896
+
3897
+ get #interactiveElements() {
3898
+ return Array.from(this.querySelectorAll("button, input"))
3899
+ }
3897
3900
  }
3898
3901
 
3899
- class LinkDialog extends ToolbarDialog {
3902
+ class LinkDropdown extends ToolbarDropdown {
3900
3903
  connectedCallback() {
3901
3904
  super.connectedCallback();
3902
3905
  this.input = this.querySelector("input");
@@ -3904,23 +3907,21 @@ class LinkDialog extends ToolbarDialog {
3904
3907
  this.#registerHandlers();
3905
3908
  }
3906
3909
 
3907
- updateStateCallback() {
3908
- this.input.value = this.#selectedLinkUrl;
3909
- }
3910
-
3911
3910
  #registerHandlers() {
3912
- this.dialog.addEventListener("beforetoggle", this.#handleBeforeToggle.bind(this));
3913
- this.dialog.addEventListener("submit", this.#handleSubmit.bind(this));
3911
+ this.container.addEventListener("toggle", this.#handleToggle.bind(this));
3912
+ this.addEventListener("submit", this.#handleSubmit.bind(this));
3914
3913
  this.querySelector("[value='unlink']").addEventListener("click", this.#handleUnlink.bind(this));
3915
3914
  }
3916
3915
 
3917
- #handleBeforeToggle({ newState }) {
3916
+ #handleToggle({ newState }) {
3917
+ this.input.value = this.#selectedLinkUrl;
3918
3918
  this.input.required = newState === "open";
3919
3919
  }
3920
3920
 
3921
3921
  #handleSubmit(event) {
3922
3922
  const command = event.submitter?.value;
3923
3923
  this.editor.dispatchCommand(command, this.input.value);
3924
+ this.close();
3924
3925
  }
3925
3926
 
3926
3927
  #handleUnlink() {
@@ -3949,9 +3950,7 @@ class LinkDialog extends ToolbarDialog {
3949
3950
  }
3950
3951
  }
3951
3952
 
3952
- // We should extend the native dialog and avoid the intermediary <dialog> but not
3953
- // supported by Safari yet: customElements.define("lexxy-link-dialog", LinkDialog, { extends: "dialog" })
3954
- customElements.define("lexxy-link-dialog", LinkDialog);
3953
+ customElements.define("lexxy-link-dropdown", LinkDropdown);
3955
3954
 
3956
3955
  const APPLY_HIGHLIGHT_SELECTOR = "button.lexxy-highlight-button";
3957
3956
  const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
@@ -3961,7 +3960,7 @@ const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
3961
3960
  // see https://github.com/facebook/lexical/issues/8013
3962
3961
  const NO_STYLE = Symbol("no_style");
3963
3962
 
3964
- class HighlightDialog extends ToolbarDialog {
3963
+ class HighlightDropdown extends ToolbarDropdown {
3965
3964
  connectedCallback() {
3966
3965
  super.connectedCallback();
3967
3966
 
@@ -3969,13 +3968,10 @@ class HighlightDialog extends ToolbarDialog {
3969
3968
  this.#registerHandlers();
3970
3969
  }
3971
3970
 
3972
- updateStateCallback() {
3973
- this.#updateColorButtonStates($getSelection());
3974
- }
3975
-
3976
3971
  #registerHandlers() {
3977
- this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).addEventListener("click", this.#handleRemoveHighlightClick.bind(this));
3972
+ this.container.addEventListener("toggle", this.#handleToggle.bind(this));
3978
3973
  this.#colorButtons.forEach(button => button.addEventListener("click", this.#handleColorButtonClick.bind(this)));
3974
+ this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).addEventListener("click", this.#handleRemoveHighlightClick.bind(this));
3979
3975
  }
3980
3976
 
3981
3977
  #setUpButtons() {
@@ -4002,6 +3998,14 @@ class HighlightDialog extends ToolbarDialog {
4002
3998
  return button
4003
3999
  }
4004
4000
 
4001
+ #handleToggle({ newState }) {
4002
+ if (newState === "open") {
4003
+ this.editor.getEditorState().read(() => {
4004
+ this.#updateColorButtonStates($getSelection());
4005
+ });
4006
+ }
4007
+ }
4008
+
4005
4009
  #handleColorButtonClick(event) {
4006
4010
  event.preventDefault();
4007
4011
 
@@ -4047,9 +4051,7 @@ class HighlightDialog extends ToolbarDialog {
4047
4051
  }
4048
4052
  }
4049
4053
 
4050
- // We should extend the native dialog and avoid the intermediary <dialog> but not
4051
- // supported by Safari yet: customElements.define("lexxy-hightlight-dialog", HighlightDialog, { extends: "dialog" })
4052
- customElements.define("lexxy-highlight-dialog", HighlightDialog);
4054
+ customElements.define("lexxy-highlight-dropdown", HighlightDropdown);
4053
4055
 
4054
4056
  class BaseSource {
4055
4057
  // Template method to override
@@ -4194,10 +4196,13 @@ class LexicalPromptElement extends HTMLElement {
4194
4196
  this.keyListeners = [];
4195
4197
  }
4196
4198
 
4199
+ static observedAttributes = [ "connected" ]
4200
+
4197
4201
  connectedCallback() {
4198
4202
  this.source = this.#createSource();
4199
4203
 
4200
4204
  this.#addTriggerListener();
4205
+ this.toggleAttribute("connected", true);
4201
4206
  }
4202
4207
 
4203
4208
  disconnectedCallback() {
@@ -4205,6 +4210,13 @@ class LexicalPromptElement extends HTMLElement {
4205
4210
  this.popoverElement = null;
4206
4211
  }
4207
4212
 
4213
+
4214
+ attributeChangedCallback(name, oldValue, newValue) {
4215
+ if (name === "connected" && this.isConnected && oldValue != null && oldValue !== newValue) {
4216
+ requestAnimationFrame(() => this.#reconnect());
4217
+ }
4218
+ }
4219
+
4208
4220
  get name() {
4209
4221
  return this.getAttribute("name")
4210
4222
  }
@@ -4574,6 +4586,11 @@ class LexicalPromptElement extends HTMLElement {
4574
4586
  this.#optionWasSelected();
4575
4587
  }
4576
4588
  }
4589
+
4590
+ #reconnect() {
4591
+ this.disconnectedCallback();
4592
+ this.connectedCallback();
4593
+ }
4577
4594
  }
4578
4595
 
4579
4596
  customElements.define("lexxy-prompt", LexicalPromptElement);