mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
fix: rewrite selection logic and frame selection handling logic (#12421)
Fixes [BS-3528](https://linear.app/affine-design/issue/BS-3528) Fixes [BS-3331](https://linear.app/affine-design/issue/BS-3331/frame-移动逻辑很奇怪) ### Changed - Remove `onSelected` method from gfx view, use `handleSelection` provided by `GfxViewInteraction` instead. - Add `selectable` to allow model to filter out itself from selection. - Frame can be selected by body only if it's locked or its background is not transparent. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced selection behavior for frames, edgeless text, notes, and mind map elements with refined control based on lock state and background transparency. - Introduced group-aware selection logic promoting selection of appropriate group ancestors. - Added support for element selection events in interactivity extensions. - **Bug Fixes** - Resolved frame selection issues by enabling selection via title clicks and restricting body selection to locked frames or those with non-transparent backgrounds. - **Documentation** - Added clarifying comments for group retrieval methods. - **Tests** - Updated and added end-to-end tests for frame and lock selection reflecting new selection conditions. - **Refactor** - Unified and simplified selection handling by moving logic from component methods to interaction handlers and removing deprecated selection methods. - Streamlined selection candidate processing with extension-driven target suggestion. - Removed legacy group element retrieval and selection helper methods to simplify interaction logic. - **Style** - Renamed types and improved type signatures for selection context and interaction configurations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -2,6 +2,7 @@ import { type ServiceIdentifier } from '@blocksuite/global/di';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { Bound, clamp, Point } from '@blocksuite/global/gfx';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import last from 'lodash-es/last.js';
|
||||
|
||||
import type { PointerEventState } from '../../event/state/pointer.js';
|
||||
import { getTopElements } from '../../utils/tree.js';
|
||||
@@ -9,6 +10,7 @@ import type { GfxBlockComponent } from '../../view/index.js';
|
||||
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
|
||||
import { GfxBlockElementModel } from '../model/gfx-block-model.js';
|
||||
import type { GfxModel } from '../model/model.js';
|
||||
import { GfxPrimitiveElementModel } from '../model/surface/element-model.js';
|
||||
import type { GfxElementModelView } from '../view/view.js';
|
||||
import { createInteractionContext, type SupportedEvents } from './event.js';
|
||||
import {
|
||||
@@ -40,6 +42,7 @@ import type {
|
||||
BoxSelectionContext,
|
||||
ResizeConstraint,
|
||||
RotateConstraint,
|
||||
SelectContext,
|
||||
} from './types/view.js';
|
||||
|
||||
type ExtensionPointerHandler = Exclude<
|
||||
@@ -120,6 +123,112 @@ export class InteractivityManager extends GfxExtension {
|
||||
};
|
||||
}
|
||||
|
||||
private _getSelectionConfig(models: GfxModel[]) {
|
||||
type SelectionHandlers = Required<
|
||||
ReturnType<Required<GfxViewInteractionConfig>['handleSelection']>
|
||||
>;
|
||||
|
||||
const selectionConfigMap = new Map<
|
||||
string,
|
||||
{
|
||||
view: GfxBlockComponent | GfxElementModelView;
|
||||
handlers: SelectionHandlers;
|
||||
defaultHandlers: SelectionHandlers;
|
||||
}
|
||||
>();
|
||||
|
||||
models.forEach(model => {
|
||||
const typeOrFlavour = 'flavour' in model ? model.flavour : model.type;
|
||||
const view = this.gfx.view.get(model);
|
||||
const config = this.std.getOptional(
|
||||
GfxViewInteractionIdentifier(typeOrFlavour)
|
||||
);
|
||||
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectionConfig =
|
||||
config?.handleSelection?.({
|
||||
gfx: this.gfx,
|
||||
std: this.std,
|
||||
view,
|
||||
model,
|
||||
}) ?? {};
|
||||
const defaultHandlers = {
|
||||
selectable: () => {
|
||||
return !model.isLockedByAncestor();
|
||||
},
|
||||
onSelect: (context: SelectContext) => {
|
||||
if (context.multiSelect) {
|
||||
this.gfx.selection.toggle(model);
|
||||
} else {
|
||||
this.gfx.selection.set({ elements: [model.id] });
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
selectionConfigMap.set(model.id, {
|
||||
view,
|
||||
defaultHandlers,
|
||||
handlers: {
|
||||
...defaultHandlers,
|
||||
...selectionConfig,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return selectionConfigMap;
|
||||
}
|
||||
|
||||
private _getSuggestedTarget(context: {
|
||||
candidates: GfxModel[];
|
||||
target: GfxModel;
|
||||
}) {
|
||||
const { candidates, target } = context;
|
||||
|
||||
const suggestedElements: {
|
||||
id: string;
|
||||
priority?: number;
|
||||
}[] = [];
|
||||
const suggest = (element: { id: string; priority?: number }) => {
|
||||
suggestedElements.push(element);
|
||||
};
|
||||
|
||||
const extensions = this.interactExtensions;
|
||||
extensions
|
||||
.values()
|
||||
.toArray()
|
||||
.forEach(ext => {
|
||||
return (ext.action as InteractivityActionAPI).emit('elementSelect', {
|
||||
candidates,
|
||||
target,
|
||||
suggest,
|
||||
});
|
||||
});
|
||||
|
||||
if (suggestedElements.length) {
|
||||
suggestedElements.sort((a, b) => {
|
||||
return (a.priority ?? 0) - (b.priority ?? 0);
|
||||
});
|
||||
|
||||
const suggested = last(suggestedElements) as {
|
||||
id: string;
|
||||
priority?: number;
|
||||
};
|
||||
const elm = this.gfx.getElementById(suggested.id);
|
||||
|
||||
return elm instanceof GfxPrimitiveElementModel ||
|
||||
elm instanceof GfxBlockElementModel
|
||||
? elm
|
||||
: target;
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle element selection.
|
||||
* @param evt The pointer event that triggered the selection.
|
||||
@@ -129,32 +238,72 @@ export class InteractivityManager extends GfxExtension {
|
||||
const { raw } = evt;
|
||||
const { gfx } = this;
|
||||
const [x, y] = gfx.viewport.toModelCoordFromClientCoord([raw.x, raw.y]);
|
||||
const picked = this.gfx.getElementInGroup(x, y);
|
||||
let candidates = this.gfx.getElementByPoint(x, y, {
|
||||
all: true,
|
||||
});
|
||||
|
||||
const tryGetLockedAncestor = (e: GfxModel) => {
|
||||
if (e?.isLockedByAncestor()) {
|
||||
return e.groups.findLast(group => group.isLocked()) ?? e;
|
||||
}
|
||||
return e;
|
||||
const selectionConfigs = this._getSelectionConfig(candidates);
|
||||
const context = {
|
||||
multiSelect: raw.shiftKey,
|
||||
event: raw,
|
||||
position: Point.from([x, y]),
|
||||
};
|
||||
|
||||
if (picked) {
|
||||
const lockedElement = tryGetLockedAncestor(picked);
|
||||
candidates = candidates.filter(model => {
|
||||
if (!selectionConfigs.has(model.id)) {
|
||||
return false;
|
||||
}
|
||||
const config = selectionConfigs.get(model.id)!;
|
||||
|
||||
return (
|
||||
selectionConfigs.has(model.id) &&
|
||||
selectionConfigs.get(model.id)?.handlers.selectable({
|
||||
...context,
|
||||
view: config.view,
|
||||
model,
|
||||
default: config.defaultHandlers.selectable as () => boolean,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
{
|
||||
let target = last(candidates);
|
||||
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
target = this._getSuggestedTarget({
|
||||
candidates,
|
||||
target,
|
||||
});
|
||||
|
||||
const config = selectionConfigs.has(target.id)
|
||||
? selectionConfigs.get(target.id)
|
||||
: this._getSelectionConfig([target]).get(target.id);
|
||||
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const multiSelect = raw.shiftKey;
|
||||
const view = gfx.view.get(lockedElement);
|
||||
const context = {
|
||||
selected: multiSelect ? !gfx.selection.has(picked.id) : true,
|
||||
selected: multiSelect ? !gfx.selection.has(target.id) : true,
|
||||
multiSelect,
|
||||
event: raw,
|
||||
position: Point.from([x, y]),
|
||||
fallback: lockedElement !== picked,
|
||||
};
|
||||
|
||||
const selected = view?.onSelected(context);
|
||||
return selected ?? true;
|
||||
}
|
||||
const result = config.handlers.onSelect({
|
||||
...context,
|
||||
selected: multiSelect ? !gfx.selection.has(target.id) : true,
|
||||
view: config.view,
|
||||
model: target,
|
||||
default: config.defaultHandlers.onSelect as () => void,
|
||||
});
|
||||
|
||||
return false;
|
||||
return result ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
handleBoxSelection(context: { box: BoxSelectionContext['box'] }) {
|
||||
|
||||
Reference in New Issue
Block a user