diff --git a/blocksuite/affine/blocks/paragraph/src/utils/merge-with-prev.ts b/blocksuite/affine/blocks/paragraph/src/utils/merge-with-prev.ts index 4e6daf46af..26af657b26 100644 --- a/blocksuite/affine/blocks/paragraph/src/utils/merge-with-prev.ts +++ b/blocksuite/affine/blocks/paragraph/src/utils/merge-with-prev.ts @@ -20,6 +20,7 @@ import { EMBED_BLOCK_MODEL_LIST } from '@blocksuite/affine-shared/consts'; import type { ExtendedModel } from '@blocksuite/affine-shared/types'; import { focusTitle, + getDocTitleInlineEditor, getPrevContentBlock, matchModels, } from '@blocksuite/affine-shared/utils'; @@ -45,10 +46,6 @@ export function mergeWithPrev(editorHost: EditorHost, model: BlockModel) { const parent = doc.getParent(model); if (!parent) return false; - if (matchModels(parent, [EdgelessTextBlockModel])) { - return true; - } - const prevBlock = getPrevContentBlock(editorHost, model); if (!prevBlock) { return handleNoPreviousSibling(editorHost, model); @@ -123,36 +120,63 @@ function handleNoPreviousSibling(editorHost: EditorHost, model: ExtendedModel) { const parent = doc.getParent(model); if (!parent) return false; - if (matchModels(parent, [NoteBlockModel]) && parent.isPageBlock()) { + const focusFirstBlockStart = () => { + const firstBlock = parent.firstChild(); + if (firstBlock) { + focusTextModel(editorHost.std, firstBlock.id, 0); + } + }; + + if (matchModels(parent, [NoteBlockModel])) { + const hasTitleEditor = getDocTitleInlineEditor(editorHost); const rootModel = model.store.root as RootBlockModel; const title = rootModel.props.title; + const shouldHandleTitle = parent.isPageBlock() && hasTitleEditor; + doc.captureSync(); - let textLength = 0; - if (text) { - textLength = text.length; - title.join(text); + + if (shouldHandleTitle) { + let textLength = 0; + if (text) { + textLength = text.length; + title.join(text); + } + if (model.children.length > 0 || doc.getNext(model)) { + doc.deleteBlock(model, { + bringChildrenTo: parent, + }); + } + // no other blocks, preserve a empty line + else { + text?.clear(); + } + focusTitle(editorHost, title.length - textLength); + return true; } // Preserve at least one block to be able to focus on container click - if (doc.getNext(model) || model.children.length > 0) { + if ( + text?.length === 0 && + (model.children.length > 0 || doc.getNext(model)) + ) { doc.deleteBlock(model, { bringChildrenTo: parent, }); - } else { - text?.clear(); + focusFirstBlockStart(); + return true; } - focusTitle(editorHost, title.length - textLength); - return true; } if ( - matchModels(parent, [EdgelessTextBlockModel]) || - model.children.length > 0 + matchModels(parent, [EdgelessTextBlockModel]) && + text?.length === 0 && + (model.children.length > 0 || doc.getNext(model)) ) { doc.deleteBlock(model, { bringChildrenTo: parent, }); + focusFirstBlockStart(); return true; } diff --git a/tests/blocksuite/e2e/edgeless/edgeless-text.spec.ts b/tests/blocksuite/e2e/edgeless/edgeless-text.spec.ts index 5794161102..1930e7afcf 100644 --- a/tests/blocksuite/e2e/edgeless/edgeless-text.spec.ts +++ b/tests/blocksuite/e2e/edgeless/edgeless-text.spec.ts @@ -11,6 +11,8 @@ import { edgelessCommonSetup, enterPlaygroundRoom, getEdgelessSelectedRect, + getIds, + getInlineSelectionIndex, getPageSnapshot, initEmptyEdgelessState, pasteByKeyboard, @@ -21,6 +23,8 @@ import { pressBackspace, pressEnter, pressEscape, + pressShiftTab, + pressTab, redoByKeyboard, selectAllByKeyboard, setEdgelessTool, @@ -552,6 +556,52 @@ test.describe('edgeless text block', () => { 1 ); }); + + test('edgeless text should be able to delete line', async ({ page }) => { + await dblclickView(page, [100, -100]); + // 5: aaa + // 6: bbb + // 7: ccc| + { + await type(page, 'aaa'); + await pressEnter(page); + await pressTab(page); + await type(page, 'bbb'); + await pressEnter(page); + await pressShiftTab(page); + await type(page, 'ccc'); + } + + // 5: aaa + // 6: bbb| + await pressBackspace(page, 4); + expect(await getIds(page)).not.toContain(7); + await assertBlockTextContent(page, 5, 'aaa'); + await assertBlockTextContent(page, 6, 'bbb'); + expect(await getInlineSelectionIndex(page)).toBe(3); + + // 5: |aaa + // 6: bbb + { + await pressArrowUp(page); + await pressArrowLeft(page, 3); + await pressBackspace(page); + } + + await assertBlockTextContent(page, 5, 'aaa'); + await assertBlockTextContent(page, 6, 'bbb'); + expect(await getInlineSelectionIndex(page)).toBe(0); + + // 6: |bbb + { + await pressArrowRight(page, 3); + await pressBackspace(page, 4); + } + + expect(await getIds(page)).not.toContain(7); + await assertBlockTextContent(page, 6, 'bbb'); + expect(await getInlineSelectionIndex(page)).toBe(0); + }); }); test('press backspace at the start of first line when edgeless text exist', async ({ diff --git a/tests/blocksuite/e2e/paragraph.spec.ts b/tests/blocksuite/e2e/paragraph.spec.ts index be53c8f054..0f49f3e9f4 100644 --- a/tests/blocksuite/e2e/paragraph.spec.ts +++ b/tests/blocksuite/e2e/paragraph.spec.ts @@ -8,7 +8,9 @@ import { enterPlaygroundRoom, focusRichText, focusTitle, + getBlockIds, getIndexCoordinate, + getInlineSelectionIndex, getPageSnapshot, initEmptyEdgelessState, initEmptyParagraphState, @@ -44,6 +46,7 @@ import { assertBlockChildrenIds, assertBlockCount, assertBlockSelections, + assertBlockTextContent, assertBlockType, assertClassName, assertDivider, @@ -734,6 +737,45 @@ test('delete at start of paragraph with content', async ({ page }) => { await assertRichTexts(page, ['123', '456']); }); +test('delete empty line should work correctly', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyParagraphState(page); + await focusRichText(page); + + // 2: aaa + // 3: bbb + // 4: ccc| + { + await type(page, 'aaa'); + await pressEnter(page); + await pressTab(page); + await type(page, 'bbb'); + await pressEnter(page); + await pressShiftTab(page); + await type(page, 'ccc'); + } + + // 2: aaa + // 3: bbb| + await pressBackspace(page, 4); + expect(await getBlockIds(page)).not.toContain(4); + await assertBlockTextContent(page, 2, 'aaa'); + await assertBlockTextContent(page, 3, 'bbb'); + expect(await getInlineSelectionIndex(page)).toBe(3); + + // title: |aaa + // 3: bbb + { + await pressArrowUp(page); + await pressArrowLeft(page, 3); + await pressBackspace(page); + } + await expect(page.locator('doc-title')).toContainText('aaa'); + expect(await getBlockIds(page)).not.toContain(2); + await assertBlockTextContent(page, 3, 'bbb'); + expect(await getInlineSelectionIndex(page)).toBe(0); +}); + test('get focus from page title enter', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); diff --git a/tests/blocksuite/e2e/utils/actions/block.ts b/tests/blocksuite/e2e/utils/actions/block.ts index fee7ba2ebc..72e90e96e5 100644 --- a/tests/blocksuite/e2e/utils/actions/block.ts +++ b/tests/blocksuite/e2e/utils/actions/block.ts @@ -23,3 +23,9 @@ export async function updateBlockType( ); await waitNextFrame(page, 400); } + +export async function getBlockIds(page: Page) { + return page.evaluate(() => { + return window.host.std.store.getAllModels().map(m => m.id); + }); +}