Mobile: Rich Text Editor: Fix adding headings moves the cursor to the next line (#12934)

This commit is contained in:
Henry Heino
2025-08-18 16:07:55 +03:00
committed by GitHub
parent 34b7f4e1f8
commit bbd8f6f40e
2 changed files with 115 additions and 28 deletions
@@ -4,47 +4,133 @@ import extractSelectedLinesTo from './extractSelectedLinesTo';
import schema from '../schema'; import schema from '../schema';
describe('extractSelectedLinesTo', () => { describe('extractSelectedLinesTo', () => {
test('should extract a single line containing the cursor to a heading', () => { test.each([
{
label: 'should extract a single line containing the cursor to a heading',
initial: {
docHtml: '<p>Line 1<br>Line 2<br>Line 3</p>',
// Put the cursor in the middle of the second line
cursorPosition: '<Line 1|Line'.length,
},
convertTo: { type: schema.nodes.heading, attrs: { level: 1 } },
expected: {
// The section of the document containing the cursor should now be a new line
docJson: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Line 1' }],
},
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: 'Line 2' }],
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Line 3' }],
},
],
// The cursor should move to the end of the extracted line
cursorPosition: '<Line 1><Line 2'.length,
},
},
{
label: 'should convert an empty paragraph to a heading',
initial: {
docHtml: '<p>Line 1</p><p></p><p>Line 3</p>',
cursorPosition: '<Line 1><'.length,
},
convertTo: { type: schema.nodes.heading },
expected: {
docJson: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Line 1' }],
},
{
type: 'heading',
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Line 3' }],
},
],
cursorPosition: '<Line 1><'.length,
},
},
{
label: 'should convert the last line in a paragraph to a heading',
initial: {
docHtml: '<p>Line 1<br/></p><p>End</p>',
cursorPosition: '<Line 1|'.length,
},
convertTo: { type: schema.nodes.heading },
expected: {
docJson: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'Line 1' }],
},
{
type: 'heading',
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'End' }],
},
],
cursorPosition: '<Line 1><'.length,
},
},
{
label: 'should convert the first line in a paragraph to a heading',
initial: {
docHtml: '<p><br/>Line 1</p><p>End</p>',
cursorPosition: '<'.length,
},
convertTo: { type: schema.nodes.heading },
expected: {
docJson: [
{
type: 'heading',
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Line 1' }],
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'End' }],
},
],
cursorPosition: '<'.length,
},
},
])('$label', ({ initial, expected, convertTo }) => {
const view = createTestEditor({ const view = createTestEditor({
html: '<p>Line 1<br>Line 2<br>Line 3</p>', html: initial.docHtml,
}); });
view.dispatch(view.state.tr.setSelection( view.dispatch(view.state.tr.setSelection(
// Put the cursor in the middle of the second line TextSelection.create(
TextSelection.create(view.state.doc, '<Line 1|Line'.length), view.state.doc,
initial.cursorPosition,
),
)); ));
const { transaction } = extractSelectedLinesTo( const { transaction } = extractSelectedLinesTo(
{ type: schema.nodes.heading, attrs: { level: 1 } }, { type: convertTo.type, attrs: convertTo.attrs ?? { } },
view.state.tr, view.state.tr,
view.state.selection, view.state.selection,
); );
view.dispatch(transaction); view.dispatch(transaction);
// The section of the document containing the cursor should now be a new line
expect(view.state.doc.toJSON()).toMatchObject({ expect(view.state.doc.toJSON()).toMatchObject({
content: [ content: expected.docJson,
{
type: 'paragraph',
content: [{ type: 'text', text: 'Line 1' }],
},
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: 'Line 2' }],
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Line 3' }],
},
],
}); });
// The selection should still be in the heading // All of the tests in this group expect a single cursor
expect(view.state.selection.$anchor.parent.toJSON()).toMatchObject({ expect(view.state.selection.empty).toBe(true);
type: 'heading', expect(view.state.selection.from).toBe(expected.cursorPosition);
attrs: { level: 1 },
});
}); });
test('should extract multiple lines in the same paragraph to a new paragraph', () => { test('should extract multiple lines in the same paragraph to a new paragraph', () => {
@@ -33,7 +33,8 @@ const extractSelectedLinesTo = (extractTo: ExtractToOptions, transaction: Transa
const firstParagraphFrom = firstParagraphPos; const firstParagraphFrom = firstParagraphPos;
const lastParagraph = transaction.doc.nodeAt(lastParagraphPos); const lastParagraph = transaction.doc.nodeAt(lastParagraphPos);
const lastParagraphTo = lastParagraphPos + lastParagraph.nodeSize; // -1: Exclude the end token
const lastParagraphTo = lastParagraphPos + lastParagraph.nodeSize - 1;
// Find the previous and next <br/>s (or the start/end of the paragraph) // Find the previous and next <br/>s (or the start/end of the paragraph)
let fromBreakPosition = firstParagraphFrom; let fromBreakPosition = firstParagraphFrom;