### What's Changed! #### Added Manage various types of toolbars uniformly in one place. * `affine-toolbar-widget` * `ToolbarRegistryExtension` The toolbar currently supports and handles several scenarios: 1. Select blocks: `BlockSelection` 2. Select text: `TextSelection` or `NativeSelection` 3. Hover a link: `affine-link` and `affine-reference` #### Removed Remove redundant toolbar implementations. * `attachment` toolbar * `bookmark` toolbar * `embed` toolbar * `formatting` toolbar * `affine-link` toolbar * `affine-reference` toolbar ### How to migrate? Here is an example that can help us migrate some unrefactored toolbars: Check out the more detailed types of [`ToolbarModuleConfig`](https://github.com/toeverything/AFFiNE/blob/c178debf2d49c40b753e1bcaa6f07270bdde7401/blocksuite/affine/shared/src/services/toolbar-service/config.ts). 1. Add toolbar configuration file to a block type, such as bookmark block: [`config.ts`](https://github.com/toeverything/AFFiNE/blob/c178debf2d49c40b753e1bcaa6f07270bdde7401/blocksuite/affine/block-bookmark/src/configs/toolbar.ts) ```ts export const builtinToolbarConfig = { actions: [ { id: 'a.preview', content(ctx) { const model = ctx.getCurrentModelBy(BlockSelection, BookmarkBlockModel); if (!model) return null; const { url } = model; return html`<affine-link-preview .url=${url}></affine-link-preview>`; }, }, { id: 'b.conversions', actions: [ { id: 'inline', label: 'Inline view', run(ctx) { }, }, { id: 'card', label: 'Card view', disabled: true, }, { id: 'embed', label: 'Embed view', disabled(ctx) { }, run(ctx) { }, }, ], content(ctx) { }, } satisfies ToolbarActionGroup<ToolbarAction>, { id: 'c.style', actions: [ { id: 'horizontal', label: 'Large horizontal style', }, { id: 'list', label: 'Small horizontal style', }, ], content(ctx) { }, } satisfies ToolbarActionGroup<ToolbarAction>, { id: 'd.caption', tooltip: 'Caption', icon: CaptionIcon(), run(ctx) { }, }, { placement: ActionPlacement.More, id: 'a.clipboard', actions: [ { id: 'copy', label: 'Copy', icon: CopyIcon(), run(ctx) { }, }, { id: 'duplicate', label: 'Duplicate', icon: DuplicateIcon(), run(ctx) { }, }, ], }, { placement: ActionPlacement.More, id: 'b.refresh', label: 'Reload', icon: ResetIcon(), run(ctx) { }, }, { placement: ActionPlacement.More, id: 'c.delete', label: 'Delete', icon: DeleteIcon(), variant: 'destructive', run(ctx) { }, }, ], } as const satisfies ToolbarModuleConfig; ``` 2. Add configuration extension to a block spec: [bookmark's spec](https://github.com/toeverything/AFFiNE/blob/c178debf2d49c40b753e1bcaa6f07270bdde7401/blocksuite/affine/block-bookmark/src/bookmark-spec.ts) ```ts const flavour = BookmarkBlockSchema.model.flavour; export const BookmarkBlockSpec: ExtensionType[] = [ ..., ToolbarModuleExtension({ id: BlockFlavourIdentifier(flavour), config: builtinToolbarConfig, }), ].flat(); ``` 3. If the bock type already has a toolbar configuration built in, we can customize it in the following ways: Check out the [editor's config](https://github.com/toeverything/AFFiNE/blob/c178debf2d49c40b753e1bcaa6f07270bdde7401/packages/frontend/core/src/blocksuite/extensions/editor-config/index.ts#L51C4-L54C8) file. ```ts // Defines a toolbar configuration for the bookmark block type const customBookmarkToolbarConfig = { actions: [ ... ] } as const satisfies ToolbarModuleConfig; // Adds it into the editor's config ToolbarModuleExtension({ id: BlockFlavourIdentifier('custom:affine:bookmark'), config: customBookmarkToolbarConfig, }), ``` 4. If we want to extend the global: ```ts // Defines a toolbar configuration const customWildcardToolbarConfig = { actions: [ ... ] } as const satisfies ToolbarModuleConfig; // Adds it into the editor's config ToolbarModuleExtension({ id: BlockFlavourIdentifier('custom:affine:*'), config: customWildcardToolbarConfig, }), ``` Currently, only most toolbars in page mode have been refactored. Next is edgeless mode.
121 lines
3.4 KiB
TypeScript
121 lines
3.4 KiB
TypeScript
import type { BlockStdScope } from '@blocksuite/block-std';
|
|
import { html, nothing } from 'lit';
|
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
import { join } from 'lit/directives/join.js';
|
|
import { repeat } from 'lit/directives/repeat.js';
|
|
|
|
import { ToolbarMoreMenuConfigExtension } from './config.js';
|
|
import type { MenuContext } from './menu-context.js';
|
|
import type {
|
|
FatMenuItems,
|
|
MenuItem,
|
|
MenuItemGroup,
|
|
ToolbarMoreMenuConfig,
|
|
} from './types.js';
|
|
|
|
export function groupsToActions<T>(
|
|
groups: MenuItemGroup<T>[],
|
|
context: T
|
|
): MenuItem[][] {
|
|
return groups
|
|
.filter(group => group.when?.(context) ?? true)
|
|
.map(({ items }) =>
|
|
items
|
|
.filter(item => item.when?.(context) ?? true)
|
|
.map(({ type, label, tooltip, icon, action, disabled, generate }) => {
|
|
if (action && typeof action === 'function') {
|
|
return {
|
|
type,
|
|
label,
|
|
tooltip,
|
|
icon,
|
|
action: () => {
|
|
action(context)?.catch(console.error);
|
|
},
|
|
disabled:
|
|
typeof disabled === 'function' ? disabled(context) : disabled,
|
|
};
|
|
}
|
|
|
|
if (generate && typeof generate === 'function') {
|
|
const result = generate(context);
|
|
|
|
if (!result) return null;
|
|
|
|
return {
|
|
type,
|
|
label,
|
|
tooltip,
|
|
icon,
|
|
...result,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
})
|
|
.filter(item => !!item)
|
|
);
|
|
}
|
|
|
|
export function renderActions(
|
|
fatMenuItems: FatMenuItems,
|
|
action?: (item: MenuItem) => Promise<void> | void,
|
|
selectedName?: string
|
|
) {
|
|
return join(
|
|
fatMenuItems
|
|
.filter(g => g.length)
|
|
.map(g => g.filter(a => a !== nothing) as MenuItem[])
|
|
.filter(g => g.length)
|
|
.map(items =>
|
|
repeat(
|
|
items,
|
|
item => item.type,
|
|
item =>
|
|
item.render?.(item) ??
|
|
html`
|
|
<editor-menu-action
|
|
class=${ifDefined(
|
|
item.type === 'delete' ? 'delete' : undefined
|
|
)}
|
|
aria-label=${ifDefined(item.label)}
|
|
?data-selected=${selectedName === item.label}
|
|
?disabled=${item.disabled}
|
|
@click=${item.action ? item.action : () => action?.(item)}
|
|
>
|
|
${item.icon}${item.label
|
|
? html`<span class="label">${item.label}</span>`
|
|
: nothing}
|
|
</editor-menu-action>
|
|
`
|
|
)
|
|
),
|
|
() => html`
|
|
<editor-toolbar-separator
|
|
data-orientation="horizontal"
|
|
></editor-toolbar-separator>
|
|
`
|
|
);
|
|
}
|
|
|
|
export function cloneGroups<T>(groups: MenuItemGroup<T>[]) {
|
|
return groups.map(group => ({ ...group, items: [...group.items] }));
|
|
}
|
|
|
|
export function renderGroups<T>(groups: MenuItemGroup<T>[], context: T) {
|
|
return renderActions(groupsToActions(groups, context));
|
|
}
|
|
|
|
export function renderToolbarSeparator(orientation?: 'horizontal') {
|
|
return html`<editor-toolbar-separator
|
|
data-orientation=${ifDefined(orientation)}
|
|
></editor-toolbar-separator>`;
|
|
}
|
|
|
|
export function getMoreMenuConfig(std: BlockStdScope): ToolbarMoreMenuConfig {
|
|
return {
|
|
configure: <T extends MenuContext>(groups: MenuItemGroup<T>[]) => groups,
|
|
...std.getOptional(ToolbarMoreMenuConfigExtension.identifier),
|
|
};
|
|
}
|