--- /dev/null
+// 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';
+ },
+};
// A map from regular expressions to paths to transformers
transform: {
"^.+.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
const debugView = document.getElementById('lexical-debug');
if (debugView) {
debugView.hidden = true;
- }
-
- let changeFromLoading = true;
- 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
- // console.log('editorState', editorState.toJSON());
- if (debugView) {
+ editor.registerUpdateListener(({dirtyElements, dirtyLeaves, editorState, prevEditorState}) => {
+ // Debug logic
+ // console.log('editorState', editorState.toJSON());
debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2);
- }
- });
+ });
+ }
// @ts-ignore
window.debugEditorState = () => {
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
* @param callbackFn - A function to run after the editor is focused.
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
import {
+ $getSelection,
$isRangeSelection,
createEditor,
DecoratorNode,
import {resetRandomKey} from '../../LexicalUtils';
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
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 = {
TableRowNode,
AutoLinkNode,
LinkNode,
+ DetailsNode,
TestElementNode,
TestSegmentedNode,
TestExcludeFromCopyElementNode,
...config,
nodes: DEFAULT_NODES.concat(customNodes),
});
+
return editor;
}
});
}
+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 {
if (!$isRangeSelection(selection)) {
throw new Error(`Expected RangeSelection, got ${selection}`);
function formatHtml(s: string): string {
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);
+ }
+ });
}
\ No newline at end of file
it('should be headless environment', async () => {
expect(typeof window === 'undefined').toBe(true);
expect(typeof document === 'undefined').toBe(true);
- expect(typeof navigator === 'undefined').toBe(true);
});
it('can update editor', async () => {
--- /dev/null
+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>`);
+ });
+
+
+ });
+})
\ No newline at end of file
--- /dev/null
+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!');
+ });
+ });
+});
\ No newline at end of file
-import {LexicalEditor} from "lexical";
+import {$getSelection, LexicalEditor} from "lexical";
import {
appendHtmlToEditor,
focusEditor,
window.$events.listen<EditorEventContent>('editor::focus', () => {
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', '');
+ }
+ }
+ });
}
import {EditorFormModal, EditorFormModalDefinition} from "./modals";
import {EditorContainerUiElement, EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
import {EditorDecorator, EditorDecoratorAdapter} from "./decorator";
-import {BaseSelection, LexicalEditor} from "lexical";
+import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
import {DecoratorListener} from "lexical/LexicalEditor";
import type {NodeKey} from "lexical/LexicalNode";
import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";
});
}
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) {