From c172c76e5c630c498cf0266ddb862154d3b2db24 Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Tue, 7 Jan 2025 19:52:42 +0800 Subject: [PATCH] feat(editor): add toolbar registry extension --- .../block-attachment/src/attachment-spec.ts | 24 ++++-- .../affine/block-attachment/src/toolbar.ts | 81 +++++++++++++++++++ .../affine/block-image/src/image-spec.ts | 21 +++-- blocksuite/affine/block-image/src/toolbar.ts | 54 +++++++++++++ .../affine/shared/src/services/index.ts | 1 + .../src/services/toolbar-service/action.ts | 41 ++++++++++ .../src/services/toolbar-service/config.ts | 5 ++ .../src/services/toolbar-service/context.ts | 11 +++ .../src/services/toolbar-service/index.ts | 6 ++ .../src/services/toolbar-service/module.ts | 9 +++ .../src/services/toolbar-service/registry.ts | 59 ++++++++++++++ .../src/services/toolbar-service/utils.ts | 7 ++ .../affine/widget-toolbar/src/toolbar.ts | 15 +++- .../root-block/edgeless/edgeless-root-spec.ts | 2 + .../src/root-block/page/page-root-spec.ts | 2 + .../block-suite-editor/specs/common.ts | 2 + 16 files changed, 327 insertions(+), 13 deletions(-) create mode 100644 blocksuite/affine/block-attachment/src/toolbar.ts create mode 100644 blocksuite/affine/block-image/src/toolbar.ts create mode 100644 blocksuite/affine/shared/src/services/toolbar-service/action.ts create mode 100644 blocksuite/affine/shared/src/services/toolbar-service/config.ts create mode 100644 blocksuite/affine/shared/src/services/toolbar-service/context.ts create mode 100644 blocksuite/affine/shared/src/services/toolbar-service/index.ts create mode 100644 blocksuite/affine/shared/src/services/toolbar-service/module.ts create mode 100644 blocksuite/affine/shared/src/services/toolbar-service/registry.ts create mode 100644 blocksuite/affine/shared/src/services/toolbar-service/utils.ts diff --git a/blocksuite/affine/block-attachment/src/attachment-spec.ts b/blocksuite/affine/block-attachment/src/attachment-spec.ts index fed3db5d084eb..62d70ca9ebb3a 100644 --- a/blocksuite/affine/block-attachment/src/attachment-spec.ts +++ b/blocksuite/affine/block-attachment/src/attachment-spec.ts @@ -1,21 +1,29 @@ -import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std'; +import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services'; +import { + BlockFlavourIdentifier, + BlockViewExtension, + FlavourExtension, +} from '@blocksuite/block-std'; import type { ExtensionType } from '@blocksuite/store'; import { literal } from 'lit/static-html.js'; -import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html.js'; +import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html'; import { AttachmentBlockService, AttachmentDropOption, -} from './attachment-service.js'; +} from './attachment-service'; import { AttachmentEmbedConfigExtension, AttachmentEmbedService, -} from './embed.js'; +} from './embed'; +import { BuiltinToolbarConfig } from './toolbar'; + +const Flavour = 'affine:attachment'; export const AttachmentBlockSpec: ExtensionType[] = [ - FlavourExtension('affine:attachment'), + FlavourExtension(Flavour), AttachmentBlockService, - BlockViewExtension('affine:attachment', model => { + BlockViewExtension(Flavour, model => { return model.parent?.flavour === 'affine:surface' ? literal`affine-edgeless-attachment` : literal`affine-attachment`; @@ -24,4 +32,8 @@ export const AttachmentBlockSpec: ExtensionType[] = [ AttachmentEmbedConfigExtension(), AttachmentEmbedService, AttachmentBlockNotionHtmlAdapterExtension, + ToolbarModuleExtension({ + id: BlockFlavourIdentifier(Flavour), + config: BuiltinToolbarConfig, + }), ]; diff --git a/blocksuite/affine/block-attachment/src/toolbar.ts b/blocksuite/affine/block-attachment/src/toolbar.ts new file mode 100644 index 0000000000000..7571cb24e5504 --- /dev/null +++ b/blocksuite/affine/block-attachment/src/toolbar.ts @@ -0,0 +1,81 @@ +import type { + ToolbarActionGroup, + ToolbarModuleConfig, +} from '@blocksuite/affine-shared/services'; + +export const BuiltinToolbarConfig = { + actions: [ + { + id: 'rename', + tooltip: 'Rename', + run(_cx) {}, + }, + { + id: 'switch-view', + actions: [ + { + id: 'card-view', + label: 'Card view', + run(_cx) {}, + }, + { + id: 'embed-view', + label: 'Embed view', + run(_cx) {}, + }, + ], + content(_cx) { + this.actions; + return null; + }, + } satisfies ToolbarActionGroup, + { + id: 'download', + tooltip: 'Download', + run(_cx) {}, + }, + { + id: 'caption', + tooltip: 'Caption', + run(_cx) {}, + }, + { + id: 'clipboard', + placement: 'more', + actions: [ + { + id: 'copy', + label: 'Copy', + run(_cx) {}, + }, + { + id: 'duplicate', + label: 'Duplicate', + run(_cx) {}, + }, + ], + }, + { + id: 'refresh', + placement: 'more', + actions: [ + { + id: 'reload', + label: 'Reload', + run(_cx) {}, + }, + ], + }, + { + id: 'delete', + placement: 'more', + actions: [ + { + id: 'delete', + label: 'Delete', + run(_cx) {}, + }, + ], + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/block-image/src/image-spec.ts b/blocksuite/affine/block-image/src/image-spec.ts index a1fbe453bf62e..b566d799eeed5 100644 --- a/blocksuite/affine/block-image/src/image-spec.ts +++ b/blocksuite/affine/block-image/src/image-spec.ts @@ -1,4 +1,6 @@ +import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services'; import { + BlockFlavourIdentifier, BlockViewExtension, CommandExtension, FlavourExtension, @@ -7,15 +9,18 @@ import { import type { ExtensionType } from '@blocksuite/store'; import { literal } from 'lit/static-html.js'; -import { ImageBlockAdapterExtensions } from './adapters/extension.js'; -import { commands } from './commands/index.js'; -import { ImageBlockService, ImageDropOption } from './image-service.js'; +import { ImageBlockAdapterExtensions } from './adapters/extension'; +import { commands } from './commands/index'; +import { ImageBlockService, ImageDropOption } from './image-service'; +import { BuiltinToolbarConfig } from './toolbar'; + +const Flavour = 'affine:image'; export const ImageBlockSpec: ExtensionType[] = [ - FlavourExtension('affine:image'), + FlavourExtension(Flavour), ImageBlockService, CommandExtension(commands), - BlockViewExtension('affine:image', model => { + BlockViewExtension(Flavour, model => { const parent = model.doc.getParent(model.id); if (parent?.flavour === 'affine:surface') { @@ -24,9 +29,13 @@ export const ImageBlockSpec: ExtensionType[] = [ return literal`affine-image`; }), - WidgetViewMapExtension('affine:image', { + WidgetViewMapExtension(Flavour, { imageToolbar: literal`affine-image-toolbar-widget`, }), ImageDropOption, ImageBlockAdapterExtensions, + ToolbarModuleExtension({ + id: BlockFlavourIdentifier(Flavour), + config: BuiltinToolbarConfig, + }), ].flat(); diff --git a/blocksuite/affine/block-image/src/toolbar.ts b/blocksuite/affine/block-image/src/toolbar.ts new file mode 100644 index 0000000000000..4c734671f6303 --- /dev/null +++ b/blocksuite/affine/block-image/src/toolbar.ts @@ -0,0 +1,54 @@ +import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; + +export const BuiltinToolbarConfig = { + actions: [ + { + id: 'download', + tooltip: 'Download', + run(_cx) {}, + }, + { + id: 'caption', + tooltip: 'Caption', + run(_cx) {}, + }, + { + id: 'clipboard', + placement: 'more', + actions: [ + { + id: 'copy', + label: 'Copy', + run(_cx) {}, + }, + { + id: 'duplicate', + label: 'Duplicate', + run(_cx) {}, + }, + ], + }, + { + id: 'conversions', + placement: 'more', + actions: [ + { + id: 'turn-into-card-view', + label: 'Turn into card view', + run(_cx) {}, + }, + ], + }, + { + id: 'delete', + placement: 'more', + actions: [ + { + id: 'delete', + label: 'Delete', + run(_cx) {}, + }, + ], + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/shared/src/services/index.ts b/blocksuite/affine/shared/src/services/index.ts index 340679bad79cf..ed89fe0c683c1 100644 --- a/blocksuite/affine/shared/src/services/index.ts +++ b/blocksuite/affine/shared/src/services/index.ts @@ -15,3 +15,4 @@ export * from './parse-url-service'; export * from './quick-search-service'; export * from './telemetry-service'; export * from './theme-service'; +export * from './toolbar-service'; diff --git a/blocksuite/affine/shared/src/services/toolbar-service/action.ts b/blocksuite/affine/shared/src/services/toolbar-service/action.ts new file mode 100644 index 0000000000000..1217925abcfaf --- /dev/null +++ b/blocksuite/affine/shared/src/services/toolbar-service/action.ts @@ -0,0 +1,41 @@ +import type { TemplateResult } from 'lit'; + +import type { ToolbarContext } from './context'; + +type ActionBase = { + id: string; + score?: number; + when?: (cx: ToolbarContext) => boolean; + placement?: 'start' | 'end' | 'more'; +}; + +export type ToolbarAction = ActionBase & { + label?: string; + icon?: TemplateResult; + tooltip?: string; + content?: (cx: ToolbarContext) => TemplateResult | null; + run: (cx: ToolbarContext) => void; +}; + +// Generates an action at runtime +export type ToolbarActionGenerator = ActionBase & { + generate: (cx: ToolbarContext) => ToolbarAction; +}; + +export type ToolbarActionGroup = ActionBase & { + actions: ToolbarAction[]; + content?: (cx: ToolbarContext) => TemplateResult | null; +}; + +// Generates an action group at runtime +export type ToolbarActionGroupGenerator = ActionBase & { + generate: (cx: ToolbarContext) => ToolbarActionGroup; +}; + +export type ToolbarActions< + T extends ActionBase = + | ToolbarAction + | ToolbarActionGenerator + | ToolbarActionGroup + | ToolbarActionGroupGenerator, +> = T[]; diff --git a/blocksuite/affine/shared/src/services/toolbar-service/config.ts b/blocksuite/affine/shared/src/services/toolbar-service/config.ts new file mode 100644 index 0000000000000..cac3a4ca8f04f --- /dev/null +++ b/blocksuite/affine/shared/src/services/toolbar-service/config.ts @@ -0,0 +1,5 @@ +import type { ToolbarActions } from './action'; + +export type ToolbarModuleConfig = { + actions: ToolbarActions; +}; diff --git a/blocksuite/affine/shared/src/services/toolbar-service/context.ts b/blocksuite/affine/shared/src/services/toolbar-service/context.ts new file mode 100644 index 0000000000000..8a0768adde86e --- /dev/null +++ b/blocksuite/affine/shared/src/services/toolbar-service/context.ts @@ -0,0 +1,11 @@ +import type { BlockStdScope } from '@blocksuite/block-std'; + +abstract class ToolbarContextBase { + constructor(readonly std: BlockStdScope) {} + + get isReadonly() { + return this.std.store.readonly; + } +} + +export class ToolbarContext extends ToolbarContextBase {} diff --git a/blocksuite/affine/shared/src/services/toolbar-service/index.ts b/blocksuite/affine/shared/src/services/toolbar-service/index.ts new file mode 100644 index 0000000000000..da2e917b22ab7 --- /dev/null +++ b/blocksuite/affine/shared/src/services/toolbar-service/index.ts @@ -0,0 +1,6 @@ +export * from './action'; +export * from './config'; +export * from './context'; +export * from './module'; +export * from './registry'; +export * from './utils'; diff --git a/blocksuite/affine/shared/src/services/toolbar-service/module.ts b/blocksuite/affine/shared/src/services/toolbar-service/module.ts new file mode 100644 index 0000000000000..3892fc3f98308 --- /dev/null +++ b/blocksuite/affine/shared/src/services/toolbar-service/module.ts @@ -0,0 +1,9 @@ +import type { BlockFlavourIdentifier } from '@blocksuite/block-std'; + +import type { ToolbarModuleConfig } from './config'; + +export type ToolbarModule = { + readonly id: ReturnType; + + readonly config: ToolbarModuleConfig; +}; diff --git a/blocksuite/affine/shared/src/services/toolbar-service/registry.ts b/blocksuite/affine/shared/src/services/toolbar-service/registry.ts new file mode 100644 index 0000000000000..62599ea62a118 --- /dev/null +++ b/blocksuite/affine/shared/src/services/toolbar-service/registry.ts @@ -0,0 +1,59 @@ +import { + type BlockStdScope, + LifeCycleWatcher, + StdIdentifier, +} from '@blocksuite/block-std'; +import { + type Container, + createIdentifier, + createScope, +} from '@blocksuite/global/di'; +import type { ExtensionType } from '@blocksuite/store'; + +import type { ToolbarModule } from './module'; + +export const ToolbarModuleIdentifier = createIdentifier( + 'AffineToolbarModuleIdentifier' +); + +export const ToolbarModulesIdentifier = createIdentifier< + Map +>('AffineToolbarModulesIdentifier'); + +export const ToolbarRegistryScope = createScope('AffineToolbarRegistryScope'); + +export const ToolbarRegistryIdentifier = + createIdentifier('AffineToolbarRegistryIdentifier'); + +export function ToolbarModuleExtension(module: ToolbarModule): ExtensionType { + return { + setup: di => { + di.scope(ToolbarRegistryScope).addImpl( + ToolbarModuleIdentifier(module.id.variant), + module + ); + }, + }; +} + +export class ToolbarRegistryExtension extends LifeCycleWatcher { + constructor( + std: BlockStdScope, + readonly modules: Map + ) { + super(std); + } + + static override readonly key = 'toolbar-registry'; + + static override setup(di: Container) { + di.scope(ToolbarRegistryScope) + .addImpl(ToolbarModulesIdentifier, provider => + provider.getAll(ToolbarModuleIdentifier) + ) + .addImpl(ToolbarRegistryIdentifier, this, [ + StdIdentifier, + ToolbarModulesIdentifier, + ]); + } +} diff --git a/blocksuite/affine/shared/src/services/toolbar-service/utils.ts b/blocksuite/affine/shared/src/services/toolbar-service/utils.ts new file mode 100644 index 0000000000000..cd6ec45c1ef28 --- /dev/null +++ b/blocksuite/affine/shared/src/services/toolbar-service/utils.ts @@ -0,0 +1,7 @@ +export function generateActionIdWith( + flavour: string, + name: string, + prefix = 'com.affine.toolbar.internal' +) { + return `${prefix}.${flavour}.${name}`; +} diff --git a/blocksuite/affine/widget-toolbar/src/toolbar.ts b/blocksuite/affine/widget-toolbar/src/toolbar.ts index b6e8472d11df2..998fcaf1e181c 100644 --- a/blocksuite/affine/widget-toolbar/src/toolbar.ts +++ b/blocksuite/affine/widget-toolbar/src/toolbar.ts @@ -1,5 +1,18 @@ +import { + ToolbarRegistryIdentifier, + ToolbarRegistryScope, +} from '@blocksuite/affine-shared/services'; import { WidgetComponent } from '@blocksuite/block-std'; export const AFFINE_TOOLBAR_WIDGET = 'affine-toolbar-widget'; -export class AffineToolbarWidget extends WidgetComponent {} +export class AffineToolbarWidget extends WidgetComponent { + override connectedCallback() { + super.connectedCallback(); + + const toolbarRegistry = this.std.container + .provider(ToolbarRegistryScope, this.std.provider) + .get(ToolbarRegistryIdentifier); + console.log(toolbarRegistry); + } +} diff --git a/blocksuite/blocks/src/root-block/edgeless/edgeless-root-spec.ts b/blocksuite/blocks/src/root-block/edgeless/edgeless-root-spec.ts index 461a54eaf0a89..984fa03618fd0 100644 --- a/blocksuite/blocks/src/root-block/edgeless/edgeless-root-spec.ts +++ b/blocksuite/blocks/src/root-block/edgeless/edgeless-root-spec.ts @@ -5,6 +5,7 @@ import { EmbedOptionService, PageViewportServiceExtension, ThemeService, + ToolbarRegistryExtension, } from '@blocksuite/affine-shared/services'; import { AFFINE_DRAG_HANDLE_WIDGET } from '@blocksuite/affine-widget-drag-handle'; import { AFFINE_FRAME_TITLE_WIDGET } from '@blocksuite/affine-widget-frame-title'; @@ -102,6 +103,7 @@ const EdgelessCommonExtension: ExtensionType[] = [ PageViewportServiceExtension, RootBlockAdapterExtensions, FileDropExtension, + ToolbarRegistryExtension, ].flat(); export const EdgelessRootBlockSpec: ExtensionType[] = [ diff --git a/blocksuite/blocks/src/root-block/page/page-root-spec.ts b/blocksuite/blocks/src/root-block/page/page-root-spec.ts index c7ae87d84ed08..95f6039b73afe 100644 --- a/blocksuite/blocks/src/root-block/page/page-root-spec.ts +++ b/blocksuite/blocks/src/root-block/page/page-root-spec.ts @@ -5,6 +5,7 @@ import { EmbedOptionService, PageViewportServiceExtension, ThemeService, + ToolbarRegistryExtension, } from '@blocksuite/affine-shared/services'; import { AFFINE_DRAG_HANDLE_WIDGET } from '@blocksuite/affine-widget-drag-handle'; import { AFFINE_DOC_REMOTE_SELECTION_WIDGET } from '@blocksuite/affine-widget-remote-selection'; @@ -83,6 +84,7 @@ export const PageRootBlockSpec: ExtensionType[] = [ DNDAPIExtension, RootBlockAdapterExtensions, FileDropExtension, + ToolbarRegistryExtension, ].flat(); export const PreviewPageRootBlockSpec: ExtensionType[] = [ diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/common.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/common.ts index b3a7af2c30816..27bb08832790a 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/common.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/common.ts @@ -22,6 +22,7 @@ import { ParagraphBlockSpec, RefNodeSlotsExtension, RichTextExtensions, + ToolbarRegistryExtension, } from '@blocksuite/affine/blocks'; import type { ExtensionType } from '@blocksuite/affine/store'; @@ -40,6 +41,7 @@ const CommonBlockSpecs: ExtensionType[] = [ AdapterFactoryExtensions, FontLoaderService, DefaultOpenDocExtension, + ToolbarRegistryExtension, ].flat(); export const DefaultBlockSpecs: ExtensionType[] = [