refactor: moving connector label to connector view (#11738)

### Changed
Moved connector label moving logic from `default-tool` to connector view.

#### Other infrastructure changes:​​
- Gfx element view now can handles drag events
- Added `context.preventDefault()` support to bypass built-in interactions in extension
- Handle the pointer events in element view will bypass the built-in interactions automatically

> The built-in interactions include element dragging, click selection, drag-to-scale operations, etc.
This commit is contained in:
doouding
2025-04-22 08:18:24 +00:00
parent 21bf009553
commit e0e84d302d
12 changed files with 305 additions and 130 deletions

View File

@@ -24,6 +24,7 @@ export type {
ExtensionDragEndContext,
ExtensionDragMoveContext,
ExtensionDragStartContext,
GfxInteractivityContext,
SelectedContext,
} from './interactivity/index.js';
export {

View File

@@ -1,3 +1,5 @@
import type { PointerEventState, UIEventState } from '../../event';
export type SupportedEvents =
| 'click'
| 'dblclick'
@@ -5,4 +7,42 @@ export type SupportedEvents =
| 'pointerenter'
| 'pointerleave'
| 'pointermove'
| 'pointerup';
| 'pointerup'
| 'dragstart'
| 'dragmove'
| 'dragend';
export type GfxInteractivityContext<
EventState extends UIEventState = PointerEventState,
RawEvent extends Event = EventState['event'],
> = {
event: EventState;
/**
* The raw dom event.
*/
raw: RawEvent;
/**
* Prevent the default gfx interaction
*/
preventDefault: () => void;
};
export const createInteractionContext = (event: PointerEventState) => {
let preventDefaultState = false;
return {
context: {
event,
raw: event.raw,
preventDefault: () => {
preventDefaultState = true;
event.raw.preventDefault();
},
},
get preventDefaultState() {
return preventDefaultState;
},
};
};

View File

@@ -2,11 +2,10 @@ 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 { GfxModel } from '../../model/model.js';
import type { SupportedEvents } from '../event.js';
import type { GfxInteractivityContext, SupportedEvents } from '../event.js';
import type { ExtensionElementsCloneContext } from '../types/clone.js';
import type {
DragExtensionInitializeContext,
@@ -64,10 +63,13 @@ export class InteractivityExtension extends Extension {
export class InteractivityEventAPI {
private readonly _handlersMap = new Map<
SupportedEvents,
((evt: PointerEventState) => void)[]
((evt: GfxInteractivityContext) => void)[]
>();
on(eventName: SupportedEvents, handler: (evt: PointerEventState) => void) {
on(
eventName: SupportedEvents,
handler: (evt: GfxInteractivityContext) => void
) {
const handlers = this._handlersMap.get(eventName) ?? [];
handlers.push(handler);
this._handlersMap.set(eventName, handlers);
@@ -81,7 +83,7 @@ export class InteractivityEventAPI {
};
}
emit(eventName: SupportedEvents, evt: PointerEventState) {
emit(eventName: SupportedEvents, evt: GfxInteractivityContext) {
const handlers = this._handlersMap.get(eventName);
if (!handlers) {
return;

View File

@@ -3,14 +3,15 @@ import last from 'lodash-es/last';
import type { PointerEventState } from '../../event';
import type { GfxController } from '../controller.js';
import type { GfxElementModelView } from '../view/view.js';
import type { GfxElementModelView, SupportedEvent } from '../view/view.js';
export class GfxViewEventManager {
private _currentStackedElm: GfxElementModelView[] = [];
private _hoveredElementsStack: GfxElementModelView[] = [];
private _draggingElement: GfxElementModelView | null = null;
private _callInReverseOrder(
callback: (view: GfxElementModelView) => void,
arr = this._currentStackedElm
arr = this._hoveredElementsStack
) {
for (let i = arr.length - 1; i >= 0; i--) {
const view = arr[i];
@@ -21,26 +22,52 @@ export class GfxViewEventManager {
constructor(private readonly gfx: GfxController) {}
click(_evt: PointerEventState): void {
last(this._currentStackedElm)?.dispatch('click', _evt);
dispatch(eventName: SupportedEvent, evt: PointerEventState) {
if (eventName === 'pointermove') {
this._handlePointerMove(evt);
return false;
} else if (eventName.startsWith('drag')) {
return this._handleDrag(
eventName as 'dragstart' | 'dragend' | 'dragmove',
evt
);
} else {
return last(this._hoveredElementsStack)?.dispatch(eventName, evt);
}
}
dblClick(_evt: PointerEventState): void {
last(this._currentStackedElm)?.dispatch('dblclick', _evt);
private _handleDrag(
evtName: 'dragstart' | 'dragend' | 'dragmove',
_evt: PointerEventState
): boolean {
switch (evtName) {
case 'dragstart': {
if (this._draggingElement) {
this._draggingElement.dispatch('dragend', _evt);
}
this._draggingElement = last(this._hoveredElementsStack) ?? null;
return this._draggingElement?.dispatch('dragstart', _evt) ?? false;
}
case 'dragmove': {
return this._draggingElement?.dispatch('dragmove', _evt) ?? false;
}
case 'dragend': {
const dispatched =
this._draggingElement?.dispatch('dragend', _evt) ?? false;
this._draggingElement = null;
return dispatched;
}
}
}
pointerDown(_evt: PointerEventState): void {
last(this._currentStackedElm)?.dispatch('pointerdown', _evt);
}
pointerMove(_evt: PointerEventState): void {
private _handlePointerMove(_evt: PointerEventState): void {
const [x, y] = this.gfx.viewport.toModelCoord(_evt.x, _evt.y);
const hoveredElmViews = this.gfx.grid
.search(new Bound(x - 5, y - 5, 10, 10), {
filter: ['canvas', 'local'],
})
.map(model => this.gfx.view.get(model)) as GfxElementModelView[];
const currentStackedViews = new Set(this._currentStackedElm);
const currentStackedViews = new Set(this._hoveredElementsStack);
const visited = new Set<GfxElementModelView>();
this._callInReverseOrder(view => {
@@ -54,10 +81,6 @@ export class GfxViewEventManager {
this._callInReverseOrder(
view => !visited.has(view) && view.dispatch('pointerleave', _evt)
);
this._currentStackedElm = hoveredElmViews;
}
pointerUp(_evt: PointerEventState): void {
last(this._currentStackedElm)?.dispatch('pointerup', _evt);
this._hoveredElementsStack = hoveredElmViews;
}
}

View File

@@ -1,3 +1,4 @@
export type { GfxInteractivityContext } from './event.js';
export { InteractivityExtension } from './extension/base.js';
export { GfxViewEventManager } from './gfx-view-event-handler.js';
export { InteractivityIdentifier, InteractivityManager } from './manager.js';

View File

@@ -5,7 +5,7 @@ import { Bound, Point } from '@blocksuite/global/gfx';
import type { PointerEventState } from '../../event/state/pointer.js';
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
import type { GfxModel } from '../model/model.js';
import type { SupportedEvents } from './event.js';
import { createInteractionContext, type SupportedEvents } from './event.js';
import {
type InteractivityActionAPI,
type InteractivityEventAPI,
@@ -30,16 +30,6 @@ export const InteractivityIdentifier = GfxExtensionIdentifier(
'interactivity-manager'
) as ServiceIdentifier<InteractivityManager>;
const CAMEL_CASE_MAP: {
[key in ExtensionPointerHandler]: keyof GfxViewEventManager;
} = {
click: 'click',
dblclick: 'dblClick',
pointerdown: 'pointerDown',
pointermove: 'pointerMove',
pointerup: 'pointerUp',
};
export class InteractivityManager extends GfxExtension {
static override key = 'interactivity-manager';
@@ -78,20 +68,26 @@ export class InteractivityManager extends GfxExtension {
}
/**
* Dispatch the event to canvas elements
* Dispatch event to extensions and gfx view.
* @param eventName
* @param evt
* @returns
*/
dispatch(eventName: ExtensionPointerHandler, evt: PointerEventState) {
const handlerName = CAMEL_CASE_MAP[eventName];
this.canvasEventHandler[handlerName](evt);
dispatchEvent(eventName: ExtensionPointerHandler, evt: PointerEventState) {
const { context, preventDefaultState } = createInteractionContext(evt);
const extensions = this.interactExtensions;
extensions.forEach(ext => {
(ext.event as InteractivityEventAPI).emit(eventName, evt);
(ext.event as InteractivityEventAPI).emit(eventName, context);
});
const handledByView =
this.canvasEventHandler.dispatch(eventName, evt) ?? false;
return {
preventDefaultState,
handledByView,
};
}
dispatchOnSelected(evt: PointerEventState) {
@@ -126,6 +122,14 @@ export class InteractivityManager extends GfxExtension {
return false;
}
/**
* Initialize drag operation for elements.
* Handles drag start, move and end events automatically.
* Note: Call this when mouse is already down.
*
* @param options
* @returns
*/
initializeDrag(options: DragInitializationOption) {
let cancelledByExt = false;

View File

@@ -26,6 +26,9 @@ export type EventsHandlerMap = {
pointerleave: PointerEventState;
pointermove: PointerEventState;
pointerup: PointerEventState;
dragstart: PointerEventState;
dragmove: PointerEventState;
dragend: PointerEventState;
};
export type SupportedEvent = keyof EventsHandlerMap;
@@ -97,11 +100,24 @@ export class GfxElementModelView<
return this.model.containsBound(bounds);
}
/**
* Dispatches an event to the view.
* @param event
* @param evt
* @returns Whether the event view has any handlers for the event.
*/
dispatch<K extends keyof EventsHandlerMap>(
event: K,
evt: EventsHandlerMap[K]
) {
this._handlers.get(event)?.forEach(callback => callback(evt));
const handlers = this._handlers.get(event);
if (handlers?.length) {
handlers.forEach(callback => callback(evt));
return true;
}
return false;
}
getLineIntersections(start: IVec, end: IVec) {