diff --git a/libs/components/affine-board/src/Board.tsx b/libs/components/affine-board/src/Board.tsx index 82c2948c49..6192ac3d74 100644 --- a/libs/components/affine-board/src/Board.tsx +++ b/libs/components/affine-board/src/Board.tsx @@ -5,7 +5,10 @@ import { getSession } from '@toeverything/components/board-sessions'; import { deepCopy, TldrawApp } from '@toeverything/components/board-state'; import { tools } from '@toeverything/components/board-tools'; import { TDShapeType } from '@toeverything/components/board-types'; -import { RecastBlockProvider } from '@toeverything/components/editor-core'; +import { + RecastBlockProvider, + getClipDataOfBlocksById, +} from '@toeverything/components/editor-core'; import { services } from '@toeverything/datasource/db-service'; import { AsyncBlock, BlockEditor } from '@toeverything/framework/virgo'; import { useEffect, useState } from 'react'; @@ -16,7 +19,11 @@ interface AffineBoardProps { rootBlockId: string; } -const AffineBoard = ({ workspace, rootBlockId }: AffineBoardProps) => { +const AffineBoard = ({ + workspace, + rootBlockId, + editor, +}: AffineBoardProps & { editor: BlockEditor }) => { const [app, set_app] = useState(); const [document] = useState(() => { @@ -62,6 +69,14 @@ const AffineBoard = ({ workspace, rootBlockId }: AffineBoardProps) => { onMount(app) { set_app(app); }, + async onCopy(e, groupIds) { + const [mimeType, data] = await getClipDataOfBlocksById( + editor, + groupIds + ); + + e.clipboardData?.setData(mimeType, data); + }, onChangePage(app, shapes, bindings, assets) { Promise.all( Object.entries(shapes).map(async ([id, shape]) => { @@ -130,7 +145,11 @@ export const AffineBoardWitchContext = ({ }, [editor, rootBlockId]); return page ? ( - + ) : null; }; diff --git a/libs/components/board-state/src/tldraw-app.ts b/libs/components/board-state/src/tldraw-app.ts index 16d6edc1eb..2a4ba54433 100644 --- a/libs/components/board-state/src/tldraw-app.ts +++ b/libs/components/board-state/src/tldraw-app.ts @@ -171,6 +171,10 @@ interface TDCallbacks { * (optional) A callback to run when the user exports their page or selection. */ onExport?: (app: TldrawApp, info: TDExport) => Promise; + /** + * (optional) A callback to run when the shape is copied. + */ + onCopy?: (e: ClipboardEvent, ids: string[]) => void; } export interface TldrawAppCtorProps { @@ -1898,12 +1902,14 @@ export class TldrawApp extends StateManager { /** * Copy one or more shapes to the clipboard. * @param ids The ids of the shapes to copy. + * @param pageId + * @param e */ - copy = ( + copy = async ( ids = this.selectedIds, pageId = this.currentPageId, e?: ClipboardEvent - ): this => { + ) => { e?.preventDefault(); this.clipboard = this.get_clipboard(ids, pageId); @@ -1919,17 +1925,24 @@ export class TldrawApp extends StateManager { if (e) { e.clipboardData?.setData('text/html', tldrawString); + await this.callbacks.onCopy?.(e, this.selectedIds); } - if (navigator.clipboard && window.ClipboardItem) { - navigator.clipboard.write([ - new ClipboardItem({ - 'text/html': new Blob([tldrawString], { - type: 'text/html', - }), - }), - ]); - } + /** + * Reasons for not using Clipboard API for now: + * 1. The `clipboardData.setData` method temporarily satisfies the need for replication functionality + * 2. Clipboard API requires the user to agree to access(https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API) + * + * **/ + // if (navigator.clipboard && window.ClipboardItem) { + // navigator.clipboard.write([ + // new ClipboardItem({ + // 'text/html': new Blob([tldrawString], { + // type: 'text/html', + // }), + // }), + // ]); + // } this.pasteInfo.offset = [0, 0]; this.pasteInfo.center = [0, 0]; diff --git a/libs/components/editor-core/src/editor/clipboard/utils.ts b/libs/components/editor-core/src/editor/clipboard/utils.ts index cb5d10241e..465d2788b8 100644 --- a/libs/components/editor-core/src/editor/clipboard/utils.ts +++ b/libs/components/editor-core/src/editor/clipboard/utils.ts @@ -1,4 +1,6 @@ import { Editor } from '../editor'; +import { ClipBlockInfo, OFFICE_CLIPBOARD_MIMETYPE } from './types'; +import { Clip } from './clip'; export const shouldHandlerContinue = (event: Event, editor: Editor) => { const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA']; @@ -12,3 +14,38 @@ export const shouldHandlerContinue = (event: Event, editor: Editor) => { return editor.selectionManager.currentSelectInfo.type !== 'None'; }; + +export const getClipInfoOfBlockById = async ( + editor: Editor, + blockId: string +) => { + const block = await editor.getBlockById(blockId); + const blockView = editor.getView(block.type); + const blockInfo: ClipBlockInfo = { + type: block.type, + properties: blockView.getSelProperties(block, {}), + children: [] as ClipBlockInfo[], + }; + const children = await block?.children(); + + for (let i = 0; i < children.length; i++) { + const childInfo = await getClipInfoOfBlockById(editor, children[i].id); + blockInfo.children.push(childInfo); + } + return blockInfo; +}; + +export const getClipDataOfBlocksById = async ( + editor: Editor, + blockIds: string[] +) => { + const clipInfos = await Promise.all( + blockIds.map(blockId => getClipInfoOfBlockById(editor, blockId)) + ); + return [ + OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED, + JSON.stringify({ + data: clipInfos, + }), + ]; +}; diff --git a/libs/components/editor-core/src/editor/index.ts b/libs/components/editor-core/src/editor/index.ts index 03903d31bf..3131e6f0a1 100644 --- a/libs/components/editor-core/src/editor/index.ts +++ b/libs/components/editor-core/src/editor/index.ts @@ -10,3 +10,4 @@ export { BlockDropPlacement, HookType, GroupDirection } from './types'; export type { Plugin, PluginCreator, PluginHooks, Virgo } from './types'; export { BaseView, getTextHtml, getTextProperties } from './views/base-view'; export type { ChildrenView, CreateView } from './views/base-view'; +export { getClipDataOfBlocksById } from './clipboard/utils';