Files
AFFiNE-Mirror/blocksuite/blocks/src/root-block/clipboard/index.ts

241 lines
6.7 KiB
TypeScript

import { deleteTextCommand } from '@blocksuite/affine-components/rich-text';
import {
AttachmentAdapter,
copyMiddleware,
HtmlAdapter,
ImageAdapter,
MixTextAdapter,
NotionTextAdapter,
pasteMiddleware,
} from '@blocksuite/affine-shared/adapters';
import {
clearAndSelectFirstModelCommand,
copySelectedModelsCommand,
deleteSelectedModelsCommand,
draftSelectedModelsCommand,
getBlockIndexCommand,
getBlockSelectionsCommand,
getImageSelectionsCommand,
getSelectedModelsCommand,
getTextSelectionCommand,
retainFirstModelCommand,
} from '@blocksuite/affine-shared/commands';
import type { BlockComponent, UIEventHandler } from '@blocksuite/block-std';
import { DisposableGroup } from '@blocksuite/global/utils';
import type { BlockSnapshot, Store } from '@blocksuite/store';
import {
defaultImageProxyMiddleware,
replaceIdMiddleware,
titleMiddleware,
} from '../../_common/transformers/middlewares.js';
import { ClipboardAdapter } from './adapter.js';
export class PageClipboard {
private readonly _copySelected = (onCopy?: () => void) => {
return this._std.command
.chain()
.with({ onCopy })
.pipe(getSelectedModelsCommand)
.pipe(draftSelectedModelsCommand)
.pipe(copySelectedModelsCommand);
};
protected _disposables = new DisposableGroup();
protected _init = () => {
this._std.clipboard.registerAdapter(
ClipboardAdapter.MIME,
ClipboardAdapter,
100
);
this._std.clipboard.registerAdapter(
'text/_notion-text-production',
NotionTextAdapter,
95
);
this._std.clipboard.registerAdapter('text/html', HtmlAdapter, 90);
[
'image/apng',
'image/avif',
'image/gif',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/webp',
].forEach(type =>
this._std.clipboard.registerAdapter(type, ImageAdapter, 80)
);
this._std.clipboard.registerAdapter('text/plain', MixTextAdapter, 70);
this._std.clipboard.registerAdapter('*/*', AttachmentAdapter, 60);
const copy = copyMiddleware(this._std);
const paste = pasteMiddleware(this._std);
this._std.clipboard.use(copy);
this._std.clipboard.use(paste);
this._std.clipboard.use(
replaceIdMiddleware(this._std.store.workspace.idGenerator)
);
this._std.clipboard.use(
titleMiddleware(this._std.store.workspace.meta.docMetas)
);
this._std.clipboard.use(defaultImageProxyMiddleware);
this._disposables.add({
dispose: () => {
this._std.clipboard.unregisterAdapter(ClipboardAdapter.MIME);
this._std.clipboard.unregisterAdapter('text/plain');
[
'image/apng',
'image/avif',
'image/gif',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/webp',
].forEach(type => this._std.clipboard.unregisterAdapter(type));
this._std.clipboard.unregisterAdapter('text/html');
this._std.clipboard.unregisterAdapter('*/*');
this._std.clipboard.unuse(copy);
this._std.clipboard.unuse(paste);
this._std.clipboard.unuse(
replaceIdMiddleware(this._std.store.workspace.idGenerator)
);
this._std.clipboard.unuse(
titleMiddleware(this._std.store.workspace.meta.docMetas)
);
this._std.clipboard.unuse(defaultImageProxyMiddleware);
},
});
};
host: BlockComponent;
onBlockSnapshotPaste = async (
snapshot: BlockSnapshot,
doc: Store,
parent?: string,
index?: number
) => {
const block = await this._std.clipboard.pasteBlockSnapshot(
snapshot,
doc,
parent,
index
);
return block?.id ?? null;
};
onPageCopy: UIEventHandler = ctx => {
const e = ctx.get('clipboardState').raw;
e.preventDefault();
this._copySelected().run();
};
onPageCut: UIEventHandler = ctx => {
const e = ctx.get('clipboardState').raw;
e.preventDefault();
this._copySelected(() => {
this._std.command
.chain()
.try<{}>(cmd => [
cmd.pipe(getTextSelectionCommand).pipe(deleteTextCommand),
cmd.pipe(getSelectedModelsCommand).pipe(deleteSelectedModelsCommand),
])
.run();
}).run();
};
onPagePaste: UIEventHandler = ctx => {
const e = ctx.get('clipboardState').raw;
e.preventDefault();
this._std.store.captureSync();
this._std.command
.chain()
.try(cmd => [
cmd.pipe(getTextSelectionCommand),
cmd
.pipe(getSelectedModelsCommand)
.pipe(clearAndSelectFirstModelCommand)
.pipe(retainFirstModelCommand)
.pipe(deleteSelectedModelsCommand),
])
.try<{ currentSelectionPath: string }>(cmd => [
cmd.pipe(getTextSelectionCommand).pipe((ctx, next) => {
const textSelection = ctx.currentTextSelection;
if (!textSelection) {
return;
}
next({ currentSelectionPath: textSelection.from.blockId });
}),
cmd.pipe(getBlockSelectionsCommand).pipe((ctx, next) => {
const currentBlockSelections = ctx.currentBlockSelections;
if (!currentBlockSelections) {
return;
}
const blockSelection = currentBlockSelections.at(-1);
if (!blockSelection) {
return;
}
next({ currentSelectionPath: blockSelection.blockId });
}),
cmd.pipe(getImageSelectionsCommand).pipe((ctx, next) => {
const currentImageSelections = ctx.currentImageSelections;
if (!currentImageSelections) {
return;
}
const imageSelection = currentImageSelections.at(-1);
if (!imageSelection) {
return;
}
next({ currentSelectionPath: imageSelection.blockId });
}),
])
.pipe(getBlockIndexCommand)
.pipe((ctx, next) => {
if (!ctx.parentBlock) {
return;
}
this._std.clipboard
.paste(
e,
this._std.store,
ctx.parentBlock.model.id,
ctx.blockIndex ? ctx.blockIndex + 1 : 1
)
.catch(console.error);
return next();
})
.run();
};
private get _std() {
return this.host.std;
}
constructor(host: BlockComponent) {
this.host = host;
}
hostConnected() {
if (this._disposables.disposed) {
this._disposables = new DisposableGroup();
}
if (navigator.clipboard) {
this.host.handleEvent('copy', this.onPageCopy);
this.host.handleEvent('paste', this.onPagePaste);
this.host.handleEvent('cut', this.onPageCut);
this._init();
}
}
hostDisconnected() {
this._disposables.dispose();
}
}
export { copyMiddleware, pasteMiddleware };