2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
9 import {$createLinkNode} from '@lexical/link';
10 import {$createHeadingNode, $isHeadingNode} from '@lexical/rich-text';
12 $getSelectionStyleValueForProperty,
14 } from '@lexical/selection';
18 $createRangeSelection,
37 $createTestDecoratorNode,
38 $createTestElementNode,
39 $createTestShadowRootNode,
41 createTestHeadlessEditor,
44 } from 'lexical/src/__tests__/utils';
46 import {$setAnchorPoint, $setFocusPoint} from '../utils';
48 Range.prototype.getBoundingClientRect = function (): DOMRect {
67 function $createParagraphWithNodes(
68 editor: LexicalEditor,
69 nodes: {text: string; key: string; mergeable?: boolean}[],
71 const paragraph = $createParagraphNode();
72 const nodeMap = editor._pendingEditorState!._nodeMap;
74 for (let i = 0; i < nodes.length; i++) {
75 const {text, key, mergeable} = nodes[i];
76 const textNode = new TextNode(text, key);
77 nodeMap.set(key, textNode);
80 textNode.toggleUnmergeable();
83 paragraph.append(textNode);
89 describe('LexicalSelectionHelpers tests', () => {
90 describe('Collapsed', () => {
91 test('Can handle a text point', () => {
92 const setupTestCase = (
93 cb: (selection: RangeSelection, node: ElementNode) => void,
95 const editor = createTestEditor();
98 const root = $getRoot();
100 const element = $createParagraphWithNodes(editor, [
118 root.append(element);
131 const selection = $getSelection();
132 cb(selection as RangeSelection, element);
137 setupTestCase((selection, state) => {
138 expect(selection.getNodes()).toEqual([$getNodeByKey('a')]);
142 setupTestCase((selection) => {
143 expect(selection.getTextContent()).toEqual('');
147 setupTestCase((selection, state) => {
148 selection.insertText('Test');
150 expect($getNodeByKey('a')!.getTextContent()).toBe('Testa');
152 expect(selection.anchor).toEqual(
153 expect.objectContaining({
160 expect(selection.focus).toEqual(
161 expect.objectContaining({
170 setupTestCase((selection, element) => {
171 selection.insertNodes([$createTextNode('foo')]);
173 expect(selection.anchor).toEqual(
174 expect.objectContaining({
175 key: element.getFirstChild()!.getKey(),
181 expect(selection.focus).toEqual(
182 expect.objectContaining({
183 key: element.getFirstChild()!.getKey(),
191 setupTestCase((selection) => {
192 selection.insertParagraph();
194 expect(selection.anchor).toEqual(
195 expect.objectContaining({
202 expect(selection.focus).toEqual(
203 expect.objectContaining({
212 setupTestCase((selection, element) => {
213 selection.insertLineBreak(true);
215 expect(selection.anchor).toEqual(
216 expect.objectContaining({
217 key: element.getKey(),
223 expect(selection.focus).toEqual(
224 expect.objectContaining({
225 key: element.getKey(),
233 setupTestCase((selection, element) => {
234 selection.formatText('bold');
235 selection.insertText('Test');
237 expect(element.getFirstChild()!.getTextContent()).toBe('Test');
239 expect(selection.anchor).toEqual(
240 expect.objectContaining({
241 key: element.getFirstChild()!.getKey(),
247 expect(selection.focus).toEqual(
248 expect.objectContaining({
249 key: element.getFirstChild()!.getKey(),
256 element.getFirstChild()!.getNextSibling()!.getTextContent(),
261 setupTestCase((selection, state) => {
262 expect(selection.extract()).toEqual([$getNodeByKey('a')]);
266 test('Has correct text point after removal after merge', async () => {
267 const editor = createTestEditor();
269 const domElement = document.createElement('div');
272 editor.setRootElement(domElement);
274 editor.update(() => {
275 const root = $getRoot();
277 element = $createParagraphWithNodes(editor, [
305 root.append(element);
320 await Promise.resolve().then();
322 editor.getEditorState().read(() => {
323 const selection = $getSelection();
325 if (!$isRangeSelection(selection)) {
329 expect(selection.anchor).toEqual(
330 expect.objectContaining({
337 expect(selection.focus).toEqual(
338 expect.objectContaining({
347 test('Has correct text point after removal after merge (2)', async () => {
348 const editor = createTestEditor();
350 const domElement = document.createElement('div');
353 editor.setRootElement(domElement);
355 editor.update(() => {
356 const root = $getRoot();
358 element = $createParagraphWithNodes(editor, [
381 root.append(element);
396 await Promise.resolve().then();
398 editor.getEditorState().read(() => {
399 const selection = $getSelection();
401 if (!$isRangeSelection(selection)) {
405 expect(selection.anchor).toEqual(
406 expect.objectContaining({
413 expect(selection.focus).toEqual(
414 expect.objectContaining({
423 test('Has correct text point adjust to element point after removal of a single empty text node', async () => {
424 const editor = createTestEditor();
426 const domElement = document.createElement('div');
427 let element: ParagraphNode;
429 editor.setRootElement(domElement);
431 editor.update(() => {
432 const root = $getRoot();
434 element = $createParagraphWithNodes(editor, [
442 root.append(element);
457 await Promise.resolve().then();
459 editor.getEditorState().read(() => {
460 const selection = $getSelection();
462 if (!$isRangeSelection(selection)) {
466 expect(selection.anchor).toEqual(
467 expect.objectContaining({
468 key: element.getKey(),
474 expect(selection.focus).toEqual(
475 expect.objectContaining({
476 key: element.getKey(),
484 test('Has correct element point after removal of an empty text node in a group #1', async () => {
485 const editor = createTestEditor();
487 const domElement = document.createElement('div');
490 editor.setRootElement(domElement);
492 editor.update(() => {
493 const root = $getRoot();
495 element = $createParagraphWithNodes(editor, [
508 root.append(element);
511 key: element.getKey(),
517 key: element.getKey(),
523 await Promise.resolve().then();
525 editor.getEditorState().read(() => {
526 const selection = $getSelection();
528 if (!$isRangeSelection(selection)) {
532 expect(selection.anchor).toEqual(
533 expect.objectContaining({
540 expect(selection.focus).toEqual(
541 expect.objectContaining({
550 test('Has correct element point after removal of an empty text node in a group #2', async () => {
551 const editor = createTestEditor();
553 const domElement = document.createElement('div');
556 editor.setRootElement(domElement);
558 editor.update(() => {
559 const root = $getRoot();
561 element = $createParagraphWithNodes(editor, [
584 root.append(element);
587 key: element.getKey(),
593 key: element.getKey(),
599 await Promise.resolve().then();
601 editor.getEditorState().read(() => {
602 const selection = $getSelection();
604 if (!$isRangeSelection(selection)) {
608 expect(selection.anchor).toEqual(
609 expect.objectContaining({
616 expect(selection.focus).toEqual(
617 expect.objectContaining({
626 test('Has correct text point after removal of an empty text node in a group #3', async () => {
627 const editor = createTestEditor();
629 const domElement = document.createElement('div');
632 editor.setRootElement(domElement);
634 editor.update(() => {
635 const root = $getRoot();
637 element = $createParagraphWithNodes(editor, [
660 root.append(element);
675 await Promise.resolve().then();
677 editor.getEditorState().read(() => {
678 const selection = $getSelection();
680 if (!$isRangeSelection(selection)) {
684 expect(selection.anchor).toEqual(
685 expect.objectContaining({
692 expect(selection.focus).toEqual(
693 expect.objectContaining({
702 test('Can handle an element point on empty element', () => {
703 const setupTestCase = (
704 cb: (selection: RangeSelection, el: ElementNode) => void,
706 const editor = createTestEditor();
708 editor.update(() => {
709 const root = $getRoot();
711 const element = $createParagraphWithNodes(editor, []);
713 root.append(element);
716 key: element.getKey(),
722 key: element.getKey(),
726 const selection = $getSelection();
727 cb(selection as RangeSelection, element);
732 setupTestCase((selection, element) => {
733 expect(selection.getNodes()).toEqual([element]);
737 setupTestCase((selection) => {
738 expect(selection.getTextContent()).toEqual('');
742 setupTestCase((selection, element) => {
743 selection.insertText('Test');
744 const firstChild = element.getFirstChild()!;
746 expect(firstChild.getTextContent()).toBe('Test');
748 expect(selection.anchor).toEqual(
749 expect.objectContaining({
750 key: firstChild.getKey(),
756 expect(selection.focus).toEqual(
757 expect.objectContaining({
758 key: firstChild.getKey(),
766 setupTestCase((selection, element) => {
767 selection.insertParagraph();
768 const nextElement = element.getNextSibling()!;
770 expect(selection.anchor).toEqual(
771 expect.objectContaining({
772 key: nextElement.getKey(),
778 expect(selection.focus).toEqual(
779 expect.objectContaining({
780 key: nextElement.getKey(),
788 setupTestCase((selection, element) => {
789 selection.insertLineBreak(true);
791 expect(selection.anchor).toEqual(
792 expect.objectContaining({
793 key: element.getKey(),
799 expect(selection.focus).toEqual(
800 expect.objectContaining({
801 key: element.getKey(),
809 setupTestCase((selection, element) => {
810 selection.formatText('bold');
811 selection.insertText('Test');
812 const firstChild = element.getFirstChild()!;
814 expect(firstChild.getTextContent()).toBe('Test');
816 expect(selection.anchor).toEqual(
817 expect.objectContaining({
818 key: firstChild.getKey(),
824 expect(selection.focus).toEqual(
825 expect.objectContaining({
826 key: firstChild.getKey(),
834 setupTestCase((selection, element) => {
835 expect(selection.extract()).toEqual([element]);
839 test('Can handle a start element point', () => {
840 const setupTestCase = (
841 cb: (selection: RangeSelection, el: ElementNode) => void,
843 const editor = createTestEditor();
845 editor.update(() => {
846 const root = $getRoot();
848 const element = $createParagraphWithNodes(editor, [
866 root.append(element);
869 key: element.getKey(),
875 key: element.getKey(),
879 const selection = $getSelection();
880 cb(selection as RangeSelection, element);
885 setupTestCase((selection, state) => {
886 expect(selection.getNodes()).toEqual([$getNodeByKey('a')]);
890 setupTestCase((selection) => {
891 expect(selection.getTextContent()).toEqual('');
895 setupTestCase((selection, element) => {
896 selection.insertText('Test');
897 const firstChild = element.getFirstChild()!;
899 expect(firstChild.getTextContent()).toBe('Test');
901 expect(selection.anchor).toEqual(
902 expect.objectContaining({
903 key: firstChild.getKey(),
909 expect(selection.focus).toEqual(
910 expect.objectContaining({
911 key: firstChild.getKey(),
919 setupTestCase((selection, element) => {
920 selection.insertParagraph();
922 expect(selection.anchor).toEqual(
923 expect.objectContaining({
930 expect(selection.focus).toEqual(
931 expect.objectContaining({
940 setupTestCase((selection, element) => {
941 selection.insertLineBreak(true);
943 expect(selection.anchor).toEqual(
944 expect.objectContaining({
945 key: element.getKey(),
951 expect(selection.focus).toEqual(
952 expect.objectContaining({
953 key: element.getKey(),
961 setupTestCase((selection, element) => {
962 selection.formatText('bold');
963 selection.insertText('Test');
965 const firstChild = element.getFirstChild()!;
967 expect(firstChild.getTextContent()).toBe('Test');
969 expect(selection.anchor).toEqual(
970 expect.objectContaining({
971 key: firstChild.getKey(),
977 expect(selection.focus).toEqual(
978 expect.objectContaining({
979 key: firstChild.getKey(),
987 setupTestCase((selection, element) => {
988 expect(selection.extract()).toEqual([$getNodeByKey('a')]);
992 test('Can handle an end element point', () => {
993 const setupTestCase = (
994 cb: (selection: RangeSelection, el: ElementNode) => void,
996 const editor = createTestEditor();
998 editor.update(() => {
999 const root = $getRoot();
1001 const element = $createParagraphWithNodes(editor, [
1019 root.append(element);
1022 key: element.getKey(),
1028 key: element.getKey(),
1032 const selection = $getSelection();
1033 cb(selection as RangeSelection, element);
1038 setupTestCase((selection, state) => {
1039 expect(selection.getNodes()).toEqual([$getNodeByKey('c')]);
1043 setupTestCase((selection) => {
1044 expect(selection.getTextContent()).toEqual('');
1048 setupTestCase((selection, element) => {
1049 selection.insertText('Test');
1050 const lastChild = element.getLastChild()!;
1052 expect(lastChild.getTextContent()).toBe('Test');
1054 expect(selection.anchor).toEqual(
1055 expect.objectContaining({
1056 key: lastChild.getKey(),
1062 expect(selection.focus).toEqual(
1063 expect.objectContaining({
1064 key: lastChild.getKey(),
1072 setupTestCase((selection, element) => {
1073 selection.insertParagraph();
1074 const nextSibling = element.getNextSibling()!;
1076 expect(selection.anchor).toEqual(
1077 expect.objectContaining({
1078 key: nextSibling.getKey(),
1084 expect(selection.focus).toEqual(
1085 expect.objectContaining({
1086 key: nextSibling.getKey(),
1094 setupTestCase((selection, element) => {
1095 selection.insertLineBreak();
1097 expect(selection.anchor).toEqual(
1098 expect.objectContaining({
1099 key: element.getKey(),
1105 expect(selection.focus).toEqual(
1106 expect.objectContaining({
1107 key: element.getKey(),
1115 setupTestCase((selection, element) => {
1116 selection.formatText('bold');
1117 selection.insertText('Test');
1118 const lastChild = element.getLastChild()!;
1120 expect(lastChild.getTextContent()).toBe('Test');
1122 expect(selection.anchor).toEqual(
1123 expect.objectContaining({
1124 key: lastChild.getKey(),
1130 expect(selection.focus).toEqual(
1131 expect.objectContaining({
1132 key: lastChild.getKey(),
1139 // Extract selection
1140 setupTestCase((selection, element) => {
1141 expect(selection.extract()).toEqual([$getNodeByKey('c')]);
1145 test('Has correct element point after merge from middle', async () => {
1146 const editor = createTestEditor();
1148 const domElement = document.createElement('div');
1151 editor.setRootElement(domElement);
1153 editor.update(() => {
1154 const root = $getRoot();
1156 element = $createParagraphWithNodes(editor, [
1174 root.append(element);
1177 key: element.getKey(),
1183 key: element.getKey(),
1189 await Promise.resolve().then();
1191 editor.getEditorState().read(() => {
1192 const selection = $getSelection();
1194 if (!$isRangeSelection(selection)) {
1198 expect(selection.anchor).toEqual(
1199 expect.objectContaining({
1206 expect(selection.focus).toEqual(
1207 expect.objectContaining({
1216 test('Has correct element point after merge from end', async () => {
1217 const editor = createTestEditor();
1219 const domElement = document.createElement('div');
1222 editor.setRootElement(domElement);
1224 editor.update(() => {
1225 const root = $getRoot();
1227 element = $createParagraphWithNodes(editor, [
1245 root.append(element);
1248 key: element.getKey(),
1254 key: element.getKey(),
1260 await Promise.resolve().then();
1262 editor.getEditorState().read(() => {
1263 const selection = $getSelection();
1265 if (!$isRangeSelection(selection)) {
1269 expect(selection.anchor).toEqual(
1270 expect.objectContaining({
1277 expect(selection.focus).toEqual(
1278 expect.objectContaining({
1288 describe('Simple range', () => {
1289 test('Can handle multiple text points', () => {
1290 const setupTestCase = (
1291 cb: (selection: RangeSelection, el: ElementNode) => void,
1293 const editor = createTestEditor();
1295 editor.update(() => {
1296 const root = $getRoot();
1298 const element = $createParagraphWithNodes(editor, [
1316 root.append(element);
1329 const selection = $getSelection();
1330 if (!$isRangeSelection(selection)) {
1333 cb(selection, element);
1338 setupTestCase((selection, state) => {
1339 expect(selection.getNodes()).toEqual([
1346 setupTestCase((selection) => {
1347 expect(selection.getTextContent()).toEqual('a');
1351 setupTestCase((selection, state) => {
1352 selection.insertText('Test');
1354 expect($getNodeByKey('a')!.getTextContent()).toBe('Test');
1356 expect(selection.anchor).toEqual(
1357 expect.objectContaining({
1364 expect(selection.focus).toEqual(
1365 expect.objectContaining({
1374 setupTestCase((selection, element) => {
1375 selection.insertNodes([$createTextNode('foo')]);
1377 expect(selection.anchor).toEqual(
1378 expect.objectContaining({
1379 key: element.getFirstChild()!.getKey(),
1385 expect(selection.focus).toEqual(
1386 expect.objectContaining({
1387 key: element.getFirstChild()!.getKey(),
1395 setupTestCase((selection) => {
1396 selection.insertParagraph();
1398 expect(selection.anchor).toEqual(
1399 expect.objectContaining({
1406 expect(selection.focus).toEqual(
1407 expect.objectContaining({
1416 setupTestCase((selection, element) => {
1417 selection.insertLineBreak(true);
1419 expect(selection.anchor).toEqual(
1420 expect.objectContaining({
1421 key: element.getKey(),
1427 expect(selection.focus).toEqual(
1428 expect.objectContaining({
1429 key: element.getKey(),
1437 setupTestCase((selection, element) => {
1438 selection.formatText('bold');
1439 selection.insertText('Test');
1441 expect(element.getFirstChild()!.getTextContent()).toBe('Test');
1443 expect(selection.anchor).toEqual(
1444 expect.objectContaining({
1445 key: element.getFirstChild()!.getKey(),
1451 expect(selection.focus).toEqual(
1452 expect.objectContaining({
1453 key: element.getFirstChild()!.getKey(),
1460 // Extract selection
1461 setupTestCase((selection, state) => {
1462 expect(selection.extract()).toEqual([{...$getNodeByKey('a')}]);
1466 test('Can handle multiple element points', () => {
1467 const setupTestCase = (
1468 cb: (selection: RangeSelection, el: ElementNode) => void,
1470 const editor = createTestEditor();
1472 editor.update(() => {
1473 const root = $getRoot();
1475 const element = $createParagraphWithNodes(editor, [
1493 root.append(element);
1496 key: element.getKey(),
1502 key: element.getKey(),
1506 const selection = $getSelection();
1507 if (!$isRangeSelection(selection)) {
1510 cb(selection, element);
1515 setupTestCase((selection) => {
1516 expect(selection.getNodes()).toEqual([$getNodeByKey('a')]);
1520 setupTestCase((selection) => {
1521 expect(selection.getTextContent()).toEqual('a');
1525 setupTestCase((selection, element) => {
1526 selection.insertText('Test');
1527 const firstChild = element.getFirstChild()!;
1529 expect(firstChild.getTextContent()).toBe('Test');
1531 expect(selection.anchor).toEqual(
1532 expect.objectContaining({
1533 key: firstChild.getKey(),
1539 expect(selection.focus).toEqual(
1540 expect.objectContaining({
1541 key: firstChild.getKey(),
1549 setupTestCase((selection, element) => {
1550 selection.insertParagraph();
1552 expect(selection.anchor).toEqual(
1553 expect.objectContaining({
1560 expect(selection.focus).toEqual(
1561 expect.objectContaining({
1570 setupTestCase((selection, element) => {
1571 selection.insertLineBreak(true);
1573 expect(selection.anchor).toEqual(
1574 expect.objectContaining({
1575 key: element.getKey(),
1581 expect(selection.focus).toEqual(
1582 expect.objectContaining({
1583 key: element.getKey(),
1591 setupTestCase((selection, element) => {
1592 selection.formatText('bold');
1593 selection.insertText('Test');
1594 const firstChild = element.getFirstChild()!;
1596 expect(firstChild.getTextContent()).toBe('Test');
1598 expect(selection.anchor).toEqual(
1599 expect.objectContaining({
1600 key: firstChild.getKey(),
1606 expect(selection.focus).toEqual(
1607 expect.objectContaining({
1608 key: firstChild.getKey(),
1615 // Extract selection
1616 setupTestCase((selection, element) => {
1617 const firstChild = element.getFirstChild();
1619 expect(selection.extract()).toEqual([firstChild]);
1623 test('Can handle a mix of text and element points', () => {
1624 const setupTestCase = (
1625 cb: (selection: RangeSelection, el: ElementNode) => void,
1627 const editor = createTestEditor();
1629 editor.update(() => {
1630 const root = $getRoot();
1632 const element = $createParagraphWithNodes(editor, [
1650 root.append(element);
1653 key: element.getKey(),
1663 const selection = $getSelection();
1664 if (!$isRangeSelection(selection)) {
1667 cb(selection, element);
1672 setupTestCase((selection, state) => {
1673 expect(selection.anchor.isBefore(selection.focus)).toEqual(true);
1677 setupTestCase((selection, state) => {
1678 expect(selection.getNodes()).toEqual([
1686 setupTestCase((selection) => {
1687 expect(selection.getTextContent()).toEqual('abc');
1691 setupTestCase((selection, element) => {
1692 selection.insertText('Test');
1693 const firstChild = element.getFirstChild()!;
1695 expect(firstChild.getTextContent()).toBe('Test');
1697 expect(selection.anchor).toEqual(
1698 expect.objectContaining({
1699 key: firstChild.getKey(),
1705 expect(selection.focus).toEqual(
1706 expect.objectContaining({
1707 key: firstChild.getKey(),
1715 setupTestCase((selection, element) => {
1716 selection.insertParagraph();
1717 const nextElement = element.getNextSibling()!;
1719 expect(selection.anchor).toEqual(
1720 expect.objectContaining({
1721 key: nextElement.getKey(),
1727 expect(selection.focus).toEqual(
1728 expect.objectContaining({
1729 key: nextElement.getKey(),
1737 setupTestCase((selection, element) => {
1738 selection.insertLineBreak(true);
1740 expect(selection.anchor).toEqual(
1741 expect.objectContaining({
1742 key: element.getKey(),
1748 expect(selection.focus).toEqual(
1749 expect.objectContaining({
1750 key: element.getKey(),
1758 setupTestCase((selection, element) => {
1759 selection.formatText('bold');
1760 selection.insertText('Test');
1761 const firstChild = element.getFirstChild()!;
1763 expect(firstChild.getTextContent()).toBe('Test');
1765 expect(selection.anchor).toEqual(
1766 expect.objectContaining({
1767 key: firstChild.getKey(),
1773 expect(selection.focus).toEqual(
1774 expect.objectContaining({
1775 key: firstChild.getKey(),
1782 // Extract selection
1783 setupTestCase((selection, element) => {
1784 expect(selection.extract()).toEqual([
1793 describe('can insert non-element nodes correctly', () => {
1794 describe('with an empty paragraph node selected', () => {
1795 test('a single text node', async () => {
1796 const editor = createTestEditor();
1798 const element = document.createElement('div');
1800 editor.setRootElement(element);
1802 await editor.update(() => {
1803 const root = $getRoot();
1805 const paragraph = $createParagraphNode();
1806 root.append(paragraph);
1809 key: paragraph.getKey(),
1815 key: paragraph.getKey(),
1820 const selection = $getSelection();
1822 if (!$isRangeSelection(selection)) {
1826 selection.insertNodes([$createTextNode('foo')]);
1829 expect(element.innerHTML).toBe(
1830 '<p dir="ltr"><span data-lexical-text="true">foo</span></p>',
1834 test('two text nodes', async () => {
1835 const editor = createTestEditor();
1837 const element = document.createElement('div');
1839 editor.setRootElement(element);
1841 await editor.update(() => {
1842 const root = $getRoot();
1844 const paragraph = $createParagraphNode();
1845 root.append(paragraph);
1848 key: paragraph.getKey(),
1854 key: paragraph.getKey(),
1858 const selection = $getSelection();
1860 if (!$isRangeSelection(selection)) {
1864 selection.insertNodes([
1865 $createTextNode('foo'),
1866 $createTextNode('bar'),
1870 expect(element.innerHTML).toBe(
1871 '<p dir="ltr"><span data-lexical-text="true">foobar</span></p>',
1875 test('link insertion without parent element', async () => {
1876 const editor = createTestEditor();
1878 const element = document.createElement('div');
1880 editor.setRootElement(element);
1882 await editor.update(() => {
1883 const root = $getRoot();
1885 const paragraph = $createParagraphNode();
1886 root.append(paragraph);
1889 key: paragraph.getKey(),
1895 key: paragraph.getKey(),
1899 const link = $createLinkNode('https://');
1900 link.append($createTextNode('ello worl'));
1902 const selection = $getSelection();
1904 if (!$isRangeSelection(selection)) {
1908 selection.insertNodes([
1909 $createTextNode('h'),
1911 $createTextNode('d'),
1915 expect(element.innerHTML).toBe(
1916 '<p dir="ltr"><span data-lexical-text="true">h</span><a href="https://" dir="ltr"><span data-lexical-text="true">ello worl</span></a><span data-lexical-text="true">d</span></p>',
1920 test('a single heading node with a child text node', async () => {
1921 const editor = createTestEditor();
1923 const element = document.createElement('div');
1925 editor.setRootElement(element);
1927 await editor.update(() => {
1928 const root = $getRoot();
1930 const paragraph = $createParagraphNode();
1931 root.append(paragraph);
1934 key: paragraph.getKey(),
1940 key: paragraph.getKey(),
1945 const heading = $createHeadingNode('h1');
1946 const child = $createTextNode('foo');
1948 heading.append(child);
1950 const selection = $getSelection();
1952 if (!$isRangeSelection(selection)) {
1955 selection.insertNodes([heading]);
1958 expect(element.innerHTML).toBe(
1959 '<h1 dir="ltr"><span data-lexical-text="true">foo</span></h1>',
1964 describe('with a paragraph node selected on some existing text', () => {
1965 test('a single text node', async () => {
1966 const editor = createTestEditor();
1968 const element = document.createElement('div');
1970 editor.setRootElement(element);
1972 await editor.update(() => {
1973 const root = $getRoot();
1975 const paragraph = $createParagraphNode();
1976 const text = $createTextNode('Existing text...');
1978 paragraph.append(text);
1979 root.append(paragraph);
1993 const selection = $getSelection();
1995 if (!$isRangeSelection(selection)) {
1998 selection.insertNodes([$createTextNode('foo')]);
2001 expect(element.innerHTML).toBe(
2002 '<p dir="ltr"><span data-lexical-text="true">Existing text...foo</span></p>',
2006 test('two text nodes', async () => {
2007 const editor = createTestEditor();
2009 const element = document.createElement('div');
2011 editor.setRootElement(element);
2013 await editor.update(() => {
2014 const root = $getRoot();
2016 const paragraph = $createParagraphNode();
2017 const text = $createTextNode('Existing text...');
2019 paragraph.append(text);
2020 root.append(paragraph);
2034 const selection = $getSelection();
2036 if (!$isRangeSelection(selection)) {
2040 selection.insertNodes([
2041 $createTextNode('foo'),
2042 $createTextNode('bar'),
2046 expect(element.innerHTML).toBe(
2047 '<p dir="ltr"><span data-lexical-text="true">Existing text...foobar</span></p>',
2051 test('a single heading node with a child text node', async () => {
2052 const editor = createTestEditor();
2054 const element = document.createElement('div');
2056 editor.setRootElement(element);
2058 await editor.update(() => {
2059 const root = $getRoot();
2061 const paragraph = $createParagraphNode();
2062 const text = $createTextNode('Existing text...');
2064 paragraph.append(text);
2065 root.append(paragraph);
2079 const heading = $createHeadingNode('h1');
2080 const child = $createTextNode('foo');
2082 heading.append(child);
2084 const selection = $getSelection();
2086 if (!$isRangeSelection(selection)) {
2090 selection.insertNodes([heading]);
2093 expect(element.innerHTML).toBe(
2094 '<p dir="ltr"><span data-lexical-text="true">Existing text...foo</span></p>',
2098 test('a paragraph with a child text and a child italic text and a child text', async () => {
2099 const editor = createTestEditor();
2101 const element = document.createElement('div');
2103 editor.setRootElement(element);
2105 await editor.update(() => {
2106 const root = $getRoot();
2108 const paragraph = $createParagraphNode();
2109 const text = $createTextNode('AE');
2111 paragraph.append(text);
2112 root.append(paragraph);
2126 const insertedParagraph = $createParagraphNode();
2127 const insertedTextB = $createTextNode('B');
2128 const insertedTextC = $createTextNode('C');
2129 const insertedTextD = $createTextNode('D');
2131 insertedTextC.toggleFormat('italic');
2133 insertedParagraph.append(insertedTextB, insertedTextC, insertedTextD);
2135 const selection = $getSelection();
2137 if (!$isRangeSelection(selection)) {
2141 selection.insertNodes([insertedParagraph]);
2143 expect(selection.anchor).toEqual(
2144 expect.objectContaining({
2146 .getChildAtIndex(paragraph.getChildrenSize() - 2)!
2153 expect(selection.focus).toEqual(
2154 expect.objectContaining({
2156 .getChildAtIndex(paragraph.getChildrenSize() - 2)!
2164 expect(element.innerHTML).toBe(
2165 '<p dir="ltr"><span data-lexical-text="true">AB</span><em data-lexical-text="true">C</em><span data-lexical-text="true">DE</span></p>',
2170 describe('with a fully-selected text node', () => {
2171 test('a single text node', async () => {
2172 const editor = createTestEditor();
2174 const element = document.createElement('div');
2176 editor.setRootElement(element);
2178 await editor.update(() => {
2179 const root = $getRoot();
2181 const paragraph = $createParagraphNode();
2182 root.append(paragraph);
2184 const text = $createTextNode('Existing text...');
2185 paragraph.append(text);
2195 offset: 'Existing text...'.length,
2199 const selection = $getSelection();
2201 if (!$isRangeSelection(selection)) {
2204 selection.insertNodes([$createTextNode('foo')]);
2207 expect(element.innerHTML).toBe(
2208 '<p dir="ltr"><span data-lexical-text="true">foo</span></p>',
2213 describe('with a fully-selected text node followed by an inline element', () => {
2214 test('a single text node', async () => {
2215 const editor = createTestEditor();
2217 const element = document.createElement('div');
2219 editor.setRootElement(element);
2221 await editor.update(() => {
2222 const root = $getRoot();
2224 const paragraph = $createParagraphNode();
2225 root.append(paragraph);
2227 const text = $createTextNode('Existing text...');
2228 paragraph.append(text);
2230 const link = $createLinkNode('https://');
2231 link.append($createTextNode('link'));
2232 paragraph.append(link);
2242 offset: 'Existing text...'.length,
2246 const selection = $getSelection();
2248 if (!$isRangeSelection(selection)) {
2251 selection.insertNodes([$createTextNode('foo')]);
2254 expect(element.innerHTML).toBe(
2255 '<p dir="ltr"><span data-lexical-text="true">foo</span><a href="https://" dir="ltr"><span data-lexical-text="true">link</span></a></p>',
2260 describe('with a fully-selected text node preceded by an inline element', () => {
2261 test('a single text node', async () => {
2262 const editor = createTestEditor();
2264 const element = document.createElement('div');
2266 editor.setRootElement(element);
2268 await editor.update(() => {
2269 const root = $getRoot();
2271 const paragraph = $createParagraphNode();
2272 root.append(paragraph);
2274 const link = $createLinkNode('https://');
2275 link.append($createTextNode('link'));
2276 paragraph.append(link);
2278 const text = $createTextNode('Existing text...');
2279 paragraph.append(text);
2289 offset: 'Existing text...'.length,
2293 const selection = $getSelection();
2295 if (!$isRangeSelection(selection)) {
2298 selection.insertNodes([$createTextNode('foo')]);
2301 expect(element.innerHTML).toBe(
2302 '<p dir="ltr"><a href="https://" dir="ltr"><span data-lexical-text="true">link</span></a><span data-lexical-text="true">foo</span></p>',
2307 test.skip('can insert a linebreak node before an inline element node', async () => {
2308 const editor = createTestEditor();
2309 const element = document.createElement('div');
2310 editor.setRootElement(element);
2312 await editor.update(() => {
2313 const root = $getRoot();
2314 const paragraph = $createParagraphNode();
2315 root.append(paragraph);
2316 const link = $createLinkNode('https://lexical.dev/');
2317 paragraph.append(link);
2318 const text = $createTextNode('Lexical');
2322 $insertNodes([$createLineBreakNode()]);
2325 // TODO #5109 ElementNode should have a way to control when other nodes can be inserted inside
2326 expect(element.innerHTML).toBe(
2327 '<p><a href="https://lexical.dev/" dir="ltr"><br><span data-lexical-text="true">Lexical</span></a></p>',
2332 describe('can insert block element nodes correctly', () => {
2333 describe('with a fully-selected text node', () => {
2334 test('a paragraph node', async () => {
2335 const editor = createTestEditor();
2337 const element = document.createElement('div');
2339 editor.setRootElement(element);
2341 await editor.update(() => {
2342 const root = $getRoot();
2344 const paragraph = $createParagraphNode();
2345 root.append(paragraph);
2347 const text = $createTextNode('Existing text...');
2348 paragraph.append(text);
2358 offset: 'Existing text...'.length,
2362 const paragraphToInsert = $createParagraphNode();
2363 paragraphToInsert.append($createTextNode('foo'));
2365 const selection = $getSelection();
2367 if (!$isRangeSelection(selection)) {
2370 selection.insertNodes([paragraphToInsert]);
2373 expect(element.innerHTML).toBe(
2374 '<p dir="ltr"><span data-lexical-text="true">foo</span></p>',
2379 describe('with a fully-selected text node followed by an inline element', () => {
2380 test('a paragraph node', async () => {
2381 const editor = createTestEditor();
2383 const element = document.createElement('div');
2385 editor.setRootElement(element);
2387 await editor.update(() => {
2388 const root = $getRoot();
2390 const paragraph = $createParagraphNode();
2391 root.append(paragraph);
2393 const text = $createTextNode('Existing text...');
2394 paragraph.append(text);
2396 const link = $createLinkNode('https://');
2397 link.append($createTextNode('link'));
2398 paragraph.append(link);
2408 offset: 'Existing text...'.length,
2412 const paragraphToInsert = $createParagraphNode();
2413 paragraphToInsert.append($createTextNode('foo'));
2415 const selection = $getSelection();
2417 if (!$isRangeSelection(selection)) {
2420 selection.insertNodes([paragraphToInsert]);
2423 expect(element.innerHTML).toBe(
2424 '<p dir="ltr"><span data-lexical-text="true">foo</span><a href="https://" dir="ltr"><span data-lexical-text="true">link</span></a></p>',
2429 describe('with a fully-selected text node preceded by an inline element', () => {
2430 test('a paragraph node', async () => {
2431 const editor = createTestEditor();
2433 const element = document.createElement('div');
2435 editor.setRootElement(element);
2437 await editor.update(() => {
2438 const root = $getRoot();
2440 const paragraph = $createParagraphNode();
2441 root.append(paragraph);
2443 const link = $createLinkNode('https://');
2444 link.append($createTextNode('link'));
2445 paragraph.append(link);
2447 const text = $createTextNode('Existing text...');
2448 paragraph.append(text);
2458 offset: 'Existing text...'.length,
2462 const paragraphToInsert = $createParagraphNode();
2463 paragraphToInsert.append($createTextNode('foo'));
2465 const selection = $getSelection();
2467 if (!$isRangeSelection(selection)) {
2470 selection.insertNodes([paragraphToInsert]);
2473 expect(element.innerHTML).toBe(
2474 '<p dir="ltr"><a href="https://" dir="ltr"><span data-lexical-text="true">link</span></a><span data-lexical-text="true">foo</span></p>',
2479 test('Can insert link into empty paragraph', async () => {
2480 const editor = createTestEditor();
2481 const element = document.createElement('div');
2482 editor.setRootElement(element);
2484 await editor.update(() => {
2485 const root = $getRoot();
2486 const paragraph = $createParagraphNode();
2487 root.append(paragraph);
2488 const linkNode = $createLinkNode('https://lexical.dev');
2489 const linkTextNode = $createTextNode('Lexical');
2490 linkNode.append(linkTextNode);
2491 $insertNodes([linkNode]);
2493 expect(element.innerHTML).toBe(
2494 '<p><a href="https://lexical.dev" dir="ltr"><span data-lexical-text="true">Lexical</span></a></p>',
2498 test('Can insert link into empty paragraph (2)', async () => {
2499 const editor = createTestEditor();
2500 const element = document.createElement('div');
2501 editor.setRootElement(element);
2503 await editor.update(() => {
2504 const root = $getRoot();
2505 const paragraph = $createParagraphNode();
2506 root.append(paragraph);
2507 const linkNode = $createLinkNode('https://lexical.dev');
2508 const linkTextNode = $createTextNode('Lexical');
2509 linkNode.append(linkTextNode);
2510 const textNode2 = $createTextNode('...');
2511 $insertNodes([linkNode, textNode2]);
2513 expect(element.innerHTML).toBe(
2514 '<p><a href="https://lexical.dev" dir="ltr"><span data-lexical-text="true">Lexical</span></a><span data-lexical-text="true">...</span></p>',
2518 test('Can insert an ElementNode after ShadowRoot', async () => {
2519 const editor = createTestEditor();
2520 const element = document.createElement('div');
2521 editor.setRootElement(element);
2523 await editor.update(() => {
2524 const root = $getRoot();
2525 const paragraph = $createParagraphNode();
2526 root.append(paragraph);
2527 paragraph.selectStart();
2528 const element1 = $createTestShadowRootNode();
2529 const element2 = $createTestElementNode();
2530 $insertNodes([element1, element2]);
2533 '<div><br></div><div><br></div>',
2534 '<div><br></div><p><br></p>',
2535 ]).toContain(element.innerHTML);
2540 describe('extract', () => {
2541 test('Should return the selected node when collapsed on a TextNode', async () => {
2542 const editor = createTestEditor();
2544 const element = document.createElement('div');
2546 editor.setRootElement(element);
2548 await editor.update(() => {
2549 const root = $getRoot();
2551 const paragraph = $createParagraphNode();
2552 const text = $createTextNode('Existing text...');
2554 paragraph.append(text);
2555 root.append(paragraph);
2569 const selection = $getSelection();
2570 expect($isRangeSelection(selection)).toBeTruthy();
2572 expect(selection!.extract()).toEqual([text]);
2577 describe('insertNodes', () => {
2579 jest.clearAllMocks();
2582 it('can insert element next to top level decorator node', async () => {
2583 const editor = createTestEditor();
2584 const element = document.createElement('div');
2585 editor.setRootElement(element);
2587 jest.spyOn(TestDecoratorNode.prototype, 'isInline').mockReturnValue(false);
2589 await editor.update(() => {
2591 $createParagraphNode(),
2592 $createTestDecoratorNode(),
2593 $createParagraphNode().append($createTextNode('Text after')),
2597 await editor.update(() => {
2598 const selectionNode = $getRoot().getFirstChild();
2599 invariant($isElementNode(selectionNode));
2600 const selection = selectionNode.select();
2601 selection.insertNodes([
2602 $createParagraphNode().append($createTextNode('Text before')),
2606 expect(element.innerHTML).toBe(
2607 '<p dir="ltr"><span data-lexical-text="true">Text before</span></p>' +
2608 '<span data-lexical-decorator="true" contenteditable="false"></span>' +
2609 '<p dir="ltr"><span data-lexical-text="true">Text after</span></p>',
2613 it('can insert when previous selection was null', async () => {
2614 const editor = createTestHeadlessEditor();
2615 await editor.update(() => {
2616 const selection = $createRangeSelection();
2617 selection.anchor.set('root', 0, 'element');
2618 selection.focus.set('root', 0, 'element');
2620 selection.insertNodes([
2621 $createParagraphNode().append($createTextNode('Text')),
2624 expect($getRoot().getTextContent()).toBe('Text');
2626 $setSelection(null);
2628 await editor.update(() => {
2629 const selection = $createRangeSelection();
2630 const text = $getRoot().getLastDescendant()!;
2631 selection.anchor.set(text.getKey(), 0, 'text');
2632 selection.focus.set(text.getKey(), 0, 'text');
2634 selection.insertNodes([
2635 $createParagraphNode().append($createTextNode('Before ')),
2638 expect($getRoot().getTextContent()).toBe('Before Text');
2642 it('can insert when before empty text node', async () => {
2643 const editor = createTestEditor();
2644 const element = document.createElement('div');
2645 editor.setRootElement(element);
2647 await editor.update(() => {
2648 // Empty text node to test empty text split
2649 const emptyTextNode = $createTextNode('');
2651 $createParagraphNode().append(emptyTextNode, $createTextNode('text')),
2653 emptyTextNode.select(0, 0);
2654 const selection = $getSelection()!;
2655 expect($isRangeSelection(selection)).toBeTruthy();
2656 selection.insertNodes([$createTextNode('foo')]);
2658 expect($getRoot().getTextContent()).toBe('footext');
2662 it('last node is LineBreakNode', async () => {
2663 const editor = createTestEditor();
2664 const element = document.createElement('div');
2665 editor.setRootElement(element);
2667 await editor.update(() => {
2668 // Empty text node to test empty text split
2669 const paragraph = $createParagraphNode();
2670 $getRoot().append(paragraph);
2671 const selection = paragraph.select();
2672 expect($isRangeSelection(selection)).toBeTruthy();
2674 const newHeading = $createHeadingNode('h1').append(
2675 $createTextNode('heading'),
2677 selection.insertNodes([newHeading, $createLineBreakNode()]);
2679 editor.getEditorState().read(() => {
2680 expect(element.innerHTML).toBe(
2681 '<h1 dir="ltr"><span data-lexical-text="true">heading</span></h1><p><br></p>',
2683 const selectedNode = ($getSelection() as RangeSelection).anchor.getNode();
2684 expect($isParagraphNode(selectedNode)).toBeTruthy();
2685 expect($isHeadingNode(selectedNode.getPreviousSibling())).toBeTruthy();
2690 describe('$patchStyleText', () => {
2691 test('can patch a selection anchored to the end of a TextNode before an inline element', async () => {
2692 const editor = createTestEditor();
2693 const element = document.createElement('div');
2694 editor.setRootElement(element);
2696 await editor.update(() => {
2697 const root = $getRoot();
2699 const paragraph = $createParagraphWithNodes(editor, [
2712 root.append(paragraph);
2714 const link = $createLinkNode('https://');
2715 link.append($createTextNode('link'));
2717 const a = $getNodeByKey('a')!;
2718 a.insertAfter(link);
2731 const selection = $getSelection();
2732 if (!$isRangeSelection(selection)) {
2735 $patchStyleText(selection, {'text-emphasis': 'filled'});
2738 expect(element.innerHTML).toBe(
2739 '<p dir="ltr"><span data-lexical-text="true">a</span>' +
2740 '<a href="https://" dir="ltr">' +
2741 '<span style="text-emphasis: filled;" data-lexical-text="true">link</span>' +
2743 '<span style="text-emphasis: filled;" data-lexical-text="true">b</span></p>',
2747 test('can patch a selection anchored to the end of a TextNode at the end of a paragraph', async () => {
2748 const editor = createTestEditor();
2749 const element = document.createElement('div');
2750 editor.setRootElement(element);
2752 await editor.update(() => {
2753 const root = $getRoot();
2755 const paragraph1 = $createParagraphWithNodes(editor, [
2762 const paragraph2 = $createParagraphWithNodes(editor, [
2770 root.append(paragraph1);
2771 root.append(paragraph2);
2784 const selection = $getSelection();
2785 if (!$isRangeSelection(selection)) {
2788 $patchStyleText(selection, {'text-emphasis': 'filled'});
2791 expect(element.innerHTML).toBe(
2792 '<p dir="ltr"><span data-lexical-text="true">a</span></p>' +
2793 '<p dir="ltr"><span style="text-emphasis: filled;" data-lexical-text="true">b</span></p>',
2797 test('can patch a selection that ends on an element', async () => {
2798 const editor = createTestEditor();
2799 const element = document.createElement('div');
2800 editor.setRootElement(element);
2802 await editor.update(() => {
2803 const root = $getRoot();
2805 const paragraph = $createParagraphWithNodes(editor, [
2813 root.append(paragraph);
2815 const link = $createLinkNode('https://');
2816 link.append($createTextNode('link'));
2818 const a = $getNodeByKey('a')!;
2819 a.insertAfter(link);
2826 // Select to end of the link _element_
2833 const selection = $getSelection();
2834 if (!$isRangeSelection(selection)) {
2837 $patchStyleText(selection, {'text-emphasis': 'filled'});
2840 expect(element.innerHTML).toBe(
2842 '<span style="text-emphasis: filled;" data-lexical-text="true">a</span>' +
2843 '<a href="https://" dir="ltr">' +
2844 '<span style="text-emphasis: filled;" data-lexical-text="true">link</span>' +
2850 test('can patch a reversed selection that ends on an element', async () => {
2851 const editor = createTestEditor();
2852 const element = document.createElement('div');
2853 editor.setRootElement(element);
2855 await editor.update(() => {
2856 const root = $getRoot();
2858 const paragraph = $createParagraphWithNodes(editor, [
2866 root.append(paragraph);
2868 const link = $createLinkNode('https://');
2869 link.append($createTextNode('link'));
2871 const a = $getNodeByKey('a')!;
2872 a.insertAfter(link);
2874 // Select from the end of the link _element_
2886 const selection = $getSelection();
2887 if (!$isRangeSelection(selection)) {
2890 $patchStyleText(selection, {'text-emphasis': 'filled'});
2893 expect(element.innerHTML).toBe(
2895 '<span style="text-emphasis: filled;" data-lexical-text="true">a</span>' +
2896 '<a href="https://" dir="ltr">' +
2897 '<span style="text-emphasis: filled;" data-lexical-text="true">link</span>' +
2903 test('can patch a selection that starts and ends on an element', async () => {
2904 const editor = createTestEditor();
2905 const element = document.createElement('div');
2906 editor.setRootElement(element);
2908 await editor.update(() => {
2909 const root = $getRoot();
2911 const paragraph = $createParagraphNode();
2912 root.append(paragraph);
2914 const link = $createLinkNode('https://');
2915 link.append($createTextNode('link'));
2916 paragraph.append(link);
2929 const selection = $getSelection();
2930 if (!$isRangeSelection(selection)) {
2933 $patchStyleText(selection, {'text-emphasis': 'filled'});
2936 expect(element.innerHTML).toBe(
2938 '<a href="https://" dir="ltr">' +
2939 '<span style="text-emphasis: filled;" data-lexical-text="true">link</span>' +
2945 test('can clear a style', async () => {
2946 const editor = createTestEditor();
2947 const element = document.createElement('div');
2948 editor.setRootElement(element);
2950 await editor.update(() => {
2951 const root = $getRoot();
2953 const paragraph = $createParagraphNode();
2954 root.append(paragraph);
2956 const text = $createTextNode('text');
2957 paragraph.append(text);
2966 offset: text.getTextContentSize(),
2970 const selection = $getSelection();
2971 if (!$isRangeSelection(selection)) {
2974 $patchStyleText(selection, {'text-emphasis': 'filled'});
2975 $patchStyleText(selection, {'text-emphasis': null});
2978 expect(element.innerHTML).toBe(
2979 '<p dir="ltr"><span data-lexical-text="true">text</span></p>',
2983 test('can toggle a style on a collapsed selection', async () => {
2984 const editor = createTestEditor();
2985 const element = document.createElement('div');
2986 editor.setRootElement(element);
2988 await editor.update(() => {
2989 const root = $getRoot();
2991 const paragraph = $createParagraphNode();
2992 root.append(paragraph);
2994 const text = $createTextNode('text');
2995 paragraph.append(text);
3008 const selection = $getSelection();
3009 if (!$isRangeSelection(selection)) {
3012 $patchStyleText(selection, {'text-emphasis': 'filled'});
3015 $getSelectionStyleValueForProperty(selection, 'text-emphasis', ''),
3016 ).toEqual('filled');
3018 $patchStyleText(selection, {'text-emphasis': null});
3021 $getSelectionStyleValueForProperty(selection, 'text-emphasis', ''),
3024 $patchStyleText(selection, {'text-emphasis': 'filled'});
3027 $getSelectionStyleValueForProperty(selection, 'text-emphasis', ''),
3028 ).toEqual('filled');
3032 test('updates cached styles when setting on a collapsed selection', async () => {
3033 const editor = createTestEditor();
3034 const element = document.createElement('div');
3035 editor.setRootElement(element);
3037 await editor.update(() => {
3038 const root = $getRoot();
3040 const paragraph = $createParagraphNode();
3041 root.append(paragraph);
3043 const text = $createTextNode('text');
3044 paragraph.append(text);
3057 // First fetch the initial style -- this will cause the CSS cache to be
3058 // populated with an empty string pointing to an empty style object.
3059 const selection = $getSelection();
3060 if (!$isRangeSelection(selection)) {
3063 $getSelectionStyleValueForProperty(selection, 'color', '');
3065 // Now when we set the style, we should _not_ touch the previously created
3066 // empty style object, but create a new one instead.
3067 $patchStyleText(selection, {color: 'red'});
3069 // We can check that result by clearing the style and re-querying it.
3070 ($getSelection() as RangeSelection).setStyle('');
3072 const color = $getSelectionStyleValueForProperty(
3073 $getSelection() as RangeSelection,
3077 expect(color).toEqual('');
3081 test.each<TextModeType>(['token', 'segmented'])(
3082 'can update style of text node that is in %s mode',
3084 const editor = createTestEditor();
3086 const element = document.createElement('div');
3087 editor.setRootElement(element);
3089 await editor.update(() => {
3090 const root = $getRoot();
3092 const paragraph = $createParagraphNode();
3093 root.append(paragraph);
3095 const text = $createTextNode('first').setFormat('bold');
3096 paragraph.append(text);
3098 const textInMode = $createTextNode('second').setMode(mode);
3099 paragraph.append(textInMode);
3103 offset: 'fir'.length,
3108 key: textInMode.getKey(),
3109 offset: 'sec'.length,
3113 const selection = $getSelection();
3114 $patchStyleText(selection!, {'font-size': '15px'});
3117 expect(element.innerHTML).toBe(
3119 '<strong data-lexical-text="true">fir</strong>' +
3120 '<strong style="font-size: 15px;" data-lexical-text="true">st</strong>' +
3121 '<span style="font-size: 15px;" data-lexical-text="true">second</span>' +
3127 test('preserve backward selection when changing style of 2 different text nodes', async () => {
3128 const editor = createTestEditor();
3130 const element = document.createElement('div');
3132 editor.setRootElement(element);
3134 editor.update(() => {
3135 const root = $getRoot();
3137 const paragraph = $createParagraphNode();
3138 root.append(paragraph);
3140 const firstText = $createTextNode('first ').setFormat('bold');
3141 paragraph.append(firstText);
3143 const secondText = $createTextNode('second').setFormat('italic');
3144 paragraph.append(secondText);
3147 key: secondText.getKey(),
3148 offset: 'sec'.length,
3153 key: firstText.getKey(),
3154 offset: 'fir'.length,
3158 const selection = $getSelection();
3160 $patchStyleText(selection!, {'font-size': '11px'});
3162 const [newAnchor, newFocus] = selection!.getStartEndPoints()!;
3164 const newAnchorNode: LexicalNode = newAnchor.getNode();
3165 expect(newAnchorNode.getTextContent()).toBe('sec');
3166 expect(newAnchor.offset).toBe('sec'.length);
3168 const newFocusNode: LexicalNode = newFocus.getNode();
3169 expect(newFocusNode.getTextContent()).toBe('st ');
3170 expect(newFocus.offset).toBe(0);