fix(editor): paste when select multiple block texts (#10227)

[BS-2512](https://linear.app/affine-design/issue/BS-2512/选中多段粘贴,多段只有第一段会被replace,这个bug还在)
This commit is contained in:
donteatfriedrice
2025-02-18 12:13:55 +00:00
parent 176e0a1950
commit 15e9acefc2
4 changed files with 236 additions and 2 deletions

View File

@@ -61,8 +61,20 @@ export class PageClipboard extends ReadOnlyClipboard {
this._std.store.captureSync(); this._std.store.captureSync();
this._std.command this._std.command
.chain() .chain()
.try(cmd => [ .try<{}>(cmd => [
cmd.pipe(getTextSelectionCommand), cmd.pipe(getTextSelectionCommand).pipe((ctx, next) => {
const { currentTextSelection } = ctx;
if (!currentTextSelection) {
return;
}
const { from, to } = currentTextSelection;
if (to && from.blockId !== to.blockId) {
this._std.command.exec(deleteTextCommand, {
currentTextSelection,
});
}
return next();
}),
cmd cmd
.pipe(getSelectedModelsCommand) .pipe(getSelectedModelsCommand)
.pipe(clearAndSelectFirstModelCommand) .pipe(clearAndSelectFirstModelCommand)

View File

@@ -0,0 +1,143 @@
import { test } from '@affine-test/kit/playwright';
import { pasteContent } from '@affine-test/kit/utils/clipboard';
import {
copyByKeyboard,
pasteByKeyboard,
pressEnter,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { setSelection } from '@affine-test/kit/utils/selection';
import type { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
import { expect, type Page } from '@playwright/test';
const paragraphLocator = 'affine-note affine-paragraph';
// Helper function to create paragraph blocks with text
async function createParagraphBlocks(page: Page, texts: string[]) {
for (const text of texts) {
await pressEnter(page);
await type(page, text);
}
}
// Helper function to verify paragraph text content
async function verifyParagraphContent(
page: Page,
index: number,
expectedText: string
) {
expect(
await page
.locator(paragraphLocator)
.nth(index)
.evaluate(
(block: ParagraphBlockComponent, expected: string) =>
block.model.type === 'text' &&
block.model.text.toString() === expected,
expectedText
)
).toBeTruthy();
}
// Helper function to get paragraph block ids
async function getParagraphIds(page: Page) {
const paragraph = page.locator(paragraphLocator);
const paragraphIds = await paragraph.evaluateAll(
(blocks: ParagraphBlockComponent[]) => blocks.map(block => block.model.id)
);
return { paragraphIds };
}
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page, 'Clipboard Test');
await waitForEditorLoad(page);
});
test.describe('paste in multiple blocks text selection', () => {
test('paste plain text', async ({ page }) => {
const texts = ['hello world', 'hello world', 'hello world'];
await createParagraphBlocks(page, texts);
const { paragraphIds } = await getParagraphIds(page);
/**
* select text cross the 3 blocks:
* hello |world
* hello world
* hello| world
*/
await setSelection(page, paragraphIds[0], 6, paragraphIds[2], 5);
await pasteContent(page, { 'text/plain': 'test' });
/**
* after paste:
* hello test world
*/
await verifyParagraphContent(page, 0, 'hello test world');
});
test('paste snapshot', async ({ page }) => {
// Create initial test blocks
await createParagraphBlocks(page, ['test', 'test']);
// Create target blocks
await createParagraphBlocks(page, [
'hello world',
'hello world',
'hello world',
]);
/**
* before paste:
* test
* test
* hello world
* hello world
* hello world
*/
const { paragraphIds } = await getParagraphIds(page);
/**
* select the first 2 blocks:
* |test
* test|
* hello world
* hello world
* hello world
*/
await setSelection(page, paragraphIds[0], 0, paragraphIds[1], 4);
// copy the first 2 blocks
await copyByKeyboard(page);
/**
* select the last 3 blocks:
* test
* test
* hello |world
* hello world
* hello| world
*/
await setSelection(page, paragraphIds[2], 6, paragraphIds[4], 5);
// paste the first 2 blocks
await pasteByKeyboard(page);
await page.waitForTimeout(100);
/**
* after paste:
* test
* test
* hello test
* test world
*/
await verifyParagraphContent(page, 2, 'hello test');
await verifyParagraphContent(page, 3, 'test world');
});
});

View File

@@ -0,0 +1,24 @@
import type { Page } from '@playwright/test';
export async function pasteContent(
page: Page,
clipData: Record<string, unknown>
) {
await page.evaluate(
({ clipData }) => {
const e = new ClipboardEvent('paste', {
clipboardData: new DataTransfer(),
});
Object.defineProperty(e, 'target', {
writable: false,
value: document,
});
Object.keys(clipData).forEach(key => {
e.clipboardData?.setData(key, clipData[key] as string);
});
document.dispatchEvent(e);
},
{ clipData }
);
await page.waitForTimeout(100);
}

View File

@@ -0,0 +1,55 @@
import type { RichText } from '@blocksuite/affine-components/rich-text';
import type { Page } from '@playwright/test';
export async function setSelection(
page: Page,
anchorBlockId: string,
anchorOffset: number,
focusBlockId: string,
focusOffset: number
) {
await page.evaluate(
({ anchorBlockId, anchorOffset, focusBlockId, focusOffset }) => {
const editorHost = document.querySelector('editor-host');
if (!editorHost) {
throw new Error('Cannot find editor host');
}
const anchorRichText = editorHost.querySelector<RichText>(
`[data-block-id="${anchorBlockId}"] rich-text`
)!;
const anchorRichTextRange = anchorRichText.inlineEditor!.toDomRange({
index: anchorOffset,
length: 0,
})!;
const focusRichText = editorHost.querySelector<RichText>(
`[data-block-id="${focusBlockId}"] rich-text`
)!;
const focusRichTextRange = focusRichText.inlineEditor!.toDomRange({
index: focusOffset,
length: 0,
})!;
const sl = getSelection();
if (!sl) throw new Error('Cannot get selection');
const range = document.createRange();
range.setStart(
anchorRichTextRange.startContainer,
anchorRichTextRange.startOffset
);
range.setEnd(
focusRichTextRange.startContainer,
focusRichTextRange.startOffset
);
sl.removeAllRanges();
sl.addRange(range);
},
{
anchorBlockId,
anchorOffset,
focusBlockId,
focusOffset,
}
);
}