diff --git a/blocksuite/affine/all/src/extensions/preview-specs.ts b/blocksuite/affine/all/src/extensions/preview-specs.ts index 9dc83768eb..5948748ab0 100644 --- a/blocksuite/affine/all/src/extensions/preview-specs.ts +++ b/blocksuite/affine/all/src/extensions/preview-specs.ts @@ -1,6 +1,7 @@ import { PreviewEdgelessRootBlockSpec, PreviewPageRootBlockSpec, + ReadOnlyClipboard, } from '@blocksuite/affine-block-root'; import type { ExtensionType } from '@blocksuite/store'; @@ -12,9 +13,11 @@ import { export const PreviewEdgelessEditorBlockSpecs: ExtensionType[] = [ PreviewEdgelessRootBlockSpec, EdgelessFirstPartyBlockSpecs, + ReadOnlyClipboard, ].flat(); export const PreviewPageEditorBlockSpecs: ExtensionType[] = [ PreviewPageRootBlockSpec, PageFirstPartyBlockSpecs, + ReadOnlyClipboard, ].flat(); diff --git a/blocksuite/affine/blocks/block-root/src/clipboard/page-clipboard.ts b/blocksuite/affine/blocks/block-root/src/clipboard/page-clipboard.ts index 3f5c336c7e..9834e2d56f 100644 --- a/blocksuite/affine/blocks/block-root/src/clipboard/page-clipboard.ts +++ b/blocksuite/affine/blocks/block-root/src/clipboard/page-clipboard.ts @@ -25,23 +25,23 @@ import { ReadOnlyClipboard } from './readonly-clipboard'; * It is supported to copy and paste models in the page root block. */ export class PageClipboard extends ReadOnlyClipboard { + static override key = 'affine-page-clipboard'; + protected _init = () => { this._initAdapters(); - const paste = pasteMiddleware(this._std); + const paste = pasteMiddleware(this.std); // Use surfaceRefToEmbed middleware to convert surface-ref to embed-linked-doc // When pastina a surface-ref block to another doc - const surfaceRefToEmbedMiddleware = surfaceRefToEmbed(this._std); - const replaceId = replaceIdMiddleware( - this._std.store.workspace.idGenerator - ); - this._std.clipboard.use(paste); - this._std.clipboard.use(surfaceRefToEmbedMiddleware); - this._std.clipboard.use(replaceId); + const surfaceRefToEmbedMiddleware = surfaceRefToEmbed(this.std); + const replaceId = replaceIdMiddleware(this.std.store.workspace.idGenerator); + this.std.clipboard.use(paste); + this.std.clipboard.use(surfaceRefToEmbedMiddleware); + this.std.clipboard.use(replaceId); this._disposables.add({ dispose: () => { - this._std.clipboard.unuse(paste); - this._std.clipboard.unuse(surfaceRefToEmbedMiddleware); - this._std.clipboard.unuse(replaceId); + this.std.clipboard.unuse(paste); + this.std.clipboard.unuse(surfaceRefToEmbedMiddleware); + this.std.clipboard.unuse(replaceId); }, }); }; @@ -52,7 +52,7 @@ export class PageClipboard extends ReadOnlyClipboard { parent?: string, index?: number ) => { - const block = await this._std.clipboard.pasteBlockSnapshot( + const block = await this.std.clipboard.pasteBlockSnapshot( snapshot, doc, parent, @@ -66,7 +66,7 @@ export class PageClipboard extends ReadOnlyClipboard { e.preventDefault(); this._copySelected(() => { - this._std.command + this.std.command .chain() .try<{}>(cmd => [ cmd.pipe(getTextSelectionCommand).pipe(deleteTextCommand), @@ -80,8 +80,8 @@ export class PageClipboard extends ReadOnlyClipboard { const e = ctx.get('clipboardState').raw; e.preventDefault(); - this._std.store.captureSync(); - this._std.command + this.std.store.captureSync(); + this.std.command .chain() .try<{}>(cmd => [ cmd.pipe(getTextSelectionCommand).pipe((ctx, next) => { @@ -91,7 +91,7 @@ export class PageClipboard extends ReadOnlyClipboard { } const { from, to } = currentTextSelection; if (to && from.blockId !== to.blockId) { - this._std.command.exec(deleteTextCommand, { + this.std.command.exec(deleteTextCommand, { currentTextSelection, }); } @@ -139,10 +139,10 @@ export class PageClipboard extends ReadOnlyClipboard { if (!ctx.parentBlock) { return; } - this._std.clipboard + this.std.clipboard .paste( e, - this._std.store, + this.std.store, ctx.parentBlock.model.id, ctx.blockIndex ? ctx.blockIndex + 1 : 1 ) @@ -153,15 +153,19 @@ export class PageClipboard extends ReadOnlyClipboard { .run(); }; - override hostConnected() { + override mounted() { + if (!navigator.clipboard) { + console.error( + 'navigator.clipboard is not supported in current environment.' + ); + return; + } 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(); - } + this.std.event.add('copy', this.onPageCopy); + this.std.event.add('paste', this.onPagePaste); + this.std.event.add('cut', this.onPageCut); + this._init(); } } diff --git a/blocksuite/affine/blocks/block-root/src/clipboard/readonly-clipboard.ts b/blocksuite/affine/blocks/block-root/src/clipboard/readonly-clipboard.ts index aa9b7c08b9..e478897db0 100644 --- a/blocksuite/affine/blocks/block-root/src/clipboard/readonly-clipboard.ts +++ b/blocksuite/affine/blocks/block-root/src/clipboard/readonly-clipboard.ts @@ -15,8 +15,8 @@ import { getSelectedModelsCommand, } from '@blocksuite/affine-shared/commands'; import { - type BlockComponent, ClipboardAdapterConfigExtension, + LifeCycleWatcher, type UIEventHandler, } from '@blocksuite/block-std'; import { DisposableGroup } from '@blocksuite/global/disposable'; @@ -81,9 +81,11 @@ export const clipboardConfigs: ExtensionType[] = [ * 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 { +export class ReadOnlyClipboard extends LifeCycleWatcher { + static override key = 'affine-readonly-clipboard'; + protected readonly _copySelected = (onCopy?: () => void) => { - return this._std.command + return this.std.command .chain() .with({ onCopy }) .pipe(getSelectedModelsCommand) @@ -94,26 +96,24 @@ export class ReadOnlyClipboard { protected _disposables = new DisposableGroup(); protected _initAdapters = () => { - const copy = copyMiddleware(this._std); - this._std.clipboard.use(copy); - this._std.clipboard.use( - titleMiddleware(this._std.store.workspace.meta.docMetas) + const copy = copyMiddleware(this.std); + this.std.clipboard.use(copy); + this.std.clipboard.use( + titleMiddleware(this.std.store.workspace.meta.docMetas) ); - this._std.clipboard.use(defaultImageProxyMiddleware); + this.std.clipboard.use(defaultImageProxyMiddleware); this._disposables.add({ dispose: () => { - this._std.clipboard.unuse(copy); - this._std.clipboard.unuse( - titleMiddleware(this._std.store.workspace.meta.docMetas) + this.std.clipboard.unuse(copy); + this.std.clipboard.unuse( + titleMiddleware(this.std.store.workspace.meta.docMetas) ); - this._std.clipboard.unuse(defaultImageProxyMiddleware); + this.std.clipboard.unuse(defaultImageProxyMiddleware); }, }); }; - host: BlockComponent; - onPageCopy: UIEventHandler = ctx => { const e = ctx.get('clipboardState').raw; e.preventDefault(); @@ -121,25 +121,17 @@ export class ReadOnlyClipboard { this._copySelected().run(); }; - protected get _std() { - return this.host.std; - } - - constructor(host: BlockComponent) { - this.host = host; - } - - hostConnected() { + override mounted(): void { + if (!navigator.clipboard) { + console.error( + 'navigator.clipboard is not supported in current environment.' + ); + return; + } if (this._disposables.disposed) { this._disposables = new DisposableGroup(); } - if (navigator.clipboard) { - this.host.handleEvent('copy', this.onPageCopy); - this._initAdapters(); - } - } - - hostDisconnected() { - this._disposables.dispose(); + this.std.event.add('copy', this.onPageCopy); + this._initAdapters(); } } 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 8da2887eef..2416cc55df 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/clipboard/clipboard.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/clipboard/clipboard.ts @@ -22,7 +22,6 @@ import { import { ClipboardAdapter, decodeClipboardBlobs, - encodeClipboardBlobs, } from '@blocksuite/affine-shared/adapters'; import { CANVAS_EXPORT_IGNORE_TAGS, @@ -42,8 +41,6 @@ import { referenceToNode, } from '@blocksuite/affine-shared/utils'; import type { - BlockComponent, - BlockStdScope, EditorHost, SurfaceSelection, UIEventStateContext, @@ -74,21 +71,18 @@ import { BlockSnapshotSchema, type SliceSnapshot, } from '@blocksuite/store'; -import DOMPurify from 'dompurify'; import * as Y from 'yjs'; import { PageClipboard } from '../../clipboard/index.js'; -import { edgelessElementsBoundFromRawData } from '../utils/bound-utils.js'; -import { createNewPresentationIndexes } from '../utils/clipboard-utils.js'; +import { getSortedCloneElements } from '../utils/clone-utils.js'; +import { isCanvasElementWithText } from '../utils/query.js'; import { - getSortedCloneElements, - serializeElement, -} from '../utils/clone-utils.js'; -import { - isAttachmentBlock, - isCanvasElementWithText, - isImageBlock, -} from '../utils/query.js'; + createNewPresentationIndexes, + edgelessElementsBoundFromRawData, + isPureFileInClipboard, + prepareClipboardData, + tryGetSvgFromClipboard, +} from './utils.js'; const BLOCKSUITE_SURFACE = 'blocksuite/surface'; @@ -102,35 +96,25 @@ interface CanvasExportOptions { } export class EdgelessClipboardController extends PageClipboard { + static override key = 'affine-edgeless-clipboard'; + private readonly _initEdgelessClipboard = () => { - this.host.handleEvent( - 'copy', - ctx => { - const { surfaceSelections, selectedIds } = this.selectionManager; + this.std.event.add('copy', ctx => { + const { surfaceSelections, selectedIds } = this.selectionManager; - if (selectedIds.length === 0) return false; + if (selectedIds.length === 0) return false; - this._onCopy(ctx, surfaceSelections).catch(console.error); - return; - }, - { global: true } - ); + this._onCopy(ctx, surfaceSelections).catch(console.error); + return; + }); - this.host.handleEvent( - 'paste', - ctx => { - this._onPaste(ctx).catch(console.error); - }, - { global: true } - ); + this.std.event.add('paste', ctx => { + this._onPaste(ctx).catch(console.error); + }); - this.host.handleEvent( - 'cut', - ctx => { - this._onCut(ctx).catch(console.error); - }, - { global: true } - ); + this.std.event.add('cut', ctx => { + this._onCut(ctx).catch(console.error); + }); }; private readonly _onCopy = async ( @@ -373,21 +357,13 @@ export class EdgelessClipboardController extends PageClipboard { } private get doc() { - return this.host.doc; - } - - private get edgeless() { - return this.host; + return this.std.store; } private get selectionManager() { return this.gfx.selection; } - private get std() { - return this.host.std; - } - private get surface() { return getSurfaceComponent(this.std); } @@ -523,7 +499,6 @@ export class EdgelessClipboardController extends PageClipboard { } private async _edgelessToCanvas( - edgeless: BlockComponent, bound: IBound, nodes?: GfxBlockElementModel[], canvasElements: GfxPrimitiveElementModel[] = [], @@ -533,7 +508,7 @@ export class EdgelessClipboardController extends PageClipboard { dpr = window.devicePixelRatio || 1, }: CanvasExportOptions = {} ): Promise { - const host = edgeless.host; + const host = this.std.host; const rootModel = this.doc.root; if (!rootModel) return; @@ -874,7 +849,7 @@ export class EdgelessClipboardController extends PageClipboard { originalIndexes: new Map(), newPresentationIndexes: createNewPresentationIndexes( elementsRawData, - this.edgeless + this.std ), }; @@ -960,7 +935,13 @@ export class EdgelessClipboardController extends PageClipboard { }; } - override hostConnected() { + override mounted() { + if (!navigator.clipboard) { + console.error( + 'navigator.clipboard is not supported in current environment.' + ); + return; + } if (this._disposables.disposed) { this._disposables = new DisposableGroup(); } @@ -989,72 +970,7 @@ export class EdgelessClipboardController extends PageClipboard { return; } - const canvas = await this._edgelessToCanvas( - this.host, - bound, - blocks, - shapes, - options - ); + const canvas = await this._edgelessToCanvas(bound, blocks, shapes, options); return canvas; } } - -export async function prepareClipboardData( - selectedAll: GfxModel[], - std: BlockStdScope -) { - const job = std.store.getTransformer(); - const selected = await Promise.all( - selectedAll.map(async selected => { - const data = serializeElement(selected, selectedAll, job); - if (!data) { - return; - } - if (isAttachmentBlock(selected) || isImageBlock(selected)) { - await job.assetsManager.readFromBlob(data.props.sourceId as string); - } - return data; - }) - ); - const blobs = await encodeClipboardBlobs(job.assetsManager.getAssets()); - return { - snapshot: selected.filter(d => !!d), - blobs, - }; -} - -function isPureFileInClipboard(clipboardData: DataTransfer) { - const types = clipboardData.types; - return ( - (types.length === 1 && types[0] === 'Files') || - (types.length === 2 && - (types.includes('text/plain') || types.includes('text/html')) && - types.includes('Files')) - ); -} - -function tryGetSvgFromClipboard(clipboardData: DataTransfer) { - const types = clipboardData.types; - - if (types.length === 1 && types[0] !== 'text/plain') { - return null; - } - - const parser = new DOMParser(); - const svgDoc = parser.parseFromString( - clipboardData.getData('text/plain'), - 'image/svg+xml' - ); - const svg = svgDoc.documentElement; - - if (svg.tagName !== 'svg' || !svg.hasAttribute('xmlns')) { - return null; - } - const svgContent = DOMPurify.sanitize(svgDoc.documentElement, { - USE_PROFILES: { svg: true }, - }); - const blob = new Blob([svgContent], { type: 'image/svg+xml' }); - const file = new File([blob], 'pasted-image.svg', { type: 'image/svg+xml' }); - return file; -} diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/clipboard/utils.ts b/blocksuite/affine/blocks/block-root/src/edgeless/clipboard/utils.ts new file mode 100644 index 0000000000..df6752bdec --- /dev/null +++ b/blocksuite/affine/blocks/block-root/src/edgeless/clipboard/utils.ts @@ -0,0 +1,138 @@ +import { + EdgelessFrameManager, + EdgelessFrameManagerIdentifier, +} from '@blocksuite/affine-block-frame'; +import type { FrameBlockProps } from '@blocksuite/affine-model'; +import { encodeClipboardBlobs } from '@blocksuite/affine-shared/adapters'; +import type { BlockStdScope } from '@blocksuite/block-std'; +import { + generateKeyBetweenV2, + type GfxModel, + type SerializedElement, +} from '@blocksuite/block-std/gfx'; +import { Bound, getBoundWithRotation } from '@blocksuite/global/gfx'; +import { type BlockSnapshot, BlockSnapshotSchema } from '@blocksuite/store'; +import DOMPurify from 'dompurify'; + +import { serializeElement } from '../utils/clone-utils'; +import { isAttachmentBlock, isImageBlock } from '../utils/query'; + +type FrameSnapshot = BlockSnapshot & { + props: FrameBlockProps; +}; + +export function createNewPresentationIndexes( + raw: (SerializedElement | BlockSnapshot)[], + std: BlockStdScope +) { + const frames = raw + .filter((block): block is FrameSnapshot => { + const { data } = BlockSnapshotSchema.safeParse(block); + return data?.flavour === 'affine:frame'; + }) + .sort((a, b) => EdgelessFrameManager.framePresentationComparator(a, b)); + + const frameMgr = std.get(EdgelessFrameManagerIdentifier); + let before = frameMgr.generatePresentationIndex(); + const result = new Map(); + frames.forEach(frame => { + result.set(frame.id, before); + before = generateKeyBetweenV2(before, null); + }); + + return result; +} + +export async function prepareClipboardData( + selectedAll: GfxModel[], + std: BlockStdScope +) { + const job = std.store.getTransformer(); + const selected = await Promise.all( + selectedAll.map(async selected => { + const data = serializeElement(selected, selectedAll, job); + if (!data) { + return; + } + if (isAttachmentBlock(selected) || isImageBlock(selected)) { + await job.assetsManager.readFromBlob(data.props.sourceId as string); + } + return data; + }) + ); + const blobs = await encodeClipboardBlobs(job.assetsManager.getAssets()); + return { + snapshot: selected.filter(d => !!d), + blobs, + }; +} + +export function isPureFileInClipboard(clipboardData: DataTransfer) { + const types = clipboardData.types; + return ( + (types.length === 1 && types[0] === 'Files') || + (types.length === 2 && + (types.includes('text/plain') || types.includes('text/html')) && + types.includes('Files')) + ); +} + +export function tryGetSvgFromClipboard(clipboardData: DataTransfer) { + const types = clipboardData.types; + + if (types.length === 1 && types[0] !== 'text/plain') { + return null; + } + + const parser = new DOMParser(); + const svgDoc = parser.parseFromString( + clipboardData.getData('text/plain'), + 'image/svg+xml' + ); + const svg = svgDoc.documentElement; + + if (svg.tagName !== 'svg' || !svg.hasAttribute('xmlns')) { + return null; + } + const svgContent = DOMPurify.sanitize(svgDoc.documentElement, { + USE_PROFILES: { svg: true }, + }); + const blob = new Blob([svgContent], { type: 'image/svg+xml' }); + const file = new File([blob], 'pasted-image.svg', { type: 'image/svg+xml' }); + return file; +} + +export function edgelessElementsBoundFromRawData( + elementsRawData: (SerializedElement | BlockSnapshot)[] +) { + if (elementsRawData.length === 0) return new Bound(); + + let prev: Bound | null = null; + + for (const data of elementsRawData) { + const { data: blockSnapshot } = BlockSnapshotSchema.safeParse(data); + const bound = blockSnapshot + ? getBoundFromGfxBlockSnapshot(blockSnapshot) + : getBoundFromSerializedElement(data as SerializedElement); + + if (!bound) continue; + if (!prev) prev = bound; + else prev = prev.unite(bound); + } + + return prev ?? new Bound(); +} + +function getBoundFromSerializedElement(element: SerializedElement) { + return Bound.from( + getBoundWithRotation({ + ...Bound.deserialize(element.xywh), + rotate: typeof element.rotate === 'number' ? element.rotate : 0, + }) + ); +} + +function getBoundFromGfxBlockSnapshot(snapshot: BlockSnapshot) { + if (typeof snapshot.props.xywh !== 'string') return null; + return Bound.deserialize(snapshot.props.xywh); +} diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/more.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/more.ts index 47f44cf6f8..ead7086c4a 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/more.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/more.ts @@ -46,6 +46,7 @@ import { ResetIcon, } from '@blocksuite/icons/lit'; +import { EdgelessClipboardController } from '../../clipboard/clipboard'; import { duplicate } from '../../utils/clipboard-utils'; import { getSortedCloneElements } from '../../utils/clone-utils'; import { moveConnectors } from '../../utils/connector'; @@ -154,10 +155,12 @@ export const moreActions = [ const models = ctx.getSurfaceModels(); if (!models.length) return; - const edgeless = getEdgelessWith(ctx); - if (!edgeless) return; + const edgelessClipboard = ctx.std.getOptional( + EdgelessClipboardController + ); + if (!edgelessClipboard) return; - edgeless.clipboardController.copy(); + edgelessClipboard.copy(); }, }, { diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-block.ts b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-block.ts index 6731c44415..b358385c49 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-block.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-block.ts @@ -48,7 +48,6 @@ import { query } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import type { EdgelessRootBlockWidgetName } from '../types.js'; -import { EdgelessClipboardController } from './clipboard/clipboard.js'; import type { EdgelessSelectedRectWidget } from './components/rects/edgeless-selected-rect.js'; import { EdgelessPageKeyboardManager } from './edgeless-keyboard.js'; import type { EdgelessRootService } from './edgeless-root-service.js'; @@ -124,8 +123,6 @@ export class EdgelessRootBlockComponent extends BlockComponent< private _resizeObserver: ResizeObserver | null = null; - clipboardController = new EdgelessClipboardController(this); - keyboardManager: EdgelessPageKeyboardManager | null = null; get dispatcher() { @@ -473,7 +470,6 @@ export class EdgelessRootBlockComponent extends BlockComponent< this._initViewport(); - this.clipboardController.hostConnected(); this.keyboardManager = new EdgelessPageKeyboardManager(this); this.handleEvent('selectionChange', () => { @@ -493,7 +489,6 @@ export class EdgelessRootBlockComponent extends BlockComponent< override disconnectedCallback() { super.disconnectedCallback(); - this.clipboardController.hostDisconnected(); if (this._resizeObserver) { this._resizeObserver.disconnect(); this._resizeObserver = null; 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 134ccb6f6a..9262ca540c 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,7 @@ 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 { EdgelessClipboardController } from './clipboard/clipboard.js'; import { EdgelessClipboardAttachmentConfig, EdgelessClipboardBookmarkConfig, @@ -113,6 +114,7 @@ export const EdgelessRootBlockSpec: ExtensionType[] = [ edgelessNavigatorBgWidget, edgelessSelectedRectWidget, edgelessToolbarWidget, + EdgelessClipboardController, ]; export const PreviewEdgelessRootBlockSpec: ExtensionType[] = [ diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/default-tool.ts b/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/default-tool.ts index 68ee299ba3..1cd70e7e95 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/default-tool.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/default-tool.ts @@ -49,7 +49,7 @@ import { effect } from '@preact/signals-core'; import clamp from 'lodash-es/clamp'; import last from 'lodash-es/last'; -import type { EdgelessRootBlockComponent } from '../index.js'; +import { EdgelessClipboardController } from '../clipboard/clipboard.js'; import { prepareCloneData } from '../utils/clone-utils.js'; import { calPanDelta } from '../utils/panning-utils.js'; import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js'; @@ -238,10 +238,9 @@ export class DefaultTool extends BaseTool { private async _cloneContent() { if (!this._edgeless) return; - // FIXME: edgeless clipboard should be an extension - const clipboardController = ( - this._edgeless as EdgelessRootBlockComponent | null - )?.clipboardController; + const clipboardController = this.std.getOptional( + EdgelessClipboardController + ); if (!clipboardController) return; const snapshot = prepareCloneData(this._toBeMoved, this.std); diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/index.ts b/blocksuite/affine/blocks/block-root/src/edgeless/index.ts index ea433cb759..698ac73c9d 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/index.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/index.ts @@ -1,3 +1,4 @@ +export * from './clipboard/clipboard'; export { EdgelessTemplatePanel } from './components/toolbar/template/template-panel.js'; export * from './components/toolbar/template/template-type.js'; export * from './edgeless-root-block.js'; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/utils/bound-utils.ts b/blocksuite/affine/blocks/block-root/src/edgeless/utils/bound-utils.ts deleted file mode 100644 index 5bcb1451f7..0000000000 --- a/blocksuite/affine/blocks/block-root/src/edgeless/utils/bound-utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { SerializedElement } from '@blocksuite/block-std/gfx'; -import { Bound, getBoundWithRotation } from '@blocksuite/global/gfx'; -import { type BlockSnapshot, BlockSnapshotSchema } from '@blocksuite/store'; - -export function getBoundFromSerializedElement(element: SerializedElement) { - return Bound.from( - getBoundWithRotation({ - ...Bound.deserialize(element.xywh), - rotate: typeof element.rotate === 'number' ? element.rotate : 0, - }) - ); -} - -export function getBoundFromGfxBlockSnapshot(snapshot: BlockSnapshot) { - if (typeof snapshot.props.xywh !== 'string') return null; - return Bound.deserialize(snapshot.props.xywh); -} - -export function edgelessElementsBoundFromRawData( - elementsRawData: (SerializedElement | BlockSnapshot)[] -) { - if (elementsRawData.length === 0) return new Bound(); - - let prev: Bound | null = null; - - for (const data of elementsRawData) { - const { data: blockSnapshot } = BlockSnapshotSchema.safeParse(data); - const bound = blockSnapshot - ? getBoundFromGfxBlockSnapshot(blockSnapshot) - : getBoundFromSerializedElement(data as SerializedElement); - - if (!bound) continue; - if (!prev) prev = bound; - else prev = prev.unite(bound); - } - - return prev ?? new Bound(); -} diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/utils/clipboard-utils.ts b/blocksuite/affine/blocks/block-root/src/edgeless/utils/clipboard-utils.ts index e71d9b3feb..f65bddf09e 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/utils/clipboard-utils.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/utils/clipboard-utils.ts @@ -1,14 +1,12 @@ +import { isFrameBlock } from '@blocksuite/affine-block-frame'; import { - EdgelessFrameManager, - EdgelessFrameManagerIdentifier, - isFrameBlock, -} from '@blocksuite/affine-block-frame'; -import { isNoteBlock } from '@blocksuite/affine-block-surface'; + getSurfaceComponent, + isNoteBlock, +} from '@blocksuite/affine-block-surface'; import type { EdgelessTextBlockModel, EmbedSyncedDocModel, FrameBlockModel, - FrameBlockProps, ImageBlockModel, NoteBlockModel, ShapeElementModel, @@ -16,15 +14,13 @@ import type { import { getElementsWithoutGroup } from '@blocksuite/affine-shared/utils'; import type { BlockComponent } from '@blocksuite/block-std'; import { - generateKeyBetweenV2, + GfxControllerIdentifier, type GfxModel, - type SerializedElement, } from '@blocksuite/block-std/gfx'; import { getCommonBoundWithRotation } from '@blocksuite/global/gfx'; -import { type BlockSnapshot, BlockSnapshotSchema } from '@blocksuite/store'; import groupBy from 'lodash-es/groupBy'; -import type { EdgelessRootBlockComponent } from '../edgeless-root-block.js'; +import { EdgelessClipboardController } from '../clipboard/clipboard.js'; import { getSortedCloneElements, prepareCloneData } from './clone-utils.js'; import { isEdgelessTextBlock, @@ -34,28 +30,33 @@ import { const offset = 10; export async function duplicate( - edgeless: EdgelessRootBlockComponent, + edgeless: BlockComponent, elements: GfxModel[], select = true ) { - const { clipboardController } = edgeless; + const edgelessClipboard = edgeless.std.get(EdgelessClipboardController); + const gfx = edgeless.std.get(GfxControllerIdentifier); + + const surface = getSurfaceComponent(edgeless.std); + if (!surface) return; + const copyElements = getSortedCloneElements(elements); const totalBound = getCommonBoundWithRotation(copyElements); totalBound.x += totalBound.w + offset; const snapshot = prepareCloneData(copyElements, edgeless.std); const { canvasElements, blockModels } = - await clipboardController.createElementsFromClipboardData( + await edgelessClipboard.createElementsFromClipboardData( snapshot, totalBound.center ); const newElements = [...canvasElements, ...blockModels]; - edgeless.surface.fitToViewport(totalBound); + surface.fitToViewport(totalBound); if (select) { - edgeless.service.selection.set({ + gfx.selection.set({ elements: newElements.map(e => e.id), editing: false, }); @@ -94,29 +95,3 @@ export const splitElements = (elements: GfxModel[]) => { embedSyncedDocs: embedSyncedDocs ?? [], }; }; - -type FrameSnapshot = BlockSnapshot & { - props: FrameBlockProps; -}; - -export function createNewPresentationIndexes( - raw: (SerializedElement | BlockSnapshot)[], - edgeless: BlockComponent -) { - const frames = raw - .filter((block): block is FrameSnapshot => { - const { data } = BlockSnapshotSchema.safeParse(block); - return data?.flavour === 'affine:frame'; - }) - .sort((a, b) => EdgelessFrameManager.framePresentationComparator(a, b)); - - const frameMgr = edgeless.std.get(EdgelessFrameManagerIdentifier); - let before = frameMgr.generatePresentationIndex(); - const result = new Map(); - frames.forEach(frame => { - result.set(frame.id, before); - before = generateKeyBetweenV2(before, null); - }); - - return result; -} diff --git a/blocksuite/affine/blocks/block-root/src/page/page-root-block.ts b/blocksuite/affine/blocks/block-root/src/page/page-root-block.ts index 028db31761..25721692fa 100644 --- a/blocksuite/affine/blocks/block-root/src/page/page-root-block.ts +++ b/blocksuite/affine/blocks/block-root/src/page/page-root-block.ts @@ -31,7 +31,6 @@ import { css, html } from 'lit'; import { query } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; -import { PageClipboard } from '../clipboard/index.js'; import type { PageRootBlockWidgetName } from '../index.js'; import { PageKeyboardManager } from '../keyboard/keyboard-manager.js'; import type { PageRootService } from './page-root-service.js'; @@ -114,8 +113,6 @@ export class PageRootBlockComponent extends BlockComponent< } `; - clipboardController = new PageClipboard(this); - /** * Focus the first paragraph in the default note block. * If there is no paragraph, create one. @@ -211,7 +208,6 @@ export class PageRootBlockComponent extends BlockComponent< override connectedCallback() { super.connectedCallback(); - this.clipboardController.hostConnected(); this.keyboardManager = new PageKeyboardManager(this); @@ -386,7 +382,6 @@ export class PageRootBlockComponent extends BlockComponent< override disconnectedCallback() { super.disconnectedCallback(); - this.clipboardController.hostDisconnected(); this._disposables.dispose(); this.keyboardManager = null; } diff --git a/blocksuite/affine/blocks/block-root/src/page/page-root-spec.ts b/blocksuite/affine/blocks/block-root/src/page/page-root-spec.ts index 18076587b6..bae605ae2b 100644 --- a/blocksuite/affine/blocks/block-root/src/page/page-root-spec.ts +++ b/blocksuite/affine/blocks/block-root/src/page/page-root-spec.ts @@ -3,6 +3,7 @@ import { BlockViewExtension, WidgetViewExtension } from '@blocksuite/block-std'; import type { ExtensionType } from '@blocksuite/store'; import { literal, unsafeStatic } from 'lit/static-html.js'; +import { PageClipboard } from '../clipboard/page-clipboard.js'; import { CommonSpecs } from '../common-specs/index.js'; import { AFFINE_KEYBOARD_TOOLBAR_WIDGET } from '../widgets/keyboard-toolbar/index.js'; import { AFFINE_PAGE_DRAGGING_AREA_WIDGET } from '../widgets/page-dragging-area/page-dragging-area.js'; @@ -31,6 +32,7 @@ export const PageRootBlockSpec: ExtensionType[] = [ ...PageCommonExtension, BlockViewExtension('affine:page', literal`affine-page-root`), keyboardToolbarWidget, + PageClipboard, ].flat(); export const PreviewPageRootBlockSpec: ExtensionType[] = [ diff --git a/blocksuite/affine/blocks/block-root/src/preview/preview-root-block.ts b/blocksuite/affine/blocks/block-root/src/preview/preview-root-block.ts index c4617bd9ce..09bff43e7c 100644 --- a/blocksuite/affine/blocks/block-root/src/preview/preview-root-block.ts +++ b/blocksuite/affine/blocks/block-root/src/preview/preview-root-block.ts @@ -4,8 +4,6 @@ import { BlockComponent } from '@blocksuite/block-std'; import { css, html } from 'lit'; import { repeat } from 'lit/directives/repeat.js'; -import { ReadOnlyClipboard } from '../clipboard/readonly-clipboard'; - export class PreviewRootBlockComponent extends BlockComponent { static override styles = css` affine-preview-root { @@ -13,16 +11,12 @@ 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() { diff --git a/blocksuite/affine/blocks/block-root/src/preview/preview-root-spec.ts b/blocksuite/affine/blocks/block-root/src/preview/preview-root-spec.ts deleted file mode 100644 index 5c45a0ff47..0000000000 --- a/blocksuite/affine/blocks/block-root/src/preview/preview-root-spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std'; -import type { ExtensionType } from '@blocksuite/store'; -import { literal } from 'lit/static-html.js'; - -import { PageRootService } from '../page/page-root-service.js'; - -export const PreviewPageSpec: ExtensionType[] = [ - FlavourExtension('affine:page'), - PageRootService, - BlockViewExtension('affine:page', literal`affine-preview-root`), -]; diff --git a/packages/frontend/core/src/blocksuite/ai/entries/edgeless/actions-config.ts b/packages/frontend/core/src/blocksuite/ai/entries/edgeless/actions-config.ts index 65980403ef..e2af34d38a 100644 --- a/packages/frontend/core/src/blocksuite/ai/entries/edgeless/actions-config.ts +++ b/packages/frontend/core/src/blocksuite/ai/entries/edgeless/actions-config.ts @@ -1,4 +1,7 @@ -import { splitElements } from '@blocksuite/affine/blocks/root'; +import { + EdgelessClipboardController, + splitElements, +} from '@blocksuite/affine/blocks/root'; import { AIStarIconWithAnimation } from '@blocksuite/affine/components/icons'; import { MindmapElementModel, @@ -52,7 +55,6 @@ import { mindMapToMarkdown } from '../../utils/edgeless'; import { canvasToBlob, randomSeed } from '../../utils/image'; import { getCopilotSelectedElems, - getEdgelessRootFromEditor, imageCustomInput, } from '../../utils/selection-utils'; @@ -312,17 +314,16 @@ const generateGroup: AIItemGroupConfig = { }; } - // image to image - const edgelessRoot = getEdgelessRootFromEditor(host); - const canvas = await edgelessRoot.clipboardController.toCanvas( - images, - pureShapes, - { - dpr: 1, - padding: 0, - background: 'white', - } + const edgelessClipboard = host.std.getOptional( + EdgelessClipboardController ); + if (!edgelessClipboard) return; + // image to image + const canvas = await edgelessClipboard.toCanvas(images, pureShapes, { + dpr: 1, + padding: 0, + background: 'white', + }); if (!canvas) return; const png = await canvasToBlob(canvas); @@ -445,8 +446,11 @@ const generateGroup: AIItemGroupConfig = { content = aiPanel.inputText?.trim() || ''; } - const edgelessRoot = getEdgelessRootFromEditor(host); - const canvas = await edgelessRoot.clipboardController.toCanvas( + const edgelessClipboard = host.std.getOptional( + EdgelessClipboardController + ); + if (!edgelessClipboard) return; + const canvas = await edgelessClipboard.toCanvas( [...notes, ...frames, ...images], shapes, { diff --git a/packages/frontend/core/src/blocksuite/ai/utils/selection-utils.ts b/packages/frontend/core/src/blocksuite/ai/utils/selection-utils.ts index a54d5beb36..0040e25d21 100644 --- a/packages/frontend/core/src/blocksuite/ai/utils/selection-utils.ts +++ b/packages/frontend/core/src/blocksuite/ai/utils/selection-utils.ts @@ -3,7 +3,11 @@ import { GfxControllerIdentifier, type GfxModel, } from '@blocksuite/affine/block-std/gfx'; -import { isCanvasElement, splitElements } from '@blocksuite/affine/blocks/root'; +import { + EdgelessClipboardController, + isCanvasElement, + splitElements, +} from '@blocksuite/affine/blocks/root'; import { getSurfaceBlock, type SurfaceBlockComponent, @@ -73,10 +77,9 @@ export async function elementsToCanvas(host: EditorHost, elements: GfxModel[]) { } try { - const canvas = await edgelessRoot.clipboardController.toCanvas( - blockElements, - shapes - ); + const canvas = await edgelessRoot.std + .get(EdgelessClipboardController) + .toCanvas(blockElements, shapes); if (!canvas) { return; }