mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-24 04:27:27 +08:00
refactor(editor): move gfx frame title to widget (#11021)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-11
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user