mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
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:
@@ -1,14 +1,10 @@
|
||||
export enum DefaultModeDragType {
|
||||
/** Moving connector label */
|
||||
ConnectorLabelMoving = 'connector-label-moving',
|
||||
/** Moving selected contents */
|
||||
ContentMoving = 'content-moving',
|
||||
/** Native range dragging inside active note block */
|
||||
NativeEditing = 'native-editing',
|
||||
/** Default void state */
|
||||
None = 'none',
|
||||
/** Dragging preview */
|
||||
PreviewDragging = 'preview-dragging',
|
||||
/** Expanding the dragging area, select the content covered inside */
|
||||
Selecting = 'selecting',
|
||||
}
|
||||
|
||||
@@ -2,12 +2,8 @@ import {
|
||||
type FrameOverlay,
|
||||
isFrameBlock,
|
||||
} from '@blocksuite/affine-block-frame';
|
||||
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
ConnectorUtils,
|
||||
OverlayIdentifier,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
type ConnectorElementModel,
|
||||
GroupElementModel,
|
||||
MindmapElementModel,
|
||||
NoteBlockModel,
|
||||
@@ -16,7 +12,7 @@ import {
|
||||
import { resetNativeSelection } from '@blocksuite/affine-shared/utils';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import type { IVec } from '@blocksuite/global/gfx';
|
||||
import { Bound, Vec } from '@blocksuite/global/gfx';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import type { PointerEventState } from '@blocksuite/std';
|
||||
import {
|
||||
BaseTool,
|
||||
@@ -29,7 +25,6 @@ import {
|
||||
import { effect } from '@preact/signals-core';
|
||||
|
||||
import { calPanDelta } from '../utils/panning-utils.js';
|
||||
import { isCanvasElement } from '../utils/query.js';
|
||||
import { DefaultModeDragType } from './default-tool-ext/ext.js';
|
||||
|
||||
export class DefaultTool extends BaseTool {
|
||||
@@ -59,11 +54,6 @@ export class DefaultTool extends BaseTool {
|
||||
this.gfx.viewport.applyDeltaCenter(delta[0], delta[1]);
|
||||
};
|
||||
|
||||
// For moving the connector label
|
||||
private _selectedConnector: ConnectorElementModel | null = null;
|
||||
|
||||
private _selectedConnectorLabelBounds: Bound | null = null;
|
||||
|
||||
private _selectionRectTransition: null | {
|
||||
w: number;
|
||||
h: number;
|
||||
@@ -168,6 +158,8 @@ export class DefaultTool extends BaseTool {
|
||||
|
||||
enableHover = true;
|
||||
|
||||
dragging = false;
|
||||
|
||||
/**
|
||||
* Get the end position of the dragging area in the model coordinate
|
||||
*/
|
||||
@@ -232,20 +224,6 @@ export class DefaultTool extends BaseTool {
|
||||
editing: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isCanvasElement(selected) &&
|
||||
ConnectorUtils.isConnectorWithLabel(selected) &&
|
||||
(selected as ConnectorElementModel).labelIncludesPoint(
|
||||
this.gfx.viewport.toModelCoord(x, y)
|
||||
)
|
||||
) {
|
||||
this._selectedConnector = selected as ConnectorElementModel;
|
||||
this._selectedConnectorLabelBounds = Bound.fromXYWH(
|
||||
this._selectedConnector.labelXYWH!
|
||||
);
|
||||
return DefaultModeDragType.ConnectorLabelMoving;
|
||||
}
|
||||
}
|
||||
|
||||
return this.edgelessSelectionManager.editing
|
||||
@@ -259,20 +237,6 @@ export class DefaultTool extends BaseTool {
|
||||
editing: false,
|
||||
});
|
||||
|
||||
if (
|
||||
isCanvasElement(selected) &&
|
||||
ConnectorUtils.isConnectorWithLabel(selected) &&
|
||||
(selected as ConnectorElementModel).labelIncludesPoint(
|
||||
this.gfx.viewport.toModelCoord(x, y)
|
||||
)
|
||||
) {
|
||||
this._selectedConnector = selected as ConnectorElementModel;
|
||||
this._selectedConnectorLabelBounds = Bound.fromXYWH(
|
||||
this._selectedConnector.labelXYWH!
|
||||
);
|
||||
return DefaultModeDragType.ConnectorLabelMoving;
|
||||
}
|
||||
|
||||
return DefaultModeDragType.ContentMoving;
|
||||
} else {
|
||||
return DefaultModeDragType.Selecting;
|
||||
@@ -280,24 +244,6 @@ export class DefaultTool extends BaseTool {
|
||||
}
|
||||
}
|
||||
|
||||
private _moveLabel(delta: IVec) {
|
||||
const connector = this._selectedConnector;
|
||||
let bounds = this._selectedConnectorLabelBounds;
|
||||
if (!connector || !bounds) return;
|
||||
bounds = bounds.clone();
|
||||
const center = connector.getNearestPoint(
|
||||
Vec.add(bounds.center, delta) as IVec
|
||||
);
|
||||
const distance = connector.getOffsetDistanceByPoint(center as IVec);
|
||||
bounds.center = center;
|
||||
this.gfx.updateElement(connector, {
|
||||
labelXYWH: bounds.toXYWH(),
|
||||
labelOffset: {
|
||||
distance,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _pick(x: number, y: number, options?: PointTestOptions) {
|
||||
const modelPos = this.gfx.viewport.toModelCoord(x, y);
|
||||
|
||||
@@ -387,7 +333,7 @@ export class DefaultTool extends BaseTool {
|
||||
resetNativeSelection(null);
|
||||
}
|
||||
|
||||
this.interactivity?.dispatch('click', e);
|
||||
this.interactivity?.dispatchEvent('click', e);
|
||||
}
|
||||
|
||||
override deactivate() {
|
||||
@@ -409,21 +355,28 @@ export class DefaultTool extends BaseTool {
|
||||
return;
|
||||
}
|
||||
|
||||
this.interactivity?.dispatch('dblclick', e);
|
||||
this.interactivity?.dispatchEvent('dblclick', e);
|
||||
}
|
||||
|
||||
override dragEnd() {
|
||||
if (this.edgelessSelectionManager.editing) return;
|
||||
override dragEnd(e: PointerEventState) {
|
||||
this.interactivity?.dispatchEvent('dragend', e);
|
||||
|
||||
if (this.edgelessSelectionManager.editing || !this.dragging) return;
|
||||
|
||||
this.dragging = false;
|
||||
this.frameOverlay.clear();
|
||||
this._toBeMoved = [];
|
||||
this._selectedConnector = null;
|
||||
this._selectedConnectorLabelBounds = null;
|
||||
this._clearSelectingState();
|
||||
this.dragType = DefaultModeDragType.None;
|
||||
}
|
||||
|
||||
override dragMove(e: PointerEventState) {
|
||||
this.interactivity?.dispatchEvent('dragmove', e);
|
||||
|
||||
if (!this.dragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewport } = this.gfx;
|
||||
switch (this.dragType) {
|
||||
case DefaultModeDragType.Selecting: {
|
||||
@@ -441,12 +394,6 @@ export class DefaultTool extends BaseTool {
|
||||
case DefaultModeDragType.ContentMoving: {
|
||||
break;
|
||||
}
|
||||
case DefaultModeDragType.ConnectorLabelMoving: {
|
||||
const dx = this.dragLastPos[0] - this.dragStartPos[0];
|
||||
const dy = this.dragLastPos[1] - this.dragStartPos[1];
|
||||
this._moveLabel([dx, dy]);
|
||||
break;
|
||||
}
|
||||
case DefaultModeDragType.NativeEditing: {
|
||||
// TODO reset if drag out of note
|
||||
break;
|
||||
@@ -456,7 +403,18 @@ export class DefaultTool extends BaseTool {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
override async dragStart(e: PointerEventState) {
|
||||
if (this.edgelessSelectionManager.editing) return;
|
||||
const { preventDefaultState, handledByView } =
|
||||
this.interactivity?.dispatchEvent('dragstart', e) ?? {};
|
||||
|
||||
if (
|
||||
this.edgelessSelectionManager.editing ||
|
||||
preventDefaultState ||
|
||||
handledByView
|
||||
)
|
||||
return;
|
||||
|
||||
this.dragging = true;
|
||||
|
||||
// Determine the drag type based on the current state and event
|
||||
let dragType = this._determineDragType(e);
|
||||
|
||||
@@ -508,7 +466,7 @@ export class DefaultTool extends BaseTool {
|
||||
}
|
||||
|
||||
override pointerDown(e: PointerEventState): void {
|
||||
this.interactivity?.dispatch('pointerdown', e);
|
||||
this.interactivity?.dispatchEvent('pointerdown', e);
|
||||
}
|
||||
|
||||
override pointerMove(e: PointerEventState) {
|
||||
@@ -527,11 +485,11 @@ export class DefaultTool extends BaseTool {
|
||||
this.frameOverlay.clear();
|
||||
}
|
||||
|
||||
this.interactivity?.dispatch('pointermove', e);
|
||||
this.interactivity?.dispatchEvent('pointermove', e);
|
||||
}
|
||||
|
||||
override pointerUp(e: PointerEventState) {
|
||||
this.interactivity?.dispatch('pointerup', e);
|
||||
this.interactivity?.dispatchEvent('pointerup', e);
|
||||
}
|
||||
|
||||
override tripleClick() {}
|
||||
|
||||
@@ -6,14 +6,17 @@ import {
|
||||
FeatureFlagService,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { PointerEventState } from '@blocksuite/std';
|
||||
import { InteractivityExtension } from '@blocksuite/std/gfx';
|
||||
import {
|
||||
type GfxInteractivityContext,
|
||||
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) => {
|
||||
this.event.on('dblclick', (ctx: GfxInteractivityContext) => {
|
||||
const { event: e } = ctx;
|
||||
const textFlag = this.std.store
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_edgeless_text');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user