From f87cc0f5997eac815a9f9c2f1733016b74453bd5 Mon Sep 17 00:00:00 2001 From: fundon Date: Wed, 19 Mar 2025 12:34:18 +0000 Subject: [PATCH] refactor(editor): edgeless frame toolbar config extension (#10769) --- .../blocks/block-frame/src/frame-spec.ts | 5 +- .../src/edgeless/configs/toolbar/frame.ts | 197 +++++++++++++++++- .../src/edgeless/configs/toolbar/index.ts | 10 +- 3 files changed, 200 insertions(+), 12 deletions(-) diff --git a/blocksuite/affine/blocks/block-frame/src/frame-spec.ts b/blocksuite/affine/blocks/block-frame/src/frame-spec.ts index 374d2dd6a1..0e21a6905a 100644 --- a/blocksuite/affine/blocks/block-frame/src/frame-spec.ts +++ b/blocksuite/affine/blocks/block-frame/src/frame-spec.ts @@ -1,11 +1,14 @@ +import { FrameBlockSchema } from '@blocksuite/affine-model'; import { BlockViewExtension } from '@blocksuite/block-std'; import type { ExtensionType } from '@blocksuite/store'; import { literal } from 'lit/static-html.js'; import { EdgelessFrameManager, FrameOverlay } from './frame-manager'; +const flavour = FrameBlockSchema.model.flavour; + export const FrameBlockSpec: ExtensionType[] = [ - BlockViewExtension('affine:frame', literal`affine-frame`), + BlockViewExtension(flavour, literal`affine-frame`), FrameOverlay, EdgelessFrameManager, ]; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/frame.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/frame.ts index fccce2bd52..89a8a2b4d4 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/frame.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/frame.ts @@ -1,11 +1,198 @@ -import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; +import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame'; +import { EdgelessCRUDExtension } 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, + NoteBlockModel, + NoteBlockSchema, + NoteDisplayMode, + resolveColor, + SurfaceRefBlockSchema, +} from '@blocksuite/affine-model'; +import { + FeatureFlagService, + type ToolbarModuleConfig, + ToolbarModuleExtension, +} from '@blocksuite/affine-shared/services'; +import { + getMostCommonResolvedValue, + matchModels, +} from '@blocksuite/affine-shared/utils'; +import { BlockFlavourIdentifier } from '@blocksuite/block-std'; +import { Bound } from '@blocksuite/global/gfx'; +import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit'; +import type { ExtensionType } from '@blocksuite/store'; +import { html } from 'lit'; -export const builtinFrameToolbarConfig = { +import { EdgelessRootBlockComponent } from '../..'; +import { mountFrameTitleEditor } from '../../utils/text'; + +const builtinSurfaceToolbarConfig = { actions: [ { - id: 'a.test', - label: 'Frame', - run() {}, + id: 'a.insert-into-page', + label: 'Insert into Page', + 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 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; + } + + mountFrameTitleEditor(model, edgeless); + }, + }, + { + id: 'b.ungroup', + tooltip: 'Ungroup', + icon: UngroupIcon(), + run(ctx) { + const models = ctx.getSurfaceModelsByType(FrameBlockModel); + 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; + } + + ctx.store.captureSync(); + + const frameManager = ctx.std.get(EdgelessFrameManagerIdentifier); + + for (const model of models) { + frameManager.removeAllChildrenFromFrame(model); + } + + for (const model of models) { + edgeless.service.removeElement(model); + } + + edgeless.service.selection.clear(); + }, + }, + { + id: 'c.color-picker', + content(ctx) { + const models = ctx.getSurfaceModelsByType(FrameBlockModel); + if (!models.length) return null; + + const enableCustomColor = ctx.std + .get(FeatureFlagService) + .getFlag('enable_color_picker'); + const theme = ctx.themeProvider.edgelessTheme; + + const firstModel = models[0]; + const background = + getMostCommonResolvedValue( + models.map(model => model.props), + 'background', + background => resolveColor(background, theme) + ) ?? DefaultTheme.transparent; + + const onPick = (e: PickColorEvent) => { + const field = 'background'; + + if (e.type === 'pick') { + const color = e.detail.value; + for (const model of models) { + const props = packColor(field, color); + ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, props); + } + return; + } + + for (const model of models) { + model[e.type === 'start' ? 'stash' : 'pop'](field); + } + }; + + return html` + + + `; + }, }, ], + + when: ctx => ctx.getSurfaceModelsByType(FrameBlockModel).length > 0, } as const satisfies ToolbarModuleConfig; + +export const createFrameToolbarConfig = (flavour: string): ExtensionType => { + const name = flavour.split(':').pop(); + + return ToolbarModuleExtension({ + id: BlockFlavourIdentifier(`affine:surface:${name}`), + config: builtinSurfaceToolbarConfig, + }); +}; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/index.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/index.ts index 2b7534a226..34f42f4e05 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/index.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/index.ts @@ -1,10 +1,11 @@ +import { FrameBlockSchema } from '@blocksuite/affine-model'; import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services'; import { BlockFlavourIdentifier } from '@blocksuite/block-std'; import type { ExtensionType } from '@blocksuite/store'; import { builtinBrushToolbarConfig } from './brush'; import { builtinConnectorToolbarConfig } from './connector'; -import { builtinFrameToolbarConfig } from './frame'; +import { createFrameToolbarConfig } from './frame'; import { builtinGroupToolbarConfig } from './group'; import { builtinMindmapToolbarConfig } from './mindmap'; import { builtinMiscToolbarConfig } from './misc'; @@ -12,6 +13,8 @@ import { builtinShapeToolbarConfig } from './shape'; import { builtinTextToolbarConfig } from './text'; export const EdgelessElementToolbarExtension: ExtensionType[] = [ + createFrameToolbarConfig(FrameBlockSchema.model.flavour), + ToolbarModuleExtension({ id: BlockFlavourIdentifier('affine:surface:brush'), config: builtinBrushToolbarConfig, @@ -22,11 +25,6 @@ export const EdgelessElementToolbarExtension: ExtensionType[] = [ config: builtinConnectorToolbarConfig, }), - ToolbarModuleExtension({ - id: BlockFlavourIdentifier('affine:surface:frame'), - config: builtinFrameToolbarConfig, - }), - ToolbarModuleExtension({ id: BlockFlavourIdentifier('affine:surface:group'), config: builtinGroupToolbarConfig,