@37signals/lexxy 0.9.0-beta → 0.9.2-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lexxy.esm.js CHANGED
@@ -10,7 +10,7 @@ import 'prismjs/components/prism-json';
10
10
  import 'prismjs/components/prism-diff';
11
11
  import DOMPurify from 'dompurify';
12
12
  import { getStyleObjectFromCSS, getCSSFromStyleObject, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType } from '@lexical/selection';
13
- import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, DecoratorNode, $createTextNode, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, $isElementNode, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $createParagraphNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $getEditor, $getNearestRootOrShadowRoot, $isNodeSelection, $getRoot, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $isRootOrShadowRoot, ElementNode, $splitNode, $isParagraphNode, $createLineBreakNode, $isRootNode, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, CLEAR_HISTORY_COMMAND, $addUpdateTag, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
13
+ import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, DecoratorNode, $createTextNode, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, $isElementNode, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $createParagraphNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $getEditor, $getNearestRootOrShadowRoot, $isNodeSelection, $getRoot, mergeRegister as mergeRegister$1, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $isRootOrShadowRoot, ElementNode, $splitNode, $isParagraphNode, $createLineBreakNode, $isRootNode, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, CLEAR_HISTORY_COMMAND, $addUpdateTag, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
14
14
  import { buildEditorFromExtensions } from '@lexical/extension';
15
15
  import { ListNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListItemNode, $getListDepth, $isListItemNode, $isListNode, registerList } from '@lexical/list';
16
16
  import { $createAutoLinkNode, $toggleLink, LinkNode, $createLinkNode, AutoLinkNode, $isLinkNode } from '@lexical/link';
@@ -22,7 +22,7 @@ import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
22
22
  import { createEmptyHistoryState, registerHistory } from '@lexical/history';
23
23
  import { createElement, extractPlainTextFromHtml, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
24
24
  export { highlightCode as highlightAll, highlightCode } from './lexxy_helpers.esm.js';
25
- import { INSERT_TABLE_COMMAND, $getTableCellNodeFromLexicalNode, TableCellNode, TableNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
25
+ import { INSERT_TABLE_COMMAND, $getTableCellNodeFromLexicalNode, TableCellNode, TableNode, TableRowNode, setScrollableTablesActive, registerTablePlugin, registerTableSelectionObserver, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
26
26
  import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $descendantsMatching } from '@lexical/utils';
27
27
  import { marked } from 'marked';
28
28
  import { $insertDataTransferForRichText } from '@lexical/clipboard';
@@ -382,9 +382,22 @@ class LexicalToolbarElement extends HTMLElement {
382
382
  }
383
383
 
384
384
  disconnectedCallback() {
385
+ this.dispose();
386
+ }
387
+
388
+ dispose() {
385
389
  this.#uninstallResizeObserver();
390
+ this.#unbindButtons();
386
391
  this.#unbindHotkeys();
387
392
  this.#unbindFocusListeners();
393
+ this.unregisterSelectionListener?.();
394
+ this.unregisterHistoryListener?.();
395
+
396
+ this.editorElement = null;
397
+ this.editor = null;
398
+ this.selection = null;
399
+
400
+ this.#createEditorPromise();
388
401
  }
389
402
 
390
403
  attributeChangedCallback(name, oldValue, newValue) {
@@ -428,10 +441,12 @@ class LexicalToolbarElement extends HTMLElement {
428
441
  this.connectedCallback();
429
442
  }
430
443
 
431
- #createEditorPromise() {
444
+ async #createEditorPromise() {
432
445
  this.editorPromise = new Promise((resolve) => {
433
446
  this.resolveEditorPromise = resolve;
434
447
  });
448
+
449
+ this.editorElement = await this.editorPromise;
435
450
  }
436
451
 
437
452
  #installResizeObserver() {
@@ -447,10 +462,14 @@ class LexicalToolbarElement extends HTMLElement {
447
462
  }
448
463
 
449
464
  #bindButtons() {
450
- this.addEventListener("click", this.#handleButtonClicked.bind(this));
465
+ this.addEventListener("click", this.#handleButtonClicked);
466
+ }
467
+
468
+ #unbindButtons() {
469
+ this.removeEventListener("click", this.#handleButtonClicked);
451
470
  }
452
471
 
453
- #handleButtonClicked(event) {
472
+ #handleButtonClicked = (event) => {
454
473
  this.#handleTargetClicked(event, "[data-command]", this.#dispatchButtonCommand.bind(this));
455
474
  }
456
475
 
@@ -510,8 +529,8 @@ class LexicalToolbarElement extends HTMLElement {
510
529
  }
511
530
 
512
531
  #unbindFocusListeners() {
513
- this.editorElement.removeEventListener("lexxy:focus", this.#handleEditorFocus);
514
- this.editorElement.removeEventListener("lexxy:blur", this.#handleEditorBlur);
532
+ this.editorElement?.removeEventListener("lexxy:focus", this.#handleEditorFocus);
533
+ this.editorElement?.removeEventListener("lexxy:blur", this.#handleEditorBlur);
515
534
  this.removeEventListener("keydown", this.#handleKeydown);
516
535
  }
517
536
 
@@ -535,7 +554,7 @@ class LexicalToolbarElement extends HTMLElement {
535
554
  }
536
555
 
537
556
  #monitorSelectionChanges() {
538
- this.editor.registerUpdateListener(() => {
557
+ this.unregisterSelectionListener = this.editor.registerUpdateListener(() => {
539
558
  this.editor.getEditorState().read(() => {
540
559
  this.#updateButtonStates();
541
560
  this.#closeDropdowns();
@@ -544,7 +563,7 @@ class LexicalToolbarElement extends HTMLElement {
544
563
  }
545
564
 
546
565
  #monitorHistoryChanges() {
547
- this.editor.registerUpdateListener(() => {
566
+ this.unregisterHistoryListener = this.editor.registerUpdateListener(() => {
548
567
  this.#updateUndoRedoButtonStates();
549
568
  });
550
569
  }
@@ -696,13 +715,13 @@ class LexicalToolbarElement extends HTMLElement {
696
715
 
697
716
  static get defaultTemplate() {
698
717
  return `
699
- <button class="lexxy-editor__toolbar-button" type="button" name="image" data-command="uploadAttachments" data-prevent-overflow="true" title="Add images">
700
- ${ToolbarIcons.image}
701
- </button>
718
+ <button class="lexxy-editor__toolbar-button" type="button" name="image" data-command="uploadImage" data-prevent-overflow="true" title="Add images and video">
719
+ ${ToolbarIcons.image}
720
+ </button>
702
721
 
703
- <button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" type="button" name="file" data-command="uploadAttachments" title="Upload files">
704
- ${ToolbarIcons.attachment}
705
- </button>
722
+ <button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" type="button" name="file" data-command="uploadFile" title="Upload files">
723
+ ${ToolbarIcons.attachment}
724
+ </button>
706
725
 
707
726
  <button class="lexxy-editor__toolbar-button" type="button" name="bold" data-command="bold" title="Bold">
708
727
  ${ToolbarIcons.bold}
@@ -726,10 +745,10 @@ class LexicalToolbarElement extends HTMLElement {
726
745
  <button type="button" name="heading-medium" data-command="setFormatHeadingMedium" title="Medium heading">
727
746
  ${ToolbarIcons.h3} <span>Medium Heading</span>
728
747
  </button>
729
- <button type="button" name="heading-small" data-command="setFormatHeadingSmall" title="Small heading">
748
+ <button class="lexxy-editor__toolbar-group-end" type="button" name="heading-small" data-command="setFormatHeadingSmall" title="Small heading">
730
749
  ${ToolbarIcons.h4} <span>Small Heading</span>
731
750
  </button>
732
- <div class="separator" role="separator"></div>
751
+ <div class="lexxy-editor__toolbar-separator" role="separator"></div>
733
752
  <button type="button" name="strikethrough" data-command="strikethrough" title="Strikethrough">
734
753
  ${ToolbarIcons.strikethrough} <span>Strikethrough</span>
735
754
  </button>
@@ -739,21 +758,6 @@ class LexicalToolbarElement extends HTMLElement {
739
758
  </div>
740
759
  </details>
741
760
 
742
-
743
- <details class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-dropdown--chevron" name="lexxy-dropdown">
744
- <summary class="lexxy-editor__toolbar-button" name="lists" title="Lists">
745
- ${ToolbarIcons.ul}
746
- </summary>
747
- <div class="lexxy-editor__toolbar-dropdown-list">
748
- <button type="button" name="unordered-list" data-command="insertUnorderedList" title="Bullet list">
749
- ${ToolbarIcons.ul} <span>Bullets</span>
750
- </button>
751
- <button type="button" name="ordered-list" data-command="insertOrderedList" title="Numbered list">
752
- ${ToolbarIcons.ol} <span>Numbers</span>
753
- </button>
754
- </div>
755
- </details>
756
-
757
761
  <details class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-dropdown--chevron" name="lexxy-dropdown">
758
762
  <summary class="lexxy-editor__toolbar-button" name="highlight" title="Color highlight">
759
763
  ${ToolbarIcons.highlight}
@@ -765,7 +769,7 @@ class LexicalToolbarElement extends HTMLElement {
765
769
  </details>
766
770
 
767
771
  <details class="lexxy-editor__toolbar-dropdown" name="lexxy-dropdown">
768
- <summary class="lexxy-editor__toolbar-button" name="link" title="Link" data-hotkey="cmd+k ctrl+k">
772
+ <summary class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" name="link" title="Link" data-hotkey="cmd+k ctrl+k">
769
773
  ${ToolbarIcons.link}
770
774
  </summary>
771
775
  <lexxy-link-dropdown class="lexxy-editor__toolbar-dropdown-content">
@@ -783,10 +787,17 @@ class LexicalToolbarElement extends HTMLElement {
783
787
  ${ToolbarIcons.quote}
784
788
  </button>
785
789
 
786
- <button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" type="button" name="code" data-command="insertCodeBlock" title="Code">
790
+ <button class="lexxy-editor__toolbar-button" type="button" name="code" data-command="insertCodeBlock" title="Code">
787
791
  ${ToolbarIcons.code}
788
792
  </button>
789
793
 
794
+ <button class="lexxy-editor__toolbar-button" type="button" name="unordered-list" data-command="insertUnorderedList" title="Bullet list">
795
+ ${ToolbarIcons.ul}
796
+ </button>
797
+ <button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" type="button" name="ordered-list" data-command="insertOrderedList" title="Numbered list">
798
+ ${ToolbarIcons.ol}
799
+ </button>
800
+
790
801
  <button class="lexxy-editor__toolbar-button" type="button" name="table" data-command="insertTable" title="Insert a table">
791
802
  ${ToolbarIcons.table}
792
803
  </button>
@@ -1961,7 +1972,8 @@ const COMMANDS = [
1961
1972
  "insertQuoteBlock",
1962
1973
  "insertCodeBlock",
1963
1974
  "insertHorizontalDivider",
1964
- "uploadAttachments",
1975
+ "uploadImage",
1976
+ "uploadFile",
1965
1977
 
1966
1978
  "insertTable",
1967
1979
 
@@ -1971,9 +1983,10 @@ const COMMANDS = [
1971
1983
 
1972
1984
  class CommandDispatcher {
1973
1985
  #selectionBeforeDrag = null
1986
+ #unregister = []
1974
1987
 
1975
1988
  static configureFor(editorElement) {
1976
- new CommandDispatcher(editorElement);
1989
+ return new CommandDispatcher(editorElement)
1977
1990
  }
1978
1991
 
1979
1992
  constructor(editorElement) {
@@ -2145,15 +2158,27 @@ class CommandDispatcher {
2145
2158
  this.contents.applyParagraphFormat();
2146
2159
  }
2147
2160
 
2148
- dispatchUploadAttachments() {
2149
- const input = createElement("input", {
2161
+ dispatchUploadImage() {
2162
+ this.#dispatchUploadAttachment("image/*,video/*");
2163
+ }
2164
+
2165
+ dispatchUploadFile() {
2166
+ this.#dispatchUploadAttachment();
2167
+ }
2168
+
2169
+ #dispatchUploadAttachment(accept = null) {
2170
+ const attributes = {
2150
2171
  type: "file",
2151
2172
  multiple: true,
2152
2173
  style: "display: none;",
2153
2174
  onchange: ({ target: { files } }) => {
2154
2175
  this.contents.uploadFiles(files, { selectLast: true });
2155
2176
  }
2156
- });
2177
+ };
2178
+
2179
+ if (accept) attributes.accept = accept;
2180
+
2181
+ const input = createElement("input", attributes);
2157
2182
 
2158
2183
  // Append and remove to make testable
2159
2184
  this.editorElement.appendChild(input);
@@ -2173,6 +2198,13 @@ class CommandDispatcher {
2173
2198
  this.editor.dispatchCommand(REDO_COMMAND, undefined);
2174
2199
  }
2175
2200
 
2201
+ dispose() {
2202
+ while (this.#unregister.length) {
2203
+ const unregister = this.#unregister.pop();
2204
+ unregister();
2205
+ }
2206
+ }
2207
+
2176
2208
  #registerCommands() {
2177
2209
  for (const command of COMMANDS) {
2178
2210
  const methodName = `dispatch${capitalize(command)}`;
@@ -2183,12 +2215,12 @@ class CommandDispatcher {
2183
2215
  }
2184
2216
 
2185
2217
  #registerCommandHandler(command, priority, handler) {
2186
- this.editor.registerCommand(command, handler, priority);
2218
+ this.#unregister.push(this.editor.registerCommand(command, handler, priority));
2187
2219
  }
2188
2220
 
2189
2221
  #registerKeyboardCommands() {
2190
- this.editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, this.#handleArrowRightKey.bind(this), COMMAND_PRIORITY_NORMAL);
2191
- this.editor.registerCommand(KEY_TAB_COMMAND, this.#handleTabKey.bind(this), COMMAND_PRIORITY_NORMAL);
2222
+ this.#registerCommandHandler(KEY_ARROW_RIGHT_COMMAND, COMMAND_PRIORITY_NORMAL, this.#handleArrowRightKey.bind(this));
2223
+ this.#registerCommandHandler(KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, this.#handleTabKey.bind(this));
2192
2224
  }
2193
2225
 
2194
2226
  #handleArrowRightKey(event) {
@@ -2674,6 +2706,8 @@ function $isActionTextAttachmentNode(node) {
2674
2706
  }
2675
2707
 
2676
2708
  class Selection {
2709
+ #unregister = []
2710
+
2677
2711
  constructor(editorElement) {
2678
2712
  this.editorElement = editorElement;
2679
2713
  this.editorContentElement = editorElement.editorContentElement;
@@ -2930,6 +2964,18 @@ class Selection {
2930
2964
  return this.#findPreviousSiblingUp(anchorNode)
2931
2965
  }
2932
2966
 
2967
+ dispose() {
2968
+ this.editorElement = null;
2969
+ this.editorContentElement = null;
2970
+ this.editor = null;
2971
+ this.previouslySelectedKeys = null;
2972
+
2973
+ while (this.#unregister.length) {
2974
+ const unregister = this.#unregister.pop();
2975
+ unregister();
2976
+ }
2977
+ }
2978
+
2933
2979
  // When all inline code text is deleted, Lexical's selection retains the stale
2934
2980
  // code format flag. Verify the flag is backed by actual code-formatted content:
2935
2981
  // a code block ancestor or a text node that carries the code format.
@@ -2945,7 +2991,7 @@ class Selection {
2945
2991
  // detects that stale state and clears it so newly typed text won't be
2946
2992
  // code-formatted.
2947
2993
  #clearStaleInlineCodeFormat() {
2948
- this.editor.registerUpdateListener(({ editorState, tags }) => {
2994
+ this.#unregister.push(this.editor.registerUpdateListener(({ editorState, tags }) => {
2949
2995
  if (tags.has("history-merge") || tags.has("skip-dom-selection")) return
2950
2996
 
2951
2997
  let isStale = false;
@@ -2974,7 +3020,7 @@ class Selection {
2974
3020
  });
2975
3021
  }, 0);
2976
3022
  }
2977
- });
3023
+ }));
2978
3024
  }
2979
3025
 
2980
3026
  get #currentlySelectedKeys() {
@@ -2993,29 +3039,32 @@ class Selection {
2993
3039
  }
2994
3040
 
2995
3041
  #processSelectionChangeCommands() {
2996
- this.editor.registerCommand(KEY_ARROW_LEFT_COMMAND, this.#selectPreviousNode.bind(this), COMMAND_PRIORITY_LOW);
2997
- this.editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, this.#selectNextNode.bind(this), COMMAND_PRIORITY_LOW);
2998
- this.editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#selectPreviousTopLevelNode.bind(this), COMMAND_PRIORITY_LOW);
2999
- this.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, this.#selectNextTopLevelNode.bind(this), COMMAND_PRIORITY_LOW);
3042
+ this.#unregister.push(mergeRegister$1(
3043
+ this.editor.registerCommand(KEY_ARROW_LEFT_COMMAND, this.#selectPreviousNode.bind(this), COMMAND_PRIORITY_LOW),
3044
+ this.editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, this.#selectNextNode.bind(this), COMMAND_PRIORITY_LOW),
3045
+ this.editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#selectPreviousTopLevelNode.bind(this), COMMAND_PRIORITY_LOW),
3046
+ this.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, this.#selectNextTopLevelNode.bind(this), COMMAND_PRIORITY_LOW),
3000
3047
 
3001
- this.editor.registerCommand(DELETE_CHARACTER_COMMAND, this.#selectDecoratorNodeBeforeDeletion.bind(this), COMMAND_PRIORITY_LOW);
3048
+ this.editor.registerCommand(DELETE_CHARACTER_COMMAND, this.#selectDecoratorNodeBeforeDeletion.bind(this), COMMAND_PRIORITY_LOW),
3002
3049
 
3003
- this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
3004
- this.current = $getSelection();
3005
- }, COMMAND_PRIORITY_LOW);
3050
+ this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
3051
+ this.current = $getSelection();
3052
+ }, COMMAND_PRIORITY_LOW)
3053
+ ));
3006
3054
  }
3007
3055
 
3008
3056
  #listenForNodeSelections() {
3009
- this.editor.registerCommand(CLICK_COMMAND, ({ target }) => {
3057
+ this.#unregister.push(this.editor.registerCommand(CLICK_COMMAND, ({ target }) => {
3010
3058
  if (!isDOMNode(target)) return false
3011
3059
 
3012
3060
  const targetNode = $getNearestNodeFromDOMNode(target);
3013
3061
  return $isDecoratorNode(targetNode) && this.#selectInLexical(targetNode)
3014
- }, COMMAND_PRIORITY_LOW);
3062
+ }, COMMAND_PRIORITY_LOW));
3015
3063
 
3016
- this.editor.getRootElement().addEventListener("lexxy:internal:move-to-next-line", (event) => {
3017
- this.#selectOrAppendNextLine();
3018
- });
3064
+ const moveNextLineHandler = () => this.#selectOrAppendNextLine();
3065
+ const rootElement = this.editor.getRootElement();
3066
+ rootElement.addEventListener("lexxy:internal:move-to-next-line", moveNextLineHandler);
3067
+ this.#unregister.push(() => rootElement.removeEventListener("lexxy:internal:move-to-next-line", moveNextLineHandler));
3019
3068
  }
3020
3069
 
3021
3070
  #containEditorFocus() {
@@ -4150,7 +4199,11 @@ class Contents {
4150
4199
  constructor(editorElement) {
4151
4200
  this.editorElement = editorElement;
4152
4201
  this.editor = editorElement.editor;
4202
+ }
4153
4203
 
4204
+ dispose() {
4205
+ this.editorElement = null;
4206
+ this.editor = null;
4154
4207
  }
4155
4208
 
4156
4209
  insertHtml(html, { tag } = {}) {
@@ -5181,11 +5234,12 @@ class TablesExtension extends LexxyExtension {
5181
5234
  TableRowNode
5182
5235
  ],
5183
5236
  register(editor) {
5237
+ setScrollableTablesActive(editor, true);
5238
+
5184
5239
  return mergeRegister(
5185
5240
  // Register Lexical table plugins
5186
5241
  registerTablePlugin(editor),
5187
5242
  registerTableSelectionObserver(editor, true),
5188
- setScrollableTablesActive(editor, true),
5189
5243
 
5190
5244
  // Bug fix: Prevent hardcoded background color (Lexical #8089)
5191
5245
  editor.registerNodeTransform(TableCellNode, (node) => {
@@ -5912,6 +5966,7 @@ class LexicalEditorElement extends HTMLElement {
5912
5966
 
5913
5967
  #initialValue = ""
5914
5968
  #validationTextArea = document.createElement("textarea")
5969
+ #disposables = []
5915
5970
 
5916
5971
  constructor() {
5917
5972
  super();
@@ -5925,12 +5980,19 @@ class LexicalEditorElement extends HTMLElement {
5925
5980
  this.extensions = new Extensions(this);
5926
5981
 
5927
5982
  this.editor = this.#createEditor();
5983
+ this.#disposables.push(this.editor);
5928
5984
 
5929
5985
  this.contents = new Contents(this);
5986
+ this.#disposables.push(this.contents);
5987
+
5930
5988
  this.selection = new Selection(this);
5989
+ this.#disposables.push(this.selection);
5990
+
5931
5991
  this.clipboard = new Clipboard(this);
5932
5992
 
5933
- CommandDispatcher.configureFor(this);
5993
+ const commandDispatcher = CommandDispatcher.configureFor(this);
5994
+ this.#disposables.push(commandDispatcher);
5995
+
5934
5996
  this.#initialize();
5935
5997
 
5936
5998
  requestAnimationFrame(() => dispatch(this, "lexxy:initialize"));
@@ -5983,7 +6045,7 @@ class LexicalEditorElement extends HTMLElement {
5983
6045
  get toolbarElement() {
5984
6046
  if (!this.#hasToolbar) return null
5985
6047
 
5986
- this.toolbar = this.toolbar || this.#findOrCreateDefaultToolbar();
6048
+ this.toolbar ??= this.#findOrCreateDefaultToolbar();
5987
6049
  return this.toolbar
5988
6050
  }
5989
6051
 
@@ -6119,6 +6181,7 @@ class LexicalEditorElement extends HTMLElement {
6119
6181
 
6120
6182
  #createEditor() {
6121
6183
  this.editorContentElement ||= this.#createEditorContentElement();
6184
+ this.appendChild(this.editorContentElement);
6122
6185
 
6123
6186
  const editor = buildEditorFromExtensions({
6124
6187
  name: "lexxy/core",
@@ -6168,7 +6231,6 @@ class LexicalEditorElement extends HTMLElement {
6168
6231
  });
6169
6232
  editorContentElement.id = `${this.id}-content`;
6170
6233
  this.#ariaAttributes.forEach(attribute => editorContentElement.setAttribute(attribute.name, attribute.value));
6171
- this.appendChild(editorContentElement);
6172
6234
 
6173
6235
  if (this.getAttribute("tabindex")) {
6174
6236
  editorContentElement.setAttribute("tabindex", this.getAttribute("tabindex"));
@@ -6244,36 +6306,48 @@ class LexicalEditorElement extends HTMLElement {
6244
6306
  }
6245
6307
 
6246
6308
  #registerComponents() {
6309
+ const registered = [];
6310
+
6247
6311
  if (this.supportsRichText) {
6248
- registerRichText(this.editor);
6249
- registerList(this.editor);
6312
+ registered.push(
6313
+ registerRichText(this.editor),
6314
+ registerList(this.editor)
6315
+ );
6250
6316
  this.#registerTableComponents();
6251
6317
  this.#registerCodeHiglightingComponents();
6252
6318
  if (this.supportsMarkdown) {
6253
- registerMarkdownShortcuts(this.editor, TRANSFORMERS);
6254
- registerMarkdownLeadingTagHandler(this.editor, TRANSFORMERS);
6319
+ registered.push(
6320
+ registerMarkdownShortcuts(this.editor, TRANSFORMERS),
6321
+ registerMarkdownLeadingTagHandler(this.editor, TRANSFORMERS)
6322
+ );
6255
6323
  }
6256
6324
  } else {
6257
- registerPlainText(this.editor);
6325
+ registered.push(registerPlainText(this.editor));
6258
6326
  }
6259
6327
  this.historyState = createEmptyHistoryState();
6260
- registerHistory(this.editor, this.historyState, 20);
6328
+ registered.push(registerHistory(this.editor, this.historyState, 20));
6329
+
6330
+ this.#addUnregisterHandler(mergeRegister$1(...registered));
6261
6331
  }
6262
6332
 
6263
6333
  #registerTableComponents() {
6264
- this.tableTools = createElement("lexxy-table-tools");
6265
- this.append(this.tableTools);
6334
+ let tableTools = this.querySelector("lexxy-table-tools");
6335
+ tableTools ??= createElement("lexxy-table-tools");
6336
+ this.append(tableTools);
6337
+ this.#disposables.push(tableTools);
6266
6338
  }
6267
6339
 
6268
6340
  #registerCodeHiglightingComponents() {
6269
6341
  registerCodeHighlighting(this.editor);
6270
- this.codeLanguagePicker = createElement("lexxy-code-language-picker");
6271
- this.append(this.codeLanguagePicker);
6342
+ let codeLanguagePicker = this.querySelector("lexxy-code-language-picker");
6343
+ codeLanguagePicker ??= createElement("lexxy-code-language-picker");
6344
+ this.append(codeLanguagePicker);
6345
+ this.#disposables.push(codeLanguagePicker);
6272
6346
  }
6273
6347
 
6274
6348
  #handleEnter() {
6275
6349
  // We can't prevent these externally using regular keydown because Lexical handles it first.
6276
- this.editor.registerCommand(
6350
+ this.#addUnregisterHandler(this.editor.registerCommand(
6277
6351
  KEY_ENTER_COMMAND,
6278
6352
  (event) => {
6279
6353
  // Prevent CTRL+ENTER
@@ -6291,12 +6365,17 @@ class LexicalEditorElement extends HTMLElement {
6291
6365
  return false
6292
6366
  },
6293
6367
  COMMAND_PRIORITY_NORMAL
6294
- );
6368
+ ));
6295
6369
  }
6296
6370
 
6297
6371
  #registerFocusEvents() {
6298
6372
  this.addEventListener("focusin", this.#handleFocusIn);
6299
6373
  this.addEventListener("focusout", this.#handleFocusOut);
6374
+
6375
+ this.#addUnregisterHandler(() => {
6376
+ this.removeEventListener("focusin", this.#handleFocusIn);
6377
+ this.removeEventListener("focusout", this.#handleFocusOut);
6378
+ });
6300
6379
  }
6301
6380
 
6302
6381
  #handleFocusIn(event) {
@@ -6339,6 +6418,10 @@ class LexicalEditorElement extends HTMLElement {
6339
6418
  #attachToolbar() {
6340
6419
  if (this.#hasToolbar) {
6341
6420
  this.toolbarElement.setEditor(this);
6421
+ if (typeof this.toolbarElement.dispose === "function") {
6422
+ this.#disposables.push(this.toolbarElement);
6423
+ }
6424
+
6342
6425
  this.extensions.initializeToolbars();
6343
6426
  }
6344
6427
  }
@@ -6348,7 +6431,7 @@ class LexicalEditorElement extends HTMLElement {
6348
6431
  if (typeof toolbarConfig === "string") {
6349
6432
  return document.getElementById(toolbarConfig)
6350
6433
  } else {
6351
- return this.#createDefaultToolbar()
6434
+ return this.querySelector("lexxy-toolbar") ?? this.#createDefaultToolbar()
6352
6435
  }
6353
6436
  }
6354
6437
 
@@ -6378,34 +6461,22 @@ class LexicalEditorElement extends HTMLElement {
6378
6461
  }
6379
6462
 
6380
6463
  #reset() {
6381
- this.#unregisterHandlers();
6382
-
6383
- if (this.editorContentElement) {
6384
- this.editorContentElement.remove();
6385
- this.editorContentElement = null;
6386
- }
6387
-
6388
- this.contents = null;
6389
- this.editor = null;
6464
+ this.#dispose();
6465
+ this.editorContentElement?.remove();
6466
+ this.editorContentElement = null;
6390
6467
 
6391
- if (this.toolbar) {
6392
- if (!this.getAttribute("toolbar")) { this.toolbar.remove(); }
6393
- this.toolbar = null;
6394
- }
6468
+ // Prevents issues with turbo morphing receiving an empty <lexxy-editor> which wipes
6469
+ // out the DOM for the tools, and the old toolbar reference will cause issues
6470
+ this.toolbar = null;
6471
+ }
6395
6472
 
6396
- if (this.codeLanguagePicker) {
6397
- this.codeLanguagePicker.remove();
6398
- this.codeLanguagePicker = null;
6399
- }
6473
+ #dispose() {
6474
+ this.#unregisterHandlers();
6475
+ document.removeEventListener("turbo:before-cache", this.#handleTurboBeforeCache);
6400
6476
 
6401
- if (this.tableHandler) {
6402
- this.tableHandler.remove();
6403
- this.tableHandler = null;
6477
+ while (this.#disposables.length) {
6478
+ this.#disposables.pop().dispose();
6404
6479
  }
6405
-
6406
- this.selection = null;
6407
-
6408
- document.removeEventListener("turbo:before-cache", this.#handleTurboBeforeCache);
6409
6480
  }
6410
6481
 
6411
6482
  #reconnect() {
@@ -6446,14 +6517,15 @@ class ToolbarDropdown extends HTMLElement {
6446
6517
  connectedCallback() {
6447
6518
  this.container = this.closest("details");
6448
6519
 
6449
- this.container.addEventListener("toggle", this.#handleToggle.bind(this));
6450
- this.container.addEventListener("keydown", this.#handleKeyDown.bind(this));
6520
+ this.container.addEventListener("toggle", this.#handleToggle);
6521
+ this.container.addEventListener("keydown", this.#handleKeyDown);
6451
6522
 
6452
6523
  this.#onToolbarEditor(this.initialize.bind(this));
6453
6524
  }
6454
6525
 
6455
6526
  disconnectedCallback() {
6456
- this.container.removeEventListener("keydown", this.#handleKeyDown.bind(this));
6527
+ this.container?.removeEventListener("toggle", this.#handleToggle);
6528
+ this.container?.removeEventListener("keydown", this.#handleKeyDown);
6457
6529
  }
6458
6530
 
6459
6531
  get toolbar() {
@@ -6478,11 +6550,11 @@ class ToolbarDropdown extends HTMLElement {
6478
6550
  }
6479
6551
 
6480
6552
  async #onToolbarEditor(callback) {
6481
- await this.toolbar.editorConnected;
6553
+ await this.toolbar.editorElement;
6482
6554
  callback();
6483
6555
  }
6484
6556
 
6485
- #handleToggle() {
6557
+ #handleToggle = () => {
6486
6558
  if (this.container.open) {
6487
6559
  this.#handleOpen();
6488
6560
  }
@@ -6493,7 +6565,7 @@ class ToolbarDropdown extends HTMLElement {
6493
6565
  this.#resetTabIndexValues();
6494
6566
  }
6495
6567
 
6496
- #handleKeyDown(event) {
6568
+ #handleKeyDown = (event) => {
6497
6569
  if (event.key === "Escape") {
6498
6570
  event.stopPropagation();
6499
6571
  this.close();
@@ -6521,27 +6593,30 @@ class LinkDropdown extends ToolbarDropdown {
6521
6593
  super.connectedCallback();
6522
6594
  this.input = this.querySelector("input");
6523
6595
 
6524
- this.#registerHandlers();
6596
+ this.container.addEventListener("toggle", this.#handleToggle);
6597
+ this.addEventListener("submit", this.#handleSubmit);
6598
+ this.querySelector("[value='unlink']").addEventListener("click", this.#handleUnlink);
6525
6599
  }
6526
6600
 
6527
- #registerHandlers() {
6528
- this.container.addEventListener("toggle", this.#handleToggle.bind(this));
6529
- this.addEventListener("submit", this.#handleSubmit.bind(this));
6530
- this.querySelector("[value='unlink']").addEventListener("click", this.#handleUnlink.bind(this));
6601
+ disconnectedCallback() {
6602
+ this.container?.removeEventListener("toggle", this.#handleToggle);
6603
+ this.removeEventListener("submit", this.#handleSubmit);
6604
+ this.querySelector("[value='unlink']")?.removeEventListener("click", this.#handleUnlink);
6605
+ super.disconnectedCallback();
6531
6606
  }
6532
6607
 
6533
- #handleToggle({ newState }) {
6608
+ #handleToggle = ({ newState }) => {
6534
6609
  this.input.value = this.#selectedLinkUrl;
6535
6610
  this.input.required = newState === "open";
6536
6611
  }
6537
6612
 
6538
- #handleSubmit(event) {
6613
+ #handleSubmit = (event) => {
6539
6614
  const command = event.submitter?.value;
6540
6615
  this.editor.dispatchCommand(command, this.input.value);
6541
6616
  this.close();
6542
6617
  }
6543
6618
 
6544
- #handleUnlink() {
6619
+ #handleUnlink = () => {
6545
6620
  this.editor.dispatchCommand("unlink");
6546
6621
  this.close();
6547
6622
  }
@@ -6576,26 +6651,35 @@ const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
6576
6651
  const NO_STYLE = Symbol("no_style");
6577
6652
 
6578
6653
  class HighlightDropdown extends ToolbarDropdown {
6579
- connectedCallback() {
6580
- super.connectedCallback();
6581
- this.#registerToggleHandler();
6582
- }
6583
-
6584
6654
  initialize() {
6585
6655
  this.#setUpButtons();
6586
6656
  this.#registerButtonHandlers();
6587
6657
  }
6588
6658
 
6589
- #registerToggleHandler() {
6590
- this.container.addEventListener("toggle", this.#handleToggle.bind(this));
6659
+ connectedCallback() {
6660
+ super.connectedCallback();
6661
+ this.container.addEventListener("toggle", this.#handleToggle);
6662
+ }
6663
+
6664
+ disconnectedCallback() {
6665
+ this.container?.removeEventListener("toggle", this.#handleToggle);
6666
+ this.#removeButtonHandlers();
6667
+ super.disconnectedCallback();
6591
6668
  }
6592
6669
 
6593
6670
  #registerButtonHandlers() {
6594
- this.#colorButtons.forEach(button => button.addEventListener("click", this.#handleColorButtonClick.bind(this)));
6595
- this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).addEventListener("click", this.#handleRemoveHighlightClick.bind(this));
6671
+ this.#colorButtons.forEach(button => button.addEventListener("click", this.#handleColorButtonClick));
6672
+ this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).addEventListener("click", this.#handleRemoveHighlightClick);
6673
+ }
6674
+
6675
+ #removeButtonHandlers() {
6676
+ this.#colorButtons.forEach(button => button.removeEventListener("click", this.#handleColorButtonClick));
6677
+ this.querySelector(REMOVE_HIGHLIGHT_SELECTOR)?.removeEventListener("click", this.#handleRemoveHighlightClick);
6596
6678
  }
6597
6679
 
6598
6680
  #setUpButtons() {
6681
+ this.#buttonContainer.innerHTML = "";
6682
+
6599
6683
  const colorGroups = this.editorElement.config.get("highlight.buttons");
6600
6684
 
6601
6685
  this.#populateButtonGroup("color", colorGroups.color);
@@ -6621,7 +6705,7 @@ class HighlightDropdown extends ToolbarDropdown {
6621
6705
  return button
6622
6706
  }
6623
6707
 
6624
- #handleToggle({ newState }) {
6708
+ #handleToggle = ({ newState }) => {
6625
6709
  if (newState === "open") {
6626
6710
  this.editor.getEditorState().read(() => {
6627
6711
  this.#updateColorButtonStates($getSelection());
@@ -6629,7 +6713,7 @@ class HighlightDropdown extends ToolbarDropdown {
6629
6713
  }
6630
6714
  }
6631
6715
 
6632
- #handleColorButtonClick(event) {
6716
+ #handleColorButtonClick = (event) => {
6633
6717
  event.preventDefault();
6634
6718
 
6635
6719
  const button = event.target.closest(APPLY_HIGHLIGHT_SELECTOR);
@@ -6642,7 +6726,7 @@ class HighlightDropdown extends ToolbarDropdown {
6642
6726
  this.close();
6643
6727
  }
6644
6728
 
6645
- #handleRemoveHighlightClick(event) {
6729
+ #handleRemoveHighlightClick = (event) => {
6646
6730
  event.preventDefault();
6647
6731
 
6648
6732
  this.editor.dispatchCommand("removeHighlight");
@@ -7304,19 +7388,21 @@ class CodeLanguagePicker extends HTMLElement {
7304
7388
  }
7305
7389
 
7306
7390
  disconnectedCallback() {
7391
+ this.dispose();
7392
+ }
7393
+
7394
+ dispose() {
7307
7395
  this.unregisterUpdateListener?.();
7308
7396
  this.unregisterUpdateListener = null;
7309
7397
  }
7310
7398
 
7311
7399
  #attachLanguagePicker() {
7312
- this.languagePickerElement = this.#createLanguagePicker();
7313
-
7314
- this.languagePickerElement.addEventListener("change", () => {
7315
- this.#updateCodeBlockLanguage(this.languagePickerElement.value);
7316
- });
7400
+ this.languagePickerElement = this.#findLanguagePicker() ?? this.#createLanguagePicker();
7401
+ this.append(this.languagePickerElement);
7402
+ }
7317
7403
 
7318
- this.languagePickerElement.setAttribute("nonce", getNonce());
7319
- this.appendChild(this.languagePickerElement);
7404
+ #findLanguagePicker() {
7405
+ return this.querySelector("select")
7320
7406
  }
7321
7407
 
7322
7408
  #createLanguagePicker() {
@@ -7329,6 +7415,12 @@ class CodeLanguagePicker extends HTMLElement {
7329
7415
  selectElement.appendChild(option);
7330
7416
  }
7331
7417
 
7418
+ selectElement.addEventListener("change", () => {
7419
+ this.#updateCodeBlockLanguage(this.languagePickerElement.value);
7420
+ });
7421
+
7422
+ selectElement.setAttribute("nonce", getNonce());
7423
+
7332
7424
  return selectElement
7333
7425
  }
7334
7426
 
@@ -7889,6 +7981,10 @@ class TableTools extends HTMLElement {
7889
7981
  }
7890
7982
 
7891
7983
  disconnectedCallback() {
7984
+ this.dispose();
7985
+ }
7986
+
7987
+ dispose() {
7892
7988
  this.#unregisterKeyboardShortcuts();
7893
7989
 
7894
7990
  this.unregisterUpdateListener?.();
@@ -7913,6 +8009,8 @@ class TableTools extends HTMLElement {
7913
8009
  }
7914
8010
 
7915
8011
  #setUpButtons() {
8012
+ this.innerHTML = "";
8013
+
7916
8014
  this.appendChild(this.#createRowButtonsContainer());
7917
8015
  this.appendChild(this.#createColumnButtonsContainer());
7918
8016
 
@@ -488,14 +488,15 @@
488
488
 
489
489
  &:after {
490
490
  background-color: var(--lexxy-color-ink-lighter);
491
+ block-size: var(--lexxy-toolbar-icon-size);
491
492
  content: "";
492
493
  display: block;
493
- width: 1px;
494
- height: 60%;
494
+ inline-size: 1px;
495
495
  inset-inline-end: calc(-1 * var(--lexxy-toolbar-spacing));
496
- inset-block-start: 20%;
497
- position: absolute;
496
+ inset-block: 0;
497
+ margin: auto;
498
498
  pointer-events: none;
499
+ position: absolute;
499
500
  }
500
501
  }
501
502
  }
@@ -556,11 +557,6 @@
556
557
  }
557
558
  }
558
559
 
559
- [overflowing] &:not(.lexxy-editor__toolbar-overflow) summary ~ * {
560
- inset-inline-end: var(--lexxy-toolbar-spacing);
561
- inset-inline-start: var(--lexxy-toolbar-spacing);
562
- }
563
-
564
560
  button {
565
561
  color: var(--lexxy-color-text);
566
562
 
@@ -572,7 +568,6 @@
572
568
  .lexxy-editor__toolbar-dropdown-list {
573
569
  border-start-start-radius: 0;
574
570
  flex-direction: column;
575
- gap: 0.1ch;
576
571
  padding: 0.1ch;
577
572
 
578
573
  button {
@@ -581,6 +576,7 @@
581
576
  flex-direction: row;
582
577
  gap: 1ch;
583
578
  padding: 1ch;
579
+ position: relative;
584
580
 
585
581
  &[aria-pressed="true"] {
586
582
  background-color: var(--lexxy-color-selected);
@@ -600,11 +596,22 @@
600
596
  }
601
597
  }
602
598
 
603
- .separator {
599
+ .lexxy-editor__toolbar-separator {
604
600
  background: var(--lexxy-color-ink-lighter);
605
601
  block-size: 1px;
606
602
  inline-size: 100%;
607
603
  }
604
+
605
+ [overflowing] & {
606
+ display: grid;
607
+ grid-template-columns: repeat(4, 1fr);
608
+
609
+ button span { display: none; }
610
+
611
+ .lexxy-editor__toolbar-separator {
612
+ grid-column: 1 / -1;
613
+ }
614
+ }
608
615
  }
609
616
 
610
617
 
@@ -706,6 +713,11 @@
706
713
  inset-inline-start: 0;
707
714
  max-inline-size: var(--max-inline-size);
708
715
 
716
+ [overflowing] & {
717
+ inset-inline-end: var(--lexxy-toolbar-spacing);
718
+ inset-inline-start: var(--lexxy-toolbar-spacing);
719
+ }
720
+
709
721
  button {
710
722
  position: relative;
711
723
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.0-beta",
3
+ "version": "0.9.2-beta",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",