mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-01 09:30:01 +08:00
refactor(editor): edgeless toolbar lock and unlock actions (#10878)
This commit is contained in:
@@ -9,7 +9,7 @@ import { builtinEdgelessTextToolbarConfig } from './edgeless-text';
|
||||
import { createFrameToolbarConfig } from './frame';
|
||||
import { builtinGroupToolbarConfig } from './group';
|
||||
import { builtinMindmapToolbarConfig } from './mindmap';
|
||||
import { builtinMiscToolbarConfig } from './misc';
|
||||
import { builtinLockedToolbarConfig, builtinMiscToolbarConfig } from './misc';
|
||||
import { builtinShapeToolbarConfig } from './shape';
|
||||
import { builtinTextToolbarConfig } from './text';
|
||||
|
||||
@@ -55,4 +55,11 @@ export const EdgelessElementToolbarExtension: ExtensionType[] = [
|
||||
id: BlockFlavourIdentifier('affine:surface:*'),
|
||||
config: builtinMiscToolbarConfig,
|
||||
}),
|
||||
|
||||
// Special Scenarios
|
||||
// Only display the `unlock` button when the selection includes a locked element.
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier('affine:surface:locked'),
|
||||
config: builtinLockedToolbarConfig,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
ConnectorElementModel,
|
||||
DEFAULT_CONNECTOR_MODE,
|
||||
GroupElementModel,
|
||||
MindmapElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
ActionPlacement,
|
||||
type ElementLockEvent,
|
||||
type ToolbarAction,
|
||||
type ToolbarContext,
|
||||
type ToolbarModuleConfig,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { GfxModel } from '@blocksuite/block-std/gfx';
|
||||
import {
|
||||
ConnectorCIcon,
|
||||
LockIcon,
|
||||
ReleaseFromGroupIcon,
|
||||
UnlockIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
|
||||
import { EdgelessRootBlockComponent } from '../..';
|
||||
|
||||
export const builtinMiscToolbarConfig = {
|
||||
actions: [
|
||||
{
|
||||
@@ -37,7 +46,6 @@ export const builtinMiscToolbarConfig = {
|
||||
when(ctx) {
|
||||
const models = ctx.getSurfaceModels();
|
||||
if (models.length !== 1) return false;
|
||||
if (models[0].isLocked()) return false;
|
||||
return !ctx.matchModel(models[0], ConnectorElementModel);
|
||||
},
|
||||
content(ctx) {
|
||||
@@ -74,9 +82,166 @@ export const builtinMiscToolbarConfig = {
|
||||
{
|
||||
placement: ActionPlacement.End,
|
||||
id: 'b.lock',
|
||||
icon: LockIcon(),
|
||||
tooltip: 'Lock',
|
||||
run() {},
|
||||
icon: LockIcon(),
|
||||
run(ctx) {
|
||||
const models = ctx.getSurfaceModels();
|
||||
if (!models.length) return;
|
||||
|
||||
const rootModel = ctx.store.root;
|
||||
if (!rootModel) return;
|
||||
|
||||
// TODO(@fundon): it should be simple
|
||||
const edgeless = ctx.view.getBlock(rootModel.id);
|
||||
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
|
||||
console.error('edgeless view is not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// get most top selected elements(*) from tree, like in a tree below
|
||||
// G0
|
||||
// / \
|
||||
// E1* G1
|
||||
// / \
|
||||
// E2* E3*
|
||||
//
|
||||
// (*) selected elements, [E1, E2, E3]
|
||||
// return [E1]
|
||||
|
||||
const elements = Array.from(
|
||||
new Set(
|
||||
models.map(model =>
|
||||
ctx.matchModel(model.group, MindmapElementModel)
|
||||
? model.group
|
||||
: model
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const levels = elements.map(element => element.groups.length);
|
||||
const topElement = elements[levels.indexOf(Math.min(...levels))];
|
||||
const otherElements = elements.filter(
|
||||
element => element !== topElement
|
||||
);
|
||||
|
||||
ctx.store.captureSync();
|
||||
|
||||
// release other elements from their groups and group with top element
|
||||
otherElements.forEach(element => {
|
||||
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
||||
element.group?.removeChild(element);
|
||||
topElement.group?.addChild(element);
|
||||
});
|
||||
|
||||
if (otherElements.length === 0) {
|
||||
topElement.lock();
|
||||
|
||||
ctx.gfx.selection.set({
|
||||
editing: false,
|
||||
elements: [topElement.id],
|
||||
});
|
||||
|
||||
track(ctx, topElement, 'lock');
|
||||
return;
|
||||
}
|
||||
|
||||
const groupId = edgeless.service.createGroup([
|
||||
topElement,
|
||||
...otherElements,
|
||||
]);
|
||||
|
||||
if (groupId) {
|
||||
const element = ctx.std
|
||||
.get(EdgelessCRUDIdentifier)
|
||||
.getElementById(groupId);
|
||||
|
||||
if (element) {
|
||||
element.lock();
|
||||
ctx.gfx.selection.set({
|
||||
editing: false,
|
||||
elements: [groupId],
|
||||
});
|
||||
|
||||
track(ctx, element, 'group-lock');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const element of elements) {
|
||||
element.lock();
|
||||
|
||||
track(ctx, element, 'lock');
|
||||
}
|
||||
|
||||
ctx.gfx.selection.set({
|
||||
editing: false,
|
||||
elements: elements.map(e => e.id),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
when(ctx) {
|
||||
const models = ctx.getSurfaceModels();
|
||||
return models.length > 0 && !models.some(model => model.isLocked());
|
||||
},
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
export const builtinLockedToolbarConfig = {
|
||||
actions: [
|
||||
{
|
||||
placement: ActionPlacement.End,
|
||||
id: 'b.unlock',
|
||||
label: 'Click to unlock',
|
||||
icon: UnlockIcon(),
|
||||
run(ctx) {
|
||||
const models = ctx.getSurfaceModels();
|
||||
if (!models.length) return;
|
||||
|
||||
const rootModel = ctx.store.root;
|
||||
if (!rootModel) return;
|
||||
|
||||
// TODO(@fundon): it should be simple
|
||||
const edgeless = ctx.view.getBlock(rootModel.id);
|
||||
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
|
||||
console.error('edgeless view is not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = new Set(
|
||||
models.map(model =>
|
||||
ctx.matchModel(model.group, MindmapElementModel)
|
||||
? model.group
|
||||
: model
|
||||
)
|
||||
);
|
||||
|
||||
ctx.store.captureSync();
|
||||
|
||||
for (const element of elements) {
|
||||
if (element instanceof GroupElementModel) {
|
||||
edgeless.service.ungroup(element);
|
||||
} else {
|
||||
element.lockedBySelf = false;
|
||||
}
|
||||
|
||||
track(ctx, element, 'unlock');
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
when: ctx => ctx.getSurfaceModels().some(model => model.isLocked()),
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
function track(
|
||||
ctx: ToolbarContext,
|
||||
element: GfxModel,
|
||||
control: ElementLockEvent['control']
|
||||
) {
|
||||
ctx.track('EdgelessElementLocked', {
|
||||
control,
|
||||
type:
|
||||
'flavour' in element
|
||||
? (element.flavour.split(':')[1] ?? element.flavour)
|
||||
: element.type,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user