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

@@ -1,8 +1,14 @@
import type { ConnectorElementModel } from '@blocksuite/affine-model';
import {
type ConnectorElementModel,
LocalShapeElementModel,
} from '@blocksuite/affine-model';
import { Bound, serializeXYWH, Vec } from '@blocksuite/global/gfx';
import type { PointerEventState } from '@blocksuite/std';
import {
type DragEndContext,
type DragMoveContext,
type DragStartContext,
generateKeyBetween,
GfxElementModelView,
} from '@blocksuite/std/gfx';
@@ -30,11 +36,17 @@ export class ConnectorElementView extends GfxElementModelView<ConnectorElementMo
override onCreated(): void {
super.onCreated();
this._initDblClickToEdit();
this._initLabelMoving();
}
private _initDblClickToEdit(): void {
this.on('dblclick', evt => {
private _initLabelMoving(): void {
let curLabelElement: LocalShapeElementModel | null = null;
if (this.model.isLocked()) {
return;
}
const enterLabelEditor = (evt: PointerEventState) => {
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
if (edgeless && !this.model.isLocked()) {
@@ -44,6 +56,121 @@ export class ConnectorElementView extends GfxElementModelView<ConnectorElementMo
this.gfx.viewport.toModelCoord(evt.x, evt.y)
);
}
};
const getCurrentPosition = (evt: PointerEventState) => {
const [x, y] = this.gfx.viewport.toModelCoord(evt.x, evt.y);
return {
x,
y,
clientX: evt.raw.clientX,
clientY: evt.raw.clientY,
};
};
const watchEvent = (labelModel: LocalShapeElementModel) => {
const view = this.gfx.view.get(labelModel) as GfxElementModelView;
const connectorModel = this.model;
let labelBound: Bound | null = null;
let startPoint = {
x: 0,
y: 0,
clientX: 0,
clientY: 0,
};
let lastPoint = {
x: 0,
y: 0,
clientX: 0,
clientY: 0,
};
view.on('dblclick', evt => {
enterLabelEditor(evt);
});
view.on('dragstart', evt => {
startPoint = getCurrentPosition(evt);
labelBound = Bound.deserialize(labelModel.xywh);
connectorModel.stash('labelXYWH');
connectorModel.stash('labelOffset');
});
view.on('dragmove', evt => {
if (!labelBound) {
return;
}
lastPoint = getCurrentPosition(evt);
const newBound = labelBound.clone();
const delta = [lastPoint.x - startPoint.x, lastPoint.y - startPoint.y];
const center = connectorModel.getNearestPoint(
Vec.add(newBound.center, delta)
);
const distance = connectorModel.getOffsetDistanceByPoint(center);
newBound.center = center;
connectorModel.labelXYWH = newBound.toXYWH();
connectorModel.labelOffset = {
distance,
};
});
view.on('dragend', () => {
if (labelBound) {
labelBound = null;
connectorModel.pop('labelXYWH');
connectorModel.pop('labelOffset');
}
});
};
const updateLabelElement = () => {
if (!this.model.labelXYWH || !this.model.text) {
// Clean up existing label element if conditions are no longer met
if (curLabelElement) {
this.surface.deleteLocalElement(curLabelElement);
curLabelElement = null;
}
return;
}
const labelElement =
curLabelElement || new LocalShapeElementModel(this.surface);
labelElement.xywh = serializeXYWH(...this.model.labelXYWH);
labelElement.index = generateKeyBetween(this.model.index, null);
if (!curLabelElement) {
curLabelElement = labelElement;
labelElement.fillColor = 'transparent';
labelElement.strokeColor = 'transparent';
labelElement.strokeWidth = 0;
this.surface.addLocalElement(labelElement);
this.disposable.add(() => {
this.surface.deleteLocalElement(labelElement);
});
watchEvent(labelElement);
}
};
this.disposable.add(
this.model.propsUpdated.subscribe(payload => {
if (
payload.key === 'labelXYWH' ||
payload.key === 'text' ||
payload.key === 'index'
) {
updateLabelElement();
}
})
);
updateLabelElement();
this.on('dblclick', evt => {
if (!curLabelElement) {
enterLabelEditor(evt);
}
});
}
}

View File

@@ -1,4 +1,4 @@
import type { ShapeElementModel } from '@blocksuite/affine-model';
import { ShapeElementModel } from '@blocksuite/affine-model';
import { GfxElementModelView } from '@blocksuite/std/gfx';
import { mountShapeTextEditor } from './text/edgeless-shape-text-editor';
@@ -16,7 +16,11 @@ export class ShapeElementView extends GfxElementModelView<ShapeElementModel> {
this.on('dblclick', () => {
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
if (edgeless && !this.model.isLocked()) {
if (
edgeless &&
!this.model.isLocked() &&
this.model instanceof ShapeElementModel
) {
mountShapeTextEditor(this.model, edgeless);
}
});