fix(editor): behavior of deleting at the start of line (#12787)
Close BS-3182, #12736 #### PR Dependency Tree * **PR #12787** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved the behavior when deleting empty lines and merging blocks, ensuring more accurate handling of block deletion and cursor focus in various scenarios. - **Tests** - Added new end-to-end tests to verify correct deletion of lines in edgeless text and paragraph blocks, including checks for block removal and cursor position. - Introduced a utility function to retrieve block IDs for testing purposes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -20,6 +20,7 @@ import { EMBED_BLOCK_MODEL_LIST } from '@blocksuite/affine-shared/consts';
|
|||||||
import type { ExtendedModel } from '@blocksuite/affine-shared/types';
|
import type { ExtendedModel } from '@blocksuite/affine-shared/types';
|
||||||
import {
|
import {
|
||||||
focusTitle,
|
focusTitle,
|
||||||
|
getDocTitleInlineEditor,
|
||||||
getPrevContentBlock,
|
getPrevContentBlock,
|
||||||
matchModels,
|
matchModels,
|
||||||
} from '@blocksuite/affine-shared/utils';
|
} from '@blocksuite/affine-shared/utils';
|
||||||
@@ -45,10 +46,6 @@ export function mergeWithPrev(editorHost: EditorHost, model: BlockModel) {
|
|||||||
const parent = doc.getParent(model);
|
const parent = doc.getParent(model);
|
||||||
if (!parent) return false;
|
if (!parent) return false;
|
||||||
|
|
||||||
if (matchModels(parent, [EdgelessTextBlockModel])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevBlock = getPrevContentBlock(editorHost, model);
|
const prevBlock = getPrevContentBlock(editorHost, model);
|
||||||
if (!prevBlock) {
|
if (!prevBlock) {
|
||||||
return handleNoPreviousSibling(editorHost, model);
|
return handleNoPreviousSibling(editorHost, model);
|
||||||
@@ -123,36 +120,63 @@ function handleNoPreviousSibling(editorHost: EditorHost, model: ExtendedModel) {
|
|||||||
const parent = doc.getParent(model);
|
const parent = doc.getParent(model);
|
||||||
if (!parent) return false;
|
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 rootModel = model.store.root as RootBlockModel;
|
||||||
const title = rootModel.props.title;
|
const title = rootModel.props.title;
|
||||||
|
|
||||||
|
const shouldHandleTitle = parent.isPageBlock() && hasTitleEditor;
|
||||||
|
|
||||||
doc.captureSync();
|
doc.captureSync();
|
||||||
|
|
||||||
|
if (shouldHandleTitle) {
|
||||||
let textLength = 0;
|
let textLength = 0;
|
||||||
if (text) {
|
if (text) {
|
||||||
textLength = text.length;
|
textLength = text.length;
|
||||||
title.join(text);
|
title.join(text);
|
||||||
}
|
}
|
||||||
|
if (model.children.length > 0 || doc.getNext(model)) {
|
||||||
// Preserve at least one block to be able to focus on container click
|
|
||||||
if (doc.getNext(model) || model.children.length > 0) {
|
|
||||||
doc.deleteBlock(model, {
|
doc.deleteBlock(model, {
|
||||||
bringChildrenTo: parent,
|
bringChildrenTo: parent,
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
|
// no other blocks, preserve a empty line
|
||||||
|
else {
|
||||||
text?.clear();
|
text?.clear();
|
||||||
}
|
}
|
||||||
focusTitle(editorHost, title.length - textLength);
|
focusTitle(editorHost, title.length - textLength);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preserve at least one block to be able to focus on container click
|
||||||
if (
|
if (
|
||||||
matchModels(parent, [EdgelessTextBlockModel]) ||
|
text?.length === 0 &&
|
||||||
model.children.length > 0
|
(model.children.length > 0 || doc.getNext(model))
|
||||||
) {
|
) {
|
||||||
doc.deleteBlock(model, {
|
doc.deleteBlock(model, {
|
||||||
bringChildrenTo: parent,
|
bringChildrenTo: parent,
|
||||||
});
|
});
|
||||||
|
focusFirstBlockStart();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
matchModels(parent, [EdgelessTextBlockModel]) &&
|
||||||
|
text?.length === 0 &&
|
||||||
|
(model.children.length > 0 || doc.getNext(model))
|
||||||
|
) {
|
||||||
|
doc.deleteBlock(model, {
|
||||||
|
bringChildrenTo: parent,
|
||||||
|
});
|
||||||
|
focusFirstBlockStart();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
edgelessCommonSetup,
|
edgelessCommonSetup,
|
||||||
enterPlaygroundRoom,
|
enterPlaygroundRoom,
|
||||||
getEdgelessSelectedRect,
|
getEdgelessSelectedRect,
|
||||||
|
getIds,
|
||||||
|
getInlineSelectionIndex,
|
||||||
getPageSnapshot,
|
getPageSnapshot,
|
||||||
initEmptyEdgelessState,
|
initEmptyEdgelessState,
|
||||||
pasteByKeyboard,
|
pasteByKeyboard,
|
||||||
@@ -21,6 +23,8 @@ import {
|
|||||||
pressBackspace,
|
pressBackspace,
|
||||||
pressEnter,
|
pressEnter,
|
||||||
pressEscape,
|
pressEscape,
|
||||||
|
pressShiftTab,
|
||||||
|
pressTab,
|
||||||
redoByKeyboard,
|
redoByKeyboard,
|
||||||
selectAllByKeyboard,
|
selectAllByKeyboard,
|
||||||
setEdgelessTool,
|
setEdgelessTool,
|
||||||
@@ -552,6 +556,52 @@ test.describe('edgeless text block', () => {
|
|||||||
1
|
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 ({
|
test('press backspace at the start of first line when edgeless text exist', async ({
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import {
|
|||||||
enterPlaygroundRoom,
|
enterPlaygroundRoom,
|
||||||
focusRichText,
|
focusRichText,
|
||||||
focusTitle,
|
focusTitle,
|
||||||
|
getBlockIds,
|
||||||
getIndexCoordinate,
|
getIndexCoordinate,
|
||||||
|
getInlineSelectionIndex,
|
||||||
getPageSnapshot,
|
getPageSnapshot,
|
||||||
initEmptyEdgelessState,
|
initEmptyEdgelessState,
|
||||||
initEmptyParagraphState,
|
initEmptyParagraphState,
|
||||||
@@ -44,6 +46,7 @@ import {
|
|||||||
assertBlockChildrenIds,
|
assertBlockChildrenIds,
|
||||||
assertBlockCount,
|
assertBlockCount,
|
||||||
assertBlockSelections,
|
assertBlockSelections,
|
||||||
|
assertBlockTextContent,
|
||||||
assertBlockType,
|
assertBlockType,
|
||||||
assertClassName,
|
assertClassName,
|
||||||
assertDivider,
|
assertDivider,
|
||||||
@@ -734,6 +737,45 @@ test('delete at start of paragraph with content', async ({ page }) => {
|
|||||||
await assertRichTexts(page, ['123', '456']);
|
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 }) => {
|
test('get focus from page title enter', async ({ page }) => {
|
||||||
await enterPlaygroundRoom(page);
|
await enterPlaygroundRoom(page);
|
||||||
await initEmptyParagraphState(page);
|
await initEmptyParagraphState(page);
|
||||||
|
|||||||
@@ -23,3 +23,9 @@ export async function updateBlockType(
|
|||||||
);
|
);
|
||||||
await waitNextFrame(page, 400);
|
await waitNextFrame(page, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getBlockIds(page: Page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
return window.host.std.store.getAllModels().map(m => m.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user