UNPKG

@terrible-lexical/selection

Version:

This package contains utilities and helpers for handling Lexical selection.

2,113 lines (1,734 loc) 75.4 kB
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import {$createLinkNode} from '@terrible-lexical/link/src'; import {$createHeadingNode} from '@terrible-lexical/rich-text/src'; import { $getSelectionStyleValueForProperty, $patchStyleText, } from '@terrible-lexical/selection/src'; import { $createParagraphNode, $createTextNode, $getNodeByKey, $getRoot, $getSelection, $insertNodes, $isNodeSelection, $isRangeSelection, RangeSelection, TextNode, } from 'terrible-lexical'; import { $createTestDecoratorNode, $createTestElementNode, $createTestShadowRootNode, createTestEditor, TestDecoratorNode, } from 'lexical/src//__tests__/utils'; import {setAnchorPoint, setFocusPoint} from '../utils'; Range.prototype.getBoundingClientRect = function (): DOMRect { const rect = { bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0, }; return { ...rect, toJSON() { return rect; }, }; }; function createParagraphWithNodes(editor, nodes) { const paragraph = $createParagraphNode(); const nodeMap = editor._pendingEditorState._nodeMap; for (let i = 0; i < nodes.length; i++) { const {text, key, mergeable} = nodes[i]; const textNode = new TextNode(text, key); nodeMap.set(key, textNode); if (!mergeable) { textNode.toggleUnmergeable(); } paragraph.append(textNode); } return paragraph; } describe('LexicalSelectionHelpers tests', () => { describe('Collapsed', () => { test('Can handle a text point', () => { const setupTestCase = (cb) => { const editor = createTestEditor(); editor.update(() => { const root = $getRoot(); const element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: false, text: 'a', }, { key: 'b', mergeable: false, text: 'b', }, { key: 'c', mergeable: false, text: 'c', }, ]); root.append(element); setAnchorPoint({ key: 'a', offset: 0, type: 'text', }); setFocusPoint({ key: 'a', offset: 0, type: 'text', }); const selection = $getSelection(); cb(selection, element); }); }; // getNodes setupTestCase((selection, state) => { expect(selection.getNodes()).toEqual([$getNodeByKey('a')]); }); // getTextContent setupTestCase((selection) => { expect(selection.getTextContent()).toEqual(''); }); // insertText setupTestCase((selection, state) => { selection.insertText('Test'); expect($getNodeByKey('a').getTextContent()).toBe('Testa'); expect(selection.anchor).toEqual( expect.objectContaining({ key: 'a', offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: 'a', offset: 4, type: 'text', }), ); }); // insertNodes setupTestCase((selection, element) => { selection.insertNodes([$createTextNode('foo')]); expect(selection.anchor).toEqual( expect.objectContaining({ key: element.getFirstChild().getKey(), offset: 3, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: element.getFirstChild().getKey(), offset: 3, type: 'text', }), ); }); // insertParagraph setupTestCase((selection) => { selection.insertParagraph(); expect(selection.anchor).toEqual( expect.objectContaining({ key: 'a', offset: 0, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: 'a', offset: 0, type: 'text', }), ); }); // insertLineBreak setupTestCase((selection, element) => { selection.insertLineBreak(true); expect(selection.anchor).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); }); // Format text setupTestCase((selection, element) => { selection.formatText('bold'); selection.insertText('Test'); expect(element.getFirstChild().getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: element.getFirstChild().getKey(), offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: element.getFirstChild().getKey(), offset: 4, type: 'text', }), ); expect(element.getFirstChild().getNextSibling().getTextContent()).toBe( 'a', ); }); // Extract selection setupTestCase((selection, state) => { expect(selection.extract()).toEqual([$getNodeByKey('a')]); }); }); test('Has correct text point after removal after merge', async () => { const editor = createTestEditor(); const domElement = document.createElement('div'); let element; editor.setRootElement(domElement); editor.update(() => { const root = $getRoot(); element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: true, text: 'a', }, { key: 'bb', mergeable: true, text: 'bb', }, { key: 'empty', mergeable: true, text: '', }, { key: 'cc', mergeable: true, text: 'cc', }, { key: 'd', mergeable: true, text: 'd', }, ]); root.append(element); setAnchorPoint({ key: 'bb', offset: 1, type: 'text', }); setFocusPoint({ key: 'cc', offset: 1, type: 'text', }); }); await Promise.resolve().then(); editor.getEditorState().read(() => { const selection = $getSelection(); if ($isNodeSelection(selection)) { return; } expect(selection.anchor).toEqual( expect.objectContaining({ key: 'a', offset: 2, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: 'a', offset: 4, type: 'text', }), ); }); }); test('Has correct text point after removal after merge (2)', async () => { const editor = createTestEditor(); const domElement = document.createElement('div'); let element; editor.setRootElement(domElement); editor.update(() => { const root = $getRoot(); element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: true, text: 'a', }, { key: 'empty', mergeable: true, text: '', }, { key: 'b', mergeable: true, text: 'b', }, { key: 'c', mergeable: true, text: 'c', }, ]); root.append(element); setAnchorPoint({ key: 'a', offset: 0, type: 'text', }); setFocusPoint({ key: 'c', offset: 1, type: 'text', }); }); await Promise.resolve().then(); editor.getEditorState().read(() => { const selection = $getSelection(); if ($isNodeSelection(selection)) { return; } if ($isNodeSelection(selection)) { return; } expect(selection.anchor).toEqual( expect.objectContaining({ key: 'a', offset: 0, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: 'a', offset: 3, type: 'text', }), ); }); }); test('Has correct text point adjust to element point after removal of a single empty text node', async () => { const editor = createTestEditor(); const domElement = document.createElement('div'); let element; editor.setRootElement(domElement); editor.update(() => { const root = $getRoot(); element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: true, text: '', }, ]); root.append(element); setAnchorPoint({ key: 'a', offset: 0, type: 'text', }); setFocusPoint({ key: 'a', offset: 0, type: 'text', }); }); await Promise.resolve().then(); editor.getEditorState().read(() => { const selection = $getSelection(); if ($isNodeSelection(selection)) { return; } if ($isNodeSelection(selection)) { return; } expect(selection.anchor).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); }); }); test('Has correct element point after removal of an empty text node in a group #1', async () => { const editor = createTestEditor(); const domElement = document.createElement('div'); let element; editor.setRootElement(domElement); editor.update(() => { const root = $getRoot(); element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: true, text: '', }, { key: 'b', mergeable: false, text: 'b', }, ]); root.append(element); setAnchorPoint({ key: element.getKey(), offset: 2, type: 'element', }); setFocusPoint({ key: element.getKey(), offset: 2, type: 'element', }); }); await Promise.resolve().then(); editor.getEditorState().read(() => { const selection = $getSelection(); if ($isNodeSelection(selection)) { return; } expect(selection.anchor).toEqual( expect.objectContaining({ key: 'b', offset: 1, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: 'b', offset: 1, type: 'text', }), ); }); }); test('Has correct element point after removal of an empty text node in a group #2', async () => { const editor = createTestEditor(); const domElement = document.createElement('div'); let element; editor.setRootElement(domElement); editor.update(() => { const root = $getRoot(); element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: true, text: '', }, { key: 'b', mergeable: false, text: 'b', }, { key: 'c', mergeable: true, text: 'c', }, { key: 'd', mergeable: true, text: 'd', }, ]); root.append(element); setAnchorPoint({ key: element.getKey(), offset: 4, type: 'element', }); setFocusPoint({ key: element.getKey(), offset: 4, type: 'element', }); }); await Promise.resolve().then(); editor.getEditorState().read(() => { const selection = $getSelection(); if ($isNodeSelection(selection)) { return; } expect(selection.anchor).toEqual( expect.objectContaining({ key: 'c', offset: 2, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: 'c', offset: 2, type: 'text', }), ); }); }); test('Has correct text point after removal of an empty text node in a group #3', async () => { const editor = createTestEditor(); const domElement = document.createElement('div'); let element; editor.setRootElement(domElement); editor.update(() => { const root = $getRoot(); element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: true, text: '', }, { key: 'b', mergeable: false, text: 'b', }, { key: 'c', mergeable: true, text: 'c', }, { key: 'd', mergeable: true, text: 'd', }, ]); root.append(element); setAnchorPoint({ key: 'd', offset: 1, type: 'text', }); setFocusPoint({ key: 'd', offset: 1, type: 'text', }); }); await Promise.resolve().then(); editor.getEditorState().read(() => { const selection = $getSelection(); if ($isNodeSelection(selection)) { return; } expect(selection.anchor).toEqual( expect.objectContaining({ key: 'c', offset: 2, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: 'c', offset: 2, type: 'text', }), ); }); }); test('Can handle an element point on empty element', () => { const setupTestCase = (cb) => { const editor = createTestEditor(); editor.update(() => { const root = $getRoot(); const element = createParagraphWithNodes(editor, []); root.append(element); setAnchorPoint({ key: element.getKey(), offset: 0, type: 'element', }); setFocusPoint({ key: element.getKey(), offset: 0, type: 'element', }); const selection = $getSelection(); cb(selection, element); }); }; // getNodes setupTestCase((selection, element) => { expect(selection.getNodes()).toEqual([element]); }); // getTextContent setupTestCase((selection) => { expect(selection.getTextContent()).toEqual(''); }); // insertText setupTestCase((selection, element) => { selection.insertText('Test'); const firstChild = element.getFirstChild(); expect(firstChild.getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); }); // insertParagraph setupTestCase((selection, element) => { selection.insertParagraph(); const nextElement = element.getNextSibling(); expect(selection.anchor).toEqual( expect.objectContaining({ key: nextElement.getKey(), offset: 0, type: 'element', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: nextElement.getKey(), offset: 0, type: 'element', }), ); }); // insertLineBreak setupTestCase((selection, element) => { selection.insertLineBreak(true); expect(selection.anchor).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); }); // Format text setupTestCase((selection, element) => { selection.formatText('bold'); selection.insertText('Test'); const firstChild = element.getFirstChild(); expect(firstChild.getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); }); // Extract selection setupTestCase((selection, element) => { expect(selection.extract()).toEqual([element]); }); }); test('Can handle a start element point', () => { const setupTestCase = (cb) => { const editor = createTestEditor(); editor.update(() => { const root = $getRoot(); const element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: false, text: 'a', }, { key: 'b', mergeable: false, text: 'b', }, { key: 'c', mergeable: false, text: 'c', }, ]); root.append(element); setAnchorPoint({ key: element.getKey(), offset: 0, type: 'element', }); setFocusPoint({ key: element.getKey(), offset: 0, type: 'element', }); const selection = $getSelection(); cb(selection, element); }); }; // getNodes setupTestCase((selection, state) => { expect(selection.getNodes()).toEqual([$getNodeByKey('a')]); }); // getTextContent setupTestCase((selection) => { expect(selection.getTextContent()).toEqual(''); }); // insertText setupTestCase((selection, element) => { selection.insertText('Test'); const firstChild = element.getFirstChild(); expect(firstChild.getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); }); // insertParagraph setupTestCase((selection, element) => { selection.insertParagraph(); const firstChild = element; expect(selection.anchor).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 0, type: 'element', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 0, type: 'element', }), ); }); // insertLineBreak setupTestCase((selection, element) => { selection.insertLineBreak(true); expect(selection.anchor).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); }); // Format text setupTestCase((selection, element) => { selection.formatText('bold'); selection.insertText('Test'); const firstChild = element.getFirstChild(); expect(firstChild.getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); }); // Extract selection setupTestCase((selection, element) => { expect(selection.extract()).toEqual([$getNodeByKey('a')]); }); }); test('Can handle an end element point', () => { const setupTestCase = (cb) => { const editor = createTestEditor(); editor.update(() => { const root = $getRoot(); const element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: false, text: 'a', }, { key: 'b', mergeable: false, text: 'b', }, { key: 'c', mergeable: false, text: 'c', }, ]); root.append(element); setAnchorPoint({ key: element.getKey(), offset: 3, type: 'element', }); setFocusPoint({ key: element.getKey(), offset: 3, type: 'element', }); const selection = $getSelection(); cb(selection, element); }); }; // getNodes setupTestCase((selection, state) => { expect(selection.getNodes()).toEqual([$getNodeByKey('c')]); }); // getTextContent setupTestCase((selection) => { expect(selection.getTextContent()).toEqual(''); }); // insertText setupTestCase((selection, element) => { selection.insertText('Test'); const lastChild = element.getLastChild(); expect(lastChild.getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: lastChild.getKey(), offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: lastChild.getKey(), offset: 4, type: 'text', }), ); }); // insertParagraph setupTestCase((selection, element) => { selection.insertParagraph(); const nextSibling = element.getNextSibling(); expect(selection.anchor).toEqual( expect.objectContaining({ key: nextSibling.getKey(), offset: 0, type: 'element', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: nextSibling.getKey(), offset: 0, type: 'element', }), ); }); // insertLineBreak setupTestCase((selection, element) => { selection.insertLineBreak(true); const thirdChild = $getNodeByKey('c'); expect(selection.anchor).toEqual( expect.objectContaining({ key: thirdChild.getKey(), offset: 1, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: thirdChild.getKey(), offset: 1, type: 'text', }), ); }); // Format text setupTestCase((selection, element) => { selection.formatText('bold'); selection.insertText('Test'); const lastChild = element.getLastChild(); expect(lastChild.getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: lastChild.getKey(), offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: lastChild.getKey(), offset: 4, type: 'text', }), ); }); // Extract selection setupTestCase((selection, element) => { expect(selection.extract()).toEqual([$getNodeByKey('c')]); }); }); test('Has correct element point after merge from middle', async () => { const editor = createTestEditor(); const domElement = document.createElement('div'); let element; editor.setRootElement(domElement); editor.update(() => { const root = $getRoot(); element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: true, text: 'a', }, { key: 'b', mergeable: true, text: 'b', }, { key: 'c', mergeable: true, text: 'c', }, ]); root.append(element); setAnchorPoint({ key: element.getKey(), offset: 2, type: 'element', }); setFocusPoint({ key: element.getKey(), offset: 2, type: 'element', }); }); await Promise.resolve().then(); editor.getEditorState().read(() => { const selection = $getSelection(); if ($isNodeSelection(selection)) { return; } expect(selection.anchor).toEqual( expect.objectContaining({ key: 'a', offset: 2, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: 'a', offset: 2, type: 'text', }), ); }); }); test('Has correct element point after merge from end', async () => { const editor = createTestEditor(); const domElement = document.createElement('div'); let element; editor.setRootElement(domElement); editor.update(() => { const root = $getRoot(); element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: true, text: 'a', }, { key: 'b', mergeable: true, text: 'b', }, { key: 'c', mergeable: true, text: 'c', }, ]); root.append(element); setAnchorPoint({ key: element.getKey(), offset: 3, type: 'element', }); setFocusPoint({ key: element.getKey(), offset: 3, type: 'element', }); }); await Promise.resolve().then(); editor.getEditorState().read(() => { const selection = $getSelection(); if ($isNodeSelection(selection)) { return; } expect(selection.anchor).toEqual( expect.objectContaining({ key: 'a', offset: 3, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: 'a', offset: 3, type: 'text', }), ); }); }); }); describe('Simple range', () => { test('Can handle multiple text points', () => { const setupTestCase = (cb) => { const editor = createTestEditor(); editor.update(() => { const root = $getRoot(); const element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: false, text: 'a', }, { key: 'b', mergeable: false, text: 'b', }, { key: 'c', mergeable: false, text: 'c', }, ]); root.append(element); setAnchorPoint({ key: 'a', offset: 0, type: 'text', }); setFocusPoint({ key: 'b', offset: 0, type: 'text', }); const selection = $getSelection(); cb(selection, element); }); }; // getNodes setupTestCase((selection, state) => { expect(selection.getNodes()).toEqual([ $getNodeByKey('a'), $getNodeByKey('b'), ]); }); // getTextContent setupTestCase((selection) => { expect(selection.getTextContent()).toEqual('a'); }); // insertText setupTestCase((selection, state) => { selection.insertText('Test'); expect($getNodeByKey('a').getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: 'a', offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: 'a', offset: 4, type: 'text', }), ); }); // insertNodes setupTestCase((selection, element) => { selection.insertNodes([$createTextNode('foo')]); expect(selection.anchor).toEqual( expect.objectContaining({ key: element.getFirstChild().getKey(), offset: 3, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: element.getFirstChild().getKey(), offset: 3, type: 'text', }), ); }); // insertParagraph setupTestCase((selection) => { selection.insertParagraph(); expect(selection.anchor).toEqual( expect.objectContaining({ key: 'b', offset: 0, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: 'b', offset: 0, type: 'text', }), ); }); // insertLineBreak setupTestCase((selection, element) => { selection.insertLineBreak(true); expect(selection.anchor).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); }); // Format text setupTestCase((selection, element) => { selection.formatText('bold'); selection.insertText('Test'); expect(element.getFirstChild().getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: element.getFirstChild().getKey(), offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: element.getFirstChild().getKey(), offset: 4, type: 'text', }), ); }); // Extract selection setupTestCase((selection, state) => { expect(selection.extract()).toEqual([{...$getNodeByKey('a')}]); }); }); test('Can handle multiple element points', () => { const setupTestCase = (cb) => { const editor = createTestEditor(); editor.update(() => { const root = $getRoot(); const element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: false, text: 'a', }, { key: 'b', mergeable: false, text: 'b', }, { key: 'c', mergeable: false, text: 'c', }, ]); root.append(element); setAnchorPoint({ key: element.getKey(), offset: 0, type: 'element', }); setFocusPoint({ key: element.getKey(), offset: 1, type: 'element', }); const selection = $getSelection(); cb(selection, element); }); }; // getNodes setupTestCase((selection) => { expect(selection.getNodes()).toEqual([$getNodeByKey('a')]); }); // getTextContent setupTestCase((selection) => { expect(selection.getTextContent()).toEqual('a'); }); // insertText setupTestCase((selection, element) => { selection.insertText('Test'); const firstChild = element.getFirstChild(); expect(firstChild.getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); }); // insertParagraph setupTestCase((selection, element) => { selection.insertParagraph(); const firstChild = element.getFirstChild(); expect(selection.anchor).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 0, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 0, type: 'text', }), ); }); // insertLineBreak setupTestCase((selection, element) => { selection.insertLineBreak(true); expect(selection.anchor).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); }); // Format text setupTestCase((selection, element) => { selection.formatText('bold'); selection.insertText('Test'); const firstChild = element.getFirstChild(); expect(firstChild.getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); }); // Extract selection setupTestCase((selection, element) => { const firstChild = element.getFirstChild(); expect(selection.extract()).toEqual([firstChild]); }); }); test('Can handle a mix of text and element points', () => { const setupTestCase = (cb) => { const editor = createTestEditor(); editor.update(() => { const root = $getRoot(); const element = createParagraphWithNodes(editor, [ { key: 'a', mergeable: false, text: 'a', }, { key: 'b', mergeable: false, text: 'b', }, { key: 'c', mergeable: false, text: 'c', }, ]); root.append(element); setAnchorPoint({ key: element.getKey(), offset: 0, type: 'element', }); setFocusPoint({ key: 'c', offset: 1, type: 'text', }); const selection = $getSelection(); cb(selection, element); }); }; // isBefore setupTestCase((selection, state) => { expect(selection.anchor.isBefore(selection.focus)).toEqual(true); }); // getNodes setupTestCase((selection, state) => { expect(selection.getNodes()).toEqual([ $getNodeByKey('a'), $getNodeByKey('b'), $getNodeByKey('c'), ]); }); // getTextContent setupTestCase((selection) => { expect(selection.getTextContent()).toEqual('abc'); }); // insertText setupTestCase((selection, element) => { selection.insertText('Test'); const firstChild = element.getFirstChild(); expect(firstChild.getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); }); // insertParagraph setupTestCase((selection, element) => { selection.insertParagraph(); const nextElement = element.getNextSibling(); expect(selection.anchor).toEqual( expect.objectContaining({ key: nextElement.getKey(), offset: 0, type: 'element', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: nextElement.getKey(), offset: 0, type: 'element', }), ); }); // insertLineBreak setupTestCase((selection, element) => { selection.insertLineBreak(true); expect(selection.anchor).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: element.getKey(), offset: 0, type: 'element', }), ); }); // Format text setupTestCase((selection, element) => { selection.formatText('bold'); selection.insertText('Test'); const firstChild = element.getFirstChild(); expect(firstChild.getTextContent()).toBe('Test'); expect(selection.anchor).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); expect(selection.focus).toEqual( expect.objectContaining({ key: firstChild.getKey(), offset: 4, type: 'text', }), ); }); // Extract selection setupTestCase((selection, element) => { expect(selection.extract()).toEqual([ $getNodeByKey('a'), $getNodeByKey('b'), $getNodeByKey('c'), ]); }); }); }); describe('can insert non-element nodes correctly', () => { describe('with an empty paragraph node selected', () => { test('a single text node', async () => { const editor = createTestEditor(); const element = document.createElement('div'); editor.setRootElement(element); await editor.update(() => { const root = $getRoot(); const paragraph = $createParagraphNode(); root.append(paragraph); setAnchorPoint({ key: paragraph.getKey(), offset: 0, type: 'element', }); setFocusPoint({ key: paragraph.getKey(), offset: 0, type: 'element', }); const selection = $getSelection(); if (!$isRangeSelection(selection)) { return; } selection.insertNodes([$createTextNode('foo')]); }); expect(element.innerHTML).toBe( '<p dir="ltr"><span data-lexical-text="true">foo</span></p>', ); }); test('two text nodes', async () => { const editor = createTestEditor(); const element = document.createElement('div'); editor.setRootElement(element); await editor.update(() => { const root = $getRoot(); const paragraph = $createParagraphNode(); root.append(paragraph); setAnchorPoint({ key: paragraph.getKey(), offset: 0, type: 'element', }); setFocusPoint({ key: paragraph.getKey(), offset: 0, type: 'element', }); const selection = $getSelection(); if (!$isRangeSelection(selection)) { return; } selection.insertNodes([ $createTextNode('foo'), $createTextNode('bar'), ]); }); expect(element.innerHTML).toBe( '<p dir="ltr"><span data-lexical-text="true">foobar</span></p>', ); }); test('link insertion without parent element', async () => { const editor = createTestEditor(); const element = document.createElement('div'); editor.setRootElement(element); await editor.update(() => { const root = $getRoot(); const paragraph = $createParagraphNode(); root.append(paragraph); setAnchorPoint({ key: paragraph.getKey(), offset: 0, type: 'element', }); setFocusPoint({ key: paragraph.getKey(), offset: 0, type: 'element', }); const link = $createLinkNode('https://'); link.append($createTextNode('ello worl')); const selection = $getSelection(); if (!$isRangeSelection(selection)) { return; } selection.insertNodes([ $createTextNode('h'), link, $createTextNode('d'), ]); }); expect(element.innerHTML).toBe( '<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>', ); }); test('a single heading node with a child text node', async () => { const editor = createTestEditor(); const element = document.createElement('div'); editor.setRootElement(element); await editor.update(() => { const root = $getRoot(); const paragraph = $createParagraphNode(); root.append(paragraph); setAnchorPoint({ key: paragraph.getKey(), offset: 0, type: 'element', }); setFocusPoint({ key: paragraph.getKey(), offset: 0, type: 'element', }); const heading = $createHeadingNode('h1'); const child = $createTextNode('foo'); heading.append(child); const selection = $getSelection(); if (!$isRangeSelection(selection)) { return; } selection.insertNodes([heading]); }); expect(element.innerHTML).toBe( '<h1 dir="ltr"><span data-lexical-text="true">foo</span></h1>', ); }); test('a heading node with a child text node and a disjoint sibling text node should throw', async () => { const editor = createTestEditor(); const element = document.createElement('div'); editor.setRootElement(element); await editor.update(() => { const root = $getRoot(); const paragraph = $createParagraphNode(); root.append(paragraph); setAnchorPoint({ key: paragraph.getKey(), offset: 0, type: 'element', }); setFocusPoint({ key: paragraph.getKey(), offset: 0, type: 'element', }); const heading = $createHeadingNode('h1'); const child = $createTextNode('foo'); heading.append(child); const selection = $getSelection(); if (!$isRangeSelection(selection)) { return; } expect(() => { selection.insertNodes([heading, $createTextNode('bar')]); }).toThrow(); }); expect(element.innerHTML).toBe( '<h1 dir="ltr"><span data-lexical-text="true">foo</span></h1>', ); }); }); describe('with a paragraph node selected on some existing text', () => { test('a single text node', async () => { const editor = createTestEditor(); const element = document.createElement('div'); editor.setRootElement(element); await editor.update(() => { const root = $getRoot(); const paragraph = $createParagraphNode(); const text = $createTextNode('Existing text...'); paragraph.append(text); root.append(paragraph); setAnchorPoint({ key: text.getKey(), offset: 16, type: 'text', }); setFocusPoint({ key: text.getKey(), offset: 16, type: 'text', }); const selection = $getSelection(); if (!$isRangeSelection(selection)) { return; } selection.insertNodes([$createTextNode('foo')]); }); expect(element.innerHTML).toBe( '<p dir="ltr"><span data-lexical-text="true">Existing text...foo</span></p>', ); }); test('two text nodes', async () => { const editor = createTestEditor(); const element = document.createElement('div'); editor.setRootElement(element); await editor.update(() => { const root = $getRoot(); const paragraph = $createParagraphNode(); const text = $createTextNode('Existing text...'); paragraph.append(text); root.append(paragraph); setAnchorPoint({ key: text.getKey(), offset: 16, type: 'text', }); setFocusPoint({ key: text.getKey(), offset: 16, type: 'text', }); const selection = $getSelection(); if (!$isRangeSelection(selection)) { return; } selection.insertNodes([ $createTextNode('foo'), $createTextNode('bar'), ]); }); expect(element.innerHTML).toBe( '<p dir="ltr"><span data-lexical-text="true">Existing text...foobar</span></p>', ); }); test('a single heading node with a child text node', async () => { const editor = createTestEditor(); const element = document.createElement('div'); editor.setRootElement(element); await editor.update(() => { const root = $getRoot(); const paragraph = $createParagraphNode(); const text = $createTextNode('Existing text...'); paragraph.append(text); root.append(paragraph); setAnchorPoint({ key: text.getKey(), offset: 16, type: 'text', }); setFocusPoint({ key: text.getKey(), offset: 16, type: 'text', }); const heading = $createHeadingNode('h1'); const child = $createTextNode('foo'); heading.append(child); const selection = $getSelection(); if (!$isRangeSelection(selection)) { return; } selection.insertNodes([heading]); }); expect(element.innerHTML).to