mirror of
https://github.com/facebook/lexical.git
synced 2025-05-16 22:48:52 +08:00
chore: allow tsc to typecheck tests, fix type issues in those tests (#5982)
This commit is contained in:
@ -178,7 +178,7 @@ describe('LexicalCodeNode tests', () => {
|
||||
const code = $createCodeNode();
|
||||
root.append(code);
|
||||
code.selectStart();
|
||||
$getSelection().insertText('function');
|
||||
$getSelection()!.insertText('function');
|
||||
});
|
||||
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
|
||||
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
|
||||
// appending more text
|
||||
let tabKey;
|
||||
let tabKey: string;
|
||||
await editor.update(() => {
|
||||
tabKey = $dfs()
|
||||
.find(({node}) => $isTabNode(node))
|
||||
.find(({node}) => $isTabNode(node))!
|
||||
.node.getKey();
|
||||
$getSelection().insertText('foo');
|
||||
$getSelection()!.insertText('foo');
|
||||
});
|
||||
expect(
|
||||
editor.getEditorState().read(() => {
|
||||
@ -214,7 +214,7 @@ describe('LexicalCodeNode tests', () => {
|
||||
const code = $createCodeNode();
|
||||
root.append(code);
|
||||
code.selectStart();
|
||||
$getSelection().insertText('function');
|
||||
$getSelection()!.insertText('function');
|
||||
});
|
||||
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
|
||||
await editor.update(() => {
|
||||
@ -238,7 +238,7 @@ describe('LexicalCodeNode tests', () => {
|
||||
const code = $createCodeNode();
|
||||
root.append(code);
|
||||
code.selectStart();
|
||||
$getSelection().insertText('function');
|
||||
$getSelection()!.insertText('function');
|
||||
});
|
||||
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
|
||||
await editor.update(() => {
|
||||
@ -253,8 +253,8 @@ describe('LexicalCodeNode tests', () => {
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const codeTab = root.getFirstDescendant();
|
||||
const codeText = root.getLastDescendant();
|
||||
const codeTab = root.getFirstDescendant()!;
|
||||
const codeText = root.getLastDescendant()!;
|
||||
const selection = $createRangeSelection();
|
||||
selection.anchor.set(codeTab.getKey(), 0, 'text');
|
||||
selection.focus.set(codeText.getKey(), 'function'.length, 'text');
|
||||
@ -275,7 +275,7 @@ describe('LexicalCodeNode tests', () => {
|
||||
const code = $createCodeNode();
|
||||
root.append(code);
|
||||
code.selectStart();
|
||||
$getSelection().insertText('function');
|
||||
$getSelection()!.insertText('function');
|
||||
});
|
||||
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
|
||||
await editor.update(() => {
|
||||
@ -290,8 +290,8 @@ describe('LexicalCodeNode tests', () => {
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const codeTab = root.getFirstDescendant();
|
||||
const codeText = root.getLastDescendant();
|
||||
const codeTab = root.getFirstDescendant()!;
|
||||
const codeText = root.getLastDescendant()!;
|
||||
const selection = $createRangeSelection();
|
||||
selection.anchor.set(codeTab.getKey(), 0, 'text');
|
||||
selection.focus.set(codeText.getKey(), 0, 'text');
|
||||
@ -313,12 +313,12 @@ describe('LexicalCodeNode tests', () => {
|
||||
const code = $createCodeNode();
|
||||
root.append(code);
|
||||
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
|
||||
await editor.update(() => {
|
||||
const firstCodeText = $getRoot().getFirstDescendant();
|
||||
const lastCodeText = $getRoot().getLastDescendant();
|
||||
const firstCodeText = $getRoot().getFirstDescendant()!;
|
||||
const lastCodeText = $getRoot().getLastDescendant()!;
|
||||
const selection = $createRangeSelection();
|
||||
selection.anchor.set(firstCodeText.getKey(), 1, 'text');
|
||||
selection.focus.set(lastCodeText.getKey(), 1, 'text');
|
||||
@ -347,7 +347,7 @@ describe('LexicalCodeNode tests', () => {
|
||||
const code = $createCodeNode();
|
||||
root.append(code);
|
||||
code.selectStart();
|
||||
$getSelection().insertRawText('hello\n');
|
||||
$getSelection()!.insertRawText('hello\n');
|
||||
});
|
||||
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
|
||||
expect(testEnv.innerHTML)
|
||||
@ -365,7 +365,7 @@ describe('LexicalCodeNode tests', () => {
|
||||
const code = $createCodeNode();
|
||||
root.append(code);
|
||||
code.selectStart();
|
||||
$getSelection().insertRawText('\thello');
|
||||
$getSelection()!.insertRawText('\thello');
|
||||
});
|
||||
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
|
||||
await editor.update(() => {
|
||||
@ -389,7 +389,7 @@ describe('LexicalCodeNode tests', () => {
|
||||
const code = $createCodeNode();
|
||||
root.append(code);
|
||||
code.selectStart();
|
||||
$getSelection().insertRawText('abc\tdef\nghi\tjkl');
|
||||
$getSelection()!.insertRawText('abc\tdef\nghi\tjkl');
|
||||
});
|
||||
const keyEvent = new KeyboardEventMock();
|
||||
keyEvent.altKey = true;
|
||||
@ -409,16 +409,16 @@ describe('LexicalCodeNode tests', () => {
|
||||
const code = $createCodeNode();
|
||||
root.append(code);
|
||||
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
|
||||
await editor.update(() => {
|
||||
const firstCodeText = $getRoot().getFirstDescendant();
|
||||
const firstCodeText = $getRoot().getFirstDescendant()!;
|
||||
const secondCodeText = firstCodeText
|
||||
.getNextSibling() // tab
|
||||
.getNextSibling() // def
|
||||
.getNextSibling() // linebreak
|
||||
.getNextSibling(); // ghi;
|
||||
.getNextSibling()! // tab
|
||||
.getNextSibling()! // def
|
||||
.getNextSibling()! // linebreak
|
||||
.getNextSibling()!; // ghi;
|
||||
const selection = $createRangeSelection();
|
||||
selection.anchor.set(firstCodeText.getKey(), 1, 'text');
|
||||
selection.focus.set(secondCodeText.getKey(), 1, 'text');
|
||||
@ -455,7 +455,7 @@ describe('LexicalCodeNode tests', () => {
|
||||
const code = $createCodeNode();
|
||||
root.append(code);
|
||||
code.selectStart();
|
||||
const selection = $getSelection();
|
||||
const selection = $getSelection()!;
|
||||
if (tabOrSpaces === 'tab') {
|
||||
selection.insertRawText('\t\tfunction foo\n\t\tfunction bar');
|
||||
} else {
|
||||
@ -570,7 +570,7 @@ describe('LexicalCodeNode tests', () => {
|
||||
const firstChild = code.getFirstChild();
|
||||
invariant($isTextNode(firstChild));
|
||||
if (tabOrSpaces === 'tab') {
|
||||
firstChild.getNextSibling().selectNext(0, 0);
|
||||
firstChild.getNextSibling()!.selectNext(0, 0);
|
||||
} else {
|
||||
firstChild.select(4, 4);
|
||||
}
|
||||
@ -605,7 +605,7 @@ describe('LexicalCodeNode tests', () => {
|
||||
$isLineBreakNode(dfsNode.node),
|
||||
)[0].node;
|
||||
if (tabOrSpaces === 'tab') {
|
||||
const firstTab = linebreak.getNextSibling();
|
||||
const firstTab = linebreak.getNextSibling()!;
|
||||
firstTab.selectNext();
|
||||
} else {
|
||||
linebreak.selectNext(4, 4);
|
||||
@ -687,7 +687,7 @@ describe('LexicalCodeNode tests', () => {
|
||||
$isLineBreakNode(dfsNode.node),
|
||||
)[0].node;
|
||||
if (tabOrSpaces === 'tab') {
|
||||
const firstTab = linebreak.getNextSibling();
|
||||
const firstTab = linebreak.getNextSibling()!;
|
||||
firstTab.selectNext(0, 0);
|
||||
} else {
|
||||
linebreak.selectNext(2, 2);
|
||||
|
@ -16,7 +16,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import type {RangeSelection} from 'lexical';
|
||||
import type {EditorState, LexicalEditor, RangeSelection} from 'lexical';
|
||||
|
||||
import {$generateHtmlFromNodes} from '@lexical/html';
|
||||
import {JSDOM} from 'jsdom';
|
||||
@ -33,14 +33,17 @@ import {
|
||||
import {createHeadlessEditor} from '../..';
|
||||
|
||||
describe('LexicalHeadlessEditor', () => {
|
||||
let editor;
|
||||
let editor: LexicalEditor;
|
||||
|
||||
async function update(updateFn) {
|
||||
async function update(updateFn: () => void) {
|
||||
editor.update(updateFn);
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
function assertEditorState(editorState, nodes) {
|
||||
function assertEditorState(
|
||||
editorState: EditorState,
|
||||
nodes: Record<string, unknown>[],
|
||||
) {
|
||||
const nodesFromState = Array.from(editorState._nodeMap.values());
|
||||
expect(nodesFromState).toEqual(
|
||||
nodes.map((node) => expect.objectContaining(node)),
|
||||
|
@ -34,12 +34,12 @@ import {
|
||||
} from 'lexical/src';
|
||||
import {createTestEditor, TestComposer} from 'lexical/src/__tests__/utils';
|
||||
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';
|
||||
|
||||
describe('LexicalHistory tests', () => {
|
||||
let container: HTMLDivElement | null = null;
|
||||
let reactRoot;
|
||||
let reactRoot: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
|
@ -6,7 +6,12 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import {$createParagraphNode, $getRoot, TextNode} from 'lexical';
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$createRangeSelection,
|
||||
$getRoot,
|
||||
TextNode,
|
||||
} from 'lexical';
|
||||
import {
|
||||
expectHtmlToBeEqual,
|
||||
html,
|
||||
@ -147,10 +152,10 @@ describe('LexicalListItemNode tests', () => {
|
||||
});
|
||||
|
||||
describe('ListItemNode.replace()', () => {
|
||||
let listNode;
|
||||
let listItemNode1;
|
||||
let listItemNode2;
|
||||
let listItemNode3;
|
||||
let listNode: ListNode;
|
||||
let listItemNode1: ListItemNode;
|
||||
let listItemNode2: ListItemNode;
|
||||
let listItemNode3: ListItemNode;
|
||||
|
||||
beforeEach(async () => {
|
||||
const {editor} = testEnv;
|
||||
@ -391,7 +396,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
// - B
|
||||
test('siblings are not nested', async () => {
|
||||
const {editor} = testEnv;
|
||||
let x;
|
||||
let x: ListItemNode;
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
@ -459,7 +464,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
// - B
|
||||
test('the previous sibling is nested', async () => {
|
||||
const {editor} = testEnv;
|
||||
let x;
|
||||
let x: ListItemNode;
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
@ -539,7 +544,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
// - B
|
||||
test('the next sibling is nested', async () => {
|
||||
const {editor} = testEnv;
|
||||
let x;
|
||||
let x: ListItemNode;
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
@ -619,7 +624,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
// - B
|
||||
test('both siblings are nested', async () => {
|
||||
const {editor} = testEnv;
|
||||
let x;
|
||||
let x: ListItemNode;
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
@ -708,7 +713,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
// - B
|
||||
test('the previous sibling is nested deeper than the next sibling', async () => {
|
||||
const {editor} = testEnv;
|
||||
let x;
|
||||
let x: ListItemNode;
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
@ -818,7 +823,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
// - B2
|
||||
test('the next sibling is nested deeper than the previous sibling', async () => {
|
||||
const {editor} = testEnv;
|
||||
let x;
|
||||
let x: ListItemNode;
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
@ -929,7 +934,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
// - B2
|
||||
test('both siblings are deeply nested', async () => {
|
||||
const {editor} = testEnv;
|
||||
let x;
|
||||
let x: ListItemNode;
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
@ -1052,10 +1057,10 @@ describe('LexicalListItemNode tests', () => {
|
||||
});
|
||||
|
||||
describe('ListItemNode.insertNewAfter(): non-empty list items', () => {
|
||||
let listNode;
|
||||
let listItemNode1;
|
||||
let listItemNode2;
|
||||
let listItemNode3;
|
||||
let listNode: ListNode;
|
||||
let listItemNode1: ListItemNode;
|
||||
let listItemNode2: ListItemNode;
|
||||
let listItemNode3: ListItemNode;
|
||||
|
||||
beforeEach(async () => {
|
||||
const {editor} = testEnv;
|
||||
@ -1103,7 +1108,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
const {editor} = testEnv;
|
||||
|
||||
await editor.update(() => {
|
||||
listItemNode1.insertNewAfter();
|
||||
listItemNode1.insertNewAfter($createRangeSelection());
|
||||
});
|
||||
|
||||
expectHtmlToBeEqual(
|
||||
@ -1134,7 +1139,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
const {editor} = testEnv;
|
||||
|
||||
await editor.update(() => {
|
||||
listItemNode3.insertNewAfter();
|
||||
listItemNode3.insertNewAfter($createRangeSelection());
|
||||
});
|
||||
|
||||
expectHtmlToBeEqual(
|
||||
@ -1165,7 +1170,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
const {editor} = testEnv;
|
||||
|
||||
await editor.update(() => {
|
||||
listItemNode3.insertNewAfter();
|
||||
listItemNode3.insertNewAfter($createRangeSelection());
|
||||
});
|
||||
|
||||
expectHtmlToBeEqual(
|
||||
@ -1217,7 +1222,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
);
|
||||
|
||||
await editor.update(() => {
|
||||
listItemNode1.insertNewAfter();
|
||||
listItemNode1.insertNewAfter($createRangeSelection());
|
||||
});
|
||||
|
||||
expectHtmlToBeEqual(
|
||||
@ -1264,9 +1269,9 @@ describe('LexicalListItemNode tests', () => {
|
||||
});
|
||||
|
||||
describe('ListItemNode.setIndent()', () => {
|
||||
let listNode;
|
||||
let listItemNode1;
|
||||
let listItemNode2;
|
||||
let listNode: ListNode;
|
||||
let listItemNode1: ListItemNode;
|
||||
let listItemNode2: ListItemNode;
|
||||
|
||||
beforeEach(async () => {
|
||||
const {editor} = testEnv;
|
||||
@ -1296,7 +1301,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
});
|
||||
|
||||
expectHtmlToBeEqual(
|
||||
editor.getRootElement().innerHTML,
|
||||
editor.getRootElement()!.innerHTML,
|
||||
html`
|
||||
<ul>
|
||||
<li value="1">
|
||||
@ -1330,7 +1335,7 @@ describe('LexicalListItemNode tests', () => {
|
||||
});
|
||||
|
||||
expectHtmlToBeEqual(
|
||||
editor.getRootElement().innerHTML,
|
||||
editor.getRootElement()!.innerHTML,
|
||||
html`
|
||||
<ul>
|
||||
<li value="1" dir="ltr">
|
||||
|
@ -238,7 +238,7 @@ describe('LexicalListNode tests', () => {
|
||||
|
||||
expect(listNode.append(...nodesToAppend)).toBe(listNode);
|
||||
expect($isListItemNode(listNode.getFirstChild())).toBe(true);
|
||||
expect(listNode.getFirstChild<ListItemNode>().getFirstChild()).toBe(
|
||||
expect(listNode.getFirstChild<ListItemNode>()!.getFirstChild()).toBe(
|
||||
nestedListNode,
|
||||
);
|
||||
});
|
||||
|
@ -8,10 +8,10 @@
|
||||
|
||||
import {$createTextNode, $getRoot, ParagraphNode, TextNode} from 'lexical';
|
||||
|
||||
import {createTestConnection, waitForReact} from './utils';
|
||||
import {Client, createTestConnection, waitForReact} from './utils';
|
||||
|
||||
describe('Collaboration', () => {
|
||||
let container = null;
|
||||
let container: null | HTMLDivElement = null;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
@ -19,11 +19,11 @@ describe('Collaboration', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(container!);
|
||||
container = null;
|
||||
});
|
||||
|
||||
async function expectCorrectInitialContent(client1, client2) {
|
||||
async function expectCorrectInitialContent(client1: Client, client2: Client) {
|
||||
// Should be empty, as client has not yet updated
|
||||
expect(client1.getHTML()).toEqual('');
|
||||
expect(client1.getHTML()).toEqual(client2.getHTML());
|
||||
@ -42,8 +42,8 @@ describe('Collaboration', () => {
|
||||
const client1 = connector.createClient('1');
|
||||
const client2 = connector.createClient('2');
|
||||
|
||||
client1.start(container);
|
||||
client2.start(container);
|
||||
client1.start(container!);
|
||||
client2.start(container!);
|
||||
|
||||
await expectCorrectInitialContent(client1, client2);
|
||||
|
||||
@ -56,7 +56,7 @@ describe('Collaboration', () => {
|
||||
|
||||
const text = $createTextNode('Hello world');
|
||||
|
||||
paragraph.append(text);
|
||||
paragraph!.append(text);
|
||||
});
|
||||
});
|
||||
|
||||
@ -71,8 +71,8 @@ describe('Collaboration', () => {
|
||||
client2.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = root.getFirstChild<ParagraphNode>();
|
||||
const text = paragraph.getFirstChild<TextNode>();
|
||||
const paragraph = root.getFirstChild<ParagraphNode>()!;
|
||||
const text = paragraph.getFirstChild<TextNode>()!;
|
||||
|
||||
text.spliceText(6, 5, 'metaverse');
|
||||
});
|
||||
@ -97,8 +97,8 @@ describe('Collaboration', () => {
|
||||
const client1 = connector.createClient('1');
|
||||
const client2 = connector.createClient('2');
|
||||
|
||||
client1.start(container);
|
||||
client2.start(container);
|
||||
client1.start(container!);
|
||||
client2.start(container!);
|
||||
|
||||
await expectCorrectInitialContent(client1, client2);
|
||||
|
||||
@ -109,7 +109,7 @@ describe('Collaboration', () => {
|
||||
client1.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = root.getFirstChild<ParagraphNode>();
|
||||
const paragraph = root.getFirstChild<ParagraphNode>()!;
|
||||
const text = $createTextNode('Hello world');
|
||||
|
||||
paragraph.append(text);
|
||||
@ -125,7 +125,7 @@ describe('Collaboration', () => {
|
||||
client2.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = root.getFirstChild<ParagraphNode>();
|
||||
const paragraph = root.getFirstChild<ParagraphNode>()!;
|
||||
const text = $createTextNode('Hello world');
|
||||
|
||||
paragraph.append(text);
|
||||
@ -157,8 +157,8 @@ describe('Collaboration', () => {
|
||||
client1.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = root.getFirstChild<ParagraphNode>();
|
||||
const text = paragraph.getFirstChild<TextNode>();
|
||||
const paragraph = root.getFirstChild<ParagraphNode>()!;
|
||||
const text = paragraph.getFirstChild<TextNode>()!;
|
||||
|
||||
text.spliceText(11, 11, '');
|
||||
});
|
||||
@ -175,8 +175,8 @@ describe('Collaboration', () => {
|
||||
client2.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = root.getFirstChild<ParagraphNode>();
|
||||
const text = paragraph.getFirstChild<TextNode>();
|
||||
const paragraph = root.getFirstChild<ParagraphNode>()!;
|
||||
const text = paragraph.getFirstChild<TextNode>()!;
|
||||
|
||||
text.spliceText(11, 11, '!');
|
||||
});
|
||||
@ -203,8 +203,8 @@ describe('Collaboration', () => {
|
||||
const connector = createTestConnection();
|
||||
const client1 = connector.createClient('1');
|
||||
const client2 = connector.createClient('2');
|
||||
client1.start(container);
|
||||
client2.start(container);
|
||||
client1.start(container!);
|
||||
client2.start(container!);
|
||||
|
||||
await expectCorrectInitialContent(client1, client2);
|
||||
|
||||
@ -213,7 +213,7 @@ describe('Collaboration', () => {
|
||||
client1.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = root.getFirstChild<ParagraphNode>();
|
||||
const paragraph = root.getFirstChild<ParagraphNode>()!;
|
||||
const text = $createTextNode('Hello world');
|
||||
paragraph.append(text);
|
||||
});
|
||||
@ -235,8 +235,8 @@ describe('Collaboration', () => {
|
||||
client1.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = root.getFirstChild<ParagraphNode>();
|
||||
paragraph.getFirstChild().remove();
|
||||
const paragraph = root.getFirstChild<ParagraphNode>()!;
|
||||
paragraph.getFirstChild()!.remove();
|
||||
});
|
||||
});
|
||||
|
||||
@ -250,9 +250,9 @@ describe('Collaboration', () => {
|
||||
client2.update(() => {
|
||||
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),
|
||||
};
|
||||
|
||||
client1.start(container, awarenessData1);
|
||||
client2.start(container, awarenessData2);
|
||||
client1.start(container!, awarenessData1);
|
||||
client2.start(container!, awarenessData2);
|
||||
|
||||
await expectCorrectInitialContent(client1, client2);
|
||||
|
||||
expect(client1.awareness.getLocalState().awarenessData).toEqual(
|
||||
expect(client1.awareness.getLocalState()!.awarenessData).toEqual(
|
||||
awarenessData1,
|
||||
);
|
||||
expect(client2.awareness.getLocalState().awarenessData).toEqual(
|
||||
expect(client2.awareness.getLocalState()!.awarenessData).toEqual(
|
||||
awarenessData2,
|
||||
);
|
||||
|
||||
|
@ -52,11 +52,11 @@ const $createSelectionByPath = ({
|
||||
const root = $getRoot();
|
||||
|
||||
const anchorNode = anchorPath.reduce(
|
||||
(node, index) => node.getChildAtIndex(index),
|
||||
(node, index) => node.getChildAtIndex(index)!,
|
||||
root,
|
||||
);
|
||||
const focusNode = focusPath.reduce(
|
||||
(node, index) => node.getChildAtIndex(index),
|
||||
(node, index) => node.getChildAtIndex(index)!,
|
||||
root,
|
||||
);
|
||||
|
||||
@ -95,11 +95,11 @@ const $replaceTextByPath = ({
|
||||
focusOffset,
|
||||
focusPath,
|
||||
});
|
||||
selection.insertText(text);
|
||||
selection.insertText(text!);
|
||||
};
|
||||
|
||||
describe('CollaborationWithCollisions', () => {
|
||||
let container = null;
|
||||
let container: HTMLDivElement | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
@ -107,7 +107,7 @@ describe('CollaborationWithCollisions', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(container!);
|
||||
container = null;
|
||||
});
|
||||
|
||||
@ -131,7 +131,7 @@ describe('CollaborationWithCollisions', () => {
|
||||
},
|
||||
() => {
|
||||
// Second client deletes first paragraph
|
||||
$getRoot().getFirstChild().remove();
|
||||
$getRoot().getFirstChild()!.remove();
|
||||
},
|
||||
],
|
||||
expectedHTML: null,
|
||||
@ -152,7 +152,7 @@ describe('CollaborationWithCollisions', () => {
|
||||
},
|
||||
() => {
|
||||
// Second client deletes first paragraph
|
||||
$getRoot().getFirstChild().remove();
|
||||
$getRoot().getFirstChild()!.remove();
|
||||
},
|
||||
],
|
||||
expectedHTML: null,
|
||||
@ -199,7 +199,7 @@ describe('CollaborationWithCollisions', () => {
|
||||
const connection = createTestConnection();
|
||||
const clients = createAndStartClients(
|
||||
connection,
|
||||
container,
|
||||
container!,
|
||||
testCase.clients.length,
|
||||
);
|
||||
|
||||
|
@ -7,14 +7,15 @@
|
||||
*/
|
||||
|
||||
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 {LexicalComposer} from '../../LexicalComposer';
|
||||
|
||||
describe('LexicalNodeHelpers tests', () => {
|
||||
let container = null;
|
||||
let reactRoot;
|
||||
let container: HTMLDivElement | null = null;
|
||||
let reactRoot: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
@ -23,7 +24,7 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(container!);
|
||||
container = null;
|
||||
|
||||
jest.restoreAllMocks();
|
||||
|
@ -22,8 +22,10 @@ import {
|
||||
$getRoot,
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
LexicalEditor,
|
||||
} 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 {LexicalComposer} from '../../LexicalComposer';
|
||||
@ -48,8 +50,8 @@ const RICH_TEXT_NODES = [
|
||||
];
|
||||
|
||||
describe('LexicalNodeHelpers tests', () => {
|
||||
let container = null;
|
||||
let reactRoot;
|
||||
let container: HTMLDivElement | null = null;
|
||||
let reactRoot: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
@ -58,7 +60,7 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(container!);
|
||||
container = null;
|
||||
|
||||
jest.restoreAllMocks();
|
||||
@ -113,7 +115,7 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
reactRoot.render(<App />);
|
||||
});
|
||||
|
||||
const text = editor.getEditorState().read($rootTextContent);
|
||||
const text = editor!.getEditorState().read($rootTextContent);
|
||||
expect(text).toBe('foo');
|
||||
});
|
||||
}
|
||||
@ -164,9 +166,9 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
reactRoot.render(<App />);
|
||||
});
|
||||
|
||||
await editor.focus();
|
||||
await editor!.focus();
|
||||
|
||||
await editor.getEditorState().read(() => {
|
||||
await editor!.getEditorState().read(() => {
|
||||
expect($rootTextContent()).toBe('foo');
|
||||
|
||||
const selection = $getSelection();
|
||||
@ -183,7 +185,7 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
|
||||
for (const plugin of ['PlainTextPlugin', 'RichTextPlugin']) {
|
||||
it(`${plugin} can hide placeholder when non-editable`, async () => {
|
||||
let editor;
|
||||
let editor: LexicalEditor;
|
||||
|
||||
function GrabEditor() {
|
||||
[editor] = useLexicalComposerContext();
|
||||
@ -232,7 +234,7 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
});
|
||||
|
||||
function placeholderText() {
|
||||
const placeholderContainer = container.querySelector('.placeholder');
|
||||
const placeholderContainer = container!.querySelector('.placeholder');
|
||||
return placeholderContainer && placeholderContainer.textContent;
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
paragraph.append(overflowRight);
|
||||
});
|
||||
|
||||
return [overflowLeftKey, overflowRightKey];
|
||||
return [overflowLeftKey!, overflowRightKey!];
|
||||
}
|
||||
|
||||
it('merges an empty overflow node (left overflow selected)', async () => {
|
||||
@ -63,8 +63,9 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
await initializeEditorWithLeftRightOverflowNodes();
|
||||
|
||||
await editor.update(() => {
|
||||
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey);
|
||||
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey);
|
||||
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey)!;
|
||||
const overflowRight =
|
||||
$getNodeByKey<OverflowNode>(overflowRightKey)!;
|
||||
|
||||
const text1 = $createTextNode('1');
|
||||
const text2 = $createTextNode('2');
|
||||
@ -77,8 +78,9 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
});
|
||||
|
||||
await editor.update(() => {
|
||||
const paragraph = $getRoot().getFirstChild<ParagraphNode>();
|
||||
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey);
|
||||
const paragraph = $getRoot().getFirstChild<ParagraphNode>()!;
|
||||
const overflowRight =
|
||||
$getNodeByKey<OverflowNode>(overflowRightKey)!;
|
||||
|
||||
mergePrevious(overflowRight);
|
||||
|
||||
@ -110,8 +112,9 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
let text1Key: NodeKey;
|
||||
|
||||
await editor.update(() => {
|
||||
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey);
|
||||
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey);
|
||||
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey)!;
|
||||
const overflowRight =
|
||||
$getNodeByKey<OverflowNode>(overflowRightKey)!;
|
||||
|
||||
const text1 = $createTextNode('1');
|
||||
const text2 = $createTextNode('2');
|
||||
@ -133,9 +136,10 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@ -168,8 +172,9 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
let text3Key: NodeKey;
|
||||
|
||||
await editor.update(() => {
|
||||
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey);
|
||||
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey);
|
||||
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey)!;
|
||||
const overflowRight =
|
||||
$getNodeByKey<OverflowNode>(overflowRightKey)!;
|
||||
|
||||
const text1 = $createTextNode('1');
|
||||
const text2 = $createTextNode('2');
|
||||
@ -202,8 +207,9 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
});
|
||||
|
||||
await editor.update(() => {
|
||||
const paragraph = $getRoot().getFirstChild<ParagraphNode>();
|
||||
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey);
|
||||
const paragraph = $getRoot().getFirstChild<ParagraphNode>()!;
|
||||
const overflowRight =
|
||||
$getNodeByKey<OverflowNode>(overflowRightKey)!;
|
||||
|
||||
mergePrevious(overflowRight);
|
||||
|
||||
|
@ -11,17 +11,19 @@ import {
|
||||
$createTextNode,
|
||||
$getRoot,
|
||||
createEditor,
|
||||
LexicalEditor,
|
||||
ParagraphNode,
|
||||
} from 'lexical';
|
||||
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 {useLexicalIsTextContentEmpty} from '../../useLexicalIsTextContentEmpty';
|
||||
|
||||
describe('useLexicalIsTextContentEmpty', () => {
|
||||
let container = null;
|
||||
let reactRoot;
|
||||
let container: HTMLDivElement | null = null;
|
||||
let reactRoot: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
@ -30,13 +32,13 @@ describe('useLexicalIsTextContentEmpty', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(container!);
|
||||
container = null;
|
||||
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function useLexicalEditor(rootElementRef) {
|
||||
function useLexicalEditor(rootElementRef: React.RefObject<HTMLDivElement>) {
|
||||
const editor = React.useMemo(
|
||||
() =>
|
||||
createEditor({
|
||||
@ -58,8 +60,8 @@ describe('useLexicalIsTextContentEmpty', () => {
|
||||
}
|
||||
|
||||
test('hook works', async () => {
|
||||
const ref = React.createRef<HTMLDivElement>();
|
||||
let editor;
|
||||
const ref = createRef<HTMLDivElement>();
|
||||
let editor: LexicalEditor;
|
||||
let hasText = false;
|
||||
|
||||
function TestBase() {
|
||||
|
@ -6,9 +6,10 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import {UserState} from '@lexical/yjs';
|
||||
import {Provider, UserState} from '@lexical/yjs';
|
||||
import {LexicalEditor} from 'lexical';
|
||||
import * as React from 'react';
|
||||
import {Container} from 'react-dom';
|
||||
import {createRoot, Root} from 'react-dom/client';
|
||||
import * as ReactTestUtils from 'react-dom/test-utils';
|
||||
import * as Y from 'yjs';
|
||||
@ -21,7 +22,17 @@ import {ContentEditable} from '../../LexicalContentEditable';
|
||||
import LexicalErrorBoundary from '../../LexicalErrorBoundary';
|
||||
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 [editor] = useLexicalComposerContext();
|
||||
@ -49,20 +60,20 @@ function Editor({doc, provider, setEditor, awarenessData}) {
|
||||
);
|
||||
}
|
||||
|
||||
class Client {
|
||||
export class Client implements Provider {
|
||||
_id: string;
|
||||
_reactRoot: Root;
|
||||
_container: HTMLDivElement;
|
||||
_editor: LexicalEditor;
|
||||
_reactRoot: Root | null = null;
|
||||
_container: HTMLDivElement | null = null;
|
||||
_editor: LexicalEditor | null = null;
|
||||
_connection: {
|
||||
_clients: Client[];
|
||||
_clients: Map<string, Client>;
|
||||
};
|
||||
_connected: boolean;
|
||||
_doc: Y.Doc;
|
||||
_awarenessState: unknown;
|
||||
_connected: boolean = false;
|
||||
_doc: Y.Doc = new Y.Doc();
|
||||
|
||||
_listeners: Map<string, Set<(data: unknown) => void>>;
|
||||
_updates: Uint8Array[];
|
||||
_listeners = new Map<string, Set<(data: unknown) => void>>();
|
||||
_updates: Uint8Array[] = [];
|
||||
_awarenessState: UserState | null = null;
|
||||
awareness: {
|
||||
getLocalState: () => UserState | null;
|
||||
getStates: () => Map<number, UserState>;
|
||||
@ -71,54 +82,37 @@ class Client {
|
||||
setLocalState: (state: UserState) => void;
|
||||
};
|
||||
|
||||
constructor(id, connection) {
|
||||
constructor(id: Client['_id'], connection: Client['_connection']) {
|
||||
this._id = id;
|
||||
this._reactRoot = null;
|
||||
this._container = null;
|
||||
this._connection = connection;
|
||||
this._connected = false;
|
||||
this._doc = new Y.Doc();
|
||||
this._awarenessState = {};
|
||||
this._onUpdate = this._onUpdate.bind(this);
|
||||
|
||||
this._doc.on('update', this._onUpdate);
|
||||
|
||||
this._listeners = new Map();
|
||||
this._updates = [];
|
||||
this._editor = null;
|
||||
|
||||
this.awareness = {
|
||||
getLocalState() {
|
||||
return this._awarenessState;
|
||||
},
|
||||
|
||||
getStates() {
|
||||
const states: Map<number, UserState> = new Map();
|
||||
states[0] = this._awarenessState as UserState;
|
||||
return states;
|
||||
},
|
||||
|
||||
off() {
|
||||
getLocalState: () => this._awarenessState,
|
||||
getStates: () => new Map([[0, this._awarenessState!]]),
|
||||
off: () => {
|
||||
// TODO
|
||||
},
|
||||
|
||||
on() {
|
||||
on: () => {
|
||||
// TODO
|
||||
},
|
||||
|
||||
setLocalState(state) {
|
||||
setLocalState: (state) => {
|
||||
this._awarenessState = state;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_onUpdate(update, origin, transaction) {
|
||||
_onUpdate(update: Uint8Array, origin: unknown, transaction: unknown) {
|
||||
if (origin !== this._connection && this._connected) {
|
||||
this._broadcastUpdate(update);
|
||||
}
|
||||
}
|
||||
|
||||
_broadcastUpdate(update) {
|
||||
_broadcastUpdate(update: Uint8Array) {
|
||||
this._connection._clients.forEach((client) => {
|
||||
if (client !== this) {
|
||||
if (client._connected) {
|
||||
@ -154,7 +148,7 @@ class Client {
|
||||
this._connected = false;
|
||||
}
|
||||
|
||||
start(rootContainer, awarenessData?) {
|
||||
start(rootContainer: Container, awarenessData?: object) {
|
||||
const container = document.createElement('div');
|
||||
const reactRoot = createRoot(container);
|
||||
this._container = container;
|
||||
@ -185,15 +179,16 @@ class Client {
|
||||
|
||||
stop() {
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (listenerSet === undefined) {
|
||||
@ -205,7 +200,8 @@ class Client {
|
||||
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);
|
||||
|
||||
if (listenerSet !== undefined) {
|
||||
@ -213,7 +209,7 @@ class Client {
|
||||
}
|
||||
}
|
||||
|
||||
_dispatch(type, data) {
|
||||
_dispatch(type: string, data: unknown) {
|
||||
const listenerSet = this._listeners.get(type);
|
||||
|
||||
if (listenerSet !== undefined) {
|
||||
@ -222,7 +218,7 @@ class Client {
|
||||
}
|
||||
|
||||
getHTML() {
|
||||
return (this._container.firstChild as HTMLElement).innerHTML;
|
||||
return (this.getContainer().firstChild as HTMLElement).innerHTML;
|
||||
}
|
||||
|
||||
getDocJSON() {
|
||||
@ -230,36 +226,32 @@ class Client {
|
||||
}
|
||||
|
||||
getEditorState() {
|
||||
return this._editor.getEditorState();
|
||||
return this.getEditor().getEditorState();
|
||||
}
|
||||
|
||||
getEditor() {
|
||||
return this._editor;
|
||||
return this._editor!;
|
||||
}
|
||||
|
||||
getContainer() {
|
||||
return this._container;
|
||||
return this._container!;
|
||||
}
|
||||
|
||||
async focus() {
|
||||
this._container.focus();
|
||||
this.getContainer().focus();
|
||||
|
||||
await Promise.resolve().then();
|
||||
}
|
||||
|
||||
update(cb) {
|
||||
this._editor.update(cb);
|
||||
update(cb: () => void) {
|
||||
this.getEditor().update(cb);
|
||||
}
|
||||
}
|
||||
|
||||
class TestConnection {
|
||||
_clients: Map<string, Client>;
|
||||
_clients = new Map<string, Client>();
|
||||
|
||||
constructor() {
|
||||
this._clients = new Map();
|
||||
}
|
||||
|
||||
createClient(id) {
|
||||
createClient(id: string) {
|
||||
const client = new Client(id, this);
|
||||
|
||||
this._clients.set(id, client);
|
||||
@ -272,7 +264,7 @@ export function createTestConnection() {
|
||||
return new TestConnection();
|
||||
}
|
||||
|
||||
export async function waitForReact(cb) {
|
||||
export async function waitForReact(cb: () => void) {
|
||||
await ReactTestUtils.act(async () => {
|
||||
cb();
|
||||
await Promise.resolve().then();
|
||||
|
@ -11,7 +11,13 @@ import {
|
||||
$isHeadingNode,
|
||||
HeadingNode,
|
||||
} 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';
|
||||
|
||||
const editorConfig = Object.freeze({
|
||||
@ -81,7 +87,7 @@ describe('LexicalHeadingNode tests', () => {
|
||||
|
||||
test('HeadingNode.insertNewAfter()', async () => {
|
||||
const {editor} = testEnv;
|
||||
let headingNode;
|
||||
let headingNode: HeadingNode;
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
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>',
|
||||
);
|
||||
await editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
const selection = $getSelection() as RangeSelection;
|
||||
const result = headingNode.insertNewAfter(selection);
|
||||
expect(result).toBeInstanceOf(ParagraphNode);
|
||||
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 () => {
|
||||
const {editor} = testEnv;
|
||||
let headingNode;
|
||||
let headingNode: HeadingNode;
|
||||
const text = 'hello world';
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
|
@ -6,8 +6,8 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import {$createQuoteNode} from '@lexical/rich-text';
|
||||
import {$getRoot, ParagraphNode} from 'lexical';
|
||||
import {$createQuoteNode, QuoteNode} from '@lexical/rich-text';
|
||||
import {$createRangeSelection, $getRoot, ParagraphNode} from 'lexical';
|
||||
import {initializeUnitTest} from 'lexical/src/__tests__/utils';
|
||||
|
||||
const editorConfig = Object.freeze({
|
||||
@ -64,7 +64,7 @@ describe('LexicalQuoteNode tests', () => {
|
||||
|
||||
test('QuoteNode.insertNewAfter()', async () => {
|
||||
const {editor} = testEnv;
|
||||
let quoteNode;
|
||||
let quoteNode: QuoteNode;
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
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>',
|
||||
);
|
||||
await editor.update(() => {
|
||||
const result = quoteNode.insertNewAfter();
|
||||
const result = quoteNode.insertNewAfter($createRangeSelection());
|
||||
expect(result).toBeInstanceOf(ParagraphNode);
|
||||
expect(result.getDirection()).toEqual(quoteNode.getDirection());
|
||||
});
|
||||
|
@ -30,7 +30,13 @@ import {
|
||||
$isElementNode,
|
||||
$isRangeSelection,
|
||||
$setSelection,
|
||||
DecoratorNode,
|
||||
ElementNode,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
ParagraphNode,
|
||||
PointType,
|
||||
TextNode,
|
||||
} from 'lexical';
|
||||
import {
|
||||
$assertRangeSelection,
|
||||
@ -72,6 +78,13 @@ import {
|
||||
undo,
|
||||
} from '../utils';
|
||||
|
||||
interface ExpectedSelection {
|
||||
anchorPath: number[];
|
||||
anchorOffset: number;
|
||||
focusPath: number[];
|
||||
focusOffset: number;
|
||||
}
|
||||
|
||||
initializeClipboard();
|
||||
|
||||
jest.mock('shared/environment', () => {
|
||||
@ -116,7 +129,7 @@ describe('LexicalSelection tests', () => {
|
||||
container = null;
|
||||
});
|
||||
|
||||
let editor = null;
|
||||
let editor: LexicalEditor | null = null;
|
||||
|
||||
async function init() {
|
||||
function TestBase() {
|
||||
@ -162,8 +175,8 @@ describe('LexicalSelection tests', () => {
|
||||
}}>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
// eslint-disable-next-line jsx-a11y/aria-role
|
||||
<ContentEditable role={null} spellCheck={null} />
|
||||
// eslint-disable-next-line jsx-a11y/aria-role, @typescript-eslint/no-explicit-any
|
||||
<ContentEditable role={null as any} spellCheck={null as any} />
|
||||
}
|
||||
placeholder={null}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
@ -175,32 +188,41 @@ describe('LexicalSelection tests', () => {
|
||||
}
|
||||
|
||||
ReactTestUtils.act(() => {
|
||||
createRoot(container).render(<TestBase />);
|
||||
createRoot(container!).render(<TestBase />);
|
||||
});
|
||||
|
||||
editor.getRootElement().focus();
|
||||
editor!.getRootElement()!.focus();
|
||||
|
||||
await Promise.resolve().then();
|
||||
// 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 editor.update(fn);
|
||||
await editor!.update(fn);
|
||||
});
|
||||
|
||||
return Promise.resolve().then();
|
||||
}
|
||||
|
||||
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>',
|
||||
);
|
||||
});
|
||||
|
||||
function assertSelection(rootElement, expectedSelection) {
|
||||
const actualSelection = window.getSelection();
|
||||
function assertSelection(
|
||||
rootElement: HTMLElement,
|
||||
expectedSelection: ExpectedSelection,
|
||||
) {
|
||||
const actualSelection = window.getSelection()!;
|
||||
|
||||
expect(actualSelection.anchorNode).toBe(
|
||||
getNodeFromPath(expectedSelection.anchorPath, rootElement),
|
||||
@ -1107,13 +1129,13 @@ describe('LexicalSelection tests', () => {
|
||||
const name = testUnit.name || 'Test case';
|
||||
|
||||
test(name + ` (#${i + 1})`, async () => {
|
||||
await applySelectionInputs(testUnit.inputs, update, editor);
|
||||
await applySelectionInputs(testUnit.inputs, update, editor!);
|
||||
|
||||
// Validate HTML matches
|
||||
expect(container.innerHTML).toBe(testUnit.expectedHTML);
|
||||
expect(container!.innerHTML).toBe(testUnit.expectedHTML);
|
||||
|
||||
// Validate selection matches
|
||||
const rootElement = editor.getRootElement();
|
||||
const rootElement = editor!.getRootElement()!;
|
||||
const expectedSelection = testUnit.expectedSelection;
|
||||
|
||||
assertSelection(rootElement, expectedSelection);
|
||||
@ -1122,10 +1144,10 @@ describe('LexicalSelection tests', () => {
|
||||
|
||||
test('insert text one selected node element selection', async () => {
|
||||
await ReactTestUtils.act(async () => {
|
||||
await editor.update(() => {
|
||||
await editor!.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = root.getFirstChild<ParagraphNode>();
|
||||
const paragraph = root.getFirstChild<ParagraphNode>()!;
|
||||
|
||||
const elementNode = $createTestElementNode();
|
||||
const text = $createTextNode('foo');
|
||||
@ -1146,10 +1168,10 @@ describe('LexicalSelection tests', () => {
|
||||
|
||||
test('getNodes resolves nested block nodes', async () => {
|
||||
await ReactTestUtils.act(async () => {
|
||||
await editor.update(() => {
|
||||
await editor!.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = root.getFirstChild<ParagraphNode>();
|
||||
const paragraph = root.getFirstChild<ParagraphNode>()!;
|
||||
|
||||
const elementNode = $createTestElementNode();
|
||||
const text = $createTextNode();
|
||||
@ -1157,7 +1179,7 @@ describe('LexicalSelection tests', () => {
|
||||
paragraph.append(elementNode);
|
||||
elementNode.append(text);
|
||||
|
||||
const selectedNodes = $getSelection().getNodes();
|
||||
const selectedNodes = $getSelection()!.getNodes();
|
||||
|
||||
expect(selectedNodes.length).toBe(1);
|
||||
expect(selectedNodes[0].getKey()).toBe(text.getKey());
|
||||
@ -1166,7 +1188,23 @@ describe('LexicalSelection tests', () => {
|
||||
});
|
||||
|
||||
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
|
||||
{
|
||||
anchorOffset: 2,
|
||||
@ -1313,8 +1351,8 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, originalText1) => {
|
||||
const originalText2 = originalText1.getPreviousSibling();
|
||||
const lastChild = paragraph.getLastChild();
|
||||
const originalText2 = originalText1.getPreviousSibling()!;
|
||||
const lastChild = paragraph.getLastChild()!;
|
||||
const newText = $createTextNode('2');
|
||||
lastChild.insertBefore(newText);
|
||||
|
||||
@ -1335,7 +1373,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, text) => {
|
||||
const lastChild = paragraph.getLastChild();
|
||||
const lastChild = paragraph.getLastChild()!;
|
||||
const newText = $createTextNode('2');
|
||||
lastChild.insertAfter(newText);
|
||||
|
||||
@ -1352,7 +1390,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, originalText1) => {
|
||||
const originalText2 = originalText1.getPreviousSibling();
|
||||
const originalText2 = originalText1.getPreviousSibling()!;
|
||||
const [, text] = originalText1.splitText(1);
|
||||
|
||||
return {
|
||||
@ -1372,7 +1410,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, text) => {
|
||||
const lastChild = paragraph.getLastChild();
|
||||
const lastChild = paragraph.getLastChild()!;
|
||||
lastChild.remove();
|
||||
|
||||
return {
|
||||
@ -1389,7 +1427,7 @@ describe('LexicalSelection tests', () => {
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, text) => {
|
||||
const newText = $createTextNode('replacement');
|
||||
const lastChild = paragraph.getLastChild();
|
||||
const lastChild = paragraph.getLastChild()!;
|
||||
lastChild.replace(newText);
|
||||
|
||||
return {
|
||||
@ -1406,7 +1444,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, text) => {
|
||||
const lastChild = paragraph.getLastChild();
|
||||
const lastChild = paragraph.getLastChild()!;
|
||||
const newText = $createTextNode('2');
|
||||
lastChild.insertBefore(newText);
|
||||
|
||||
@ -1439,7 +1477,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, originalText1) => {
|
||||
const originalText2 = originalText1.getPreviousSibling();
|
||||
const originalText2 = originalText1.getPreviousSibling()!;
|
||||
const [, text] = originalText1.splitText(1);
|
||||
|
||||
return {
|
||||
@ -1459,7 +1497,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 1,
|
||||
fn: (paragraph, originalText1) => {
|
||||
const lastChild = paragraph.getLastChild();
|
||||
const lastChild = paragraph.getLastChild()!;
|
||||
lastChild.remove();
|
||||
|
||||
return {
|
||||
@ -1480,7 +1518,7 @@ describe('LexicalSelection tests', () => {
|
||||
anchorOffset: 1,
|
||||
fn: (paragraph, originalText1) => {
|
||||
const newText = $createTextNode('replacement');
|
||||
const lastChild = paragraph.getLastChild();
|
||||
const lastChild = paragraph.getLastChild()!;
|
||||
lastChild.replace(newText);
|
||||
|
||||
return {
|
||||
@ -1501,8 +1539,8 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, originalText1) => {
|
||||
const originalText2 = originalText1.getPreviousSibling();
|
||||
const lastChild = paragraph.getLastChild();
|
||||
const originalText2 = originalText1.getPreviousSibling()!;
|
||||
const lastChild = paragraph.getLastChild()!;
|
||||
const newText = $createTextNode('2');
|
||||
lastChild.insertBefore(newText);
|
||||
|
||||
@ -1523,7 +1561,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, originalText1) => {
|
||||
const originalText2 = originalText1.getPreviousSibling();
|
||||
const originalText2 = originalText1.getPreviousSibling()!;
|
||||
const newText = $createTextNode('2');
|
||||
originalText1.insertAfter(newText);
|
||||
|
||||
@ -1544,7 +1582,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, originalText1) => {
|
||||
const originalText2 = originalText1.getPreviousSibling();
|
||||
const originalText2 = originalText1.getPreviousSibling()!;
|
||||
originalText1.splitText(1);
|
||||
|
||||
return {
|
||||
@ -1564,7 +1602,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, originalText1) => {
|
||||
const originalText2 = originalText1.getPreviousSibling();
|
||||
const originalText2 = originalText1.getPreviousSibling()!;
|
||||
originalText1.remove();
|
||||
|
||||
return {
|
||||
@ -1605,7 +1643,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 3,
|
||||
fn: (paragraph, originalText1) => {
|
||||
const originalText2 = paragraph.getLastChild();
|
||||
const originalText2 = paragraph.getLastChild()!;
|
||||
const newText = $createTextNode('new');
|
||||
originalText1.insertBefore(newText);
|
||||
|
||||
@ -1626,7 +1664,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, originalText1) => {
|
||||
const originalText2 = paragraph.getLastChild();
|
||||
const originalText2 = paragraph.getLastChild()!;
|
||||
const newText = $createTextNode('new');
|
||||
originalText1.insertBefore(newText);
|
||||
|
||||
@ -1647,7 +1685,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 1,
|
||||
fn: (paragraph, originalText1) => {
|
||||
originalText1.getNextSibling().remove();
|
||||
originalText1.getNextSibling()!.remove();
|
||||
|
||||
return {
|
||||
expectedAnchor: originalText1,
|
||||
@ -1662,7 +1700,7 @@ describe('LexicalSelection tests', () => {
|
||||
{
|
||||
anchorOffset: 0,
|
||||
fn: (paragraph, originalText1) => {
|
||||
originalText1.getNextSibling().remove();
|
||||
originalText1.getNextSibling()!.remove();
|
||||
|
||||
return {
|
||||
expectedAnchor: originalText1,
|
||||
@ -1674,8 +1712,9 @@ describe('LexicalSelection tests', () => {
|
||||
focusOffset: 1,
|
||||
name: 'remove - Remove with non-collapsed selection at offset',
|
||||
},
|
||||
]
|
||||
.reduce((testSuite, testCase) => {
|
||||
];
|
||||
baseCases
|
||||
.flatMap((testCase) => {
|
||||
// Test inverse selection
|
||||
const inverse = {
|
||||
...testCase,
|
||||
@ -1684,9 +1723,8 @@ describe('LexicalSelection tests', () => {
|
||||
invertSelection: true,
|
||||
name: testCase.name + ' (inverse selection)',
|
||||
};
|
||||
|
||||
return testSuite.concat(testCase, inverse);
|
||||
}, [])
|
||||
return [testCase, inverse];
|
||||
})
|
||||
.forEach(
|
||||
({
|
||||
name,
|
||||
@ -1703,10 +1741,10 @@ describe('LexicalSelection tests', () => {
|
||||
const test_ = only === true ? test.only : test;
|
||||
test_(name, async () => {
|
||||
await ReactTestUtils.act(async () => {
|
||||
await editor.update(() => {
|
||||
await editor!.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = root.getFirstChild<ParagraphNode>();
|
||||
const paragraph = root.getFirstChild<ParagraphNode>()!;
|
||||
const textNode = $createTextNode('foo');
|
||||
// Note: line break can't be selected by the DOM
|
||||
const linebreak = $createLineBreakNode();
|
||||
@ -1755,7 +1793,7 @@ describe('LexicalSelection tests', () => {
|
||||
describe('Selection correctly resolves to a sibling ElementNode when a node is removed', () => {
|
||||
test('', async () => {
|
||||
await ReactTestUtils.act(async () => {
|
||||
await editor.update(() => {
|
||||
await editor!.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
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', () => {
|
||||
test('', async () => {
|
||||
await ReactTestUtils.act(async () => {
|
||||
let paragraphNodeKey;
|
||||
await editor.update(() => {
|
||||
let paragraphNodeKey: string;
|
||||
await editor!.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraphNode = $createParagraphNode();
|
||||
@ -1808,7 +1846,7 @@ describe('LexicalSelection tests', () => {
|
||||
|
||||
listNode.remove();
|
||||
});
|
||||
await editor.getEditorState().read(() => {
|
||||
await editor!.getEditorState().read(() => {
|
||||
const selection = $assertRangeSelection($getSelection());
|
||||
expect(selection.anchor.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', () => {
|
||||
test('', async () => {
|
||||
await ReactTestUtils.act(async () => {
|
||||
await editor.update(() => {
|
||||
await editor!.update(() => {
|
||||
// Arrange
|
||||
// Root
|
||||
// |- Paragraph
|
||||
@ -1871,10 +1909,10 @@ describe('LexicalSelection tests', () => {
|
||||
|
||||
test('isBackward', async () => {
|
||||
await ReactTestUtils.act(async () => {
|
||||
await editor.update(() => {
|
||||
await editor!.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = root.getFirstChild<ParagraphNode>();
|
||||
const paragraph = root.getFirstChild<ParagraphNode>()!;
|
||||
const paragraphKey = paragraph.getKey();
|
||||
const textNode = $createTextNode('foo');
|
||||
const textNodeKey = textNode.getKey();
|
||||
@ -1914,7 +1952,18 @@ describe('LexicalSelection tests', () => {
|
||||
});
|
||||
|
||||
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}) => {
|
||||
anchor.set(textNode1.getKey(), 1, 'text');
|
||||
@ -1971,23 +2020,24 @@ describe('LexicalSelection tests', () => {
|
||||
},
|
||||
name: 'Included if decorator is selected as the only node',
|
||||
},
|
||||
]
|
||||
.reduce((testSuite, testCase) => {
|
||||
];
|
||||
baseCases
|
||||
.flatMap((testCase) => {
|
||||
const inverse = {
|
||||
...testCase,
|
||||
invertSelection: true,
|
||||
name: testCase.name + ' (inverse selection)',
|
||||
};
|
||||
|
||||
return testSuite.concat(testCase, inverse);
|
||||
}, [])
|
||||
return [testCase, inverse];
|
||||
})
|
||||
.forEach(({name, fn, invertSelection}) => {
|
||||
it(name, async () => {
|
||||
await ReactTestUtils.act(async () => {
|
||||
await editor.update(() => {
|
||||
await editor!.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = root.getFirstChild<ParagraphNode>();
|
||||
const paragraph = root.getFirstChild<ParagraphNode>()!;
|
||||
const textNode1 = $createTextNode('1');
|
||||
const textNode2 = $createTextNode('2');
|
||||
const decorator = $createTestDecoratorNode();
|
||||
@ -2113,7 +2163,7 @@ describe('LexicalSelection tests', () => {
|
||||
it('adjust offset for inline elements text formatting', async () => {
|
||||
init();
|
||||
|
||||
await editor.update(() => {
|
||||
await editor!.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const text1 = $createTextNode('--');
|
||||
@ -2154,7 +2204,11 @@ describe('LexicalSelection tests', () => {
|
||||
});
|
||||
|
||||
describe('Node.replace', () => {
|
||||
let text1, text2, text3, paragraph, testEditor;
|
||||
let text1: TextNode,
|
||||
text2: TextNode,
|
||||
text3: TextNode,
|
||||
paragraph: ParagraphNode,
|
||||
testEditor: LexicalEditor;
|
||||
|
||||
beforeEach(async () => {
|
||||
testEditor = createTestEditor();
|
||||
@ -2481,7 +2535,7 @@ describe('LexicalSelection tests', () => {
|
||||
expect(rootChildren[0].__type).toBe('heading');
|
||||
expect(rootChildren[1].__type).toBe('heading');
|
||||
expect(rootChildren.length).toBe(2);
|
||||
const sel = $getSelection();
|
||||
const sel = $getSelection()!;
|
||||
expect(sel.getNodes().length).toBe(2);
|
||||
});
|
||||
});
|
||||
@ -2540,7 +2594,7 @@ describe('LexicalSelection tests', () => {
|
||||
const paragraph = column.getFirstChild();
|
||||
invariant($isElementNode(paragraph));
|
||||
if (paragraph.getFirstChild()) {
|
||||
paragraph.getFirstChild().remove();
|
||||
paragraph.getFirstChild()!.remove();
|
||||
}
|
||||
root.append(table);
|
||||
|
||||
|
@ -25,6 +25,9 @@ import {
|
||||
$isParagraphNode,
|
||||
$isRangeSelection,
|
||||
$setSelection,
|
||||
ElementNode,
|
||||
LexicalEditor,
|
||||
ParagraphNode,
|
||||
RangeSelection,
|
||||
TextNode,
|
||||
} 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 nodeMap = editor._pendingEditorState._nodeMap;
|
||||
const nodeMap = editor._pendingEditorState!._nodeMap;
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const {text, key, mergeable} = nodes[i];
|
||||
@ -81,7 +87,9 @@ function createParagraphWithNodes(editor, nodes) {
|
||||
describe('LexicalSelectionHelpers tests', () => {
|
||||
describe('Collapsed', () => {
|
||||
test('Can handle a text point', () => {
|
||||
const setupTestCase = (cb) => {
|
||||
const setupTestCase = (
|
||||
cb: (selection: RangeSelection, node: ElementNode) => void,
|
||||
) => {
|
||||
const editor = createTestEditor();
|
||||
|
||||
editor.update(() => {
|
||||
@ -119,7 +127,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
type: 'text',
|
||||
});
|
||||
const selection = $getSelection();
|
||||
cb(selection, element);
|
||||
cb(selection as RangeSelection, element);
|
||||
});
|
||||
};
|
||||
|
||||
@ -137,7 +145,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
setupTestCase((selection, state) => {
|
||||
selection.insertText('Test');
|
||||
|
||||
expect($getNodeByKey('a').getTextContent()).toBe('Testa');
|
||||
expect($getNodeByKey('a')!.getTextContent()).toBe('Testa');
|
||||
|
||||
expect(selection.anchor).toEqual(
|
||||
expect.objectContaining({
|
||||
@ -162,7 +170,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
|
||||
expect(selection.anchor).toEqual(
|
||||
expect.objectContaining({
|
||||
key: element.getFirstChild().getKey(),
|
||||
key: element.getFirstChild()!.getKey(),
|
||||
offset: 3,
|
||||
type: 'text',
|
||||
}),
|
||||
@ -170,7 +178,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
|
||||
expect(selection.focus).toEqual(
|
||||
expect.objectContaining({
|
||||
key: element.getFirstChild().getKey(),
|
||||
key: element.getFirstChild()!.getKey(),
|
||||
offset: 3,
|
||||
type: 'text',
|
||||
}),
|
||||
@ -224,11 +232,11 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
selection.formatText('bold');
|
||||
selection.insertText('Test');
|
||||
|
||||
expect(element.getFirstChild().getTextContent()).toBe('Test');
|
||||
expect(element.getFirstChild()!.getTextContent()).toBe('Test');
|
||||
|
||||
expect(selection.anchor).toEqual(
|
||||
expect.objectContaining({
|
||||
key: element.getFirstChild().getKey(),
|
||||
key: element.getFirstChild()!.getKey(),
|
||||
offset: 4,
|
||||
type: 'text',
|
||||
}),
|
||||
@ -236,15 +244,15 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
|
||||
expect(selection.focus).toEqual(
|
||||
expect.objectContaining({
|
||||
key: element.getFirstChild().getKey(),
|
||||
key: element.getFirstChild()!.getKey(),
|
||||
offset: 4,
|
||||
type: 'text',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(element.getFirstChild().getNextSibling().getTextContent()).toBe(
|
||||
'a',
|
||||
);
|
||||
expect(
|
||||
element.getFirstChild()!.getNextSibling()!.getTextContent(),
|
||||
).toBe('a');
|
||||
});
|
||||
|
||||
// Extract selection
|
||||
@ -414,7 +422,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
const editor = createTestEditor();
|
||||
|
||||
const domElement = document.createElement('div');
|
||||
let element;
|
||||
let element: ParagraphNode;
|
||||
|
||||
editor.setRootElement(domElement);
|
||||
|
||||
@ -690,7 +698,9 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
});
|
||||
|
||||
test('Can handle an element point on empty element', () => {
|
||||
const setupTestCase = (cb) => {
|
||||
const setupTestCase = (
|
||||
cb: (selection: RangeSelection, el: ElementNode) => void,
|
||||
) => {
|
||||
const editor = createTestEditor();
|
||||
|
||||
editor.update(() => {
|
||||
@ -712,7 +722,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
type: 'element',
|
||||
});
|
||||
const selection = $getSelection();
|
||||
cb(selection, element);
|
||||
cb(selection as RangeSelection, element);
|
||||
});
|
||||
};
|
||||
|
||||
@ -729,7 +739,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
// insertText
|
||||
setupTestCase((selection, element) => {
|
||||
selection.insertText('Test');
|
||||
const firstChild = element.getFirstChild();
|
||||
const firstChild = element.getFirstChild()!;
|
||||
|
||||
expect(firstChild.getTextContent()).toBe('Test');
|
||||
|
||||
@ -753,7 +763,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
// insertParagraph
|
||||
setupTestCase((selection, element) => {
|
||||
selection.insertParagraph();
|
||||
const nextElement = element.getNextSibling();
|
||||
const nextElement = element.getNextSibling()!;
|
||||
|
||||
expect(selection.anchor).toEqual(
|
||||
expect.objectContaining({
|
||||
@ -797,7 +807,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
setupTestCase((selection, element) => {
|
||||
selection.formatText('bold');
|
||||
selection.insertText('Test');
|
||||
const firstChild = element.getFirstChild();
|
||||
const firstChild = element.getFirstChild()!;
|
||||
|
||||
expect(firstChild.getTextContent()).toBe('Test');
|
||||
|
||||
@ -825,7 +835,9 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
});
|
||||
|
||||
test('Can handle a start element point', () => {
|
||||
const setupTestCase = (cb) => {
|
||||
const setupTestCase = (
|
||||
cb: (selection: RangeSelection, el: ElementNode) => void,
|
||||
) => {
|
||||
const editor = createTestEditor();
|
||||
|
||||
editor.update(() => {
|
||||
@ -863,7 +875,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
type: 'element',
|
||||
});
|
||||
const selection = $getSelection();
|
||||
cb(selection, element);
|
||||
cb(selection as RangeSelection, element);
|
||||
});
|
||||
};
|
||||
|
||||
@ -880,7 +892,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
// insertText
|
||||
setupTestCase((selection, element) => {
|
||||
selection.insertText('Test');
|
||||
const firstChild = element.getFirstChild();
|
||||
const firstChild = element.getFirstChild()!;
|
||||
|
||||
expect(firstChild.getTextContent()).toBe('Test');
|
||||
|
||||
@ -948,7 +960,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
selection.formatText('bold');
|
||||
selection.insertText('Test');
|
||||
|
||||
const firstChild = element.getFirstChild();
|
||||
const firstChild = element.getFirstChild()!;
|
||||
|
||||
expect(firstChild.getTextContent()).toBe('Test');
|
||||
|
||||
@ -976,7 +988,9 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
});
|
||||
|
||||
test('Can handle an end element point', () => {
|
||||
const setupTestCase = (cb) => {
|
||||
const setupTestCase = (
|
||||
cb: (selection: RangeSelection, el: ElementNode) => void,
|
||||
) => {
|
||||
const editor = createTestEditor();
|
||||
|
||||
editor.update(() => {
|
||||
@ -1014,7 +1028,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
type: 'element',
|
||||
});
|
||||
const selection = $getSelection();
|
||||
cb(selection, element);
|
||||
cb(selection as RangeSelection, element);
|
||||
});
|
||||
};
|
||||
|
||||
@ -1031,7 +1045,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
// insertText
|
||||
setupTestCase((selection, element) => {
|
||||
selection.insertText('Test');
|
||||
const lastChild = element.getLastChild();
|
||||
const lastChild = element.getLastChild()!;
|
||||
|
||||
expect(lastChild.getTextContent()).toBe('Test');
|
||||
|
||||
@ -1055,7 +1069,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
// insertParagraph
|
||||
setupTestCase((selection, element) => {
|
||||
selection.insertParagraph();
|
||||
const nextSibling = element.getNextSibling();
|
||||
const nextSibling = element.getNextSibling()!;
|
||||
|
||||
expect(selection.anchor).toEqual(
|
||||
expect.objectContaining({
|
||||
@ -1099,7 +1113,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
setupTestCase((selection, element) => {
|
||||
selection.formatText('bold');
|
||||
selection.insertText('Test');
|
||||
const lastChild = element.getLastChild();
|
||||
const lastChild = element.getLastChild()!;
|
||||
|
||||
expect(lastChild.getTextContent()).toBe('Test');
|
||||
|
||||
@ -1271,7 +1285,9 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
|
||||
describe('Simple range', () => {
|
||||
test('Can handle multiple text points', () => {
|
||||
const setupTestCase = (cb) => {
|
||||
const setupTestCase = (
|
||||
cb: (selection: RangeSelection, el: ElementNode) => void,
|
||||
) => {
|
||||
const editor = createTestEditor();
|
||||
|
||||
editor.update(() => {
|
||||
@ -1333,7 +1349,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
setupTestCase((selection, state) => {
|
||||
selection.insertText('Test');
|
||||
|
||||
expect($getNodeByKey('a').getTextContent()).toBe('Test');
|
||||
expect($getNodeByKey('a')!.getTextContent()).toBe('Test');
|
||||
|
||||
expect(selection.anchor).toEqual(
|
||||
expect.objectContaining({
|
||||
@ -1358,7 +1374,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
|
||||
expect(selection.anchor).toEqual(
|
||||
expect.objectContaining({
|
||||
key: element.getFirstChild().getKey(),
|
||||
key: element.getFirstChild()!.getKey(),
|
||||
offset: 3,
|
||||
type: 'text',
|
||||
}),
|
||||
@ -1366,7 +1382,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
|
||||
expect(selection.focus).toEqual(
|
||||
expect.objectContaining({
|
||||
key: element.getFirstChild().getKey(),
|
||||
key: element.getFirstChild()!.getKey(),
|
||||
offset: 3,
|
||||
type: 'text',
|
||||
}),
|
||||
@ -1420,11 +1436,11 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
selection.formatText('bold');
|
||||
selection.insertText('Test');
|
||||
|
||||
expect(element.getFirstChild().getTextContent()).toBe('Test');
|
||||
expect(element.getFirstChild()!.getTextContent()).toBe('Test');
|
||||
|
||||
expect(selection.anchor).toEqual(
|
||||
expect.objectContaining({
|
||||
key: element.getFirstChild().getKey(),
|
||||
key: element.getFirstChild()!.getKey(),
|
||||
offset: 4,
|
||||
type: 'text',
|
||||
}),
|
||||
@ -1432,7 +1448,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
|
||||
expect(selection.focus).toEqual(
|
||||
expect.objectContaining({
|
||||
key: element.getFirstChild().getKey(),
|
||||
key: element.getFirstChild()!.getKey(),
|
||||
offset: 4,
|
||||
type: 'text',
|
||||
}),
|
||||
@ -1446,7 +1462,9 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
});
|
||||
|
||||
test('Can handle multiple element points', () => {
|
||||
const setupTestCase = (cb) => {
|
||||
const setupTestCase = (
|
||||
cb: (selection: RangeSelection, el: ElementNode) => void,
|
||||
) => {
|
||||
const editor = createTestEditor();
|
||||
|
||||
editor.update(() => {
|
||||
@ -1504,7 +1522,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
// insertText
|
||||
setupTestCase((selection, element) => {
|
||||
selection.insertText('Test');
|
||||
const firstChild = element.getFirstChild();
|
||||
const firstChild = element.getFirstChild()!;
|
||||
|
||||
expect(firstChild.getTextContent()).toBe('Test');
|
||||
|
||||
@ -1571,7 +1589,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
setupTestCase((selection, element) => {
|
||||
selection.formatText('bold');
|
||||
selection.insertText('Test');
|
||||
const firstChild = element.getFirstChild();
|
||||
const firstChild = element.getFirstChild()!;
|
||||
|
||||
expect(firstChild.getTextContent()).toBe('Test');
|
||||
|
||||
@ -1601,7 +1619,9 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
});
|
||||
|
||||
test('Can handle a mix of text and element points', () => {
|
||||
const setupTestCase = (cb) => {
|
||||
const setupTestCase = (
|
||||
cb: (selection: RangeSelection, el: ElementNode) => void,
|
||||
) => {
|
||||
const editor = createTestEditor();
|
||||
|
||||
editor.update(() => {
|
||||
@ -1668,7 +1688,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
// insertText
|
||||
setupTestCase((selection, element) => {
|
||||
selection.insertText('Test');
|
||||
const firstChild = element.getFirstChild();
|
||||
const firstChild = element.getFirstChild()!;
|
||||
|
||||
expect(firstChild.getTextContent()).toBe('Test');
|
||||
|
||||
@ -1692,7 +1712,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
// insertParagraph
|
||||
setupTestCase((selection, element) => {
|
||||
selection.insertParagraph();
|
||||
const nextElement = element.getNextSibling();
|
||||
const nextElement = element.getNextSibling()!;
|
||||
|
||||
expect(selection.anchor).toEqual(
|
||||
expect.objectContaining({
|
||||
@ -1736,7 +1756,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
setupTestCase((selection, element) => {
|
||||
selection.formatText('bold');
|
||||
selection.insertText('Test');
|
||||
const firstChild = element.getFirstChild();
|
||||
const firstChild = element.getFirstChild()!;
|
||||
|
||||
expect(firstChild.getTextContent()).toBe('Test');
|
||||
|
||||
@ -2121,7 +2141,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
expect(selection.anchor).toEqual(
|
||||
expect.objectContaining({
|
||||
key: paragraph
|
||||
.getChildAtIndex(paragraph.getChildrenSize() - 2)
|
||||
.getChildAtIndex(paragraph.getChildrenSize() - 2)!
|
||||
.getKey(),
|
||||
offset: 1,
|
||||
type: 'text',
|
||||
@ -2131,7 +2151,7 @@ describe('LexicalSelectionHelpers tests', () => {
|
||||
expect(selection.focus).toEqual(
|
||||
expect.objectContaining({
|
||||
key: paragraph
|
||||
.getChildAtIndex(paragraph.getChildrenSize() - 2)
|
||||
.getChildAtIndex(paragraph.getChildrenSize() - 2)!
|
||||
.getKey(),
|
||||
offset: 1,
|
||||
type: 'text',
|
||||
@ -2547,7 +2567,7 @@ describe('extract', () => {
|
||||
const selection = $getSelection();
|
||||
expect($isRangeSelection(selection)).toBeTruthy();
|
||||
|
||||
expect(selection.extract()).toEqual([text]);
|
||||
expect(selection!.extract()).toEqual([text]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2605,7 +2625,7 @@ describe('insertNodes', () => {
|
||||
});
|
||||
await editor.update(() => {
|
||||
const selection = $createRangeSelection();
|
||||
const text = $getRoot().getLastDescendant();
|
||||
const text = $getRoot().getLastDescendant()!;
|
||||
selection.anchor.set(text.getKey(), 0, 'text');
|
||||
selection.focus.set(text.getKey(), 0, 'text');
|
||||
|
||||
@ -2629,7 +2649,7 @@ describe('insertNodes', () => {
|
||||
$createParagraphNode().append(emptyTextNode, $createTextNode('text')),
|
||||
);
|
||||
emptyTextNode.select(0, 0);
|
||||
const selection = $getSelection();
|
||||
const selection = $getSelection()!;
|
||||
expect($isRangeSelection(selection)).toBeTruthy();
|
||||
selection.insertNodes([$createTextNode('foo')]);
|
||||
|
||||
@ -2692,7 +2712,7 @@ describe('$patchStyleText', () => {
|
||||
const link = $createLinkNode('https://');
|
||||
link.append($createTextNode('link'));
|
||||
|
||||
const a = $getNodeByKey('a');
|
||||
const a = $getNodeByKey('a')!;
|
||||
a.insertAfter(link);
|
||||
|
||||
setAnchorPoint({
|
||||
@ -2793,7 +2813,7 @@ describe('$patchStyleText', () => {
|
||||
const link = $createLinkNode('https://');
|
||||
link.append($createTextNode('link'));
|
||||
|
||||
const a = $getNodeByKey('a');
|
||||
const a = $getNodeByKey('a')!;
|
||||
a.insertAfter(link);
|
||||
|
||||
setAnchorPoint({
|
||||
@ -2846,7 +2866,7 @@ describe('$patchStyleText', () => {
|
||||
const link = $createLinkNode('https://');
|
||||
link.append($createTextNode('link'));
|
||||
|
||||
const a = $getNodeByKey('a');
|
||||
const a = $getNodeByKey('a')!;
|
||||
a.insertAfter(link);
|
||||
|
||||
// Select from the end of the link _element_
|
||||
|
@ -12,6 +12,8 @@ import {
|
||||
$isNodeSelection,
|
||||
$isRangeSelection,
|
||||
$isTextNode,
|
||||
LexicalEditor,
|
||||
PointType,
|
||||
} from 'lexical';
|
||||
|
||||
Object.defineProperty(HTMLElement.prototype, 'contentEditable', {
|
||||
@ -49,7 +51,7 @@ if (!Selection.prototype.modify) {
|
||||
};
|
||||
|
||||
const getWordsFromString = function (string: string): Array<Segment> {
|
||||
const segments = [];
|
||||
const segments: Segment[] = [];
|
||||
let wordString = '';
|
||||
let nonWordString = '';
|
||||
let i;
|
||||
@ -90,7 +92,8 @@ if (!Selection.prototype.modify) {
|
||||
// This is not a thorough implementation, it was more to get tests working
|
||||
// given the refactor to use this selection method.
|
||||
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 anchor = impl._anchor;
|
||||
|
||||
@ -164,11 +167,11 @@ if (!Selection.prototype.modify) {
|
||||
}
|
||||
}
|
||||
} else if (granularity === 'word') {
|
||||
const anchorNode = this.anchorNode;
|
||||
const anchorNode = this.anchorNode!;
|
||||
const targetTextContent =
|
||||
direction === 'backward'
|
||||
? anchorNode.textContent.slice(0, this.anchorOffset)
|
||||
: anchorNode.textContent.slice(this.anchorOffset);
|
||||
? anchorNode.textContent!.slice(0, this.anchorOffset)
|
||||
: anchorNode.textContent!.slice(this.anchorOffset);
|
||||
const segments = getWordsFromString(targetTextContent);
|
||||
const segmentsLength = segments.length;
|
||||
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
|
||||
? ' '
|
||||
: whitespaceCharacter;
|
||||
}
|
||||
|
||||
export function insertText(text) {
|
||||
export function insertText(text: string) {
|
||||
return {
|
||||
text,
|
||||
type: 'insert_text',
|
||||
};
|
||||
}
|
||||
|
||||
export function insertTokenNode(text) {
|
||||
export function insertTokenNode(text: string) {
|
||||
return {
|
||||
text,
|
||||
type: 'insert_token_node',
|
||||
};
|
||||
}
|
||||
|
||||
export function insertSegmentedNode(text) {
|
||||
export function insertSegmentedNode(text: string) {
|
||||
return {
|
||||
text,
|
||||
type: 'insert_segmented_node',
|
||||
@ -385,10 +388,10 @@ export function pasteHTML(text: string) {
|
||||
}
|
||||
|
||||
export function moveNativeSelection(
|
||||
anchorPath,
|
||||
anchorOffset,
|
||||
focusPath,
|
||||
focusOffset,
|
||||
anchorPath: number[],
|
||||
anchorOffset: number,
|
||||
focusPath: number[],
|
||||
focusOffset: number,
|
||||
) {
|
||||
return {
|
||||
anchorOffset,
|
||||
@ -399,7 +402,7 @@ export function moveNativeSelection(
|
||||
};
|
||||
}
|
||||
|
||||
export function getNodeFromPath(path, rootElement) {
|
||||
export function getNodeFromPath(path: number[], rootElement: Node) {
|
||||
let node = rootElement;
|
||||
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
@ -410,12 +413,12 @@ export function getNodeFromPath(path, rootElement) {
|
||||
}
|
||||
|
||||
export function setNativeSelection(
|
||||
anchorNode,
|
||||
anchorOffset,
|
||||
focusNode,
|
||||
focusOffset,
|
||||
anchorNode: Node,
|
||||
anchorOffset: number,
|
||||
focusNode: Node,
|
||||
focusOffset: number,
|
||||
) {
|
||||
const domSelection = window.getSelection();
|
||||
const domSelection = window.getSelection()!;
|
||||
const range = document.createRange();
|
||||
range.setStart(anchorNode, anchorOffset);
|
||||
range.setEnd(focusNode, focusOffset);
|
||||
@ -427,18 +430,18 @@ export function setNativeSelection(
|
||||
}
|
||||
|
||||
export function setNativeSelectionWithPaths(
|
||||
rootElement,
|
||||
anchorPath,
|
||||
anchorOffset,
|
||||
focusPath,
|
||||
focusOffset,
|
||||
rootElement: Node,
|
||||
anchorPath: number[],
|
||||
anchorOffset: number,
|
||||
focusPath: number[],
|
||||
focusOffset: number,
|
||||
) {
|
||||
const anchorNode = getNodeFromPath(anchorPath, rootElement);
|
||||
const focusNode = getNodeFromPath(focusPath, rootElement);
|
||||
setNativeSelection(anchorNode, anchorOffset, focusNode, focusOffset);
|
||||
}
|
||||
|
||||
function getLastTextNode(startingNode) {
|
||||
function getLastTextNode(startingNode: Node) {
|
||||
let node = startingNode;
|
||||
|
||||
mainLoop: while (node !== null) {
|
||||
@ -477,7 +480,7 @@ function getLastTextNode(startingNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getNextTextNode(startingNode) {
|
||||
function getNextTextNode(startingNode: Node) {
|
||||
let node = startingNode;
|
||||
|
||||
mainLoop: while (node !== null) {
|
||||
@ -517,12 +520,14 @@ function getNextTextNode(startingNode) {
|
||||
}
|
||||
|
||||
function moveNativeSelectionBackward() {
|
||||
const domSelection = window.getSelection();
|
||||
let {anchorNode, anchorOffset} = domSelection;
|
||||
const domSelection = window.getSelection()!;
|
||||
let anchorNode = domSelection.anchorNode!;
|
||||
let anchorOffset = domSelection.anchorOffset!;
|
||||
|
||||
if (domSelection.isCollapsed) {
|
||||
const target =
|
||||
anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode;
|
||||
const target = (
|
||||
anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode
|
||||
)!;
|
||||
const keyDownEvent = new KeyboardEvent('keydown', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
@ -539,7 +544,7 @@ function moveNativeSelectionBackward() {
|
||||
if (lastTextNode === null) {
|
||||
throw new Error('moveNativeSelectionBackward: TODO');
|
||||
} else {
|
||||
const textLength = lastTextNode.nodeValue.length;
|
||||
const textLength = lastTextNode.nodeValue!.length;
|
||||
setNativeSelection(
|
||||
lastTextNode,
|
||||
textLength,
|
||||
@ -557,7 +562,7 @@ function moveNativeSelectionBackward() {
|
||||
}
|
||||
} else if (anchorNode.nodeType === 1) {
|
||||
if (anchorNode.nodeName === 'BR') {
|
||||
const parentNode = anchorNode.parentNode;
|
||||
const parentNode = anchorNode.parentNode!;
|
||||
const childNodes = Array.from(parentNode.childNodes);
|
||||
anchorOffset = childNodes.indexOf(anchorNode as ChildNode);
|
||||
anchorNode = parentNode;
|
||||
@ -584,12 +589,14 @@ function moveNativeSelectionBackward() {
|
||||
}
|
||||
|
||||
function moveNativeSelectionForward() {
|
||||
const domSelection = window.getSelection();
|
||||
const {anchorNode, anchorOffset} = domSelection;
|
||||
const domSelection = window.getSelection()!;
|
||||
const anchorNode = domSelection.anchorNode!;
|
||||
const anchorOffset = domSelection.anchorOffset!;
|
||||
|
||||
if (domSelection.isCollapsed) {
|
||||
const target =
|
||||
anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode;
|
||||
const target = (
|
||||
anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode
|
||||
)!;
|
||||
const keyDownEvent = new KeyboardEvent('keydown', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
@ -600,7 +607,7 @@ function moveNativeSelectionForward() {
|
||||
|
||||
if (!keyDownEvent.defaultPrevented) {
|
||||
if (anchorNode.nodeType === 3) {
|
||||
const text = anchorNode.nodeValue;
|
||||
const text = anchorNode.nodeValue!;
|
||||
|
||||
if (text.length === anchorOffset) {
|
||||
const nextTextNode = getNextTextNode(anchorNode);
|
||||
@ -635,8 +642,13 @@ function moveNativeSelectionForward() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function applySelectionInputs(inputs, update, editor) {
|
||||
const rootElement = editor.getRootElement();
|
||||
export async function applySelectionInputs(
|
||||
// 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++) {
|
||||
const input = inputs[i];
|
||||
@ -644,7 +656,7 @@ export async function applySelectionInputs(inputs, update, editor) {
|
||||
|
||||
for (let j = 0; j < times; j++) {
|
||||
await update(() => {
|
||||
const selection = $getSelection();
|
||||
const selection = $getSelection()!;
|
||||
|
||||
switch (input.type) {
|
||||
case 'insert_text': {
|
||||
@ -800,7 +812,7 @@ export async function applySelectionInputs(inputs, update, editor) {
|
||||
}),
|
||||
{
|
||||
clipboardData: {
|
||||
getData: (type) => {
|
||||
getData: (type: string) => {
|
||||
if (type === 'text/plain') {
|
||||
return input.text;
|
||||
}
|
||||
@ -823,7 +835,7 @@ export async function applySelectionInputs(inputs, update, editor) {
|
||||
}),
|
||||
{
|
||||
clipboardData: {
|
||||
getData: (type) => {
|
||||
getData: (type: string) => {
|
||||
if (type === 'application/x-lexical-editor') {
|
||||
return input.text;
|
||||
}
|
||||
@ -846,7 +858,7 @@ export async function applySelectionInputs(inputs, update, editor) {
|
||||
}),
|
||||
{
|
||||
clipboardData: {
|
||||
getData: (type) => {
|
||||
getData: (type: string) => {
|
||||
if (type === 'text/html') {
|
||||
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();
|
||||
|
||||
if (!$isRangeSelection(selection)) {
|
||||
@ -884,7 +898,9 @@ export function setAnchorPoint(point) {
|
||||
anchor.key = point.key;
|
||||
}
|
||||
|
||||
export function setFocusPoint(point) {
|
||||
export function setFocusPoint(
|
||||
point: Pick<PointType, 'type' | 'offset' | 'key'>,
|
||||
) {
|
||||
const selection = $getSelection();
|
||||
|
||||
if (!$isRangeSelection(selection)) {
|
||||
|
@ -13,23 +13,27 @@ import {
|
||||
$createTextNode,
|
||||
$getRoot,
|
||||
$setSelection,
|
||||
EditorState,
|
||||
ParagraphNode,
|
||||
RootNode,
|
||||
TextNode,
|
||||
} from 'lexical';
|
||||
import {createTestEditor} from 'lexical/src/__tests__/utils';
|
||||
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';
|
||||
|
||||
describe('table selection', () => {
|
||||
let originalText;
|
||||
let parsedParagraph;
|
||||
let parsedRoot;
|
||||
let parsedText;
|
||||
let paragraphKey;
|
||||
let textKey;
|
||||
let parsedEditorState;
|
||||
let reactRoot;
|
||||
let originalText: TextNode;
|
||||
let parsedParagraph: ParagraphNode;
|
||||
let parsedRoot: RootNode;
|
||||
let parsedText: TextNode;
|
||||
let paragraphKey: string;
|
||||
let textKey: string;
|
||||
let parsedEditorState: EditorState;
|
||||
let reactRoot: Root;
|
||||
let container: HTMLDivElement | null = null;
|
||||
let editor: LexicalEditor = null;
|
||||
let editor: LexicalEditor | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
@ -37,7 +41,10 @@ describe('table selection', () => {
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
function useLexicalEditor(rootElementRef, onError) {
|
||||
function useLexicalEditor(
|
||||
rootElementRef: React.RefObject<HTMLDivElement>,
|
||||
onError?: () => void,
|
||||
) {
|
||||
const editorInHook = useMemo(
|
||||
() =>
|
||||
createTestEditor({
|
||||
@ -77,8 +84,8 @@ describe('table selection', () => {
|
||||
});
|
||||
}
|
||||
|
||||
async function update(fn) {
|
||||
editor.update(fn);
|
||||
async function update(fn: () => void) {
|
||||
editor!.update(fn);
|
||||
|
||||
return Promise.resolve().then();
|
||||
}
|
||||
@ -101,15 +108,15 @@ describe('table selection', () => {
|
||||
});
|
||||
|
||||
const stringifiedEditorState = JSON.stringify(
|
||||
editor.getEditorState().toJSON(),
|
||||
editor!.getEditorState().toJSON(),
|
||||
);
|
||||
|
||||
parsedEditorState = editor.parseEditorState(stringifiedEditorState);
|
||||
parsedEditorState = editor!.parseEditorState(stringifiedEditorState);
|
||||
parsedEditorState.read(() => {
|
||||
parsedRoot = $getRoot();
|
||||
parsedParagraph = parsedRoot.getFirstChild();
|
||||
parsedParagraph = parsedRoot.getFirstChild()!;
|
||||
paragraphKey = parsedParagraph.getKey();
|
||||
parsedText = parsedParagraph.getFirstChild();
|
||||
parsedText = parsedParagraph.getFirstChild()!;
|
||||
textKey = parsedText.getKey();
|
||||
});
|
||||
});
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
pasteHTML,
|
||||
} from '@lexical/selection/src/__tests__/utils';
|
||||
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
|
||||
import {LexicalEditor} from 'lexical';
|
||||
import {initializeClipboard, TestComposer} from 'lexical/src/__tests__/utils';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import * as ReactTestUtils from 'react-dom/test-utils';
|
||||
@ -72,7 +73,7 @@ Range.prototype.getBoundingClientRect = function (): DOMRect {
|
||||
};
|
||||
|
||||
describe('LexicalEventHelpers', () => {
|
||||
let container = null;
|
||||
let container: HTMLDivElement | null = null;
|
||||
|
||||
beforeEach(async () => {
|
||||
container = document.createElement('div');
|
||||
@ -81,15 +82,15 @@ describe('LexicalEventHelpers', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(container!);
|
||||
container = null;
|
||||
});
|
||||
|
||||
let editor = null;
|
||||
let editor: LexicalEditor | null = null;
|
||||
|
||||
async function init() {
|
||||
function TestBase() {
|
||||
function TestPlugin(): JSX.Element {
|
||||
function TestPlugin(): null {
|
||||
[editor] = useLexicalComposerContext();
|
||||
|
||||
return null;
|
||||
@ -146,8 +147,8 @@ describe('LexicalEventHelpers', () => {
|
||||
}}>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
// eslint-disable-next-line jsx-a11y/aria-role
|
||||
<ContentEditable role={null} spellCheck={null} />
|
||||
// eslint-disable-next-line jsx-a11y/aria-role, @typescript-eslint/no-explicit-any
|
||||
<ContentEditable role={null as any} spellCheck={null as any} />
|
||||
}
|
||||
placeholder={null}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
@ -159,20 +160,20 @@ describe('LexicalEventHelpers', () => {
|
||||
}
|
||||
|
||||
ReactTestUtils.act(() => {
|
||||
createRoot(container).render(<TestBase />);
|
||||
createRoot(container!).render(<TestBase />);
|
||||
});
|
||||
}
|
||||
|
||||
async function update(fn) {
|
||||
async function update(fn: () => void) {
|
||||
await ReactTestUtils.act(async () => {
|
||||
await editor.update(fn);
|
||||
await editor!.update(fn);
|
||||
});
|
||||
|
||||
return Promise.resolve().then();
|
||||
}
|
||||
|
||||
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>',
|
||||
);
|
||||
});
|
||||
@ -344,10 +345,10 @@ describe('LexicalEventHelpers', () => {
|
||||
const name = testUnit.name || 'Test case';
|
||||
|
||||
test(name + ` (#${i + 1})`, async () => {
|
||||
await applySelectionInputs(testUnit.inputs, update, editor);
|
||||
await applySelectionInputs(testUnit.inputs, update, editor!);
|
||||
|
||||
// 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';
|
||||
|
||||
test(name + ` (#${i + 1})`, async () => {
|
||||
await applySelectionInputs(testUnit.inputs, update, editor);
|
||||
await applySelectionInputs(testUnit.inputs, update, editor!);
|
||||
|
||||
// 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';
|
||||
|
||||
// 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 () => {
|
||||
await applySelectionInputs(testUnit.inputs, update, editor);
|
||||
await applySelectionInputs(testUnit.inputs, update, editor!);
|
||||
|
||||
// Validate HTML matches
|
||||
expect(container.firstChild.innerHTML).toBe(testUnit.expectedHTML);
|
||||
expect((container!.firstChild as HTMLElement).innerHTML).toBe(
|
||||
testUnit.expectedHTML,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -127,7 +127,7 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
editor.getEditorState().read(() => {
|
||||
const expectedNodes = expectedKeys.map(({depth, node: nodeKey}) => ({
|
||||
depth,
|
||||
node: $getNodeByKey(nodeKey).getLatest(),
|
||||
node: $getNodeByKey(nodeKey)!.getLatest(),
|
||||
}));
|
||||
|
||||
const first = expectedNodes[0];
|
||||
@ -147,10 +147,10 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
test('DFS triggers getLatest()', async () => {
|
||||
const editor: LexicalEditor = testEnv.editor;
|
||||
|
||||
let rootKey;
|
||||
let paragraphKey;
|
||||
let block1Key;
|
||||
let block2Key;
|
||||
let rootKey: string;
|
||||
let paragraphKey: string;
|
||||
let block1Key: string;
|
||||
let block2Key: string;
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
@ -179,14 +179,14 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
|
||||
block1.append(block3);
|
||||
|
||||
expect($dfs(root)).toEqual([
|
||||
expect($dfs(root!)).toEqual([
|
||||
{
|
||||
depth: 0,
|
||||
node: root.getLatest(),
|
||||
node: root!.getLatest(),
|
||||
},
|
||||
{
|
||||
depth: 1,
|
||||
node: paragraph.getLatest(),
|
||||
node: paragraph!.getLatest(),
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
@ -198,7 +198,7 @@ describe('LexicalNodeHelpers tests', () => {
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
node: block2.getLatest(),
|
||||
node: block2!.getLatest(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -17,7 +17,7 @@ import {$splitNode} from '../../index';
|
||||
describe('LexicalUtils#splitNode', () => {
|
||||
let editor: LexicalEditor;
|
||||
|
||||
const update = async (updateFn) => {
|
||||
const update = async (updateFn: () => void) => {
|
||||
editor.update(updateFn);
|
||||
await Promise.resolve();
|
||||
};
|
||||
@ -115,7 +115,7 @@ describe('LexicalUtils#splitNode', () => {
|
||||
|
||||
let nodeToSplit: ElementNode = $getRoot();
|
||||
for (const index of testCase.splitPath) {
|
||||
nodeToSplit = nodeToSplit.getChildAtIndex(index);
|
||||
nodeToSplit = nodeToSplit.getChildAtIndex(index)!;
|
||||
if (!$isElementNode(nodeToSplit)) {
|
||||
throw new Error('Expected node to be element');
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import {$insertNodeToNearestRoot} from '../..';
|
||||
describe('LexicalUtils#insertNodeToNearestRoot', () => {
|
||||
let editor: LexicalEditor;
|
||||
|
||||
const update = async (updateFn) => {
|
||||
const update = async (updateFn: () => void) => {
|
||||
editor.update(updateFn);
|
||||
await Promise.resolve();
|
||||
};
|
||||
@ -155,7 +155,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
|
||||
'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
|
||||
|
@ -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.
|
||||
|
||||
## 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?
|
||||
|
||||
Listening to text insertion events is problematic with content editables in general. It's a common source of bugs due to how
|
||||
|
@ -207,7 +207,6 @@
|
||||
"include": ["./libdefs", "./packages"],
|
||||
"exclude": [
|
||||
"./libdefs/*.js",
|
||||
"**/lexical-*/src/__tests__/**",
|
||||
"**/dist/**",
|
||||
"**/npm/**",
|
||||
"**/node_modules/**",
|
||||
|
@ -10,7 +10,6 @@
|
||||
"**/dist/**",
|
||||
"**/npm/**",
|
||||
"**/node_modules/**",
|
||||
"./packages/playwright-core/**",
|
||||
"./packages/lexical-devtools/**"
|
||||
],
|
||||
"extends": "./tsconfig.json"
|
||||
|
Reference in New Issue
Block a user