From df057b4c122680ff37a7bc13824c1c5c6921cd6f Mon Sep 17 00:00:00 2001 From: Saul-Mirone Date: Tue, 25 Mar 2025 12:09:23 +0000 Subject: [PATCH] feat(editor): edgeless clipboard config extension (#11168) --- .../src/edgeless/clipboard/clipboard.ts | 499 ++---------------- .../src/edgeless/clipboard/config.ts | 435 +++++++++++++++ .../src/edgeless/edgeless-root-spec.ts | 34 ++ .../src/extensions/clipboard-config.ts | 80 +++ .../block-surface/src/extensions/index.ts | 1 + .../block-std/src/__tests__/hast.unit.spec.ts | 2 +- .../block-std/src/clipboard/index.ts | 34 +- .../block-std/src/clipboard/utils.ts | 33 ++ .../block-suite-editor/lit-adaper.tsx | 4 +- .../extensions/edgeless-clipboard.ts | 58 +- 10 files changed, 648 insertions(+), 532 deletions(-) create mode 100644 blocksuite/affine/blocks/block-root/src/edgeless/clipboard/config.ts create mode 100644 blocksuite/affine/blocks/block-surface/src/extensions/clipboard-config.ts create mode 100644 blocksuite/framework/block-std/src/clipboard/utils.ts diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/clipboard/clipboard.ts b/blocksuite/affine/blocks/block-root/src/edgeless/clipboard/clipboard.ts index 8df3547e74..b72f2fa35a 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/clipboard/clipboard.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/clipboard/clipboard.ts @@ -1,9 +1,13 @@ import { addAttachments } from '@blocksuite/affine-block-attachment'; +import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame'; import { addImages } from '@blocksuite/affine-block-image'; import { CanvasElementType, + type ClipboardConfigCreationContext, + EdgelessClipboardConfigIdentifier, EdgelessCRUDIdentifier, ExportManager, + getSurfaceComponent, SurfaceGroupLikeModel, TextUtils, } from '@blocksuite/affine-block-surface'; @@ -14,7 +18,6 @@ import { DEFAULT_NOTE_WIDTH, FrameBlockModel, MAX_IMAGE_WIDTH, - ReferenceInfoSchema, } from '@blocksuite/affine-model'; import { CANVAS_EXPORT_IGNORE_TAGS, @@ -34,6 +37,7 @@ import { referenceToNode, } from '@blocksuite/affine-shared/utils'; import type { + BlockComponent, BlockStdScope, EditorHost, SurfaceSelection, @@ -63,7 +67,6 @@ import { assertType } from '@blocksuite/global/utils'; import { type BlockSnapshot, BlockSnapshotSchema, - fromJSON, type SliceSnapshot, } from '@blocksuite/store'; import DOMPurify from 'dompurify'; @@ -75,15 +78,12 @@ import { decodeClipboardBlobs, encodeClipboardBlobs, } from '../../clipboard/utils.js'; -import type { RootBlockComponent } from '../../types.js'; -import type { EdgelessRootBlockComponent } from '../edgeless-root-block.js'; import { edgelessElementsBoundFromRawData } from '../utils/bound-utils.js'; import { createNewPresentationIndexes } from '../utils/clipboard-utils.js'; import { getSortedCloneElements, serializeElement, } from '../utils/clone-utils.js'; -import { deleteElements } from '../utils/crud.js'; import { isAttachmentBlock, isCanvasElementWithText, @@ -95,41 +95,13 @@ const BLOCKSUITE_SURFACE = 'blocksuite/surface'; const { GROUP, MINDMAP, CONNECTOR } = CanvasElementType; const IMAGE_PADDING = 5; // for rotated shapes some padding is needed -type CreationContext = { - /** - * element old id to new id - */ - oldToNewIdMap: Map; - /** - * element old id to new layer index - */ - originalIndexes: Map; - - /** - * frame old id to new presentation index - */ - newPresentationIndexes: Map; -}; - -type BlockCreationFunction = ( - snapshot: BlockSnapshot, - context: CreationContext -) => Promise | string | null; // new Id - interface CanvasExportOptions { dpr?: number; padding?: number; background?: string; } -interface BlockConfig { - flavour: string; - createFunction: BlockCreationFunction; -} - export class EdgelessClipboardController extends PageClipboard { - private readonly _blockConfigs: BlockConfig[] = []; - private readonly _initEdgelessClipboard = () => { this.host.handleEvent( 'copy', @@ -210,7 +182,7 @@ export class EdgelessClipboardController extends PageClipboard { this.selectionManager.selectedElements ); this.doc.transact(() => { - deleteElements(this.edgeless, elements); + this.crud.deleteElements(elements); }); this.selectionManager.set({ @@ -241,6 +213,8 @@ export class EdgelessClipboardController extends PageClipboard { const data = event.clipboardData; if (!data) return; + if (!this.surface) return; + const lastMousePos = this.toolManager.lastMousePos$.peek(); const point: IVec = [lastMousePos.x, lastMousePos.y]; @@ -283,7 +257,7 @@ export class EdgelessClipboardController extends PageClipboard { if (isUrlInClipboard(data)) { const url = data.getData('text/plain'); const lastMousePos = this.toolManager.lastMousePos$.peek(); - const [x, y] = this.host.service.viewport.toModelCoord( + const [x, y] = this.gfx.viewport.toModelCoord( lastMousePos.x, lastMousePos.y ); @@ -407,7 +381,7 @@ export class EdgelessClipboardController extends PageClipboard { } private get selectionManager() { - return this.std.get(GfxControllerIdentifier).selection; + return this.gfx.selection; } private get std() { @@ -415,7 +389,15 @@ export class EdgelessClipboardController extends PageClipboard { } private get surface() { - return this.host.surface; + return getSurfaceComponent(this.std); + } + + private get frame() { + return this.std.get(EdgelessFrameManagerIdentifier); + } + + private get gfx() { + return this.std.get(GfxControllerIdentifier); } private get crud() { @@ -423,36 +405,7 @@ export class EdgelessClipboardController extends PageClipboard { } private get toolManager() { - return this.host.gfx.tool; - } - - constructor(public override host: EdgelessRootBlockComponent) { - super(host); - // Register existing block creation functions - this.registerBlock('affine:note', this._createNoteBlock); - this.registerBlock('affine:edgeless-text', this._createEdgelessTextBlock); - this.registerBlock('affine:image', this._createImageBlock); - this.registerBlock('affine:frame', this._createFrameBlock); - this.registerBlock('affine:attachment', this._createAttachmentBlock); - - // external links - this.registerBlock('affine:bookmark', this._createBookmarkBlock); - this.registerBlock('affine:embed-figma', this._createFigmaEmbedBlock); - this.registerBlock('affine:embed-github', this._createGithubEmbedBlock); - this.registerBlock('affine:embed-html', this._createHtmlEmbedBlock); - this.registerBlock('affine:embed-loom', this._createLoomEmbedBlock); - this.registerBlock('affine:embed-youtube', this._createYoutubeEmbedBlock); - this.registerBlock('affine:embed-iframe', this._createIframeEmbedBlock); - - // internal links - this.registerBlock( - 'affine:embed-linked-doc', - this._createLinkedDocEmbedBlock - ); - this.registerBlock( - 'affine:embed-synced-doc', - this._createSyncedDocEmbedBlock - ); + return this.gfx.tool; } private _checkCanContinueToCanvas( @@ -468,54 +421,9 @@ export class EdgelessClipboardController extends PageClipboard { } } - private async _createAttachmentBlock(attachment: BlockSnapshot) { - const { xywh, rotate, sourceId, name, size, type, embed, style } = - attachment.props; - - if (!(await this.host.std.workspace.blobSync.get(sourceId as string))) { - return null; - } - const attachmentId = this.crud.addBlock( - 'affine:attachment', - { - xywh, - rotate, - sourceId, - name, - size, - type, - embed, - style, - }, - this.surface.model.id - ); - return attachmentId; - } - - private _createBookmarkBlock(bookmark: BlockSnapshot) { - const { xywh, style, url, caption, description, icon, image, title } = - bookmark.props; - - const bookmarkId = this.crud.addBlock( - 'affine:bookmark', - { - xywh, - style, - url, - caption, - description, - icon, - image, - title, - }, - this.surface.model.id - ); - return bookmarkId; - } - private _createCanvasElement( clipboardData: SerializedElement, - context: CreationContext, + context: ClipboardConfigCreationContext, newXYWH: SerializedXYWH ): GfxPrimitiveElementModel | null { if (clipboardData.type === GROUP) { @@ -614,307 +522,8 @@ export class EdgelessClipboardController extends PageClipboard { return element; } - private async _createEdgelessTextBlock(edgelessText: BlockSnapshot) { - const oldId = edgelessText.id; - delete edgelessText.props.index; - if (!edgelessText.props.xywh) { - console.error( - `EdgelessText block(id: ${oldId}) does not have xywh property` - ); - return null; - } - const newId = await this.onBlockSnapshotPaste( - edgelessText, - this.doc, - this.edgeless.surface.model.id - ); - if (!newId) { - console.error(`Failed to paste EdgelessText block(id: ${oldId})`); - return null; - } - - return newId; - } - - private _createFigmaEmbedBlock(figmaEmbed: BlockSnapshot) { - const { xywh, style, url, caption, title, description } = figmaEmbed.props; - - const embedFigmaId = this.crud.addBlock( - 'affine:embed-figma', - { - xywh, - style, - url, - caption, - title, - description, - }, - this.surface.model.id - ); - return embedFigmaId; - } - - private _createFrameBlock(frame: BlockSnapshot, context: CreationContext) { - const { oldToNewIdMap, newPresentationIndexes } = context; - const { xywh, title, background, childElementIds } = frame.props; - - const newChildElementIds: Record = {}; - - if (typeof childElementIds === 'object' && childElementIds !== null) { - Object.keys(childElementIds).forEach(oldId => { - const newId = oldToNewIdMap.get(oldId); - if (newId) { - newChildElementIds[newId] = true; - } - }); - } - - const frameId = this.crud.addBlock( - 'affine:frame', - { - xywh, - background, - title: fromJSON(title), - childElementIds: newChildElementIds, - presentationIndex: newPresentationIndexes.get(frame.id), - }, - this.surface.model.id - ); - return frameId; - } - - private _createGithubEmbedBlock(githubEmbed: BlockSnapshot) { - const { - xywh, - style, - owner, - repo, - githubType, - githubId, - url, - caption, - image, - status, - statusReason, - title, - description, - createdAt, - assignees, - } = githubEmbed.props; - - const embedGithubId = this.crud.addBlock( - 'affine:embed-github', - { - xywh, - style, - owner, - repo, - githubType, - githubId, - url, - caption, - image, - status, - statusReason, - title, - description, - createdAt, - assignees, - }, - this.surface.model.id - ); - return embedGithubId; - } - - private _createHtmlEmbedBlock(htmlEmbed: BlockSnapshot) { - const { xywh, style, caption, html, design } = htmlEmbed.props; - - const embedHtmlId = this.crud.addBlock( - 'affine:embed-html', - { - xywh, - style, - caption, - html, - design, - }, - this.surface.model.id - ); - return embedHtmlId; - } - - private async _createImageBlock(image: BlockSnapshot) { - const { xywh, rotate, sourceId, size, width, height, caption } = - image.props; - - if (!(await this.host.std.workspace.blobSync.get(sourceId as string))) { - return null; - } - return this.crud.addBlock( - 'affine:image', - { - caption, - sourceId, - xywh, - rotate, - size, - width, - height, - }, - this.surface.model.id - ); - } - - private _createLinkedDocEmbedBlock(linkedDocEmbed: BlockSnapshot) { - const { xywh, style, caption, pageId, params, title, description } = - linkedDocEmbed.props; - const referenceInfo = ReferenceInfoSchema.parse({ - pageId, - params, - title, - description, - }); - - return this.crud.addBlock( - 'affine:embed-linked-doc', - { - xywh, - style, - caption, - ...referenceInfo, - }, - this.surface.model.id - ); - } - - private _createLoomEmbedBlock(loomEmbed: BlockSnapshot) { - const { xywh, style, url, caption, videoId, image, title, description } = - loomEmbed.props; - - const embedLoomId = this.crud.addBlock( - 'affine:embed-loom', - { - xywh, - style, - url, - caption, - videoId, - image, - title, - description, - }, - this.surface.model.id - ); - return embedLoomId; - } - - private async _createNoteBlock(note: BlockSnapshot) { - const oldId = note.id; - - delete note.props.index; - if (!note.props.xywh) { - console.error(`Note block(id: ${oldId}) does not have xywh property`); - return null; - } - - const newId = await this.onBlockSnapshotPaste( - note, - this.doc, - this.doc.root!.id - ); - if (!newId) { - console.error(`Failed to paste note block(id: ${oldId})`); - return null; - } - - return newId; - } - - private _createSyncedDocEmbedBlock(syncedDocEmbed: BlockSnapshot) { - const { xywh, style, caption, scale, pageId, params } = - syncedDocEmbed.props; - const referenceInfo = ReferenceInfoSchema.parse({ pageId, params }); - - return this.crud.addBlock( - 'affine:embed-synced-doc', - { - xywh, - style, - caption, - scale, - ...referenceInfo, - }, - this.surface.model.id - ); - } - - private _createYoutubeEmbedBlock(youtubeEmbed: BlockSnapshot) { - const { - xywh, - style, - url, - caption, - videoId, - image, - title, - description, - creator, - creatorUrl, - creatorImage, - } = youtubeEmbed.props; - - const embedYoutubeId = this.crud.addBlock( - 'affine:embed-youtube', - { - xywh, - style, - url, - caption, - videoId, - image, - title, - description, - creator, - creatorUrl, - creatorImage, - }, - this.surface.model.id - ); - return embedYoutubeId; - } - - private _createIframeEmbedBlock(embedIframe: BlockSnapshot) { - const { - xywh, - caption, - url, - title, - description, - iframeUrl, - scale, - width, - height, - } = embedIframe.props; - - return this.crud.addBlock( - 'affine:embed-iframe', - { - url, - iframeUrl, - xywh, - caption, - title, - description, - scale, - width, - height, - }, - this.surface.model.id - ); - } - private async _edgelessToCanvas( - edgeless: EdgelessRootBlockComponent, + edgeless: BlockComponent, bound: IBound, nodes?: GfxBlockElementModel[], canvasElements: GfxPrimitiveElementModel[] = [], @@ -930,18 +539,11 @@ export class EdgelessClipboardController extends PageClipboard { const html2canvas = (await import('html2canvas')).default; if (!(html2canvas instanceof Function)) return; + if (!this.surface) return; const pathname = location.pathname; const editorMode = isInsidePageEditor(host); - const rootComponent = getRootByEditorHost(host); - if (!rootComponent) return; - - const container = rootComponent.querySelector( - '.affine-block-children-container' - ); - if (!container) return; - const canvas = document.createElement('canvas'); canvas.width = (bound.w + padding * 2) * dpr; canvas.height = (bound.h + padding * 2) * dpr; @@ -1024,7 +626,7 @@ export class EdgelessClipboardController extends PageClipboard { const nodeElements = nodes ?? - (edgeless.service.gfx.getElementsByBound(bound, { + (this.gfx.getElementsByBound(bound, { type: 'block', }) as GfxBlockElementModel[]); for (const nodeElement of nodeElements) { @@ -1032,15 +634,13 @@ export class EdgelessClipboardController extends PageClipboard { if (matchModels(nodeElement, [FrameBlockModel])) { const blocksInsideFrame: GfxBlockElementModel[] = []; - this.edgeless.service.frame - .getElementsInFrameBound(nodeElement, false) - .forEach(ele => { - if (isTopLevelBlock(ele)) { - blocksInsideFrame.push(ele as GfxBlockElementModel); - } else { - canvasElements.push(ele as GfxPrimitiveElementModel); - } - }); + this.frame.getElementsInFrameBound(nodeElement, false).forEach(ele => { + if (isTopLevelBlock(ele)) { + blocksInsideFrame.push(ele as GfxBlockElementModel); + } else { + canvasElements.push(ele as GfxPrimitiveElementModel); + } + }); // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < blocksInsideFrame.length; i++) { @@ -1052,7 +652,7 @@ export class EdgelessClipboardController extends PageClipboard { this._checkCanContinueToCanvas(host, pathname, editorMode); } - const surfaceCanvas = edgeless.surface.renderer.getCanvasByBound( + const surfaceCanvas = this.surface.renderer.getCanvasByBound( bound, canvasElements ); @@ -1114,9 +714,8 @@ export class EdgelessClipboardController extends PageClipboard { } private async _pasteTextContentAsNote(content: BlockSnapshot[] | string) { - const edgeless = this.host; const lastMousePos = this.toolManager.lastMousePos$.peek(); - const [x, y] = edgeless.service.viewport.toModelCoord( + const [x, y] = this.gfx.viewport.toModelCoord( lastMousePos.x, lastMousePos.y ); @@ -1167,11 +766,11 @@ export class EdgelessClipboardController extends PageClipboard { } } - edgeless.service.selection.set({ + this.gfx.selection.set({ elements: [noteId], editing: false, }); - edgeless.gfx.tool.setTool('default'); + this.gfx.tool.setTool('default'); } private _replaceRichTextWithSvgElement(element: HTMLElement) { @@ -1221,7 +820,7 @@ export class EdgelessClipboardController extends PageClipboard { } } - const idxGenerator = this.edgeless.service.layer.createIndexGenerator(); + const idxGenerator = this.gfx.layer.createIndexGenerator(); const sortedElements = elements.sort(compare); sortedElements.forEach(ele => { const newIndex = idxGenerator(); @@ -1250,7 +849,7 @@ export class EdgelessClipboardController extends PageClipboard { const lastMousePos = this.toolManager.lastMousePos$.peek(); pasteCenter = pasteCenter ?? - this.host.service.viewport.toModelCoord(lastMousePos.x, lastMousePos.y); + this.gfx.viewport.toModelCoord(lastMousePos.x, lastMousePos.y); const [modelX, modelY] = pasteCenter; oldCommonBound = edgelessElementsBoundFromRawData(elementsRawData); @@ -1270,7 +869,7 @@ export class EdgelessClipboardController extends PageClipboard { // create blocks and canvas elements - const context: CreationContext = { + const context: ClipboardConfigCreationContext = { oldToNewIdMap: new Map(), originalIndexes: new Map(), newPresentationIndexes: createNewPresentationIndexes( @@ -1288,8 +887,8 @@ export class EdgelessClipboardController extends PageClipboard { if (blockSnapshot) { const oldId = blockSnapshot.id; - const config = this._blockConfigs.find( - ({ flavour }) => flavour === blockSnapshot.flavour + const config = this.std.getOptional( + EdgelessClipboardConfigIdentifier(blockSnapshot.flavour) ); if (!config) continue; @@ -1311,7 +910,7 @@ export class EdgelessClipboardController extends PageClipboard { ); blockSnapshot.props.lockedBySelf = false; - const newId = await config.createFunction(blockSnapshot, context); + const newId = await config.createBlock(blockSnapshot, context); if (!newId) continue; const block = this.doc.getBlock(newId); @@ -1369,13 +968,6 @@ export class EdgelessClipboardController extends PageClipboard { this._initEdgelessClipboard(); } - registerBlock(flavour: string, createFunction: BlockCreationFunction) { - this._blockConfigs.push({ - flavour, - createFunction: createFunction.bind(this), - }); - } - async toCanvas( blocks: GfxBlockElementModel[], shapes: ShapeElementModel[], @@ -1466,12 +1058,3 @@ function tryGetSvgFromClipboard(clipboardData: DataTransfer) { const file = new File([blob], 'pasted-image.svg', { type: 'image/svg+xml' }); return file; } - -function getRootByEditorHost( - editorHost: EditorHost -): RootBlockComponent | null { - const model = editorHost.doc.root; - if (!model) return null; - const root = editorHost.view.getBlock(model.id); - return root as RootBlockComponent | null; -} diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/clipboard/config.ts b/blocksuite/affine/blocks/block-root/src/edgeless/clipboard/config.ts new file mode 100644 index 0000000000..cd4d0a3401 --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/clipboard/config.ts @@ -0,0 +1,435 @@ +import { + type ClipboardConfigCreationContext, + EdgelessClipboardConfig, +} from '@blocksuite/affine-block-surface'; +import { ReferenceInfoSchema } from '@blocksuite/affine-model'; +import { type BlockSnapshot, fromJSON } from '@blocksuite/store'; + +export class EdgelessClipboardNoteConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:note'; + + override async createBlock(note: BlockSnapshot): Promise { + const oldId = note.id; + + delete note.props.index; + if (!note.props.xywh) { + console.error(`Note block(id: ${oldId}) does not have xywh property`); + return null; + } + + const newId = await this.onBlockSnapshotPaste( + note, + this.std.store, + this.std.store.root!.id + ); + if (!newId) { + console.error(`Failed to paste note block(id: ${oldId})`); + return null; + } + + return newId; + } +} + +export class EdgelessClipboardEdgelessTextConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:edgeless-text'; + + override async createBlock( + edgelessText: BlockSnapshot + ): Promise { + const oldId = edgelessText.id; + delete edgelessText.props.index; + if (!edgelessText.props.xywh) { + console.error( + `EdgelessText block(id: ${oldId}) does not have xywh property` + ); + return null; + } + if (!this.surface) { + return null; + } + const newId = await this.onBlockSnapshotPaste( + edgelessText, + this.std.store, + this.surface.model.id + ); + if (!newId) { + console.error(`Failed to paste EdgelessText block(id: ${oldId})`); + return null; + } + + return newId; + } +} + +export class EdgelessClipboardImageConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:image'; + + override async createBlock(image: BlockSnapshot) { + const { xywh, rotate, sourceId, size, width, height, caption } = + image.props; + + if (!this.surface) return null; + + if (!(await this.std.workspace.blobSync.get(sourceId as string))) { + return null; + } + return this.crud.addBlock( + 'affine:image', + { + caption, + sourceId, + xywh, + rotate, + size, + width, + height, + }, + this.surface.model.id + ); + } +} + +export class EdgelessClipboardFrameConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:frame'; + + override createBlock( + frame: BlockSnapshot, + context: ClipboardConfigCreationContext + ): string | null { + if (!this.surface) return null; + + const { oldToNewIdMap, newPresentationIndexes } = context; + const { xywh, title, background, childElementIds } = frame.props; + + const newChildElementIds: Record = {}; + + if (typeof childElementIds === 'object' && childElementIds !== null) { + Object.keys(childElementIds).forEach(oldId => { + const newId = oldToNewIdMap.get(oldId); + if (newId) { + newChildElementIds[newId] = true; + } + }); + } + + const frameId = this.crud.addBlock( + 'affine:frame', + { + xywh, + background, + title: fromJSON(title), + childElementIds: newChildElementIds, + presentationIndex: newPresentationIndexes.get(frame.id), + }, + this.surface.model.id + ); + return frameId; + } +} + +export class EdgelessClipboardAttachmentConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:attachment'; + + override async createBlock( + attachment: BlockSnapshot + ): Promise { + if (!this.surface) return null; + + const { xywh, rotate, sourceId, name, size, type, embed, style } = + attachment.props; + + if (!(await this.std.workspace.blobSync.get(sourceId as string))) { + return null; + } + const attachmentId = this.crud.addBlock( + 'affine:attachment', + { + xywh, + rotate, + sourceId, + name, + size, + type, + embed, + style, + }, + this.surface.model.id + ); + return attachmentId; + } +} + +export class EdgelessClipboardBookmarkConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:bookmark'; + + override createBlock(bookmark: BlockSnapshot): string | null { + if (!this.surface) return null; + + const { xywh, style, url, caption, description, icon, image, title } = + bookmark.props; + + const bookmarkId = this.crud.addBlock( + 'affine:bookmark', + { + xywh, + style, + url, + caption, + description, + icon, + image, + title, + }, + this.surface.model.id + ); + return bookmarkId; + } +} + +export class EdgelessClipboardEmbedFigmaConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:embed-figma'; + + override createBlock(figmaEmbed: BlockSnapshot): string | null { + if (!this.surface) return null; + const { xywh, style, url, caption, title, description } = figmaEmbed.props; + + const embedFigmaId = this.crud.addBlock( + 'affine:embed-figma', + { + xywh, + style, + url, + caption, + title, + description, + }, + this.surface.model.id + ); + return embedFigmaId; + } +} + +export class EdgelessClipboardEmbedGithubConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:embed-github'; + + override createBlock(githubEmbed: BlockSnapshot): string | null { + if (!this.surface) return null; + + const { + xywh, + style, + owner, + repo, + githubType, + githubId, + url, + caption, + image, + status, + statusReason, + title, + description, + createdAt, + assignees, + } = githubEmbed.props; + + const embedGithubId = this.crud.addBlock( + 'affine:embed-github', + { + xywh, + style, + owner, + repo, + githubType, + githubId, + url, + caption, + image, + status, + statusReason, + title, + description, + createdAt, + assignees, + }, + this.surface.model.id + ); + return embedGithubId; + } +} + +export class EdgelessClipboardEmbedHtmlConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:embed-html'; + + override createBlock(htmlEmbed: BlockSnapshot): string | null { + if (!this.surface) return null; + const { xywh, style, caption, html, design } = htmlEmbed.props; + + const embedHtmlId = this.crud.addBlock( + 'affine:embed-html', + { + xywh, + style, + caption, + html, + design, + }, + this.surface.model.id + ); + return embedHtmlId; + } +} + +export class EdgelessClipboardEmbedLoomConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:embed-loom'; + + override createBlock(loomEmbed: BlockSnapshot): string | null { + if (!this.surface) return null; + const { xywh, style, url, caption, videoId, image, title, description } = + loomEmbed.props; + + const embedLoomId = this.crud.addBlock( + 'affine:embed-loom', + { + xywh, + style, + url, + caption, + videoId, + image, + title, + description, + }, + this.surface.model.id + ); + return embedLoomId; + } +} + +export class EdgelessClipboardEmbedYoutubeConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:embed-youtube'; + + override createBlock(youtubeEmbed: BlockSnapshot): string | null { + if (!this.surface) return null; + const { + xywh, + style, + url, + caption, + videoId, + image, + title, + description, + creator, + creatorUrl, + creatorImage, + } = youtubeEmbed.props; + + const embedYoutubeId = this.crud.addBlock( + 'affine:embed-youtube', + { + xywh, + style, + url, + caption, + videoId, + image, + title, + description, + creator, + creatorUrl, + creatorImage, + }, + this.surface.model.id + ); + return embedYoutubeId; + } +} + +export class EdgelessClipboardEmbedIframeConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:embed-iframe'; + + override createBlock(embedIframe: BlockSnapshot): string | null { + if (!this.surface) return null; + const { + xywh, + caption, + url, + title, + description, + iframeUrl, + scale, + width, + height, + } = embedIframe.props; + + return this.crud.addBlock( + 'affine:embed-iframe', + { + url, + iframeUrl, + xywh, + caption, + title, + description, + scale, + width, + height, + }, + this.surface.model.id + ); + } +} + +export class EdgelessClipboardEmbedLinkedDocConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:embed-linked-doc'; + + override createBlock(linkedDocEmbed: BlockSnapshot): string | null { + if (!this.surface) return null; + + const { xywh, style, caption, pageId, params, title, description } = + linkedDocEmbed.props; + const referenceInfo = ReferenceInfoSchema.parse({ + pageId, + params, + title, + description, + }); + + return this.crud.addBlock( + 'affine:embed-linked-doc', + { + xywh, + style, + caption, + ...referenceInfo, + }, + this.surface.model.id + ); + } +} + +export class EdgelessClipboardEmbedSyncedDocConfig extends EdgelessClipboardConfig { + static override readonly key = 'affine:embed-synced-doc'; + + override createBlock(syncedDocEmbed: BlockSnapshot): string | null { + if (!this.surface) return null; + + const { xywh, style, caption, scale, pageId, params } = + syncedDocEmbed.props; + const referenceInfo = ReferenceInfoSchema.parse({ pageId, params }); + + return this.crud.addBlock( + 'affine:embed-synced-doc', + { + xywh, + style, + caption, + scale, + ...referenceInfo, + }, + this.surface.model.id + ); + } +} diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-spec.ts b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-spec.ts index 229697f284..134ccb6f6a 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-spec.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-spec.ts @@ -20,6 +20,22 @@ import { literal, unsafeStatic } from 'lit/static-html.js'; import { CommonSpecs } from '../common-specs/index.js'; import { edgelessNavigatorBgWidget } from '../widgets/edgeless-navigator-bg/index.js'; import { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from '../widgets/edgeless-zoom-toolbar/index.js'; +import { + EdgelessClipboardAttachmentConfig, + EdgelessClipboardBookmarkConfig, + EdgelessClipboardEdgelessTextConfig, + EdgelessClipboardEmbedFigmaConfig, + EdgelessClipboardEmbedGithubConfig, + EdgelessClipboardEmbedHtmlConfig, + EdgelessClipboardEmbedIframeConfig, + EdgelessClipboardEmbedLinkedDocConfig, + EdgelessClipboardEmbedLoomConfig, + EdgelessClipboardEmbedSyncedDocConfig, + EdgelessClipboardEmbedYoutubeConfig, + EdgelessClipboardFrameConfig, + EdgelessClipboardImageConfig, + EdgelessClipboardNoteConfig, +} from './clipboard/config.js'; import { NOTE_SLICER_WIDGET } from './components/note-slicer/index.js'; import { EDGELESS_DRAGGING_AREA_WIDGET } from './components/rects/edgeless-dragging-area-rect.js'; import { EDGELESS_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selected-rect.js'; @@ -56,6 +72,23 @@ class EdgelessLocker extends LifeCycleWatcher { } } +const EdgelessClipboardConfigs: ExtensionType[] = [ + EdgelessClipboardNoteConfig, + EdgelessClipboardEdgelessTextConfig, + EdgelessClipboardImageConfig, + EdgelessClipboardFrameConfig, + EdgelessClipboardAttachmentConfig, + EdgelessClipboardBookmarkConfig, + EdgelessClipboardEmbedFigmaConfig, + EdgelessClipboardEmbedGithubConfig, + EdgelessClipboardEmbedHtmlConfig, + EdgelessClipboardEmbedLoomConfig, + EdgelessClipboardEmbedYoutubeConfig, + EdgelessClipboardEmbedIframeConfig, + EdgelessClipboardEmbedLinkedDocConfig, + EdgelessClipboardEmbedSyncedDocConfig, +]; + const EdgelessCommonExtension: ExtensionType[] = [ CommonSpecs, ToolController, @@ -65,6 +98,7 @@ const EdgelessCommonExtension: ExtensionType[] = [ ConnectorElementView, ...quickTools, ...seniorTools, + ...EdgelessClipboardConfigs, ].flat(); export const EdgelessRootBlockSpec: ExtensionType[] = [ diff --git a/blocksuite/affine/blocks/block-surface/src/extensions/clipboard-config.ts b/blocksuite/affine/blocks/block-surface/src/extensions/clipboard-config.ts new file mode 100644 index 0000000000..7d34e1ed6a --- /dev/null +++ b/blocksuite/affine/blocks/block-surface/src/extensions/clipboard-config.ts @@ -0,0 +1,80 @@ +import { type BlockStdScope, StdIdentifier } from '@blocksuite/block-std'; +import { type Container, createIdentifier } from '@blocksuite/global/di'; +import { BlockSuiteError } from '@blocksuite/global/exceptions'; +import { type BlockSnapshot, Extension, type Store } from '@blocksuite/store'; + +import { getSurfaceComponent } from '../utils/get-surface-block'; +import { EdgelessCRUDIdentifier } from './crud-extension'; + +export type ClipboardConfigCreationContext = { + /** + * element old id to new id + */ + oldToNewIdMap: Map; + /** + * element old id to new layer index + */ + originalIndexes: Map; + + /** + * frame old id to new presentation index + */ + newPresentationIndexes: Map; +}; + +export const EdgelessClipboardConfigIdentifier = + createIdentifier('edgeless-clipboard-config'); + +export abstract class EdgelessClipboardConfig extends Extension { + static key: string; + + constructor(readonly std: BlockStdScope) { + super(); + } + + get surface() { + return getSurfaceComponent(this.std); + } + + get crud() { + return this.std.get(EdgelessCRUDIdentifier); + } + + 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; + }; + + abstract createBlock( + snapshot: BlockSnapshot, + context: ClipboardConfigCreationContext + ): string | null | Promise; + + static override setup(di: Container) { + if (!this.key) { + throw new BlockSuiteError( + BlockSuiteError.ErrorCode.ValueNotExists, + 'Key is not defined in the EdgelessClipboardConfig' + ); + } + + di.add( + this as unknown as { new (std: BlockStdScope): EdgelessClipboardConfig }, + [StdIdentifier] + ); + + di.addImpl(EdgelessClipboardConfigIdentifier(this.key), provider => + provider.get(this) + ); + } +} diff --git a/blocksuite/affine/blocks/block-surface/src/extensions/index.ts b/blocksuite/affine/blocks/block-surface/src/extensions/index.ts index d156da538d..547c4122a7 100644 --- a/blocksuite/affine/blocks/block-surface/src/extensions/index.ts +++ b/blocksuite/affine/blocks/block-surface/src/extensions/index.ts @@ -1,3 +1,4 @@ +export * from './clipboard-config'; export * from './crud-extension'; export * from './export-manager'; export * from './legacy-slot-extension'; diff --git a/blocksuite/framework/block-std/src/__tests__/hast.unit.spec.ts b/blocksuite/framework/block-std/src/__tests__/hast.unit.spec.ts index 3f2e1e9863..fb97be8074 100644 --- a/blocksuite/framework/block-std/src/__tests__/hast.unit.spec.ts +++ b/blocksuite/framework/block-std/src/__tests__/hast.unit.spec.ts @@ -2,7 +2,7 @@ import rehypeParse from 'rehype-parse'; import { unified } from 'unified'; import { describe, expect, test } from 'vitest'; -import { onlyContainImgElement } from '../clipboard/index.js'; +import { onlyContainImgElement } from '../clipboard/utils.js'; describe('only contains img elements', () => { test('normal with head', () => { diff --git a/blocksuite/framework/block-std/src/clipboard/index.ts b/blocksuite/framework/block-std/src/clipboard/index.ts index e1209daa10..67f3a34b31 100644 --- a/blocksuite/framework/block-std/src/clipboard/index.ts +++ b/blocksuite/framework/block-std/src/clipboard/index.ts @@ -9,12 +9,12 @@ import type { TransformerMiddleware, } from '@blocksuite/store'; import DOMPurify from 'dompurify'; -import type { RootContentMap } from 'hast'; import * as lz from 'lz-string'; import rehypeParse from 'rehype-parse'; import { unified } from 'unified'; import { LifeCycleWatcher } from '../extension/index.js'; +import { onlyContainImgElement } from './utils.js'; type AdapterConstructor = | { new (job: Transformer): T } @@ -28,38 +28,6 @@ type AdapterMap = Map< } >; -type HastUnionType< - K extends keyof RootContentMap, - V extends RootContentMap[K], -> = V; - -export function onlyContainImgElement( - ast: HastUnionType -): 'yes' | 'no' | 'maybe' { - if (ast.type === 'element') { - switch (ast.tagName) { - case 'html': - case 'body': - return ast.children.map(onlyContainImgElement).reduce((a, b) => { - if (a === 'no' || b === 'no') { - return 'no'; - } - if (a === 'maybe' && b === 'maybe') { - return 'maybe'; - } - return 'yes'; - }, 'maybe'); - case 'img': - return 'yes'; - case 'head': - return 'maybe'; - default: - return 'no'; - } - } - return 'maybe'; -} - export class Clipboard extends LifeCycleWatcher { static override readonly key = 'clipboard'; diff --git a/blocksuite/framework/block-std/src/clipboard/utils.ts b/blocksuite/framework/block-std/src/clipboard/utils.ts new file mode 100644 index 0000000000..5791f92b5d --- /dev/null +++ b/blocksuite/framework/block-std/src/clipboard/utils.ts @@ -0,0 +1,33 @@ +import type { RootContentMap } from 'hast'; + +type HastUnionType< + K extends keyof RootContentMap, + V extends RootContentMap[K], +> = V; + +export function onlyContainImgElement( + ast: HastUnionType +): 'yes' | 'no' | 'maybe' { + if (ast.type === 'element') { + switch (ast.tagName) { + case 'html': + case 'body': + return ast.children.map(onlyContainImgElement).reduce((a, b) => { + if (a === 'no' || b === 'no') { + return 'no'; + } + if (a === 'maybe' && b === 'maybe') { + return 'maybe'; + } + return 'yes'; + }, 'maybe'); + case 'img': + return 'yes'; + case 'head': + return 'maybe'; + default: + return 'no'; + } + } + return 'maybe'; +} diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx index 9685b0e504..57dee736ed 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx @@ -57,7 +57,7 @@ import { import { patchDatabaseBlockConfigService } from '../extensions/database-block-config-service'; import { patchDocModeService } from '../extensions/doc-mode-service'; import { patchDocUrlExtensions } from '../extensions/doc-url'; -import { EdgelessClipboardWatcher } from '../extensions/edgeless-clipboard'; +import { EdgelessClipboardAIChatConfig } from '../extensions/edgeless-clipboard'; import { patchForClipboardInElectron } from '../extensions/electron-clipboard'; import { enableEditorExtension } from '../extensions/entry/enable-editor'; import { enableMobileExtension } from '../extensions/entry/enable-mobile'; @@ -169,7 +169,7 @@ const usePatchSpecs = (mode: DocMode) => { patchNotificationService(confirmModal), patchPeekViewService(peekViewService), patchOpenDocExtension(), - EdgelessClipboardWatcher, + EdgelessClipboardAIChatConfig, patchDocUrlExtensions(framework), patchQuickSearchService(framework), patchSideBarService(framework), diff --git a/packages/frontend/core/src/blocksuite/extensions/edgeless-clipboard.ts b/packages/frontend/core/src/blocksuite/extensions/edgeless-clipboard.ts index abafd5a11b..ac4a7b38cf 100644 --- a/packages/frontend/core/src/blocksuite/extensions/edgeless-clipboard.ts +++ b/packages/frontend/core/src/blocksuite/extensions/edgeless-clipboard.ts @@ -1,44 +1,26 @@ import { AIChatBlockSchema } from '@affine/core/blocksuite/ai/blocks'; -import { LifeCycleWatcher } from '@blocksuite/affine/block-std'; -import { EdgelessRootBlockComponent } from '@blocksuite/affine/blocks/root'; +import { EdgelessClipboardConfig } from '@blocksuite/affine/blocks/surface'; import type { BlockSnapshot } from '@blocksuite/affine/store'; -export class EdgelessClipboardWatcher extends LifeCycleWatcher { - static override key = 'edgeless-clipboard-watcher'; +export class EdgelessClipboardAIChatConfig extends EdgelessClipboardConfig { + static override readonly key = AIChatBlockSchema.model.flavour; - override mounted() { - super.mounted(); - const { view } = this.std; - view.viewUpdated.subscribe(payload => { - if (payload.type !== 'block' || payload.method !== 'add') { - return; - } - const component = payload.view; - if (!(component instanceof EdgelessRootBlockComponent)) { - return; - } - const AIChatBlockFlavour = AIChatBlockSchema.model.flavour; - const createFunc = (block: BlockSnapshot) => { - const { xywh, scale, messages, sessionId, rootDocId, rootWorkspaceId } = - block.props; - const blockId = component.service.crud.addBlock( - AIChatBlockFlavour, - { - xywh, - scale, - messages, - sessionId, - rootDocId, - rootWorkspaceId, - }, - component.surface.model.id - ); - return blockId; - }; - component.clipboardController.registerBlock( - AIChatBlockFlavour, - createFunc - ); - }); + override createBlock(block: BlockSnapshot): null | string { + if (!this.surface) return null; + const { xywh, scale, messages, sessionId, rootDocId, rootWorkspaceId } = + block.props; + const blockId = this.crud.addBlock( + AIChatBlockSchema.model.flavour, + { + xywh, + scale, + messages, + sessionId, + rootDocId, + rootWorkspaceId, + }, + this.surface.model.id + ); + return blockId; } }