Lexical: Added testing for some added shortcuts
Also: - Added svg loading support (dummy stub) for jest. - Updated headless test case due to node changes. - Split out editor change detected to where appropriate. - Added functions to help with testing, like mocking our context.
This commit is contained in:
		
							parent
							
								
									8486775edf
								
							
						
					
					
						commit
						e50cd33277
					
				| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					// This is a basic transformer stub to help jest handle SVG files.
 | 
				
			||||||
 | 
					// Essentially blanks them since we don't really need to involve them
 | 
				
			||||||
 | 
					// in our tests (yet).
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					    process() {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            code: 'module.exports = \'\';',
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    getCacheKey() {
 | 
				
			||||||
 | 
					        // The output is always the same.
 | 
				
			||||||
 | 
					        return 'svgTransform';
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -185,6 +185,7 @@ const config: Config = {
 | 
				
			||||||
  // A map from regular expressions to paths to transformers
 | 
					  // A map from regular expressions to paths to transformers
 | 
				
			||||||
  transform: {
 | 
					  transform: {
 | 
				
			||||||
    "^.+.tsx?$": ["ts-jest",{}],
 | 
					    "^.+.tsx?$": ["ts-jest",{}],
 | 
				
			||||||
 | 
					    "^.+.svg$": ["<rootDir>/dev/build/svg-blank-transform.js",{}],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
 | 
					  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -75,38 +75,12 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
 | 
				
			||||||
    const debugView = document.getElementById('lexical-debug');
 | 
					    const debugView = document.getElementById('lexical-debug');
 | 
				
			||||||
    if (debugView) {
 | 
					    if (debugView) {
 | 
				
			||||||
        debugView.hidden = true;
 | 
					        debugView.hidden = true;
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let changeFromLoading = true;
 | 
					 | 
				
			||||||
        editor.registerUpdateListener(({dirtyElements, dirtyLeaves, editorState, prevEditorState}) => {
 | 
					        editor.registerUpdateListener(({dirtyElements, dirtyLeaves, editorState, prevEditorState}) => {
 | 
				
			||||||
        // Watch for selection changes to update the UI on change
 | 
					 | 
				
			||||||
        // Used to be done via SELECTION_CHANGE_COMMAND but this would not always emit
 | 
					 | 
				
			||||||
        // for all selection changes, so this proved more reliable.
 | 
					 | 
				
			||||||
        const selectionChange = !(prevEditorState._selection?.is(editorState._selection) || false);
 | 
					 | 
				
			||||||
        if (selectionChange) {
 | 
					 | 
				
			||||||
            editor.update(() => {
 | 
					 | 
				
			||||||
                const selection = $getSelection();
 | 
					 | 
				
			||||||
                context.manager.triggerStateUpdate({
 | 
					 | 
				
			||||||
                    editor, selection,
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Emit change event to component system (for draft detection) on actual user content change
 | 
					 | 
				
			||||||
        if (dirtyElements.size > 0 || dirtyLeaves.size > 0) {
 | 
					 | 
				
			||||||
            if (changeFromLoading) {
 | 
					 | 
				
			||||||
                changeFromLoading = false;
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                window.$events.emit('editor-html-change', '');
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Debug logic
 | 
					            // Debug logic
 | 
				
			||||||
            // console.log('editorState', editorState.toJSON());
 | 
					            // console.log('editorState', editorState.toJSON());
 | 
				
			||||||
        if (debugView) {
 | 
					 | 
				
			||||||
            debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2);
 | 
					            debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2);
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // @ts-ignore
 | 
					    // @ts-ignore
 | 
				
			||||||
    window.debugEditorState = () => {
 | 
					    window.debugEditorState = () => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1188,6 +1188,14 @@ export class LexicalEditor {
 | 
				
			||||||
    updateEditor(this, updateFn, options);
 | 
					    updateEditor(this, updateFn, options);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Helper to run the update and commitUpdates methods in a single call.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  updateAndCommit(updateFn: () => void, options?: EditorUpdateOptions): void {
 | 
				
			||||||
 | 
					    this.update(updateFn, options);
 | 
				
			||||||
 | 
					    this.commitUpdates();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Focuses the editor
 | 
					   * Focuses the editor
 | 
				
			||||||
   * @param callbackFn - A function to run after the editor is focused.
 | 
					   * @param callbackFn - A function to run after the editor is focused.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ import {ListItemNode, ListNode} from '@lexical/list';
 | 
				
			||||||
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
 | 
					import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  $getSelection,
 | 
				
			||||||
  $isRangeSelection,
 | 
					  $isRangeSelection,
 | 
				
			||||||
  createEditor,
 | 
					  createEditor,
 | 
				
			||||||
  DecoratorNode,
 | 
					  DecoratorNode,
 | 
				
			||||||
| 
						 | 
					@ -37,6 +38,10 @@ import {
 | 
				
			||||||
import {resetRandomKey} from '../../LexicalUtils';
 | 
					import {resetRandomKey} from '../../LexicalUtils';
 | 
				
			||||||
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 | 
					import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 | 
				
			||||||
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
 | 
					import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
 | 
				
			||||||
 | 
					import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
 | 
				
			||||||
 | 
					import {EditorUiContext} from "../../../../ui/framework/core";
 | 
				
			||||||
 | 
					import {EditorUIManager} from "../../../../ui/framework/manager";
 | 
				
			||||||
 | 
					import {registerRichText} from "@lexical/rich-text";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TestEnv = {
 | 
					type TestEnv = {
 | 
				
			||||||
| 
						 | 
					@ -420,6 +425,7 @@ const DEFAULT_NODES: NonNullable<ReadonlyArray<Klass<LexicalNode> | LexicalNodeR
 | 
				
			||||||
  TableRowNode,
 | 
					  TableRowNode,
 | 
				
			||||||
  AutoLinkNode,
 | 
					  AutoLinkNode,
 | 
				
			||||||
  LinkNode,
 | 
					  LinkNode,
 | 
				
			||||||
 | 
					  DetailsNode,
 | 
				
			||||||
  TestElementNode,
 | 
					  TestElementNode,
 | 
				
			||||||
  TestSegmentedNode,
 | 
					  TestSegmentedNode,
 | 
				
			||||||
  TestExcludeFromCopyElementNode,
 | 
					  TestExcludeFromCopyElementNode,
 | 
				
			||||||
| 
						 | 
					@ -451,6 +457,7 @@ export function createTestEditor(
 | 
				
			||||||
    ...config,
 | 
					    ...config,
 | 
				
			||||||
    nodes: DEFAULT_NODES.concat(customNodes),
 | 
					    nodes: DEFAULT_NODES.concat(customNodes),
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return editor;
 | 
					  return editor;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -465,6 +472,26 @@ export function createTestHeadlessEditor(
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createTestContext(env: TestEnv): EditorUiContext {
 | 
				
			||||||
 | 
					  const context = {
 | 
				
			||||||
 | 
					    containerDOM: document.createElement('div'),
 | 
				
			||||||
 | 
					    editor: env.editor,
 | 
				
			||||||
 | 
					    editorDOM: document.createElement('div'),
 | 
				
			||||||
 | 
					    error(text: string | Error): void {
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    manager: new EditorUIManager(),
 | 
				
			||||||
 | 
					    options: {},
 | 
				
			||||||
 | 
					    scrollDOM: document.createElement('div'),
 | 
				
			||||||
 | 
					    translate(text: string): string {
 | 
				
			||||||
 | 
					      return "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  context.manager.setContext(context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return context;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function $assertRangeSelection(selection: unknown): RangeSelection {
 | 
					export function $assertRangeSelection(selection: unknown): RangeSelection {
 | 
				
			||||||
  if (!$isRangeSelection(selection)) {
 | 
					  if (!$isRangeSelection(selection)) {
 | 
				
			||||||
    throw new Error(`Expected RangeSelection, got ${selection}`);
 | 
					    throw new Error(`Expected RangeSelection, got ${selection}`);
 | 
				
			||||||
| 
						 | 
					@ -718,3 +745,22 @@ export function expectHtmlToBeEqual(expected: string, actual: string): void {
 | 
				
			||||||
function formatHtml(s: string): string {
 | 
					function formatHtml(s: string): string {
 | 
				
			||||||
  return s.replace(/>\s+</g, '><').replace(/\s*\n\s*/g, ' ').trim();
 | 
					  return s.replace(/>\s+</g, '><').replace(/\s*\n\s*/g, ' ').trim();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function dispatchKeydownEventForNode(node: LexicalNode, editor: LexicalEditor, key: string) {
 | 
				
			||||||
 | 
					  const nodeDomEl = editor.getElementByKey(node.getKey());
 | 
				
			||||||
 | 
					  const event = new KeyboardEvent('keydown', {
 | 
				
			||||||
 | 
					    bubbles: true,
 | 
				
			||||||
 | 
					    cancelable: true,
 | 
				
			||||||
 | 
					    key,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  nodeDomEl?.dispatchEvent(event);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function dispatchKeydownEventForSelectedNode(editor: LexicalEditor, key: string) {
 | 
				
			||||||
 | 
					  editor.getEditorState().read((): void => {
 | 
				
			||||||
 | 
					    const node = $getSelection()?.getNodes()[0] || null;
 | 
				
			||||||
 | 
					    if (node) {
 | 
				
			||||||
 | 
					      dispatchKeydownEventForNode(node, editor, key);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -62,7 +62,6 @@ describe('LexicalHeadlessEditor', () => {
 | 
				
			||||||
  it('should be headless environment', async () => {
 | 
					  it('should be headless environment', async () => {
 | 
				
			||||||
    expect(typeof window === 'undefined').toBe(true);
 | 
					    expect(typeof window === 'undefined').toBe(true);
 | 
				
			||||||
    expect(typeof document === 'undefined').toBe(true);
 | 
					    expect(typeof document === 'undefined').toBe(true);
 | 
				
			||||||
    expect(typeof navigator === 'undefined').toBe(true);
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('can update editor', async () => {
 | 
					  it('can update editor', async () => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					import {dispatchKeydownEventForNode, initializeUnitTest} from "lexical/__tests__/utils";
 | 
				
			||||||
 | 
					import {$createDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
 | 
				
			||||||
 | 
					import {$createParagraphNode, $getRoot, LexicalNode, ParagraphNode} from "lexical";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const editorConfig = Object.freeze({
 | 
				
			||||||
 | 
					    namespace: '',
 | 
				
			||||||
 | 
					    theme: {
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('LexicalDetailsNode tests', () => {
 | 
				
			||||||
 | 
					    initializeUnitTest((testEnv) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        test('createDOM()', () => {
 | 
				
			||||||
 | 
					            const {editor} = testEnv;
 | 
				
			||||||
 | 
					            let html!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            editor.updateAndCommit(() => {
 | 
				
			||||||
 | 
					                const details = $createDetailsNode();
 | 
				
			||||||
 | 
					                html = details.createDOM(editorConfig, editor).outerHTML;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            expect(html).toBe(`<details><summary contenteditable="false"></summary></details>`);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        test('exportDOM()', () => {
 | 
				
			||||||
 | 
					            const {editor} = testEnv;
 | 
				
			||||||
 | 
					            let html!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            editor.updateAndCommit(() => {
 | 
				
			||||||
 | 
					                const details = $createDetailsNode();
 | 
				
			||||||
 | 
					                html = (details.exportDOM(editor).element as HTMLElement).outerHTML;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            expect(html).toBe(`<details><summary></summary></details>`);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,95 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    createTestContext,
 | 
				
			||||||
 | 
					    dispatchKeydownEventForNode,
 | 
				
			||||||
 | 
					    dispatchKeydownEventForSelectedNode,
 | 
				
			||||||
 | 
					    initializeUnitTest
 | 
				
			||||||
 | 
					} from "lexical/__tests__/utils";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    $createParagraphNode, $createTextNode,
 | 
				
			||||||
 | 
					    $getRoot, LexicalNode,
 | 
				
			||||||
 | 
					    ParagraphNode,
 | 
				
			||||||
 | 
					} from "lexical";
 | 
				
			||||||
 | 
					import {$createDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
 | 
				
			||||||
 | 
					import {registerKeyboardHandling} from "../keyboard-handling";
 | 
				
			||||||
 | 
					import {registerRichText} from "@lexical/rich-text";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('Keyboard-handling service tests', () => {
 | 
				
			||||||
 | 
					    initializeUnitTest((testEnv) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        test('Details: down key on last lines creates new sibling node', () => {
 | 
				
			||||||
 | 
					            const {editor} = testEnv;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            registerRichText(editor);
 | 
				
			||||||
 | 
					            registerKeyboardHandling(createTestContext(testEnv));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let lastRootChild!: LexicalNode|null;
 | 
				
			||||||
 | 
					            let detailsPara!: ParagraphNode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            editor.updateAndCommit(() => {
 | 
				
			||||||
 | 
					                const root = $getRoot()
 | 
				
			||||||
 | 
					                const details = $createDetailsNode();
 | 
				
			||||||
 | 
					                detailsPara = $createParagraphNode();
 | 
				
			||||||
 | 
					                details.append(detailsPara);
 | 
				
			||||||
 | 
					                $getRoot().append(details);
 | 
				
			||||||
 | 
					                detailsPara.select();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                lastRootChild = root.getLastChild();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            expect(lastRootChild).toBeInstanceOf(DetailsNode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            dispatchKeydownEventForNode(detailsPara, editor, 'ArrowDown');
 | 
				
			||||||
 | 
					            editor.commitUpdates();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            editor.getEditorState().read(() => {
 | 
				
			||||||
 | 
					                lastRootChild = $getRoot().getLastChild();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            expect(lastRootChild).toBeInstanceOf(ParagraphNode);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        test('Details: enter on last empy block creates new sibling node', () => {
 | 
				
			||||||
 | 
					            const {editor} = testEnv;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            registerRichText(editor);
 | 
				
			||||||
 | 
					            registerKeyboardHandling(createTestContext(testEnv));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let lastRootChild!: LexicalNode|null;
 | 
				
			||||||
 | 
					            let detailsPara!: ParagraphNode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            editor.updateAndCommit(() => {
 | 
				
			||||||
 | 
					                const root = $getRoot()
 | 
				
			||||||
 | 
					                const details = $createDetailsNode();
 | 
				
			||||||
 | 
					                const text = $createTextNode('Hello!');
 | 
				
			||||||
 | 
					                detailsPara = $createParagraphNode();
 | 
				
			||||||
 | 
					                detailsPara.append(text);
 | 
				
			||||||
 | 
					                details.append(detailsPara);
 | 
				
			||||||
 | 
					                $getRoot().append(details);
 | 
				
			||||||
 | 
					                text.selectEnd();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                lastRootChild = root.getLastChild();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            expect(lastRootChild).toBeInstanceOf(DetailsNode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            dispatchKeydownEventForNode(detailsPara, editor, 'Enter');
 | 
				
			||||||
 | 
					            editor.commitUpdates();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            dispatchKeydownEventForSelectedNode(editor, 'Enter');
 | 
				
			||||||
 | 
					            editor.commitUpdates();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let detailsChildren!: LexicalNode[];
 | 
				
			||||||
 | 
					            let lastDetailsText!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            editor.getEditorState().read(() => {
 | 
				
			||||||
 | 
					                detailsChildren = (lastRootChild as DetailsNode).getChildren();
 | 
				
			||||||
 | 
					                lastRootChild = $getRoot().getLastChild();
 | 
				
			||||||
 | 
					                lastDetailsText = detailsChildren[0].getTextContent();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            expect(lastRootChild).toBeInstanceOf(ParagraphNode);
 | 
				
			||||||
 | 
					            expect(detailsChildren).toHaveLength(1);
 | 
				
			||||||
 | 
					            expect(lastDetailsText).toBe('Hello!');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import {LexicalEditor} from "lexical";
 | 
					import {$getSelection, LexicalEditor} from "lexical";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    appendHtmlToEditor,
 | 
					    appendHtmlToEditor,
 | 
				
			||||||
    focusEditor,
 | 
					    focusEditor,
 | 
				
			||||||
| 
						 | 
					@ -40,4 +40,16 @@ export function listen(editor: LexicalEditor): void {
 | 
				
			||||||
    window.$events.listen<EditorEventContent>('editor::focus', () => {
 | 
					    window.$events.listen<EditorEventContent>('editor::focus', () => {
 | 
				
			||||||
        focusEditor(editor);
 | 
					        focusEditor(editor);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let changeFromLoading = true;
 | 
				
			||||||
 | 
					    editor.registerUpdateListener(({dirtyElements, dirtyLeaves, editorState, prevEditorState}) => {
 | 
				
			||||||
 | 
					        // Emit change event to component system (for draft detection) on actual user content change
 | 
				
			||||||
 | 
					        if (dirtyElements.size > 0 || dirtyLeaves.size > 0) {
 | 
				
			||||||
 | 
					            if (changeFromLoading) {
 | 
				
			||||||
 | 
					                changeFromLoading = false;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                window.$events.emit('editor-html-change', '');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import {EditorFormModal, EditorFormModalDefinition} from "./modals";
 | 
					import {EditorFormModal, EditorFormModalDefinition} from "./modals";
 | 
				
			||||||
import {EditorContainerUiElement, EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
 | 
					import {EditorContainerUiElement, EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
 | 
				
			||||||
import {EditorDecorator, EditorDecoratorAdapter} from "./decorator";
 | 
					import {EditorDecorator, EditorDecoratorAdapter} from "./decorator";
 | 
				
			||||||
import {BaseSelection, LexicalEditor} from "lexical";
 | 
					import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
 | 
				
			||||||
import {DecoratorListener} from "lexical/LexicalEditor";
 | 
					import {DecoratorListener} from "lexical/LexicalEditor";
 | 
				
			||||||
import type {NodeKey} from "lexical/LexicalNode";
 | 
					import type {NodeKey} from "lexical/LexicalNode";
 | 
				
			||||||
import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";
 | 
					import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";
 | 
				
			||||||
| 
						 | 
					@ -231,6 +231,22 @@ export class EditorUIManager {
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        editor.registerDecoratorListener(domDecorateListener);
 | 
					        editor.registerDecoratorListener(domDecorateListener);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Watch for changes to update local state
 | 
				
			||||||
 | 
					        editor.registerUpdateListener(({editorState, prevEditorState}) => {
 | 
				
			||||||
 | 
					            // Watch for selection changes to update the UI on change
 | 
				
			||||||
 | 
					            // Used to be done via SELECTION_CHANGE_COMMAND but this would not always emit
 | 
				
			||||||
 | 
					            // for all selection changes, so this proved more reliable.
 | 
				
			||||||
 | 
					            const selectionChange = !(prevEditorState._selection?.is(editorState._selection) || false);
 | 
				
			||||||
 | 
					            if (selectionChange) {
 | 
				
			||||||
 | 
					                editor.update(() => {
 | 
				
			||||||
 | 
					                    const selection = $getSelection();
 | 
				
			||||||
 | 
					                    this.triggerStateUpdate({
 | 
				
			||||||
 | 
					                        editor, selection,
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected setupEventListeners(context: EditorUiContext) {
 | 
					    protected setupEventListeners(context: EditorUiContext) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue