chore: allow tsc to typecheck tests, fix type issues in those tests (#5982)

This commit is contained in:
Bob Ippolito
2024-05-02 14:58:18 -07:00
committed by GitHub
parent fdce31d056
commit a0bb9b056a
25 changed files with 565 additions and 401 deletions

View File

@ -178,7 +178,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode(); const code = $createCodeNode();
root.append(code); root.append(code);
code.selectStart(); code.selectStart();
$getSelection().insertText('function'); $getSelection()!.insertText('function');
}); });
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent()); await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
expect(testEnv.innerHTML).toBe( expect(testEnv.innerHTML).toBe(
@ -187,12 +187,12 @@ describe('LexicalCodeNode tests', () => {
// CodeNode should only render diffs, make sure that the TabNode is not cloned when // CodeNode should only render diffs, make sure that the TabNode is not cloned when
// appending more text // appending more text
let tabKey; let tabKey: string;
await editor.update(() => { await editor.update(() => {
tabKey = $dfs() tabKey = $dfs()
.find(({node}) => $isTabNode(node)) .find(({node}) => $isTabNode(node))!
.node.getKey(); .node.getKey();
$getSelection().insertText('foo'); $getSelection()!.insertText('foo');
}); });
expect( expect(
editor.getEditorState().read(() => { editor.getEditorState().read(() => {
@ -214,7 +214,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode(); const code = $createCodeNode();
root.append(code); root.append(code);
code.selectStart(); code.selectStart();
$getSelection().insertText('function'); $getSelection()!.insertText('function');
}); });
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection // TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
await editor.update(() => { await editor.update(() => {
@ -238,7 +238,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode(); const code = $createCodeNode();
root.append(code); root.append(code);
code.selectStart(); code.selectStart();
$getSelection().insertText('function'); $getSelection()!.insertText('function');
}); });
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection // TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
await editor.update(() => { await editor.update(() => {
@ -253,8 +253,8 @@ describe('LexicalCodeNode tests', () => {
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();
const codeTab = root.getFirstDescendant(); const codeTab = root.getFirstDescendant()!;
const codeText = root.getLastDescendant(); const codeText = root.getLastDescendant()!;
const selection = $createRangeSelection(); const selection = $createRangeSelection();
selection.anchor.set(codeTab.getKey(), 0, 'text'); selection.anchor.set(codeTab.getKey(), 0, 'text');
selection.focus.set(codeText.getKey(), 'function'.length, 'text'); selection.focus.set(codeText.getKey(), 'function'.length, 'text');
@ -275,7 +275,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode(); const code = $createCodeNode();
root.append(code); root.append(code);
code.selectStart(); code.selectStart();
$getSelection().insertText('function'); $getSelection()!.insertText('function');
}); });
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection // TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
await editor.update(() => { await editor.update(() => {
@ -290,8 +290,8 @@ describe('LexicalCodeNode tests', () => {
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();
const codeTab = root.getFirstDescendant(); const codeTab = root.getFirstDescendant()!;
const codeText = root.getLastDescendant(); const codeText = root.getLastDescendant()!;
const selection = $createRangeSelection(); const selection = $createRangeSelection();
selection.anchor.set(codeTab.getKey(), 0, 'text'); selection.anchor.set(codeTab.getKey(), 0, 'text');
selection.focus.set(codeText.getKey(), 0, 'text'); selection.focus.set(codeText.getKey(), 0, 'text');
@ -313,12 +313,12 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode(); const code = $createCodeNode();
root.append(code); root.append(code);
code.selectStart(); code.selectStart();
$getSelection().insertRawText('hello\tworld\nhello\tworld'); $getSelection()!.insertRawText('hello\tworld\nhello\tworld');
}); });
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection // TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
await editor.update(() => { await editor.update(() => {
const firstCodeText = $getRoot().getFirstDescendant(); const firstCodeText = $getRoot().getFirstDescendant()!;
const lastCodeText = $getRoot().getLastDescendant(); const lastCodeText = $getRoot().getLastDescendant()!;
const selection = $createRangeSelection(); const selection = $createRangeSelection();
selection.anchor.set(firstCodeText.getKey(), 1, 'text'); selection.anchor.set(firstCodeText.getKey(), 1, 'text');
selection.focus.set(lastCodeText.getKey(), 1, 'text'); selection.focus.set(lastCodeText.getKey(), 1, 'text');
@ -347,7 +347,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode(); const code = $createCodeNode();
root.append(code); root.append(code);
code.selectStart(); code.selectStart();
$getSelection().insertRawText('hello\n'); $getSelection()!.insertRawText('hello\n');
}); });
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent()); await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
expect(testEnv.innerHTML) expect(testEnv.innerHTML)
@ -365,7 +365,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode(); const code = $createCodeNode();
root.append(code); root.append(code);
code.selectStart(); code.selectStart();
$getSelection().insertRawText('\thello'); $getSelection()!.insertRawText('\thello');
}); });
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection // TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
await editor.update(() => { await editor.update(() => {
@ -389,7 +389,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode(); const code = $createCodeNode();
root.append(code); root.append(code);
code.selectStart(); code.selectStart();
$getSelection().insertRawText('abc\tdef\nghi\tjkl'); $getSelection()!.insertRawText('abc\tdef\nghi\tjkl');
}); });
const keyEvent = new KeyboardEventMock(); const keyEvent = new KeyboardEventMock();
keyEvent.altKey = true; keyEvent.altKey = true;
@ -409,16 +409,16 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode(); const code = $createCodeNode();
root.append(code); root.append(code);
code.selectStart(); code.selectStart();
$getSelection().insertRawText('abc\tdef\nghi\tjkl\nmno\tpqr'); $getSelection()!.insertRawText('abc\tdef\nghi\tjkl\nmno\tpqr');
}); });
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection // TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
await editor.update(() => { await editor.update(() => {
const firstCodeText = $getRoot().getFirstDescendant(); const firstCodeText = $getRoot().getFirstDescendant()!;
const secondCodeText = firstCodeText const secondCodeText = firstCodeText
.getNextSibling() // tab .getNextSibling()! // tab
.getNextSibling() // def .getNextSibling()! // def
.getNextSibling() // linebreak .getNextSibling()! // linebreak
.getNextSibling(); // ghi; .getNextSibling()!; // ghi;
const selection = $createRangeSelection(); const selection = $createRangeSelection();
selection.anchor.set(firstCodeText.getKey(), 1, 'text'); selection.anchor.set(firstCodeText.getKey(), 1, 'text');
selection.focus.set(secondCodeText.getKey(), 1, 'text'); selection.focus.set(secondCodeText.getKey(), 1, 'text');
@ -455,7 +455,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode(); const code = $createCodeNode();
root.append(code); root.append(code);
code.selectStart(); code.selectStart();
const selection = $getSelection(); const selection = $getSelection()!;
if (tabOrSpaces === 'tab') { if (tabOrSpaces === 'tab') {
selection.insertRawText('\t\tfunction foo\n\t\tfunction bar'); selection.insertRawText('\t\tfunction foo\n\t\tfunction bar');
} else { } else {
@ -570,7 +570,7 @@ describe('LexicalCodeNode tests', () => {
const firstChild = code.getFirstChild(); const firstChild = code.getFirstChild();
invariant($isTextNode(firstChild)); invariant($isTextNode(firstChild));
if (tabOrSpaces === 'tab') { if (tabOrSpaces === 'tab') {
firstChild.getNextSibling().selectNext(0, 0); firstChild.getNextSibling()!.selectNext(0, 0);
} else { } else {
firstChild.select(4, 4); firstChild.select(4, 4);
} }
@ -605,7 +605,7 @@ describe('LexicalCodeNode tests', () => {
$isLineBreakNode(dfsNode.node), $isLineBreakNode(dfsNode.node),
)[0].node; )[0].node;
if (tabOrSpaces === 'tab') { if (tabOrSpaces === 'tab') {
const firstTab = linebreak.getNextSibling(); const firstTab = linebreak.getNextSibling()!;
firstTab.selectNext(); firstTab.selectNext();
} else { } else {
linebreak.selectNext(4, 4); linebreak.selectNext(4, 4);
@ -687,7 +687,7 @@ describe('LexicalCodeNode tests', () => {
$isLineBreakNode(dfsNode.node), $isLineBreakNode(dfsNode.node),
)[0].node; )[0].node;
if (tabOrSpaces === 'tab') { if (tabOrSpaces === 'tab') {
const firstTab = linebreak.getNextSibling(); const firstTab = linebreak.getNextSibling()!;
firstTab.selectNext(0, 0); firstTab.selectNext(0, 0);
} else { } else {
linebreak.selectNext(2, 2); linebreak.selectNext(2, 2);

View File

@ -16,7 +16,7 @@
* *
*/ */
import type {RangeSelection} from 'lexical'; import type {EditorState, LexicalEditor, RangeSelection} from 'lexical';
import {$generateHtmlFromNodes} from '@lexical/html'; import {$generateHtmlFromNodes} from '@lexical/html';
import {JSDOM} from 'jsdom'; import {JSDOM} from 'jsdom';
@ -33,14 +33,17 @@ import {
import {createHeadlessEditor} from '../..'; import {createHeadlessEditor} from '../..';
describe('LexicalHeadlessEditor', () => { describe('LexicalHeadlessEditor', () => {
let editor; let editor: LexicalEditor;
async function update(updateFn) { async function update(updateFn: () => void) {
editor.update(updateFn); editor.update(updateFn);
await Promise.resolve(); await Promise.resolve();
} }
function assertEditorState(editorState, nodes) { function assertEditorState(
editorState: EditorState,
nodes: Record<string, unknown>[],
) {
const nodesFromState = Array.from(editorState._nodeMap.values()); const nodesFromState = Array.from(editorState._nodeMap.values());
expect(nodesFromState).toEqual( expect(nodesFromState).toEqual(
nodes.map((node) => expect.objectContaining(node)), nodes.map((node) => expect.objectContaining(node)),

View File

@ -34,12 +34,12 @@ import {
} from 'lexical/src'; } from 'lexical/src';
import {createTestEditor, TestComposer} from 'lexical/src/__tests__/utils'; import {createTestEditor, TestComposer} from 'lexical/src/__tests__/utils';
import React from 'react'; import React from 'react';
import {createRoot} from 'react-dom/client'; import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils'; import * as ReactTestUtils from 'react-dom/test-utils';
describe('LexicalHistory tests', () => { describe('LexicalHistory tests', () => {
let container: HTMLDivElement | null = null; let container: HTMLDivElement | null = null;
let reactRoot; let reactRoot: Root;
beforeEach(() => { beforeEach(() => {
container = document.createElement('div'); container = document.createElement('div');

View File

@ -6,7 +6,12 @@
* *
*/ */
import {$createParagraphNode, $getRoot, TextNode} from 'lexical'; import {
$createParagraphNode,
$createRangeSelection,
$getRoot,
TextNode,
} from 'lexical';
import { import {
expectHtmlToBeEqual, expectHtmlToBeEqual,
html, html,
@ -147,10 +152,10 @@ describe('LexicalListItemNode tests', () => {
}); });
describe('ListItemNode.replace()', () => { describe('ListItemNode.replace()', () => {
let listNode; let listNode: ListNode;
let listItemNode1; let listItemNode1: ListItemNode;
let listItemNode2; let listItemNode2: ListItemNode;
let listItemNode3; let listItemNode3: ListItemNode;
beforeEach(async () => { beforeEach(async () => {
const {editor} = testEnv; const {editor} = testEnv;
@ -391,7 +396,7 @@ describe('LexicalListItemNode tests', () => {
// - B // - B
test('siblings are not nested', async () => { test('siblings are not nested', async () => {
const {editor} = testEnv; const {editor} = testEnv;
let x; let x: ListItemNode;
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();
@ -459,7 +464,7 @@ describe('LexicalListItemNode tests', () => {
// - B // - B
test('the previous sibling is nested', async () => { test('the previous sibling is nested', async () => {
const {editor} = testEnv; const {editor} = testEnv;
let x; let x: ListItemNode;
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();
@ -539,7 +544,7 @@ describe('LexicalListItemNode tests', () => {
// - B // - B
test('the next sibling is nested', async () => { test('the next sibling is nested', async () => {
const {editor} = testEnv; const {editor} = testEnv;
let x; let x: ListItemNode;
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();
@ -619,7 +624,7 @@ describe('LexicalListItemNode tests', () => {
// - B // - B
test('both siblings are nested', async () => { test('both siblings are nested', async () => {
const {editor} = testEnv; const {editor} = testEnv;
let x; let x: ListItemNode;
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();
@ -708,7 +713,7 @@ describe('LexicalListItemNode tests', () => {
// - B // - B
test('the previous sibling is nested deeper than the next sibling', async () => { test('the previous sibling is nested deeper than the next sibling', async () => {
const {editor} = testEnv; const {editor} = testEnv;
let x; let x: ListItemNode;
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();
@ -818,7 +823,7 @@ describe('LexicalListItemNode tests', () => {
// - B2 // - B2
test('the next sibling is nested deeper than the previous sibling', async () => { test('the next sibling is nested deeper than the previous sibling', async () => {
const {editor} = testEnv; const {editor} = testEnv;
let x; let x: ListItemNode;
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();
@ -929,7 +934,7 @@ describe('LexicalListItemNode tests', () => {
// - B2 // - B2
test('both siblings are deeply nested', async () => { test('both siblings are deeply nested', async () => {
const {editor} = testEnv; const {editor} = testEnv;
let x; let x: ListItemNode;
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();
@ -1052,10 +1057,10 @@ describe('LexicalListItemNode tests', () => {
}); });
describe('ListItemNode.insertNewAfter(): non-empty list items', () => { describe('ListItemNode.insertNewAfter(): non-empty list items', () => {
let listNode; let listNode: ListNode;
let listItemNode1; let listItemNode1: ListItemNode;
let listItemNode2; let listItemNode2: ListItemNode;
let listItemNode3; let listItemNode3: ListItemNode;
beforeEach(async () => { beforeEach(async () => {
const {editor} = testEnv; const {editor} = testEnv;
@ -1103,7 +1108,7 @@ describe('LexicalListItemNode tests', () => {
const {editor} = testEnv; const {editor} = testEnv;
await editor.update(() => { await editor.update(() => {
listItemNode1.insertNewAfter(); listItemNode1.insertNewAfter($createRangeSelection());
}); });
expectHtmlToBeEqual( expectHtmlToBeEqual(
@ -1134,7 +1139,7 @@ describe('LexicalListItemNode tests', () => {
const {editor} = testEnv; const {editor} = testEnv;
await editor.update(() => { await editor.update(() => {
listItemNode3.insertNewAfter(); listItemNode3.insertNewAfter($createRangeSelection());
}); });
expectHtmlToBeEqual( expectHtmlToBeEqual(
@ -1165,7 +1170,7 @@ describe('LexicalListItemNode tests', () => {
const {editor} = testEnv; const {editor} = testEnv;
await editor.update(() => { await editor.update(() => {
listItemNode3.insertNewAfter(); listItemNode3.insertNewAfter($createRangeSelection());
}); });
expectHtmlToBeEqual( expectHtmlToBeEqual(
@ -1217,7 +1222,7 @@ describe('LexicalListItemNode tests', () => {
); );
await editor.update(() => { await editor.update(() => {
listItemNode1.insertNewAfter(); listItemNode1.insertNewAfter($createRangeSelection());
}); });
expectHtmlToBeEqual( expectHtmlToBeEqual(
@ -1264,9 +1269,9 @@ describe('LexicalListItemNode tests', () => {
}); });
describe('ListItemNode.setIndent()', () => { describe('ListItemNode.setIndent()', () => {
let listNode; let listNode: ListNode;
let listItemNode1; let listItemNode1: ListItemNode;
let listItemNode2; let listItemNode2: ListItemNode;
beforeEach(async () => { beforeEach(async () => {
const {editor} = testEnv; const {editor} = testEnv;
@ -1296,7 +1301,7 @@ describe('LexicalListItemNode tests', () => {
}); });
expectHtmlToBeEqual( expectHtmlToBeEqual(
editor.getRootElement().innerHTML, editor.getRootElement()!.innerHTML,
html` html`
<ul> <ul>
<li value="1"> <li value="1">
@ -1330,7 +1335,7 @@ describe('LexicalListItemNode tests', () => {
}); });
expectHtmlToBeEqual( expectHtmlToBeEqual(
editor.getRootElement().innerHTML, editor.getRootElement()!.innerHTML,
html` html`
<ul> <ul>
<li value="1" dir="ltr"> <li value="1" dir="ltr">

View File

@ -238,7 +238,7 @@ describe('LexicalListNode tests', () => {
expect(listNode.append(...nodesToAppend)).toBe(listNode); expect(listNode.append(...nodesToAppend)).toBe(listNode);
expect($isListItemNode(listNode.getFirstChild())).toBe(true); expect($isListItemNode(listNode.getFirstChild())).toBe(true);
expect(listNode.getFirstChild<ListItemNode>().getFirstChild()).toBe( expect(listNode.getFirstChild<ListItemNode>()!.getFirstChild()).toBe(
nestedListNode, nestedListNode,
); );
}); });

View File

@ -8,10 +8,10 @@
import {$createTextNode, $getRoot, ParagraphNode, TextNode} from 'lexical'; import {$createTextNode, $getRoot, ParagraphNode, TextNode} from 'lexical';
import {createTestConnection, waitForReact} from './utils'; import {Client, createTestConnection, waitForReact} from './utils';
describe('Collaboration', () => { describe('Collaboration', () => {
let container = null; let container: null | HTMLDivElement = null;
beforeEach(() => { beforeEach(() => {
container = document.createElement('div'); container = document.createElement('div');
@ -19,11 +19,11 @@ describe('Collaboration', () => {
}); });
afterEach(() => { afterEach(() => {
document.body.removeChild(container); document.body.removeChild(container!);
container = null; container = null;
}); });
async function expectCorrectInitialContent(client1, client2) { async function expectCorrectInitialContent(client1: Client, client2: Client) {
// Should be empty, as client has not yet updated // Should be empty, as client has not yet updated
expect(client1.getHTML()).toEqual(''); expect(client1.getHTML()).toEqual('');
expect(client1.getHTML()).toEqual(client2.getHTML()); expect(client1.getHTML()).toEqual(client2.getHTML());
@ -42,8 +42,8 @@ describe('Collaboration', () => {
const client1 = connector.createClient('1'); const client1 = connector.createClient('1');
const client2 = connector.createClient('2'); const client2 = connector.createClient('2');
client1.start(container); client1.start(container!);
client2.start(container); client2.start(container!);
await expectCorrectInitialContent(client1, client2); await expectCorrectInitialContent(client1, client2);
@ -56,7 +56,7 @@ describe('Collaboration', () => {
const text = $createTextNode('Hello world'); const text = $createTextNode('Hello world');
paragraph.append(text); paragraph!.append(text);
}); });
}); });
@ -71,8 +71,8 @@ describe('Collaboration', () => {
client2.update(() => { client2.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
const text = paragraph.getFirstChild<TextNode>(); const text = paragraph.getFirstChild<TextNode>()!;
text.spliceText(6, 5, 'metaverse'); text.spliceText(6, 5, 'metaverse');
}); });
@ -97,8 +97,8 @@ describe('Collaboration', () => {
const client1 = connector.createClient('1'); const client1 = connector.createClient('1');
const client2 = connector.createClient('2'); const client2 = connector.createClient('2');
client1.start(container); client1.start(container!);
client2.start(container); client2.start(container!);
await expectCorrectInitialContent(client1, client2); await expectCorrectInitialContent(client1, client2);
@ -109,7 +109,7 @@ describe('Collaboration', () => {
client1.update(() => { client1.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
const text = $createTextNode('Hello world'); const text = $createTextNode('Hello world');
paragraph.append(text); paragraph.append(text);
@ -125,7 +125,7 @@ describe('Collaboration', () => {
client2.update(() => { client2.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
const text = $createTextNode('Hello world'); const text = $createTextNode('Hello world');
paragraph.append(text); paragraph.append(text);
@ -157,8 +157,8 @@ describe('Collaboration', () => {
client1.update(() => { client1.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
const text = paragraph.getFirstChild<TextNode>(); const text = paragraph.getFirstChild<TextNode>()!;
text.spliceText(11, 11, ''); text.spliceText(11, 11, '');
}); });
@ -175,8 +175,8 @@ describe('Collaboration', () => {
client2.update(() => { client2.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
const text = paragraph.getFirstChild<TextNode>(); const text = paragraph.getFirstChild<TextNode>()!;
text.spliceText(11, 11, '!'); text.spliceText(11, 11, '!');
}); });
@ -203,8 +203,8 @@ describe('Collaboration', () => {
const connector = createTestConnection(); const connector = createTestConnection();
const client1 = connector.createClient('1'); const client1 = connector.createClient('1');
const client2 = connector.createClient('2'); const client2 = connector.createClient('2');
client1.start(container); client1.start(container!);
client2.start(container); client2.start(container!);
await expectCorrectInitialContent(client1, client2); await expectCorrectInitialContent(client1, client2);
@ -213,7 +213,7 @@ describe('Collaboration', () => {
client1.update(() => { client1.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
const text = $createTextNode('Hello world'); const text = $createTextNode('Hello world');
paragraph.append(text); paragraph.append(text);
}); });
@ -235,8 +235,8 @@ describe('Collaboration', () => {
client1.update(() => { client1.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
paragraph.getFirstChild().remove(); paragraph.getFirstChild()!.remove();
}); });
}); });
@ -250,9 +250,9 @@ describe('Collaboration', () => {
client2.update(() => { client2.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
paragraph.getFirstChild<TextNode>().spliceText(11, 0, 'Hello world'); paragraph.getFirstChild<TextNode>()!.spliceText(11, 0, 'Hello world');
}); });
}); });
@ -297,15 +297,15 @@ describe('Collaboration', () => {
uuid: Math.floor(Math.random() * 10000), uuid: Math.floor(Math.random() * 10000),
}; };
client1.start(container, awarenessData1); client1.start(container!, awarenessData1);
client2.start(container, awarenessData2); client2.start(container!, awarenessData2);
await expectCorrectInitialContent(client1, client2); await expectCorrectInitialContent(client1, client2);
expect(client1.awareness.getLocalState().awarenessData).toEqual( expect(client1.awareness.getLocalState()!.awarenessData).toEqual(
awarenessData1, awarenessData1,
); );
expect(client2.awareness.getLocalState().awarenessData).toEqual( expect(client2.awareness.getLocalState()!.awarenessData).toEqual(
awarenessData2, awarenessData2,
); );

View File

@ -52,11 +52,11 @@ const $createSelectionByPath = ({
const root = $getRoot(); const root = $getRoot();
const anchorNode = anchorPath.reduce( const anchorNode = anchorPath.reduce(
(node, index) => node.getChildAtIndex(index), (node, index) => node.getChildAtIndex(index)!,
root, root,
); );
const focusNode = focusPath.reduce( const focusNode = focusPath.reduce(
(node, index) => node.getChildAtIndex(index), (node, index) => node.getChildAtIndex(index)!,
root, root,
); );
@ -95,11 +95,11 @@ const $replaceTextByPath = ({
focusOffset, focusOffset,
focusPath, focusPath,
}); });
selection.insertText(text); selection.insertText(text!);
}; };
describe('CollaborationWithCollisions', () => { describe('CollaborationWithCollisions', () => {
let container = null; let container: HTMLDivElement | null = null;
beforeEach(() => { beforeEach(() => {
container = document.createElement('div'); container = document.createElement('div');
@ -107,7 +107,7 @@ describe('CollaborationWithCollisions', () => {
}); });
afterEach(() => { afterEach(() => {
document.body.removeChild(container); document.body.removeChild(container!);
container = null; container = null;
}); });
@ -131,7 +131,7 @@ describe('CollaborationWithCollisions', () => {
}, },
() => { () => {
// Second client deletes first paragraph // Second client deletes first paragraph
$getRoot().getFirstChild().remove(); $getRoot().getFirstChild()!.remove();
}, },
], ],
expectedHTML: null, expectedHTML: null,
@ -152,7 +152,7 @@ describe('CollaborationWithCollisions', () => {
}, },
() => { () => {
// Second client deletes first paragraph // Second client deletes first paragraph
$getRoot().getFirstChild().remove(); $getRoot().getFirstChild()!.remove();
}, },
], ],
expectedHTML: null, expectedHTML: null,
@ -199,7 +199,7 @@ describe('CollaborationWithCollisions', () => {
const connection = createTestConnection(); const connection = createTestConnection();
const clients = createAndStartClients( const clients = createAndStartClients(
connection, connection,
container, container!,
testCase.clients.length, testCase.clients.length,
); );

View File

@ -7,14 +7,15 @@
*/ */
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {createRoot} from 'react-dom/client'; import * as React from 'react';
import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils'; import * as ReactTestUtils from 'react-dom/test-utils';
import {LexicalComposer} from '../../LexicalComposer'; import {LexicalComposer} from '../../LexicalComposer';
describe('LexicalNodeHelpers tests', () => { describe('LexicalNodeHelpers tests', () => {
let container = null; let container: HTMLDivElement | null = null;
let reactRoot; let reactRoot: Root;
beforeEach(() => { beforeEach(() => {
container = document.createElement('div'); container = document.createElement('div');
@ -23,7 +24,7 @@ describe('LexicalNodeHelpers tests', () => {
}); });
afterEach(() => { afterEach(() => {
document.body.removeChild(container); document.body.removeChild(container!);
container = null; container = null;
jest.restoreAllMocks(); jest.restoreAllMocks();

View File

@ -22,8 +22,10 @@ import {
$getRoot, $getRoot,
$getSelection, $getSelection,
$isRangeSelection, $isRangeSelection,
LexicalEditor,
} from 'lexical'; } from 'lexical';
import {createRoot} from 'react-dom/client'; import * as React from 'react';
import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils'; import * as ReactTestUtils from 'react-dom/test-utils';
import {LexicalComposer} from '../../LexicalComposer'; import {LexicalComposer} from '../../LexicalComposer';
@ -48,8 +50,8 @@ const RICH_TEXT_NODES = [
]; ];
describe('LexicalNodeHelpers tests', () => { describe('LexicalNodeHelpers tests', () => {
let container = null; let container: HTMLDivElement | null = null;
let reactRoot; let reactRoot: Root;
beforeEach(() => { beforeEach(() => {
container = document.createElement('div'); container = document.createElement('div');
@ -58,7 +60,7 @@ describe('LexicalNodeHelpers tests', () => {
}); });
afterEach(() => { afterEach(() => {
document.body.removeChild(container); document.body.removeChild(container!);
container = null; container = null;
jest.restoreAllMocks(); jest.restoreAllMocks();
@ -113,7 +115,7 @@ describe('LexicalNodeHelpers tests', () => {
reactRoot.render(<App />); reactRoot.render(<App />);
}); });
const text = editor.getEditorState().read($rootTextContent); const text = editor!.getEditorState().read($rootTextContent);
expect(text).toBe('foo'); expect(text).toBe('foo');
}); });
} }
@ -164,9 +166,9 @@ describe('LexicalNodeHelpers tests', () => {
reactRoot.render(<App />); reactRoot.render(<App />);
}); });
await editor.focus(); await editor!.focus();
await editor.getEditorState().read(() => { await editor!.getEditorState().read(() => {
expect($rootTextContent()).toBe('foo'); expect($rootTextContent()).toBe('foo');
const selection = $getSelection(); const selection = $getSelection();
@ -183,7 +185,7 @@ describe('LexicalNodeHelpers tests', () => {
for (const plugin of ['PlainTextPlugin', 'RichTextPlugin']) { for (const plugin of ['PlainTextPlugin', 'RichTextPlugin']) {
it(`${plugin} can hide placeholder when non-editable`, async () => { it(`${plugin} can hide placeholder when non-editable`, async () => {
let editor; let editor: LexicalEditor;
function GrabEditor() { function GrabEditor() {
[editor] = useLexicalComposerContext(); [editor] = useLexicalComposerContext();
@ -232,7 +234,7 @@ describe('LexicalNodeHelpers tests', () => {
}); });
function placeholderText() { function placeholderText() {
const placeholderContainer = container.querySelector('.placeholder'); const placeholderContainer = container!.querySelector('.placeholder');
return placeholderContainer && placeholderContainer.textContent; return placeholderContainer && placeholderContainer.textContent;
} }

View File

@ -54,7 +54,7 @@ describe('LexicalNodeHelpers tests', () => {
paragraph.append(overflowRight); paragraph.append(overflowRight);
}); });
return [overflowLeftKey, overflowRightKey]; return [overflowLeftKey!, overflowRightKey!];
} }
it('merges an empty overflow node (left overflow selected)', async () => { it('merges an empty overflow node (left overflow selected)', async () => {
@ -63,8 +63,9 @@ describe('LexicalNodeHelpers tests', () => {
await initializeEditorWithLeftRightOverflowNodes(); await initializeEditorWithLeftRightOverflowNodes();
await editor.update(() => { await editor.update(() => {
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey); const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey)!;
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey); const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
const text1 = $createTextNode('1'); const text1 = $createTextNode('1');
const text2 = $createTextNode('2'); const text2 = $createTextNode('2');
@ -77,8 +78,9 @@ describe('LexicalNodeHelpers tests', () => {
}); });
await editor.update(() => { await editor.update(() => {
const paragraph = $getRoot().getFirstChild<ParagraphNode>(); const paragraph = $getRoot().getFirstChild<ParagraphNode>()!;
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey); const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
mergePrevious(overflowRight); mergePrevious(overflowRight);
@ -110,8 +112,9 @@ describe('LexicalNodeHelpers tests', () => {
let text1Key: NodeKey; let text1Key: NodeKey;
await editor.update(() => { await editor.update(() => {
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey); const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey)!;
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey); const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
const text1 = $createTextNode('1'); const text1 = $createTextNode('1');
const text2 = $createTextNode('2'); const text2 = $createTextNode('2');
@ -133,9 +136,10 @@ describe('LexicalNodeHelpers tests', () => {
}); });
await editor.update(() => { await editor.update(() => {
const paragraph = $getRoot().getFirstChild<ParagraphNode>(); const paragraph = $getRoot().getFirstChild<ParagraphNode>()!;
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey); const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
mergePrevious(overflowRight); mergePrevious(overflowRight);
@ -168,8 +172,9 @@ describe('LexicalNodeHelpers tests', () => {
let text3Key: NodeKey; let text3Key: NodeKey;
await editor.update(() => { await editor.update(() => {
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey); const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey)!;
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey); const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
const text1 = $createTextNode('1'); const text1 = $createTextNode('1');
const text2 = $createTextNode('2'); const text2 = $createTextNode('2');
@ -202,8 +207,9 @@ describe('LexicalNodeHelpers tests', () => {
}); });
await editor.update(() => { await editor.update(() => {
const paragraph = $getRoot().getFirstChild<ParagraphNode>(); const paragraph = $getRoot().getFirstChild<ParagraphNode>()!;
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey); const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
mergePrevious(overflowRight); mergePrevious(overflowRight);

View File

@ -11,17 +11,19 @@ import {
$createTextNode, $createTextNode,
$getRoot, $getRoot,
createEditor, createEditor,
LexicalEditor,
ParagraphNode, ParagraphNode,
} from 'lexical'; } from 'lexical';
import * as React from 'react'; import * as React from 'react';
import {createRoot} from 'react-dom/client'; import {createRef} from 'react';
import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils'; import * as ReactTestUtils from 'react-dom/test-utils';
import {useLexicalIsTextContentEmpty} from '../../useLexicalIsTextContentEmpty'; import {useLexicalIsTextContentEmpty} from '../../useLexicalIsTextContentEmpty';
describe('useLexicalIsTextContentEmpty', () => { describe('useLexicalIsTextContentEmpty', () => {
let container = null; let container: HTMLDivElement | null = null;
let reactRoot; let reactRoot: Root;
beforeEach(() => { beforeEach(() => {
container = document.createElement('div'); container = document.createElement('div');
@ -30,13 +32,13 @@ describe('useLexicalIsTextContentEmpty', () => {
}); });
afterEach(() => { afterEach(() => {
document.body.removeChild(container); document.body.removeChild(container!);
container = null; container = null;
jest.restoreAllMocks(); jest.restoreAllMocks();
}); });
function useLexicalEditor(rootElementRef) { function useLexicalEditor(rootElementRef: React.RefObject<HTMLDivElement>) {
const editor = React.useMemo( const editor = React.useMemo(
() => () =>
createEditor({ createEditor({
@ -58,8 +60,8 @@ describe('useLexicalIsTextContentEmpty', () => {
} }
test('hook works', async () => { test('hook works', async () => {
const ref = React.createRef<HTMLDivElement>(); const ref = createRef<HTMLDivElement>();
let editor; let editor: LexicalEditor;
let hasText = false; let hasText = false;
function TestBase() { function TestBase() {

View File

@ -6,9 +6,10 @@
* *
*/ */
import {UserState} from '@lexical/yjs'; import {Provider, UserState} from '@lexical/yjs';
import {LexicalEditor} from 'lexical'; import {LexicalEditor} from 'lexical';
import * as React from 'react'; import * as React from 'react';
import {Container} from 'react-dom';
import {createRoot, Root} from 'react-dom/client'; import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils'; import * as ReactTestUtils from 'react-dom/test-utils';
import * as Y from 'yjs'; import * as Y from 'yjs';
@ -21,7 +22,17 @@ import {ContentEditable} from '../../LexicalContentEditable';
import LexicalErrorBoundary from '../../LexicalErrorBoundary'; import LexicalErrorBoundary from '../../LexicalErrorBoundary';
import {RichTextPlugin} from '../../LexicalRichTextPlugin'; import {RichTextPlugin} from '../../LexicalRichTextPlugin';
function Editor({doc, provider, setEditor, awarenessData}) { function Editor({
doc,
provider,
setEditor,
awarenessData,
}: {
doc: Y.Doc;
provider: Provider;
setEditor: (editor: LexicalEditor) => void;
awarenessData?: object | undefined;
}) {
const context = useCollaborationContext(); const context = useCollaborationContext();
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
@ -49,20 +60,20 @@ function Editor({doc, provider, setEditor, awarenessData}) {
); );
} }
class Client { export class Client implements Provider {
_id: string; _id: string;
_reactRoot: Root; _reactRoot: Root | null = null;
_container: HTMLDivElement; _container: HTMLDivElement | null = null;
_editor: LexicalEditor; _editor: LexicalEditor | null = null;
_connection: { _connection: {
_clients: Client[]; _clients: Map<string, Client>;
}; };
_connected: boolean; _connected: boolean = false;
_doc: Y.Doc; _doc: Y.Doc = new Y.Doc();
_awarenessState: unknown;
_listeners: Map<string, Set<(data: unknown) => void>>; _listeners = new Map<string, Set<(data: unknown) => void>>();
_updates: Uint8Array[]; _updates: Uint8Array[] = [];
_awarenessState: UserState | null = null;
awareness: { awareness: {
getLocalState: () => UserState | null; getLocalState: () => UserState | null;
getStates: () => Map<number, UserState>; getStates: () => Map<number, UserState>;
@ -71,54 +82,37 @@ class Client {
setLocalState: (state: UserState) => void; setLocalState: (state: UserState) => void;
}; };
constructor(id, connection) { constructor(id: Client['_id'], connection: Client['_connection']) {
this._id = id; this._id = id;
this._reactRoot = null;
this._container = null;
this._connection = connection; this._connection = connection;
this._connected = false;
this._doc = new Y.Doc();
this._awarenessState = {};
this._onUpdate = this._onUpdate.bind(this); this._onUpdate = this._onUpdate.bind(this);
this._doc.on('update', this._onUpdate); this._doc.on('update', this._onUpdate);
this._listeners = new Map();
this._updates = [];
this._editor = null;
this.awareness = { this.awareness = {
getLocalState() { getLocalState: () => this._awarenessState,
return this._awarenessState; getStates: () => new Map([[0, this._awarenessState!]]),
}, off: () => {
getStates() {
const states: Map<number, UserState> = new Map();
states[0] = this._awarenessState as UserState;
return states;
},
off() {
// TODO // TODO
}, },
on() { on: () => {
// TODO // TODO
}, },
setLocalState(state) { setLocalState: (state) => {
this._awarenessState = state; this._awarenessState = state;
}, },
}; };
} }
_onUpdate(update, origin, transaction) { _onUpdate(update: Uint8Array, origin: unknown, transaction: unknown) {
if (origin !== this._connection && this._connected) { if (origin !== this._connection && this._connected) {
this._broadcastUpdate(update); this._broadcastUpdate(update);
} }
} }
_broadcastUpdate(update) { _broadcastUpdate(update: Uint8Array) {
this._connection._clients.forEach((client) => { this._connection._clients.forEach((client) => {
if (client !== this) { if (client !== this) {
if (client._connected) { if (client._connected) {
@ -154,7 +148,7 @@ class Client {
this._connected = false; this._connected = false;
} }
start(rootContainer, awarenessData?) { start(rootContainer: Container, awarenessData?: object) {
const container = document.createElement('div'); const container = document.createElement('div');
const reactRoot = createRoot(container); const reactRoot = createRoot(container);
this._container = container; this._container = container;
@ -185,15 +179,16 @@ class Client {
stop() { stop() {
ReactTestUtils.act(() => { ReactTestUtils.act(() => {
this._reactRoot.render(null); this._reactRoot!.render(null);
}); });
this._container.parentNode.removeChild(this._container); this.getContainer().parentNode!.removeChild(this.getContainer());
this._container = null; this._container = null;
} }
on(type, callback) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
on(type: string, callback: (arg: any) => void) {
let listenerSet = this._listeners.get(type); let listenerSet = this._listeners.get(type);
if (listenerSet === undefined) { if (listenerSet === undefined) {
@ -205,7 +200,8 @@ class Client {
listenerSet.add(callback); listenerSet.add(callback);
} }
off(type, callback) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
off(type: string, callback: (arg: any) => void) {
const listenerSet = this._listeners.get(type); const listenerSet = this._listeners.get(type);
if (listenerSet !== undefined) { if (listenerSet !== undefined) {
@ -213,7 +209,7 @@ class Client {
} }
} }
_dispatch(type, data) { _dispatch(type: string, data: unknown) {
const listenerSet = this._listeners.get(type); const listenerSet = this._listeners.get(type);
if (listenerSet !== undefined) { if (listenerSet !== undefined) {
@ -222,7 +218,7 @@ class Client {
} }
getHTML() { getHTML() {
return (this._container.firstChild as HTMLElement).innerHTML; return (this.getContainer().firstChild as HTMLElement).innerHTML;
} }
getDocJSON() { getDocJSON() {
@ -230,36 +226,32 @@ class Client {
} }
getEditorState() { getEditorState() {
return this._editor.getEditorState(); return this.getEditor().getEditorState();
} }
getEditor() { getEditor() {
return this._editor; return this._editor!;
} }
getContainer() { getContainer() {
return this._container; return this._container!;
} }
async focus() { async focus() {
this._container.focus(); this.getContainer().focus();
await Promise.resolve().then(); await Promise.resolve().then();
} }
update(cb) { update(cb: () => void) {
this._editor.update(cb); this.getEditor().update(cb);
} }
} }
class TestConnection { class TestConnection {
_clients: Map<string, Client>; _clients = new Map<string, Client>();
constructor() { createClient(id: string) {
this._clients = new Map();
}
createClient(id) {
const client = new Client(id, this); const client = new Client(id, this);
this._clients.set(id, client); this._clients.set(id, client);
@ -272,7 +264,7 @@ export function createTestConnection() {
return new TestConnection(); return new TestConnection();
} }
export async function waitForReact(cb) { export async function waitForReact(cb: () => void) {
await ReactTestUtils.act(async () => { await ReactTestUtils.act(async () => {
cb(); cb();
await Promise.resolve().then(); await Promise.resolve().then();

View File

@ -11,7 +11,13 @@ import {
$isHeadingNode, $isHeadingNode,
HeadingNode, HeadingNode,
} from '@lexical/rich-text'; } from '@lexical/rich-text';
import {$createTextNode, $getRoot, $getSelection, ParagraphNode} from 'lexical'; import {
$createTextNode,
$getRoot,
$getSelection,
ParagraphNode,
RangeSelection,
} from 'lexical';
import {initializeUnitTest} from 'lexical/src/__tests__/utils'; import {initializeUnitTest} from 'lexical/src/__tests__/utils';
const editorConfig = Object.freeze({ const editorConfig = Object.freeze({
@ -81,7 +87,7 @@ describe('LexicalHeadingNode tests', () => {
test('HeadingNode.insertNewAfter()', async () => { test('HeadingNode.insertNewAfter()', async () => {
const {editor} = testEnv; const {editor} = testEnv;
let headingNode; let headingNode: HeadingNode;
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();
headingNode = new HeadingNode('h1'); headingNode = new HeadingNode('h1');
@ -91,7 +97,7 @@ describe('LexicalHeadingNode tests', () => {
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><h1><br></h1></div>', '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><h1><br></h1></div>',
); );
await editor.update(() => { await editor.update(() => {
const selection = $getSelection(); const selection = $getSelection() as RangeSelection;
const result = headingNode.insertNewAfter(selection); const result = headingNode.insertNewAfter(selection);
expect(result).toBeInstanceOf(ParagraphNode); expect(result).toBeInstanceOf(ParagraphNode);
expect(result.getDirection()).toEqual(headingNode.getDirection()); expect(result.getDirection()).toEqual(headingNode.getDirection());
@ -122,7 +128,7 @@ describe('LexicalHeadingNode tests', () => {
test('creates a h2 with text and can insert a new paragraph after', async () => { test('creates a h2 with text and can insert a new paragraph after', async () => {
const {editor} = testEnv; const {editor} = testEnv;
let headingNode; let headingNode: HeadingNode;
const text = 'hello world'; const text = 'hello world';
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();

View File

@ -6,8 +6,8 @@
* *
*/ */
import {$createQuoteNode} from '@lexical/rich-text'; import {$createQuoteNode, QuoteNode} from '@lexical/rich-text';
import {$getRoot, ParagraphNode} from 'lexical'; import {$createRangeSelection, $getRoot, ParagraphNode} from 'lexical';
import {initializeUnitTest} from 'lexical/src/__tests__/utils'; import {initializeUnitTest} from 'lexical/src/__tests__/utils';
const editorConfig = Object.freeze({ const editorConfig = Object.freeze({
@ -64,7 +64,7 @@ describe('LexicalQuoteNode tests', () => {
test('QuoteNode.insertNewAfter()', async () => { test('QuoteNode.insertNewAfter()', async () => {
const {editor} = testEnv; const {editor} = testEnv;
let quoteNode; let quoteNode: QuoteNode;
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();
quoteNode = $createQuoteNode(); quoteNode = $createQuoteNode();
@ -74,7 +74,7 @@ describe('LexicalQuoteNode tests', () => {
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><blockquote><br></blockquote></div>', '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><blockquote><br></blockquote></div>',
); );
await editor.update(() => { await editor.update(() => {
const result = quoteNode.insertNewAfter(); const result = quoteNode.insertNewAfter($createRangeSelection());
expect(result).toBeInstanceOf(ParagraphNode); expect(result).toBeInstanceOf(ParagraphNode);
expect(result.getDirection()).toEqual(quoteNode.getDirection()); expect(result.getDirection()).toEqual(quoteNode.getDirection());
}); });

View File

@ -30,7 +30,13 @@ import {
$isElementNode, $isElementNode,
$isRangeSelection, $isRangeSelection,
$setSelection, $setSelection,
DecoratorNode,
ElementNode,
LexicalEditor,
LexicalNode,
ParagraphNode, ParagraphNode,
PointType,
TextNode,
} from 'lexical'; } from 'lexical';
import { import {
$assertRangeSelection, $assertRangeSelection,
@ -72,6 +78,13 @@ import {
undo, undo,
} from '../utils'; } from '../utils';
interface ExpectedSelection {
anchorPath: number[];
anchorOffset: number;
focusPath: number[];
focusOffset: number;
}
initializeClipboard(); initializeClipboard();
jest.mock('shared/environment', () => { jest.mock('shared/environment', () => {
@ -116,7 +129,7 @@ describe('LexicalSelection tests', () => {
container = null; container = null;
}); });
let editor = null; let editor: LexicalEditor | null = null;
async function init() { async function init() {
function TestBase() { function TestBase() {
@ -162,8 +175,8 @@ describe('LexicalSelection tests', () => {
}}> }}>
<RichTextPlugin <RichTextPlugin
contentEditable={ contentEditable={
// eslint-disable-next-line jsx-a11y/aria-role // eslint-disable-next-line jsx-a11y/aria-role, @typescript-eslint/no-explicit-any
<ContentEditable role={null} spellCheck={null} /> <ContentEditable role={null as any} spellCheck={null as any} />
} }
placeholder={null} placeholder={null}
ErrorBoundary={LexicalErrorBoundary} ErrorBoundary={LexicalErrorBoundary}
@ -175,32 +188,41 @@ describe('LexicalSelection tests', () => {
} }
ReactTestUtils.act(() => { ReactTestUtils.act(() => {
createRoot(container).render(<TestBase />); createRoot(container!).render(<TestBase />);
}); });
editor.getRootElement().focus(); editor!.getRootElement()!.focus();
await Promise.resolve().then(); await Promise.resolve().then();
// Focus first element // Focus first element
setNativeSelectionWithPaths(editor.getRootElement(), [0, 0], 0, [0, 0], 0); setNativeSelectionWithPaths(
editor!.getRootElement()!,
[0, 0],
0,
[0, 0],
0,
);
} }
async function update(fn) { async function update(fn: () => void) {
await ReactTestUtils.act(async () => { await ReactTestUtils.act(async () => {
await editor.update(fn); await editor!.update(fn);
}); });
return Promise.resolve().then(); return Promise.resolve().then();
} }
test('Expect initial output to be a block with no text.', () => { test('Expect initial output to be a block with no text.', () => {
expect(container.innerHTML).toBe( expect(container!.innerHTML).toBe(
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><br></p></div>', '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><br></p></div>',
); );
}); });
function assertSelection(rootElement, expectedSelection) { function assertSelection(
const actualSelection = window.getSelection(); rootElement: HTMLElement,
expectedSelection: ExpectedSelection,
) {
const actualSelection = window.getSelection()!;
expect(actualSelection.anchorNode).toBe( expect(actualSelection.anchorNode).toBe(
getNodeFromPath(expectedSelection.anchorPath, rootElement), getNodeFromPath(expectedSelection.anchorPath, rootElement),
@ -1107,13 +1129,13 @@ describe('LexicalSelection tests', () => {
const name = testUnit.name || 'Test case'; const name = testUnit.name || 'Test case';
test(name + ` (#${i + 1})`, async () => { test(name + ` (#${i + 1})`, async () => {
await applySelectionInputs(testUnit.inputs, update, editor); await applySelectionInputs(testUnit.inputs, update, editor!);
// Validate HTML matches // Validate HTML matches
expect(container.innerHTML).toBe(testUnit.expectedHTML); expect(container!.innerHTML).toBe(testUnit.expectedHTML);
// Validate selection matches // Validate selection matches
const rootElement = editor.getRootElement(); const rootElement = editor!.getRootElement()!;
const expectedSelection = testUnit.expectedSelection; const expectedSelection = testUnit.expectedSelection;
assertSelection(rootElement, expectedSelection); assertSelection(rootElement, expectedSelection);
@ -1122,10 +1144,10 @@ describe('LexicalSelection tests', () => {
test('insert text one selected node element selection', async () => { test('insert text one selected node element selection', async () => {
await ReactTestUtils.act(async () => { await ReactTestUtils.act(async () => {
await editor.update(() => { await editor!.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
const elementNode = $createTestElementNode(); const elementNode = $createTestElementNode();
const text = $createTextNode('foo'); const text = $createTextNode('foo');
@ -1146,10 +1168,10 @@ describe('LexicalSelection tests', () => {
test('getNodes resolves nested block nodes', async () => { test('getNodes resolves nested block nodes', async () => {
await ReactTestUtils.act(async () => { await ReactTestUtils.act(async () => {
await editor.update(() => { await editor!.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
const elementNode = $createTestElementNode(); const elementNode = $createTestElementNode();
const text = $createTextNode(); const text = $createTextNode();
@ -1157,7 +1179,7 @@ describe('LexicalSelection tests', () => {
paragraph.append(elementNode); paragraph.append(elementNode);
elementNode.append(text); elementNode.append(text);
const selectedNodes = $getSelection().getNodes(); const selectedNodes = $getSelection()!.getNodes();
expect(selectedNodes.length).toBe(1); expect(selectedNodes.length).toBe(1);
expect(selectedNodes[0].getKey()).toBe(text.getKey()); expect(selectedNodes[0].getKey()).toBe(text.getKey());
@ -1166,7 +1188,23 @@ describe('LexicalSelection tests', () => {
}); });
describe('Block selection moves when new nodes are inserted', () => { describe('Block selection moves when new nodes are inserted', () => {
[ const baseCases: {
name: string;
anchorOffset: number;
focusOffset: number;
fn: (
paragraph: ElementNode,
text: TextNode,
) => {
expectedAnchor: LexicalNode;
expectedAnchorOffset: number;
expectedFocus: LexicalNode;
expectedFocusOffset: number;
};
fnBefore?: (paragraph: ElementNode, text: TextNode) => void;
invertSelection?: true;
only?: true;
}[] = [
// Collapsed selection on end; add/remove/replace beginning // Collapsed selection on end; add/remove/replace beginning
{ {
anchorOffset: 2, anchorOffset: 2,
@ -1313,8 +1351,8 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling(); const originalText2 = originalText1.getPreviousSibling()!;
const lastChild = paragraph.getLastChild(); const lastChild = paragraph.getLastChild()!;
const newText = $createTextNode('2'); const newText = $createTextNode('2');
lastChild.insertBefore(newText); lastChild.insertBefore(newText);
@ -1335,7 +1373,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, text) => { fn: (paragraph, text) => {
const lastChild = paragraph.getLastChild(); const lastChild = paragraph.getLastChild()!;
const newText = $createTextNode('2'); const newText = $createTextNode('2');
lastChild.insertAfter(newText); lastChild.insertAfter(newText);
@ -1352,7 +1390,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling(); const originalText2 = originalText1.getPreviousSibling()!;
const [, text] = originalText1.splitText(1); const [, text] = originalText1.splitText(1);
return { return {
@ -1372,7 +1410,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, text) => { fn: (paragraph, text) => {
const lastChild = paragraph.getLastChild(); const lastChild = paragraph.getLastChild()!;
lastChild.remove(); lastChild.remove();
return { return {
@ -1389,7 +1427,7 @@ describe('LexicalSelection tests', () => {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, text) => { fn: (paragraph, text) => {
const newText = $createTextNode('replacement'); const newText = $createTextNode('replacement');
const lastChild = paragraph.getLastChild(); const lastChild = paragraph.getLastChild()!;
lastChild.replace(newText); lastChild.replace(newText);
return { return {
@ -1406,7 +1444,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, text) => { fn: (paragraph, text) => {
const lastChild = paragraph.getLastChild(); const lastChild = paragraph.getLastChild()!;
const newText = $createTextNode('2'); const newText = $createTextNode('2');
lastChild.insertBefore(newText); lastChild.insertBefore(newText);
@ -1439,7 +1477,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling(); const originalText2 = originalText1.getPreviousSibling()!;
const [, text] = originalText1.splitText(1); const [, text] = originalText1.splitText(1);
return { return {
@ -1459,7 +1497,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 1, anchorOffset: 1,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
const lastChild = paragraph.getLastChild(); const lastChild = paragraph.getLastChild()!;
lastChild.remove(); lastChild.remove();
return { return {
@ -1480,7 +1518,7 @@ describe('LexicalSelection tests', () => {
anchorOffset: 1, anchorOffset: 1,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
const newText = $createTextNode('replacement'); const newText = $createTextNode('replacement');
const lastChild = paragraph.getLastChild(); const lastChild = paragraph.getLastChild()!;
lastChild.replace(newText); lastChild.replace(newText);
return { return {
@ -1501,8 +1539,8 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling(); const originalText2 = originalText1.getPreviousSibling()!;
const lastChild = paragraph.getLastChild(); const lastChild = paragraph.getLastChild()!;
const newText = $createTextNode('2'); const newText = $createTextNode('2');
lastChild.insertBefore(newText); lastChild.insertBefore(newText);
@ -1523,7 +1561,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling(); const originalText2 = originalText1.getPreviousSibling()!;
const newText = $createTextNode('2'); const newText = $createTextNode('2');
originalText1.insertAfter(newText); originalText1.insertAfter(newText);
@ -1544,7 +1582,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling(); const originalText2 = originalText1.getPreviousSibling()!;
originalText1.splitText(1); originalText1.splitText(1);
return { return {
@ -1564,7 +1602,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling(); const originalText2 = originalText1.getPreviousSibling()!;
originalText1.remove(); originalText1.remove();
return { return {
@ -1605,7 +1643,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 3, anchorOffset: 3,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
const originalText2 = paragraph.getLastChild(); const originalText2 = paragraph.getLastChild()!;
const newText = $createTextNode('new'); const newText = $createTextNode('new');
originalText1.insertBefore(newText); originalText1.insertBefore(newText);
@ -1626,7 +1664,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
const originalText2 = paragraph.getLastChild(); const originalText2 = paragraph.getLastChild()!;
const newText = $createTextNode('new'); const newText = $createTextNode('new');
originalText1.insertBefore(newText); originalText1.insertBefore(newText);
@ -1647,7 +1685,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 1, anchorOffset: 1,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
originalText1.getNextSibling().remove(); originalText1.getNextSibling()!.remove();
return { return {
expectedAnchor: originalText1, expectedAnchor: originalText1,
@ -1662,7 +1700,7 @@ describe('LexicalSelection tests', () => {
{ {
anchorOffset: 0, anchorOffset: 0,
fn: (paragraph, originalText1) => { fn: (paragraph, originalText1) => {
originalText1.getNextSibling().remove(); originalText1.getNextSibling()!.remove();
return { return {
expectedAnchor: originalText1, expectedAnchor: originalText1,
@ -1674,8 +1712,9 @@ describe('LexicalSelection tests', () => {
focusOffset: 1, focusOffset: 1,
name: 'remove - Remove with non-collapsed selection at offset', name: 'remove - Remove with non-collapsed selection at offset',
}, },
] ];
.reduce((testSuite, testCase) => { baseCases
.flatMap((testCase) => {
// Test inverse selection // Test inverse selection
const inverse = { const inverse = {
...testCase, ...testCase,
@ -1684,9 +1723,8 @@ describe('LexicalSelection tests', () => {
invertSelection: true, invertSelection: true,
name: testCase.name + ' (inverse selection)', name: testCase.name + ' (inverse selection)',
}; };
return [testCase, inverse];
return testSuite.concat(testCase, inverse); })
}, [])
.forEach( .forEach(
({ ({
name, name,
@ -1703,10 +1741,10 @@ describe('LexicalSelection tests', () => {
const test_ = only === true ? test.only : test; const test_ = only === true ? test.only : test;
test_(name, async () => { test_(name, async () => {
await ReactTestUtils.act(async () => { await ReactTestUtils.act(async () => {
await editor.update(() => { await editor!.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
const textNode = $createTextNode('foo'); const textNode = $createTextNode('foo');
// Note: line break can't be selected by the DOM // Note: line break can't be selected by the DOM
const linebreak = $createLineBreakNode(); const linebreak = $createLineBreakNode();
@ -1755,7 +1793,7 @@ describe('LexicalSelection tests', () => {
describe('Selection correctly resolves to a sibling ElementNode when a node is removed', () => { describe('Selection correctly resolves to a sibling ElementNode when a node is removed', () => {
test('', async () => { test('', async () => {
await ReactTestUtils.act(async () => { await ReactTestUtils.act(async () => {
await editor.update(() => { await editor!.update(() => {
const root = $getRoot(); const root = $getRoot();
const listNode = $createListNode('bullet'); const listNode = $createListNode('bullet');
@ -1785,8 +1823,8 @@ describe('LexicalSelection tests', () => {
describe('Selection correctly resolves to a sibling ElementNode when a selected node child is removed', () => { describe('Selection correctly resolves to a sibling ElementNode when a selected node child is removed', () => {
test('', async () => { test('', async () => {
await ReactTestUtils.act(async () => { await ReactTestUtils.act(async () => {
let paragraphNodeKey; let paragraphNodeKey: string;
await editor.update(() => { await editor!.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraphNode = $createParagraphNode(); const paragraphNode = $createParagraphNode();
@ -1808,7 +1846,7 @@ describe('LexicalSelection tests', () => {
listNode.remove(); listNode.remove();
}); });
await editor.getEditorState().read(() => { await editor!.getEditorState().read(() => {
const selection = $assertRangeSelection($getSelection()); const selection = $assertRangeSelection($getSelection());
expect(selection.anchor.key).toBe(paragraphNodeKey); expect(selection.anchor.key).toBe(paragraphNodeKey);
expect(selection.focus.key).toBe(paragraphNodeKey); expect(selection.focus.key).toBe(paragraphNodeKey);
@ -1820,7 +1858,7 @@ describe('LexicalSelection tests', () => {
describe('Selection correctly resolves to a sibling ElementNode that has multiple children with the correct offset when a node is removed', () => { describe('Selection correctly resolves to a sibling ElementNode that has multiple children with the correct offset when a node is removed', () => {
test('', async () => { test('', async () => {
await ReactTestUtils.act(async () => { await ReactTestUtils.act(async () => {
await editor.update(() => { await editor!.update(() => {
// Arrange // Arrange
// Root // Root
// |- Paragraph // |- Paragraph
@ -1871,10 +1909,10 @@ describe('LexicalSelection tests', () => {
test('isBackward', async () => { test('isBackward', async () => {
await ReactTestUtils.act(async () => { await ReactTestUtils.act(async () => {
await editor.update(() => { await editor!.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
const paragraphKey = paragraph.getKey(); const paragraphKey = paragraph.getKey();
const textNode = $createTextNode('foo'); const textNode = $createTextNode('foo');
const textNodeKey = textNode.getKey(); const textNodeKey = textNode.getKey();
@ -1914,7 +1952,18 @@ describe('LexicalSelection tests', () => {
}); });
describe('Decorator text content for selection', () => { describe('Decorator text content for selection', () => {
[ const baseCases: {
name: string;
fn: (opts: {
textNode1: TextNode;
textNode2: TextNode;
decorator: DecoratorNode<unknown>;
paragraph: ParagraphNode;
anchor: PointType;
focus: PointType;
}) => string;
invertSelection?: true;
}[] = [
{ {
fn: ({textNode1, anchor, focus}) => { fn: ({textNode1, anchor, focus}) => {
anchor.set(textNode1.getKey(), 1, 'text'); anchor.set(textNode1.getKey(), 1, 'text');
@ -1971,23 +2020,24 @@ describe('LexicalSelection tests', () => {
}, },
name: 'Included if decorator is selected as the only node', name: 'Included if decorator is selected as the only node',
}, },
] ];
.reduce((testSuite, testCase) => { baseCases
.flatMap((testCase) => {
const inverse = { const inverse = {
...testCase, ...testCase,
invertSelection: true, invertSelection: true,
name: testCase.name + ' (inverse selection)', name: testCase.name + ' (inverse selection)',
}; };
return testSuite.concat(testCase, inverse); return [testCase, inverse];
}, []) })
.forEach(({name, fn, invertSelection}) => { .forEach(({name, fn, invertSelection}) => {
it(name, async () => { it(name, async () => {
await ReactTestUtils.act(async () => { await ReactTestUtils.act(async () => {
await editor.update(() => { await editor!.update(() => {
const root = $getRoot(); const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>(); const paragraph = root.getFirstChild<ParagraphNode>()!;
const textNode1 = $createTextNode('1'); const textNode1 = $createTextNode('1');
const textNode2 = $createTextNode('2'); const textNode2 = $createTextNode('2');
const decorator = $createTestDecoratorNode(); const decorator = $createTestDecoratorNode();
@ -2113,7 +2163,7 @@ describe('LexicalSelection tests', () => {
it('adjust offset for inline elements text formatting', async () => { it('adjust offset for inline elements text formatting', async () => {
init(); init();
await editor.update(() => { await editor!.update(() => {
const root = $getRoot(); const root = $getRoot();
const text1 = $createTextNode('--'); const text1 = $createTextNode('--');
@ -2154,7 +2204,11 @@ describe('LexicalSelection tests', () => {
}); });
describe('Node.replace', () => { describe('Node.replace', () => {
let text1, text2, text3, paragraph, testEditor; let text1: TextNode,
text2: TextNode,
text3: TextNode,
paragraph: ParagraphNode,
testEditor: LexicalEditor;
beforeEach(async () => { beforeEach(async () => {
testEditor = createTestEditor(); testEditor = createTestEditor();
@ -2481,7 +2535,7 @@ describe('LexicalSelection tests', () => {
expect(rootChildren[0].__type).toBe('heading'); expect(rootChildren[0].__type).toBe('heading');
expect(rootChildren[1].__type).toBe('heading'); expect(rootChildren[1].__type).toBe('heading');
expect(rootChildren.length).toBe(2); expect(rootChildren.length).toBe(2);
const sel = $getSelection(); const sel = $getSelection()!;
expect(sel.getNodes().length).toBe(2); expect(sel.getNodes().length).toBe(2);
}); });
}); });
@ -2540,7 +2594,7 @@ describe('LexicalSelection tests', () => {
const paragraph = column.getFirstChild(); const paragraph = column.getFirstChild();
invariant($isElementNode(paragraph)); invariant($isElementNode(paragraph));
if (paragraph.getFirstChild()) { if (paragraph.getFirstChild()) {
paragraph.getFirstChild().remove(); paragraph.getFirstChild()!.remove();
} }
root.append(table); root.append(table);

View File

@ -25,6 +25,9 @@ import {
$isParagraphNode, $isParagraphNode,
$isRangeSelection, $isRangeSelection,
$setSelection, $setSelection,
ElementNode,
LexicalEditor,
ParagraphNode,
RangeSelection, RangeSelection,
TextNode, TextNode,
} from 'lexical'; } from 'lexical';
@ -59,9 +62,12 @@ Range.prototype.getBoundingClientRect = function (): DOMRect {
}; };
}; };
function createParagraphWithNodes(editor, nodes) { function createParagraphWithNodes(
editor: LexicalEditor,
nodes: {text: string; key: string; mergeable?: boolean}[],
) {
const paragraph = $createParagraphNode(); const paragraph = $createParagraphNode();
const nodeMap = editor._pendingEditorState._nodeMap; const nodeMap = editor._pendingEditorState!._nodeMap;
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
const {text, key, mergeable} = nodes[i]; const {text, key, mergeable} = nodes[i];
@ -81,7 +87,9 @@ function createParagraphWithNodes(editor, nodes) {
describe('LexicalSelectionHelpers tests', () => { describe('LexicalSelectionHelpers tests', () => {
describe('Collapsed', () => { describe('Collapsed', () => {
test('Can handle a text point', () => { test('Can handle a text point', () => {
const setupTestCase = (cb) => { const setupTestCase = (
cb: (selection: RangeSelection, node: ElementNode) => void,
) => {
const editor = createTestEditor(); const editor = createTestEditor();
editor.update(() => { editor.update(() => {
@ -119,7 +127,7 @@ describe('LexicalSelectionHelpers tests', () => {
type: 'text', type: 'text',
}); });
const selection = $getSelection(); const selection = $getSelection();
cb(selection, element); cb(selection as RangeSelection, element);
}); });
}; };
@ -137,7 +145,7 @@ describe('LexicalSelectionHelpers tests', () => {
setupTestCase((selection, state) => { setupTestCase((selection, state) => {
selection.insertText('Test'); selection.insertText('Test');
expect($getNodeByKey('a').getTextContent()).toBe('Testa'); expect($getNodeByKey('a')!.getTextContent()).toBe('Testa');
expect(selection.anchor).toEqual( expect(selection.anchor).toEqual(
expect.objectContaining({ expect.objectContaining({
@ -162,7 +170,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.anchor).toEqual( expect(selection.anchor).toEqual(
expect.objectContaining({ expect.objectContaining({
key: element.getFirstChild().getKey(), key: element.getFirstChild()!.getKey(),
offset: 3, offset: 3,
type: 'text', type: 'text',
}), }),
@ -170,7 +178,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.focus).toEqual( expect(selection.focus).toEqual(
expect.objectContaining({ expect.objectContaining({
key: element.getFirstChild().getKey(), key: element.getFirstChild()!.getKey(),
offset: 3, offset: 3,
type: 'text', type: 'text',
}), }),
@ -224,11 +232,11 @@ describe('LexicalSelectionHelpers tests', () => {
selection.formatText('bold'); selection.formatText('bold');
selection.insertText('Test'); selection.insertText('Test');
expect(element.getFirstChild().getTextContent()).toBe('Test'); expect(element.getFirstChild()!.getTextContent()).toBe('Test');
expect(selection.anchor).toEqual( expect(selection.anchor).toEqual(
expect.objectContaining({ expect.objectContaining({
key: element.getFirstChild().getKey(), key: element.getFirstChild()!.getKey(),
offset: 4, offset: 4,
type: 'text', type: 'text',
}), }),
@ -236,15 +244,15 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.focus).toEqual( expect(selection.focus).toEqual(
expect.objectContaining({ expect.objectContaining({
key: element.getFirstChild().getKey(), key: element.getFirstChild()!.getKey(),
offset: 4, offset: 4,
type: 'text', type: 'text',
}), }),
); );
expect(element.getFirstChild().getNextSibling().getTextContent()).toBe( expect(
'a', element.getFirstChild()!.getNextSibling()!.getTextContent(),
); ).toBe('a');
}); });
// Extract selection // Extract selection
@ -414,7 +422,7 @@ describe('LexicalSelectionHelpers tests', () => {
const editor = createTestEditor(); const editor = createTestEditor();
const domElement = document.createElement('div'); const domElement = document.createElement('div');
let element; let element: ParagraphNode;
editor.setRootElement(domElement); editor.setRootElement(domElement);
@ -690,7 +698,9 @@ describe('LexicalSelectionHelpers tests', () => {
}); });
test('Can handle an element point on empty element', () => { test('Can handle an element point on empty element', () => {
const setupTestCase = (cb) => { const setupTestCase = (
cb: (selection: RangeSelection, el: ElementNode) => void,
) => {
const editor = createTestEditor(); const editor = createTestEditor();
editor.update(() => { editor.update(() => {
@ -712,7 +722,7 @@ describe('LexicalSelectionHelpers tests', () => {
type: 'element', type: 'element',
}); });
const selection = $getSelection(); const selection = $getSelection();
cb(selection, element); cb(selection as RangeSelection, element);
}); });
}; };
@ -729,7 +739,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertText // insertText
setupTestCase((selection, element) => { setupTestCase((selection, element) => {
selection.insertText('Test'); selection.insertText('Test');
const firstChild = element.getFirstChild(); const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test'); expect(firstChild.getTextContent()).toBe('Test');
@ -753,7 +763,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertParagraph // insertParagraph
setupTestCase((selection, element) => { setupTestCase((selection, element) => {
selection.insertParagraph(); selection.insertParagraph();
const nextElement = element.getNextSibling(); const nextElement = element.getNextSibling()!;
expect(selection.anchor).toEqual( expect(selection.anchor).toEqual(
expect.objectContaining({ expect.objectContaining({
@ -797,7 +807,7 @@ describe('LexicalSelectionHelpers tests', () => {
setupTestCase((selection, element) => { setupTestCase((selection, element) => {
selection.formatText('bold'); selection.formatText('bold');
selection.insertText('Test'); selection.insertText('Test');
const firstChild = element.getFirstChild(); const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test'); expect(firstChild.getTextContent()).toBe('Test');
@ -825,7 +835,9 @@ describe('LexicalSelectionHelpers tests', () => {
}); });
test('Can handle a start element point', () => { test('Can handle a start element point', () => {
const setupTestCase = (cb) => { const setupTestCase = (
cb: (selection: RangeSelection, el: ElementNode) => void,
) => {
const editor = createTestEditor(); const editor = createTestEditor();
editor.update(() => { editor.update(() => {
@ -863,7 +875,7 @@ describe('LexicalSelectionHelpers tests', () => {
type: 'element', type: 'element',
}); });
const selection = $getSelection(); const selection = $getSelection();
cb(selection, element); cb(selection as RangeSelection, element);
}); });
}; };
@ -880,7 +892,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertText // insertText
setupTestCase((selection, element) => { setupTestCase((selection, element) => {
selection.insertText('Test'); selection.insertText('Test');
const firstChild = element.getFirstChild(); const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test'); expect(firstChild.getTextContent()).toBe('Test');
@ -948,7 +960,7 @@ describe('LexicalSelectionHelpers tests', () => {
selection.formatText('bold'); selection.formatText('bold');
selection.insertText('Test'); selection.insertText('Test');
const firstChild = element.getFirstChild(); const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test'); expect(firstChild.getTextContent()).toBe('Test');
@ -976,7 +988,9 @@ describe('LexicalSelectionHelpers tests', () => {
}); });
test('Can handle an end element point', () => { test('Can handle an end element point', () => {
const setupTestCase = (cb) => { const setupTestCase = (
cb: (selection: RangeSelection, el: ElementNode) => void,
) => {
const editor = createTestEditor(); const editor = createTestEditor();
editor.update(() => { editor.update(() => {
@ -1014,7 +1028,7 @@ describe('LexicalSelectionHelpers tests', () => {
type: 'element', type: 'element',
}); });
const selection = $getSelection(); const selection = $getSelection();
cb(selection, element); cb(selection as RangeSelection, element);
}); });
}; };
@ -1031,7 +1045,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertText // insertText
setupTestCase((selection, element) => { setupTestCase((selection, element) => {
selection.insertText('Test'); selection.insertText('Test');
const lastChild = element.getLastChild(); const lastChild = element.getLastChild()!;
expect(lastChild.getTextContent()).toBe('Test'); expect(lastChild.getTextContent()).toBe('Test');
@ -1055,7 +1069,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertParagraph // insertParagraph
setupTestCase((selection, element) => { setupTestCase((selection, element) => {
selection.insertParagraph(); selection.insertParagraph();
const nextSibling = element.getNextSibling(); const nextSibling = element.getNextSibling()!;
expect(selection.anchor).toEqual( expect(selection.anchor).toEqual(
expect.objectContaining({ expect.objectContaining({
@ -1099,7 +1113,7 @@ describe('LexicalSelectionHelpers tests', () => {
setupTestCase((selection, element) => { setupTestCase((selection, element) => {
selection.formatText('bold'); selection.formatText('bold');
selection.insertText('Test'); selection.insertText('Test');
const lastChild = element.getLastChild(); const lastChild = element.getLastChild()!;
expect(lastChild.getTextContent()).toBe('Test'); expect(lastChild.getTextContent()).toBe('Test');
@ -1271,7 +1285,9 @@ describe('LexicalSelectionHelpers tests', () => {
describe('Simple range', () => { describe('Simple range', () => {
test('Can handle multiple text points', () => { test('Can handle multiple text points', () => {
const setupTestCase = (cb) => { const setupTestCase = (
cb: (selection: RangeSelection, el: ElementNode) => void,
) => {
const editor = createTestEditor(); const editor = createTestEditor();
editor.update(() => { editor.update(() => {
@ -1333,7 +1349,7 @@ describe('LexicalSelectionHelpers tests', () => {
setupTestCase((selection, state) => { setupTestCase((selection, state) => {
selection.insertText('Test'); selection.insertText('Test');
expect($getNodeByKey('a').getTextContent()).toBe('Test'); expect($getNodeByKey('a')!.getTextContent()).toBe('Test');
expect(selection.anchor).toEqual( expect(selection.anchor).toEqual(
expect.objectContaining({ expect.objectContaining({
@ -1358,7 +1374,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.anchor).toEqual( expect(selection.anchor).toEqual(
expect.objectContaining({ expect.objectContaining({
key: element.getFirstChild().getKey(), key: element.getFirstChild()!.getKey(),
offset: 3, offset: 3,
type: 'text', type: 'text',
}), }),
@ -1366,7 +1382,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.focus).toEqual( expect(selection.focus).toEqual(
expect.objectContaining({ expect.objectContaining({
key: element.getFirstChild().getKey(), key: element.getFirstChild()!.getKey(),
offset: 3, offset: 3,
type: 'text', type: 'text',
}), }),
@ -1420,11 +1436,11 @@ describe('LexicalSelectionHelpers tests', () => {
selection.formatText('bold'); selection.formatText('bold');
selection.insertText('Test'); selection.insertText('Test');
expect(element.getFirstChild().getTextContent()).toBe('Test'); expect(element.getFirstChild()!.getTextContent()).toBe('Test');
expect(selection.anchor).toEqual( expect(selection.anchor).toEqual(
expect.objectContaining({ expect.objectContaining({
key: element.getFirstChild().getKey(), key: element.getFirstChild()!.getKey(),
offset: 4, offset: 4,
type: 'text', type: 'text',
}), }),
@ -1432,7 +1448,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.focus).toEqual( expect(selection.focus).toEqual(
expect.objectContaining({ expect.objectContaining({
key: element.getFirstChild().getKey(), key: element.getFirstChild()!.getKey(),
offset: 4, offset: 4,
type: 'text', type: 'text',
}), }),
@ -1446,7 +1462,9 @@ describe('LexicalSelectionHelpers tests', () => {
}); });
test('Can handle multiple element points', () => { test('Can handle multiple element points', () => {
const setupTestCase = (cb) => { const setupTestCase = (
cb: (selection: RangeSelection, el: ElementNode) => void,
) => {
const editor = createTestEditor(); const editor = createTestEditor();
editor.update(() => { editor.update(() => {
@ -1504,7 +1522,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertText // insertText
setupTestCase((selection, element) => { setupTestCase((selection, element) => {
selection.insertText('Test'); selection.insertText('Test');
const firstChild = element.getFirstChild(); const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test'); expect(firstChild.getTextContent()).toBe('Test');
@ -1571,7 +1589,7 @@ describe('LexicalSelectionHelpers tests', () => {
setupTestCase((selection, element) => { setupTestCase((selection, element) => {
selection.formatText('bold'); selection.formatText('bold');
selection.insertText('Test'); selection.insertText('Test');
const firstChild = element.getFirstChild(); const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test'); expect(firstChild.getTextContent()).toBe('Test');
@ -1601,7 +1619,9 @@ describe('LexicalSelectionHelpers tests', () => {
}); });
test('Can handle a mix of text and element points', () => { test('Can handle a mix of text and element points', () => {
const setupTestCase = (cb) => { const setupTestCase = (
cb: (selection: RangeSelection, el: ElementNode) => void,
) => {
const editor = createTestEditor(); const editor = createTestEditor();
editor.update(() => { editor.update(() => {
@ -1668,7 +1688,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertText // insertText
setupTestCase((selection, element) => { setupTestCase((selection, element) => {
selection.insertText('Test'); selection.insertText('Test');
const firstChild = element.getFirstChild(); const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test'); expect(firstChild.getTextContent()).toBe('Test');
@ -1692,7 +1712,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertParagraph // insertParagraph
setupTestCase((selection, element) => { setupTestCase((selection, element) => {
selection.insertParagraph(); selection.insertParagraph();
const nextElement = element.getNextSibling(); const nextElement = element.getNextSibling()!;
expect(selection.anchor).toEqual( expect(selection.anchor).toEqual(
expect.objectContaining({ expect.objectContaining({
@ -1736,7 +1756,7 @@ describe('LexicalSelectionHelpers tests', () => {
setupTestCase((selection, element) => { setupTestCase((selection, element) => {
selection.formatText('bold'); selection.formatText('bold');
selection.insertText('Test'); selection.insertText('Test');
const firstChild = element.getFirstChild(); const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test'); expect(firstChild.getTextContent()).toBe('Test');
@ -2121,7 +2141,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.anchor).toEqual( expect(selection.anchor).toEqual(
expect.objectContaining({ expect.objectContaining({
key: paragraph key: paragraph
.getChildAtIndex(paragraph.getChildrenSize() - 2) .getChildAtIndex(paragraph.getChildrenSize() - 2)!
.getKey(), .getKey(),
offset: 1, offset: 1,
type: 'text', type: 'text',
@ -2131,7 +2151,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.focus).toEqual( expect(selection.focus).toEqual(
expect.objectContaining({ expect.objectContaining({
key: paragraph key: paragraph
.getChildAtIndex(paragraph.getChildrenSize() - 2) .getChildAtIndex(paragraph.getChildrenSize() - 2)!
.getKey(), .getKey(),
offset: 1, offset: 1,
type: 'text', type: 'text',
@ -2547,7 +2567,7 @@ describe('extract', () => {
const selection = $getSelection(); const selection = $getSelection();
expect($isRangeSelection(selection)).toBeTruthy(); expect($isRangeSelection(selection)).toBeTruthy();
expect(selection.extract()).toEqual([text]); expect(selection!.extract()).toEqual([text]);
}); });
}); });
}); });
@ -2605,7 +2625,7 @@ describe('insertNodes', () => {
}); });
await editor.update(() => { await editor.update(() => {
const selection = $createRangeSelection(); const selection = $createRangeSelection();
const text = $getRoot().getLastDescendant(); const text = $getRoot().getLastDescendant()!;
selection.anchor.set(text.getKey(), 0, 'text'); selection.anchor.set(text.getKey(), 0, 'text');
selection.focus.set(text.getKey(), 0, 'text'); selection.focus.set(text.getKey(), 0, 'text');
@ -2629,7 +2649,7 @@ describe('insertNodes', () => {
$createParagraphNode().append(emptyTextNode, $createTextNode('text')), $createParagraphNode().append(emptyTextNode, $createTextNode('text')),
); );
emptyTextNode.select(0, 0); emptyTextNode.select(0, 0);
const selection = $getSelection(); const selection = $getSelection()!;
expect($isRangeSelection(selection)).toBeTruthy(); expect($isRangeSelection(selection)).toBeTruthy();
selection.insertNodes([$createTextNode('foo')]); selection.insertNodes([$createTextNode('foo')]);
@ -2692,7 +2712,7 @@ describe('$patchStyleText', () => {
const link = $createLinkNode('https://'); const link = $createLinkNode('https://');
link.append($createTextNode('link')); link.append($createTextNode('link'));
const a = $getNodeByKey('a'); const a = $getNodeByKey('a')!;
a.insertAfter(link); a.insertAfter(link);
setAnchorPoint({ setAnchorPoint({
@ -2793,7 +2813,7 @@ describe('$patchStyleText', () => {
const link = $createLinkNode('https://'); const link = $createLinkNode('https://');
link.append($createTextNode('link')); link.append($createTextNode('link'));
const a = $getNodeByKey('a'); const a = $getNodeByKey('a')!;
a.insertAfter(link); a.insertAfter(link);
setAnchorPoint({ setAnchorPoint({
@ -2846,7 +2866,7 @@ describe('$patchStyleText', () => {
const link = $createLinkNode('https://'); const link = $createLinkNode('https://');
link.append($createTextNode('link')); link.append($createTextNode('link'));
const a = $getNodeByKey('a'); const a = $getNodeByKey('a')!;
a.insertAfter(link); a.insertAfter(link);
// Select from the end of the link _element_ // Select from the end of the link _element_

View File

@ -12,6 +12,8 @@ import {
$isNodeSelection, $isNodeSelection,
$isRangeSelection, $isRangeSelection,
$isTextNode, $isTextNode,
LexicalEditor,
PointType,
} from 'lexical'; } from 'lexical';
Object.defineProperty(HTMLElement.prototype, 'contentEditable', { Object.defineProperty(HTMLElement.prototype, 'contentEditable', {
@ -49,7 +51,7 @@ if (!Selection.prototype.modify) {
}; };
const getWordsFromString = function (string: string): Array<Segment> { const getWordsFromString = function (string: string): Array<Segment> {
const segments = []; const segments: Segment[] = [];
let wordString = ''; let wordString = '';
let nonWordString = ''; let nonWordString = '';
let i; let i;
@ -90,7 +92,8 @@ if (!Selection.prototype.modify) {
// This is not a thorough implementation, it was more to get tests working // This is not a thorough implementation, it was more to get tests working
// given the refactor to use this selection method. // given the refactor to use this selection method.
const symbol = Object.getOwnPropertySymbols(this)[0]; const symbol = Object.getOwnPropertySymbols(this)[0];
const impl = this[symbol]; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const impl = (this as any)[symbol];
const focus = impl._focus; const focus = impl._focus;
const anchor = impl._anchor; const anchor = impl._anchor;
@ -164,11 +167,11 @@ if (!Selection.prototype.modify) {
} }
} }
} else if (granularity === 'word') { } else if (granularity === 'word') {
const anchorNode = this.anchorNode; const anchorNode = this.anchorNode!;
const targetTextContent = const targetTextContent =
direction === 'backward' direction === 'backward'
? anchorNode.textContent.slice(0, this.anchorOffset) ? anchorNode.textContent!.slice(0, this.anchorOffset)
: anchorNode.textContent.slice(this.anchorOffset); : anchorNode.textContent!.slice(this.anchorOffset);
const segments = getWordsFromString(targetTextContent); const segments = getWordsFromString(targetTextContent);
const segmentsLength = segments.length; const segmentsLength = segments.length;
let index = anchor.offset; let index = anchor.offset;
@ -218,27 +221,27 @@ if (!Selection.prototype.modify) {
}; };
} }
export function printWhitespace(whitespaceCharacter) { export function printWhitespace(whitespaceCharacter: string) {
return whitespaceCharacter.charCodeAt(0) === 160 return whitespaceCharacter.charCodeAt(0) === 160
? '&nbsp;' ? '&nbsp;'
: whitespaceCharacter; : whitespaceCharacter;
} }
export function insertText(text) { export function insertText(text: string) {
return { return {
text, text,
type: 'insert_text', type: 'insert_text',
}; };
} }
export function insertTokenNode(text) { export function insertTokenNode(text: string) {
return { return {
text, text,
type: 'insert_token_node', type: 'insert_token_node',
}; };
} }
export function insertSegmentedNode(text) { export function insertSegmentedNode(text: string) {
return { return {
text, text,
type: 'insert_segmented_node', type: 'insert_segmented_node',
@ -385,10 +388,10 @@ export function pasteHTML(text: string) {
} }
export function moveNativeSelection( export function moveNativeSelection(
anchorPath, anchorPath: number[],
anchorOffset, anchorOffset: number,
focusPath, focusPath: number[],
focusOffset, focusOffset: number,
) { ) {
return { return {
anchorOffset, anchorOffset,
@ -399,7 +402,7 @@ export function moveNativeSelection(
}; };
} }
export function getNodeFromPath(path, rootElement) { export function getNodeFromPath(path: number[], rootElement: Node) {
let node = rootElement; let node = rootElement;
for (let i = 0; i < path.length; i++) { for (let i = 0; i < path.length; i++) {
@ -410,12 +413,12 @@ export function getNodeFromPath(path, rootElement) {
} }
export function setNativeSelection( export function setNativeSelection(
anchorNode, anchorNode: Node,
anchorOffset, anchorOffset: number,
focusNode, focusNode: Node,
focusOffset, focusOffset: number,
) { ) {
const domSelection = window.getSelection(); const domSelection = window.getSelection()!;
const range = document.createRange(); const range = document.createRange();
range.setStart(anchorNode, anchorOffset); range.setStart(anchorNode, anchorOffset);
range.setEnd(focusNode, focusOffset); range.setEnd(focusNode, focusOffset);
@ -427,18 +430,18 @@ export function setNativeSelection(
} }
export function setNativeSelectionWithPaths( export function setNativeSelectionWithPaths(
rootElement, rootElement: Node,
anchorPath, anchorPath: number[],
anchorOffset, anchorOffset: number,
focusPath, focusPath: number[],
focusOffset, focusOffset: number,
) { ) {
const anchorNode = getNodeFromPath(anchorPath, rootElement); const anchorNode = getNodeFromPath(anchorPath, rootElement);
const focusNode = getNodeFromPath(focusPath, rootElement); const focusNode = getNodeFromPath(focusPath, rootElement);
setNativeSelection(anchorNode, anchorOffset, focusNode, focusOffset); setNativeSelection(anchorNode, anchorOffset, focusNode, focusOffset);
} }
function getLastTextNode(startingNode) { function getLastTextNode(startingNode: Node) {
let node = startingNode; let node = startingNode;
mainLoop: while (node !== null) { mainLoop: while (node !== null) {
@ -477,7 +480,7 @@ function getLastTextNode(startingNode) {
return null; return null;
} }
function getNextTextNode(startingNode) { function getNextTextNode(startingNode: Node) {
let node = startingNode; let node = startingNode;
mainLoop: while (node !== null) { mainLoop: while (node !== null) {
@ -517,12 +520,14 @@ function getNextTextNode(startingNode) {
} }
function moveNativeSelectionBackward() { function moveNativeSelectionBackward() {
const domSelection = window.getSelection(); const domSelection = window.getSelection()!;
let {anchorNode, anchorOffset} = domSelection; let anchorNode = domSelection.anchorNode!;
let anchorOffset = domSelection.anchorOffset!;
if (domSelection.isCollapsed) { if (domSelection.isCollapsed) {
const target = const target = (
anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode; anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode
)!;
const keyDownEvent = new KeyboardEvent('keydown', { const keyDownEvent = new KeyboardEvent('keydown', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
@ -539,7 +544,7 @@ function moveNativeSelectionBackward() {
if (lastTextNode === null) { if (lastTextNode === null) {
throw new Error('moveNativeSelectionBackward: TODO'); throw new Error('moveNativeSelectionBackward: TODO');
} else { } else {
const textLength = lastTextNode.nodeValue.length; const textLength = lastTextNode.nodeValue!.length;
setNativeSelection( setNativeSelection(
lastTextNode, lastTextNode,
textLength, textLength,
@ -557,7 +562,7 @@ function moveNativeSelectionBackward() {
} }
} else if (anchorNode.nodeType === 1) { } else if (anchorNode.nodeType === 1) {
if (anchorNode.nodeName === 'BR') { if (anchorNode.nodeName === 'BR') {
const parentNode = anchorNode.parentNode; const parentNode = anchorNode.parentNode!;
const childNodes = Array.from(parentNode.childNodes); const childNodes = Array.from(parentNode.childNodes);
anchorOffset = childNodes.indexOf(anchorNode as ChildNode); anchorOffset = childNodes.indexOf(anchorNode as ChildNode);
anchorNode = parentNode; anchorNode = parentNode;
@ -584,12 +589,14 @@ function moveNativeSelectionBackward() {
} }
function moveNativeSelectionForward() { function moveNativeSelectionForward() {
const domSelection = window.getSelection(); const domSelection = window.getSelection()!;
const {anchorNode, anchorOffset} = domSelection; const anchorNode = domSelection.anchorNode!;
const anchorOffset = domSelection.anchorOffset!;
if (domSelection.isCollapsed) { if (domSelection.isCollapsed) {
const target = const target = (
anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode; anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode
)!;
const keyDownEvent = new KeyboardEvent('keydown', { const keyDownEvent = new KeyboardEvent('keydown', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
@ -600,7 +607,7 @@ function moveNativeSelectionForward() {
if (!keyDownEvent.defaultPrevented) { if (!keyDownEvent.defaultPrevented) {
if (anchorNode.nodeType === 3) { if (anchorNode.nodeType === 3) {
const text = anchorNode.nodeValue; const text = anchorNode.nodeValue!;
if (text.length === anchorOffset) { if (text.length === anchorOffset) {
const nextTextNode = getNextTextNode(anchorNode); const nextTextNode = getNextTextNode(anchorNode);
@ -635,8 +642,13 @@ function moveNativeSelectionForward() {
} }
} }
export async function applySelectionInputs(inputs, update, editor) { export async function applySelectionInputs(
const rootElement = editor.getRootElement(); // eslint-disable-next-line @typescript-eslint/no-explicit-any
inputs: Record<string, any>[],
update: (fn: () => void) => Promise<void>,
editor: LexicalEditor,
) {
const rootElement = editor.getRootElement()!;
for (let i = 0; i < inputs.length; i++) { for (let i = 0; i < inputs.length; i++) {
const input = inputs[i]; const input = inputs[i];
@ -644,7 +656,7 @@ export async function applySelectionInputs(inputs, update, editor) {
for (let j = 0; j < times; j++) { for (let j = 0; j < times; j++) {
await update(() => { await update(() => {
const selection = $getSelection(); const selection = $getSelection()!;
switch (input.type) { switch (input.type) {
case 'insert_text': { case 'insert_text': {
@ -800,7 +812,7 @@ export async function applySelectionInputs(inputs, update, editor) {
}), }),
{ {
clipboardData: { clipboardData: {
getData: (type) => { getData: (type: string) => {
if (type === 'text/plain') { if (type === 'text/plain') {
return input.text; return input.text;
} }
@ -823,7 +835,7 @@ export async function applySelectionInputs(inputs, update, editor) {
}), }),
{ {
clipboardData: { clipboardData: {
getData: (type) => { getData: (type: string) => {
if (type === 'application/x-lexical-editor') { if (type === 'application/x-lexical-editor') {
return input.text; return input.text;
} }
@ -846,7 +858,7 @@ export async function applySelectionInputs(inputs, update, editor) {
}), }),
{ {
clipboardData: { clipboardData: {
getData: (type) => { getData: (type: string) => {
if (type === 'text/html') { if (type === 'text/html') {
return input.text; return input.text;
} }
@ -865,7 +877,9 @@ export async function applySelectionInputs(inputs, update, editor) {
} }
} }
export function setAnchorPoint(point) { export function setAnchorPoint(
point: Pick<PointType, 'type' | 'offset' | 'key'>,
) {
const selection = $getSelection(); const selection = $getSelection();
if (!$isRangeSelection(selection)) { if (!$isRangeSelection(selection)) {
@ -884,7 +898,9 @@ export function setAnchorPoint(point) {
anchor.key = point.key; anchor.key = point.key;
} }
export function setFocusPoint(point) { export function setFocusPoint(
point: Pick<PointType, 'type' | 'offset' | 'key'>,
) {
const selection = $getSelection(); const selection = $getSelection();
if (!$isRangeSelection(selection)) { if (!$isRangeSelection(selection)) {

View File

@ -13,23 +13,27 @@ import {
$createTextNode, $createTextNode,
$getRoot, $getRoot,
$setSelection, $setSelection,
EditorState,
ParagraphNode,
RootNode,
TextNode,
} from 'lexical'; } from 'lexical';
import {createTestEditor} from 'lexical/src/__tests__/utils'; import {createTestEditor} from 'lexical/src/__tests__/utils';
import {createRef, useEffect, useMemo} from 'react'; import {createRef, useEffect, useMemo} from 'react';
import {createRoot} from 'react-dom/client'; import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils'; import * as ReactTestUtils from 'react-dom/test-utils';
describe('table selection', () => { describe('table selection', () => {
let originalText; let originalText: TextNode;
let parsedParagraph; let parsedParagraph: ParagraphNode;
let parsedRoot; let parsedRoot: RootNode;
let parsedText; let parsedText: TextNode;
let paragraphKey; let paragraphKey: string;
let textKey; let textKey: string;
let parsedEditorState; let parsedEditorState: EditorState;
let reactRoot; let reactRoot: Root;
let container: HTMLDivElement | null = null; let container: HTMLDivElement | null = null;
let editor: LexicalEditor = null; let editor: LexicalEditor | null = null;
beforeEach(() => { beforeEach(() => {
container = document.createElement('div'); container = document.createElement('div');
@ -37,7 +41,10 @@ describe('table selection', () => {
document.body.appendChild(container); document.body.appendChild(container);
}); });
function useLexicalEditor(rootElementRef, onError) { function useLexicalEditor(
rootElementRef: React.RefObject<HTMLDivElement>,
onError?: () => void,
) {
const editorInHook = useMemo( const editorInHook = useMemo(
() => () =>
createTestEditor({ createTestEditor({
@ -77,8 +84,8 @@ describe('table selection', () => {
}); });
} }
async function update(fn) { async function update(fn: () => void) {
editor.update(fn); editor!.update(fn);
return Promise.resolve().then(); return Promise.resolve().then();
} }
@ -101,15 +108,15 @@ describe('table selection', () => {
}); });
const stringifiedEditorState = JSON.stringify( const stringifiedEditorState = JSON.stringify(
editor.getEditorState().toJSON(), editor!.getEditorState().toJSON(),
); );
parsedEditorState = editor.parseEditorState(stringifiedEditorState); parsedEditorState = editor!.parseEditorState(stringifiedEditorState);
parsedEditorState.read(() => { parsedEditorState.read(() => {
parsedRoot = $getRoot(); parsedRoot = $getRoot();
parsedParagraph = parsedRoot.getFirstChild(); parsedParagraph = parsedRoot.getFirstChild()!;
paragraphKey = parsedParagraph.getKey(); paragraphKey = parsedParagraph.getKey();
parsedText = parsedParagraph.getFirstChild(); parsedText = parsedParagraph.getFirstChild()!;
textKey = parsedText.getKey(); textKey = parsedText.getKey();
}); });
}); });

View File

@ -22,6 +22,7 @@ import {
pasteHTML, pasteHTML,
} from '@lexical/selection/src/__tests__/utils'; } from '@lexical/selection/src/__tests__/utils';
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table'; import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
import {LexicalEditor} from 'lexical';
import {initializeClipboard, TestComposer} from 'lexical/src/__tests__/utils'; import {initializeClipboard, TestComposer} from 'lexical/src/__tests__/utils';
import {createRoot} from 'react-dom/client'; import {createRoot} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils'; import * as ReactTestUtils from 'react-dom/test-utils';
@ -72,7 +73,7 @@ Range.prototype.getBoundingClientRect = function (): DOMRect {
}; };
describe('LexicalEventHelpers', () => { describe('LexicalEventHelpers', () => {
let container = null; let container: HTMLDivElement | null = null;
beforeEach(async () => { beforeEach(async () => {
container = document.createElement('div'); container = document.createElement('div');
@ -81,15 +82,15 @@ describe('LexicalEventHelpers', () => {
}); });
afterEach(() => { afterEach(() => {
document.body.removeChild(container); document.body.removeChild(container!);
container = null; container = null;
}); });
let editor = null; let editor: LexicalEditor | null = null;
async function init() { async function init() {
function TestBase() { function TestBase() {
function TestPlugin(): JSX.Element { function TestPlugin(): null {
[editor] = useLexicalComposerContext(); [editor] = useLexicalComposerContext();
return null; return null;
@ -146,8 +147,8 @@ describe('LexicalEventHelpers', () => {
}}> }}>
<RichTextPlugin <RichTextPlugin
contentEditable={ contentEditable={
// eslint-disable-next-line jsx-a11y/aria-role // eslint-disable-next-line jsx-a11y/aria-role, @typescript-eslint/no-explicit-any
<ContentEditable role={null} spellCheck={null} /> <ContentEditable role={null as any} spellCheck={null as any} />
} }
placeholder={null} placeholder={null}
ErrorBoundary={LexicalErrorBoundary} ErrorBoundary={LexicalErrorBoundary}
@ -159,20 +160,20 @@ describe('LexicalEventHelpers', () => {
} }
ReactTestUtils.act(() => { ReactTestUtils.act(() => {
createRoot(container).render(<TestBase />); createRoot(container!).render(<TestBase />);
}); });
} }
async function update(fn) { async function update(fn: () => void) {
await ReactTestUtils.act(async () => { await ReactTestUtils.act(async () => {
await editor.update(fn); await editor!.update(fn);
}); });
return Promise.resolve().then(); return Promise.resolve().then();
} }
test('Expect initial output to be a block with no text', () => { test('Expect initial output to be a block with no text', () => {
expect(container.innerHTML).toBe( expect(container!.innerHTML).toBe(
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><br></p></div>', '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><br></p></div>',
); );
}); });
@ -344,10 +345,10 @@ describe('LexicalEventHelpers', () => {
const name = testUnit.name || 'Test case'; const name = testUnit.name || 'Test case';
test(name + ` (#${i + 1})`, async () => { test(name + ` (#${i + 1})`, async () => {
await applySelectionInputs(testUnit.inputs, update, editor); await applySelectionInputs(testUnit.inputs, update, editor!);
// Validate HTML matches // Validate HTML matches
expect(container.innerHTML).toBe(testUnit.expectedHTML); expect(container!.innerHTML).toBe(testUnit.expectedHTML);
}); });
}); });
}); });
@ -400,10 +401,10 @@ describe('LexicalEventHelpers', () => {
const name = testUnit.name || 'Test case'; const name = testUnit.name || 'Test case';
test(name + ` (#${i + 1})`, async () => { test(name + ` (#${i + 1})`, async () => {
await applySelectionInputs(testUnit.inputs, update, editor); await applySelectionInputs(testUnit.inputs, update, editor!);
// Validate HTML matches // Validate HTML matches
expect(container.innerHTML).toBe(testUnit.expectedHTML); expect(container!.innerHTML).toBe(testUnit.expectedHTML);
}); });
}); });
}); });
@ -673,12 +674,14 @@ describe('LexicalEventHelpers', () => {
const name = testUnit.name || 'Test case'; const name = testUnit.name || 'Test case';
// eslint-disable-next-line no-only-tests/no-only-tests, dot-notation // eslint-disable-next-line no-only-tests/no-only-tests, dot-notation
const test_ = testUnit['only'] ? test.only : test; const test_ = 'only' in testUnit && testUnit['only'] ? test.only : test;
test_(name + ` (#${i + 1})`, async () => { test_(name + ` (#${i + 1})`, async () => {
await applySelectionInputs(testUnit.inputs, update, editor); await applySelectionInputs(testUnit.inputs, update, editor!);
// Validate HTML matches // Validate HTML matches
expect(container.firstChild.innerHTML).toBe(testUnit.expectedHTML); expect((container!.firstChild as HTMLElement).innerHTML).toBe(
testUnit.expectedHTML,
);
}); });
}); });
}); });

View File

@ -127,7 +127,7 @@ describe('LexicalNodeHelpers tests', () => {
editor.getEditorState().read(() => { editor.getEditorState().read(() => {
const expectedNodes = expectedKeys.map(({depth, node: nodeKey}) => ({ const expectedNodes = expectedKeys.map(({depth, node: nodeKey}) => ({
depth, depth,
node: $getNodeByKey(nodeKey).getLatest(), node: $getNodeByKey(nodeKey)!.getLatest(),
})); }));
const first = expectedNodes[0]; const first = expectedNodes[0];
@ -147,10 +147,10 @@ describe('LexicalNodeHelpers tests', () => {
test('DFS triggers getLatest()', async () => { test('DFS triggers getLatest()', async () => {
const editor: LexicalEditor = testEnv.editor; const editor: LexicalEditor = testEnv.editor;
let rootKey; let rootKey: string;
let paragraphKey; let paragraphKey: string;
let block1Key; let block1Key: string;
let block2Key; let block2Key: string;
await editor.update(() => { await editor.update(() => {
const root = $getRoot(); const root = $getRoot();
@ -179,14 +179,14 @@ describe('LexicalNodeHelpers tests', () => {
block1.append(block3); block1.append(block3);
expect($dfs(root)).toEqual([ expect($dfs(root!)).toEqual([
{ {
depth: 0, depth: 0,
node: root.getLatest(), node: root!.getLatest(),
}, },
{ {
depth: 1, depth: 1,
node: paragraph.getLatest(), node: paragraph!.getLatest(),
}, },
{ {
depth: 2, depth: 2,
@ -198,7 +198,7 @@ describe('LexicalNodeHelpers tests', () => {
}, },
{ {
depth: 2, depth: 2,
node: block2.getLatest(), node: block2!.getLatest(),
}, },
]); ]);
}); });

View File

@ -17,7 +17,7 @@ import {$splitNode} from '../../index';
describe('LexicalUtils#splitNode', () => { describe('LexicalUtils#splitNode', () => {
let editor: LexicalEditor; let editor: LexicalEditor;
const update = async (updateFn) => { const update = async (updateFn: () => void) => {
editor.update(updateFn); editor.update(updateFn);
await Promise.resolve(); await Promise.resolve();
}; };
@ -115,7 +115,7 @@ describe('LexicalUtils#splitNode', () => {
let nodeToSplit: ElementNode = $getRoot(); let nodeToSplit: ElementNode = $getRoot();
for (const index of testCase.splitPath) { for (const index of testCase.splitPath) {
nodeToSplit = nodeToSplit.getChildAtIndex(index); nodeToSplit = nodeToSplit.getChildAtIndex(index)!;
if (!$isElementNode(nodeToSplit)) { if (!$isElementNode(nodeToSplit)) {
throw new Error('Expected node to be element'); throw new Error('Expected node to be element');
} }

View File

@ -25,7 +25,7 @@ import {$insertNodeToNearestRoot} from '../..';
describe('LexicalUtils#insertNodeToNearestRoot', () => { describe('LexicalUtils#insertNodeToNearestRoot', () => {
let editor: LexicalEditor; let editor: LexicalEditor;
const update = async (updateFn) => { const update = async (updateFn: () => void) => {
editor.update(updateFn); editor.update(updateFn);
await Promise.resolve(); await Promise.resolve();
}; };
@ -155,7 +155,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
'Expected node to be element (to traverse the tree)', 'Expected node to be element (to traverse the tree)',
); );
} }
selectionNode = selectionNode.getChildAtIndex(index); selectionNode = selectionNode.getChildAtIndex(index)!;
} }
// Calling selectionNode.select() would "normalize" selection and move it // Calling selectionNode.select() would "normalize" selection and move it

View File

@ -37,6 +37,55 @@ If you've used React Hooks before, you can think of `$` functions as being somet
Internally, we've found this scales really well and developers get to grips with it in almost no time at all. Internally, we've found this scales really well and developers get to grips with it in almost no time at all.
## When does reconciliation happen?
Reconciliation is scheduled with
[queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask),
which means that it will happen very soon, but asynchronously. This is similar
to something like `setTimeout(reconcile, 0)` with a bit more immediacy or
`Promise.resolve().then(reconcile)` with less overhead. This is done so
that all of the updates that occur as a result of a single logical event will
be batched into one reconciliation.
You can force a reconciliation to take place synchronously with the discrete
option to `editor.update` (demonstrated below).
## Why do tests use `await editor.update(…)`
You may notice that many tests look like this:
```js
await editor.update(updateA);
await editor.update(updateB);
```
An astute observer would notice that this seems very strange, since
`editor.update()` returns `void` and not `Promise<void>`. However,
it does happen to work as you would want it to because
the implementation of Promise uses the same microtask queue.
It's not recommended to rely on this in browser code as it could depend on
implementation details of the compilers, bundlers, and VM. It's best to stick
to using the `discrete` or the `onUpdate` callback options to be sure that
the reconciliation has taken place.
Ignoring any other microtasks that were scheduled elsewhere,
it is roughly equivalent to this synchronous code:
```js
editor.update(updateA, {discrete: true});
editor.update(updateB, {discrete: true});
```
At a high level, very roughly, the order of operations looks like this:
1. `editor.update()` is called
2. `updateA()` is called and updates the editor state
3. `editor.update()` schedules a reconciliation microtask and returns
4. `await` schedules a resume microtask and yields control to the task executor
5. the reconciliation microtask runs, reconciling the editor state with the DOM
6. the resume microtask runs
## How do I listen for user text insertions? ## How do I listen for user text insertions?
Listening to text insertion events is problematic with content editables in general. It's a common source of bugs due to how Listening to text insertion events is problematic with content editables in general. It's a common source of bugs due to how

View File

@ -207,7 +207,6 @@
"include": ["./libdefs", "./packages"], "include": ["./libdefs", "./packages"],
"exclude": [ "exclude": [
"./libdefs/*.js", "./libdefs/*.js",
"**/lexical-*/src/__tests__/**",
"**/dist/**", "**/dist/**",
"**/npm/**", "**/npm/**",
"**/node_modules/**", "**/node_modules/**",

View File

@ -10,7 +10,6 @@
"**/dist/**", "**/dist/**",
"**/npm/**", "**/npm/**",
"**/node_modules/**", "**/node_modules/**",
"./packages/playwright-core/**",
"./packages/lexical-devtools/**" "./packages/lexical-devtools/**"
], ],
"extends": "./tsconfig.json" "extends": "./tsconfig.json"