From 0a17920dc6e76d57c622c783f32cf52ecc4fcaf5 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 +++++++++ .../block-bookmark/src/bookmark-spec.ts | 19 +- .../affine/block-bookmark/src/toolbar.ts | 76 ++++++++ .../affine/block-embed/src/common/toolbar.ts | 159 +++++++++++++++++ .../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 | 15 ++ .../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 + blocksuite/affine/widget-toolbar/package.json | 2 + .../widget-toolbar/src/configs/formatting.ts | 83 +++++++++ .../widget-toolbar/src/configs/index.ts | 1 + blocksuite/affine/widget-toolbar/src/index.ts | 1 + .../affine/widget-toolbar/src/toolbar.ts | 167 +++++++++++++++++- .../affine/widget-toolbar/tsconfig.json | 8 +- .../root-block/edgeless/edgeless-root-spec.ts | 2 + .../src/root-block/page/page-root-spec.ts | 2 + .../block-suite-editor/specs/common.ts | 2 + tests/affine-local/e2e/toolbar.spec.ts | 8 + tools/utils/src/workspace.gen.ts | 2 + yarn.lock | 2 + 27 files changed, 835 insertions(+), 22 deletions(-) create mode 100644 blocksuite/affine/block-attachment/src/toolbar.ts create mode 100644 blocksuite/affine/block-bookmark/src/toolbar.ts create mode 100644 blocksuite/affine/block-embed/src/common/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 create mode 100644 blocksuite/affine/widget-toolbar/src/configs/formatting.ts create mode 100644 blocksuite/affine/widget-toolbar/src/configs/index.ts create mode 100644 tests/affine-local/e2e/toolbar.spec.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..ea3079f8ab568 --- /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: 'conversions', + 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-bookmark/src/bookmark-spec.ts b/blocksuite/affine/block-bookmark/src/bookmark-spec.ts index 9b2ef554a1358..a4744c7bf8243 100644 --- a/blocksuite/affine/block-bookmark/src/bookmark-spec.ts +++ b/blocksuite/affine/block-bookmark/src/bookmark-spec.ts @@ -1,4 +1,6 @@ +import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services'; import { + BlockFlavourIdentifier, BlockViewExtension, CommandExtension, FlavourExtension, @@ -6,18 +8,25 @@ import { import type { ExtensionType } from '@blocksuite/store'; import { literal } from 'lit/static-html.js'; -import { BookmarkBlockAdapterExtensions } from './adapters/extension.js'; -import { BookmarkBlockService } from './bookmark-service.js'; -import { commands } from './commands/index.js'; +import { BookmarkBlockAdapterExtensions } from './adapters/extension'; +import { BookmarkBlockService } from './bookmark-service'; +import { commands } from './commands/index'; +import { BuiltinToolbarConfig } from './toolbar'; + +const Flavour = 'affine:bookmark'; export const BookmarkBlockSpec: ExtensionType[] = [ - FlavourExtension('affine:bookmark'), + FlavourExtension(Flavour), BookmarkBlockService, CommandExtension(commands), - BlockViewExtension('affine:bookmark', model => { + BlockViewExtension(Flavour, model => { return model.parent?.flavour === 'affine:surface' ? literal`affine-edgeless-bookmark` : literal`affine-bookmark`; }), BookmarkBlockAdapterExtensions, + ToolbarModuleExtension({ + id: BlockFlavourIdentifier(Flavour), + config: BuiltinToolbarConfig, + }), ].flat(); diff --git a/blocksuite/affine/block-bookmark/src/toolbar.ts b/blocksuite/affine/block-bookmark/src/toolbar.ts new file mode 100644 index 0000000000000..f5001849211e8 --- /dev/null +++ b/blocksuite/affine/block-bookmark/src/toolbar.ts @@ -0,0 +1,76 @@ +import type { + ToolbarActionGroup, + ToolbarModuleConfig, +} from '@blocksuite/affine-shared/services'; + +export const BuiltinToolbarConfig = { + actions: [ + { + id: 'conversions', + actions: [ + { + id: 'inline-view', + label: 'Inline view', + run(_cx) {}, + }, + { + id: 'card-view', + label: 'Card view', + run(_cx) {}, + }, + ], + content(_cx) { + this.actions; + return null; + }, + } satisfies ToolbarActionGroup, + { + id: 'style', + tooltip: 'Card style', + 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-embed/src/common/toolbar.ts b/blocksuite/affine/block-embed/src/common/toolbar.ts new file mode 100644 index 0000000000000..299191106a0e7 --- /dev/null +++ b/blocksuite/affine/block-embed/src/common/toolbar.ts @@ -0,0 +1,159 @@ +import type { + ToolbarActionGroup, + ToolbarModuleConfig, +} from '@blocksuite/affine-shared/services'; + +// External embed blocks +export const BuiltinToolbarConfigForExternal = { + actions: [ + { + id: 'conversions', + actions: [ + { + id: 'inline-view', + label: 'Inline view', + run(_cx) {}, + }, + { + id: 'card-view', + label: 'Card view', + run(_cx) {}, + }, + { + id: 'embed-view', + label: 'Embed view', + run(_cx) {}, + when(_cx) { + return false; + }, + }, + ], + content(_cx) { + this.actions; + return null; + }, + } satisfies ToolbarActionGroup, + { + id: 'style', + tooltip: 'Card style', + 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; + +// Internal embed blocks +export const BuiltinToolbarConfigForInternal = { + actions: [ + { + id: 'conversions', + actions: [ + { + id: 'inline-view', + label: 'Inline view', + run(_cx) {}, + }, + { + id: 'card-view', + label: 'Card view', + run(_cx) {}, + }, + { + id: 'embed-view', + label: 'Embed view', + run(_cx) {}, + when(_cx) { + return false; + }, + }, + ], + content(_cx) { + this.actions; + return null; + }, + } satisfies ToolbarActionGroup, + { + id: 'style', + tooltip: 'Card style', + run(_cx) {}, + // linked doc: true, synced doc: false + when(_cx) { + return false; + }, + }, + { + id: 'caption', + tooltip: 'Caption', + run(_cx) {}, + }, + { + id: 'clipboard', + placement: 'more', + actions: [ + { + id: 'copy', + label: 'Copy', + run(_cx) {}, + }, + { + id: 'duplicate', + label: 'Duplicate', + 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..b2c52e9b9a980 --- /dev/null +++ b/blocksuite/affine/shared/src/services/toolbar-service/context.ts @@ -0,0 +1,15 @@ +import type { BlockStdScope } from '@blocksuite/block-std'; + +abstract class ToolbarContextBase { + constructor(readonly std: BlockStdScope) {} + + get command() { + return this.std.command; + } + + 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/package.json b/blocksuite/affine/widget-toolbar/package.json index 78f751a617508..7c9e88ce7f8d1 100644 --- a/blocksuite/affine/widget-toolbar/package.json +++ b/blocksuite/affine/widget-toolbar/package.json @@ -13,9 +13,11 @@ "author": "toeverything", "license": "MIT", "dependencies": { + "@blocksuite/affine-components": "workspace:*", "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/block-std": "workspace:*", + "@blocksuite/data-view": "workspace:*", "@blocksuite/global": "workspace:*", "@preact/signals-core": "^1.8.0", "@toeverything/theme": "^1.1.3", diff --git a/blocksuite/affine/widget-toolbar/src/configs/formatting.ts b/blocksuite/affine/widget-toolbar/src/configs/formatting.ts new file mode 100644 index 0000000000000..68af83a0489a9 --- /dev/null +++ b/blocksuite/affine/widget-toolbar/src/configs/formatting.ts @@ -0,0 +1,83 @@ +import { + textConversionConfigs, + textFormatConfigs, +} from '@blocksuite/affine-components/rich-text'; +import type { + ToolbarActionGenerator, + ToolbarActionGroup, + ToolbarModuleConfig, +} from '@blocksuite/affine-shared/services'; + +const ParagraphActionGroup = { + id: 'paragraph', + generate(_cx) { + const defaultValue = textConversionConfigs[0]; + return { + id: defaultValue.name, + // icon: defaultValue.icon, + content(_cx) { + return defaultValue.icon; + }, + run(_cx) {}, + }; + }, +} as const satisfies ToolbarActionGenerator; + +const InlineTextActionGroup = { + id: 'inline-text', + actions: textFormatConfigs.map(({ id, name, action }) => { + return { + id, + tooltip: name, + run: cx => action(cx.std.host), + // todo active state + }; + }), +} as const satisfies ToolbarActionGroup; + +const HighlightActionGroup = { + id: 'highlight', + actions: [ + { + id: 'bold', + run(cx) { + cx.command.chain(); + }, + }, + ], +} as const satisfies ToolbarActionGroup; + +export const BuiltinFormattingConfig = { + actions: [ + ParagraphActionGroup, + InlineTextActionGroup, + HighlightActionGroup, + { + id: 'clipboard', + placement: 'more', + actions: [ + { + id: 'copy', + label: 'Copy', + run(_cx) {}, + }, + { + id: 'duplicate', + label: 'Duplicate', + run(_cx) {}, + }, + ], + }, + { + id: 'delete', + placement: 'more', + actions: [ + { + id: 'delete', + label: 'Delete', + run(_cx) {}, + }, + ], + }, + ], +} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/widget-toolbar/src/configs/index.ts b/blocksuite/affine/widget-toolbar/src/configs/index.ts new file mode 100644 index 0000000000000..bb9948eed717b --- /dev/null +++ b/blocksuite/affine/widget-toolbar/src/configs/index.ts @@ -0,0 +1 @@ +export * from './formatting'; diff --git a/blocksuite/affine/widget-toolbar/src/index.ts b/blocksuite/affine/widget-toolbar/src/index.ts index 316a52a324ed7..7cbf8890b3ce0 100644 --- a/blocksuite/affine/widget-toolbar/src/index.ts +++ b/blocksuite/affine/widget-toolbar/src/index.ts @@ -1 +1,2 @@ +export * from './configs'; export * from './toolbar'; diff --git a/blocksuite/affine/widget-toolbar/src/toolbar.ts b/blocksuite/affine/widget-toolbar/src/toolbar.ts index b6e8472d11df2..9383e58a2f4c3 100644 --- a/blocksuite/affine/widget-toolbar/src/toolbar.ts +++ b/blocksuite/affine/widget-toolbar/src/toolbar.ts @@ -1,5 +1,168 @@ -import { WidgetComponent } from '@blocksuite/block-std'; +import { + ToolbarRegistryIdentifier, + ToolbarRegistryScope, +} from '@blocksuite/affine-shared/services'; +import { + BlockSelection, + SurfaceSelection, + TextSelection, + WidgetComponent, +} from '@blocksuite/block-std'; +import { DatabaseSelection } from '@blocksuite/data-view'; +import { signal } from '@preact/signals-core'; + +enum Flag { + Surface = 0b1, + Block = 0b10, + Text = 0b100, + Native = 0b1000, + // hovering something, e.g. inline links + Hovering = 0b10000, + // dragging something, e.g. drag handle, drag resources from outside + Dragging = 0b100000, +} export const AFFINE_TOOLBAR_WIDGET = 'affine-toolbar-widget'; -export class AffineToolbarWidget extends WidgetComponent {} +export class AffineToolbarWidget extends WidgetComponent { + flags$ = signal(0b000000); + + toggleWith(flag: Flag, activated: boolean) { + if (activated) { + this.flags$.value |= flag; + return; + } + this.flags$.value &= ~flag; + } + + checkWith(flag: Flag) { + return (this.flags$.peek() & flag) === flag; + } + + get toolbarRegistry() { + const { container, provider } = this.std; + return container + .provider(ToolbarRegistryScope, provider) + .get(ToolbarRegistryIdentifier); + } + + override connectedCallback() { + super.connectedCallback(); + + const std = this.std; + + // Formatting + // Selects text in note. + this.disposables.add( + std.selection.find$(TextSelection).subscribe(result => { + const activated = Boolean( + result && result.from.length + (result.to?.length ?? 0) + ); + this.toggleWith(Flag.Text, activated); + }) + ); + + // Formatting + // Selects `native` text in database's cell. + this.disposables.addFromEvent(document, 'selectionchange', () => { + if (!this.host.event.active) return; + + const result = std.selection.find(DatabaseSelection); + const viewSelection = result?.viewSelection; + + let activated = Boolean( + viewSelection && + ((viewSelection.selectionType === 'area' && + viewSelection.isEditing) || + (viewSelection.selectionType === 'cell' && viewSelection.isEditing)) + ); + + if (activated) { + const selection = window.getSelection(); + const range = selection?.rangeCount && selection.getRangeAt(0); + activated = Boolean(range && !range.collapsed); + } + + this.toggleWith(Flag.Native, activated); + }); + + // Selects blocks in note. + this.disposables.add( + std.selection.filter$(BlockSelection).subscribe(result => { + const activated = Boolean(result.length); + this.toggleWith(Flag.Block, activated); + }) + ); + + // Selects elements in edgeless. + // Triggered only when not in editing state. + this.disposables.add( + std.selection.filter$(SurfaceSelection).subscribe(result => { + const activated = + Boolean(result.length) && !result.some(e => e.editing); + this.toggleWith(Flag.Surface, activated); + }) + ); + + const dragStart = () => this.toggleWith(Flag.Dragging, true); + const dragEnd = () => this.toggleWith(Flag.Dragging, false); + const dragOptions = { global: true }; + this.handleEvent('dragStart', dragStart, dragOptions); + this.handleEvent('dragEnd', dragEnd, dragOptions); + this.handleEvent( + 'nativeDragOver', + () => !this.checkWith(Flag.Dragging) && dragStart(), + dragOptions + ); + this.handleEvent('nativeDrop', dragEnd, dragOptions); + + this.disposables.add( + this.flags$.subscribe(value => { + // Hides toolbar + if (value === 0) { + console.log('hide toolbar'); + return; + } + + // Hides toolbar + if (this.checkWith(Flag.Dragging)) { + console.log('dragging'); + return; + } + + // Shows toolbar of inline links + if (this.checkWith(Flag.Hovering)) { + console.log('hovering'); + return; + } + + // Shows formatting toolbar in database. + if (this.checkWith(Flag.Native)) { + console.log('show formatting toolbar in database'); + return; + } + + // Shows formatting toolbar in note. + if (this.checkWith(Flag.Text)) { + console.log('show formatting toolbar in note'); + return; + } + + // Shows normal toolbar in note + if (this.checkWith(Flag.Block)) { + console.log('show normal toolbar in note'); + return; + } + + // Shows toolbar in edgeles + console.log('show toolbar in edgeless'); + }) + ); + + this.disposables.add( + std.selection.slots.changed.on(() => { + console.log('selection changed'); + }) + ); + } +} diff --git a/blocksuite/affine/widget-toolbar/tsconfig.json b/blocksuite/affine/widget-toolbar/tsconfig.json index 50310f2829f8a..bf7d791b1e5cb 100644 --- a/blocksuite/affine/widget-toolbar/tsconfig.json +++ b/blocksuite/affine/widget-toolbar/tsconfig.json @@ -1,12 +1,14 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "rootDir": "./src/", - "outDir": "./dist/", - "noEmit": false + "rootDir": "./src", + "outDir": "./dist", + "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" }, "include": ["./src"], "references": [ + { "path": "../block-data-view" }, + { "path": "../components" }, { "path": "../model" }, { "path": "../shared" }, { "path": "../../framework/block-std" }, 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[] = [ diff --git a/tests/affine-local/e2e/toolbar.spec.ts b/tests/affine-local/e2e/toolbar.spec.ts new file mode 100644 index 0000000000000..aefad65f7d15b --- /dev/null +++ b/tests/affine-local/e2e/toolbar.spec.ts @@ -0,0 +1,8 @@ +import { test } from '@affine-test/kit/playwright'; +import { openHomePage } from '@affine-test/kit/utils/load-page'; +import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic'; + +test('toolbar', async ({ page }) => { + await openHomePage(page); + await waitForEditorLoad(page); +}); diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index 10cfe613789f0..05cc8858e21f2 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -334,6 +334,8 @@ export const PackageList = [ location: 'blocksuite/affine/widget-toolbar', name: '@blocksuite/affine-widget-toolbar', workspaceDependencies: [ + 'blocksuite/affine/block-data-view', + 'blocksuite/affine/components', 'blocksuite/affine/model', 'blocksuite/affine/shared', 'blocksuite/framework/block-std', diff --git a/yarn.lock b/yarn.lock index 027397eacd3bd..107bf3936fcb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3818,6 +3818,8 @@ __metadata: version: 0.0.0-use.local resolution: "@blocksuite/affine-widget-toolbar@workspace:blocksuite/affine/widget-toolbar" dependencies: + "@blocksuite/affine-block-data-view": "workspace:*" + "@blocksuite/affine-components": "workspace:*" "@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/block-std": "workspace:*"