mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 04:48:53 +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 {
|
export enum DefaultModeDragType {
|
||||||
/** Moving connector label */
|
|
||||||
ConnectorLabelMoving = 'connector-label-moving',
|
|
||||||
/** Moving selected contents */
|
/** Moving selected contents */
|
||||||
ContentMoving = 'content-moving',
|
ContentMoving = 'content-moving',
|
||||||
/** Native range dragging inside active note block */
|
/** Native range dragging inside active note block */
|
||||||
NativeEditing = 'native-editing',
|
NativeEditing = 'native-editing',
|
||||||
/** Default void state */
|
/** Default void state */
|
||||||
None = 'none',
|
None = 'none',
|
||||||
/** Dragging preview */
|
|
||||||
PreviewDragging = 'preview-dragging',
|
|
||||||
/** Expanding the dragging area, select the content covered inside */
|
/** Expanding the dragging area, select the content covered inside */
|
||||||
Selecting = 'selecting',
|
Selecting = 'selecting',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,8 @@ import {
|
|||||||
type FrameOverlay,
|
type FrameOverlay,
|
||||||
isFrameBlock,
|
isFrameBlock,
|
||||||
} from '@blocksuite/affine-block-frame';
|
} from '@blocksuite/affine-block-frame';
|
||||||
|
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
|
||||||
import {
|
import {
|
||||||
ConnectorUtils,
|
|
||||||
OverlayIdentifier,
|
|
||||||
} from '@blocksuite/affine-block-surface';
|
|
||||||
import {
|
|
||||||
type ConnectorElementModel,
|
|
||||||
GroupElementModel,
|
GroupElementModel,
|
||||||
MindmapElementModel,
|
MindmapElementModel,
|
||||||
NoteBlockModel,
|
NoteBlockModel,
|
||||||
@@ -16,7 +12,7 @@ import {
|
|||||||
import { resetNativeSelection } from '@blocksuite/affine-shared/utils';
|
import { resetNativeSelection } from '@blocksuite/affine-shared/utils';
|
||||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||||
import type { IVec } from '@blocksuite/global/gfx';
|
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 type { PointerEventState } from '@blocksuite/std';
|
||||||
import {
|
import {
|
||||||
BaseTool,
|
BaseTool,
|
||||||
@@ -29,7 +25,6 @@ import {
|
|||||||
import { effect } from '@preact/signals-core';
|
import { effect } from '@preact/signals-core';
|
||||||
|
|
||||||
import { calPanDelta } from '../utils/panning-utils.js';
|
import { calPanDelta } from '../utils/panning-utils.js';
|
||||||
import { isCanvasElement } from '../utils/query.js';
|
|
||||||
import { DefaultModeDragType } from './default-tool-ext/ext.js';
|
import { DefaultModeDragType } from './default-tool-ext/ext.js';
|
||||||
|
|
||||||
export class DefaultTool extends BaseTool {
|
export class DefaultTool extends BaseTool {
|
||||||
@@ -59,11 +54,6 @@ export class DefaultTool extends BaseTool {
|
|||||||
this.gfx.viewport.applyDeltaCenter(delta[0], delta[1]);
|
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 | {
|
private _selectionRectTransition: null | {
|
||||||
w: number;
|
w: number;
|
||||||
h: number;
|
h: number;
|
||||||
@@ -168,6 +158,8 @@ export class DefaultTool extends BaseTool {
|
|||||||
|
|
||||||
enableHover = true;
|
enableHover = true;
|
||||||
|
|
||||||
|
dragging = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the end position of the dragging area in the model coordinate
|
* Get the end position of the dragging area in the model coordinate
|
||||||
*/
|
*/
|
||||||
@@ -232,20 +224,6 @@ export class DefaultTool extends BaseTool {
|
|||||||
editing: false,
|
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
|
return this.edgelessSelectionManager.editing
|
||||||
@@ -259,20 +237,6 @@ export class DefaultTool extends BaseTool {
|
|||||||
editing: false,
|
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;
|
return DefaultModeDragType.ContentMoving;
|
||||||
} else {
|
} else {
|
||||||
return DefaultModeDragType.Selecting;
|
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) {
|
private _pick(x: number, y: number, options?: PointTestOptions) {
|
||||||
const modelPos = this.gfx.viewport.toModelCoord(x, y);
|
const modelPos = this.gfx.viewport.toModelCoord(x, y);
|
||||||
|
|
||||||
@@ -387,7 +333,7 @@ export class DefaultTool extends BaseTool {
|
|||||||
resetNativeSelection(null);
|
resetNativeSelection(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.interactivity?.dispatch('click', e);
|
this.interactivity?.dispatchEvent('click', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
override deactivate() {
|
override deactivate() {
|
||||||
@@ -409,21 +355,28 @@ export class DefaultTool extends BaseTool {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.interactivity?.dispatch('dblclick', e);
|
this.interactivity?.dispatchEvent('dblclick', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
override dragEnd() {
|
override dragEnd(e: PointerEventState) {
|
||||||
if (this.edgelessSelectionManager.editing) return;
|
this.interactivity?.dispatchEvent('dragend', e);
|
||||||
|
|
||||||
|
if (this.edgelessSelectionManager.editing || !this.dragging) return;
|
||||||
|
|
||||||
|
this.dragging = false;
|
||||||
this.frameOverlay.clear();
|
this.frameOverlay.clear();
|
||||||
this._toBeMoved = [];
|
this._toBeMoved = [];
|
||||||
this._selectedConnector = null;
|
|
||||||
this._selectedConnectorLabelBounds = null;
|
|
||||||
this._clearSelectingState();
|
this._clearSelectingState();
|
||||||
this.dragType = DefaultModeDragType.None;
|
this.dragType = DefaultModeDragType.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
override dragMove(e: PointerEventState) {
|
override dragMove(e: PointerEventState) {
|
||||||
|
this.interactivity?.dispatchEvent('dragmove', e);
|
||||||
|
|
||||||
|
if (!this.dragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { viewport } = this.gfx;
|
const { viewport } = this.gfx;
|
||||||
switch (this.dragType) {
|
switch (this.dragType) {
|
||||||
case DefaultModeDragType.Selecting: {
|
case DefaultModeDragType.Selecting: {
|
||||||
@@ -441,12 +394,6 @@ export class DefaultTool extends BaseTool {
|
|||||||
case DefaultModeDragType.ContentMoving: {
|
case DefaultModeDragType.ContentMoving: {
|
||||||
break;
|
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: {
|
case DefaultModeDragType.NativeEditing: {
|
||||||
// TODO reset if drag out of note
|
// TODO reset if drag out of note
|
||||||
break;
|
break;
|
||||||
@@ -456,7 +403,18 @@ export class DefaultTool extends BaseTool {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
override async dragStart(e: PointerEventState) {
|
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
|
// Determine the drag type based on the current state and event
|
||||||
let dragType = this._determineDragType(e);
|
let dragType = this._determineDragType(e);
|
||||||
|
|
||||||
@@ -508,7 +466,7 @@ export class DefaultTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override pointerDown(e: PointerEventState): void {
|
override pointerDown(e: PointerEventState): void {
|
||||||
this.interactivity?.dispatch('pointerdown', e);
|
this.interactivity?.dispatchEvent('pointerdown', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
override pointerMove(e: PointerEventState) {
|
override pointerMove(e: PointerEventState) {
|
||||||
@@ -527,11 +485,11 @@ export class DefaultTool extends BaseTool {
|
|||||||
this.frameOverlay.clear();
|
this.frameOverlay.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.interactivity?.dispatch('pointermove', e);
|
this.interactivity?.dispatchEvent('pointermove', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
override pointerUp(e: PointerEventState) {
|
override pointerUp(e: PointerEventState) {
|
||||||
this.interactivity?.dispatch('pointerup', e);
|
this.interactivity?.dispatchEvent('pointerup', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
override tripleClick() {}
|
override tripleClick() {}
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ import {
|
|||||||
FeatureFlagService,
|
FeatureFlagService,
|
||||||
TelemetryProvider,
|
TelemetryProvider,
|
||||||
} from '@blocksuite/affine-shared/services';
|
} from '@blocksuite/affine-shared/services';
|
||||||
import type { PointerEventState } from '@blocksuite/std';
|
import {
|
||||||
import { InteractivityExtension } from '@blocksuite/std/gfx';
|
type GfxInteractivityContext,
|
||||||
|
InteractivityExtension,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
export class DblClickAddEdgelessText extends InteractivityExtension {
|
export class DblClickAddEdgelessText extends InteractivityExtension {
|
||||||
static override key = 'dbl-click-add-edgeless-text';
|
static override key = 'dbl-click-add-edgeless-text';
|
||||||
|
|
||||||
override mounted() {
|
override mounted() {
|
||||||
this.event.on('dblclick', (e: PointerEventState) => {
|
this.event.on('dblclick', (ctx: GfxInteractivityContext) => {
|
||||||
|
const { event: e } = ctx;
|
||||||
const textFlag = this.std.store
|
const textFlag = this.std.store
|
||||||
.get(FeatureFlagService)
|
.get(FeatureFlagService)
|
||||||
.getFlag('enable_edgeless_text');
|
.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 {
|
import {
|
||||||
type DragEndContext,
|
type DragEndContext,
|
||||||
type DragMoveContext,
|
type DragMoveContext,
|
||||||
type DragStartContext,
|
type DragStartContext,
|
||||||
|
generateKeyBetween,
|
||||||
GfxElementModelView,
|
GfxElementModelView,
|
||||||
} from '@blocksuite/std/gfx';
|
} from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
@@ -30,11 +36,17 @@ export class ConnectorElementView extends GfxElementModelView<ConnectorElementMo
|
|||||||
override onCreated(): void {
|
override onCreated(): void {
|
||||||
super.onCreated();
|
super.onCreated();
|
||||||
|
|
||||||
this._initDblClickToEdit();
|
this._initLabelMoving();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _initDblClickToEdit(): void {
|
private _initLabelMoving(): void {
|
||||||
this.on('dblclick', evt => {
|
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);
|
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
|
||||||
|
|
||||||
if (edgeless && !this.model.isLocked()) {
|
if (edgeless && !this.model.isLocked()) {
|
||||||
@@ -44,6 +56,121 @@ export class ConnectorElementView extends GfxElementModelView<ConnectorElementMo
|
|||||||
this.gfx.viewport.toModelCoord(evt.x, evt.y)
|
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 { GfxElementModelView } from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
import { mountShapeTextEditor } from './text/edgeless-shape-text-editor';
|
import { mountShapeTextEditor } from './text/edgeless-shape-text-editor';
|
||||||
@@ -16,7 +16,11 @@ export class ShapeElementView extends GfxElementModelView<ShapeElementModel> {
|
|||||||
this.on('dblclick', () => {
|
this.on('dblclick', () => {
|
||||||
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
|
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);
|
mountShapeTextEditor(this.model, edgeless);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type {
|
|||||||
ExtensionDragEndContext,
|
ExtensionDragEndContext,
|
||||||
ExtensionDragMoveContext,
|
ExtensionDragMoveContext,
|
||||||
ExtensionDragStartContext,
|
ExtensionDragStartContext,
|
||||||
|
GfxInteractivityContext,
|
||||||
SelectedContext,
|
SelectedContext,
|
||||||
} from './interactivity/index.js';
|
} from './interactivity/index.js';
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { PointerEventState, UIEventState } from '../../event';
|
||||||
|
|
||||||
export type SupportedEvents =
|
export type SupportedEvents =
|
||||||
| 'click'
|
| 'click'
|
||||||
| 'dblclick'
|
| 'dblclick'
|
||||||
@@ -5,4 +7,42 @@ export type SupportedEvents =
|
|||||||
| 'pointerenter'
|
| 'pointerenter'
|
||||||
| 'pointerleave'
|
| 'pointerleave'
|
||||||
| 'pointermove'
|
| '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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { type Container, createIdentifier } from '@blocksuite/global/di';
|
|||||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||||
import { Extension } from '@blocksuite/store';
|
import { Extension } from '@blocksuite/store';
|
||||||
|
|
||||||
import type { PointerEventState } from '../../../event/index.js';
|
|
||||||
import type { GfxController } from '../../controller.js';
|
import type { GfxController } from '../../controller.js';
|
||||||
import { GfxControllerIdentifier } from '../../identifiers.js';
|
import { GfxControllerIdentifier } from '../../identifiers.js';
|
||||||
import type { GfxModel } from '../../model/model.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 { ExtensionElementsCloneContext } from '../types/clone.js';
|
||||||
import type {
|
import type {
|
||||||
DragExtensionInitializeContext,
|
DragExtensionInitializeContext,
|
||||||
@@ -64,10 +63,13 @@ export class InteractivityExtension extends Extension {
|
|||||||
export class InteractivityEventAPI {
|
export class InteractivityEventAPI {
|
||||||
private readonly _handlersMap = new Map<
|
private readonly _handlersMap = new Map<
|
||||||
SupportedEvents,
|
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) ?? [];
|
const handlers = this._handlersMap.get(eventName) ?? [];
|
||||||
handlers.push(handler);
|
handlers.push(handler);
|
||||||
this._handlersMap.set(eventName, handlers);
|
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);
|
const handlers = this._handlersMap.get(eventName);
|
||||||
if (!handlers) {
|
if (!handlers) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import last from 'lodash-es/last';
|
|||||||
|
|
||||||
import type { PointerEventState } from '../../event';
|
import type { PointerEventState } from '../../event';
|
||||||
import type { GfxController } from '../controller.js';
|
import type { GfxController } from '../controller.js';
|
||||||
import type { GfxElementModelView } from '../view/view.js';
|
import type { GfxElementModelView, SupportedEvent } from '../view/view.js';
|
||||||
|
|
||||||
export class GfxViewEventManager {
|
export class GfxViewEventManager {
|
||||||
private _currentStackedElm: GfxElementModelView[] = [];
|
private _hoveredElementsStack: GfxElementModelView[] = [];
|
||||||
|
private _draggingElement: GfxElementModelView | null = null;
|
||||||
|
|
||||||
private _callInReverseOrder(
|
private _callInReverseOrder(
|
||||||
callback: (view: GfxElementModelView) => void,
|
callback: (view: GfxElementModelView) => void,
|
||||||
arr = this._currentStackedElm
|
arr = this._hoveredElementsStack
|
||||||
) {
|
) {
|
||||||
for (let i = arr.length - 1; i >= 0; i--) {
|
for (let i = arr.length - 1; i >= 0; i--) {
|
||||||
const view = arr[i];
|
const view = arr[i];
|
||||||
@@ -21,26 +22,52 @@ export class GfxViewEventManager {
|
|||||||
|
|
||||||
constructor(private readonly gfx: GfxController) {}
|
constructor(private readonly gfx: GfxController) {}
|
||||||
|
|
||||||
click(_evt: PointerEventState): void {
|
dispatch(eventName: SupportedEvent, evt: PointerEventState) {
|
||||||
last(this._currentStackedElm)?.dispatch('click', _evt);
|
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 {
|
private _handleDrag(
|
||||||
last(this._currentStackedElm)?.dispatch('dblclick', _evt);
|
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 {
|
private _handlePointerMove(_evt: PointerEventState): void {
|
||||||
last(this._currentStackedElm)?.dispatch('pointerdown', _evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
pointerMove(_evt: PointerEventState): void {
|
|
||||||
const [x, y] = this.gfx.viewport.toModelCoord(_evt.x, _evt.y);
|
const [x, y] = this.gfx.viewport.toModelCoord(_evt.x, _evt.y);
|
||||||
const hoveredElmViews = this.gfx.grid
|
const hoveredElmViews = this.gfx.grid
|
||||||
.search(new Bound(x - 5, y - 5, 10, 10), {
|
.search(new Bound(x - 5, y - 5, 10, 10), {
|
||||||
filter: ['canvas', 'local'],
|
filter: ['canvas', 'local'],
|
||||||
})
|
})
|
||||||
.map(model => this.gfx.view.get(model)) as GfxElementModelView[];
|
.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>();
|
const visited = new Set<GfxElementModelView>();
|
||||||
|
|
||||||
this._callInReverseOrder(view => {
|
this._callInReverseOrder(view => {
|
||||||
@@ -54,10 +81,6 @@ export class GfxViewEventManager {
|
|||||||
this._callInReverseOrder(
|
this._callInReverseOrder(
|
||||||
view => !visited.has(view) && view.dispatch('pointerleave', _evt)
|
view => !visited.has(view) && view.dispatch('pointerleave', _evt)
|
||||||
);
|
);
|
||||||
this._currentStackedElm = hoveredElmViews;
|
this._hoveredElementsStack = hoveredElmViews;
|
||||||
}
|
|
||||||
|
|
||||||
pointerUp(_evt: PointerEventState): void {
|
|
||||||
last(this._currentStackedElm)?.dispatch('pointerup', _evt);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export type { GfxInteractivityContext } from './event.js';
|
||||||
export { InteractivityExtension } from './extension/base.js';
|
export { InteractivityExtension } from './extension/base.js';
|
||||||
export { GfxViewEventManager } from './gfx-view-event-handler.js';
|
export { GfxViewEventManager } from './gfx-view-event-handler.js';
|
||||||
export { InteractivityIdentifier, InteractivityManager } from './manager.js';
|
export { InteractivityIdentifier, InteractivityManager } from './manager.js';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Bound, Point } from '@blocksuite/global/gfx';
|
|||||||
import type { PointerEventState } from '../../event/state/pointer.js';
|
import type { PointerEventState } from '../../event/state/pointer.js';
|
||||||
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
|
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
|
||||||
import type { GfxModel } from '../model/model.js';
|
import type { GfxModel } from '../model/model.js';
|
||||||
import type { SupportedEvents } from './event.js';
|
import { createInteractionContext, type SupportedEvents } from './event.js';
|
||||||
import {
|
import {
|
||||||
type InteractivityActionAPI,
|
type InteractivityActionAPI,
|
||||||
type InteractivityEventAPI,
|
type InteractivityEventAPI,
|
||||||
@@ -30,16 +30,6 @@ export const InteractivityIdentifier = GfxExtensionIdentifier(
|
|||||||
'interactivity-manager'
|
'interactivity-manager'
|
||||||
) as ServiceIdentifier<InteractivityManager>;
|
) 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 {
|
export class InteractivityManager extends GfxExtension {
|
||||||
static override key = 'interactivity-manager';
|
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 eventName
|
||||||
* @param evt
|
* @param evt
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
dispatch(eventName: ExtensionPointerHandler, evt: PointerEventState) {
|
dispatchEvent(eventName: ExtensionPointerHandler, evt: PointerEventState) {
|
||||||
const handlerName = CAMEL_CASE_MAP[eventName];
|
const { context, preventDefaultState } = createInteractionContext(evt);
|
||||||
|
|
||||||
this.canvasEventHandler[handlerName](evt);
|
|
||||||
|
|
||||||
const extensions = this.interactExtensions;
|
const extensions = this.interactExtensions;
|
||||||
|
|
||||||
extensions.forEach(ext => {
|
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) {
|
dispatchOnSelected(evt: PointerEventState) {
|
||||||
@@ -126,6 +122,14 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
return false;
|
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) {
|
initializeDrag(options: DragInitializationOption) {
|
||||||
let cancelledByExt = false;
|
let cancelledByExt = false;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ export type EventsHandlerMap = {
|
|||||||
pointerleave: PointerEventState;
|
pointerleave: PointerEventState;
|
||||||
pointermove: PointerEventState;
|
pointermove: PointerEventState;
|
||||||
pointerup: PointerEventState;
|
pointerup: PointerEventState;
|
||||||
|
dragstart: PointerEventState;
|
||||||
|
dragmove: PointerEventState;
|
||||||
|
dragend: PointerEventState;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SupportedEvent = keyof EventsHandlerMap;
|
export type SupportedEvent = keyof EventsHandlerMap;
|
||||||
@@ -97,11 +100,24 @@ export class GfxElementModelView<
|
|||||||
return this.model.containsBound(bounds);
|
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>(
|
dispatch<K extends keyof EventsHandlerMap>(
|
||||||
event: K,
|
event: K,
|
||||||
evt: EventsHandlerMap[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) {
|
getLineIntersections(start: IVec, end: IVec) {
|
||||||
|
|||||||
Reference in New Issue
Block a user