mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
fix(editor): paste to code block should delete selected text (#11546)
Close [BS-3064](https://linear.app/affine-design/issue/BS-3064/fix-bug-pasting-in-code-block-does-not-replace-text)
This commit is contained in:
@@ -124,6 +124,13 @@ class PasteTr {
|
|||||||
|
|
||||||
private readonly _mergeCode = () => {
|
private readonly _mergeCode = () => {
|
||||||
const deltas: DeltaOperation[] = [{ retain: this.pointState.point.index }];
|
const deltas: DeltaOperation[] = [{ retain: this.pointState.point.index }];
|
||||||
|
// if there is text selection, delete the text selected
|
||||||
|
if (this.pointState.text.length - this.pointState.point.index > 0) {
|
||||||
|
deltas.push({
|
||||||
|
delete: this.pointState.text.length - this.pointState.point.index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// paste the text from the snapshot to code block
|
||||||
this.snapshot.content.forEach((blockSnapshot, i) => {
|
this.snapshot.content.forEach((blockSnapshot, i) => {
|
||||||
if (blockSnapshot.props.text) {
|
if (blockSnapshot.props.text) {
|
||||||
const text = this._textFromSnapshot(blockSnapshot);
|
const text = this._textFromSnapshot(blockSnapshot);
|
||||||
@@ -133,6 +140,11 @@ class PasteTr {
|
|||||||
deltas.push(...text.delta);
|
deltas.push(...text.delta);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// paste the text after the text selection from the snapshot to code block
|
||||||
|
const { toDelta } = this._getDeltas();
|
||||||
|
if (toDelta.length > 0) {
|
||||||
|
deltas.push(...toDelta);
|
||||||
|
}
|
||||||
this.pointState.text.applyDelta(deltas);
|
this.pointState.text.applyDelta(deltas);
|
||||||
this.snapshot.content = [];
|
this.snapshot.content = [];
|
||||||
};
|
};
|
||||||
@@ -490,13 +502,13 @@ class PasteTr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
merge() {
|
merge() {
|
||||||
if (this.pointState.model.flavour === 'affine:code') {
|
if (this.firstSnapshot === this.lastSnapshot) {
|
||||||
this._mergeCode();
|
this._mergeSingle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.firstSnapshot === this.lastSnapshot) {
|
if (this.pointState.model.flavour === 'affine:code') {
|
||||||
this._mergeSingle();
|
this._mergeCode();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,20 @@ import {
|
|||||||
} from '@affine-test/kit/utils/keyboard';
|
} from '@affine-test/kit/utils/keyboard';
|
||||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
import {
|
import {
|
||||||
|
addCodeBlock,
|
||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
getBlockSuiteEditorTitle,
|
getBlockSuiteEditorTitle,
|
||||||
type,
|
type,
|
||||||
waitForEditorLoad,
|
waitForEditorLoad,
|
||||||
} from '@affine-test/kit/utils/page-logic';
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
import { setSelection } from '@affine-test/kit/utils/selection';
|
import { setSelection } from '@affine-test/kit/utils/selection';
|
||||||
|
import type { CodeBlockComponent } from '@blocksuite/affine-block-code';
|
||||||
import type { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
|
import type { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
|
||||||
|
import type { BlockComponent } from '@blocksuite/std';
|
||||||
import { expect, type Page } from '@playwright/test';
|
import { expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
const paragraphLocator = 'affine-note affine-paragraph';
|
const paragraphLocator = 'affine-note affine-paragraph';
|
||||||
|
const codeBlockLocator = 'affine-note affine-code';
|
||||||
|
|
||||||
// Helper function to create paragraph blocks with text
|
// Helper function to create paragraph blocks with text
|
||||||
async function createParagraphBlocks(page: Page, texts: string[]) {
|
async function createParagraphBlocks(page: Page, texts: string[]) {
|
||||||
@@ -31,32 +35,75 @@ async function createParagraphBlocks(page: Page, texts: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to verify paragraph text content
|
// Helper function to verify block text content
|
||||||
async function verifyParagraphContent(
|
async function verifyBlockContent<T extends BlockComponent>(
|
||||||
page: Page,
|
page: Page,
|
||||||
|
selector: string,
|
||||||
index: number,
|
index: number,
|
||||||
expectedText: string
|
expectedText: string
|
||||||
) {
|
) {
|
||||||
expect(
|
expect(
|
||||||
await page
|
await page
|
||||||
.locator(paragraphLocator)
|
.locator(selector)
|
||||||
.nth(index)
|
.nth(index)
|
||||||
.evaluate(
|
.evaluate((block: T, expected: string) => {
|
||||||
(block: ParagraphBlockComponent, expected: string) =>
|
const model = block.model;
|
||||||
block.model.props.type === 'text' &&
|
// Check if model has text property
|
||||||
block.model.props.text.toString() === expected,
|
if (!('text' in model)) return false;
|
||||||
expectedText
|
const text = model.text;
|
||||||
)
|
// Check if text exists and has toString method
|
||||||
|
return text && text.toString() === expected;
|
||||||
|
}, expectedText)
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get paragraph block ids
|
// Helper functions using the generic verifyBlockContent
|
||||||
async function getParagraphIds(page: Page) {
|
async function verifyParagraphContent(
|
||||||
const paragraph = page.locator(paragraphLocator);
|
page: Page,
|
||||||
const paragraphIds = await paragraph.evaluateAll(
|
index: number,
|
||||||
(blocks: ParagraphBlockComponent[]) => blocks.map(block => block.model.id)
|
expectedText: string
|
||||||
|
) {
|
||||||
|
await verifyBlockContent<ParagraphBlockComponent>(
|
||||||
|
page,
|
||||||
|
paragraphLocator,
|
||||||
|
index,
|
||||||
|
expectedText
|
||||||
);
|
);
|
||||||
return { paragraphIds };
|
}
|
||||||
|
|
||||||
|
async function verifyCodeBlockContent(
|
||||||
|
page: Page,
|
||||||
|
index: number,
|
||||||
|
expectedText: string
|
||||||
|
) {
|
||||||
|
await verifyBlockContent<CodeBlockComponent>(
|
||||||
|
page,
|
||||||
|
codeBlockLocator,
|
||||||
|
index,
|
||||||
|
expectedText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get block ids
|
||||||
|
async function getBlockIds<T extends BlockComponent>(
|
||||||
|
page: Page,
|
||||||
|
selector: string
|
||||||
|
) {
|
||||||
|
const blocks = page.locator(selector);
|
||||||
|
const blockIds = await blocks.evaluateAll((blocks: T[]) =>
|
||||||
|
blocks.map(block => block.model.id)
|
||||||
|
);
|
||||||
|
return { blockIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions using the generic getBlockIds
|
||||||
|
async function getParagraphIds(page: Page) {
|
||||||
|
return getBlockIds<ParagraphBlockComponent>(page, paragraphLocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions using the generic getBlockIds
|
||||||
|
async function getCodeBlockIds(page: Page) {
|
||||||
|
return getBlockIds<CodeBlockComponent>(page, codeBlockLocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
@@ -70,7 +117,7 @@ test.describe('paste in multiple blocks text selection', () => {
|
|||||||
const texts = ['hello world', 'hello world', 'hello world'];
|
const texts = ['hello world', 'hello world', 'hello world'];
|
||||||
await createParagraphBlocks(page, texts);
|
await createParagraphBlocks(page, texts);
|
||||||
|
|
||||||
const { paragraphIds } = await getParagraphIds(page);
|
const { blockIds: paragraphIds } = await getParagraphIds(page);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* select text cross the 3 blocks:
|
* select text cross the 3 blocks:
|
||||||
@@ -108,7 +155,7 @@ test.describe('paste in multiple blocks text selection', () => {
|
|||||||
* hello world
|
* hello world
|
||||||
* hello world
|
* hello world
|
||||||
*/
|
*/
|
||||||
const { paragraphIds } = await getParagraphIds(page);
|
const { blockIds: paragraphIds } = await getParagraphIds(page);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* select the first 2 blocks:
|
* select the first 2 blocks:
|
||||||
@@ -205,3 +252,75 @@ test('paste surface-ref block to another doc as embed-linked-doc block', async (
|
|||||||
);
|
);
|
||||||
await expect(embedLinkedDocBlockTitle).toHaveText('Clipboard Test');
|
await expect(embedLinkedDocBlockTitle).toHaveText('Clipboard Test');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('paste to code block', () => {
|
||||||
|
test('should replace the selected text when pasting plain text', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// press enter to focus on the first block of the editor
|
||||||
|
await pressEnter(page);
|
||||||
|
await addCodeBlock(page);
|
||||||
|
await type(page, 'hello world hello');
|
||||||
|
const { blockIds: codeBlockIds } = await getCodeBlockIds(page);
|
||||||
|
await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11);
|
||||||
|
await pasteContent(page, { 'text/plain': 'test' });
|
||||||
|
await verifyCodeBlockContent(page, 0, 'hello test hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should replace the selected text when pasting single snapshot', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// Create initial test blocks
|
||||||
|
// add a paragraph block
|
||||||
|
await createParagraphBlocks(page, ['test']);
|
||||||
|
// add a code block
|
||||||
|
await pressEnter(page);
|
||||||
|
await addCodeBlock(page);
|
||||||
|
await type(page, 'hello world hello');
|
||||||
|
|
||||||
|
// select the paragraph content
|
||||||
|
const { blockIds: paragraphIds } = await getParagraphIds(page);
|
||||||
|
await setSelection(page, paragraphIds[0], 0, paragraphIds[0], 4);
|
||||||
|
// copy the paragraph content
|
||||||
|
await copyByKeyboard(page);
|
||||||
|
|
||||||
|
// select 'world' in the code block
|
||||||
|
const { blockIds: codeBlockIds } = await getCodeBlockIds(page);
|
||||||
|
await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11);
|
||||||
|
|
||||||
|
// paste to the code block
|
||||||
|
await pasteByKeyboard(page);
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
|
||||||
|
await verifyCodeBlockContent(page, 0, 'hello test hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should replace the selected text when pasting multiple snapshots', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// Create initial test blocks
|
||||||
|
// add three paragraph blocks
|
||||||
|
await createParagraphBlocks(page, ['test', 'test', 'test']);
|
||||||
|
|
||||||
|
// add a code block
|
||||||
|
await pressEnter(page);
|
||||||
|
await addCodeBlock(page);
|
||||||
|
await type(page, 'hello world hello');
|
||||||
|
|
||||||
|
// select all paragraph content
|
||||||
|
const { blockIds: paragraphIds } = await getParagraphIds(page);
|
||||||
|
await setSelection(page, paragraphIds[0], 0, paragraphIds[2], 4);
|
||||||
|
// copy the paragraph content
|
||||||
|
await copyByKeyboard(page);
|
||||||
|
|
||||||
|
// select 'world' in the code block
|
||||||
|
const { blockIds: codeBlockIds } = await getCodeBlockIds(page);
|
||||||
|
await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11);
|
||||||
|
|
||||||
|
// paste to the code block
|
||||||
|
await pasteByKeyboard(page);
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
|
||||||
|
await verifyCodeBlockContent(page, 0, 'hello test\ntest\ntest hello');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user