fix(editor): should delete collapsed siblings when delete heading (#9376)
This commit is contained in:
@@ -34,6 +34,7 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
|
|||||||
|
|
||||||
private readonly _onClickIcon = (e: MouseEvent) => {
|
private readonly _onClickIcon = (e: MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
if (this.model.type === 'toggle') {
|
if (this.model.type === 'toggle') {
|
||||||
if (this.doc.readonly) {
|
if (this.doc.readonly) {
|
||||||
|
|||||||
@@ -4,14 +4,10 @@ import {
|
|||||||
notifyDocCreated,
|
notifyDocCreated,
|
||||||
promptDocTitle,
|
promptDocTitle,
|
||||||
} from '@blocksuite/affine-block-embed';
|
} from '@blocksuite/affine-block-embed';
|
||||||
|
import { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
|
||||||
import { matchFlavours } from '@blocksuite/affine-shared/utils';
|
import { matchFlavours } from '@blocksuite/affine-shared/utils';
|
||||||
import type {
|
import type { BlockComponent, UIEventHandler } from '@blocksuite/block-std';
|
||||||
BlockComponent,
|
|
||||||
BlockSelection,
|
|
||||||
UIEventHandler,
|
|
||||||
} from '@blocksuite/block-std';
|
|
||||||
import { IS_MAC, IS_WINDOWS } from '@blocksuite/global/env';
|
import { IS_MAC, IS_WINDOWS } from '@blocksuite/global/env';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
|
||||||
|
|
||||||
export class PageKeyboardManager {
|
export class PageKeyboardManager {
|
||||||
private readonly _handleDelete: UIEventHandler = ctx => {
|
private readonly _handleDelete: UIEventHandler = ctx => {
|
||||||
@@ -24,26 +20,43 @@ export class PageKeyboardManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this._doc.transact(() => {
|
|
||||||
const selection = this._replaceBlocksBySelection(
|
|
||||||
blockSelections,
|
|
||||||
'affine:paragraph',
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selection) {
|
const deletedBlocks: string[] = [];
|
||||||
this._selection.setGroup('note', [
|
blockSelections.forEach(sel => {
|
||||||
this._selection.create('text', {
|
const id = sel.blockId;
|
||||||
from: {
|
const block = this._doc.getBlock(id);
|
||||||
index: 0,
|
if (!block) return;
|
||||||
length: 0,
|
const model = block.model;
|
||||||
blockId: selection.blockId,
|
|
||||||
},
|
if (
|
||||||
to: null,
|
matchFlavours(model, ['affine:paragraph']) &&
|
||||||
}),
|
model.type.startsWith('h') &&
|
||||||
]);
|
model.collapsed
|
||||||
|
) {
|
||||||
|
const component = this.rootComponent.host.view.getBlock(id);
|
||||||
|
if (!(component instanceof ParagraphBlockComponent)) return;
|
||||||
|
const collapsedSiblings = component.collapsedSiblings;
|
||||||
|
|
||||||
|
deletedBlocks.push(
|
||||||
|
...[id, ...collapsedSiblings.map(sibling => sibling.id)].filter(
|
||||||
|
id => !deletedBlocks.includes(id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
deletedBlocks.push(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._doc.transact(() => {
|
||||||
|
deletedBlocks.forEach(id => {
|
||||||
|
const block = this._doc.getBlock(id);
|
||||||
|
if (block) {
|
||||||
|
this._doc.deleteBlock(block.model);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._selection.clear(['block', 'text']);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private get _currentSelection() {
|
private get _currentSelection() {
|
||||||
@@ -135,44 +148,4 @@ export class PageKeyboardManager {
|
|||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _deleteBlocksBySelection(selections: BlockSelection[]) {
|
|
||||||
selections.forEach(selection => {
|
|
||||||
const block = this._doc.getBlockById(selection.blockId);
|
|
||||||
if (block) {
|
|
||||||
this._doc.deleteBlock(block);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _replaceBlocksBySelection(
|
|
||||||
selections: BlockSelection[],
|
|
||||||
flavour: string,
|
|
||||||
props: Record<string, unknown>
|
|
||||||
) {
|
|
||||||
const current = selections[0];
|
|
||||||
const first = this._doc.getBlockById(current.blockId);
|
|
||||||
const firstElement = this.rootComponent.host.view.getBlock(current.blockId);
|
|
||||||
|
|
||||||
assertExists(first, `Cannot find block ${current.blockId}`);
|
|
||||||
assertExists(firstElement, `Cannot find block view ${current.blockId}`);
|
|
||||||
|
|
||||||
const parent = this._doc.getParent(first);
|
|
||||||
const index = parent?.children.indexOf(first);
|
|
||||||
|
|
||||||
this._deleteBlocksBySelection(selections);
|
|
||||||
|
|
||||||
try {
|
|
||||||
this._doc.schema.validate(flavour, parent?.flavour);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockId = this._doc.addBlock(flavour as never, props, parent, index);
|
|
||||||
|
|
||||||
return {
|
|
||||||
blockId,
|
|
||||||
path: blockId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -481,12 +481,7 @@ test('should attachment can be deleted', async ({ page }) => {
|
|||||||
prop:hidden={false}
|
prop:hidden={false}
|
||||||
prop:index="a0"
|
prop:index="a0"
|
||||||
prop:lockedBySelf={false}
|
prop:lockedBySelf={false}
|
||||||
>
|
/>`,
|
||||||
<affine:paragraph
|
|
||||||
prop:collapsed={false}
|
|
||||||
prop:type="text"
|
|
||||||
/>
|
|
||||||
</affine:note>`,
|
|
||||||
noteId
|
noteId
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ test('should sync selected-blocks to session-manager when clicking drag handle',
|
|||||||
await handle.click();
|
await handle.click();
|
||||||
|
|
||||||
await page.keyboard.press('Backspace');
|
await page.keyboard.press('Backspace');
|
||||||
await assertRichTexts(page, ['', '456', '789']);
|
await assertRichTexts(page, ['456', '789']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.fixme(
|
test.fixme(
|
||||||
|
|||||||
@@ -59,15 +59,15 @@ test('block level range delete', async ({ page }) => {
|
|||||||
|
|
||||||
await dragBetweenCoords(page, below789, above123);
|
await dragBetweenCoords(page, below789, above123);
|
||||||
await pressBackspace(page);
|
await pressBackspace(page);
|
||||||
await assertBlockCount(page, 'paragraph', 1);
|
await assertBlockCount(page, 'paragraph', 0);
|
||||||
await assertRichTexts(page, ['']);
|
await assertRichTexts(page, []);
|
||||||
|
|
||||||
await waitNextFrame(page);
|
await waitNextFrame(page);
|
||||||
await undoByClick(page);
|
await undoByClick(page);
|
||||||
await assertRichTexts(page, ['123', '456', '789']);
|
await assertRichTexts(page, ['123', '456', '789']);
|
||||||
|
|
||||||
await redoByClick(page);
|
await redoByClick(page);
|
||||||
await assertRichTexts(page, ['']);
|
await assertRichTexts(page, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('block level range delete by forwardDelete', async ({ page }) => {
|
test('block level range delete by forwardDelete', async ({ page }) => {
|
||||||
@@ -86,15 +86,15 @@ test('block level range delete by forwardDelete', async ({ page }) => {
|
|||||||
await dragBetweenCoords(page, below789, above123);
|
await dragBetweenCoords(page, below789, above123);
|
||||||
await pressForwardDelete(page);
|
await pressForwardDelete(page);
|
||||||
await waitNextFrame(page);
|
await waitNextFrame(page);
|
||||||
await assertBlockCount(page, 'paragraph', 1);
|
await assertBlockCount(page, 'paragraph', 0);
|
||||||
await assertRichTexts(page, ['']);
|
await assertRichTexts(page, []);
|
||||||
|
|
||||||
await waitNextFrame(page);
|
await waitNextFrame(page);
|
||||||
await undoByClick(page);
|
await undoByClick(page);
|
||||||
await assertRichTexts(page, ['123', '456', '789']);
|
await assertRichTexts(page, ['123', '456', '789']);
|
||||||
|
|
||||||
await redoByClick(page);
|
await redoByClick(page);
|
||||||
await assertRichTexts(page, ['']);
|
await assertRichTexts(page, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
// XXX: Doesn't simulate full user operation due to backspace cursor issue in Playwright.
|
// XXX: Doesn't simulate full user operation due to backspace cursor issue in Playwright.
|
||||||
@@ -108,9 +108,8 @@ test('select all and delete', async ({ page }) => {
|
|||||||
await selectAllByKeyboard(page);
|
await selectAllByKeyboard(page);
|
||||||
await shamefullyBlurActiveElement(page);
|
await shamefullyBlurActiveElement(page);
|
||||||
await pressBackspace(page);
|
await pressBackspace(page);
|
||||||
await focusRichText(page, 0);
|
await assertBlockCount(page, 'paragraph', 0);
|
||||||
await type(page, 'abc');
|
await assertRichTexts(page, []);
|
||||||
await assertRichTexts(page, ['abc']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('select all and delete by forwardDelete', async ({ page }) => {
|
test('select all and delete by forwardDelete', async ({ page }) => {
|
||||||
@@ -123,9 +122,8 @@ test('select all and delete by forwardDelete', async ({ page }) => {
|
|||||||
await selectAllByKeyboard(page);
|
await selectAllByKeyboard(page);
|
||||||
await shamefullyBlurActiveElement(page);
|
await shamefullyBlurActiveElement(page);
|
||||||
await pressForwardDelete(page);
|
await pressForwardDelete(page);
|
||||||
await focusRichText(page, 0);
|
await assertBlockCount(page, 'paragraph', 0);
|
||||||
await type(page, 'abc');
|
await assertRichTexts(page, []);
|
||||||
await assertRichTexts(page, ['abc']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('select all should work for multiple notes in doc mode', async ({
|
test('select all should work for multiple notes in doc mode', async ({
|
||||||
@@ -148,7 +146,13 @@ test('select all should work for multiple notes in doc mode', async ({
|
|||||||
|
|
||||||
async function clickListIcon(page: Page, i = 0) {
|
async function clickListIcon(page: Page, i = 0) {
|
||||||
const locator = page.locator('.affine-list-block__prefix').nth(i);
|
const locator = page.locator('.affine-list-block__prefix').nth(i);
|
||||||
await locator.click({ force: true });
|
await locator.click({
|
||||||
|
force: true,
|
||||||
|
position: {
|
||||||
|
x: 2,
|
||||||
|
y: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test('click the list icon can select and copy', async ({ page }) => {
|
test('click the list icon can select and copy', async ({ page }) => {
|
||||||
@@ -179,14 +183,10 @@ test('click the list icon can select and delete', async ({ page }) => {
|
|||||||
await initThreeLists(page);
|
await initThreeLists(page);
|
||||||
await assertRichTexts(page, ['123', '456', '789']);
|
await assertRichTexts(page, ['123', '456', '789']);
|
||||||
|
|
||||||
await clickListIcon(page, 0);
|
await clickListIcon(page, 1);
|
||||||
await waitNextFrame(page);
|
await waitNextFrame(page);
|
||||||
await pressBackspace(page);
|
await pressBackspace(page);
|
||||||
await assertRichTexts(page, ['', '456', '789']);
|
await assertRichTexts(page, ['123']);
|
||||||
await clickListIcon(page, 0);
|
|
||||||
await waitNextFrame(page);
|
|
||||||
await pressBackspace(page);
|
|
||||||
await assertRichTexts(page, ['', '']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('click the list icon can select and delete by forwardDelete', async ({
|
test('click the list icon can select and delete by forwardDelete', async ({
|
||||||
@@ -197,14 +197,10 @@ test('click the list icon can select and delete by forwardDelete', async ({
|
|||||||
await initThreeLists(page);
|
await initThreeLists(page);
|
||||||
await assertRichTexts(page, ['123', '456', '789']);
|
await assertRichTexts(page, ['123', '456', '789']);
|
||||||
|
|
||||||
await clickListIcon(page, 0);
|
await clickListIcon(page, 1);
|
||||||
await waitNextFrame(page);
|
await waitNextFrame(page);
|
||||||
await pressForwardDelete(page);
|
await pressForwardDelete(page);
|
||||||
await assertRichTexts(page, ['', '456', '789']);
|
await assertRichTexts(page, ['123']);
|
||||||
await clickListIcon(page, 0);
|
|
||||||
await waitNextFrame(page);
|
|
||||||
await pressForwardDelete(page);
|
|
||||||
await assertRichTexts(page, ['', '']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('selection on heavy page', async ({ page }) => {
|
test('selection on heavy page', async ({ page }) => {
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ test('native range delete by forwardDelete', async ({ page }) => {
|
|||||||
await assertRichTexts(page, ['123', '456', '789']);
|
await assertRichTexts(page, ['123', '456', '789']);
|
||||||
|
|
||||||
const box123 = await getRichTextBoundingBox(page, '2');
|
const box123 = await getRichTextBoundingBox(page, '2');
|
||||||
const inside123 = { x: box123.left - 1, y: box123.top + 1 };
|
const inside123 = { x: box123.left + 1, y: box123.top + 1 };
|
||||||
|
|
||||||
const box789 = await getRichTextBoundingBox(page, '4');
|
const box789 = await getRichTextBoundingBox(page, '4');
|
||||||
const inside789 = { x: box789.right - 1, y: box789.bottom - 1 };
|
const inside789 = { x: box789.right - 1, y: box789.bottom - 1 };
|
||||||
@@ -180,7 +180,7 @@ test('native range input', async ({ page }) => {
|
|||||||
await assertRichTexts(page, ['123', '456', '789']);
|
await assertRichTexts(page, ['123', '456', '789']);
|
||||||
|
|
||||||
const box123 = await getRichTextBoundingBox(page, '2');
|
const box123 = await getRichTextBoundingBox(page, '2');
|
||||||
const inside123 = { x: box123.left - 1, y: box123.top + 1 };
|
const inside123 = { x: box123.left + 1, y: box123.top + 1 };
|
||||||
|
|
||||||
const box789 = await getRichTextBoundingBox(page, '4');
|
const box789 = await getRichTextBoundingBox(page, '4');
|
||||||
const inside789 = { x: box789.right - 1, y: box789.bottom - 1 };
|
const inside789 = { x: box789.right - 1, y: box789.bottom - 1 };
|
||||||
@@ -200,10 +200,10 @@ test('native range selection backwards', async ({ page }) => {
|
|||||||
await assertRichTexts(page, ['123', '456', '789']);
|
await assertRichTexts(page, ['123', '456', '789']);
|
||||||
|
|
||||||
const box123 = await getRichTextBoundingBox(page, '2');
|
const box123 = await getRichTextBoundingBox(page, '2');
|
||||||
const above123 = { x: box123.left, y: box123.top - 2 };
|
const above123 = { x: box123.left + 1, y: box123.top + 1 };
|
||||||
|
|
||||||
const box789 = await getRichTextBoundingBox(page, '4');
|
const box789 = await getRichTextBoundingBox(page, '4');
|
||||||
const bottomRight789 = { x: box789.right, y: box789.bottom };
|
const bottomRight789 = { x: box789.right - 1, y: box789.bottom - 1 };
|
||||||
|
|
||||||
// from bottom to top
|
// from bottom to top
|
||||||
await dragBetweenCoords(page, bottomRight789, above123, { steps: 10 });
|
await dragBetweenCoords(page, bottomRight789, above123, { steps: 10 });
|
||||||
@@ -230,7 +230,7 @@ test('native range selection backwards by forwardDelete', async ({ page }) => {
|
|||||||
const above123 = { x: box123.left, y: box123.top - 2 };
|
const above123 = { x: box123.left, y: box123.top - 2 };
|
||||||
|
|
||||||
const box789 = await getRichTextBoundingBox(page, '4');
|
const box789 = await getRichTextBoundingBox(page, '4');
|
||||||
const bottomRight789 = { x: box789.right, y: box789.bottom };
|
const bottomRight789 = { x: box789.right - 1, y: box789.bottom - 1 };
|
||||||
|
|
||||||
// from bottom to top
|
// from bottom to top
|
||||||
await dragBetweenCoords(page, bottomRight789, above123, { steps: 10 });
|
await dragBetweenCoords(page, bottomRight789, above123, { steps: 10 });
|
||||||
@@ -390,7 +390,7 @@ test('select all text with dragging and delete', async ({ page }) => {
|
|||||||
await initThreeParagraphs(page);
|
await initThreeParagraphs(page);
|
||||||
await assertRichTexts(page, ['123', '456', '789']);
|
await assertRichTexts(page, ['123', '456', '789']);
|
||||||
|
|
||||||
await dragBetweenIndices(page, [0, 0], [2, 3], undefined, undefined, {
|
await dragBetweenIndices(page, [0, 0], [2, 3], { x: 1, y: 1 }, undefined, {
|
||||||
steps: 20,
|
steps: 20,
|
||||||
});
|
});
|
||||||
await pressBackspace(page);
|
await pressBackspace(page);
|
||||||
@@ -434,14 +434,7 @@ test('select all text with keyboard delete', async ({ page }) => {
|
|||||||
await selectAllByKeyboard(page);
|
await selectAllByKeyboard(page);
|
||||||
await selectAllByKeyboard(page);
|
await selectAllByKeyboard(page);
|
||||||
await pressBackspace(page);
|
await pressBackspace(page);
|
||||||
await assertRichTexts(page, ['', '456', '789']);
|
await assertRichTexts(page, ['456', '789']);
|
||||||
|
|
||||||
await type(page, 'abc');
|
|
||||||
await selectAllByKeyboard(page);
|
|
||||||
await selectAllByKeyboard(page);
|
|
||||||
await selectAllByKeyboard(page);
|
|
||||||
await pressBackspace(page);
|
|
||||||
await assertRichTexts(page, ['']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('select text leaving a few words in the last line and delete', async ({
|
test('select text leaving a few words in the last line and delete', async ({
|
||||||
@@ -801,7 +794,7 @@ test('Delete the second divider between two dividers by forwardDelete', async ({
|
|||||||
await pressArrowUp(page);
|
await pressArrowUp(page);
|
||||||
await pressForwardDelete(page);
|
await pressForwardDelete(page);
|
||||||
await assertDivider(page, 1);
|
await assertDivider(page, 1);
|
||||||
await assertRichTexts(page, ['', '', '']);
|
await assertRichTexts(page, ['', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should delete line with content after divider not lose content', async ({
|
test('should delete line with content after divider not lose content', async ({
|
||||||
@@ -818,7 +811,7 @@ test('should delete line with content after divider not lose content', async ({
|
|||||||
await waitNextFrame(page);
|
await waitNextFrame(page);
|
||||||
await pressBackspace(page, 2);
|
await pressBackspace(page, 2);
|
||||||
await assertDivider(page, 0);
|
await assertDivider(page, 0);
|
||||||
await assertRichTexts(page, ['', '123']);
|
await assertRichTexts(page, ['123']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should forwardDelete divider works properly', async ({ page }) => {
|
test('should forwardDelete divider works properly', async ({ page }) => {
|
||||||
@@ -835,7 +828,7 @@ test('should forwardDelete divider works properly', async ({ page }) => {
|
|||||||
await page.keyboard.press(`${SHORT_KEY}+ArrowRight`, { delay: 50 });
|
await page.keyboard.press(`${SHORT_KEY}+ArrowRight`, { delay: 50 });
|
||||||
await pressForwardDelete(page);
|
await pressForwardDelete(page);
|
||||||
await assertDivider(page, 0);
|
await assertDivider(page, 0);
|
||||||
await assertRichTexts(page, ['123', '', '']);
|
await assertRichTexts(page, ['123', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the cursor should move to closest editor block when clicking outside container', async ({
|
test('the cursor should move to closest editor block when clicking outside container', async ({
|
||||||
|
|||||||
Reference in New Issue
Block a user