mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 19:53:48 +08:00
refactor: default-tool box selection (#11800)
### Changed - Rewrite box selection in `default-tool`, the view can decide whether to be selected in box selection by return a boolean value in `onBoxSelected` method - Cleanup unnecessary states in `default-tool` and some naming problem
This commit is contained in:
@@ -2,7 +2,7 @@ import { DefaultTheme, type FrameBlockModel } from '@blocksuite/affine-model';
|
|||||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||||
import { Bound } from '@blocksuite/global/gfx';
|
import { Bound } from '@blocksuite/global/gfx';
|
||||||
import { GfxBlockComponent } from '@blocksuite/std';
|
import { GfxBlockComponent } from '@blocksuite/std';
|
||||||
import type { SelectedContext } from '@blocksuite/std/gfx';
|
import type { BoxSelectionContext, SelectedContext } from '@blocksuite/std/gfx';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { state } from 'lit/decorators.js';
|
import { state } from 'lit/decorators.js';
|
||||||
@@ -69,6 +69,16 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
|
|||||||
return super.onSelected(context);
|
return super.onSelected(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override onBoxSelected(context: BoxSelectionContext) {
|
||||||
|
const { box } = context;
|
||||||
|
const bound = new Bound(box.x, box.y, box.w, box.h);
|
||||||
|
const elementBound = this.model.elementBound;
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.model.childElements.length === 0 || bound.contains(elementBound)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
override renderGfxBlock() {
|
override renderGfxBlock() {
|
||||||
const { model, showBorder, std } = this;
|
const { model, showBorder, std } = this;
|
||||||
const backgroundColor = std
|
const backgroundColor = std
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '@blocksuite/affine-shared/utils';
|
} from '@blocksuite/affine-shared/utils';
|
||||||
import { Bound } from '@blocksuite/global/gfx';
|
import { Bound } from '@blocksuite/global/gfx';
|
||||||
import { toGfxBlockComponent } from '@blocksuite/std';
|
import { toGfxBlockComponent } from '@blocksuite/std';
|
||||||
import type { SelectedContext } from '@blocksuite/std/gfx';
|
import type { BoxSelectionContext, SelectedContext } from '@blocksuite/std/gfx';
|
||||||
import { html, nothing, type PropertyValues } from 'lit';
|
import { html, nothing, type PropertyValues } from 'lit';
|
||||||
import { query, state } from 'lit/decorators.js';
|
import { query, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
@@ -372,6 +372,10 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override onBoxSelected(_: BoxSelectionContext) {
|
||||||
|
return this.model.props.displayMode !== NoteDisplayMode.DocOnly;
|
||||||
|
}
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private accessor _editing = false;
|
private accessor _editing = false;
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -5,8 +5,10 @@ import { css, html, nothing, unsafeCSS } from 'lit';
|
|||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||||
import { DefaultTool } from '../../gfx-tool/default-tool.js';
|
import {
|
||||||
import { DefaultModeDragType } from '../../gfx-tool/default-tool-ext/ext.js';
|
DefaultModeDragType,
|
||||||
|
DefaultTool,
|
||||||
|
} from '../../gfx-tool/default-tool.js';
|
||||||
|
|
||||||
export const EDGELESS_DRAGGING_AREA_WIDGET = 'edgeless-dragging-area-rect';
|
export const EDGELESS_DRAGGING_AREA_WIDGET = 'edgeless-dragging-area-rect';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
export enum DefaultModeDragType {
|
|
||||||
/** Moving selected contents */
|
|
||||||
ContentMoving = 'content-moving',
|
|
||||||
/** Native range dragging inside active note block */
|
|
||||||
NativeEditing = 'native-editing',
|
|
||||||
/** Default void state */
|
|
||||||
None = 'none',
|
|
||||||
/** Expanding the dragging area, select the content covered inside */
|
|
||||||
Selecting = 'selecting',
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
import { isFrameBlock } from '@blocksuite/affine-block-frame';
|
|
||||||
import {
|
|
||||||
GroupElementModel,
|
|
||||||
MindmapElementModel,
|
|
||||||
NoteBlockModel,
|
|
||||||
NoteDisplayMode,
|
|
||||||
} from '@blocksuite/affine-model';
|
|
||||||
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 } from '@blocksuite/global/gfx';
|
|
||||||
import type { PointerEventState } from '@blocksuite/std';
|
import type { PointerEventState } from '@blocksuite/std';
|
||||||
import {
|
import {
|
||||||
BaseTool,
|
BaseTool,
|
||||||
getTopElements,
|
|
||||||
type GfxModel,
|
type GfxModel,
|
||||||
InteractivityIdentifier,
|
InteractivityIdentifier,
|
||||||
isGfxGroupCompatibleModel,
|
isGfxGroupCompatibleModel,
|
||||||
type PointTestOptions,
|
|
||||||
} from '@blocksuite/std/gfx';
|
} from '@blocksuite/std/gfx';
|
||||||
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 { DefaultModeDragType } from './default-tool-ext/ext.js';
|
|
||||||
|
export enum DefaultModeDragType {
|
||||||
|
/** Moving selected contents */
|
||||||
|
ContentMoving = 'content-moving',
|
||||||
|
/** Native range dragging inside active note block */
|
||||||
|
NativeEditing = 'native-editing',
|
||||||
|
/** Default void state */
|
||||||
|
None = 'none',
|
||||||
|
/** Expanding the dragging area, select the content covered inside */
|
||||||
|
Selecting = 'selecting',
|
||||||
|
}
|
||||||
|
|
||||||
export class DefaultTool extends BaseTool {
|
export class DefaultTool extends BaseTool {
|
||||||
static override toolName: string = 'default';
|
static override toolName: string = 'default';
|
||||||
@@ -44,11 +44,11 @@ export class DefaultTool extends BaseTool {
|
|||||||
|
|
||||||
private _disposables: DisposableGroup | null = null;
|
private _disposables: DisposableGroup | null = null;
|
||||||
|
|
||||||
private readonly _panViewport = (delta: IVec) => {
|
private _panViewport(delta: IVec) {
|
||||||
this._accumulateDelta[0] += delta[0];
|
this._accumulateDelta[0] += delta[0];
|
||||||
this._accumulateDelta[1] += delta[1];
|
this._accumulateDelta[1] += delta[1];
|
||||||
this.gfx.viewport.applyDeltaCenter(delta[0], delta[1]);
|
this.gfx.viewport.applyDeltaCenter(delta[0], delta[1]);
|
||||||
};
|
}
|
||||||
|
|
||||||
private _selectionRectTransition: null | {
|
private _selectionRectTransition: null | {
|
||||||
w: number;
|
w: number;
|
||||||
@@ -117,44 +117,21 @@ export class DefaultTool extends BaseTool {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { x, y, w, h } = this.controller.draggingArea$.peek();
|
const elements = this.interactivity?.handleBoxSelection({
|
||||||
const bound = new Bound(x, y, w, h);
|
box: this.controller.draggingArea$.peek(),
|
||||||
|
|
||||||
let elements = gfx.getElementsByBound(bound).filter(el => {
|
|
||||||
if (isFrameBlock(el)) {
|
|
||||||
return el.childElements.length === 0 || bound.contains(el.elementBound);
|
|
||||||
}
|
|
||||||
if (el instanceof MindmapElementModel) {
|
|
||||||
return bound.contains(el.elementBound);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
el instanceof NoteBlockModel &&
|
|
||||||
el.props.displayMode === NoteDisplayMode.DocOnly
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
elements = getTopElements(elements).filter(el => !el.isLocked());
|
if (!elements) return;
|
||||||
|
|
||||||
const set = new Set(
|
this.selection.set({
|
||||||
gfx.keyboard.shiftKey$.peek()
|
elements: elements.map(el => el.id),
|
||||||
? [...elements, ...gfx.selection.selectedElements]
|
|
||||||
: elements
|
|
||||||
);
|
|
||||||
|
|
||||||
this.edgelessSelectionManager.set({
|
|
||||||
elements: Array.from(set).map(element => element.id),
|
|
||||||
editing: false,
|
editing: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
dragType = DefaultModeDragType.None;
|
dragType = DefaultModeDragType.None;
|
||||||
|
|
||||||
enableHover = true;
|
movementDragging = false;
|
||||||
|
|
||||||
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
|
||||||
@@ -174,7 +151,7 @@ export class DefaultTool extends BaseTool {
|
|||||||
return [startX, startY] as IVec;
|
return [startX, startY] as IVec;
|
||||||
}
|
}
|
||||||
|
|
||||||
get edgelessSelectionManager() {
|
get selection() {
|
||||||
return this.gfx.selection;
|
return this.gfx.selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,52 +160,55 @@ export class DefaultTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _cloneContent() {
|
private async _cloneContent() {
|
||||||
const clonedResult = await this.interactivity?.requestElementsClone({
|
const clonedResult = await this.interactivity?.requestElementClone({
|
||||||
elements: this._toBeMoved,
|
elements: this._toBeMoved,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!clonedResult) return;
|
if (!clonedResult) return;
|
||||||
|
|
||||||
this._toBeMoved = clonedResult.elements;
|
this._toBeMoved = clonedResult.elements;
|
||||||
this.edgelessSelectionManager.set({
|
this.selection.set({
|
||||||
elements: this._toBeMoved.map(e => e.id),
|
elements: this._toBeMoved.map(e => e.id),
|
||||||
editing: false,
|
editing: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _determineDragType(e: PointerEventState): DefaultModeDragType {
|
private _determineDragType(evt: PointerEventState): DefaultModeDragType {
|
||||||
const { x, y } = e;
|
const { x, y } = this.controller.lastMouseModelPos$.peek();
|
||||||
// Is dragging started from current selected rect
|
if (this.selection.isInSelectedRect(x, y)) {
|
||||||
if (this.edgelessSelectionManager.isInSelectedRect(x, y)) {
|
if (this.selection.selectedElements.length === 1) {
|
||||||
if (this.edgelessSelectionManager.selectedElements.length === 1) {
|
const currentHoveredElem = this._getElementInGroup(x, y);
|
||||||
let selected = this.edgelessSelectionManager.selectedElements[0];
|
let curSelected = this.selection.selectedElements[0];
|
||||||
// double check
|
|
||||||
const currentSelected = this._pick(x, y);
|
// If one of the following condition is true, keep the selection:
|
||||||
if (
|
// 1. if group is currently selected
|
||||||
!isFrameBlock(selected) &&
|
// 2. if the selected element is descendant of the hovered element
|
||||||
!(selected instanceof GroupElementModel) &&
|
// 3. not hovering any element or hovering the same element
|
||||||
currentSelected &&
|
//
|
||||||
currentSelected !== selected
|
// Otherwise, we update the selection to the current hovered element
|
||||||
) {
|
const shouldKeepSelection =
|
||||||
selected = currentSelected;
|
isGfxGroupCompatibleModel(curSelected) ||
|
||||||
this.edgelessSelectionManager.set({
|
(isGfxGroupCompatibleModel(currentHoveredElem) &&
|
||||||
elements: [selected.id],
|
currentHoveredElem.hasDescendant(curSelected)) ||
|
||||||
|
!currentHoveredElem ||
|
||||||
|
currentHoveredElem === curSelected;
|
||||||
|
|
||||||
|
if (!shouldKeepSelection) {
|
||||||
|
curSelected = currentHoveredElem;
|
||||||
|
this.selection.set({
|
||||||
|
elements: [curSelected.id],
|
||||||
editing: false,
|
editing: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.edgelessSelectionManager.editing
|
return this.selection.editing
|
||||||
? DefaultModeDragType.NativeEditing
|
? DefaultModeDragType.NativeEditing
|
||||||
: DefaultModeDragType.ContentMoving;
|
: DefaultModeDragType.ContentMoving;
|
||||||
} else {
|
} else {
|
||||||
const selected = this._pick(x, y);
|
const checked = this.interactivity?.handleElementSelection(evt);
|
||||||
if (selected) {
|
|
||||||
this.edgelessSelectionManager.set({
|
|
||||||
elements: [selected.id],
|
|
||||||
editing: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
return DefaultModeDragType.ContentMoving;
|
return DefaultModeDragType.ContentMoving;
|
||||||
} else {
|
} else {
|
||||||
return DefaultModeDragType.Selecting;
|
return DefaultModeDragType.Selecting;
|
||||||
@@ -236,9 +216,7 @@ export class DefaultTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _pick(x: number, y: number, options?: PointTestOptions) {
|
private _getElementInGroup(modelX: number, modelY: number) {
|
||||||
const modelPos = this.gfx.viewport.toModelCoord(x, y);
|
|
||||||
|
|
||||||
const tryGetLockedAncestor = (e: GfxModel | null) => {
|
const tryGetLockedAncestor = (e: GfxModel | null) => {
|
||||||
if (e?.isLockedByAncestor()) {
|
if (e?.isLockedByAncestor()) {
|
||||||
return e.groups.findLast(group => group.isLocked());
|
return e.groups.findLast(group => group.isLocked());
|
||||||
@@ -246,34 +224,7 @@ export class DefaultTool extends BaseTool {
|
|||||||
return e;
|
return e;
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = this.gfx.getElementInGroup(
|
return tryGetLockedAncestor(this.gfx.getElementInGroup(modelX, modelY));
|
||||||
modelPos[0],
|
|
||||||
modelPos[1],
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result instanceof MindmapElementModel) {
|
|
||||||
const picked = this.gfx.getElementByPoint(modelPos[0], modelPos[1], {
|
|
||||||
...((options ?? {}) as PointTestOptions),
|
|
||||||
all: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let pickedIdx = picked.length - 1;
|
|
||||||
|
|
||||||
while (pickedIdx >= 0) {
|
|
||||||
const element = picked[pickedIdx];
|
|
||||||
if (element === result) {
|
|
||||||
pickedIdx -= 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tryGetLockedAncestor(picked[pickedIdx]) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tryGetLockedAncestor(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeDragState(
|
private initializeDragState(
|
||||||
@@ -305,7 +256,7 @@ export class DefaultTool extends BaseTool {
|
|||||||
if (this.dragType === DefaultModeDragType.ContentMoving) {
|
if (this.dragType === DefaultModeDragType.ContentMoving) {
|
||||||
if (this.interactivity) {
|
if (this.interactivity) {
|
||||||
this.doc.captureSync();
|
this.doc.captureSync();
|
||||||
this.interactivity.initializeDrag({
|
this.interactivity.handleElementMove({
|
||||||
movingElements: this._toBeMoved,
|
movingElements: this._toBeMoved,
|
||||||
event: event.raw,
|
event: event.raw,
|
||||||
onDragEnd: () => {
|
onDragEnd: () => {
|
||||||
@@ -320,8 +271,8 @@ export class DefaultTool extends BaseTool {
|
|||||||
override click(e: PointerEventState) {
|
override click(e: PointerEventState) {
|
||||||
if (this.doc.readonly) return;
|
if (this.doc.readonly) return;
|
||||||
|
|
||||||
if (!this.interactivity?.dispatchOnSelected(e)) {
|
if (!this.interactivity?.handleElementSelection(e)) {
|
||||||
this.edgelessSelectionManager.clear();
|
this.selection.clear();
|
||||||
resetNativeSelection(null);
|
resetNativeSelection(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,9 +304,9 @@ export class DefaultTool extends BaseTool {
|
|||||||
override dragEnd(e: PointerEventState) {
|
override dragEnd(e: PointerEventState) {
|
||||||
this.interactivity?.dispatchEvent('dragend', e);
|
this.interactivity?.dispatchEvent('dragend', e);
|
||||||
|
|
||||||
if (this.edgelessSelectionManager.editing || !this.dragging) return;
|
if (this.selection.editing || !this.movementDragging) return;
|
||||||
|
|
||||||
this.dragging = false;
|
this.movementDragging = false;
|
||||||
this._toBeMoved = [];
|
this._toBeMoved = [];
|
||||||
this._clearSelectingState();
|
this._clearSelectingState();
|
||||||
this.dragType = DefaultModeDragType.None;
|
this.dragType = DefaultModeDragType.None;
|
||||||
@@ -364,7 +315,7 @@ export class DefaultTool extends BaseTool {
|
|||||||
override dragMove(e: PointerEventState) {
|
override dragMove(e: PointerEventState) {
|
||||||
this.interactivity?.dispatchEvent('dragmove', e);
|
this.interactivity?.dispatchEvent('dragmove', e);
|
||||||
|
|
||||||
if (!this.dragging) {
|
if (!this.movementDragging) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,19 +348,14 @@ export class DefaultTool extends BaseTool {
|
|||||||
const { preventDefaultState, handledByView } =
|
const { preventDefaultState, handledByView } =
|
||||||
this.interactivity?.dispatchEvent('dragstart', e) ?? {};
|
this.interactivity?.dispatchEvent('dragstart', e) ?? {};
|
||||||
|
|
||||||
if (
|
if (this.selection.editing || preventDefaultState || handledByView) return;
|
||||||
this.edgelessSelectionManager.editing ||
|
|
||||||
preventDefaultState ||
|
|
||||||
handledByView
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.dragging = true;
|
this.movementDragging = 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);
|
||||||
|
|
||||||
const elements = this.edgelessSelectionManager.selectedElements;
|
const elements = this.selection.selectedElements;
|
||||||
if (elements.some(e => e.isLocked())) return;
|
if (elements.some(e => e.isLocked())) return;
|
||||||
|
|
||||||
const toBeMoved = new Set(elements);
|
const toBeMoved = new Set(elements);
|
||||||
@@ -468,8 +414,6 @@ export class DefaultTool extends BaseTool {
|
|||||||
this.interactivity?.dispatchEvent('pointerup', e);
|
this.interactivity?.dispatchEvent('pointerup', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
override tripleClick() {}
|
|
||||||
|
|
||||||
override unmounted(): void {}
|
override unmounted(): void {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,13 @@ import {
|
|||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||||
import { requestThrottledConnectedFrame } from '@blocksuite/affine-shared/utils';
|
import { requestThrottledConnectedFrame } from '@blocksuite/affine-shared/utils';
|
||||||
|
import { Bound } from '@blocksuite/global/gfx';
|
||||||
import type { PointerEventState } from '@blocksuite/std';
|
import type { PointerEventState } from '@blocksuite/std';
|
||||||
import { GfxElementModelView } from '@blocksuite/std/gfx';
|
import {
|
||||||
|
type BoxSelectionContext,
|
||||||
|
GfxElementModelView,
|
||||||
|
type SelectedContext,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
import { handleLayout } from './utils.js';
|
import { handleLayout } from './utils.js';
|
||||||
|
|
||||||
@@ -329,6 +334,40 @@ export class MindMapView extends GfxElementModelView<MindmapElementModel> {
|
|||||||
return collapseButton;
|
return collapseButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override onSelected(context: SelectedContext): void | boolean {
|
||||||
|
const { position } = context;
|
||||||
|
const target = this.model.childElements.find(child => {
|
||||||
|
if (child.elementBound.containsPoint([position.x, position.y])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
if (this.model.isLocked()) {
|
||||||
|
return super.onSelected(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.multiSelect) {
|
||||||
|
this.gfx.selection.toggle(target);
|
||||||
|
} else {
|
||||||
|
this.gfx.selection.set({ elements: [target.id] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
override onBoxSelected(context: BoxSelectionContext) {
|
||||||
|
const { box } = context;
|
||||||
|
const bound = new Bound(box.x, box.y, box.w, box.h);
|
||||||
|
|
||||||
|
return bound.contains(this.model.elementBound);
|
||||||
|
}
|
||||||
|
|
||||||
override onCreated(): void {
|
override onCreated(): void {
|
||||||
this._setLayoutMethod();
|
this._setLayoutMethod();
|
||||||
this._initCollapseButtons();
|
this._initCollapseButtons();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export { GfxExtension, GfxExtensionIdentifier } from './extension.js';
|
|||||||
export { GridManager } from './grid.js';
|
export { GridManager } from './grid.js';
|
||||||
export { GfxControllerIdentifier } from './identifiers.js';
|
export { GfxControllerIdentifier } from './identifiers.js';
|
||||||
export type {
|
export type {
|
||||||
|
BoxSelectionContext,
|
||||||
DragEndContext,
|
DragEndContext,
|
||||||
DragExtensionInitializeContext,
|
DragExtensionInitializeContext,
|
||||||
DragInitializationOption,
|
DragInitializationOption,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type {
|
|||||||
ExtensionDragStartContext,
|
ExtensionDragStartContext,
|
||||||
} from './types/drag.js';
|
} from './types/drag.js';
|
||||||
export type {
|
export type {
|
||||||
|
BoxSelectionContext,
|
||||||
DragEndContext,
|
DragEndContext,
|
||||||
DragMoveContext,
|
DragMoveContext,
|
||||||
DragStartContext,
|
DragStartContext,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DisposableGroup } from '@blocksuite/global/disposable';
|
|||||||
import { Bound, Point } from '@blocksuite/global/gfx';
|
import { Bound, Point } from '@blocksuite/global/gfx';
|
||||||
|
|
||||||
import type { PointerEventState } from '../../event/state/pointer.js';
|
import type { PointerEventState } from '../../event/state/pointer.js';
|
||||||
|
import { getTopElements } from '../../utils/tree.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 { createInteractionContext, type SupportedEvents } from './event.js';
|
import { createInteractionContext, type SupportedEvents } from './event.js';
|
||||||
@@ -20,6 +21,7 @@ import type {
|
|||||||
ExtensionDragMoveContext,
|
ExtensionDragMoveContext,
|
||||||
ExtensionDragStartContext,
|
ExtensionDragStartContext,
|
||||||
} from './types/drag.js';
|
} from './types/drag.js';
|
||||||
|
import type { BoxSelectionContext } from './types/view.js';
|
||||||
|
|
||||||
type ExtensionPointerHandler = Exclude<
|
type ExtensionPointerHandler = Exclude<
|
||||||
SupportedEvents,
|
SupportedEvents,
|
||||||
@@ -90,7 +92,12 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchOnSelected(evt: PointerEventState) {
|
/**
|
||||||
|
* Handle element selection.
|
||||||
|
* @param evt The pointer event that triggered the selection.
|
||||||
|
* @returns True if the element was selected, false otherwise.
|
||||||
|
*/
|
||||||
|
handleElementSelection(evt: PointerEventState) {
|
||||||
const { raw } = evt;
|
const { raw } = evt;
|
||||||
const { gfx } = this;
|
const { gfx } = this;
|
||||||
const [x, y] = gfx.viewport.toModelCoordFromClientCoord([raw.x, raw.y]);
|
const [x, y] = gfx.viewport.toModelCoordFromClientCoord([raw.x, raw.y]);
|
||||||
@@ -122,15 +129,30 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleBoxSelection(context: { box: BoxSelectionContext['box'] }) {
|
||||||
|
const elements = this.gfx.getElementsByBound(context.box).filter(model => {
|
||||||
|
const view = this.gfx.view.get(model);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!view ||
|
||||||
|
view.onBoxSelected({
|
||||||
|
box: context.box,
|
||||||
|
}) === false
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return getTopElements(elements).filter(elm => !elm.isLocked());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize drag operation for elements.
|
* Initialize elements movements.
|
||||||
* Handles drag start, move and end events automatically.
|
* It will handle drag start, move and end events automatically.
|
||||||
* Note: Call this when mouse is already down.
|
* Note: Call this when mouse is already down.
|
||||||
*
|
|
||||||
* @param options
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
initializeDrag(options: DragInitializationOption) {
|
handleElementMove(options: DragInitializationOption) {
|
||||||
let cancelledByExt = false;
|
let cancelledByExt = false;
|
||||||
|
|
||||||
const context: DragExtensionInitializeContext = {
|
const context: DragExtensionInitializeContext = {
|
||||||
@@ -294,7 +316,7 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
dragStart();
|
dragStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
requestElementsClone(options: RequestElementsCloneContext) {
|
requestElementClone(options: RequestElementsCloneContext) {
|
||||||
const extensions = this.interactExtensions;
|
const extensions = this.interactExtensions;
|
||||||
|
|
||||||
for (let ext of extensions.values()) {
|
for (let ext of extensions.values()) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Bound, IPoint } from '@blocksuite/global/gfx';
|
import type { Bound, IBound, IPoint } from '@blocksuite/global/gfx';
|
||||||
|
|
||||||
import type { GfxBlockComponent } from '../../../view/element/gfx-block-component.js';
|
import type { GfxBlockComponent } from '../../../view/element/gfx-block-component.js';
|
||||||
import type { GfxModel } from '../../model/model.js';
|
import type { GfxModel } from '../../model/model.js';
|
||||||
@@ -56,11 +56,25 @@ export type SelectedContext = {
|
|||||||
position: IPoint;
|
position: IPoint;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the current selection is a fallback selection, like selecting the element inside a group, the group will be selected instead
|
* If the current selection is a fallback selection.
|
||||||
|
*
|
||||||
|
* E.g., if selecting a child element inside a group, the `onSelected` method will be executed on group, and
|
||||||
|
* the fallback is true because the it's not the original target(the child element).
|
||||||
*/
|
*/
|
||||||
fallback: boolean;
|
fallback: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BoxSelectionContext = {
|
||||||
|
box: Readonly<
|
||||||
|
IBound & {
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
endX: number;
|
||||||
|
endY: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
export type GfxViewTransformInterface = {
|
export type GfxViewTransformInterface = {
|
||||||
onDragStart: (context: DragStartContext) => void;
|
onDragStart: (context: DragStartContext) => void;
|
||||||
onDragMove: (context: DragMoveContext) => void;
|
onDragMove: (context: DragMoveContext) => void;
|
||||||
@@ -70,8 +84,11 @@ export type GfxViewTransformInterface = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* When the element is selected by the pointer
|
* When the element is selected by the pointer
|
||||||
* @param context
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
onSelected: (context: SelectedContext) => void;
|
onSelected: (context: SelectedContext) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the element is selected by box selection, return false to prevent the default selection behavior.
|
||||||
|
*/
|
||||||
|
onBoxSelected: (context: BoxSelectionContext) => boolean | void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -204,12 +204,11 @@ export class GfxSelectionManager extends GfxExtension {
|
|||||||
return selections.every(sel => sel.elements.length === 0);
|
return selections.every(sel => sel.elements.length === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
isInSelectedRect(viewX: number, viewY: number) {
|
isInSelectedRect(modelX: number, modelY: number) {
|
||||||
const selected = this.selectedElements;
|
const selected = this.selectedElements;
|
||||||
if (!selected.length) return false;
|
if (!selected.length) return false;
|
||||||
|
|
||||||
const commonBound = getCommonBoundWithRotation(selected);
|
const commonBound = getCommonBoundWithRotation(selected);
|
||||||
const [modelX, modelY] = this.gfx.viewport.toModelCoord(viewX, viewY);
|
|
||||||
if (commonBound && commonBound.isPointInBound([modelX, modelY])) {
|
if (commonBound && commonBound.isPointInBound([modelX, modelY])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ServiceIdentifier } from '@blocksuite/global/di';
|
|||||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||||
import type { IBound, IPoint } from '@blocksuite/global/gfx';
|
import type { IBound, IPoint } from '@blocksuite/global/gfx';
|
||||||
import { Signal } from '@preact/signals-core';
|
import { computed, Signal } from '@preact/signals-core';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
import type { PointerEventState } from '../../event/index.js';
|
import type { PointerEventState } from '../../event/index.js';
|
||||||
@@ -125,6 +125,18 @@ export class ToolController extends GfxExtension {
|
|||||||
y: 0,
|
y: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
readonly lastMouseModelPos$ = computed(() => {
|
||||||
|
const [x, y] = this.gfx.viewport.toModelCoord(
|
||||||
|
this.lastMousePos$.value.x,
|
||||||
|
this.lastMousePos$.value.y
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
get currentTool$() {
|
get currentTool$() {
|
||||||
// oxlint-disable-next-line typescript/no-this-alias
|
// oxlint-disable-next-line typescript/no-this-alias
|
||||||
const self = this;
|
const self = this;
|
||||||
@@ -330,6 +342,10 @@ export class ToolController extends GfxExtension {
|
|||||||
w: 0,
|
w: 0,
|
||||||
h: 0,
|
h: 0,
|
||||||
};
|
};
|
||||||
|
this.lastMousePos$.value = {
|
||||||
|
x: evt.x,
|
||||||
|
y: evt.y,
|
||||||
|
};
|
||||||
|
|
||||||
// this means the dragEnd event is not even fired
|
// this means the dragEnd event is not even fired
|
||||||
// so we need to manually call the dragEnd method
|
// so we need to manually call the dragEnd method
|
||||||
@@ -372,6 +388,11 @@ export class ToolController extends GfxExtension {
|
|||||||
endY: evt.y,
|
endY: evt.y,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.lastMousePos$.value = {
|
||||||
|
x: evt.x,
|
||||||
|
y: evt.y,
|
||||||
|
};
|
||||||
|
|
||||||
invokeToolHandler('dragMove', evt, dragContext?.tool);
|
invokeToolHandler('dragMove', evt, dragContext?.tool);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { type Container, createIdentifier } from '@blocksuite/global/di';
|
import { type Container, createIdentifier } from '@blocksuite/global/di';
|
||||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||||
import type { Bound, IVec } from '@blocksuite/global/gfx';
|
import { type Bound, type IVec } from '@blocksuite/global/gfx';
|
||||||
import type { Extension } from '@blocksuite/store';
|
import type { Extension } from '@blocksuite/store';
|
||||||
|
|
||||||
import type { PointerEventState } from '../../event/index.js';
|
import type { PointerEventState } from '../../event/index.js';
|
||||||
import type { EditorHost } from '../../view/index.js';
|
import type { EditorHost } from '../../view/index.js';
|
||||||
import type { GfxController } from '../index.js';
|
import type { GfxController } from '../index.js';
|
||||||
import type {
|
import type {
|
||||||
|
BoxSelectionContext,
|
||||||
DragEndContext,
|
DragEndContext,
|
||||||
DragMoveContext,
|
DragMoveContext,
|
||||||
DragStartContext,
|
DragStartContext,
|
||||||
@@ -221,6 +222,8 @@ export class GfxElementModelView<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBoxSelected(_: BoxSelectionContext): boolean | void {}
|
||||||
|
|
||||||
onResize = () => {};
|
onResize = () => {};
|
||||||
|
|
||||||
onRotate = () => {};
|
onRotate = () => {};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { nothing } from 'lit';
|
|||||||
import type { BlockService } from '../../extension/index.js';
|
import type { BlockService } from '../../extension/index.js';
|
||||||
import { GfxControllerIdentifier } from '../../gfx/identifiers.js';
|
import { GfxControllerIdentifier } from '../../gfx/identifiers.js';
|
||||||
import type {
|
import type {
|
||||||
|
BoxSelectionContext,
|
||||||
DragMoveContext,
|
DragMoveContext,
|
||||||
GfxViewTransformInterface,
|
GfxViewTransformInterface,
|
||||||
SelectedContext,
|
SelectedContext,
|
||||||
@@ -113,6 +114,8 @@ export abstract class GfxBlockComponent<
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBoxSelected(_: BoxSelectionContext) {}
|
||||||
|
|
||||||
onRotate() {}
|
onRotate() {}
|
||||||
|
|
||||||
onResize() {}
|
onResize() {}
|
||||||
@@ -231,6 +234,8 @@ export function toGfxBlockComponent<
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBoxSelected(_: BoxSelectionContext) {}
|
||||||
|
|
||||||
onRotate() {}
|
onRotate() {}
|
||||||
|
|
||||||
onResize() {}
|
onResize() {}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const dragBetweenViewCoords = async (
|
|||||||
end: number[]
|
end: number[]
|
||||||
) => {
|
) => {
|
||||||
// dragging slowly may drop frame if mindmap is existed, so for test we drag quickly
|
// dragging slowly may drop frame if mindmap is existed, so for test we drag quickly
|
||||||
await _dragBetweenViewCoords(page, start, end, { steps: 2 });
|
await _dragBetweenViewCoords(page, start, end, { steps: 10 });
|
||||||
await waitNextFrame(page);
|
await waitNextFrame(page);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
import { clickView } from '../utils/actions/click.js';
|
import { click, clickView } from '../utils/actions/click.js';
|
||||||
import { dragBetweenCoords } from '../utils/actions/drag.js';
|
import { dragBetweenCoords } from '../utils/actions/drag.js';
|
||||||
import {
|
import {
|
||||||
addBasicRectShapeElement,
|
addBasicRectShapeElement,
|
||||||
@@ -272,7 +272,7 @@ test('drag root node should layout in real time', async ({ page }) => {
|
|||||||
|
|
||||||
// assert when dragging is in progress
|
// assert when dragging is in progress
|
||||||
await waitNextFrame(page, 500);
|
await waitNextFrame(page, 500);
|
||||||
await assertMindMapNodesPosition(50, 50);
|
await assertMindMapNodesPosition(54, 54);
|
||||||
|
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
});
|
});
|
||||||
@@ -297,6 +297,7 @@ test('drag node out of mind map should detach the node and create a new mind map
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { rect } = await getMindMapNode(page, mindmapId, [0, 1]);
|
const { rect } = await getMindMapNode(page, mindmapId, [0, 1]);
|
||||||
|
await click(page, { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 });
|
||||||
await dragBetweenCoords(
|
await dragBetweenCoords(
|
||||||
page,
|
page,
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user