diff --git a/blocksuite/affine/blocks/root/src/edgeless/edgeless-builtin-spec.ts b/blocksuite/affine/blocks/root/src/edgeless/edgeless-builtin-spec.ts index 88331a4a1d..1603272272 100644 --- a/blocksuite/affine/blocks/root/src/edgeless/edgeless-builtin-spec.ts +++ b/blocksuite/affine/blocks/root/src/edgeless/edgeless-builtin-spec.ts @@ -29,6 +29,7 @@ import { EdgelessRootBlockSpec } from './edgeless-root-spec.js'; import { DefaultTool } from './gfx-tool/default-tool.js'; import { EmptyTool } from './gfx-tool/empty-tool.js'; import { PanTool } from './gfx-tool/pan-tool.js'; +import { AltCloneExtension } from './interact-extensions/clone-ext.js'; import { DblClickAddEdgelessText } from './interact-extensions/dblclick-add-edgeless-text.js'; import { SnapExtension } from './interact-extensions/snap-manager.js'; import { EditPropsMiddlewareBuilder } from './middlewares/base.js'; @@ -63,6 +64,7 @@ export const EdgelessBuiltInManager: ExtensionType[] = [ ConnectionOverlay, MindMapIndicatorOverlay, SnapOverlay, + AltCloneExtension, EditPropsMiddlewareBuilder, EdgelessElementToolbarExtension, ].flat(); diff --git a/blocksuite/affine/blocks/root/src/edgeless/gfx-tool/default-tool.ts b/blocksuite/affine/blocks/root/src/edgeless/gfx-tool/default-tool.ts index 93ad2b9486..d2a828c3ce 100644 --- a/blocksuite/affine/blocks/root/src/edgeless/gfx-tool/default-tool.ts +++ b/blocksuite/affine/blocks/root/src/edgeless/gfx-tool/default-tool.ts @@ -16,8 +16,8 @@ import { import { resetNativeSelection } from '@blocksuite/affine-shared/utils'; import { DisposableGroup } from '@blocksuite/global/disposable'; import type { IVec } from '@blocksuite/global/gfx'; -import { Bound, getCommonBoundWithRotation, Vec } from '@blocksuite/global/gfx'; -import type { BlockComponent, PointerEventState } from '@blocksuite/std'; +import { Bound, Vec } from '@blocksuite/global/gfx'; +import type { PointerEventState } from '@blocksuite/std'; import { BaseTool, getTopElements, @@ -28,8 +28,6 @@ import { } from '@blocksuite/std/gfx'; import { effect } from '@preact/signals-core'; -import { createElementsFromClipboardDataCommand } from '../clipboard/command.js'; -import { prepareCloneData } from '../utils/clone-utils.js'; import { calPanDelta } from '../utils/panning-utils.js'; import { isCanvasElement } from '../utils/query.js'; import { DefaultModeDragType } from './default-tool-ext/ext.js'; @@ -170,10 +168,6 @@ export class DefaultTool extends BaseTool { enableHover = true; - private get _edgeless(): BlockComponent | null { - return this.std.view.getBlock(this.doc.root!.id); - } - /** * Get the end position of the dragging area in the model coordinate */ @@ -205,22 +199,13 @@ export class DefaultTool extends BaseTool { } private async _cloneContent() { - if (!this._edgeless) return; + const clonedResult = await this.interactivity?.requestElementsClone({ + elements: this._toBeMoved, + }); - const snapshot = prepareCloneData(this._toBeMoved, this.std); + if (!clonedResult) return; - const bound = getCommonBoundWithRotation(this._toBeMoved); - const [_, { createdElementsPromise }] = this.std.command.exec( - createElementsFromClipboardDataCommand, - { - elementsRawData: snapshot, - pasteCenter: bound.center, - } - ); - if (!createdElementsPromise) return; - const { canvasElements, blockModels } = await createdElementsPromise; - - this._toBeMoved = [...canvasElements, ...blockModels]; + this._toBeMoved = clonedResult.elements; this.edgelessSelectionManager.set({ elements: this._toBeMoved.map(e => e.id), editing: false, diff --git a/blocksuite/affine/blocks/root/src/edgeless/interact-extensions/clone-ext.ts b/blocksuite/affine/blocks/root/src/edgeless/interact-extensions/clone-ext.ts new file mode 100644 index 0000000000..17ad4789fe --- /dev/null +++ b/blocksuite/affine/blocks/root/src/edgeless/interact-extensions/clone-ext.ts @@ -0,0 +1,32 @@ +import { getCommonBoundWithRotation } from '@blocksuite/global/gfx'; +import { type GfxModel, InteractivityExtension } from '@blocksuite/std/gfx'; + +import { createElementsFromClipboardDataCommand } from '../clipboard/command.js'; +import { prepareCloneData } from '../utils/clone-utils.js'; + +export class AltCloneExtension extends InteractivityExtension { + static override key = 'alt-clone'; + + override mounted(): void { + this.action.onRequestElementsClone(async context => { + const { elements: elementsToClone } = context; + const snapshot = prepareCloneData(elementsToClone, this.std); + + const bound = getCommonBoundWithRotation(elementsToClone); + const [_, { createdElementsPromise }] = this.std.command.exec( + createElementsFromClipboardDataCommand, + { + elementsRawData: snapshot, + pasteCenter: bound.center, + } + ); + + if (!createdElementsPromise) return; + const { canvasElements, blockModels } = await createdElementsPromise; + + return { + elements: [...canvasElements, ...blockModels] as GfxModel[], + }; + }); + } +} diff --git a/blocksuite/framework/std/src/gfx/interactivity/extension/base.ts b/blocksuite/framework/std/src/gfx/interactivity/extension/base.ts index 66d950f848..00b50d088f 100644 --- a/blocksuite/framework/std/src/gfx/interactivity/extension/base.ts +++ b/blocksuite/framework/std/src/gfx/interactivity/extension/base.ts @@ -5,7 +5,9 @@ import { Extension } from '@blocksuite/store'; import type { PointerEventState } from '../../../event/index.js'; import type { GfxController } from '../../controller.js'; import { GfxControllerIdentifier } from '../../identifiers.js'; +import type { GfxModel } from '../../model/model.js'; import type { SupportedEvents } from '../event.js'; +import type { ExtensionElementsCloneContext } from '../types/clone.js'; import type { DragExtensionInitializeContext, ExtensionDragEndContext, @@ -105,11 +107,22 @@ type ActionContextMap = { clear?: () => void; }; }; + elementsClone: { + context: ExtensionElementsCloneContext; + returnType: Promise< + | { + elements: GfxModel[]; + } + | undefined + >; + }; }; export class InteractivityActionAPI { private readonly _handlers: Partial<{ - dragInitialize: Parameters[0]; + [K in keyof ActionContextMap]: ( + ctx: ActionContextMap[K]['context'] + ) => ActionContextMap[K]['returnType']; }> = {}; onDragInitialize( @@ -124,6 +137,18 @@ export class InteractivityActionAPI { }; } + onRequestElementsClone( + handler: ( + ctx: ActionContextMap['elementsClone']['context'] + ) => ActionContextMap['elementsClone']['returnType'] + ) { + this._handlers['elementsClone'] = handler; + + return () => { + return delete this._handlers['elementsClone']; + }; + } + emit( event: K, context: ActionContextMap[K]['context'] diff --git a/blocksuite/framework/std/src/gfx/interactivity/manager.ts b/blocksuite/framework/std/src/gfx/interactivity/manager.ts index 809e676cb4..25d196e4db 100644 --- a/blocksuite/framework/std/src/gfx/interactivity/manager.ts +++ b/blocksuite/framework/std/src/gfx/interactivity/manager.ts @@ -12,6 +12,7 @@ import { InteractivityExtensionIdentifier, } from './extension/base.js'; import { GfxViewEventManager } from './gfx-view-event-handler.js'; +import type { RequestElementsCloneContext } from './types/clone.js'; import type { DragExtensionInitializeContext, DragInitializationOption, @@ -288,4 +289,21 @@ export class InteractivityManager extends GfxExtension { listenEvent(); dragStart(); } + + requestElementsClone(options: RequestElementsCloneContext) { + const extensions = this.interactExtensions; + + for (let ext of extensions.values()) { + const cloneData = (ext.action as InteractivityActionAPI).emit( + 'elementsClone', + options + ); + + if (cloneData) { + return cloneData; + } + } + + return Promise.resolve(undefined); + } } diff --git a/blocksuite/framework/std/src/gfx/interactivity/types/clone.ts b/blocksuite/framework/std/src/gfx/interactivity/types/clone.ts new file mode 100644 index 0000000000..a1433fee26 --- /dev/null +++ b/blocksuite/framework/std/src/gfx/interactivity/types/clone.ts @@ -0,0 +1,7 @@ +import type { GfxModel } from '../../model/model'; + +export type ExtensionElementsCloneContext = { + elements: GfxModel[]; +}; + +export type RequestElementsCloneContext = ExtensionElementsCloneContext;