refactor: redesign element transform manager interface (#11679)

### Change
- Rename `ElementTransformManager` -> `InteractivityManager`
- Now you can `event.on` and `action.onXXX` method to extend interactivity behaviour. The old approach of overriding methods directly is deprecated.
This commit is contained in:
doouding
2025-04-22 08:18:22 +00:00
parent e457e2f8a8
commit 52953ce8e3
22 changed files with 507 additions and 389 deletions
@@ -21,16 +21,16 @@ import { NoteTool } from '@blocksuite/affine-gfx-note';
import { ShapeTool } from '@blocksuite/affine-gfx-shape';
import { TemplateTool } from '@blocksuite/affine-gfx-template';
import { TextTool } from '@blocksuite/affine-gfx-text';
import { ElementTransformManager } from '@blocksuite/std/gfx';
import { InteractivityManager } from '@blocksuite/std/gfx';
import type { ExtensionType } from '@blocksuite/store';
import { EdgelessElementToolbarExtension } from './configs/toolbar';
import { EdgelessRootBlockSpec } from './edgeless-root-spec.js';
import { DblClickAddEdgelessText } from './element-transform/dblclick-add-edgeless-text.js';
import { SnapExtension } from './element-transform/snap-manager.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 { DblClickAddEdgelessText } from './interact-extensions/dblclick-add-edgeless-text.js';
import { SnapExtension } from './interact-extensions/snap-manager.js';
import { EditPropsMiddlewareBuilder } from './middlewares/base.js';
import { SnapOverlay } from './utils/snap-manager.js';
@@ -51,7 +51,7 @@ export const EdgelessToolExtension: ExtensionType[] = [
];
export const EdgelessEditExtensions: ExtensionType[] = [
ElementTransformManager,
InteractivityManager,
ConnectorFilter,
SnapExtension,
MindMapDragExtension,
@@ -1,49 +0,0 @@
import {
addText,
insertEdgelessTextCommand,
} from '@blocksuite/affine-gfx-text';
import {
FeatureFlagService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import type { PointerEventState } from '@blocksuite/std';
import { TransformExtension } from '@blocksuite/std/gfx';
export class DblClickAddEdgelessText extends TransformExtension {
static override key = 'dbl-click-add-edgeless-text';
override dblClick(e: PointerEventState): void {
const textFlag = this.std.store
.get(FeatureFlagService)
.getFlag('enable_edgeless_text');
const picked = this.gfx.getElementByPoint(
...this.gfx.viewport.toModelCoord(e.x, e.y)
);
if (picked) {
return;
}
if (textFlag) {
const [x, y] = this.gfx.viewport.toModelCoord(e.x, e.y);
this.std.command.exec(insertEdgelessTextCommand, { x, y });
} else {
const edgelessView = this.std.view.getBlock(
this.std.store.root?.id || ''
);
if (edgelessView) {
addText(edgelessView, e);
}
}
this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
control: 'canvas:dbclick',
page: 'whiteboard editor',
module: 'toolbar',
segment: 'toolbar',
type: 'text',
});
return;
}
}
@@ -1,64 +0,0 @@
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
import { MindmapElementModel } from '@blocksuite/affine-model';
import type { Bound } from '@blocksuite/global/gfx';
import {
type DragExtensionInitializeContext,
type ExtensionDragMoveContext,
type GfxModel,
TransformExtension,
} from '@blocksuite/std/gfx';
import type { SnapOverlay } from '../utils/snap-manager';
export class SnapExtension extends TransformExtension {
static override key = 'snap-manager';
get snapOverlay() {
return this.std.getOptional(
OverlayIdentifier('snap-manager')
) as SnapOverlay;
}
override onDragInitialize(initContext: DragExtensionInitializeContext) {
const snapOverlay = this.snapOverlay;
if (!snapOverlay) {
return {};
}
let alignBound: Bound;
return {
onDragStart() {
alignBound = snapOverlay.setMovingElements(
initContext.elements,
initContext.elements.reduce((pre, elem) => {
if (elem.group instanceof MindmapElementModel) {
pre.push(elem.group);
}
return pre;
}, [] as GfxModel[])
);
},
onDragMove(context: ExtensionDragMoveContext) {
if (
context.elements.length === 0 ||
alignBound.w === 0 ||
alignBound.h === 0
) {
return;
}
const currentBound = alignBound.moveDelta(context.dx, context.dy);
const alignRst = snapOverlay.align(currentBound);
context.dx = alignRst.dx + context.dx;
context.dy = alignRst.dy + context.dy;
},
onDragEnd() {
snapOverlay.clear();
},
};
}
}
@@ -22,9 +22,9 @@ import {
BaseTool,
getTopElements,
type GfxModel,
InteractivityIdentifier,
isGfxGroupCompatibleModel,
type PointTestOptions,
TransformManagerIdentifier,
} from '@blocksuite/std/gfx';
import { effect } from '@preact/signals-core';
@@ -196,8 +196,8 @@ export class DefaultTool extends BaseTool {
return this.gfx.selection;
}
get elementTransformMgr() {
return this.std.getOptional(TransformManagerIdentifier);
get interactivity() {
return this.std.getOptional(InteractivityIdentifier);
}
private get frameOverlay() {
@@ -380,9 +380,9 @@ export class DefaultTool extends BaseTool {
}
if (this.dragType === DefaultModeDragType.ContentMoving) {
if (this.elementTransformMgr) {
if (this.interactivity) {
this.doc.captureSync();
this.elementTransformMgr.initializeDrag({
this.interactivity.initializeDrag({
movingElements: this._toBeMoved,
event: event.raw,
onDragEnd: () => {
@@ -397,12 +397,12 @@ export class DefaultTool extends BaseTool {
override click(e: PointerEventState) {
if (this.doc.readonly) return;
if (!this.elementTransformMgr?.dispatchOnSelected(e)) {
if (!this.interactivity?.dispatchOnSelected(e)) {
this.edgelessSelectionManager.clear();
resetNativeSelection(null);
}
this.elementTransformMgr?.dispatch('click', e);
this.interactivity?.dispatch('click', e);
}
override deactivate() {
@@ -424,7 +424,7 @@ export class DefaultTool extends BaseTool {
return;
}
this.elementTransformMgr?.dispatch('dblclick', e);
this.interactivity?.dispatch('dblclick', e);
}
override dragEnd() {
@@ -523,7 +523,7 @@ export class DefaultTool extends BaseTool {
}
override pointerDown(e: PointerEventState): void {
this.elementTransformMgr?.dispatch('pointerdown', e);
this.interactivity?.dispatch('pointerdown', e);
}
override pointerMove(e: PointerEventState) {
@@ -542,11 +542,11 @@ export class DefaultTool extends BaseTool {
this.frameOverlay.clear();
}
this.elementTransformMgr?.dispatch('pointermove', e);
this.interactivity?.dispatch('pointermove', e);
}
override pointerUp(e: PointerEventState) {
this.elementTransformMgr?.dispatch('pointerup', e);
this.interactivity?.dispatch('pointerup', e);
}
override tripleClick() {}
@@ -0,0 +1,50 @@
import {
addText,
insertEdgelessTextCommand,
} from '@blocksuite/affine-gfx-text';
import {
FeatureFlagService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import type { PointerEventState } from '@blocksuite/std';
import { InteractivityExtension } from '@blocksuite/std/gfx';
export class DblClickAddEdgelessText extends InteractivityExtension {
static override key = 'dbl-click-add-edgeless-text';
override mounted() {
this.event.on('dblclick', (e: PointerEventState) => {
const textFlag = this.std.store
.get(FeatureFlagService)
.getFlag('enable_edgeless_text');
const picked = this.gfx.getElementByPoint(
...this.gfx.viewport.toModelCoord(e.x, e.y)
);
if (picked) {
return;
}
if (textFlag) {
const [x, y] = this.gfx.viewport.toModelCoord(e.x, e.y);
this.std.command.exec(insertEdgelessTextCommand, { x, y });
} else {
const edgelessView = this.std.view.getBlock(
this.std.store.root?.id || ''
);
if (edgelessView) {
addText(edgelessView, e);
}
}
this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
control: 'canvas:dbclick',
page: 'whiteboard editor',
module: 'toolbar',
segment: 'toolbar',
type: 'text',
});
});
}
}
@@ -0,0 +1,68 @@
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
import { MindmapElementModel } from '@blocksuite/affine-model';
import type { Bound } from '@blocksuite/global/gfx';
import {
type DragExtensionInitializeContext,
type ExtensionDragMoveContext,
type GfxModel,
InteractivityExtension,
} from '@blocksuite/std/gfx';
import type { SnapOverlay } from '../utils/snap-manager';
export class SnapExtension extends InteractivityExtension {
static override key = 'snap-manager';
get snapOverlay() {
return this.std.getOptional(
OverlayIdentifier('snap-manager')
) as SnapOverlay;
}
override mounted(): void {
this.action.onDragInitialize(
(initContext: DragExtensionInitializeContext) => {
const snapOverlay = this.snapOverlay;
if (!snapOverlay) {
return {};
}
let alignBound: Bound;
return {
onDragStart() {
alignBound = snapOverlay.setMovingElements(
initContext.elements,
initContext.elements.reduce((pre, elem) => {
if (elem.group instanceof MindmapElementModel) {
pre.push(elem.group);
}
return pre;
}, [] as GfxModel[])
);
},
onDragMove(context: ExtensionDragMoveContext) {
if (
context.elements.length === 0 ||
alignBound.w === 0 ||
alignBound.h === 0
) {
return;
}
const currentBound = alignBound.moveDelta(context.dx, context.dy);
const alignRst = snapOverlay.align(currentBound);
context.dx = alignRst.dx + context.dx;
context.dy = alignRst.dy + context.dy;
},
onDragEnd() {
snapOverlay.clear();
},
};
}
);
}
}