From b6ef7dc366255de1e3a5c1927bd14f7ff4e3cbdb Mon Sep 17 00:00:00 2001 From: Pedro Bonamin <46196328+pedrobonamin@users.noreply.github.com> Date: Wed, 13 Dec 2023 19:16:56 +0100 Subject: [PATCH] fix(pte): removes empty text block when inserting a new block in that position (#5271) * fix(pte): removes empty text block when inserting a new block in that position * fix(pte): use isEqualToEmptyEditor helper function --- .../__tests__/withEditableAPIInsert.test.tsx | 207 +++++++++++++++++- .../editor/plugins/createWithEditableAPI.ts | 16 +- 2 files changed, 221 insertions(+), 2 deletions(-) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx index a9dae719633..b2ccc69fa28 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx @@ -20,12 +20,32 @@ const initialValue = [ style: 'normal', }, ] - const initialSelection = { focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 7}, anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 7}, } +const emptyTextBlock = [ + { + _key: 'emptyBlock', + _type: 'myTestBlockType', + children: [ + { + _key: 'emptySpan', + _type: 'span', + marks: [], + text: '', + }, + ], + markDefs: [], + style: 'normal', + }, +] +const emptyBlockSelection = { + focus: {path: [{_key: 'emptyBlock'}, 'children', {_key: 'emptySpan'}], offset: 0}, + anchor: {path: [{_key: 'emptyBlock'}, 'children', {_key: 'emptySpan'}], offset: 0}, +} + describe('plugin:withEditableAPI: .insertChild()', () => { it('inserts child nodes correctly', async () => { const editorRef: React.RefObject = React.createRef() @@ -137,3 +157,188 @@ describe('plugin:withEditableAPI: .insertChild()', () => { }) }) }) + +describe('plugin:withEditableAPI: .insertBlock()', () => { + it('should not add empty blank blocks: empty block', async () => { + const editorRef: React.RefObject = React.createRef() + const onChange = jest.fn() + render( + , + ) + const editor = editorRef.current + const someObject = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') + + await waitFor(() => { + if (editorRef.current && someObject) { + PortableTextEditor.focus(editorRef.current) + PortableTextEditor.select(editorRef.current, emptyBlockSelection) + PortableTextEditor.insertBlock(editorRef.current, someObject, {color: 'red'}) + + expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ + {_key: '2', _type: 'someObject', color: 'red'}, + ]) + } else { + throw new Error('No editor or someObject') + } + }) + }) + + it('should not add empty blank blocks: non-empty block', async () => { + const editorRef: React.RefObject = React.createRef() + const onChange = jest.fn() + render( + , + ) + const editor = editorRef.current + const someObject = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') + + await waitFor(() => { + if (editorRef.current && someObject) { + PortableTextEditor.focus(editorRef.current) + PortableTextEditor.select(editorRef.current, initialSelection) + PortableTextEditor.insertBlock(editorRef.current, someObject, {color: 'red'}) + expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ + ...initialValue, + {_key: '2', _type: 'someObject', color: 'red'}, + ]) + } else { + throw new Error('No editor or someObject') + } + }) + }) + it('should be inserted before if focus is on start of block', async () => { + const editorRef: React.RefObject = React.createRef() + const onChange = jest.fn() + render( + , + ) + const editor = editorRef.current + const someObject = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') + + await waitFor(() => { + if (editorRef.current && someObject) { + PortableTextEditor.focus(editorRef.current) + PortableTextEditor.select(editorRef.current, { + focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, + anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, + }) + PortableTextEditor.insertBlock(editorRef.current, someObject, {color: 'red'}) + expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ + {_key: '2', _type: 'someObject', color: 'red'}, + ...initialValue, + ]) + } else { + throw new Error('No editor or someObject') + } + }) + }) + it('should not add empty blank blocks: non text block', async () => { + const editorRef: React.RefObject = React.createRef() + const onChange = jest.fn() + const value = [...initialValue, {_key: 'b', _type: 'someObject', color: 'red'}] + render( + , + ) + const editor = editorRef.current + const someObject = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') + + await waitFor(() => { + if (editorRef.current && someObject) { + PortableTextEditor.focus(editorRef.current) + // Focus the `someObject` block + PortableTextEditor.select(editorRef.current, { + focus: {path: [{_key: 'b'}], offset: 0}, + anchor: {path: [{_key: 'b'}], offset: 0}, + }) + PortableTextEditor.insertBlock(editorRef.current, someObject, {color: 'yellow'}) + expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ + ...value, + {_key: '2', _type: 'someObject', color: 'yellow'}, + ]) + } else { + throw new Error('No editor or someObject') + } + }) + }) + it('should not add empty blank blocks: in between blocks', async () => { + const editorRef: React.RefObject = React.createRef() + const onChange = jest.fn() + const value = [...initialValue, {_key: 'b', _type: 'someObject', color: 'red'}] + render( + , + ) + const editor = editorRef.current + const someObject = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') + + await waitFor(() => { + if (editorRef.current && someObject) { + PortableTextEditor.focus(editorRef.current) + // Focus the `text` block + PortableTextEditor.select(editorRef.current, initialSelection) + PortableTextEditor.insertBlock(editorRef.current, someObject, {color: 'yellow'}) + expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ + value[0], + {_key: '2', _type: 'someObject', color: 'yellow'}, + value[1], + ]) + } else { + throw new Error('No editor or someObject') + } + }) + }) + it('should not add empty blank blocks: in new empty text block', async () => { + const editorRef: React.RefObject = React.createRef() + const onChange = jest.fn() + const value = [...initialValue, ...emptyTextBlock] + render( + , + ) + const editor = editorRef.current + const someObject = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') + + await waitFor(() => { + if (editorRef.current && someObject) { + PortableTextEditor.focus(editorRef.current) + // Focus the empty `text` block + PortableTextEditor.select(editorRef.current, emptyBlockSelection) + PortableTextEditor.insertBlock(editorRef.current, someObject, {color: 'yellow'}) + expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ + value[0], + {_key: '2', _type: 'someObject', color: 'yellow'}, + ]) + } else { + throw new Error('No editor or someObject') + } + }) + }) +}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts index 69f4f5cdf3e..59712d735d9 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts @@ -24,7 +24,7 @@ import { PortableTextMemberSchemaTypes, PortableTextSlateEditor, } from '../../types/editor' -import {toSlateValue, fromSlateValue} from '../../utils/values' +import {toSlateValue, fromSlateValue, isEqualToEmptyEditor} from '../../utils/values' import {toSlateRange, toPortableTextRange} from '../../utils/ranges' import {PortableTextEditor} from '../PortableTextEditor' @@ -181,6 +181,20 @@ export function createWithEditableAPI( ], portableTextEditor, )[0] as unknown as Node + const [focusBlock] = Array.from( + Editor.nodes(editor, { + at: editor.selection.focus.path.slice(0, 1), + match: (n) => n._type === types.block.name, + }), + )[0] || [undefined] + + const isEmptyTextBlock = focusBlock && isEqualToEmptyEditor([focusBlock], types) + + if (isEmptyTextBlock) { + // If the text block is empty, remove it before inserting the new block. + Transforms.removeNodes(editor, {at: editor.selection}) + } + Editor.insertNode(editor, block) editor.onChange() return (