Skip to content

Commit 69a85b7

Browse files
committed
feat(editor): add toolbar registry extension
1 parent 46d0ebf commit 69a85b7

File tree

14 files changed

+323
-13
lines changed

14 files changed

+323
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1-
import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std';
1+
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
2+
import {
3+
BlockFlavourIdentifier,
4+
BlockViewExtension,
5+
FlavourExtension,
6+
} from '@blocksuite/block-std';
27
import type { ExtensionType } from '@blocksuite/store';
38
import { literal } from 'lit/static-html.js';
49

5-
import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html.js';
10+
import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html';
611
import {
712
AttachmentBlockService,
813
AttachmentDropOption,
9-
} from './attachment-service.js';
14+
} from './attachment-service';
1015
import {
1116
AttachmentEmbedConfigExtension,
1217
AttachmentEmbedService,
13-
} from './embed.js';
18+
} from './embed';
19+
import { BuiltinToolbarConfig } from './toolbar';
20+
21+
const Flavour = 'affine:attachment';
1422

1523
export const AttachmentBlockSpec: ExtensionType[] = [
16-
FlavourExtension('affine:attachment'),
24+
FlavourExtension(Flavour),
1725
AttachmentBlockService,
18-
BlockViewExtension('affine:attachment', model => {
26+
BlockViewExtension(Flavour, model => {
1927
return model.parent?.flavour === 'affine:surface'
2028
? literal`affine-edgeless-attachment`
2129
: literal`affine-attachment`;
@@ -24,4 +32,8 @@ export const AttachmentBlockSpec: ExtensionType[] = [
2432
AttachmentEmbedConfigExtension(),
2533
AttachmentEmbedService,
2634
AttachmentBlockNotionHtmlAdapterExtension,
35+
ToolbarModuleExtension({
36+
id: BlockFlavourIdentifier(Flavour),
37+
config: BuiltinToolbarConfig,
38+
}),
2739
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type {
2+
ToolbarActionGroup,
3+
ToolbarModuleConfig,
4+
} from '@blocksuite/affine-shared/services';
5+
6+
export const BuiltinToolbarConfig = {
7+
actions: [
8+
{
9+
id: 'rename',
10+
tooltip: 'Rename',
11+
run(_cx) {},
12+
},
13+
{
14+
id: 'switch-view',
15+
actions: [
16+
{
17+
id: 'card-view',
18+
label: 'Card view',
19+
run(_cx) {},
20+
},
21+
{
22+
id: 'embed-view',
23+
label: 'Embed view',
24+
run(_cx) {},
25+
},
26+
],
27+
content(_cx) {
28+
this.actions;
29+
return null;
30+
},
31+
} satisfies ToolbarActionGroup,
32+
{
33+
id: 'download',
34+
tooltip: 'Download',
35+
run(_cx) {},
36+
},
37+
{
38+
id: 'caption',
39+
tooltip: 'Caption',
40+
run(_cx) {},
41+
},
42+
{
43+
id: 'clipboard',
44+
placement: 'more',
45+
actions: [
46+
{
47+
id: 'copy',
48+
label: 'Copy',
49+
run(_cx) {},
50+
},
51+
{
52+
id: 'duplicate',
53+
label: 'Duplicate',
54+
run(_cx) {},
55+
},
56+
],
57+
},
58+
{
59+
id: 'refresh',
60+
placement: 'more',
61+
actions: [
62+
{
63+
id: 'reload',
64+
label: 'Reload',
65+
run(_cx) {},
66+
},
67+
],
68+
},
69+
{
70+
id: 'delete',
71+
placement: 'more',
72+
actions: [
73+
{
74+
id: 'delete',
75+
label: 'Delete',
76+
run(_cx) {},
77+
},
78+
],
79+
},
80+
],
81+
} as const satisfies ToolbarModuleConfig;
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
12
import {
3+
BlockFlavourIdentifier,
24
BlockViewExtension,
35
CommandExtension,
46
FlavourExtension,
@@ -7,15 +9,18 @@ import {
79
import type { ExtensionType } from '@blocksuite/store';
810
import { literal } from 'lit/static-html.js';
911

10-
import { ImageBlockAdapterExtensions } from './adapters/extension.js';
11-
import { commands } from './commands/index.js';
12-
import { ImageBlockService, ImageDropOption } from './image-service.js';
12+
import { ImageBlockAdapterExtensions } from './adapters/extension';
13+
import { commands } from './commands/index';
14+
import { ImageBlockService, ImageDropOption } from './image-service';
15+
import { BuiltinToolbarConfig } from './toolbar';
16+
17+
const Flavour = 'affine:image';
1318

1419
export const ImageBlockSpec: ExtensionType[] = [
15-
FlavourExtension('affine:image'),
20+
FlavourExtension(Flavour),
1621
ImageBlockService,
1722
CommandExtension(commands),
18-
BlockViewExtension('affine:image', model => {
23+
BlockViewExtension(Flavour, model => {
1924
const parent = model.doc.getParent(model.id);
2025

2126
if (parent?.flavour === 'affine:surface') {
@@ -24,9 +29,13 @@ export const ImageBlockSpec: ExtensionType[] = [
2429

2530
return literal`affine-image`;
2631
}),
27-
WidgetViewMapExtension('affine:image', {
32+
WidgetViewMapExtension(Flavour, {
2833
imageToolbar: literal`affine-image-toolbar-widget`,
2934
}),
3035
ImageDropOption,
3136
ImageBlockAdapterExtensions,
37+
ToolbarModuleExtension({
38+
id: BlockFlavourIdentifier(Flavour),
39+
config: BuiltinToolbarConfig,
40+
}),
3241
].flat();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
2+
3+
export const BuiltinToolbarConfig = {
4+
actions: [
5+
{
6+
id: 'download',
7+
tooltip: 'Download',
8+
run(_cx) {},
9+
},
10+
{
11+
id: 'caption',
12+
tooltip: 'Caption',
13+
run(_cx) {},
14+
},
15+
{
16+
id: 'clipboard',
17+
placement: 'more',
18+
actions: [
19+
{
20+
id: 'copy',
21+
label: 'Copy',
22+
run(_cx) {},
23+
},
24+
{
25+
id: 'duplicate',
26+
label: 'Duplicate',
27+
run(_cx) {},
28+
},
29+
],
30+
},
31+
{
32+
id: 'conversions',
33+
placement: 'more',
34+
actions: [
35+
{
36+
id: 'turn-into-card-view',
37+
label: 'Turn into card view',
38+
run(_cx) {},
39+
},
40+
],
41+
},
42+
{
43+
id: 'delete',
44+
placement: 'more',
45+
actions: [
46+
{
47+
id: 'delete',
48+
label: 'Delete',
49+
run(_cx) {},
50+
},
51+
],
52+
},
53+
],
54+
} as const satisfies ToolbarModuleConfig;

blocksuite/affine/shared/src/services/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export * from './parse-url-service';
1515
export * from './quick-search-service';
1616
export * from './telemetry-service';
1717
export * from './theme-service';
18+
export * from './toolbar-service';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './module';
2+
export * from './registry';
3+
// export type { ToolbarAction, ToolbarContext, ToolbarModuleConfig } from './types';
4+
export * from './types';
5+
export * from './utils';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { BlockFlavourIdentifier } from '@blocksuite/block-std';
2+
3+
import type { ToolbarModuleConfig } from './types';
4+
5+
export type ToolbarModule = {
6+
readonly id: ReturnType<typeof BlockFlavourIdentifier>;
7+
8+
readonly config: ToolbarModuleConfig;
9+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
type BlockStdScope,
3+
LifeCycleWatcher,
4+
StdIdentifier,
5+
} from '@blocksuite/block-std';
6+
import {
7+
type Container,
8+
createIdentifier,
9+
createScope,
10+
} from '@blocksuite/global/di';
11+
import type { ExtensionType } from '@blocksuite/store';
12+
13+
import type { ToolbarModule } from './module';
14+
15+
export const ToolbarModuleIdentifier = createIdentifier<ToolbarModule>(
16+
'AffineToolbarModuleIdentifier'
17+
);
18+
19+
export const ToolbarModulesIdentifier = createIdentifier<
20+
Map<string, ToolbarModule>
21+
>('AffineToolbarModulesIdentifier');
22+
23+
export const ToolbarRegistryScope = createScope('AffineToolbarRegistryScope');
24+
25+
export const ToolbarRegistryIdentifier =
26+
createIdentifier<ToolbarRegistryExtension>('AffineToolbarRegistryIdentifier');
27+
28+
export function ToolbarModuleExtension(module: ToolbarModule): ExtensionType {
29+
return {
30+
setup: di => {
31+
di.scope(ToolbarRegistryScope).addImpl(
32+
ToolbarModuleIdentifier(module.id.variant),
33+
module
34+
);
35+
},
36+
};
37+
}
38+
39+
export class ToolbarRegistryExtension extends LifeCycleWatcher {
40+
constructor(
41+
std: BlockStdScope,
42+
readonly modules: Map<string, ToolbarModule>
43+
) {
44+
super(std);
45+
}
46+
47+
static override readonly key = 'toolbar-registry';
48+
49+
static override setup(di: Container) {
50+
di.scope(ToolbarRegistryScope)
51+
.addImpl(ToolbarModulesIdentifier, provider =>
52+
provider.getAll(ToolbarModuleIdentifier)
53+
)
54+
.addImpl(ToolbarRegistryIdentifier, this, [
55+
StdIdentifier,
56+
ToolbarModulesIdentifier,
57+
]);
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { BlockStdScope } from '@blocksuite/block-std';
2+
import type { TemplateResult } from 'lit';
3+
4+
type ActionBase = {
5+
id: string;
6+
score?: number;
7+
when?: (cx: ToolbarContext) => boolean;
8+
placement?: 'start' | 'end' | 'more';
9+
};
10+
11+
export type ToolbarAction = ActionBase & {
12+
label?: string;
13+
icon?: TemplateResult;
14+
tooltip?: string;
15+
content?: (cx: ToolbarContext) => TemplateResult | null;
16+
run: (cx: ToolbarContext) => void;
17+
};
18+
19+
// Generates an action at runtime
20+
export type ToolbarActionGenerator = ActionBase & {
21+
generate: (cx: ToolbarContext) => ToolbarAction;
22+
};
23+
24+
export type ToolbarActionGroup = ActionBase & {
25+
actions: ToolbarAction[];
26+
content?: (cx: ToolbarContext) => TemplateResult | null;
27+
};
28+
29+
// Generates an action group at runtime
30+
export type ToolbarActionGroupGenerator = ActionBase & {
31+
generate: (cx: ToolbarContext) => ToolbarActionGroup;
32+
};
33+
34+
export type ToolbarActions<
35+
T extends ActionBase =
36+
| ToolbarAction
37+
| ToolbarActionGenerator
38+
| ToolbarActionGroup
39+
| ToolbarActionGroupGenerator,
40+
> = T[];
41+
42+
abstract class ToolbarContextBase {
43+
constructor(readonly std: BlockStdScope) {}
44+
45+
get isReadonly() {
46+
return this.std.store.readonly;
47+
}
48+
}
49+
50+
export class ToolbarContext extends ToolbarContextBase {}
51+
52+
export interface ToolbarModuleConfig {
53+
actions: ToolbarActions;
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function generateActionIdWith(
2+
flavour: string,
3+
name: string,
4+
prefix = 'com.affine.toolbar.internal'
5+
) {
6+
return `${prefix}.${flavour}.${name}`;
7+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1+
import {
2+
ToolbarRegistryIdentifier,
3+
ToolbarRegistryScope,
4+
} from '@blocksuite/affine-shared/services';
15
import { WidgetComponent } from '@blocksuite/block-std';
26

37
export const AFFINE_TOOLBAR_WIDGET = 'affine-toolbar-widget';
48

5-
export class AffineToolbarWidget extends WidgetComponent {}
9+
export class AffineToolbarWidget extends WidgetComponent {
10+
override connectedCallback() {
11+
super.connectedCallback();
12+
13+
const toolbarRegistry = this.std.container
14+
.provider(ToolbarRegistryScope, this.std.provider)
15+
.get(ToolbarRegistryIdentifier);
16+
console.log(toolbarRegistry);
17+
}
18+
}

0 commit comments

Comments
 (0)