@37signals/lexxy 0.9.1-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.
Files changed (2) hide show
  1. package/dist/lexxy.esm.js +195 -102
  2. package/package.json +1 -1
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
  }
@@ -1964,9 +1983,10 @@ const COMMANDS = [
1964
1983
 
1965
1984
  class CommandDispatcher {
1966
1985
  #selectionBeforeDrag = null
1986
+ #unregister = []
1967
1987
 
1968
1988
  static configureFor(editorElement) {
1969
- new CommandDispatcher(editorElement);
1989
+ return new CommandDispatcher(editorElement)
1970
1990
  }
1971
1991
 
1972
1992
  constructor(editorElement) {
@@ -2178,6 +2198,13 @@ class CommandDispatcher {
2178
2198
  this.editor.dispatchCommand(REDO_COMMAND, undefined);
2179
2199
  }
2180
2200
 
2201
+ dispose() {
2202
+ while (this.#unregister.length) {
2203
+ const unregister = this.#unregister.pop();
2204
+ unregister();
2205
+ }
2206
+ }
2207
+
2181
2208
  #registerCommands() {
2182
2209
  for (const command of COMMANDS) {
2183
2210
  const methodName = `dispatch${capitalize(command)}`;
@@ -2188,12 +2215,12 @@ class CommandDispatcher {
2188
2215
  }
2189
2216
 
2190
2217
  #registerCommandHandler(command, priority, handler) {
2191
- this.editor.registerCommand(command, handler, priority);
2218
+ this.#unregister.push(this.editor.registerCommand(command, handler, priority));
2192
2219
  }
2193
2220
 
2194
2221
  #registerKeyboardCommands() {
2195
- this.editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, this.#handleArrowRightKey.bind(this), COMMAND_PRIORITY_NORMAL);
2196
- 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));
2197
2224
  }
2198
2225
 
2199
2226
  #handleArrowRightKey(event) {
@@ -2679,6 +2706,8 @@ function $isActionTextAttachmentNode(node) {
2679
2706
  }
2680
2707
 
2681
2708
  class Selection {
2709
+ #unregister = []
2710
+
2682
2711
  constructor(editorElement) {
2683
2712
  this.editorElement = editorElement;
2684
2713
  this.editorContentElement = editorElement.editorContentElement;
@@ -2935,6 +2964,18 @@ class Selection {
2935
2964
  return this.#findPreviousSiblingUp(anchorNode)
2936
2965
  }
2937
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
+
2938
2979
  // When all inline code text is deleted, Lexical's selection retains the stale
2939
2980
  // code format flag. Verify the flag is backed by actual code-formatted content:
2940
2981
  // a code block ancestor or a text node that carries the code format.
@@ -2950,7 +2991,7 @@ class Selection {
2950
2991
  // detects that stale state and clears it so newly typed text won't be
2951
2992
  // code-formatted.
2952
2993
  #clearStaleInlineCodeFormat() {
2953
- this.editor.registerUpdateListener(({ editorState, tags }) => {
2994
+ this.#unregister.push(this.editor.registerUpdateListener(({ editorState, tags }) => {
2954
2995
  if (tags.has("history-merge") || tags.has("skip-dom-selection")) return
2955
2996
 
2956
2997
  let isStale = false;
@@ -2979,7 +3020,7 @@ class Selection {
2979
3020
  });
2980
3021
  }, 0);
2981
3022
  }
2982
- });
3023
+ }));
2983
3024
  }
2984
3025
 
2985
3026
  get #currentlySelectedKeys() {
@@ -2998,29 +3039,32 @@ class Selection {
2998
3039
  }
2999
3040
 
3000
3041
  #processSelectionChangeCommands() {
3001
- this.editor.registerCommand(KEY_ARROW_LEFT_COMMAND, this.#selectPreviousNode.bind(this), COMMAND_PRIORITY_LOW);
3002
- this.editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, this.#selectNextNode.bind(this), COMMAND_PRIORITY_LOW);
3003
- this.editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#selectPreviousTopLevelNode.bind(this), COMMAND_PRIORITY_LOW);
3004
- 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),
3005
3047
 
3006
- 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),
3007
3049
 
3008
- this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
3009
- this.current = $getSelection();
3010
- }, COMMAND_PRIORITY_LOW);
3050
+ this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
3051
+ this.current = $getSelection();
3052
+ }, COMMAND_PRIORITY_LOW)
3053
+ ));
3011
3054
  }
3012
3055
 
3013
3056
  #listenForNodeSelections() {
3014
- this.editor.registerCommand(CLICK_COMMAND, ({ target }) => {
3057
+ this.#unregister.push(this.editor.registerCommand(CLICK_COMMAND, ({ target }) => {
3015
3058
  if (!isDOMNode(target)) return false
3016
3059
 
3017
3060
  const targetNode = $getNearestNodeFromDOMNode(target);
3018
3061
  return $isDecoratorNode(targetNode) && this.#selectInLexical(targetNode)
3019
- }, COMMAND_PRIORITY_LOW);
3062
+ }, COMMAND_PRIORITY_LOW));
3020
3063
 
3021
- this.editor.getRootElement().addEventListener("lexxy:internal:move-to-next-line", (event) => {
3022
- this.#selectOrAppendNextLine();
3023
- });
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));
3024
3068
  }
3025
3069
 
3026
3070
  #containEditorFocus() {
@@ -4155,7 +4199,11 @@ class Contents {
4155
4199
  constructor(editorElement) {
4156
4200
  this.editorElement = editorElement;
4157
4201
  this.editor = editorElement.editor;
4202
+ }
4158
4203
 
4204
+ dispose() {
4205
+ this.editorElement = null;
4206
+ this.editor = null;
4159
4207
  }
4160
4208
 
4161
4209
  insertHtml(html, { tag } = {}) {
@@ -5186,11 +5234,12 @@ class TablesExtension extends LexxyExtension {
5186
5234
  TableRowNode
5187
5235
  ],
5188
5236
  register(editor) {
5237
+ setScrollableTablesActive(editor, true);
5238
+
5189
5239
  return mergeRegister(
5190
5240
  // Register Lexical table plugins
5191
5241
  registerTablePlugin(editor),
5192
5242
  registerTableSelectionObserver(editor, true),
5193
- setScrollableTablesActive(editor, true),
5194
5243
 
5195
5244
  // Bug fix: Prevent hardcoded background color (Lexical #8089)
5196
5245
  editor.registerNodeTransform(TableCellNode, (node) => {
@@ -5917,6 +5966,7 @@ class LexicalEditorElement extends HTMLElement {
5917
5966
 
5918
5967
  #initialValue = ""
5919
5968
  #validationTextArea = document.createElement("textarea")
5969
+ #disposables = []
5920
5970
 
5921
5971
  constructor() {
5922
5972
  super();
@@ -5930,12 +5980,19 @@ class LexicalEditorElement extends HTMLElement {
5930
5980
  this.extensions = new Extensions(this);
5931
5981
 
5932
5982
  this.editor = this.#createEditor();
5983
+ this.#disposables.push(this.editor);
5933
5984
 
5934
5985
  this.contents = new Contents(this);
5986
+ this.#disposables.push(this.contents);
5987
+
5935
5988
  this.selection = new Selection(this);
5989
+ this.#disposables.push(this.selection);
5990
+
5936
5991
  this.clipboard = new Clipboard(this);
5937
5992
 
5938
- CommandDispatcher.configureFor(this);
5993
+ const commandDispatcher = CommandDispatcher.configureFor(this);
5994
+ this.#disposables.push(commandDispatcher);
5995
+
5939
5996
  this.#initialize();
5940
5997
 
5941
5998
  requestAnimationFrame(() => dispatch(this, "lexxy:initialize"));
@@ -5988,7 +6045,7 @@ class LexicalEditorElement extends HTMLElement {
5988
6045
  get toolbarElement() {
5989
6046
  if (!this.#hasToolbar) return null
5990
6047
 
5991
- this.toolbar = this.toolbar || this.#findOrCreateDefaultToolbar();
6048
+ this.toolbar ??= this.#findOrCreateDefaultToolbar();
5992
6049
  return this.toolbar
5993
6050
  }
5994
6051
 
@@ -6124,6 +6181,7 @@ class LexicalEditorElement extends HTMLElement {
6124
6181
 
6125
6182
  #createEditor() {
6126
6183
  this.editorContentElement ||= this.#createEditorContentElement();
6184
+ this.appendChild(this.editorContentElement);
6127
6185
 
6128
6186
  const editor = buildEditorFromExtensions({
6129
6187
  name: "lexxy/core",
@@ -6173,7 +6231,6 @@ class LexicalEditorElement extends HTMLElement {
6173
6231
  });
6174
6232
  editorContentElement.id = `${this.id}-content`;
6175
6233
  this.#ariaAttributes.forEach(attribute => editorContentElement.setAttribute(attribute.name, attribute.value));
6176
- this.appendChild(editorContentElement);
6177
6234
 
6178
6235
  if (this.getAttribute("tabindex")) {
6179
6236
  editorContentElement.setAttribute("tabindex", this.getAttribute("tabindex"));
@@ -6249,36 +6306,48 @@ class LexicalEditorElement extends HTMLElement {
6249
6306
  }
6250
6307
 
6251
6308
  #registerComponents() {
6309
+ const registered = [];
6310
+
6252
6311
  if (this.supportsRichText) {
6253
- registerRichText(this.editor);
6254
- registerList(this.editor);
6312
+ registered.push(
6313
+ registerRichText(this.editor),
6314
+ registerList(this.editor)
6315
+ );
6255
6316
  this.#registerTableComponents();
6256
6317
  this.#registerCodeHiglightingComponents();
6257
6318
  if (this.supportsMarkdown) {
6258
- registerMarkdownShortcuts(this.editor, TRANSFORMERS);
6259
- registerMarkdownLeadingTagHandler(this.editor, TRANSFORMERS);
6319
+ registered.push(
6320
+ registerMarkdownShortcuts(this.editor, TRANSFORMERS),
6321
+ registerMarkdownLeadingTagHandler(this.editor, TRANSFORMERS)
6322
+ );
6260
6323
  }
6261
6324
  } else {
6262
- registerPlainText(this.editor);
6325
+ registered.push(registerPlainText(this.editor));
6263
6326
  }
6264
6327
  this.historyState = createEmptyHistoryState();
6265
- registerHistory(this.editor, this.historyState, 20);
6328
+ registered.push(registerHistory(this.editor, this.historyState, 20));
6329
+
6330
+ this.#addUnregisterHandler(mergeRegister$1(...registered));
6266
6331
  }
6267
6332
 
6268
6333
  #registerTableComponents() {
6269
- this.tableTools = createElement("lexxy-table-tools");
6270
- 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);
6271
6338
  }
6272
6339
 
6273
6340
  #registerCodeHiglightingComponents() {
6274
6341
  registerCodeHighlighting(this.editor);
6275
- this.codeLanguagePicker = createElement("lexxy-code-language-picker");
6276
- 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);
6277
6346
  }
6278
6347
 
6279
6348
  #handleEnter() {
6280
6349
  // We can't prevent these externally using regular keydown because Lexical handles it first.
6281
- this.editor.registerCommand(
6350
+ this.#addUnregisterHandler(this.editor.registerCommand(
6282
6351
  KEY_ENTER_COMMAND,
6283
6352
  (event) => {
6284
6353
  // Prevent CTRL+ENTER
@@ -6296,12 +6365,17 @@ class LexicalEditorElement extends HTMLElement {
6296
6365
  return false
6297
6366
  },
6298
6367
  COMMAND_PRIORITY_NORMAL
6299
- );
6368
+ ));
6300
6369
  }
6301
6370
 
6302
6371
  #registerFocusEvents() {
6303
6372
  this.addEventListener("focusin", this.#handleFocusIn);
6304
6373
  this.addEventListener("focusout", this.#handleFocusOut);
6374
+
6375
+ this.#addUnregisterHandler(() => {
6376
+ this.removeEventListener("focusin", this.#handleFocusIn);
6377
+ this.removeEventListener("focusout", this.#handleFocusOut);
6378
+ });
6305
6379
  }
6306
6380
 
6307
6381
  #handleFocusIn(event) {
@@ -6344,6 +6418,10 @@ class LexicalEditorElement extends HTMLElement {
6344
6418
  #attachToolbar() {
6345
6419
  if (this.#hasToolbar) {
6346
6420
  this.toolbarElement.setEditor(this);
6421
+ if (typeof this.toolbarElement.dispose === "function") {
6422
+ this.#disposables.push(this.toolbarElement);
6423
+ }
6424
+
6347
6425
  this.extensions.initializeToolbars();
6348
6426
  }
6349
6427
  }
@@ -6353,7 +6431,7 @@ class LexicalEditorElement extends HTMLElement {
6353
6431
  if (typeof toolbarConfig === "string") {
6354
6432
  return document.getElementById(toolbarConfig)
6355
6433
  } else {
6356
- return this.#createDefaultToolbar()
6434
+ return this.querySelector("lexxy-toolbar") ?? this.#createDefaultToolbar()
6357
6435
  }
6358
6436
  }
6359
6437
 
@@ -6383,34 +6461,22 @@ class LexicalEditorElement extends HTMLElement {
6383
6461
  }
6384
6462
 
6385
6463
  #reset() {
6386
- this.#unregisterHandlers();
6387
-
6388
- if (this.editorContentElement) {
6389
- this.editorContentElement.remove();
6390
- this.editorContentElement = null;
6391
- }
6392
-
6393
- this.contents = null;
6394
- this.editor = null;
6464
+ this.#dispose();
6465
+ this.editorContentElement?.remove();
6466
+ this.editorContentElement = null;
6395
6467
 
6396
- if (this.toolbar) {
6397
- if (!this.getAttribute("toolbar")) { this.toolbar.remove(); }
6398
- this.toolbar = null;
6399
- }
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
+ }
6400
6472
 
6401
- if (this.codeLanguagePicker) {
6402
- this.codeLanguagePicker.remove();
6403
- this.codeLanguagePicker = null;
6404
- }
6473
+ #dispose() {
6474
+ this.#unregisterHandlers();
6475
+ document.removeEventListener("turbo:before-cache", this.#handleTurboBeforeCache);
6405
6476
 
6406
- if (this.tableHandler) {
6407
- this.tableHandler.remove();
6408
- this.tableHandler = null;
6477
+ while (this.#disposables.length) {
6478
+ this.#disposables.pop().dispose();
6409
6479
  }
6410
-
6411
- this.selection = null;
6412
-
6413
- document.removeEventListener("turbo:before-cache", this.#handleTurboBeforeCache);
6414
6480
  }
6415
6481
 
6416
6482
  #reconnect() {
@@ -6451,14 +6517,15 @@ class ToolbarDropdown extends HTMLElement {
6451
6517
  connectedCallback() {
6452
6518
  this.container = this.closest("details");
6453
6519
 
6454
- this.container.addEventListener("toggle", this.#handleToggle.bind(this));
6455
- this.container.addEventListener("keydown", this.#handleKeyDown.bind(this));
6520
+ this.container.addEventListener("toggle", this.#handleToggle);
6521
+ this.container.addEventListener("keydown", this.#handleKeyDown);
6456
6522
 
6457
6523
  this.#onToolbarEditor(this.initialize.bind(this));
6458
6524
  }
6459
6525
 
6460
6526
  disconnectedCallback() {
6461
- this.container.removeEventListener("keydown", this.#handleKeyDown.bind(this));
6527
+ this.container?.removeEventListener("toggle", this.#handleToggle);
6528
+ this.container?.removeEventListener("keydown", this.#handleKeyDown);
6462
6529
  }
6463
6530
 
6464
6531
  get toolbar() {
@@ -6483,11 +6550,11 @@ class ToolbarDropdown extends HTMLElement {
6483
6550
  }
6484
6551
 
6485
6552
  async #onToolbarEditor(callback) {
6486
- await this.toolbar.editorConnected;
6553
+ await this.toolbar.editorElement;
6487
6554
  callback();
6488
6555
  }
6489
6556
 
6490
- #handleToggle() {
6557
+ #handleToggle = () => {
6491
6558
  if (this.container.open) {
6492
6559
  this.#handleOpen();
6493
6560
  }
@@ -6498,7 +6565,7 @@ class ToolbarDropdown extends HTMLElement {
6498
6565
  this.#resetTabIndexValues();
6499
6566
  }
6500
6567
 
6501
- #handleKeyDown(event) {
6568
+ #handleKeyDown = (event) => {
6502
6569
  if (event.key === "Escape") {
6503
6570
  event.stopPropagation();
6504
6571
  this.close();
@@ -6526,27 +6593,30 @@ class LinkDropdown extends ToolbarDropdown {
6526
6593
  super.connectedCallback();
6527
6594
  this.input = this.querySelector("input");
6528
6595
 
6529
- this.#registerHandlers();
6596
+ this.container.addEventListener("toggle", this.#handleToggle);
6597
+ this.addEventListener("submit", this.#handleSubmit);
6598
+ this.querySelector("[value='unlink']").addEventListener("click", this.#handleUnlink);
6530
6599
  }
6531
6600
 
6532
- #registerHandlers() {
6533
- this.container.addEventListener("toggle", this.#handleToggle.bind(this));
6534
- this.addEventListener("submit", this.#handleSubmit.bind(this));
6535
- 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();
6536
6606
  }
6537
6607
 
6538
- #handleToggle({ newState }) {
6608
+ #handleToggle = ({ newState }) => {
6539
6609
  this.input.value = this.#selectedLinkUrl;
6540
6610
  this.input.required = newState === "open";
6541
6611
  }
6542
6612
 
6543
- #handleSubmit(event) {
6613
+ #handleSubmit = (event) => {
6544
6614
  const command = event.submitter?.value;
6545
6615
  this.editor.dispatchCommand(command, this.input.value);
6546
6616
  this.close();
6547
6617
  }
6548
6618
 
6549
- #handleUnlink() {
6619
+ #handleUnlink = () => {
6550
6620
  this.editor.dispatchCommand("unlink");
6551
6621
  this.close();
6552
6622
  }
@@ -6581,26 +6651,35 @@ const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
6581
6651
  const NO_STYLE = Symbol("no_style");
6582
6652
 
6583
6653
  class HighlightDropdown extends ToolbarDropdown {
6584
- connectedCallback() {
6585
- super.connectedCallback();
6586
- this.#registerToggleHandler();
6587
- }
6588
-
6589
6654
  initialize() {
6590
6655
  this.#setUpButtons();
6591
6656
  this.#registerButtonHandlers();
6592
6657
  }
6593
6658
 
6594
- #registerToggleHandler() {
6595
- 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();
6596
6668
  }
6597
6669
 
6598
6670
  #registerButtonHandlers() {
6599
- this.#colorButtons.forEach(button => button.addEventListener("click", this.#handleColorButtonClick.bind(this)));
6600
- 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);
6601
6678
  }
6602
6679
 
6603
6680
  #setUpButtons() {
6681
+ this.#buttonContainer.innerHTML = "";
6682
+
6604
6683
  const colorGroups = this.editorElement.config.get("highlight.buttons");
6605
6684
 
6606
6685
  this.#populateButtonGroup("color", colorGroups.color);
@@ -6626,7 +6705,7 @@ class HighlightDropdown extends ToolbarDropdown {
6626
6705
  return button
6627
6706
  }
6628
6707
 
6629
- #handleToggle({ newState }) {
6708
+ #handleToggle = ({ newState }) => {
6630
6709
  if (newState === "open") {
6631
6710
  this.editor.getEditorState().read(() => {
6632
6711
  this.#updateColorButtonStates($getSelection());
@@ -6634,7 +6713,7 @@ class HighlightDropdown extends ToolbarDropdown {
6634
6713
  }
6635
6714
  }
6636
6715
 
6637
- #handleColorButtonClick(event) {
6716
+ #handleColorButtonClick = (event) => {
6638
6717
  event.preventDefault();
6639
6718
 
6640
6719
  const button = event.target.closest(APPLY_HIGHLIGHT_SELECTOR);
@@ -6647,7 +6726,7 @@ class HighlightDropdown extends ToolbarDropdown {
6647
6726
  this.close();
6648
6727
  }
6649
6728
 
6650
- #handleRemoveHighlightClick(event) {
6729
+ #handleRemoveHighlightClick = (event) => {
6651
6730
  event.preventDefault();
6652
6731
 
6653
6732
  this.editor.dispatchCommand("removeHighlight");
@@ -7309,19 +7388,21 @@ class CodeLanguagePicker extends HTMLElement {
7309
7388
  }
7310
7389
 
7311
7390
  disconnectedCallback() {
7391
+ this.dispose();
7392
+ }
7393
+
7394
+ dispose() {
7312
7395
  this.unregisterUpdateListener?.();
7313
7396
  this.unregisterUpdateListener = null;
7314
7397
  }
7315
7398
 
7316
7399
  #attachLanguagePicker() {
7317
- this.languagePickerElement = this.#createLanguagePicker();
7318
-
7319
- this.languagePickerElement.addEventListener("change", () => {
7320
- this.#updateCodeBlockLanguage(this.languagePickerElement.value);
7321
- });
7400
+ this.languagePickerElement = this.#findLanguagePicker() ?? this.#createLanguagePicker();
7401
+ this.append(this.languagePickerElement);
7402
+ }
7322
7403
 
7323
- this.languagePickerElement.setAttribute("nonce", getNonce());
7324
- this.appendChild(this.languagePickerElement);
7404
+ #findLanguagePicker() {
7405
+ return this.querySelector("select")
7325
7406
  }
7326
7407
 
7327
7408
  #createLanguagePicker() {
@@ -7334,6 +7415,12 @@ class CodeLanguagePicker extends HTMLElement {
7334
7415
  selectElement.appendChild(option);
7335
7416
  }
7336
7417
 
7418
+ selectElement.addEventListener("change", () => {
7419
+ this.#updateCodeBlockLanguage(this.languagePickerElement.value);
7420
+ });
7421
+
7422
+ selectElement.setAttribute("nonce", getNonce());
7423
+
7337
7424
  return selectElement
7338
7425
  }
7339
7426
 
@@ -7894,6 +7981,10 @@ class TableTools extends HTMLElement {
7894
7981
  }
7895
7982
 
7896
7983
  disconnectedCallback() {
7984
+ this.dispose();
7985
+ }
7986
+
7987
+ dispose() {
7897
7988
  this.#unregisterKeyboardShortcuts();
7898
7989
 
7899
7990
  this.unregisterUpdateListener?.();
@@ -7918,6 +8009,8 @@ class TableTools extends HTMLElement {
7918
8009
  }
7919
8010
 
7920
8011
  #setUpButtons() {
8012
+ this.innerHTML = "";
8013
+
7921
8014
  this.appendChild(this.#createRowButtonsContainer());
7922
8015
  this.appendChild(this.#createColumnButtonsContainer());
7923
8016
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.1-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",