mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
refactor: rewrite dblclick and selection logic of default-tool (#11036)
continue #10824 ### Changed - Moved double-click-to-edit behavior from the default tool to individual model views - Introduced `onSelected` callback interface in gfx view components to allows developers to override default selection logic
This commit is contained in:
@@ -2,12 +2,10 @@ import { Bound } from '@blocksuite/global/gfx';
|
||||
import last from 'lodash-es/last';
|
||||
|
||||
import type { PointerEventState } from '../../../event';
|
||||
import type { GfxController } from '../..';
|
||||
import type { GfxElementModelView } from '../../view/view';
|
||||
import { TransformExtension } from '../transform-manager';
|
||||
|
||||
export class CanvasEventHandler extends TransformExtension {
|
||||
static override key = 'canvas-event-handler';
|
||||
|
||||
export class CanvasEventHandler {
|
||||
private _currentStackedElm: GfxElementModelView[] = [];
|
||||
|
||||
private _callInReverseOrder(
|
||||
@@ -21,22 +19,24 @@ export class CanvasEventHandler extends TransformExtension {
|
||||
}
|
||||
}
|
||||
|
||||
override click(_evt: PointerEventState): void {
|
||||
constructor(private readonly gfx: GfxController) {}
|
||||
|
||||
click(_evt: PointerEventState): void {
|
||||
last(this._currentStackedElm)?.dispatch('click', _evt);
|
||||
}
|
||||
|
||||
override dblClick(_evt: PointerEventState): void {
|
||||
dblClick(_evt: PointerEventState): void {
|
||||
last(this._currentStackedElm)?.dispatch('dblclick', _evt);
|
||||
}
|
||||
|
||||
override pointerDown(_evt: PointerEventState): void {
|
||||
pointerDown(_evt: PointerEventState): void {
|
||||
last(this._currentStackedElm)?.dispatch('pointerdown', _evt);
|
||||
}
|
||||
|
||||
override pointerMove(_evt: PointerEventState): void {
|
||||
pointerMove(_evt: PointerEventState): void {
|
||||
const [x, y] = this.gfx.viewport.toModelCoord(_evt.x, _evt.y);
|
||||
const hoveredElmViews = this.gfx.grid
|
||||
.search(new Bound(x, y, 1, 1), {
|
||||
.search(new Bound(x - 5, y - 5, 10, 10), {
|
||||
filter: ['canvas', 'local'],
|
||||
})
|
||||
.map(model => this.gfx.view.get(model)) as GfxElementModelView[];
|
||||
@@ -57,7 +57,7 @@ export class CanvasEventHandler extends TransformExtension {
|
||||
this._currentStackedElm = hoveredElmViews;
|
||||
}
|
||||
|
||||
override pointerUp(_evt: PointerEventState): void {
|
||||
pointerUp(_evt: PointerEventState): void {
|
||||
last(this._currentStackedElm)?.dispatch('pointerup', _evt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { PointerEventState } from '../../event/state/pointer.js';
|
||||
import { type GfxController } from '../controller.js';
|
||||
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
|
||||
import { GfxControllerIdentifier } from '../identifiers.js';
|
||||
import type { GfxModel } from '../model/model.js';
|
||||
import { type SupportedEvent } from '../view/view.js';
|
||||
import type {
|
||||
DragExtensionInitializeContext,
|
||||
@@ -20,6 +21,7 @@ import type {
|
||||
ExtensionDragMoveContext,
|
||||
ExtensionDragStartContext,
|
||||
} from './drag.js';
|
||||
import { CanvasEventHandler } from './extension/canvas-event-handler.js';
|
||||
|
||||
type ExtensionPointerHandler = Exclude<
|
||||
SupportedEvent,
|
||||
@@ -31,10 +33,7 @@ export const TransformManagerIdentifier = GfxExtensionIdentifier(
|
||||
) as ServiceIdentifier<ElementTransformManager>;
|
||||
|
||||
const CAMEL_CASE_MAP: {
|
||||
[key in ExtensionPointerHandler]: keyof Pick<
|
||||
TransformExtension,
|
||||
'click' | 'dblClick' | 'pointerDown' | 'pointerMove' | 'pointerUp'
|
||||
>;
|
||||
[key in ExtensionPointerHandler]: keyof CanvasEventHandler;
|
||||
} = {
|
||||
click: 'click',
|
||||
dblclick: 'dblClick',
|
||||
@@ -48,8 +47,10 @@ export class ElementTransformManager extends GfxExtension {
|
||||
|
||||
private readonly _disposable = new DisposableGroup();
|
||||
|
||||
private canvasEventHandler = new CanvasEventHandler(this.gfx);
|
||||
|
||||
override mounted(): void {
|
||||
//
|
||||
this.canvasEventHandler = new CanvasEventHandler(this.gfx);
|
||||
}
|
||||
|
||||
override unmounted(): void {
|
||||
@@ -72,20 +73,55 @@ export class ElementTransformManager extends GfxExtension {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the event to canvas elements
|
||||
* @param eventName
|
||||
* @param evt
|
||||
*/
|
||||
dispatch(eventName: ExtensionPointerHandler, evt: PointerEventState) {
|
||||
const transformExtensions = this.transformExtensions;
|
||||
const handlerName = CAMEL_CASE_MAP[eventName];
|
||||
|
||||
transformExtensions.forEach(ext => {
|
||||
const handlerMethodName = CAMEL_CASE_MAP[eventName];
|
||||
this.canvasEventHandler[handlerName](evt);
|
||||
|
||||
if (ext[handlerMethodName]) {
|
||||
this._safeExecute(() => {
|
||||
ext[handlerMethodName](evt);
|
||||
}, `Error while executing extension \`${handlerMethodName}\` handler`);
|
||||
}
|
||||
const extension = this.transformExtensions;
|
||||
|
||||
extension.forEach(ext => {
|
||||
ext[handlerName]?.(evt);
|
||||
});
|
||||
}
|
||||
|
||||
dispatchOnSelected(evt: PointerEventState) {
|
||||
const { raw } = evt;
|
||||
const { gfx } = this;
|
||||
const [x, y] = gfx.viewport.toModelCoordFromClientCoord([raw.x, raw.y]);
|
||||
const picked = this.gfx.getElementInGroup(x, y);
|
||||
|
||||
const tryGetLockedAncestor = (e: GfxModel) => {
|
||||
if (e?.isLockedByAncestor()) {
|
||||
return e.groups.findLast(group => group.isLocked()) ?? e;
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
if (picked) {
|
||||
const lockedElement = tryGetLockedAncestor(picked);
|
||||
const multiSelect = raw.shiftKey;
|
||||
const view = gfx.view.get(lockedElement);
|
||||
const context = {
|
||||
selected: multiSelect ? !gfx.selection.has(picked.id) : true,
|
||||
multiSelect,
|
||||
event: raw,
|
||||
position: Point.from([x, y]),
|
||||
fallback: lockedElement !== picked,
|
||||
};
|
||||
|
||||
view?.onSelected(context);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
initializeDrag(options: DragInitializationOption) {
|
||||
let cancelledByExt = false;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Bound } from '@blocksuite/global/gfx';
|
||||
import type { Bound, IPoint } from '@blocksuite/global/gfx';
|
||||
|
||||
import type { GfxBlockComponent } from '../../view';
|
||||
import type { GfxModel } from '../model/model';
|
||||
@@ -34,10 +34,44 @@ export type DragMoveContext = DragStartContext & {
|
||||
|
||||
export type DragEndContext = DragMoveContext;
|
||||
|
||||
export type SelectedContext = {
|
||||
/**
|
||||
* The selected state of the element
|
||||
*/
|
||||
selected: boolean;
|
||||
|
||||
/**
|
||||
* Whether is multi-select, usually triggered by shift key
|
||||
*/
|
||||
multiSelect: boolean;
|
||||
|
||||
/**
|
||||
* The pointer event that triggers the selection
|
||||
*/
|
||||
event: PointerEvent;
|
||||
|
||||
/**
|
||||
* The model position of the event pointer
|
||||
*/
|
||||
position: IPoint;
|
||||
|
||||
/**
|
||||
* If the current selection is a fallback selection, like selecting the element inside a group, the group will be selected instead
|
||||
*/
|
||||
fallback: boolean;
|
||||
};
|
||||
|
||||
export type GfxViewTransformInterface = {
|
||||
onDragStart: (context: DragStartContext) => void;
|
||||
onDragMove: (context: DragMoveContext) => void;
|
||||
onDragEnd: (context: DragEndContext) => void;
|
||||
onRotate: (context: {}) => void;
|
||||
onResize: (context: {}) => void;
|
||||
|
||||
/**
|
||||
* When the element is selected by the pointer
|
||||
* @param context
|
||||
* @returns
|
||||
*/
|
||||
onSelected: (context: SelectedContext) => void;
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ export type {
|
||||
DragMoveContext,
|
||||
DragStartContext,
|
||||
} from './element-transform/view-transform.js';
|
||||
export { type SelectedContext } from './element-transform/view-transform.js';
|
||||
export { GfxExtension, GfxExtensionIdentifier } from './extension.js';
|
||||
export { GridManager } from './grid.js';
|
||||
export { GfxControllerIdentifier } from './identifiers.js';
|
||||
|
||||
@@ -316,7 +316,7 @@ export class GfxSelectionManager extends GfxExtension {
|
||||
}
|
||||
|
||||
const { blocks = [], elements = [] } = groupBy(selection.elements, id => {
|
||||
return this.std.store.getModelById(id) ? 'blocks' : 'elements';
|
||||
return this.std.store.hasBlock(id) ? 'blocks' : 'elements';
|
||||
});
|
||||
let instances: (SurfaceSelection | CursorSelection)[] = [];
|
||||
|
||||
@@ -372,6 +372,21 @@ export class GfxSelectionManager extends GfxExtension {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the selection state of single element
|
||||
* @param element
|
||||
* @returns
|
||||
*/
|
||||
toggle(element: GfxModel | string) {
|
||||
element = typeof element === 'string' ? element : element.id;
|
||||
|
||||
this.set({
|
||||
elements: this.has(element)
|
||||
? this.selectedIds.filter(id => id !== element)
|
||||
: [...this.selectedIds, element],
|
||||
});
|
||||
}
|
||||
|
||||
setCursor(cursor: CursorSelection | IPoint) {
|
||||
const instance = this.stdSelection.create(
|
||||
CursorSelection,
|
||||
|
||||
@@ -506,6 +506,7 @@ export class ToolController extends GfxExtension {
|
||||
return;
|
||||
}
|
||||
|
||||
// explicitly clear the selection when switching tools
|
||||
this.gfx.selection.set({ elements: [] });
|
||||
|
||||
this.currentTool$.peek()?.deactivate();
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
DragMoveContext,
|
||||
DragStartContext,
|
||||
GfxViewTransformInterface,
|
||||
SelectedContext,
|
||||
} from '../element-transform/view-transform.js';
|
||||
import type { GfxController } from '../index.js';
|
||||
import type { GfxElementGeometry, PointTestOptions } from '../model/base.js';
|
||||
@@ -70,6 +71,10 @@ export class GfxElementModelView<
|
||||
return this.model.type;
|
||||
}
|
||||
|
||||
get std() {
|
||||
return this.gfx.std;
|
||||
}
|
||||
|
||||
constructor(
|
||||
model: T,
|
||||
readonly gfx: GfxController
|
||||
@@ -188,6 +193,16 @@ export class GfxElementModelView<
|
||||
this.model.xywh = currentBound.moveDelta(dx, dy).serialize();
|
||||
}
|
||||
|
||||
onSelected(context: SelectedContext) {
|
||||
if (this.model instanceof GfxPrimitiveElementModel) {
|
||||
if (context.multiSelect) {
|
||||
this.gfx.selection.toggle(this.model);
|
||||
} else {
|
||||
this.gfx.selection.set({ elements: [this.model.id] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onResize = () => {};
|
||||
|
||||
onRotate = () => {};
|
||||
|
||||
@@ -7,9 +7,10 @@ import type { BlockService } from '../../extension/index.js';
|
||||
import type {
|
||||
DragMoveContext,
|
||||
GfxViewTransformInterface,
|
||||
SelectedContext,
|
||||
} from '../../gfx/element-transform/view-transform.js';
|
||||
import { GfxControllerIdentifier } from '../../gfx/identifiers.js';
|
||||
import type { GfxBlockElementModel } from '../../gfx/index.js';
|
||||
import { type GfxBlockElementModel } from '../../gfx/model/gfx-block-model.js';
|
||||
import { SurfaceSelection } from '../../selection/index.js';
|
||||
import { BlockComponent } from './block-component.js';
|
||||
|
||||
@@ -81,6 +82,14 @@ export abstract class GfxBlockComponent<
|
||||
this.model.pop('xywh');
|
||||
}
|
||||
|
||||
onSelected(context: SelectedContext) {
|
||||
if (context.multiSelect) {
|
||||
this.gfx.selection.toggle(this.model);
|
||||
} else {
|
||||
this.gfx.selection.set({ elements: [this.model.id] });
|
||||
}
|
||||
}
|
||||
|
||||
onRotate() {}
|
||||
|
||||
onResize() {}
|
||||
@@ -186,6 +195,15 @@ export function toGfxBlockComponent<
|
||||
this.model.pop('xywh');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onSelected(context: SelectedContext) {
|
||||
if (context.multiSelect) {
|
||||
this.gfx.selection.toggle(this.model);
|
||||
} else {
|
||||
this.gfx.selection.set({ elements: [this.model.id] });
|
||||
}
|
||||
}
|
||||
|
||||
onRotate() {}
|
||||
|
||||
onResize() {}
|
||||
|
||||
Reference in New Issue
Block a user