Files
AFFiNE/blocksuite/affine/components/src/toolbar/utils.ts
T
fundon ec9bd1f383 feat(editor): add toolbar registry extension (#9572)
### 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.
2025-03-06 06:46:03 +00:00

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),
};
}