@37signals/lexxy 0.1.21-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/dist/lexxy.esm.js CHANGED
@@ -1,16 +1,21 @@
1
1
  import DOMPurify from 'dompurify';
2
- import { getStyleObjectFromCSS, getCSSFromStyleObject, $forEachSelectedTextNode, $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
3
- import { $isTextNode, TextNode, $getSelection, $isRangeSelection, 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';
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, 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';
7
7
  import { $isLinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, LinkNode, AutoLinkNode } from '@lexical/link';
8
+ import 'prismjs/components/prism-ruby';
9
+ import 'prismjs/components/prism-php';
10
+ import 'prismjs/components/prism-go';
11
+ import 'prismjs/components/prism-bash';
12
+ import 'prismjs/components/prism-json';
13
+ import 'prismjs/components/prism-diff';
8
14
  import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
9
15
  import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
10
16
  import { createEmptyHistoryState, registerHistory } from '@lexical/history';
11
17
  import { DirectUpload } from '@rails/activestorage';
12
18
  import { marked } from 'marked';
13
- import 'prismjs/components/prism-ruby';
14
19
 
15
20
  const ALLOWED_HTML_TAGS = [ "a", "action-text-attachment", "b", "blockquote", "br", "code", "em",
16
21
  "figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "mark", "ol", "p", "pre", "q", "s", "strong", "ul" ];
@@ -115,7 +120,24 @@ function extendConversion(nodeKlass, conversionName, callback = (output => outpu
115
120
  }
116
121
  }
117
122
 
123
+ function isSelectionHighlighted(selection) {
124
+ if (!$isRangeSelection(selection)) return false
125
+
126
+ if (selection.isCollapsed()) {
127
+ return hasHighlightStyles(selection.style)
128
+ } else {
129
+ return selection.hasFormat("highlight")
130
+ }
131
+ }
132
+
133
+ function hasHighlightStyles(cssOrStyles) {
134
+ const styles = typeof cssOrStyles === "string" ? getStyleObjectFromCSS(cssOrStyles) : cssOrStyles;
135
+ return !!(styles.color || styles["background-color"])
136
+ }
137
+
118
138
  class LexicalToolbarElement extends HTMLElement {
139
+ static observedAttributes = [ "connected" ]
140
+
119
141
  constructor() {
120
142
  super();
121
143
  this.internals = this.attachInternals();
@@ -134,6 +156,13 @@ class LexicalToolbarElement extends HTMLElement {
134
156
  this._resizeObserver.disconnect();
135
157
  this._resizeObserver = null;
136
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
+ }
137
166
  }
138
167
 
139
168
  setEditor(editorElement) {
@@ -141,7 +170,8 @@ class LexicalToolbarElement extends HTMLElement {
141
170
  this.editor = editorElement.editor;
142
171
  this.#bindButtons();
143
172
  this.#bindHotkeys();
144
- this.#assignButtonTabindex();
173
+ this.#setTabIndexValues();
174
+ this.#setItemPositionValues();
145
175
  this.#monitorSelectionChanges();
146
176
  this.#monitorHistoryChanges();
147
177
  this.#refreshToolbarOverflow();
@@ -149,10 +179,9 @@ class LexicalToolbarElement extends HTMLElement {
149
179
  this.toggleAttribute("connected", true);
150
180
  }
151
181
 
152
- get #dialogs() {
153
- const dialogButtons = this.querySelectorAll("[data-dialog-target]");
154
- const dialogTags = Array.from(dialogButtons).map(button => `lexxy-${button.dataset.dialogTarget}`);
155
- return Array.from(this.querySelectorAll(dialogTags))
182
+ #reconnect() {
183
+ this.disconnectedCallback();
184
+ this.connectedCallback();
156
185
  }
157
186
 
158
187
  #bindButtons() {
@@ -161,7 +190,6 @@ class LexicalToolbarElement extends HTMLElement {
161
190
 
162
191
  #handleButtonClicked({ target }) {
163
192
  this.#handleTargetClicked(target, "[data-command]", this.#dispatchButtonCommand.bind(this));
164
- this.#handleTargetClicked(target, "[data-dialog-target]", this.#toggleDialog.bind(this));
165
193
  }
166
194
 
167
195
  #handleTargetClicked(target, selector, callback) {
@@ -176,38 +204,23 @@ class LexicalToolbarElement extends HTMLElement {
176
204
  this.editor.dispatchCommand(command, payload);
177
205
  }
178
206
 
179
- // Not using popover because of CSS anchoring still not widely available.
180
- #toggleDialog(button) {
181
- const dialogTarget = button.dataset.dialogTarget;
182
- const dialog = this.querySelector("lexxy-" + dialogTarget);
183
- if (!dialog) return
184
-
185
- if (dialog.open) {
186
- dialog.close();
187
- } else {
188
- this.#closeOpenDialogs();
189
- dialog.show(button);
190
- }
207
+ #bindHotkeys() {
208
+ this.editorElement.addEventListener("keydown", this.#handleHotkey);
191
209
  }
192
210
 
193
- #closeOpenDialogs() {
194
- const openDialogs = this.querySelectorAll("dialog[open]");
195
- openDialogs.forEach(openDialog => {
196
- openDialog.closest(".lexxy-dialog").close();
197
- });
211
+ #unbindHotkeys() {
212
+ this.editorElement?.removeEventListener("keydown", this.#handleHotkey);
198
213
  }
199
214
 
200
- #bindHotkeys() {
201
- this.editorElement.addEventListener("keydown", (event) => {
202
- const buttons = this.querySelectorAll("[data-hotkey]");
203
- buttons.forEach((button) => {
204
- const hotkeys = button.dataset.hotkey.toLowerCase().split(/\s+/);
205
- if (hotkeys.includes(this.#keyCombinationFor(event))) {
206
- event.preventDefault();
207
- event.stopPropagation();
208
- button.click();
209
- }
210
- });
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
+ }
211
224
  });
212
225
  }
213
226
 
@@ -223,10 +236,9 @@ class LexicalToolbarElement extends HTMLElement {
223
236
  return [ ...modifiers, pressedKey ].join("+")
224
237
  }
225
238
 
226
- #assignButtonTabindex() {
227
- const baseTabIndex = parseInt(this.editorElement.editorContentElement.getAttribute("tabindex") ?? "0");
228
- this.#buttons.forEach((button, index) => {
229
- button.setAttribute("tabindex", `${baseTabIndex + index + 1}`);
239
+ #setTabIndexValues() {
240
+ this.#buttons.forEach((button) => {
241
+ button.setAttribute("tabindex", 0);
230
242
  });
231
243
  }
232
244
 
@@ -234,7 +246,6 @@ class LexicalToolbarElement extends HTMLElement {
234
246
  this.editor.registerUpdateListener(() => {
235
247
  this.editor.getEditorState().read(() => {
236
248
  this.#updateButtonStates();
237
- this.#updateDialogStates();
238
249
  });
239
250
  });
240
251
  }
@@ -267,7 +278,7 @@ class LexicalToolbarElement extends HTMLElement {
267
278
  const isBold = selection.hasFormat("bold");
268
279
  const isItalic = selection.hasFormat("italic");
269
280
  const isStrikethrough = selection.hasFormat("strikethrough");
270
- const isHighlight = selection.hasFormat("highlight");
281
+ const isHighlight = isSelectionHighlighted(selection);
271
282
  const isInLink = this.#isInLink(anchorNode);
272
283
  const isInQuote = $isQuoteNode(topLevelElement);
273
284
  const isInHeading = $isHeadingNode(topLevelElement);
@@ -289,10 +300,6 @@ class LexicalToolbarElement extends HTMLElement {
289
300
  this.#updateUndoRedoButtonStates();
290
301
  }
291
302
 
292
- #updateDialogStates() {
293
- this.#dialogs.forEach(dialog => dialog.updateStateCallback());
294
- }
295
-
296
303
  #isInList(node) {
297
304
  let current = node;
298
305
  while (current) {
@@ -341,22 +348,8 @@ class LexicalToolbarElement extends HTMLElement {
341
348
  this.toggleAttribute("overflowing", isOverflowing);
342
349
  }
343
350
 
344
- get #overflow() {
345
- return this.querySelector(".lexxy-editor__toolbar-overflow")
346
- }
347
-
348
- get #overflowMenu() {
349
- return this.querySelector(".lexxy-editor__toolbar-overflow-menu")
350
- }
351
-
352
- #resetToolbar() {
353
- while (this.#overflowMenu.children.length > 0) {
354
- this.insertBefore(this.#overflowMenu.children[0], this.#overflow);
355
- }
356
- }
357
-
358
351
  #compactMenu() {
359
- const buttons = this.#buttonsWithSeparator.reverse();
352
+ const buttons = this.#buttons.reverse();
360
353
  let movedToOverflow = false;
361
354
 
362
355
  for (const button of buttons) {
@@ -370,12 +363,42 @@ class LexicalToolbarElement extends HTMLElement {
370
363
  }
371
364
  }
372
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
+
373
396
  get #buttons() {
374
397
  return Array.from(this.querySelectorAll(":scope > button"))
375
398
  }
376
399
 
377
- get #buttonsWithSeparator() {
378
- 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)"))
379
402
  }
380
403
 
381
404
  static get defaultTemplate() {
@@ -394,35 +417,31 @@ class LexicalToolbarElement extends HTMLElement {
394
417
  </svg>
395
418
  </button>
396
419
 
397
- <lexxy-highlight-dialog class="lexxy-dialog lexxy-highlight-dialog">
398
- <dialog class="highlight-dialog">
399
- <div class="lexxy-highlight-dialog-content">
400
- <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>
401
- <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>
402
- <button data-command="removeHighlight" class="lexxy-highlight-dialog-reset">Remove all coloring</button>
403
- </div>
404
- </dialog>
405
- </lexxy-highlight-dialog>
406
-
407
- <button class="lexxy-editor__toolbar-button" type="button" name="highlight" title="Color highlight" data-dialog-target="highlight-dialog">
408
- <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>
409
- </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>
410
430
 
411
- <lexxy-link-dialog class="lexxy-dialog lexxy-link-dialog">
412
- <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">
413
436
  <form method="dialog">
414
- <input type="url" placeholder="Enter a URL…" class="input" required>
415
- <div class="lexxy-dialog-actions">
437
+ <input type="url" placeholder="Enter a URL…" class="input">
438
+ <div class="lexxy-editor__toolbar-dropdown-actions">
416
439
  <button type="submit" class="btn" value="link">Link</button>
417
440
  <button type="button" class="btn" value="unlink">Unlink</button>
418
441
  </div>
419
442
  </form>
420
- </dialog>
421
- </lexxy-link-dialog>
422
-
423
- <button class="lexxy-editor__toolbar-button" type="button" name="link" title="Link" data-dialog-target="link-dialog" data-hotkey="cmd+k ctrl+k">
424
- <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>
425
- </button>
443
+ </lexxy-link-dropdown>
444
+ </details>
426
445
 
427
446
  <button class="lexxy-editor__toolbar-button" type="button" name="quote" data-command="insertQuoteBlock" title="Quote">
428
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>
@@ -486,6 +505,7 @@ var theme = {
486
505
  }
487
506
  },
488
507
  codeHighlight: {
508
+ addition: "code-token__selector",
489
509
  atrule: "code-token__attr",
490
510
  attr: "code-token__attr",
491
511
  "attr-name": "code-token__attr",
@@ -500,24 +520,30 @@ var theme = {
500
520
  color: "code-token__property",
501
521
  comment: "code-token__comment",
502
522
  constant: "code-token__property",
503
- coord: "code-token__property",
523
+ coord: "code-token__comment",
504
524
  decorator: "code-token__function",
505
- deleted: "code-token__property",
525
+ deleted: "code-token__operator",
526
+ deletion: "code-token__operator",
527
+ directive: "code-token__attr",
528
+ "directive-hash": "code-token__property",
506
529
  doctype: "code-token__comment",
507
530
  entity: "code-token__operator",
508
531
  function: "code-token__function",
509
532
  hexcode: "code-token__property",
510
- important: "code-token__variable",
533
+ important: "code-token__function",
511
534
  inserted: "code-token__selector",
512
535
  italic: "code-token__comment",
513
536
  keyword: "code-token__attr",
537
+ line: "code-token__selector",
514
538
  namespace: "code-token__variable",
515
539
  number: "code-token__property",
540
+ macro: "code-token__function",
516
541
  operator: "code-token__operator",
517
542
  parameter: "code-token__variable",
518
543
  prolog: "code-token__comment",
519
544
  property: "code-token__property",
520
545
  punctuation: "code-token__punctuation",
546
+ "raw-string": "code-token__operator",
521
547
  regex: "code-token__variable",
522
548
  script: "code-token__function",
523
549
  selector: "code-token__selector",
@@ -526,6 +552,7 @@ var theme = {
526
552
  symbol: "code-token__property",
527
553
  tag: "code-token__property",
528
554
  title: "code-token__function",
555
+ "type-definition": "code-token__function",
529
556
  url: "code-token__operator",
530
557
  variable: "code-token__variable",
531
558
  }
@@ -1108,6 +1135,7 @@ class CommandDispatcher {
1108
1135
  this.highlighter = editorElement.highlighter;
1109
1136
 
1110
1137
  this.#registerCommands();
1138
+ this.#registerKeyboardCommands();
1111
1139
  this.#registerDragAndDropHandlers();
1112
1140
  }
1113
1141
 
@@ -1272,15 +1300,8 @@ class CommandDispatcher {
1272
1300
  this.editor.registerCommand(command, handler, priority);
1273
1301
  }
1274
1302
 
1275
- // Not using TOGGLE_LINK_COMMAND because it's not handled unless you use React/LinkPlugin
1276
- #toggleLink(url) {
1277
- this.editor.update(() => {
1278
- if (url === null) {
1279
- $toggleLink(null);
1280
- } else {
1281
- $toggleLink(url);
1282
- }
1283
- });
1303
+ #registerKeyboardCommands() {
1304
+ this.editor.registerCommand(KEY_TAB_COMMAND, this.#handleListIndentation.bind(this), COMMAND_PRIORITY_NORMAL);
1284
1305
  }
1285
1306
 
1286
1307
  #registerDragAndDropHandlers() {
@@ -1329,6 +1350,29 @@ class CommandDispatcher {
1329
1350
 
1330
1351
  this.editor.focus();
1331
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
+ }
1332
1376
  }
1333
1377
 
1334
1378
  function capitalize(str) {
@@ -3234,27 +3278,37 @@ class Clipboard {
3234
3278
  class Highlighter {
3235
3279
  constructor(editorElement) {
3236
3280
  this.editor = editorElement.editor;
3281
+
3282
+ this.#registerHighlightTransform();
3237
3283
  }
3238
3284
 
3239
3285
  toggle(styles) {
3240
3286
  this.editor.update(() => {
3241
3287
  this.#toggleSelectionStyles(styles);
3242
- $forEachSelectedTextNode(node => this.#syncHighlightWithStyle(node));
3243
3288
  });
3244
3289
  }
3245
3290
 
3246
3291
  remove() {
3247
- this.toggle({ "color": undefined, "background-color": undefined });
3292
+ this.toggle({ "color": null, "background-color": null });
3293
+ }
3294
+
3295
+ #registerHighlightTransform() {
3296
+ return this.editor.registerNodeTransform(TextNode, (textNode) => {
3297
+ this.#syncHighlightWithStyle(textNode);
3298
+ })
3248
3299
  }
3249
3300
 
3250
3301
  #toggleSelectionStyles(styles) {
3251
3302
  const selection = $getSelection();
3303
+ if (!$isRangeSelection(selection)) return
3252
3304
 
3305
+ const patch = {};
3253
3306
  for (const property in styles) {
3254
3307
  const oldValue = $getSelectionStyleValueForProperty(selection, property);
3255
- const patch = { [property]: this.#toggleOrReplace(oldValue, styles[property]) };
3256
- $patchStyleText(selection, patch);
3308
+ patch[property] = this.#toggleOrReplace(oldValue, styles[property]);
3257
3309
  }
3310
+
3311
+ $patchStyleText(selection, patch);
3258
3312
  }
3259
3313
 
3260
3314
  #toggleOrReplace(oldValue, newValue) {
@@ -3262,15 +3316,10 @@ class Highlighter {
3262
3316
  }
3263
3317
 
3264
3318
  #syncHighlightWithStyle(node) {
3265
- if (this.#hasHighlightStyles(node) !== node.hasFormat("highlight")) {
3319
+ if (hasHighlightStyles(node.getStyle()) !== node.hasFormat("highlight")) {
3266
3320
  node.toggleFormat("highlight");
3267
3321
  }
3268
3322
  }
3269
-
3270
- #hasHighlightStyles(node) {
3271
- const styles = getStyleObjectFromCSS(node.getStyle());
3272
- return !!(styles.color || styles["background-color"])
3273
- }
3274
3323
  }
3275
3324
 
3276
3325
  class HighlightNode extends TextNode {
@@ -3442,6 +3491,10 @@ class LexicalEditorElement extends HTMLElement {
3442
3491
  return this.getAttribute("attachments") !== "false"
3443
3492
  }
3444
3493
 
3494
+ get contentTabIndex() {
3495
+ return parseInt(this.editorContentElement?.getAttribute("tabindex") ?? "0")
3496
+ }
3497
+
3445
3498
  focus() {
3446
3499
  this.editor.focus();
3447
3500
  }
@@ -3755,39 +3808,26 @@ class LexicalEditorElement extends HTMLElement {
3755
3808
 
3756
3809
  #reconnect() {
3757
3810
  this.disconnectedCallback();
3811
+ this.valueBeforeDisconnect = null;
3758
3812
  this.connectedCallback();
3759
3813
  }
3760
3814
  }
3761
3815
 
3762
3816
  customElements.define("lexxy-editor", LexicalEditorElement);
3763
3817
 
3764
- class ToolbarDialog extends HTMLElement {
3818
+ class ToolbarDropdown extends HTMLElement {
3765
3819
  connectedCallback() {
3766
- this.dialog = this.querySelector("dialog");
3767
- if ("closedBy" in this.dialog.constructor.prototype) {
3768
- this.dialog.closedBy = "any";
3769
- }
3770
- this.#registerHandlers();
3771
- }
3820
+ this.container = this.closest("details");
3772
3821
 
3773
- disconnectedCallback() {
3774
- this.#removeClickOutsideHandler();
3775
- }
3776
-
3777
- updateStateCallback() { }
3822
+ this.container.addEventListener("toggle", this.#handleToggle.bind(this));
3823
+ this.container.addEventListener("keydown", this.#handleKeyDown.bind(this));
3778
3824
 
3779
- show(triggerButton) {
3780
- if (this.preventImmediateReopen) { return }
3781
-
3782
- this.triggerButton = triggerButton;
3783
- this.#positionDialog();
3784
- this.dialog.show();
3785
-
3786
- this.#setupClickOutsideHandler();
3825
+ this.#setTabIndexValues();
3787
3826
  }
3788
3827
 
3789
- close() {
3790
- this.dialog.close();
3828
+ disconnectedCallback() {
3829
+ this.#removeClickOutsideHandler();
3830
+ this.container.removeEventListener("keydown", this.#handleKeyDown.bind(this));
3791
3831
  }
3792
3832
 
3793
3833
  get toolbar() {
@@ -3798,32 +3838,32 @@ class ToolbarDialog extends HTMLElement {
3798
3838
  return this.toolbar.editor
3799
3839
  }
3800
3840
 
3801
- get open() { return this.dialog.open }
3802
-
3803
- #registerHandlers() {
3804
- this.#setupKeydownHandler();
3805
- this.dialog.addEventListener("cancel", this.#handleCancel.bind(this));
3806
- this.dialog.addEventListener("close", this.#handleClose.bind(this));
3841
+ close() {
3842
+ this.container.removeAttribute("open");
3807
3843
  }
3808
3844
 
3809
- #handleClose() {
3810
- this.#removeClickOutsideHandler();
3811
- this.triggerButton = null;
3812
- this.editor.focus();
3845
+ #handleToggle(event) {
3846
+ if (this.container.open) {
3847
+ this.#handleOpen(event.target);
3848
+ } else {
3849
+ this.#handleClose();
3850
+ }
3813
3851
  }
3814
3852
 
3815
- #handleCancel() {
3816
- this.preventImmediateReopen = true;
3817
- requestAnimationFrame(() => this.preventImmediateReopen = undefined);
3853
+ #handleOpen(trigger) {
3854
+ this.trigger = trigger;
3855
+ this.#interactiveElements[0].focus();
3856
+ this.#setupClickOutsideHandler();
3818
3857
  }
3819
3858
 
3820
- #positionDialog() {
3821
- const left = this.triggerButton.offsetLeft;
3822
- this.dialog.style.insetInlineStart = `${left}px`;
3859
+ #handleClose() {
3860
+ this.trigger = null;
3861
+ this.#removeClickOutsideHandler();
3862
+ this.editor.focus();
3823
3863
  }
3824
3864
 
3825
3865
  #setupClickOutsideHandler() {
3826
- if (this.#browserHandlesClose || this.clickOutsideHandler) return
3866
+ if (this.clickOutsideHandler) return
3827
3867
 
3828
3868
  this.clickOutsideHandler = this.#handleClickOutside.bind(this);
3829
3869
  document.addEventListener("click", this.clickOutsideHandler, true);
@@ -3837,22 +3877,7 @@ class ToolbarDialog extends HTMLElement {
3837
3877
  }
3838
3878
 
3839
3879
  #handleClickOutside({ target }) {
3840
- if (!this.dialog.open) return
3841
-
3842
- const isClickInsideDialog = this.dialog.contains(target);
3843
- const isClickOnTrigger = this.triggerButton.contains(target);
3844
-
3845
- if (!isClickInsideDialog && !isClickOnTrigger) {
3846
- this.close();
3847
- }
3848
- }
3849
-
3850
- #setupKeydownHandler() {
3851
- if (!this.#browserHandlesClose) { this.addEventListener("keydown", this.#handleKeyDown.bind(this)); }
3852
- }
3853
-
3854
- get #browserHandlesClose() {
3855
- return this.dialog.closedBy === "any"
3880
+ if (this.container.open && !this.container.contains(target)) this.close();
3856
3881
  }
3857
3882
 
3858
3883
  #handleKeyDown(event) {
@@ -3861,9 +3886,20 @@ class ToolbarDialog extends HTMLElement {
3861
3886
  this.close();
3862
3887
  }
3863
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
+ }
3864
3900
  }
3865
3901
 
3866
- class LinkDialog extends ToolbarDialog {
3902
+ class LinkDropdown extends ToolbarDropdown {
3867
3903
  connectedCallback() {
3868
3904
  super.connectedCallback();
3869
3905
  this.input = this.querySelector("input");
@@ -3871,23 +3907,21 @@ class LinkDialog extends ToolbarDialog {
3871
3907
  this.#registerHandlers();
3872
3908
  }
3873
3909
 
3874
- updateStateCallback() {
3875
- this.input.value = this.#selectedLinkUrl;
3876
- }
3877
-
3878
3910
  #registerHandlers() {
3879
- this.dialog.addEventListener("beforetoggle", this.#handleBeforeToggle.bind(this));
3880
- 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));
3881
3913
  this.querySelector("[value='unlink']").addEventListener("click", this.#handleUnlink.bind(this));
3882
3914
  }
3883
3915
 
3884
- #handleBeforeToggle({ newState }) {
3916
+ #handleToggle({ newState }) {
3917
+ this.input.value = this.#selectedLinkUrl;
3885
3918
  this.input.required = newState === "open";
3886
3919
  }
3887
3920
 
3888
3921
  #handleSubmit(event) {
3889
3922
  const command = event.submitter?.value;
3890
3923
  this.editor.dispatchCommand(command, this.input.value);
3924
+ this.close();
3891
3925
  }
3892
3926
 
3893
3927
  #handleUnlink() {
@@ -3916,14 +3950,17 @@ class LinkDialog extends ToolbarDialog {
3916
3950
  }
3917
3951
  }
3918
3952
 
3919
- // We should extend the native dialog and avoid the intermediary <dialog> but not
3920
- // supported by Safari yet: customElements.define("lexxy-link-dialog", LinkDialog, { extends: "dialog" })
3921
- customElements.define("lexxy-link-dialog", LinkDialog);
3953
+ customElements.define("lexxy-link-dropdown", LinkDropdown);
3922
3954
 
3923
3955
  const APPLY_HIGHLIGHT_SELECTOR = "button.lexxy-highlight-button";
3924
3956
  const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
3925
3957
 
3926
- class HighlightDialog extends ToolbarDialog {
3958
+ // Use Symbol instead of null since $getSelectionStyleValueForProperty
3959
+ // responds differently for backward selections if null is the default
3960
+ // see https://github.com/facebook/lexical/issues/8013
3961
+ const NO_STYLE = Symbol("no_style");
3962
+
3963
+ class HighlightDropdown extends ToolbarDropdown {
3927
3964
  connectedCallback() {
3928
3965
  super.connectedCallback();
3929
3966
 
@@ -3931,13 +3968,10 @@ class HighlightDialog extends ToolbarDialog {
3931
3968
  this.#registerHandlers();
3932
3969
  }
3933
3970
 
3934
- updateStateCallback() {
3935
- this.#updateColorButtonStates($getSelection());
3936
- }
3937
-
3938
3971
  #registerHandlers() {
3939
- this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).addEventListener("click", this.#handleRemoveHighlightClick.bind(this));
3972
+ this.container.addEventListener("toggle", this.#handleToggle.bind(this));
3940
3973
  this.#colorButtons.forEach(button => button.addEventListener("click", this.#handleColorButtonClick.bind(this)));
3974
+ this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).addEventListener("click", this.#handleRemoveHighlightClick.bind(this));
3941
3975
  }
3942
3976
 
3943
3977
  #setUpButtons() {
@@ -3964,6 +3998,14 @@ class HighlightDialog extends ToolbarDialog {
3964
3998
  return button
3965
3999
  }
3966
4000
 
4001
+ #handleToggle({ newState }) {
4002
+ if (newState === "open") {
4003
+ this.editor.getEditorState().read(() => {
4004
+ this.#updateColorButtonStates($getSelection());
4005
+ });
4006
+ }
4007
+ }
4008
+
3967
4009
  #handleColorButtonClick(event) {
3968
4010
  event.preventDefault();
3969
4011
 
@@ -3987,16 +4029,16 @@ class HighlightDialog extends ToolbarDialog {
3987
4029
  #updateColorButtonStates(selection) {
3988
4030
  if (!$isRangeSelection(selection)) { return }
3989
4031
 
3990
- // Use null default, so "" indicates mixed highlighting
3991
- const textColor = $getSelectionStyleValueForProperty(selection, "color", null);
3992
- const backgroundColor = $getSelectionStyleValueForProperty(selection, "background-color", null);
4032
+ // Use non-"" default, so "" indicates mixed highlighting
4033
+ const textColor = $getSelectionStyleValueForProperty(selection, "color", NO_STYLE);
4034
+ const backgroundColor = $getSelectionStyleValueForProperty(selection, "background-color", NO_STYLE);
3993
4035
 
3994
4036
  this.#colorButtons.forEach(button => {
3995
4037
  const matchesSelection = button.dataset.value === textColor || button.dataset.value === backgroundColor;
3996
4038
  button.setAttribute("aria-pressed", matchesSelection);
3997
4039
  });
3998
4040
 
3999
- const hasHighlight = textColor !== null || backgroundColor !== null;
4041
+ const hasHighlight = textColor !== NO_STYLE || backgroundColor !== NO_STYLE;
4000
4042
  this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).disabled = !hasHighlight;
4001
4043
  }
4002
4044
 
@@ -4009,9 +4051,7 @@ class HighlightDialog extends ToolbarDialog {
4009
4051
  }
4010
4052
  }
4011
4053
 
4012
- // We should extend the native dialog and avoid the intermediary <dialog> but not
4013
- // supported by Safari yet: customElements.define("lexxy-hightlight-dialog", HighlightDialog, { extends: "dialog" })
4014
- customElements.define("lexxy-highlight-dialog", HighlightDialog);
4054
+ customElements.define("lexxy-highlight-dropdown", HighlightDropdown);
4015
4055
 
4016
4056
  class BaseSource {
4017
4057
  // Template method to override
@@ -4156,10 +4196,13 @@ class LexicalPromptElement extends HTMLElement {
4156
4196
  this.keyListeners = [];
4157
4197
  }
4158
4198
 
4199
+ static observedAttributes = [ "connected" ]
4200
+
4159
4201
  connectedCallback() {
4160
4202
  this.source = this.#createSource();
4161
4203
 
4162
4204
  this.#addTriggerListener();
4205
+ this.toggleAttribute("connected", true);
4163
4206
  }
4164
4207
 
4165
4208
  disconnectedCallback() {
@@ -4167,6 +4210,13 @@ class LexicalPromptElement extends HTMLElement {
4167
4210
  this.popoverElement = null;
4168
4211
  }
4169
4212
 
4213
+
4214
+ attributeChangedCallback(name, oldValue, newValue) {
4215
+ if (name === "connected" && this.isConnected && oldValue != null && oldValue !== newValue) {
4216
+ requestAnimationFrame(() => this.#reconnect());
4217
+ }
4218
+ }
4219
+
4170
4220
  get name() {
4171
4221
  return this.getAttribute("name")
4172
4222
  }
@@ -4536,6 +4586,11 @@ class LexicalPromptElement extends HTMLElement {
4536
4586
  this.#optionWasSelected();
4537
4587
  }
4538
4588
  }
4589
+
4590
+ #reconnect() {
4591
+ this.disconnectedCallback();
4592
+ this.connectedCallback();
4593
+ }
4539
4594
  }
4540
4595
 
4541
4596
  customElements.define("lexxy-prompt", LexicalPromptElement);
@@ -4578,6 +4633,11 @@ class CodeLanguagePicker extends HTMLElement {
4578
4633
  const languages = { ...CODE_LANGUAGE_FRIENDLY_NAME_MAP };
4579
4634
 
4580
4635
  if (!languages.ruby) languages.ruby = "Ruby";
4636
+ if (!languages.php) languages.php = "PHP";
4637
+ if (!languages.go) languages.go = "Go";
4638
+ if (!languages.bash) languages.bash = "Bash";
4639
+ if (!languages.json) languages.json = "JSON";
4640
+ if (!languages.diff) languages.diff = "Diff";
4581
4641
 
4582
4642
  const sortedEntries = Object.entries(languages)
4583
4643
  .sort(([ , a ], [ , b ]) => a.localeCompare(b));