mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-26 10:45:57 +08:00
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:
@@ -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)
|
||||||
|
|||||||
143
tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts
Normal file
143
tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
24
tests/kit/src/utils/clipboard.ts
Normal file
24
tests/kit/src/utils/clipboard.ts
Normal 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);
|
||||||
|
}
|
||||||
55
tests/kit/src/utils/selection.ts
Normal file
55
tests/kit/src/utils/selection.ts
Normal 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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user