@37signals/lexxy 0.1.22-beta → 0.1.24-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
@@ -1,22 +1,31 @@
1
- import DOMPurify from 'dompurify';
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';
4
- import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $createListNode, ListNode, ListItemNode, registerList } from '@lexical/list';
5
- import { $isQuoteNode, $isHeadingNode, $createQuoteNode, $createHeadingNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
6
- import { $isCodeNode, CodeNode, normalizeCodeLang, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
7
- import { $isLinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, LinkNode, AutoLinkNode } from '@lexical/link';
1
+ import Prism from 'prismjs';
2
+ import 'prismjs/components/prism-clike';
3
+ import 'prismjs/components/prism-markup';
4
+ import 'prismjs/components/prism-markup-templating';
8
5
  import 'prismjs/components/prism-ruby';
9
6
  import 'prismjs/components/prism-php';
10
7
  import 'prismjs/components/prism-go';
11
8
  import 'prismjs/components/prism-bash';
12
9
  import 'prismjs/components/prism-json';
13
10
  import 'prismjs/components/prism-diff';
11
+ import DOMPurify from 'dompurify';
12
+ import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
13
+ import { $isTextNode, TextNode, $isRangeSelection, $getSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, SELECTION_CHANGE_COMMAND, $createNodeSelection, $setSelection, $createParagraphNode, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, $insertNodes, $createLineBreakNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, SKIP_DOM_SELECTION_TAG, createEditor, BLUR_COMMAND, FOCUS_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
14
+ import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $createListNode, ListNode, ListItemNode, registerList } from '@lexical/list';
15
+ import { $isQuoteNode, $isHeadingNode, $createQuoteNode, $createHeadingNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
16
+ import { $isCodeNode, CodeNode, normalizeCodeLang, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
17
+ import { $isLinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, LinkNode, AutoLinkNode } from '@lexical/link';
14
18
  import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
15
19
  import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
16
20
  import { createEmptyHistoryState, registerHistory } from '@lexical/history';
17
21
  import { DirectUpload } from '@rails/activestorage';
18
22
  import { marked } from 'marked';
19
23
 
24
+ // Configure Prism for manual highlighting mode
25
+ // This must be set before importing prismjs
26
+ window.Prism = window.Prism || {};
27
+ window.Prism.manual = true;
28
+
20
29
  const ALLOWED_HTML_TAGS = [ "a", "action-text-attachment", "b", "blockquote", "br", "code", "em",
21
30
  "figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "mark", "ol", "p", "pre", "q", "s", "strong", "ul" ];
22
31
 
@@ -136,6 +145,8 @@ function hasHighlightStyles(cssOrStyles) {
136
145
  }
137
146
 
138
147
  class LexicalToolbarElement extends HTMLElement {
148
+ static observedAttributes = [ "connected" ]
149
+
139
150
  constructor() {
140
151
  super();
141
152
  this.internals = this.attachInternals();
@@ -154,6 +165,13 @@ class LexicalToolbarElement extends HTMLElement {
154
165
  this._resizeObserver.disconnect();
155
166
  this._resizeObserver = null;
156
167
  }
168
+ this.#unbindHotkeys();
169
+ }
170
+
171
+ attributeChangedCallback(name, oldValue, newValue) {
172
+ if (name === "connected" && this.isConnected && oldValue != null && oldValue !== newValue) {
173
+ requestAnimationFrame(() => this.#reconnect());
174
+ }
157
175
  }
158
176
 
159
177
  setEditor(editorElement) {
@@ -161,7 +179,8 @@ class LexicalToolbarElement extends HTMLElement {
161
179
  this.editor = editorElement.editor;
162
180
  this.#bindButtons();
163
181
  this.#bindHotkeys();
164
- this.#assignButtonTabindex();
182
+ this.#setTabIndexValues();
183
+ this.#setItemPositionValues();
165
184
  this.#monitorSelectionChanges();
166
185
  this.#monitorHistoryChanges();
167
186
  this.#refreshToolbarOverflow();
@@ -169,10 +188,9 @@ class LexicalToolbarElement extends HTMLElement {
169
188
  this.toggleAttribute("connected", true);
170
189
  }
171
190
 
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))
191
+ #reconnect() {
192
+ this.disconnectedCallback();
193
+ this.connectedCallback();
176
194
  }
177
195
 
178
196
  #bindButtons() {
@@ -181,7 +199,6 @@ class LexicalToolbarElement extends HTMLElement {
181
199
 
182
200
  #handleButtonClicked({ target }) {
183
201
  this.#handleTargetClicked(target, "[data-command]", this.#dispatchButtonCommand.bind(this));
184
- this.#handleTargetClicked(target, "[data-dialog-target]", this.#toggleDialog.bind(this));
185
202
  }
186
203
 
187
204
  #handleTargetClicked(target, selector, callback) {
@@ -196,38 +213,23 @@ class LexicalToolbarElement extends HTMLElement {
196
213
  this.editor.dispatchCommand(command, payload);
197
214
  }
198
215
 
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
- }
216
+ #bindHotkeys() {
217
+ this.editorElement.addEventListener("keydown", this.#handleHotkey);
211
218
  }
212
219
 
213
- #closeOpenDialogs() {
214
- const openDialogs = this.querySelectorAll("dialog[open]");
215
- openDialogs.forEach(openDialog => {
216
- openDialog.closest(".lexxy-dialog").close();
217
- });
220
+ #unbindHotkeys() {
221
+ this.editorElement?.removeEventListener("keydown", this.#handleHotkey);
218
222
  }
219
223
 
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
- });
224
+ #handleHotkey = (event) => {
225
+ const buttons = this.querySelectorAll("[data-hotkey]");
226
+ buttons.forEach((button) => {
227
+ const hotkeys = button.dataset.hotkey.toLowerCase().split(/\s+/);
228
+ if (hotkeys.includes(this.#keyCombinationFor(event))) {
229
+ event.preventDefault();
230
+ event.stopPropagation();
231
+ button.click();
232
+ }
231
233
  });
232
234
  }
233
235
 
@@ -243,10 +245,9 @@ class LexicalToolbarElement extends HTMLElement {
243
245
  return [ ...modifiers, pressedKey ].join("+")
244
246
  }
245
247
 
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}`);
248
+ #setTabIndexValues() {
249
+ this.#buttons.forEach((button) => {
250
+ button.setAttribute("tabindex", 0);
250
251
  });
251
252
  }
252
253
 
@@ -254,7 +255,6 @@ class LexicalToolbarElement extends HTMLElement {
254
255
  this.editor.registerUpdateListener(() => {
255
256
  this.editor.getEditorState().read(() => {
256
257
  this.#updateButtonStates();
257
- this.#updateDialogStates();
258
258
  });
259
259
  });
260
260
  }
@@ -309,10 +309,6 @@ class LexicalToolbarElement extends HTMLElement {
309
309
  this.#updateUndoRedoButtonStates();
310
310
  }
311
311
 
312
- #updateDialogStates() {
313
- this.#dialogs.forEach(dialog => dialog.updateStateCallback());
314
- }
315
-
316
312
  #isInList(node) {
317
313
  let current = node;
318
314
  while (current) {
@@ -361,22 +357,8 @@ class LexicalToolbarElement extends HTMLElement {
361
357
  this.toggleAttribute("overflowing", isOverflowing);
362
358
  }
363
359
 
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
360
  #compactMenu() {
379
- const buttons = this.#buttonsWithSeparator.reverse();
361
+ const buttons = this.#buttons.reverse();
380
362
  let movedToOverflow = false;
381
363
 
382
364
  for (const button of buttons) {
@@ -390,12 +372,42 @@ class LexicalToolbarElement extends HTMLElement {
390
372
  }
391
373
  }
392
374
 
375
+ #resetToolbar() {
376
+ const items = Array.from(this.#overflowMenu.children);
377
+ items.sort((a, b) => this.#itemPosition(b) - this.#itemPosition(a));
378
+
379
+ items.forEach((item) => {
380
+ const nextItem = this.querySelector(`[data-position="${this.#itemPosition(item) + 1}"]`) ?? this.#overflow;
381
+ this.insertBefore(item, nextItem);
382
+ });
383
+ }
384
+
385
+ #itemPosition(item) {
386
+ return parseInt(item.dataset.position ?? "999")
387
+ }
388
+
389
+ #setItemPositionValues() {
390
+ this.#toolbarItems.forEach((item, index) => {
391
+ if (item.dataset.position === undefined) {
392
+ item.dataset.position = index;
393
+ }
394
+ });
395
+ }
396
+
397
+ get #overflow() {
398
+ return this.querySelector(".lexxy-editor__toolbar-overflow")
399
+ }
400
+
401
+ get #overflowMenu() {
402
+ return this.querySelector(".lexxy-editor__toolbar-overflow-menu")
403
+ }
404
+
393
405
  get #buttons() {
394
406
  return Array.from(this.querySelectorAll(":scope > button"))
395
407
  }
396
408
 
397
- get #buttonsWithSeparator() {
398
- return Array.from(this.querySelectorAll(":scope > button, :scope > [role=separator]"))
409
+ get #toolbarItems() {
410
+ return Array.from(this.querySelectorAll(":scope > *:not(.lexxy-editor__toolbar-overflow)"))
399
411
  }
400
412
 
401
413
  static get defaultTemplate() {
@@ -414,35 +426,31 @@ class LexicalToolbarElement extends HTMLElement {
414
426
  </svg>
415
427
  </button>
416
428
 
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>
429
+ <details class="lexxy-editor__toolbar-dropdown" name="lexxy-dropdown">
430
+ <summary class="lexxy-editor__toolbar-button" name="highlight" title="Color highlight">
431
+ <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>
432
+ </summary>
433
+ <lexxy-highlight-dropdown class="lexxy-editor__toolbar-dropdown-content">
434
+ <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>
435
+ <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>
436
+ <button data-command="removeHighlight" class="lexxy-editor__toolbar-dropdown-reset">Remove all coloring</button>
437
+ </lexxy-highlight-dropdown>
438
+ </details>
430
439
 
431
- <lexxy-link-dialog class="lexxy-dialog lexxy-link-dialog">
432
- <dialog class="link-dialog">
440
+ <details class="lexxy-editor__toolbar-dropdown" name="lexxy-dropdown">
441
+ <summary class="lexxy-editor__toolbar-button" name="link" title="Link" data-hotkey="cmd+k ctrl+k">
442
+ <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>
443
+ </summary>
444
+ <lexxy-link-dropdown class="lexxy-editor__toolbar-dropdown-content">
433
445
  <form method="dialog">
434
- <input type="url" placeholder="Enter a URL…" class="input" required>
435
- <div class="lexxy-dialog-actions">
446
+ <input type="url" placeholder="Enter a URL…" class="input">
447
+ <div class="lexxy-editor__toolbar-dropdown-actions">
436
448
  <button type="submit" class="btn" value="link">Link</button>
437
449
  <button type="button" class="btn" value="unlink">Unlink</button>
438
450
  </div>
439
451
  </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>
452
+ </lexxy-link-dropdown>
453
+ </details>
446
454
 
447
455
  <button class="lexxy-editor__toolbar-button" type="button" name="quote" data-command="insertQuoteBlock" title="Quote">
448
456
  <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>
@@ -879,6 +887,11 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
879
887
  return new ActionTextAttachmentUploadNode({ ...serializedNode })
880
888
  }
881
889
 
890
+ // Should never run since this is a transient node. Defined to remove console warning.
891
+ static importDOM() {
892
+ return null
893
+ }
894
+
882
895
  constructor({ file, uploadUrl, blobUrlTemplate, editor, progress }, key) {
883
896
  super({ contentType: file.type }, key);
884
897
  this.file = file;
@@ -1136,6 +1149,7 @@ class CommandDispatcher {
1136
1149
  this.highlighter = editorElement.highlighter;
1137
1150
 
1138
1151
  this.#registerCommands();
1152
+ this.#registerKeyboardCommands();
1139
1153
  this.#registerDragAndDropHandlers();
1140
1154
  }
1141
1155
 
@@ -1300,15 +1314,8 @@ class CommandDispatcher {
1300
1314
  this.editor.registerCommand(command, handler, priority);
1301
1315
  }
1302
1316
 
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
- });
1317
+ #registerKeyboardCommands() {
1318
+ this.editor.registerCommand(KEY_TAB_COMMAND, this.#handleListIndentation.bind(this), COMMAND_PRIORITY_NORMAL);
1312
1319
  }
1313
1320
 
1314
1321
  #registerDragAndDropHandlers() {
@@ -1357,6 +1364,29 @@ class CommandDispatcher {
1357
1364
 
1358
1365
  this.editor.focus();
1359
1366
  }
1367
+
1368
+ #handleListIndentation(event) {
1369
+ if (this.selection.isInsideList) {
1370
+ event.preventDefault();
1371
+ if (event.shiftKey) {
1372
+ return this.editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined)
1373
+ } else {
1374
+ return this.editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)
1375
+ }
1376
+ }
1377
+ return false
1378
+ }
1379
+
1380
+ // Not using TOGGLE_LINK_COMMAND because it's not handled unless you use React/LinkPlugin
1381
+ #toggleLink(url) {
1382
+ this.editor.update(() => {
1383
+ if (url === null) {
1384
+ $toggleLink(null);
1385
+ } else {
1386
+ $toggleLink(url);
1387
+ }
1388
+ });
1389
+ }
1360
1390
  }
1361
1391
 
1362
1392
  function capitalize(str) {
@@ -3163,7 +3193,7 @@ class Clipboard {
3163
3193
 
3164
3194
  if (!clipboardData) return false
3165
3195
 
3166
- if (this.#isOnlyPlainTextPasted(clipboardData) && !this.#isPastingIntoCodeBlock()) {
3196
+ if (this.#isPlainTextOrURLPasted(clipboardData) && !this.#isPastingIntoCodeBlock()) {
3167
3197
  this.#pastePlainText(clipboardData);
3168
3198
  event.preventDefault();
3169
3199
  return true
@@ -3172,11 +3202,21 @@ class Clipboard {
3172
3202
  this.#handlePastedFiles(clipboardData);
3173
3203
  }
3174
3204
 
3205
+ #isPlainTextOrURLPasted(clipboardData) {
3206
+ return this.#isOnlyPlainTextPasted(clipboardData) || this.#isOnlyURLPasted(clipboardData)
3207
+ }
3208
+
3175
3209
  #isOnlyPlainTextPasted(clipboardData) {
3176
3210
  const types = Array.from(clipboardData.types);
3177
3211
  return types.length === 1 && types[0] === "text/plain"
3178
3212
  }
3179
3213
 
3214
+ #isOnlyURLPasted(clipboardData) {
3215
+ // Safari URLs are copied as a text/plain + text/uri-list object
3216
+ const types = Array.from(clipboardData.types);
3217
+ return types.length === 2 && types.includes("text/uri-list") && types.includes("text/plain")
3218
+ }
3219
+
3180
3220
  #isPastingIntoCodeBlock() {
3181
3221
  let result = false;
3182
3222
 
@@ -3475,6 +3515,10 @@ class LexicalEditorElement extends HTMLElement {
3475
3515
  return this.getAttribute("attachments") !== "false"
3476
3516
  }
3477
3517
 
3518
+ get contentTabIndex() {
3519
+ return parseInt(this.editorContentElement?.getAttribute("tabindex") ?? "0")
3520
+ }
3521
+
3478
3522
  focus() {
3479
3523
  this.editor.focus();
3480
3524
  }
@@ -3788,39 +3832,26 @@ class LexicalEditorElement extends HTMLElement {
3788
3832
 
3789
3833
  #reconnect() {
3790
3834
  this.disconnectedCallback();
3835
+ this.valueBeforeDisconnect = null;
3791
3836
  this.connectedCallback();
3792
3837
  }
3793
3838
  }
3794
3839
 
3795
3840
  customElements.define("lexxy-editor", LexicalEditorElement);
3796
3841
 
3797
- class ToolbarDialog extends HTMLElement {
3842
+ class ToolbarDropdown extends HTMLElement {
3798
3843
  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
- }
3844
+ this.container = this.closest("details");
3805
3845
 
3806
- disconnectedCallback() {
3807
- this.#removeClickOutsideHandler();
3808
- }
3809
-
3810
- updateStateCallback() { }
3811
-
3812
- show(triggerButton) {
3813
- if (this.preventImmediateReopen) { return }
3814
-
3815
- this.triggerButton = triggerButton;
3816
- this.#positionDialog();
3817
- this.dialog.show();
3846
+ this.container.addEventListener("toggle", this.#handleToggle.bind(this));
3847
+ this.container.addEventListener("keydown", this.#handleKeyDown.bind(this));
3818
3848
 
3819
- this.#setupClickOutsideHandler();
3849
+ this.#setTabIndexValues();
3820
3850
  }
3821
3851
 
3822
- close() {
3823
- this.dialog.close();
3852
+ disconnectedCallback() {
3853
+ this.#removeClickOutsideHandler();
3854
+ this.container.removeEventListener("keydown", this.#handleKeyDown.bind(this));
3824
3855
  }
3825
3856
 
3826
3857
  get toolbar() {
@@ -3831,32 +3862,32 @@ class ToolbarDialog extends HTMLElement {
3831
3862
  return this.toolbar.editor
3832
3863
  }
3833
3864
 
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));
3865
+ close() {
3866
+ this.container.removeAttribute("open");
3840
3867
  }
3841
3868
 
3842
- #handleClose() {
3843
- this.#removeClickOutsideHandler();
3844
- this.triggerButton = null;
3845
- this.editor.focus();
3869
+ #handleToggle(event) {
3870
+ if (this.container.open) {
3871
+ this.#handleOpen(event.target);
3872
+ } else {
3873
+ this.#handleClose();
3874
+ }
3846
3875
  }
3847
3876
 
3848
- #handleCancel() {
3849
- this.preventImmediateReopen = true;
3850
- requestAnimationFrame(() => this.preventImmediateReopen = undefined);
3877
+ #handleOpen(trigger) {
3878
+ this.trigger = trigger;
3879
+ this.#interactiveElements[0].focus();
3880
+ this.#setupClickOutsideHandler();
3851
3881
  }
3852
3882
 
3853
- #positionDialog() {
3854
- const left = this.triggerButton.offsetLeft;
3855
- this.dialog.style.insetInlineStart = `${left}px`;
3883
+ #handleClose() {
3884
+ this.trigger = null;
3885
+ this.#removeClickOutsideHandler();
3886
+ this.editor.focus();
3856
3887
  }
3857
3888
 
3858
3889
  #setupClickOutsideHandler() {
3859
- if (this.#browserHandlesClose || this.clickOutsideHandler) return
3890
+ if (this.clickOutsideHandler) return
3860
3891
 
3861
3892
  this.clickOutsideHandler = this.#handleClickOutside.bind(this);
3862
3893
  document.addEventListener("click", this.clickOutsideHandler, true);
@@ -3870,22 +3901,7 @@ class ToolbarDialog extends HTMLElement {
3870
3901
  }
3871
3902
 
3872
3903
  #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"
3904
+ if (this.container.open && !this.container.contains(target)) this.close();
3889
3905
  }
3890
3906
 
3891
3907
  #handleKeyDown(event) {
@@ -3894,9 +3910,20 @@ class ToolbarDialog extends HTMLElement {
3894
3910
  this.close();
3895
3911
  }
3896
3912
  }
3913
+
3914
+ async #setTabIndexValues() {
3915
+ await nextFrame();
3916
+ this.#interactiveElements.forEach((element) => {
3917
+ element.setAttribute("tabindex", 0);
3918
+ });
3919
+ }
3920
+
3921
+ get #interactiveElements() {
3922
+ return Array.from(this.querySelectorAll("button, input"))
3923
+ }
3897
3924
  }
3898
3925
 
3899
- class LinkDialog extends ToolbarDialog {
3926
+ class LinkDropdown extends ToolbarDropdown {
3900
3927
  connectedCallback() {
3901
3928
  super.connectedCallback();
3902
3929
  this.input = this.querySelector("input");
@@ -3904,23 +3931,21 @@ class LinkDialog extends ToolbarDialog {
3904
3931
  this.#registerHandlers();
3905
3932
  }
3906
3933
 
3907
- updateStateCallback() {
3908
- this.input.value = this.#selectedLinkUrl;
3909
- }
3910
-
3911
3934
  #registerHandlers() {
3912
- this.dialog.addEventListener("beforetoggle", this.#handleBeforeToggle.bind(this));
3913
- this.dialog.addEventListener("submit", this.#handleSubmit.bind(this));
3935
+ this.container.addEventListener("toggle", this.#handleToggle.bind(this));
3936
+ this.addEventListener("submit", this.#handleSubmit.bind(this));
3914
3937
  this.querySelector("[value='unlink']").addEventListener("click", this.#handleUnlink.bind(this));
3915
3938
  }
3916
3939
 
3917
- #handleBeforeToggle({ newState }) {
3940
+ #handleToggle({ newState }) {
3941
+ this.input.value = this.#selectedLinkUrl;
3918
3942
  this.input.required = newState === "open";
3919
3943
  }
3920
3944
 
3921
3945
  #handleSubmit(event) {
3922
3946
  const command = event.submitter?.value;
3923
3947
  this.editor.dispatchCommand(command, this.input.value);
3948
+ this.close();
3924
3949
  }
3925
3950
 
3926
3951
  #handleUnlink() {
@@ -3949,9 +3974,7 @@ class LinkDialog extends ToolbarDialog {
3949
3974
  }
3950
3975
  }
3951
3976
 
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);
3977
+ customElements.define("lexxy-link-dropdown", LinkDropdown);
3955
3978
 
3956
3979
  const APPLY_HIGHLIGHT_SELECTOR = "button.lexxy-highlight-button";
3957
3980
  const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
@@ -3961,7 +3984,7 @@ const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
3961
3984
  // see https://github.com/facebook/lexical/issues/8013
3962
3985
  const NO_STYLE = Symbol("no_style");
3963
3986
 
3964
- class HighlightDialog extends ToolbarDialog {
3987
+ class HighlightDropdown extends ToolbarDropdown {
3965
3988
  connectedCallback() {
3966
3989
  super.connectedCallback();
3967
3990
 
@@ -3969,13 +3992,10 @@ class HighlightDialog extends ToolbarDialog {
3969
3992
  this.#registerHandlers();
3970
3993
  }
3971
3994
 
3972
- updateStateCallback() {
3973
- this.#updateColorButtonStates($getSelection());
3974
- }
3975
-
3976
3995
  #registerHandlers() {
3977
- this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).addEventListener("click", this.#handleRemoveHighlightClick.bind(this));
3996
+ this.container.addEventListener("toggle", this.#handleToggle.bind(this));
3978
3997
  this.#colorButtons.forEach(button => button.addEventListener("click", this.#handleColorButtonClick.bind(this)));
3998
+ this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).addEventListener("click", this.#handleRemoveHighlightClick.bind(this));
3979
3999
  }
3980
4000
 
3981
4001
  #setUpButtons() {
@@ -4002,6 +4022,14 @@ class HighlightDialog extends ToolbarDialog {
4002
4022
  return button
4003
4023
  }
4004
4024
 
4025
+ #handleToggle({ newState }) {
4026
+ if (newState === "open") {
4027
+ this.editor.getEditorState().read(() => {
4028
+ this.#updateColorButtonStates($getSelection());
4029
+ });
4030
+ }
4031
+ }
4032
+
4005
4033
  #handleColorButtonClick(event) {
4006
4034
  event.preventDefault();
4007
4035
 
@@ -4047,9 +4075,7 @@ class HighlightDialog extends ToolbarDialog {
4047
4075
  }
4048
4076
  }
4049
4077
 
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);
4078
+ customElements.define("lexxy-highlight-dropdown", HighlightDropdown);
4053
4079
 
4054
4080
  class BaseSource {
4055
4081
  // Template method to override
@@ -4194,10 +4220,13 @@ class LexicalPromptElement extends HTMLElement {
4194
4220
  this.keyListeners = [];
4195
4221
  }
4196
4222
 
4223
+ static observedAttributes = [ "connected" ]
4224
+
4197
4225
  connectedCallback() {
4198
4226
  this.source = this.#createSource();
4199
4227
 
4200
4228
  this.#addTriggerListener();
4229
+ this.toggleAttribute("connected", true);
4201
4230
  }
4202
4231
 
4203
4232
  disconnectedCallback() {
@@ -4205,6 +4234,13 @@ class LexicalPromptElement extends HTMLElement {
4205
4234
  this.popoverElement = null;
4206
4235
  }
4207
4236
 
4237
+
4238
+ attributeChangedCallback(name, oldValue, newValue) {
4239
+ if (name === "connected" && this.isConnected && oldValue != null && oldValue !== newValue) {
4240
+ requestAnimationFrame(() => this.#reconnect());
4241
+ }
4242
+ }
4243
+
4208
4244
  get name() {
4209
4245
  return this.getAttribute("name")
4210
4246
  }
@@ -4574,6 +4610,11 @@ class LexicalPromptElement extends HTMLElement {
4574
4610
  this.#optionWasSelected();
4575
4611
  }
4576
4612
  }
4613
+
4614
+ #reconnect() {
4615
+ this.disconnectedCallback();
4616
+ this.connectedCallback();
4617
+ }
4577
4618
  }
4578
4619
 
4579
4620
  customElements.define("lexxy-prompt", LexicalPromptElement);
@@ -4721,24 +4762,17 @@ function highlightAll() {
4721
4762
 
4722
4763
  function highlightElement(preElement) {
4723
4764
  const language = preElement.getAttribute("data-language");
4724
-
4725
4765
  let code = preElement.innerHTML.replace(/<br\s*\/?>/gi, "\n");
4726
4766
 
4727
- const grammar = Prism.languages[language];
4767
+ const grammar = Prism.languages?.[language];
4728
4768
  if (!grammar) return
4729
4769
 
4730
4770
  // unescape HTML entities in the code block
4731
4771
  code = new DOMParser().parseFromString(code, "text/html").body.textContent || "";
4732
4772
 
4733
4773
  const highlightedHtml = Prism.highlight(code, grammar, language);
4734
-
4735
4774
  const codeElement = createElement("code", { "data-language": language, innerHTML: highlightedHtml });
4736
4775
  preElement.replaceWith(codeElement);
4737
4776
  }
4738
4777
 
4739
- // Manual highlighting mode to prevent invocation on every page. See https://prismjs.com/docs/prism
4740
- // This must happen before importing any Prism components
4741
- window.Prism = window.Prism || {};
4742
- Prism.manual = true;
4743
-
4744
4778
  export { highlightAll };