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

View File

@@ -5,12 +5,9 @@ import {
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
type DragExtensionInitializeContext, type DragExtensionInitializeContext,
type ExtensionDragEndContext,
type ExtensionDragMoveContext,
type ExtensionDragStartContext,
getTopElements, getTopElements,
GfxExtensionIdentifier, GfxExtensionIdentifier,
TransformExtension, InteractivityExtension,
} from '@blocksuite/std/gfx'; } from '@blocksuite/std/gfx';
import { import {
@@ -19,7 +16,7 @@ import {
isFrameBlock, isFrameBlock,
} from './frame-manager'; } from './frame-manager';
export class FrameHighlightManager extends TransformExtension { export class FrameHighlightManager extends InteractivityExtension {
static override key = 'frame-highlight-manager'; static override key = 'frame-highlight-manager';
get frameMgr() { get frameMgr() {
@@ -32,12 +29,8 @@ export class FrameHighlightManager extends TransformExtension {
return this.std.getOptional(OverlayIdentifier('frame')) as FrameOverlay; return this.std.getOptional(OverlayIdentifier('frame')) as FrameOverlay;
} }
override onDragInitialize(_: DragExtensionInitializeContext): { override mounted() {
onDragStart?: (context: ExtensionDragStartContext) => void; this.action.onDragInitialize((_: DragExtensionInitializeContext) => {
onDragMove?: (context: ExtensionDragMoveContext) => void;
onDragEnd?: (context: ExtensionDragEndContext) => void;
clear?: () => void;
} {
if (!this.frameMgr || !this.frameHighlightOverlay) { if (!this.frameMgr || !this.frameHighlightOverlay) {
return {}; return {};
} }
@@ -84,5 +77,6 @@ export class FrameHighlightManager extends TransformExtension {
frameHighlightOverlay.clear(); frameHighlightOverlay.clear();
}, },
}; };
});
} }
} }

View File

@@ -21,16 +21,16 @@ import { NoteTool } from '@blocksuite/affine-gfx-note';
import { ShapeTool } from '@blocksuite/affine-gfx-shape'; import { ShapeTool } from '@blocksuite/affine-gfx-shape';
import { TemplateTool } from '@blocksuite/affine-gfx-template'; import { TemplateTool } from '@blocksuite/affine-gfx-template';
import { TextTool } from '@blocksuite/affine-gfx-text'; 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 type { ExtensionType } from '@blocksuite/store';
import { EdgelessElementToolbarExtension } from './configs/toolbar'; import { EdgelessElementToolbarExtension } from './configs/toolbar';
import { EdgelessRootBlockSpec } from './edgeless-root-spec.js'; 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 { 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 { DblClickAddEdgelessText } from './interact-extensions/dblclick-add-edgeless-text.js';
import { SnapExtension } from './interact-extensions/snap-manager.js';
import { EditPropsMiddlewareBuilder } from './middlewares/base.js'; import { EditPropsMiddlewareBuilder } from './middlewares/base.js';
import { SnapOverlay } from './utils/snap-manager.js'; import { SnapOverlay } from './utils/snap-manager.js';
@@ -51,7 +51,7 @@ export const EdgelessToolExtension: ExtensionType[] = [
]; ];
export const EdgelessEditExtensions: ExtensionType[] = [ export const EdgelessEditExtensions: ExtensionType[] = [
ElementTransformManager, InteractivityManager,
ConnectorFilter, ConnectorFilter,
SnapExtension, SnapExtension,
MindMapDragExtension, MindMapDragExtension,

View File

@@ -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;
}
}

View File

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

View File

@@ -22,9 +22,9 @@ import {
BaseTool, BaseTool,
getTopElements, getTopElements,
type GfxModel, type GfxModel,
InteractivityIdentifier,
isGfxGroupCompatibleModel, isGfxGroupCompatibleModel,
type PointTestOptions, type PointTestOptions,
TransformManagerIdentifier,
} from '@blocksuite/std/gfx'; } from '@blocksuite/std/gfx';
import { effect } from '@preact/signals-core'; import { effect } from '@preact/signals-core';
@@ -196,8 +196,8 @@ export class DefaultTool extends BaseTool {
return this.gfx.selection; return this.gfx.selection;
} }
get elementTransformMgr() { get interactivity() {
return this.std.getOptional(TransformManagerIdentifier); return this.std.getOptional(InteractivityIdentifier);
} }
private get frameOverlay() { private get frameOverlay() {
@@ -380,9 +380,9 @@ export class DefaultTool extends BaseTool {
} }
if (this.dragType === DefaultModeDragType.ContentMoving) { if (this.dragType === DefaultModeDragType.ContentMoving) {
if (this.elementTransformMgr) { if (this.interactivity) {
this.doc.captureSync(); this.doc.captureSync();
this.elementTransformMgr.initializeDrag({ this.interactivity.initializeDrag({
movingElements: this._toBeMoved, movingElements: this._toBeMoved,
event: event.raw, event: event.raw,
onDragEnd: () => { onDragEnd: () => {
@@ -397,12 +397,12 @@ export class DefaultTool extends BaseTool {
override click(e: PointerEventState) { override click(e: PointerEventState) {
if (this.doc.readonly) return; if (this.doc.readonly) return;
if (!this.elementTransformMgr?.dispatchOnSelected(e)) { if (!this.interactivity?.dispatchOnSelected(e)) {
this.edgelessSelectionManager.clear(); this.edgelessSelectionManager.clear();
resetNativeSelection(null); resetNativeSelection(null);
} }
this.elementTransformMgr?.dispatch('click', e); this.interactivity?.dispatch('click', e);
} }
override deactivate() { override deactivate() {
@@ -424,7 +424,7 @@ export class DefaultTool extends BaseTool {
return; return;
} }
this.elementTransformMgr?.dispatch('dblclick', e); this.interactivity?.dispatch('dblclick', e);
} }
override dragEnd() { override dragEnd() {
@@ -523,7 +523,7 @@ export class DefaultTool extends BaseTool {
} }
override pointerDown(e: PointerEventState): void { override pointerDown(e: PointerEventState): void {
this.elementTransformMgr?.dispatch('pointerdown', e); this.interactivity?.dispatch('pointerdown', e);
} }
override pointerMove(e: PointerEventState) { override pointerMove(e: PointerEventState) {
@@ -542,11 +542,11 @@ export class DefaultTool extends BaseTool {
this.frameOverlay.clear(); this.frameOverlay.clear();
} }
this.elementTransformMgr?.dispatch('pointermove', e); this.interactivity?.dispatch('pointermove', e);
} }
override pointerUp(e: PointerEventState) { override pointerUp(e: PointerEventState) {
this.elementTransformMgr?.dispatch('pointerup', e); this.interactivity?.dispatch('pointerup', e);
} }
override tripleClick() {} override tripleClick() {}

View File

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

View File

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

View File

@@ -1,12 +1,14 @@
import { ConnectorElementModel } from '@blocksuite/affine-model'; import { ConnectorElementModel } from '@blocksuite/affine-model';
import { import {
type DragExtensionInitializeContext, type DragExtensionInitializeContext,
TransformExtension, InteractivityExtension,
} from '@blocksuite/std/gfx'; } from '@blocksuite/std/gfx';
export class ConnectorFilter extends TransformExtension { export class ConnectorFilter extends InteractivityExtension {
static override key = 'connector-filter'; static override key = 'connector-filter';
override onDragInitialize(context: DragExtensionInitializeContext) {
override mounted() {
this.action.onDragInitialize((context: DragExtensionInitializeContext) => {
let hasConnectorFlag = false; let hasConnectorFlag = false;
const elementSet = new Set(context.elements.map(elem => elem.id)); const elementSet = new Set(context.elements.map(elem => elem.id));
@@ -35,5 +37,6 @@ export class ConnectorFilter extends TransformExtension {
} }
return {}; return {};
});
} }
} }

View File

@@ -1,7 +1,7 @@
export * from './adapter'; export * from './adapter';
export * from './element-renderer'; export * from './element-renderer';
export * from './element-transform';
export * from './indicator-overlay'; export * from './indicator-overlay';
export * from './interactivity';
export * from './toolbar/config'; export * from './toolbar/config';
export * from './toolbar/senior-tool'; export * from './toolbar/senior-tool';
export * from './utils'; export * from './utils';

View File

@@ -16,8 +16,8 @@ import {
type ExtensionDragStartContext, type ExtensionDragStartContext,
type GfxModel, type GfxModel,
type GfxPrimitiveElementModel, type GfxPrimitiveElementModel,
InteractivityExtension,
isGfxGroupCompatibleModel, isGfxGroupCompatibleModel,
TransformExtension,
} from '@blocksuite/std/gfx'; } from '@blocksuite/std/gfx';
import type { MindMapIndicatorOverlay } from '../indicator-overlay'; import type { MindMapIndicatorOverlay } from '../indicator-overlay';
@@ -43,7 +43,7 @@ type DragMindMapCtx = {
originalMindMapBound: Bound; originalMindMapBound: Bound;
}; };
export class MindMapDragExtension extends TransformExtension { export class MindMapDragExtension extends InteractivityExtension {
static override key = 'mind-map-drag'; static override key = 'mind-map-drag';
/** /**
* The response area of the mind map is calculated in real time. * The response area of the mind map is calculated in real time.
@@ -376,7 +376,8 @@ export class MindMapDragExtension extends TransformExtension {
}; };
} }
override onDragInitialize(context: DragExtensionInitializeContext) { override mounted() {
this.action.onDragInitialize((context: DragExtensionInitializeContext) => {
if (isSingleMindMapNode(context.elements)) { if (isSingleMindMapNode(context.elements)) {
const mindmap = context.elements[0].group as MindmapElementModel; const mindmap = context.elements[0].group as MindmapElementModel;
const mindmapNode = mindmap.getNode(context.elements[0].id)!; const mindmapNode = mindmap.getNode(context.elements[0].id)!;
@@ -449,5 +450,6 @@ export class MindMapDragExtension extends TransformExtension {
} }
return {}; return {};
});
} }
} }

View File

@@ -12,29 +12,26 @@ export {
} from '../utils/tree.js'; } from '../utils/tree.js';
export { GfxController } from './controller.js'; export { GfxController } from './controller.js';
export type { CursorType, StandardCursor } from './cursor.js'; export type { CursorType, StandardCursor } from './cursor.js';
export type {
DragExtensionInitializeContext,
DragInitializationOption,
ExtensionDragEndContext,
ExtensionDragMoveContext,
ExtensionDragStartContext,
} from './element-transform/drag.js';
export { CanvasEventHandler } from './element-transform/extension/canvas-event-handler.js';
export {
ElementTransformManager,
TransformExtension,
TransformExtensionIdentifier,
TransformManagerIdentifier,
} from './element-transform/transform-manager.js';
export type {
DragEndContext,
DragMoveContext,
DragStartContext,
} from './element-transform/view-transform.js';
export { type SelectedContext } from './element-transform/view-transform.js';
export { GfxExtension, GfxExtensionIdentifier } from './extension.js'; export { GfxExtension, GfxExtensionIdentifier } from './extension.js';
export { GridManager } from './grid.js'; export { GridManager } from './grid.js';
export { GfxControllerIdentifier } from './identifiers.js'; export { GfxControllerIdentifier } from './identifiers.js';
export type {
DragEndContext,
DragExtensionInitializeContext,
DragInitializationOption,
DragMoveContext,
DragStartContext,
ExtensionDragEndContext,
ExtensionDragMoveContext,
ExtensionDragStartContext,
SelectedContext,
} from './interactivity/index.js';
export {
GfxViewEventManager,
InteractivityExtension,
InteractivityIdentifier,
InteractivityManager,
} from './interactivity/index.js';
export { LayerManager, type ReorderingDirection } from './layer.js'; export { LayerManager, type ReorderingDirection } from './layer.js';
export type { export type {
GfxCompatibleInterface, GfxCompatibleInterface,

View File

@@ -0,0 +1,8 @@
export type SupportedEvents =
| 'click'
| 'dblclick'
| 'pointerdown'
| 'pointerenter'
| 'pointerleave'
| 'pointermove'
| 'pointerup';

View File

@@ -0,0 +1,141 @@
import { type Container, createIdentifier } from '@blocksuite/global/di';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
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 { SupportedEvents } from '../event.js';
import type {
DragExtensionInitializeContext,
ExtensionDragEndContext,
ExtensionDragMoveContext,
ExtensionDragStartContext,
} from '../types/drag.js';
export const InteractivityExtensionIdentifier =
createIdentifier<InteractivityExtension>('interactivity-extension');
export class InteractivityExtension extends Extension {
static key: string;
get std() {
return this.gfx.std;
}
event: Omit<InteractivityEventAPI, 'emit'> = new InteractivityEventAPI();
action: Omit<InteractivityActionAPI, 'emit'> = new InteractivityActionAPI();
constructor(protected readonly gfx: GfxController) {
super();
}
mounted() {}
/**
* Override this method should call `super.unmounted()`
*/
unmounted() {
this.event.destroy();
this.action.destroy();
}
static override setup(di: Container) {
if (!this.key) {
throw new BlockSuiteError(
ErrorCode.ValueNotExists,
'key is not defined in the InteractivityExtension'
);
}
di.add(
this as unknown as { new (gfx: GfxController): InteractivityExtension },
[GfxControllerIdentifier]
);
di.addImpl(InteractivityExtensionIdentifier(this.key), provider =>
provider.get(this)
);
}
}
export class InteractivityEventAPI {
private readonly _handlersMap = new Map<
SupportedEvents,
((evt: PointerEventState) => void)[]
>();
on(eventName: SupportedEvents, handler: (evt: PointerEventState) => void) {
const handlers = this._handlersMap.get(eventName) ?? [];
handlers.push(handler);
this._handlersMap.set(eventName, handlers);
return () => {
const idx = handlers.indexOf(handler);
if (idx > -1) {
handlers.splice(idx, 1);
}
};
}
emit(eventName: SupportedEvents, evt: PointerEventState) {
const handlers = this._handlersMap.get(eventName);
if (!handlers) {
return;
}
for (const handler of handlers) {
handler(evt);
}
}
destroy() {
this._handlersMap.clear();
}
}
type ActionContextMap = {
dragInitialize: {
context: DragExtensionInitializeContext;
returnType: {
onDragStart?: (context: ExtensionDragStartContext) => void;
onDragMove?: (context: ExtensionDragMoveContext) => void;
onDragEnd?: (context: ExtensionDragEndContext) => void;
clear?: () => void;
};
};
};
export class InteractivityActionAPI {
private readonly _handlers: Partial<{
dragInitialize: Parameters<InteractivityActionAPI['onDragInitialize']>[0];
}> = {};
onDragInitialize(
handler: (
ctx: ActionContextMap['dragInitialize']['context']
) => ActionContextMap['dragInitialize']['returnType']
) {
this._handlers['dragInitialize'] = handler;
return () => {
delete this._handlers['dragInitialize'];
};
}
emit<K extends keyof ActionContextMap>(
event: K,
context: ActionContextMap[K]['context']
): ActionContextMap[K]['returnType'] | undefined {
const handler = this._handlers[event];
return handler?.(context);
}
destroy() {
for (const key in this._handlers) {
delete this._handlers[key as keyof typeof this._handlers];
}
}
}

View File

@@ -1,11 +1,11 @@
import { Bound } from '@blocksuite/global/gfx'; import { Bound } from '@blocksuite/global/gfx';
import last from 'lodash-es/last'; import last from 'lodash-es/last';
import type { PointerEventState } from '../../../event'; import type { PointerEventState } from '../../event';
import type { GfxController } from '../..'; import type { GfxController } from '../controller.js';
import type { GfxElementModelView } from '../../view/view'; import type { GfxElementModelView } from '../view/view.js';
export class CanvasEventHandler { export class GfxViewEventManager {
private _currentStackedElm: GfxElementModelView[] = []; private _currentStackedElm: GfxElementModelView[] = [];
private _callInReverseOrder( private _callInReverseOrder(

View File

@@ -0,0 +1,17 @@
export { InteractivityExtension } from './extension/base.js';
export { GfxViewEventManager } from './gfx-view-event-handler.js';
export { InteractivityIdentifier, InteractivityManager } from './manager.js';
export type {
DragExtensionInitializeContext,
DragInitializationOption,
ExtensionDragEndContext,
ExtensionDragMoveContext,
ExtensionDragStartContext,
} from './types/drag.js';
export type {
DragEndContext,
DragMoveContext,
DragStartContext,
GfxViewTransformInterface,
SelectedContext,
} from './types/view.js';

View File

@@ -1,39 +1,36 @@
import { import { type ServiceIdentifier } from '@blocksuite/global/di';
type Container,
createIdentifier,
type ServiceIdentifier,
} from '@blocksuite/global/di';
import { DisposableGroup } from '@blocksuite/global/disposable'; import { DisposableGroup } from '@blocksuite/global/disposable';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { Bound, Point } from '@blocksuite/global/gfx'; import { Bound, Point } from '@blocksuite/global/gfx';
import { Extension } from '@blocksuite/store';
import type { PointerEventState } from '../../event/state/pointer.js'; import type { PointerEventState } from '../../event/state/pointer.js';
import { type GfxController } from '../controller.js';
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js'; import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
import { GfxControllerIdentifier } from '../identifiers.js';
import type { GfxModel } from '../model/model.js'; import type { GfxModel } from '../model/model.js';
import { type SupportedEvent } from '../view/view.js'; import type { SupportedEvents } from './event.js';
import {
type InteractivityActionAPI,
type InteractivityEventAPI,
InteractivityExtensionIdentifier,
} from './extension/base.js';
import { GfxViewEventManager } from './gfx-view-event-handler.js';
import type { import type {
DragExtensionInitializeContext, DragExtensionInitializeContext,
DragInitializationOption, DragInitializationOption,
ExtensionDragEndContext, ExtensionDragEndContext,
ExtensionDragMoveContext, ExtensionDragMoveContext,
ExtensionDragStartContext, ExtensionDragStartContext,
} from './drag.js'; } from './types/drag.js';
import { CanvasEventHandler } from './extension/canvas-event-handler.js';
type ExtensionPointerHandler = Exclude< type ExtensionPointerHandler = Exclude<
SupportedEvent, SupportedEvents,
'pointerleave' | 'pointerenter' 'pointerleave' | 'pointerenter'
>; >;
export const TransformManagerIdentifier = GfxExtensionIdentifier( export const InteractivityIdentifier = GfxExtensionIdentifier(
'element-transform-manager' 'interactivity-manager'
) as ServiceIdentifier<ElementTransformManager>; ) as ServiceIdentifier<InteractivityManager>;
const CAMEL_CASE_MAP: { const CAMEL_CASE_MAP: {
[key in ExtensionPointerHandler]: keyof CanvasEventHandler; [key in ExtensionPointerHandler]: keyof GfxViewEventManager;
} = { } = {
click: 'click', click: 'click',
dblclick: 'dblClick', dblclick: 'dblClick',
@@ -42,23 +39,29 @@ const CAMEL_CASE_MAP: {
pointerup: 'pointerUp', pointerup: 'pointerUp',
}; };
export class ElementTransformManager extends GfxExtension { export class InteractivityManager extends GfxExtension {
static override key = 'element-transform-manager'; static override key = 'interactivity-manager';
private readonly _disposable = new DisposableGroup(); private readonly _disposable = new DisposableGroup();
private canvasEventHandler = new CanvasEventHandler(this.gfx); private canvasEventHandler = new GfxViewEventManager(this.gfx);
override mounted(): void { override mounted(): void {
this.canvasEventHandler = new CanvasEventHandler(this.gfx); this.canvasEventHandler = new GfxViewEventManager(this.gfx);
this.interactExtensions.forEach(ext => {
ext.mounted();
});
} }
override unmounted(): void { override unmounted(): void {
this._disposable.dispose(); this._disposable.dispose();
this.interactExtensions.forEach(ext => {
ext.unmounted();
});
} }
get transformExtensions() { get interactExtensions() {
return this.std.provider.getAll(TransformExtensionIdentifier); return this.std.provider.getAll(InteractivityExtensionIdentifier);
} }
get keyboard() { get keyboard() {
@@ -83,10 +86,10 @@ export class ElementTransformManager extends GfxExtension {
this.canvasEventHandler[handlerName](evt); this.canvasEventHandler[handlerName](evt);
const extension = this.transformExtensions; const extensions = this.interactExtensions;
extension.forEach(ext => { extensions.forEach(ext => {
ext[handlerName]?.(evt); (ext.event as InteractivityEventAPI).emit(eventName, evt);
}); });
} }
@@ -142,15 +145,18 @@ export class ElementTransformManager extends GfxExtension {
]) ])
), ),
}; };
const extension = this.transformExtensions; const extension = this.interactExtensions;
const activeExtensionHandlers = Array.from( const activeExtensionHandlers = Array.from(
extension.values().map(ext => { extension.values().map(ext => {
return ext.onDragInitialize(context); return (ext.action as InteractivityActionAPI).emit(
'dragInitialize',
context
);
}) })
); );
if (cancelledByExt) { if (cancelledByExt) {
activeExtensionHandlers.forEach(handler => handler.clear?.()); activeExtensionHandlers.forEach(handler => handler?.clear?.());
return; return;
} }
@@ -198,7 +204,7 @@ export class ElementTransformManager extends GfxExtension {
this._safeExecute(() => { this._safeExecute(() => {
activeExtensionHandlers.forEach(handler => activeExtensionHandlers.forEach(handler =>
handler.onDragMove?.(moveContext) handler?.onDragMove?.(moveContext)
); );
}, 'Error while executing extension `onDragMove`'); }, 'Error while executing extension `onDragMove`');
@@ -231,7 +237,7 @@ export class ElementTransformManager extends GfxExtension {
this._safeExecute(() => { this._safeExecute(() => {
activeExtensionHandlers.forEach(handler => activeExtensionHandlers.forEach(handler =>
handler.onDragEnd?.(endContext) handler?.onDragEnd?.(endContext)
); );
}, 'Error while executing extension `onDragEnd` handler'); }, 'Error while executing extension `onDragEnd` handler');
@@ -249,7 +255,7 @@ export class ElementTransformManager extends GfxExtension {
}); });
this._safeExecute(() => { this._safeExecute(() => {
activeExtensionHandlers.forEach(handler => handler.clear?.()); activeExtensionHandlers.forEach(handler => handler?.clear?.());
}, 'Error while executing extension `clear` handler'); }, 'Error while executing extension `clear` handler');
options.onDragEnd?.(); options.onDragEnd?.();
@@ -274,7 +280,7 @@ export class ElementTransformManager extends GfxExtension {
this._safeExecute(() => { this._safeExecute(() => {
activeExtensionHandlers.forEach(handler => activeExtensionHandlers.forEach(handler =>
handler.onDragStart?.(dragStartContext) handler?.onDragStart?.(dragStartContext)
); );
}, 'Error while executing extension `onDragStart` handler'); }, 'Error while executing extension `onDragStart` handler');
}; };
@@ -283,58 +289,3 @@ export class ElementTransformManager extends GfxExtension {
dragStart(); dragStart();
} }
} }
export const TransformExtensionIdentifier =
createIdentifier<TransformExtension>('element-transform-extension');
export class TransformExtension extends Extension {
static key: string;
get std() {
return this.gfx.std;
}
constructor(protected readonly gfx: GfxController) {
super();
}
mounted() {}
unmounted() {}
click(_: PointerEventState) {}
dblClick(_: PointerEventState) {}
pointerDown(_: PointerEventState) {}
pointerMove(_: PointerEventState) {}
pointerUp(_: PointerEventState) {}
onDragInitialize(_: DragExtensionInitializeContext): {
onDragStart?: (context: ExtensionDragStartContext) => void;
onDragMove?: (context: ExtensionDragMoveContext) => void;
onDragEnd?: (context: ExtensionDragEndContext) => void;
clear?: () => void;
} {
return {};
}
static override setup(di: Container) {
if (!this.key) {
throw new BlockSuiteError(
ErrorCode.ValueNotExists,
'key is not defined in the TransformExtension'
);
}
di.add(
this as unknown as { new (gfx: GfxController): TransformExtension },
[GfxControllerIdentifier]
);
di.addImpl(TransformExtensionIdentifier(this.key), provider =>
provider.get(this)
);
}
}

View File

@@ -1,8 +1,8 @@
import type { Bound } from '@blocksuite/global/gfx'; import type { Bound } from '@blocksuite/global/gfx';
import type { GfxBlockComponent } from '../../view'; import type { GfxBlockComponent } from '../../../view';
import type { GfxModel } from '../model/model'; import type { GfxModel } from '../../model/model';
import type { GfxElementModelView } from '../view/view'; import type { GfxElementModelView } from '../../view/view';
export type DragInitializationOption = { export type DragInitializationOption = {
movingElements: GfxModel[]; movingElements: GfxModel[];

View File

@@ -1,8 +1,8 @@
import type { Bound, IPoint } from '@blocksuite/global/gfx'; import type { Bound, IPoint } from '@blocksuite/global/gfx';
import type { GfxBlockComponent } from '../../view'; import type { GfxBlockComponent } from '../../../view/element/gfx-block-component.js';
import type { GfxModel } from '../model/model'; import type { GfxModel } from '../../model/model.js';
import type { GfxElementModelView } from '../view/view'; import type { GfxElementModelView } from '../../view/view.js';
export type DragStartContext = { export type DragStartContext = {
/** /**

View File

@@ -6,14 +6,14 @@ import type { Extension } from '@blocksuite/store';
import type { PointerEventState } from '../../event/index.js'; import type { PointerEventState } from '../../event/index.js';
import type { EditorHost } from '../../view/index.js'; import type { EditorHost } from '../../view/index.js';
import type { GfxController } from '../index.js';
import type { import type {
DragEndContext, DragEndContext,
DragMoveContext, DragMoveContext,
DragStartContext, DragStartContext,
GfxViewTransformInterface, GfxViewTransformInterface,
SelectedContext, SelectedContext,
} from '../element-transform/view-transform.js'; } from '../interactivity/index.js';
import type { GfxController } from '../index.js';
import type { GfxElementGeometry, PointTestOptions } from '../model/base.js'; import type { GfxElementGeometry, PointTestOptions } from '../model/base.js';
import { GfxPrimitiveElementModel } from '../model/surface/element-model.js'; import { GfxPrimitiveElementModel } from '../model/surface/element-model.js';
import type { GfxLocalElementModel } from '../model/surface/local-element-model.js'; import type { GfxLocalElementModel } from '../model/surface/local-element-model.js';

View File

@@ -4,12 +4,12 @@ import { computed, effect, signal } from '@preact/signals-core';
import { nothing } from 'lit'; import { nothing } from 'lit';
import type { BlockService } from '../../extension/index.js'; import type { BlockService } from '../../extension/index.js';
import { GfxControllerIdentifier } from '../../gfx/identifiers.js';
import type { import type {
DragMoveContext, DragMoveContext,
GfxViewTransformInterface, GfxViewTransformInterface,
SelectedContext, SelectedContext,
} from '../../gfx/element-transform/view-transform.js'; } from '../../gfx/interactivity/index.js';
import { GfxControllerIdentifier } from '../../gfx/identifiers.js';
import { type GfxBlockElementModel } from '../../gfx/model/gfx-block-model.js'; import { type GfxBlockElementModel } from '../../gfx/model/gfx-block-model.js';
import { SurfaceSelection } from '../../selection/index.js'; import { SurfaceSelection } from '../../selection/index.js';
import { BlockComponent } from './block-component.js'; import { BlockComponent } from './block-component.js';