refactor(editor): move gfx frame title to widget (#11021)

This commit is contained in:
Saul-Mirone
2025-03-20 04:10:56 +00:00
parent 120e193c58
commit 75580e881b
26 changed files with 195 additions and 147 deletions
@@ -0,0 +1,88 @@
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
import {
type FrameBlockModel,
MindmapElementModel,
} from '@blocksuite/affine-model';
import {
type DragExtensionInitializeContext,
type ExtensionDragEndContext,
type ExtensionDragMoveContext,
type ExtensionDragStartContext,
getTopElements,
GfxExtensionIdentifier,
TransformExtension,
} from '@blocksuite/block-std/gfx';
import {
type EdgelessFrameManager,
type FrameOverlay,
isFrameBlock,
} from './frame-manager';
export class FrameHighlightManager extends TransformExtension {
static override key = 'frame-highlight-manager';
get frameMgr() {
return this.std.getOptional(
GfxExtensionIdentifier('frame-manager')
) as EdgelessFrameManager;
}
get frameHighlightOverlay() {
return this.std.getOptional(OverlayIdentifier('frame')) as FrameOverlay;
}
override onDragInitialize(_: DragExtensionInitializeContext): {
onDragStart?: (context: ExtensionDragStartContext) => void;
onDragMove?: (context: ExtensionDragMoveContext) => void;
onDragEnd?: (context: ExtensionDragEndContext) => void;
clear?: () => void;
} {
if (!this.frameMgr || !this.frameHighlightOverlay) {
return {};
}
let hoveredFrame: FrameBlockModel | null = null;
const { frameMgr, frameHighlightOverlay } = this;
let draggedFrames: FrameBlockModel[] = [];
return {
onDragStart(context) {
draggedFrames = context.elements
.map(elem => elem.model)
.filter(model => isFrameBlock(model));
},
onDragMove(context) {
const { dragLastPos } = context;
hoveredFrame = frameMgr.getFrameFromPoint(
[dragLastPos.x, dragLastPos.y],
draggedFrames
);
if (hoveredFrame && !hoveredFrame.isLocked()) {
frameHighlightOverlay.highlight(hoveredFrame);
} else {
frameHighlightOverlay.clear();
}
},
onDragEnd(context) {
const topElements = getTopElements(
context.elements.map(elem =>
elem.model.group instanceof MindmapElementModel
? elem.model.group
: elem.model
)
);
if (hoveredFrame) {
frameMgr.addElementsToFrame(hoveredFrame, topElements);
} else {
topElements.forEach(elem => frameMgr.removeFromParentFrame(elem));
}
frameHighlightOverlay.clear();
},
};
}
}
@@ -0,0 +1,110 @@
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import {
EditPropsStore,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import type { PointerEventState } from '@blocksuite/block-std';
import { BaseTool, getTopElements } from '@blocksuite/block-std/gfx';
import type { IPoint, IVec } from '@blocksuite/global/gfx';
import { Bound, Vec } from '@blocksuite/global/gfx';
import { Text } from '@blocksuite/store';
import * as Y from 'yjs';
import {
EdgelessFrameManagerIdentifier,
type FrameOverlay,
} from './frame-manager';
export class FrameTool extends BaseTool {
static override toolName = 'frame';
private _frame: FrameBlockModel | null = null;
private _startPoint: IVec | null = null;
get frameManager() {
return this.std.get(EdgelessFrameManagerIdentifier);
}
get frameOverlay() {
return this.std.get(OverlayIdentifier('frame')) as FrameOverlay;
}
private _toModelCoord(p: IPoint): IVec {
return this.gfx.viewport.toModelCoord(p.x, p.y);
}
override dragEnd(): void {
if (this._frame) {
const frame = this._frame;
this.doc.transact(() => {
frame.pop('xywh');
});
// @ts-expect-error TODO: refactor gfx tool
this.gfx.tool.setTool('default');
this.gfx.selection.set({
elements: [frame.id],
editing: false,
});
this.frameManager.addElementsToFrame(
frame,
getTopElements(this.frameManager.getElementsInFrameBound(frame))
);
this.doc.captureSync();
}
this._frame = null;
this._startPoint = null;
this.frameOverlay.clear();
}
override dragMove(e: PointerEventState): void {
if (!this._startPoint) return;
const currentPoint = this._toModelCoord(e.point);
if (Vec.dist(this._startPoint, currentPoint) < 8 && !this._frame) return;
if (!this._frame) {
const frames = this.gfx.layer.blocks.filter(
block => block.flavour === 'affine:frame'
) as FrameBlockModel[];
const props = this.std
.get(EditPropsStore)
.applyLastProps('affine:frame', {
title: new Text(new Y.Text(`Frame ${frames.length + 1}`)),
xywh: Bound.fromPoints([this._startPoint, currentPoint]).serialize(),
index: this.gfx.layer.generateIndex(true),
presentationIndex: this.frameManager.generatePresentationIndex(),
});
const id = this.doc.addBlock('affine:frame', props, this.gfx.surface);
this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
control: 'canvas:draw',
page: 'whiteboard editor',
module: 'toolbar',
segment: 'toolbar',
type: 'frame',
});
this._frame = this.gfx.getElementById(id) as FrameBlockModel;
this._frame.stash('xywh');
return;
}
this.gfx.doc.updateBlock(this._frame, {
xywh: Bound.fromPoints([this._startPoint, currentPoint]).serialize(),
});
this.frameOverlay.highlight(this._frame, true);
}
override dragStart(e: PointerEventState): void {
this.doc.captureSync();
const { point } = e;
this._startPoint = this._toModelCoord(point);
}
}
@@ -0,0 +1,198 @@
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
import {
packColor,
type PickColorEvent,
} from '@blocksuite/affine-components/color-picker';
import { toast } from '@blocksuite/affine-components/toast';
import {
DEFAULT_NOTE_HEIGHT,
DefaultTheme,
FrameBlockModel,
FrameBlockSchema,
NoteBlockModel,
NoteBlockSchema,
NoteDisplayMode,
resolveColor,
SurfaceRefBlockSchema,
} from '@blocksuite/affine-model';
import {
type ToolbarContext,
type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services';
import {
getMostCommonResolvedValue,
matchModels,
} from '@blocksuite/affine-shared/utils';
import { mountFrameTitleEditor } from '@blocksuite/affine-widget-frame-title';
import {
type BlockComponent,
BlockFlavourIdentifier,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound } from '@blocksuite/global/gfx';
import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit';
import type { ExtensionType } from '@blocksuite/store';
import { html } from 'lit';
import { EdgelessFrameManagerIdentifier } from './frame-manager';
function getRootBlock(ctx: ToolbarContext): BlockComponent | null {
const rootModel = ctx.store.root;
if (!rootModel) return null;
return ctx.view.getBlock(rootModel.id);
}
const builtinSurfaceToolbarConfig = {
actions: [
{
id: 'a.insert-into-page',
label: 'Insert into Page',
showLabel: true,
tooltip: 'Insert into Page',
icon: PageIcon(),
when: ctx => ctx.getSurfaceModelsByType(FrameBlockModel).length === 1,
run(ctx) {
const model = ctx.getCurrentModelByType(FrameBlockModel);
if (!model) return;
const rootModel = ctx.store.root;
if (!rootModel) return;
const { id: frameId, xywh } = model;
let lastNoteId = rootModel.children
.filter(
note =>
matchModels(note, [NoteBlockModel]) &&
note.props.displayMode !== NoteDisplayMode.EdgelessOnly
)
.pop()?.id;
if (!lastNoteId) {
const bounds = Bound.deserialize(xywh);
bounds.y += bounds.h;
bounds.h = DEFAULT_NOTE_HEIGHT;
lastNoteId = ctx.store.addBlock(
NoteBlockSchema.model.flavour,
{ xywh: bounds.serialize() },
rootModel.id
);
}
ctx.store.addBlock(
SurfaceRefBlockSchema.model.flavour,
{ reference: frameId, refFlavour: NoteBlockSchema.model.flavour },
lastNoteId
);
toast(ctx.host, 'Frame has been inserted into doc');
},
},
{
id: 'b.rename',
tooltip: 'Rename',
icon: EditIcon(),
when: ctx => ctx.getSurfaceModelsByType(FrameBlockModel).length === 1,
run(ctx) {
const model = ctx.getCurrentModelByType(FrameBlockModel);
if (!model) return;
const rootBlock = getRootBlock(ctx);
if (!rootBlock) return;
mountFrameTitleEditor(model, rootBlock);
},
},
{
id: 'b.ungroup',
tooltip: 'Ungroup',
icon: UngroupIcon(),
run(ctx) {
const models = ctx.getSurfaceModelsByType(FrameBlockModel);
if (!models.length) return;
const crud = ctx.std.get(EdgelessCRUDIdentifier);
const gfx = ctx.std.get(GfxControllerIdentifier);
ctx.store.captureSync();
const frameManager = ctx.std.get(EdgelessFrameManagerIdentifier);
for (const model of models) {
frameManager.removeAllChildrenFromFrame(model);
}
for (const model of models) {
crud.removeElement(model.id);
}
gfx.selection.clear();
},
},
{
id: 'c.color-picker',
content(ctx) {
const models = ctx.getSurfaceModelsByType(FrameBlockModel);
if (!models.length) return null;
const enableCustomColor = ctx.features.getFlag('enable_color_picker');
const theme = ctx.theme.edgeless$.value;
const field = 'background';
const firstModel = models[0];
const background =
getMostCommonResolvedValue(
models.map(model => model.props),
field,
background => resolveColor(background, theme)
) ?? DefaultTheme.transparent;
const onPick = (e: PickColorEvent) => {
if (e.type === 'pick') {
const color = e.detail.value;
for (const model of models) {
const props = packColor(field, color);
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, props);
}
return;
}
for (const model of models) {
model[e.type === 'start' ? 'stash' : 'pop'](field);
}
};
return html`
<edgeless-color-picker-button
class="background"
.label="${'Background'}"
.pick=${onPick}
.color=${background}
.theme=${theme}
.originalColor=${firstModel.props.background}
.enableCustomColor=${enableCustomColor}
>
</edgeless-color-picker-button>
`;
},
},
],
when: ctx => ctx.getSurfaceModelsByType(FrameBlockModel).length > 0,
} as const satisfies ToolbarModuleConfig;
const createFrameToolbarConfig = (flavour: string): ExtensionType => {
const name = flavour.split(':').pop();
return ToolbarModuleExtension({
id: BlockFlavourIdentifier(`affine:surface:${name}`),
config: builtinSurfaceToolbarConfig,
});
};
export const frameToolbarExtension = createFrameToolbarConfig(
FrameBlockSchema.model.flavour
);
@@ -1,4 +1,21 @@
export * from './frame-block.js';
export * from './frame-manager.js';
export * from './frame-spec.js';
export * from './tool.js';
import type { FrameTool } from './frame-tool';
import type { PresentTool, PresentToolOption } from './preset-tool';
export * from './frame-block';
export * from './frame-highlight-manager';
export * from './frame-manager';
export * from './frame-spec';
export * from './frame-tool';
export * from './frame-toolbar';
export * from './preset-tool';
declare module '@blocksuite/block-std/gfx' {
interface GfxToolsMap {
frameNavigator: PresentTool;
frame: FrameTool;
}
interface GfxToolsOption {
frameNavigator: PresentToolOption;
}
}
@@ -2,20 +2,10 @@ import { BaseTool } from '@blocksuite/block-std/gfx';
import type { NavigatorMode } from './frame-manager';
type PresentToolOption = {
export type PresentToolOption = {
mode?: NavigatorMode;
};
export class PresentTool extends BaseTool<PresentToolOption> {
static override toolName: string = 'frameNavigator';
}
declare module '@blocksuite/block-std/gfx' {
interface GfxToolsMap {
frameNavigator: PresentTool;
}
interface GfxToolsOption {
frameNavigator: PresentToolOption;
}
}