diff --git a/blocksuite/blocks/src/root-block/clipboard/index.ts b/blocksuite/blocks/src/root-block/clipboard/index.ts index 3c54ed3422..f29386609a 100644 --- a/blocksuite/blocks/src/root-block/clipboard/index.ts +++ b/blocksuite/blocks/src/root-block/clipboard/index.ts @@ -1,240 +1,2 @@ -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 }; +export * from './page-clipboard.js'; +export * from './readonly-clipboard.js'; diff --git a/blocksuite/blocks/src/root-block/clipboard/page-clipboard.ts b/blocksuite/blocks/src/root-block/clipboard/page-clipboard.ts new file mode 100644 index 0000000000..6d803a453d --- /dev/null +++ b/blocksuite/blocks/src/root-block/clipboard/page-clipboard.ts @@ -0,0 +1,133 @@ +import { deleteTextCommand } from '@blocksuite/affine-components/rich-text'; +import { + clearAndSelectFirstModelCommand, + deleteSelectedModelsCommand, + getBlockIndexCommand, + getBlockSelectionsCommand, + getImageSelectionsCommand, + getSelectedModelsCommand, + getTextSelectionCommand, + retainFirstModelCommand, +} from '@blocksuite/affine-shared/commands'; +import type { UIEventHandler } from '@blocksuite/block-std'; +import { DisposableGroup } from '@blocksuite/global/utils'; +import type { BlockSnapshot, Store } from '@blocksuite/store'; + +import { ReadOnlyClipboard } from './readonly-clipboard'; + +/** + * PageClipboard is a class that provides a clipboard for the page root block. + * It is supported to copy and paste models in the page root block. + */ +export class PageClipboard extends ReadOnlyClipboard { + protected _init = () => { + this._initAdapters(); + }; + + 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; + }; + + 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(); + }; + + override 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(); + } + } +} diff --git a/blocksuite/blocks/src/root-block/clipboard/readonly-clipboard.ts b/blocksuite/blocks/src/root-block/clipboard/readonly-clipboard.ts new file mode 100644 index 0000000000..f031f8f02c --- /dev/null +++ b/blocksuite/blocks/src/root-block/clipboard/readonly-clipboard.ts @@ -0,0 +1,138 @@ +import { + AttachmentAdapter, + copyMiddleware, + HtmlAdapter, + ImageAdapter, + MixTextAdapter, + NotionTextAdapter, + pasteMiddleware, +} from '@blocksuite/affine-shared/adapters'; +import { + copySelectedModelsCommand, + draftSelectedModelsCommand, + getSelectedModelsCommand, +} from '@blocksuite/affine-shared/commands'; +import type { BlockComponent, UIEventHandler } from '@blocksuite/block-std'; +import { DisposableGroup } from '@blocksuite/global/utils'; + +import { + defaultImageProxyMiddleware, + replaceIdMiddleware, + titleMiddleware, +} from '../../_common/transformers/middlewares.js'; +import { ClipboardAdapter } from './adapter.js'; + +/** + * ReadOnlyClipboard is a class that provides a read-only clipboard for the root block. + * It is supported to copy models in the root block. + */ +export class ReadOnlyClipboard { + protected readonly _copySelected = (onCopy?: () => void) => { + return this._std.command + .chain() + .with({ onCopy }) + .pipe(getSelectedModelsCommand) + .pipe(draftSelectedModelsCommand) + .pipe(copySelectedModelsCommand); + }; + + protected _disposables = new DisposableGroup(); + + protected _initAdapters = () => { + 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; + + onPageCopy: UIEventHandler = ctx => { + const e = ctx.get('clipboardState').raw; + e.preventDefault(); + + this._copySelected().run(); + }; + + protected 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._initAdapters(); + } + } + + hostDisconnected() { + this._disposables.dispose(); + } +} + +export { copyMiddleware, pasteMiddleware }; diff --git a/blocksuite/blocks/src/root-block/preview/preview-root-block.ts b/blocksuite/blocks/src/root-block/preview/preview-root-block.ts index 5db1df59f0..bad0402277 100644 --- a/blocksuite/blocks/src/root-block/preview/preview-root-block.ts +++ b/blocksuite/blocks/src/root-block/preview/preview-root-block.ts @@ -1,7 +1,8 @@ -// import { PageRootBlockComponent } from '../page/page-root-block.js'; import { BlockComponent } from '@blocksuite/block-std'; import { css, html } from 'lit'; +import { ReadOnlyClipboard } from '../clipboard/readonly-clipboard'; + export class PreviewRootBlockComponent extends BlockComponent { static override styles = css` affine-preview-root { @@ -9,6 +10,18 @@ export class PreviewRootBlockComponent extends BlockComponent { } `; + clipboardController = new ReadOnlyClipboard(this); + + override connectedCallback() { + super.connectedCallback(); + this.clipboardController.hostConnected(); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + this.clipboardController.hostDisconnected(); + } + override renderBlock() { return html`
${this.host.renderChildren(this.model)}