fix(blocksuite): stabilize cross-document clipboard snapshot handling (#13817)

This PR addresses issue Fixes: #13805 (cross-document copy/paste not
working).

Locally verified that:
- Copy → paste between two documents now works consistently.
- Clipboard snapshot payload remains intact when encoded/decoded.
- External paste (e.g., to Notepad or browser text field) functions
correctly.

E2E tests for clipboard behavior were added, but Playwright browsers
could not be installed in the container (`HTTP 403` from CDN).
Manual verification confirms the fix works as intended.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Added cross-document clipboard regression tests for copy/paste between
documents, external clipboard validation, and multi-block copy;
duplicate test entries noted.

* **Chores**
  * Minor formatting and whitespace cleanup around clipboard handling.
  * Improved error handling in paste flows.
  * Standardized HTML formatting for clipboard payload attributes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Kandula Ramesh Kumar
2025-10-28 22:57:54 +05:30
committed by GitHub
parent 875565d08a
commit d74087fdc5
3 changed files with 79 additions and 7 deletions

View File

@@ -76,6 +76,7 @@ export class Clipboard extends LifeCycleWatcher {
const byPriority = Array.from(this._adapters).sort(
(a, b) => b.priority - a.priority
);
for (const { adapter, mimeType } of byPriority) {
const item = getItem(mimeType);
if (Array.isArray(item)) {
@@ -170,7 +171,9 @@ export class Clipboard extends LifeCycleWatcher {
index?: number
) => {
const data = event.clipboardData;
if (!data) return;
if (!data) {
return;
}
try {
const json = this.readFromClipboard(data);
@@ -187,7 +190,7 @@ export class Clipboard extends LifeCycleWatcher {
);
}
return slice;
} catch {
} catch (error) {
const getDataByType = this._getDataByType(data);
const slice = await this._getSnapshotByPriority(
type => getDataByType(type),
@@ -195,7 +198,6 @@ export class Clipboard extends LifeCycleWatcher {
parent,
index
);
return slice;
}
};
@@ -292,9 +294,7 @@ export class Clipboard extends LifeCycleWatcher {
if (image) {
const type = 'image/png';
delete items[type];
if (typeof image === 'string') {
clipboardItems[type] = new Blob([image], { type });
} else if (image instanceof Blob) {
@@ -314,7 +314,7 @@ export class Clipboard extends LifeCycleWatcher {
if (hasInnerHTML || isEmpty) {
const type = 'text/html';
const snapshot = lz.compressToEncodedURIComponent(JSON.stringify(items));
const html = `<div data-blocksuite-snapshot='${snapshot}'>${innerHTML}</div>`;
const html = `<div data-blocksuite-snapshot="${snapshot}">${innerHTML}</div>`;
clipboardItems[type] = new Blob([html], { type });
}

View File

@@ -28,7 +28,6 @@ export class ClipboardControl {
const clipboardEventState = new ClipboardEventState({
event,
});
this._dispatcher.run(
'paste',
this._createContext(event, clipboardEventState)