refactor: migrate elements clone from default-tool to extension (#11703)

### Changed
- Add `onRequestElementsClone` action to allow extension to define how to clone gfx elements
This commit is contained in:
doouding
2025-04-22 08:18:23 +00:00
parent 52953ce8e3
commit 21bf009553
6 changed files with 92 additions and 23 deletions

View File

@@ -29,6 +29,7 @@ import { EdgelessRootBlockSpec } from './edgeless-root-spec.js';
import { DefaultTool } from './gfx-tool/default-tool.js'; import { DefaultTool } from './gfx-tool/default-tool.js';
import { EmptyTool } from './gfx-tool/empty-tool.js'; import { EmptyTool } from './gfx-tool/empty-tool.js';
import { PanTool } from './gfx-tool/pan-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 { DblClickAddEdgelessText } from './interact-extensions/dblclick-add-edgeless-text.js';
import { SnapExtension } from './interact-extensions/snap-manager.js'; import { SnapExtension } from './interact-extensions/snap-manager.js';
import { EditPropsMiddlewareBuilder } from './middlewares/base.js'; import { EditPropsMiddlewareBuilder } from './middlewares/base.js';
@@ -63,6 +64,7 @@ export const EdgelessBuiltInManager: ExtensionType[] = [
ConnectionOverlay, ConnectionOverlay,
MindMapIndicatorOverlay, MindMapIndicatorOverlay,
SnapOverlay, SnapOverlay,
AltCloneExtension,
EditPropsMiddlewareBuilder, EditPropsMiddlewareBuilder,
EdgelessElementToolbarExtension, EdgelessElementToolbarExtension,
].flat(); ].flat();

View File

@@ -16,8 +16,8 @@ import {
import { resetNativeSelection } from '@blocksuite/affine-shared/utils'; import { resetNativeSelection } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable'; import { DisposableGroup } from '@blocksuite/global/disposable';
import type { IVec } from '@blocksuite/global/gfx'; import type { IVec } from '@blocksuite/global/gfx';
import { Bound, getCommonBoundWithRotation, Vec } from '@blocksuite/global/gfx'; import { Bound, Vec } from '@blocksuite/global/gfx';
import type { BlockComponent, PointerEventState } from '@blocksuite/std'; import type { PointerEventState } from '@blocksuite/std';
import { import {
BaseTool, BaseTool,
getTopElements, getTopElements,
@@ -28,8 +28,6 @@ import {
} from '@blocksuite/std/gfx'; } from '@blocksuite/std/gfx';
import { effect } from '@preact/signals-core'; 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 { calPanDelta } from '../utils/panning-utils.js';
import { isCanvasElement } from '../utils/query.js'; import { isCanvasElement } from '../utils/query.js';
import { DefaultModeDragType } from './default-tool-ext/ext.js'; import { DefaultModeDragType } from './default-tool-ext/ext.js';
@@ -170,10 +168,6 @@ export class DefaultTool extends BaseTool {
enableHover = true; 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 * Get the end position of the dragging area in the model coordinate
*/ */
@@ -205,22 +199,13 @@ export class DefaultTool extends BaseTool {
} }
private async _cloneContent() { 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); this._toBeMoved = clonedResult.elements;
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.edgelessSelectionManager.set({ this.edgelessSelectionManager.set({
elements: this._toBeMoved.map(e => e.id), elements: this._toBeMoved.map(e => e.id),
editing: false, editing: false,

View File

@@ -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[],
};
});
}
}

View File

@@ -5,7 +5,9 @@ import { Extension } from '@blocksuite/store';
import type { PointerEventState } from '../../../event/index.js'; import type { PointerEventState } from '../../../event/index.js';
import type { GfxController } from '../../controller.js'; import type { GfxController } from '../../controller.js';
import { GfxControllerIdentifier } from '../../identifiers.js'; import { GfxControllerIdentifier } from '../../identifiers.js';
import type { GfxModel } from '../../model/model.js';
import type { SupportedEvents } from '../event.js'; import type { SupportedEvents } from '../event.js';
import type { ExtensionElementsCloneContext } from '../types/clone.js';
import type { import type {
DragExtensionInitializeContext, DragExtensionInitializeContext,
ExtensionDragEndContext, ExtensionDragEndContext,
@@ -105,11 +107,22 @@ type ActionContextMap = {
clear?: () => void; clear?: () => void;
}; };
}; };
elementsClone: {
context: ExtensionElementsCloneContext;
returnType: Promise<
| {
elements: GfxModel[];
}
| undefined
>;
};
}; };
export class InteractivityActionAPI { export class InteractivityActionAPI {
private readonly _handlers: Partial<{ private readonly _handlers: Partial<{
dragInitialize: Parameters<InteractivityActionAPI['onDragInitialize']>[0]; [K in keyof ActionContextMap]: (
ctx: ActionContextMap[K]['context']
) => ActionContextMap[K]['returnType'];
}> = {}; }> = {};
onDragInitialize( 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<K extends keyof ActionContextMap>( emit<K extends keyof ActionContextMap>(
event: K, event: K,
context: ActionContextMap[K]['context'] context: ActionContextMap[K]['context']

View File

@@ -12,6 +12,7 @@ import {
InteractivityExtensionIdentifier, InteractivityExtensionIdentifier,
} from './extension/base.js'; } from './extension/base.js';
import { GfxViewEventManager } from './gfx-view-event-handler.js'; import { GfxViewEventManager } from './gfx-view-event-handler.js';
import type { RequestElementsCloneContext } from './types/clone.js';
import type { import type {
DragExtensionInitializeContext, DragExtensionInitializeContext,
DragInitializationOption, DragInitializationOption,
@@ -288,4 +289,21 @@ export class InteractivityManager extends GfxExtension {
listenEvent(); listenEvent();
dragStart(); 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);
}
} }

View File

@@ -0,0 +1,7 @@
import type { GfxModel } from '../../model/model';
export type ExtensionElementsCloneContext = {
elements: GfxModel[];
};
export type RequestElementsCloneContext = ExtensionElementsCloneContext;