Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed textsmushing in generateEditorState() #1404

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions packages/koenig-lexical/src/utils/generateEditorState.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,33 @@ import {$createParagraphNode, $setSelection} from 'lexical';
import {$generateNodesFromDOM} from '@lexical/html';
import {$getRoot, $insertNodes} from 'lexical';

// exported for testing
export function _$generateNodesFromHTML(editor, html) {
const parser = new DOMParser();
const dom = parser.parseFromString(html, 'text/html');
const nodes = $generateNodesFromDOM(editor, dom);
return nodes;
}

export default function generateEditorState({editor, initialHtml}) {
if (initialHtml) {
// convert html in `text` to Lexical nodes and populate the editor
editor.update(() => {
const parser = new DOMParser();
const dom = parser.parseFromString(initialHtml, 'text/html');
const nodes = $generateNodesFromDOM(editor, dom);

// There are few recent issues related to $generateNodesFromDOM
// https://github.com/facebook/lexical/issues/2807
// https://github.com/facebook/lexical/issues/3677
// As a temporary fix, checking node content to remove additional spaces and br
const filteredNodes = nodes.filter(n => n.getTextContent().trim());
const nodes = _$generateNodesFromHTML(editor, initialHtml);

// Select the root
$getRoot().select();
// Clear existing content (we initialize an editor with an empty p node so it is focusable if there's no content)
$getRoot().clear();

// Insert them at a selection.
$insertNodes(filteredNodes);
$insertNodes(nodes);

// $insertNodes is focusing an editor (https://github.com/facebook/lexical/issues/4546)
// This behaviour can break the ability to autofocus the editor because
// initial state filling can happen after the component is already mounted.
// Reset selection to make it easier to manage editor focus in components instead of editor state generation
if (filteredNodes.length) {
if (nodes.length) {
$setSelection(null);
}
}, {discrete: true, tag: 'history-merge'}); // use history merge to prevent undo clearing the initial state
Expand Down
155 changes: 155 additions & 0 deletions packages/koenig-lexical/test/unit/utils/generateEditorState.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import generateEditorState, {_$generateNodesFromHTML} from '../../../src/utils/generateEditorState';
import {DEFAULT_NODES} from '../../../src';
import {createEditor} from 'lexical';
import {describe, expect, test} from 'vitest';

describe('Utils: generateEditorState', () => {
function runGenerateEditorState(html, {nodes = DEFAULT_NODES} = {}) {
const editor = createEditor({
// lexical swallows errors inside updates by default,
// so we need to throw them to fail the test
onError: (error) => {
throw error;
},
nodes
});
const editorState = generateEditorState({editor, initialHtml: html});
return editorState.toJSON();
}

test('can generate editor state from basic paragraph', function () {
const html = '<p>Test</p>';
const editorState = runGenerateEditorState(html);

expect(editorState.root.children.length).toEqual(1);
expect(editorState.root.children[0].type).toEqual('paragraph');
expect(editorState.root.children[0].children[0].text).toEqual('Test');
});

test('handles whitespace between paragraphs', function () {
const html = '<p>Test</p> <p>Test2</p>';
const editorState = runGenerateEditorState(html);

expect(editorState.root.children.length).toEqual(2);
expect(editorState.root.children[0].type).toEqual('paragraph');
expect(editorState.root.children[0].children[0].text).toEqual('Test');
expect(editorState.root.children[1].type).toEqual('paragraph');
expect(editorState.root.children[1].children[0].text).toEqual('Test2');
});

test('handles multiple spans inside paragraph', function () {
const html = '<p><span>Test</span> <span>Test2</span></p>';
const editorState = runGenerateEditorState(html);

expect(editorState.root.children[0].children.length).toEqual(1);
expect(editorState.root.children[0].children[0].text).toEqual('Test Test2');
});

test('handles multiple spans with no wrapper', function () {
const html = '<span>Test</span> <span>Test2</span>';
const editorState = runGenerateEditorState(html);

expect(editorState.root.children.length).toEqual(1);
expect(editorState.root.children[0].children.length).toEqual(1);
expect(editorState.root.children[0].children[0].text).toEqual('Test Test2');
});

test('handles line breaks inside paragraph', function () {
const html = '<p>Test<br>Test2</p>';
const editorState = runGenerateEditorState(html);

expect(editorState.root.children[0].children.length).toEqual(3);
expect(editorState.root.children[0].children[0].text).toEqual('Test');
expect(editorState.root.children[0].children[1].type).toEqual('linebreak');
expect(editorState.root.children[0].children[2].text).toEqual('Test2');
});

test('handles line breaks with no wrapper', function () {
const html = 'Test<br>Test2';
const editorState = runGenerateEditorState(html);

expect(editorState.root.children.length).toEqual(1);
expect(editorState.root.children[0].type).toEqual('paragraph');
expect(editorState.root.children[0].children[0].text).toEqual('Test');
expect(editorState.root.children[0].children[1].type).toEqual('linebreak');
expect(editorState.root.children[0].children[2].text).toEqual('Test2');
});

test('handles line breaks and spans with no wrapper', function () {
const html = '<span>Test</span><br><span>Test2</span>';
const editorState = runGenerateEditorState(html);

expect(editorState.root.children.length).toEqual(1);
expect(editorState.root.children[0].type).toEqual('paragraph');
expect(editorState.root.children[0].children[0].text).toEqual('Test');
expect(editorState.root.children[0].children[1].type).toEqual('linebreak');
expect(editorState.root.children[0].children[2].text).toEqual('Test2');
});

// https://github.com/facebook/lexical/issues/2807
test('handles whitespace between list items', function () {
const html = '<ul><li>Test</li> <li>Test2</li></ul>';
const editorState = runGenerateEditorState(html);

expect(editorState.root.children.length).toEqual(1);
expect(editorState.root.children[0].type).toEqual('list');
expect(editorState.root.children[0].children.length).toEqual(2);
expect(editorState.root.children[0].children[0].type).toEqual('listitem');
expect(editorState.root.children[0].children[1].type).toEqual('listitem');
expect(editorState.root.children[0].children[0].children[0].text).toEqual('Test');
expect(editorState.root.children[0].children[1].children[0].text).toEqual('Test2');
});

describe('_$generateNodesFromHTML', () => {
function testGenerateNodesFromHTML(html, callback) {
const editor = createEditor({
// lexical swallows errors inside updates by default,
// so we need to throw them to fail the test
onError: (error) => {
throw error;
}
});
editor.update(() => {
const nodes = _$generateNodesFromHTML(editor, html);
callback(nodes);
}, {discrete: true});
}

test('can generate basic paragraph node from html', function () {
const html = '<p>Test</p>';
testGenerateNodesFromHTML(html, (nodes) => {
expect(nodes.length).toEqual(1);
expect(nodes[0].getType()).toEqual('paragraph');
expect(nodes[0].getTextContent()).toEqual('Test');
});
});

test('handles single span inside paragraph', function () {
const html = '<p><span>Test</span></p>';
testGenerateNodesFromHTML(html, (nodes) => {
expect(nodes[0].getChildren().length).toEqual(1);
expect(nodes[0].getChildren()[0].getType()).toEqual('text');
});
});

test('handles multiple spans inside paragraph', function () {
const html = '<p><span>Test</span> <span>Test2</span></p>';
testGenerateNodesFromHTML(html, (nodes) => {
expect(nodes[0].getChildren().length).toEqual(3);
expect(nodes[0].getChildren()[0].getTextContent()).toEqual('Test');
expect(nodes[0].getChildren()[1].getTextContent()).toEqual(' ');
expect(nodes[0].getChildren()[2].getTextContent()).toEqual('Test2');
});
});

test('handles multiple spans with no wrapper', function () {
const html = '<span>Test</span> <span>Test2</span>';
testGenerateNodesFromHTML(html, (nodes) => {
expect(nodes.length).toEqual(3);
expect(nodes[0].getTextContent()).toEqual('Test');
expect(nodes[1].getTextContent()).toEqual(' ');
expect(nodes[2].getTextContent()).toEqual('Test2');
});
});
});
});
Loading