refactor(editor): edgeless external embed card toolbar config extension (#10712)

This commit is contained in:
fundon
2025-03-19 04:05:36 +00:00
parent 284aae9b52
commit 7f4b56e05c
11 changed files with 415 additions and 160 deletions

View File

@@ -1,18 +1,29 @@
import { reassociateConnectorsCommand } from '@blocksuite/affine-block-surface';
import { toast } from '@blocksuite/affine-components/toast'; import { toast } from '@blocksuite/affine-components/toast';
import { import {
BookmarkStyles, BookmarkStyles,
type EmbedCardStyle,
EmbedGithubModel, EmbedGithubModel,
EmbedGithubStyles,
isExternalEmbedModel, isExternalEmbedModel,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { import {
ActionPlacement, ActionPlacement,
EmbedOptionProvider, EmbedOptionProvider,
type LinkEventType,
type ToolbarAction, type ToolbarAction,
type ToolbarActionGroup, type ToolbarActionGroup,
type ToolbarContext,
type ToolbarModuleConfig, type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { getBlockProps } from '@blocksuite/affine-shared/utils'; import { getBlockProps } from '@blocksuite/affine-shared/utils';
import { BlockSelection } from '@blocksuite/block-std'; import { BlockFlavourIdentifier, BlockSelection } from '@blocksuite/block-std';
import { Bound } from '@blocksuite/global/gfx';
import { import {
CaptionIcon, CaptionIcon,
CopyIcon, CopyIcon,
@@ -20,8 +31,8 @@ import {
DuplicateIcon, DuplicateIcon,
ResetIcon, ResetIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { Slice, Text } from '@blocksuite/store'; import { type ExtensionType, Slice, Text } from '@blocksuite/store';
import { signal } from '@preact/signals-core'; import { computed, signal } from '@preact/signals-core';
import { html } from 'lit'; import { html } from 'lit';
import { keyed } from 'lit/directives/keyed.js'; import { keyed } from 'lit/directives/keyed.js';
import * as Y from 'yjs'; import * as Y from 'yjs';
@@ -32,15 +43,46 @@ import type { EmbedLoomBlockComponent } from '../embed-loom-block';
import type { EmbedYoutubeBlockComponent } from '../embed-youtube-block'; import type { EmbedYoutubeBlockComponent } from '../embed-youtube-block';
const trackBaseProps = { const trackBaseProps = {
segment: 'doc',
page: 'doc editor',
module: 'toolbar',
category: 'link', category: 'link',
type: 'card view', type: 'card view',
}; };
const previewAction = {
id: 'a.preview',
content(ctx) {
const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return null;
const { url } = model.props;
const options = ctx.std.get(EmbedOptionProvider).getEmbedBlockOptions(url);
if (options?.viewType !== 'card') return null;
return html`<affine-link-preview .url=${url}></affine-link-preview>`;
},
} satisfies ToolbarAction;
const createOnToggleFn =
(
ctx: ToolbarContext,
name: Extract<
LinkEventType,
| 'OpenedViewSelector'
| 'OpenedCardStyleSelector'
| 'OpenedCardScaleSelector'
>,
control: 'switch view' | 'switch card style' | 'switch card scale'
) =>
(e: CustomEvent<boolean>) => {
e.stopPropagation();
const opened = e.detail;
if (!opened) return;
ctx.track(name, { ...trackBaseProps, control });
};
// External embed blocks // External embed blocks
export function createBuiltinToolbarConfigForExternal( function createBuiltinToolbarConfigForExternal(
klass: klass:
| typeof EmbedGithubBlockComponent | typeof EmbedGithubBlockComponent
| typeof EmbedFigmaBlockComponent | typeof EmbedFigmaBlockComponent
@@ -49,22 +91,7 @@ export function createBuiltinToolbarConfigForExternal(
) { ) {
return { return {
actions: [ actions: [
{ previewAction,
id: 'a.preview',
content(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return null;
const { url } = model.props;
const options = ctx.std
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
if (options?.viewType !== 'card') return null;
return html`<affine-link-preview .url=${url}></affine-link-preview>`;
},
},
{ {
id: 'b.conversions', id: 'b.conversions',
actions: [ actions: [
@@ -72,7 +99,7 @@ export function createBuiltinToolbarConfigForExternal(
id: 'inline', id: 'inline',
label: 'Inline view', label: 'Inline view',
run(ctx) { run(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model; const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return; if (!model || !isExternalEmbedModel(model)) return;
const { title, caption, url: link } = model.props; const { title, caption, url: link } = model.props;
@@ -105,7 +132,7 @@ export function createBuiltinToolbarConfigForExternal(
id: 'card', id: 'card',
label: 'Card view', label: 'Card view',
disabled(ctx) { disabled(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model; const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return true; if (!model || !isExternalEmbedModel(model)) return true;
const { url } = model.props; const { url } = model.props;
@@ -116,7 +143,7 @@ export function createBuiltinToolbarConfigForExternal(
return options?.viewType === 'card'; return options?.viewType === 'card';
}, },
run(ctx) { run(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model; const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return; if (!model || !isExternalEmbedModel(model)) return;
const { url, caption } = model.props; const { url, caption } = model.props;
@@ -165,7 +192,7 @@ export function createBuiltinToolbarConfigForExternal(
id: 'embed', id: 'embed',
label: 'Embed view', label: 'Embed view',
disabled(ctx) { disabled(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model; const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return false; if (!model || !isExternalEmbedModel(model)) return false;
const { url } = model.props; const { url } = model.props;
@@ -176,7 +203,7 @@ export function createBuiltinToolbarConfigForExternal(
return options?.viewType === 'embed'; return options?.viewType === 'embed';
}, },
when(ctx) { when(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model; const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return false; if (!model || !isExternalEmbedModel(model)) return false;
const { url } = model.props; const { url } = model.props;
@@ -187,7 +214,7 @@ export function createBuiltinToolbarConfigForExternal(
return options?.viewType === 'embed'; return options?.viewType === 'embed';
}, },
run(ctx) { run(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model; const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return; if (!model || !isExternalEmbedModel(model)) return;
const { url, caption } = model.props; const { url, caption } = model.props;
@@ -231,7 +258,7 @@ export function createBuiltinToolbarConfigForExternal(
}, },
], ],
content(ctx) { content(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model; const model = ctx.getCurrentModel();
if (!model || !isExternalEmbedModel(model)) return null; if (!model || !isExternalEmbedModel(model)) return null;
const { url } = model.props; const { url } = model.props;
@@ -242,15 +269,11 @@ export function createBuiltinToolbarConfigForExternal(
const viewType$ = signal( const viewType$ = signal(
`${viewType === 'card' ? 'Card' : 'Embed'} view` `${viewType === 'card' ? 'Card' : 'Embed'} view`
); );
const onToggle = (e: CustomEvent<boolean>) => { const onToggle = createOnToggleFn(
const opened = e.detail; ctx,
if (!opened) return; 'OpenedViewSelector',
'switch view'
ctx.track('OpenedViewSelector', { );
...trackBaseProps,
control: 'switch view',
});
};
return html`${keyed( return html`${keyed(
model, model,
@@ -275,6 +298,9 @@ export function createBuiltinToolbarConfigForExternal(
label: 'Small horizontal style', label: 'Small horizontal style',
}, },
], ],
when(ctx) {
return Boolean(ctx.getCurrentModelByType(EmbedGithubModel));
},
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType(EmbedGithubModel); const model = ctx.getCurrentModelByType(EmbedGithubModel);
if (!model) return null; if (!model) return null;
@@ -291,16 +317,11 @@ export function createBuiltinToolbarConfigForExternal(
}); });
}, },
})) satisfies ToolbarAction[]; })) satisfies ToolbarAction[];
const onToggle = createOnToggleFn(
const onToggle = (e: CustomEvent<boolean>) => { ctx,
const opened = e.detail; 'OpenedCardStyleSelector',
if (!opened) return; 'switch card style'
);
ctx.track('OpenedCardStyleSelector', {
...trackBaseProps,
control: 'switch card style',
});
};
return html`${keyed( return html`${keyed(
model, model,
@@ -393,3 +414,264 @@ export function createBuiltinToolbarConfigForExternal(
], ],
} as const satisfies ToolbarModuleConfig; } as const satisfies ToolbarModuleConfig;
} }
const createBuiltinSurfaceToolbarConfigForExternal = (
klass:
| typeof EmbedGithubBlockComponent
| typeof EmbedFigmaBlockComponent
| typeof EmbedLoomBlockComponent
| typeof EmbedYoutubeBlockComponent
) => {
return {
actions: [
previewAction,
{
id: 'b.conversions',
actions: [
{
id: 'card',
label: 'Card view',
run(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return;
const { id: oldId, xywh, parent } = model;
const { url, caption } = model.props;
let { style } = model.props;
let flavour = 'affine:bookmark';
if (!BookmarkStyles.includes(style)) {
style = BookmarkStyles[0];
}
const bounds = Bound.deserialize(xywh);
bounds.w = EMBED_CARD_WIDTH[style];
bounds.h = EMBED_CARD_HEIGHT[style];
const newId = ctx.store.addBlock(
flavour,
{ url, caption, style, xywh: bounds.serialize() },
parent
);
ctx.command.exec(reassociateConnectorsCommand, { oldId, newId });
ctx.store.deleteBlock(model);
// Selects new block
ctx.gfx.selection.set({ editing: false, elements: [newId] });
ctx.track('SelectedView', {
...trackBaseProps,
control: 'select view',
type: 'card view',
});
},
},
{
id: 'embed',
label: 'Embed view',
disabled: true,
},
],
when(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return false;
const { url } = model.props;
const options = ctx.std
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
return options?.viewType === 'embed';
},
content(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return null;
const { url } = model.props;
const viewType =
ctx.std.get(EmbedOptionProvider).getEmbedBlockOptions(url)
?.viewType ?? 'card';
const actions = this.actions.map(action => ({ ...action }));
const viewType$ = signal(
`${viewType === 'card' ? 'Card' : 'Embed'} view`
);
const onToggle = createOnToggleFn(
ctx,
'OpenedViewSelector',
'switch view'
);
return html`${keyed(
model,
html`<affine-view-dropdown-menu
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.viewType$=${viewType$}
></affine-view-dropdown-menu>`
)}`;
},
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'c.style',
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
].filter(action =>
EmbedGithubStyles.includes(action.id as EmbedCardStyle)
),
when(ctx) {
return Boolean(ctx.getCurrentModelByType(EmbedGithubModel));
},
content(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model) return null;
const actions = this.actions.map(action => ({
...action,
run: ({ store }) => {
const style = action.id as EmbedCardStyle;
const bounds = Bound.deserialize(model.xywh);
bounds.w = EMBED_CARD_WIDTH[style];
bounds.h = EMBED_CARD_HEIGHT[style];
const xywh = bounds.serialize();
store.updateBlock(model, { style, xywh });
ctx.track('SelectedCardStyle', {
...trackBaseProps,
control: 'select card style',
type: style,
});
},
})) satisfies ToolbarAction[];
const style$ = model.props.style$;
const onToggle = createOnToggleFn(
ctx,
'OpenedCardStyleSelector',
'switch card style'
);
return html`${keyed(
model,
html`<affine-card-style-dropdown-menu
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.style$=${style$}
></affine-card-style-dropdown-menu>`
)}`;
},
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'd.caption',
tooltip: 'Caption',
icon: CaptionIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(klass);
block?.captionEditor?.show();
ctx.track('OpenedCaptionEditor', {
...trackBaseProps,
control: 'add caption',
});
},
},
{
id: 'e.scale',
content(ctx) {
const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model) return null;
const scale$ = computed(() => {
const {
xywh$: { value: xywh },
} = model;
const {
style$: { value: style },
} = model.props;
const bounds = Bound.deserialize(xywh);
const height = EMBED_CARD_HEIGHT[style];
return Math.round(100 * (bounds.h / height));
});
const onSelect = (e: CustomEvent<number>) => {
e.stopPropagation();
const scale = e.detail / 100;
const bounds = Bound.deserialize(model.xywh);
const style = model.props.style;
bounds.h = EMBED_CARD_HEIGHT[style] * scale;
bounds.w = EMBED_CARD_WIDTH[style] * scale;
const xywh = bounds.serialize();
ctx.store.updateBlock(model, { xywh });
ctx.track('SelectedCardScale', {
...trackBaseProps,
control: 'select card scale',
});
};
const onToggle = createOnToggleFn(
ctx,
'OpenedCardScaleSelector',
'switch card scale'
);
const format = (value: number) => `${value}%`;
return html`${keyed(
model,
html`<affine-size-dropdown-menu
@select=${onSelect}
@toggle=${onToggle}
.format=${format}
.size$=${scale$}
></affine-size-dropdown-menu>`
)}`;
},
},
],
when: ctx => ctx.getSurfaceBlocksByType(klass).length === 1,
} as const satisfies ToolbarModuleConfig;
};
export const createBuiltinToolbarConfigExtension = (
flavour: string,
klass:
| typeof EmbedGithubBlockComponent
| typeof EmbedFigmaBlockComponent
| typeof EmbedLoomBlockComponent
| typeof EmbedYoutubeBlockComponent
): ExtensionType[] => {
const name = flavour.split(':').pop();
return [
ToolbarModuleExtension({
id: BlockFlavourIdentifier(flavour),
config: createBuiltinToolbarConfigForExternal(klass),
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier(`affine:surface:${name}`),
config: createBuiltinSurfaceToolbarConfigForExternal(klass),
}),
];
};

View File

@@ -1,15 +1,10 @@
import { EmbedFigmaBlockSchema } from '@blocksuite/affine-model'; import { EmbedFigmaBlockSchema } from '@blocksuite/affine-model';
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu'; import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import { import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std';
BlockServiceIdentifier,
BlockViewExtension,
FlavourExtension,
} from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store'; import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js'; import { literal } from 'lit/static-html.js';
import { createBuiltinToolbarConfigForExternal } from '../configs/toolbar'; import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
import { EmbedFigmaBlockAdapterExtensions } from './adapters/extension'; import { EmbedFigmaBlockAdapterExtensions } from './adapters/extension';
import { embedFigmaSlashMenuConfig } from './configs/slash-menu'; import { embedFigmaSlashMenuConfig } from './configs/slash-menu';
import { EmbedFigmaBlockComponent } from './embed-figma-block'; import { EmbedFigmaBlockComponent } from './embed-figma-block';
@@ -26,9 +21,6 @@ export const EmbedFigmaBlockSpec: ExtensionType[] = [
}), }),
EmbedFigmaBlockAdapterExtensions, EmbedFigmaBlockAdapterExtensions,
EmbedFigmaBlockOptionConfig, EmbedFigmaBlockOptionConfig,
ToolbarModuleExtension({ createBuiltinToolbarConfigExtension(flavour, EmbedFigmaBlockComponent),
id: BlockServiceIdentifier(flavour),
config: createBuiltinToolbarConfigForExternal(EmbedFigmaBlockComponent),
}),
SlashMenuConfigExtension(flavour, embedFigmaSlashMenuConfig), SlashMenuConfigExtension(flavour, embedFigmaSlashMenuConfig),
].flat(); ].flat();

View File

@@ -1,15 +1,10 @@
import { EmbedGithubBlockSchema } from '@blocksuite/affine-model'; import { EmbedGithubBlockSchema } from '@blocksuite/affine-model';
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu'; import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import { import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std';
BlockServiceIdentifier,
BlockViewExtension,
FlavourExtension,
} from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store'; import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js'; import { literal } from 'lit/static-html.js';
import { createBuiltinToolbarConfigForExternal } from '../configs/toolbar'; import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
import { EmbedGithubBlockAdapterExtensions } from './adapters/extension'; import { EmbedGithubBlockAdapterExtensions } from './adapters/extension';
import { embedGithubSlashMenuConfig } from './configs/slash-menu'; import { embedGithubSlashMenuConfig } from './configs/slash-menu';
import { EmbedGithubBlockComponent } from './embed-github-block'; import { EmbedGithubBlockComponent } from './embed-github-block';
@@ -30,9 +25,6 @@ export const EmbedGithubBlockSpec: ExtensionType[] = [
}), }),
EmbedGithubBlockAdapterExtensions, EmbedGithubBlockAdapterExtensions,
EmbedGithubBlockOptionConfig, EmbedGithubBlockOptionConfig,
ToolbarModuleExtension({ createBuiltinToolbarConfigExtension(flavour, EmbedGithubBlockComponent),
id: BlockServiceIdentifier(flavour),
config: createBuiltinToolbarConfigForExternal(EmbedGithubBlockComponent),
}),
SlashMenuConfigExtension(flavour, embedGithubSlashMenuConfig), SlashMenuConfigExtension(flavour, embedGithubSlashMenuConfig),
].flat(); ].flat();

View File

@@ -1,15 +1,10 @@
import { EmbedLoomBlockSchema } from '@blocksuite/affine-model'; import { EmbedLoomBlockSchema } from '@blocksuite/affine-model';
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu'; import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import { import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std';
BlockServiceIdentifier,
BlockViewExtension,
FlavourExtension,
} from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store'; import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js'; import { literal } from 'lit/static-html.js';
import { createBuiltinToolbarConfigForExternal } from '../configs/toolbar'; import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
import { EmbedLoomBlockAdapterExtensions } from './adapters/extension'; import { EmbedLoomBlockAdapterExtensions } from './adapters/extension';
import { embedLoomSlashMenuConfig } from './configs/slash-menu'; import { embedLoomSlashMenuConfig } from './configs/slash-menu';
import { EmbedLoomBlockComponent } from './embed-loom-block'; import { EmbedLoomBlockComponent } from './embed-loom-block';
@@ -30,9 +25,6 @@ export const EmbedLoomBlockSpec: ExtensionType[] = [
}), }),
EmbedLoomBlockAdapterExtensions, EmbedLoomBlockAdapterExtensions,
EmbedLoomBlockOptionConfig, EmbedLoomBlockOptionConfig,
ToolbarModuleExtension({ createBuiltinToolbarConfigExtension(flavour, EmbedLoomBlockComponent),
id: BlockServiceIdentifier(flavour),
config: createBuiltinToolbarConfigForExternal(EmbedLoomBlockComponent),
}),
SlashMenuConfigExtension(flavour, embedLoomSlashMenuConfig), SlashMenuConfigExtension(flavour, embedLoomSlashMenuConfig),
].flat(); ].flat();

View File

@@ -1,15 +1,10 @@
import { EmbedYoutubeBlockSchema } from '@blocksuite/affine-model'; import { EmbedYoutubeBlockSchema } from '@blocksuite/affine-model';
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu'; import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import { import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std';
BlockServiceIdentifier,
BlockViewExtension,
FlavourExtension,
} from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store'; import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js'; import { literal } from 'lit/static-html.js';
import { createBuiltinToolbarConfigForExternal } from '../configs/toolbar'; import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
import { EmbedYoutubeBlockAdapterExtensions } from './adapters/extension'; import { EmbedYoutubeBlockAdapterExtensions } from './adapters/extension';
import { embedYoutubeSlashMenuConfig } from './configs/slash-menu'; import { embedYoutubeSlashMenuConfig } from './configs/slash-menu';
import { EmbedYoutubeBlockComponent } from './embed-youtube-block'; import { EmbedYoutubeBlockComponent } from './embed-youtube-block';
@@ -30,9 +25,6 @@ export const EmbedYoutubeBlockSpec: ExtensionType[] = [
}), }),
EmbedYoutubeBlockAdapterExtensions, EmbedYoutubeBlockAdapterExtensions,
EmbedYoutubeBlockOptionConfig, EmbedYoutubeBlockOptionConfig,
ToolbarModuleExtension({ createBuiltinToolbarConfigExtension(flavour, EmbedYoutubeBlockComponent),
id: BlockServiceIdentifier(flavour),
config: createBuiltinToolbarConfigForExternal(EmbedYoutubeBlockComponent),
}),
SlashMenuConfigExtension('affine:embed-youtube', embedYoutubeSlashMenuConfig), SlashMenuConfigExtension('affine:embed-youtube', embedYoutubeSlashMenuConfig),
].flat(); ].flat();

View File

@@ -1,11 +0,0 @@
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
export const builtinEmbedToolbarConfig = {
actions: [
{
id: 'a.test',
label: 'Embed',
run() {},
},
],
} as const satisfies ToolbarModuleConfig;

View File

@@ -4,7 +4,6 @@ import type { ExtensionType } from '@blocksuite/store';
import { builtinBrushToolbarConfig } from './brush'; import { builtinBrushToolbarConfig } from './brush';
import { builtinConnectorToolbarConfig } from './connector'; import { builtinConnectorToolbarConfig } from './connector';
import { builtinEmbedToolbarConfig } from './embed';
import { builtinFrameToolbarConfig } from './frame'; import { builtinFrameToolbarConfig } from './frame';
import { builtinGroupToolbarConfig } from './group'; import { builtinGroupToolbarConfig } from './group';
import { builtinImageToolbarConfig } from './image'; import { builtinImageToolbarConfig } from './image';
@@ -30,11 +29,6 @@ export const EdgelessElementToolbarExtension: ExtensionType[] = [
config: builtinConnectorToolbarConfig, config: builtinConnectorToolbarConfig,
}), }),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:embed'),
config: builtinEmbedToolbarConfig,
}),
ToolbarModuleExtension({ ToolbarModuleExtension({
id: BlockFlavourIdentifier('affine:surface:frame'), id: BlockFlavourIdentifier('affine:surface:frame'),
config: builtinFrameToolbarConfig, config: builtinFrameToolbarConfig,

View File

@@ -1,3 +1,4 @@
import type { GfxModel } from '@blocksuite/block-std/gfx';
import type { BlockModel } from '@blocksuite/store'; import type { BlockModel } from '@blocksuite/store';
import { EmbedFigmaModel } from './figma'; import { EmbedFigmaModel } from './figma';
@@ -34,7 +35,7 @@ export type LinkableEmbedModel = EmbedCardModel | EmbedIframeBlockModel;
export type BuiltInEmbedModel = EmbedCardModel | EmbedHtmlModel; export type BuiltInEmbedModel = EmbedCardModel | EmbedHtmlModel;
export function isExternalEmbedModel( export function isExternalEmbedModel(
model: BlockModel model: GfxModel | BlockModel
): model is InstanceType<ExternalEmbedModel> { ): model is InstanceType<ExternalEmbedModel> {
return ( return (
model instanceof EmbedFigmaModel || model instanceof EmbedFigmaModel ||
@@ -45,7 +46,7 @@ export function isExternalEmbedModel(
} }
export function isInternalEmbedModel( export function isInternalEmbedModel(
model: BlockModel model: GfxModel | BlockModel
): model is InstanceType<InternalEmbedModel> { ): model is InstanceType<InternalEmbedModel> {
return ( return (
model instanceof EmbedLinkedDocModel || model instanceof EmbedSyncedDocModel model instanceof EmbedLinkedDocModel || model instanceof EmbedSyncedDocModel

View File

@@ -128,8 +128,8 @@ abstract class ToolbarContextBase {
); );
} }
getSurfaceModelsByType<M extends abstract new (...args: any) => any>( getSurfaceModelsByType<T extends abstract new (...args: any) => any>(
klass: M klass: T
) { ) {
if (this.hasSelectedSurfaceModels) { if (this.hasSelectedSurfaceModels) {
const elements = this.elementsMap$.peek().get(this.flavour$.peek()); const elements = this.elementsMap$.peek().get(this.flavour$.peek());
@@ -140,6 +140,20 @@ abstract class ToolbarContextBase {
return []; return [];
} }
getSurfaceBlocksByType<T extends abstract new (...args: any) => any>(
klass: T
) {
if (this.hasSelectedSurfaceModels) {
const elements = this.elementsMap$.peek().get(this.flavour$.peek());
if (elements?.length) {
return elements
.map(model => this.gfx.view.get(model.id))
.filter(block => block && this.matchBlock(block, klass));
}
}
return [];
}
getCurrentBlockBy<T extends SelectionConstructor>(type: T) { getCurrentBlockBy<T extends SelectionConstructor>(type: T) {
const selection = this.selection.find(type); const selection = this.selection.find(type);
if (!selection) return null; if (!selection) return null;
@@ -160,17 +174,17 @@ abstract class ToolbarContextBase {
: this.getCurrentBlockBy(BlockSelection); : this.getCurrentBlockBy(BlockSelection);
} }
getCurrentBlockByType<K extends abstract new (...args: any) => any>( getCurrentBlockByType<T extends abstract new (...args: any) => any>(
klass: K klass: T
) { ) {
const block = this.getCurrentBlock(); const block = this.getCurrentBlock();
return this.matchBlock(block, klass) ? block : null; return this.matchBlock(block, klass) ? block : null;
} }
matchBlock<K extends abstract new (...args: any) => any>( matchBlock<T extends abstract new (...args: any) => any>(
component: GfxElementModelView | BlockComponent | null, component: GfxElementModelView | BlockComponent | null,
klass: K klass: T
): component is InstanceType<K> { ): component is InstanceType<T> {
return component instanceof klass; return component instanceof klass;
} }
@@ -184,23 +198,23 @@ abstract class ToolbarContextBase {
return this.store.getBlock(selection.blockId)?.model ?? null; return this.store.getBlock(selection.blockId)?.model ?? null;
} }
getCurrentModel() { getCurrentModel(): GfxModel | BlockModel | null {
return this.hasSelectedSurfaceModels return this.hasSelectedSurfaceModels
? this.getCurrentModelBy(SurfaceSelection) ? this.getCurrentModelBy(SurfaceSelection)
: this.getCurrentModelBy(BlockSelection); : this.getCurrentModelBy(BlockSelection);
} }
getCurrentModelByType<M extends abstract new (...args: any) => any>( getCurrentModelByType<T extends abstract new (...args: any) => any>(
klass: M klass: T
) { ) {
const model = this.getCurrentModel(); const model = this.getCurrentModel();
return this.matchModel(model, klass) ? model : null; return this.matchModel(model, klass) ? model : null;
} }
matchModel<K extends abstract new (...args: any) => any>( matchModel<T extends abstract new (...args: any) => any>(
model: GfxModel | BlockModel | null, model: GfxModel | BlockModel | null,
klass: K klass: T
): model is InstanceType<K> { ): model is InstanceType<T> {
return model instanceof klass; return model instanceof klass;
} }

View File

@@ -397,23 +397,30 @@ export class AffineToolbarWidget extends WidgetComponent {
// `card view` or `embed view` // `card view` or `embed view`
disposables.add( disposables.add(
std.view.viewUpdated.subscribe(record => { std.view.viewUpdated.subscribe(record => {
if (record.type !== 'block') return; const hasAddedBlock =
if (!flags.isBlock()) return; record.type === 'block' && record.method === 'add';
if (!hasAddedBlock) return;
const blockIds = std.selection if (flags.isBlock()) {
.filter$(BlockSelection) const blockIds = std.selection
.peek() .filter$(BlockSelection)
.map(s => s.blockId); .peek()
.map(s => s.blockId);
if (blockIds.includes(record.id)) {
batch(() => {
this.setReferenceElementWithBlocks(
blockIds
.map(id => std.view.getBlock(id))
.filter(block => block !== null)
);
flags.refresh(Flag.Block);
});
}
return;
}
if (record.method === 'add' && blockIds.includes(record.id)) { if (flags.isSurface()) {
batch(() => { flags.refresh(Flag.Surface);
this.setReferenceElementWithBlocks(
blockIds
.map(id => std.view.getBlock(id))
.filter(block => block !== null)
);
flags.refresh(Flag.Block);
});
return; return;
} }
}) })

View File

@@ -425,7 +425,6 @@ function createExternalLinkableToolbarConfig(
toast(ctx.host, 'Copied link to clipboard'); toast(ctx.host, 'Copied link to clipboard');
ctx.track('CopiedLink', { ctx.track('CopiedLink', {
module: 'toolbar',
category: matchModels(model, [BookmarkBlockModel]) category: matchModels(model, [BookmarkBlockModel])
? 'bookmark' ? 'bookmark'
: 'link', : 'link',
@@ -462,7 +461,6 @@ function createExternalLinkableToolbarConfig(
); );
ctx.track('OpenedAliasPopup', { ctx.track('OpenedAliasPopup', {
module: 'toolbar',
category: matchModels(model, [BookmarkBlockModel]) category: matchModels(model, [BookmarkBlockModel])
? 'bookmark' ? 'bookmark'
: 'link', : 'link',
@@ -600,9 +598,6 @@ const embedLinkedDocToolbarConfig = {
toast(ctx.host, 'Copied link to clipboard'); toast(ctx.host, 'Copied link to clipboard');
ctx.track('CopiedLink', { ctx.track('CopiedLink', {
segment: 'doc',
page: 'doc editor',
module: 'toolbar',
category: 'linked doc', category: 'linked doc',
type: 'card view', type: 'card view',
control: 'copy link', control: 'copy link',
@@ -648,9 +643,6 @@ const embedLinkedDocToolbarConfig = {
); );
ctx.track('OpenedAliasPopup', { ctx.track('OpenedAliasPopup', {
segment: 'doc',
page: 'doc editor',
module: 'toolbar',
category: 'linked doc', category: 'linked doc',
type: 'embed view', type: 'embed view',
control: 'edit', control: 'edit',
@@ -689,9 +681,6 @@ const embedSyncedDocToolbarConfig = {
toast(ctx.host, 'Copied link to clipboard'); toast(ctx.host, 'Copied link to clipboard');
ctx.track('CopiedLink', { ctx.track('CopiedLink', {
segment: 'doc',
page: 'doc editor',
module: 'toolbar',
category: 'linked doc', category: 'linked doc',
type: 'embed view', type: 'embed view',
control: 'copy link', control: 'copy link',
@@ -730,9 +719,6 @@ const embedSyncedDocToolbarConfig = {
); );
ctx.track('OpenedAliasPopup', { ctx.track('OpenedAliasPopup', {
segment: 'doc',
page: 'doc editor',
module: 'toolbar',
category: 'linked doc', category: 'linked doc',
type: 'embed view', type: 'embed view',
control: 'edit', control: 'edit',
@@ -846,9 +832,6 @@ const inlineReferenceToolbarConfig = {
toast(ctx.host, 'Copied link to clipboard'); toast(ctx.host, 'Copied link to clipboard');
ctx.track('CopiedLink', { ctx.track('CopiedLink', {
segment: 'doc',
page: 'doc editor',
module: 'toolbar',
category: 'linked doc', category: 'linked doc',
type: 'inline view', type: 'inline view',
control: 'copy link', control: 'copy link',
@@ -882,9 +865,6 @@ const inlineReferenceToolbarConfig = {
abortController.signal.onabort = () => popover.remove(); abortController.signal.onabort = () => popover.remove();
ctx.track('OpenedAliasPopup', { ctx.track('OpenedAliasPopup', {
segment: 'doc',
page: 'doc editor',
module: 'toolbar',
category: 'linked doc', category: 'linked doc',
type: 'inline view', type: 'inline view',
control: 'edit', control: 'edit',
@@ -992,21 +972,41 @@ export const createCustomToolbarExtension = (
config: createExternalLinkableToolbarConfig(EmbedFigmaBlockComponent), config: createExternalLinkableToolbarConfig(EmbedFigmaBlockComponent),
}), }),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:surface:embed-figma'),
config: createExternalLinkableToolbarConfig(BookmarkBlockComponent),
}),
ToolbarModuleExtension({ ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:embed-github'), id: BlockFlavourIdentifier('custom:affine:embed-github'),
config: createExternalLinkableToolbarConfig(EmbedGithubBlockComponent), config: createExternalLinkableToolbarConfig(EmbedGithubBlockComponent),
}), }),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:surface:embed-github'),
config: createExternalLinkableToolbarConfig(BookmarkBlockComponent),
}),
ToolbarModuleExtension({ ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:embed-loom'), id: BlockFlavourIdentifier('custom:affine:embed-loom'),
config: createExternalLinkableToolbarConfig(EmbedLoomBlockComponent), config: createExternalLinkableToolbarConfig(EmbedLoomBlockComponent),
}), }),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:surface:embed-loom'),
config: createExternalLinkableToolbarConfig(BookmarkBlockComponent),
}),
ToolbarModuleExtension({ ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:embed-youtube'), id: BlockFlavourIdentifier('custom:affine:embed-youtube'),
config: createExternalLinkableToolbarConfig(EmbedYoutubeBlockComponent), config: createExternalLinkableToolbarConfig(EmbedYoutubeBlockComponent),
}), }),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:surface:embed-youtube'),
config: createExternalLinkableToolbarConfig(BookmarkBlockComponent),
}),
ToolbarModuleExtension({ ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:embed-linked-doc'), id: BlockFlavourIdentifier('custom:affine:embed-linked-doc'),
config: embedLinkedDocToolbarConfig, config: embedLinkedDocToolbarConfig,