refactor(editor): improve query methods in toolbar context (#10714)

This commit is contained in:
fundon
2025-03-19 00:52:22 +00:00
parent 02d707feab
commit 3cce147c60
14 changed files with 317 additions and 359 deletions

View File

@@ -11,10 +11,8 @@ import {
ActionPlacement, ActionPlacement,
type ToolbarAction, type ToolbarAction,
type ToolbarActionGroup, type ToolbarActionGroup,
type ToolbarContext,
type ToolbarModuleConfig, type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { BlockSelection, SurfaceSelection } from '@blocksuite/block-std';
import { Bound } from '@blocksuite/global/gfx'; import { Bound } from '@blocksuite/global/gfx';
import { import {
CaptionIcon, CaptionIcon,
@@ -25,7 +23,6 @@ import {
EditIcon, EditIcon,
ResetIcon, ResetIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import type { SelectionConstructor } from '@blocksuite/store';
import { flip, offset } from '@floating-ui/dom'; import { flip, offset } from '@floating-ui/dom';
import { computed } from '@preact/signals-core'; import { computed } from '@preact/signals-core';
import { html } from 'lit'; import { html } from 'lit';
@@ -44,101 +41,95 @@ const trackBaseProps = {
type: 'card view', type: 'card view',
}; };
const createAttachmentViewDropdownMenuWith = <T extends SelectionConstructor>( export const attachmentViewDropdownMenu = {
t: T id: 'b.conversions',
) => { actions: [
return { {
id: 'b.conversions', id: 'card',
actions: [ label: 'Card view',
{ run(ctx) {
id: 'card', const model = ctx.getCurrentModelByType(AttachmentBlockModel);
label: 'Card view', if (!model) return;
run(ctx) {
const model = ctx.getCurrentModelByType(t, AttachmentBlockModel);
if (!model) return;
const style = defaultAttachmentProps.style!; const style = defaultAttachmentProps.style!;
const width = EMBED_CARD_WIDTH[style]; const width = EMBED_CARD_WIDTH[style];
const height = EMBED_CARD_HEIGHT[style]; const height = EMBED_CARD_HEIGHT[style];
const bound = Bound.deserialize(model.xywh); const bound = Bound.deserialize(model.xywh);
bound.w = width; bound.w = width;
bound.h = height; bound.h = height;
ctx.store.updateBlock(model, { ctx.store.updateBlock(model, {
style, style,
embed: false, embed: false,
xywh: bound.serialize(), xywh: bound.serialize(),
});
},
},
{
id: 'embed',
label: 'Embed view',
run(ctx) {
const model = ctx.getCurrentModelByType(t, AttachmentBlockModel);
if (!model) return;
// Clears
ctx.reset();
ctx.select('note');
ctx.std.get(AttachmentEmbedProvider).convertTo(model);
ctx.track('SelectedView', {
...trackBaseProps,
control: 'select view',
type: 'embed view',
});
},
},
],
content(ctx) {
const model = ctx.getCurrentModelByType(t, AttachmentBlockModel);
if (!model) return null;
const embedProvider = ctx.std.get(AttachmentEmbedProvider);
const actions = this.actions.map(action => ({ ...action }));
const viewType$ = computed(() => {
const [cardAction, embedAction] = actions;
const embed = model.props.embed$.value ?? false;
cardAction.disabled = !embed;
embedAction.disabled = embed && embedProvider.embedded(model);
return embed ? embedAction.label : cardAction.label;
});
const toggle = (e: CustomEvent<boolean>) => {
const opened = e.detail;
if (!opened) return;
ctx.track('OpenedViewSelector', {
...trackBaseProps,
control: 'switch view',
}); });
}; },
return html`${keyed(
model,
html`<affine-view-dropdown-menu
.actions=${actions}
.context=${ctx}
.toggle=${toggle}
.viewType$=${viewType$}
></affine-view-dropdown-menu>`
)}`;
}, },
} satisfies ToolbarActionGroup<ToolbarAction>; {
}; id: 'embed',
label: 'Embed view',
run(ctx) {
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
if (!model) return;
// Clears
ctx.reset();
ctx.select('note');
ctx.std.get(AttachmentEmbedProvider).convertTo(model);
ctx.track('SelectedView', {
...trackBaseProps,
control: 'select view',
type: 'embed view',
});
},
},
],
content(ctx) {
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
if (!model) return null;
const embedProvider = ctx.std.get(AttachmentEmbedProvider);
const actions = this.actions.map(action => ({ ...action }));
const viewType$ = computed(() => {
const [cardAction, embedAction] = actions;
const embed = model.props.embed$.value ?? false;
cardAction.disabled = !embed;
embedAction.disabled = embed && embedProvider.embedded(model);
return embed ? embedAction.label : cardAction.label;
});
const toggle = (e: CustomEvent<boolean>) => {
const opened = e.detail;
if (!opened) return;
ctx.track('OpenedViewSelector', {
...trackBaseProps,
control: 'switch view',
});
};
return html`${keyed(
model,
html`<affine-view-dropdown-menu
.actions=${actions}
.context=${ctx}
.toggle=${toggle}
.viewType$=${viewType$}
></affine-view-dropdown-menu>`
)}`;
},
} satisfies ToolbarActionGroup<ToolbarAction>;
export const builtinToolbarConfig = { export const builtinToolbarConfig = {
actions: [ actions: [
{ {
id: 'a.rename', id: 'a.rename',
content(cx) { content(cx) {
const component = cx.getCurrentBlockComponentBy( const block = cx.getCurrentBlockByType(AttachmentBlockComponent);
AttachmentBlockComponent if (!block) return null;
);
if (!component) return null;
const abortController = new AbortController(); const abortController = new AbortController();
abortController.signal.onabort = () => cx.show(); abortController.signal.onabort = () => cx.show();
@@ -152,12 +143,12 @@ export const builtinToolbarConfig = {
createLitPortal({ createLitPortal({
template: RenameModal({ template: RenameModal({
model: component.model, model: block.model,
editorHost: cx.host, editorHost: cx.host,
abortController, abortController,
}), }),
computePosition: { computePosition: {
referenceElement: component, referenceElement: block,
placement: 'top-start', placement: 'top-start',
middleware: [flip(), offset(4)], middleware: [flip(), offset(4)],
}, },
@@ -170,16 +161,14 @@ export const builtinToolbarConfig = {
`; `;
}, },
}, },
createAttachmentViewDropdownMenuWith(BlockSelection), attachmentViewDropdownMenu,
{ {
id: 'c.download', id: 'c.download',
tooltip: 'Download', tooltip: 'Download',
icon: DownloadIcon(), icon: DownloadIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
AttachmentBlockComponent block?.download();
);
component?.download();
}, },
}, },
{ {
@@ -187,10 +176,8 @@ export const builtinToolbarConfig = {
tooltip: 'Caption', tooltip: 'Caption',
icon: CaptionIcon(), icon: CaptionIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
AttachmentBlockComponent block?.captionEditor?.show();
);
component?.captionEditor?.show();
ctx.track('OpenedCaptionEditor', { ctx.track('OpenedCaptionEditor', {
...trackBaseProps, ...trackBaseProps,
@@ -208,10 +195,8 @@ export const builtinToolbarConfig = {
icon: CopyIcon(), icon: CopyIcon(),
run(ctx) { run(ctx) {
// TODO(@fundon): unify `clone` method // TODO(@fundon): unify `clone` method
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
AttachmentBlockComponent block?.copy();
);
component?.copy();
}, },
}, },
{ {
@@ -219,9 +204,7 @@ export const builtinToolbarConfig = {
label: 'Duplicate', label: 'Duplicate',
icon: DuplicateIcon(), icon: DuplicateIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentBlockComponentBy( const model = ctx.getCurrentModelByType(AttachmentBlockModel);
AttachmentBlockComponent
)?.model;
if (!model) return; if (!model) return;
// TODO(@fundon): unify `duplicate` method // TODO(@fundon): unify `duplicate` method
@@ -241,10 +224,8 @@ export const builtinToolbarConfig = {
label: 'Reload', label: 'Reload',
icon: ResetIcon(), icon: ResetIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
AttachmentBlockComponent block?.refreshData();
);
component?.refreshData();
}, },
}, },
{ {
@@ -257,7 +238,7 @@ export const builtinToolbarConfig = {
const model = ctx.getCurrentModel(); const model = ctx.getCurrentModel();
if (!model) return; if (!model) return;
ctx.store.deleteBlock(model); ctx.store.deleteBlock(model.id);
// Clears // Clears
ctx.select('note'); ctx.select('note');
@@ -266,7 +247,3 @@ export const builtinToolbarConfig = {
}, },
], ],
} as const satisfies ToolbarModuleConfig; } as const satisfies ToolbarModuleConfig;
export const attachmentViewDropdownMenu = (ctx: ToolbarContext) => {
return createAttachmentViewDropdownMenuWith(SurfaceSelection).content(ctx);
};

View File

@@ -39,10 +39,7 @@ export const builtinToolbarConfig = {
{ {
id: 'a.preview', id: 'a.preview',
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(BookmarkBlockModel);
BlockSelection,
BookmarkBlockModel
);
if (!model) return null; if (!model) return null;
const { url } = model.props; const { url } = model.props;
@@ -57,10 +54,7 @@ export const builtinToolbarConfig = {
id: 'inline', id: 'inline',
label: 'Inline view', label: 'Inline view',
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(BookmarkBlockModel);
BlockSelection,
BookmarkBlockModel
);
if (!model) return; if (!model) return;
const { title, caption, url } = model.props; const { title, caption, url } = model.props;
@@ -98,10 +92,7 @@ export const builtinToolbarConfig = {
id: 'embed', id: 'embed',
label: 'Embed view', label: 'Embed view',
disabled(ctx) { disabled(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(BookmarkBlockModel);
BlockSelection,
BookmarkBlockModel
);
if (!model) return true; if (!model) return true;
const url = model.props.url; const url = model.props.url;
@@ -121,10 +112,7 @@ export const builtinToolbarConfig = {
return !canEmbedAsIframe && options?.viewType !== 'embed'; return !canEmbedAsIframe && options?.viewType !== 'embed';
}, },
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(BookmarkBlockModel);
BlockSelection,
BookmarkBlockModel
);
if (!model) return; if (!model) return;
const { caption, url, style } = model.props; const { caption, url, style } = model.props;
@@ -189,10 +177,7 @@ export const builtinToolbarConfig = {
}, },
], ],
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(BookmarkBlockModel);
BlockSelection,
BookmarkBlockModel
);
if (!model) return null; if (!model) return null;
const actions = this.actions.map(action => ({ ...action })); const actions = this.actions.map(action => ({ ...action }));
@@ -230,10 +215,7 @@ export const builtinToolbarConfig = {
}, },
], ],
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(BookmarkBlockModel);
BlockSelection,
BookmarkBlockModel
);
if (!model) return null; if (!model) return null;
const actions = this.actions.map(action => ({ const actions = this.actions.map(action => ({
@@ -275,10 +257,8 @@ export const builtinToolbarConfig = {
tooltip: 'Caption', tooltip: 'Caption',
icon: CaptionIcon(), icon: CaptionIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(BookmarkBlockComponent);
BookmarkBlockComponent block?.captionEditor?.show();
);
component?.captionEditor?.show();
ctx.track('OpenedCaptionEditor', { ctx.track('OpenedCaptionEditor', {
...trackBaseProps, ...trackBaseProps,
@@ -295,7 +275,7 @@ export const builtinToolbarConfig = {
label: 'Copy', label: 'Copy',
icon: CopyIcon(), icon: CopyIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentModel(); const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return; if (!model) return;
const slice = Slice.fromModels(ctx.store, [model]); const slice = Slice.fromModels(ctx.store, [model]);
@@ -310,7 +290,7 @@ export const builtinToolbarConfig = {
label: 'Duplicate', label: 'Duplicate',
icon: DuplicateIcon(), icon: DuplicateIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentModel(); const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return; if (!model) return;
const { flavour, parent } = model; const { flavour, parent } = model;
@@ -328,10 +308,8 @@ export const builtinToolbarConfig = {
label: 'Reload', label: 'Reload',
icon: ResetIcon(), icon: ResetIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(BookmarkBlockComponent);
BookmarkBlockComponent block?.refreshData();
);
component?.refreshData();
}, },
}, },
{ {
@@ -341,7 +319,7 @@ export const builtinToolbarConfig = {
icon: DeleteIcon(), icon: DeleteIcon(),
variant: 'destructive', variant: 'destructive',
run(ctx) { run(ctx) {
const model = ctx.getCurrentModel(); const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return; if (!model) return;
ctx.store.deleteBlock(model); ctx.store.deleteBlock(model);

View File

@@ -52,7 +52,7 @@ export function createBuiltinToolbarConfigForExternal(
{ {
id: 'a.preview', id: 'a.preview',
content(ctx) { content(ctx) {
const model = ctx.getCurrentModel(); const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return null; if (!model || !isExternalEmbedModel(model)) return null;
const { url } = model.props; const { url } = model.props;
@@ -72,7 +72,7 @@ export function createBuiltinToolbarConfigForExternal(
id: 'inline', id: 'inline',
label: 'Inline view', label: 'Inline view',
run(ctx) { run(ctx) {
const model = ctx.getCurrentModel(); const model = ctx.getCurrentBlockByType(klass)?.model;
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 +105,7 @@ export function createBuiltinToolbarConfigForExternal(
id: 'card', id: 'card',
label: 'Card view', label: 'Card view',
disabled(ctx) { disabled(ctx) {
const model = ctx.getCurrentModel(); const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return true; if (!model || !isExternalEmbedModel(model)) return true;
const { url } = model.props; const { url } = model.props;
@@ -116,7 +116,7 @@ export function createBuiltinToolbarConfigForExternal(
return options?.viewType === 'card'; return options?.viewType === 'card';
}, },
run(ctx) { run(ctx) {
const model = ctx.getCurrentModel(); const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return; if (!model || !isExternalEmbedModel(model)) return;
const { url, caption } = model.props; const { url, caption } = model.props;
@@ -165,7 +165,7 @@ export function createBuiltinToolbarConfigForExternal(
id: 'embed', id: 'embed',
label: 'Embed view', label: 'Embed view',
disabled(ctx) { disabled(ctx) {
const model = ctx.getCurrentModel(); const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return false; if (!model || !isExternalEmbedModel(model)) return false;
const { url } = model.props; const { url } = model.props;
@@ -176,7 +176,7 @@ export function createBuiltinToolbarConfigForExternal(
return options?.viewType === 'embed'; return options?.viewType === 'embed';
}, },
when(ctx) { when(ctx) {
const model = ctx.getCurrentModel(); const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return false; if (!model || !isExternalEmbedModel(model)) return false;
const { url } = model.props; const { url } = model.props;
@@ -187,7 +187,7 @@ export function createBuiltinToolbarConfigForExternal(
return options?.viewType === 'embed'; return options?.viewType === 'embed';
}, },
run(ctx) { run(ctx) {
const model = ctx.getCurrentModel(); const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return; if (!model || !isExternalEmbedModel(model)) return;
const { url, caption } = model.props; const { url, caption } = model.props;
@@ -231,7 +231,7 @@ export function createBuiltinToolbarConfigForExternal(
}, },
], ],
content(ctx) { content(ctx) {
const model = ctx.getCurrentModel(); const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return null; if (!model || !isExternalEmbedModel(model)) return null;
const { url } = model.props; const { url } = model.props;
@@ -277,10 +277,7 @@ export function createBuiltinToolbarConfigForExternal(
}, },
], ],
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(EmbedGithubModel);
BlockSelection,
EmbedGithubModel
);
if (!model) return null; if (!model) return null;
const actions = this.actions.map(action => ({ const actions = this.actions.map(action => ({
@@ -322,10 +319,8 @@ export function createBuiltinToolbarConfigForExternal(
tooltip: 'Caption', tooltip: 'Caption',
icon: CaptionIcon(), icon: CaptionIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy(klass); const block = ctx.getCurrentBlockByType(klass);
if (!component) return; block?.captionEditor?.show();
component.captionEditor?.show();
ctx.track('OpenedCaptionEditor', { ctx.track('OpenedCaptionEditor', {
...trackBaseProps, ...trackBaseProps,
@@ -342,7 +337,7 @@ export function createBuiltinToolbarConfigForExternal(
label: 'Copy', label: 'Copy',
icon: CopyIcon(), icon: CopyIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return; if (!model || !isExternalEmbedModel(model)) return;
const slice = Slice.fromModels(ctx.store, [model]); const slice = Slice.fromModels(ctx.store, [model]);
@@ -357,7 +352,7 @@ export function createBuiltinToolbarConfigForExternal(
label: 'Duplicate', label: 'Duplicate',
icon: DuplicateIcon(), icon: DuplicateIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return; if (!model || !isExternalEmbedModel(model)) return;
const { flavour, parent } = model; const { flavour, parent } = model;
@@ -375,8 +370,8 @@ export function createBuiltinToolbarConfigForExternal(
label: 'Reload', label: 'Reload',
icon: ResetIcon(), icon: ResetIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy(klass); const block = ctx.getCurrentBlockByType(klass);
component?.refreshData(); block?.refreshData();
}, },
}, },
{ {
@@ -386,10 +381,10 @@ export function createBuiltinToolbarConfigForExternal(
icon: DeleteIcon(), icon: DeleteIcon(),
variant: 'destructive', variant: 'destructive',
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model || !isExternalEmbedModel(model)) return; if (!model || !isExternalEmbedModel(model)) return;
ctx.store.deleteBlock(model); ctx.store.deleteBlock(model.id);
// Clears // Clears
ctx.select('note'); ctx.select('note');

View File

@@ -7,7 +7,6 @@ import {
type ToolbarModuleConfig, type ToolbarModuleConfig,
} 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 { import {
CaptionIcon, CaptionIcon,
CopyIcon, CopyIcon,
@@ -36,10 +35,8 @@ export const builtinToolbarConfig = {
icon: ExpandFullIcon(), icon: ExpandFullIcon(),
tooltip: 'Open this doc', tooltip: 'Open this doc',
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(EmbedHtmlBlockComponent);
EmbedHtmlBlockComponent block?.open();
);
component?.open();
}, },
}, },
{ {
@@ -55,7 +52,7 @@ export const builtinToolbarConfig = {
}, },
], ],
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType(BlockSelection, EmbedHtmlModel); const model = ctx.getCurrentModelByType(EmbedHtmlModel);
if (!model) return null; if (!model) return null;
const actions = this.actions.map<ToolbarAction>(action => ({ const actions = this.actions.map<ToolbarAction>(action => ({
@@ -97,10 +94,8 @@ export const builtinToolbarConfig = {
tooltip: 'Caption', tooltip: 'Caption',
icon: CaptionIcon(), icon: CaptionIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(EmbedHtmlBlockComponent);
EmbedHtmlBlockComponent block?.captionEditor?.show();
);
component?.captionEditor?.show();
ctx.track('OpenedCaptionEditor', { ctx.track('OpenedCaptionEditor', {
...trackBaseProps, ...trackBaseProps,
@@ -117,7 +112,7 @@ export const builtinToolbarConfig = {
label: 'Copy', label: 'Copy',
icon: CopyIcon(), icon: CopyIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentModelByType(EmbedHtmlModel);
if (!model) return; if (!model) return;
const slice = Slice.fromModels(ctx.store, [model]); const slice = Slice.fromModels(ctx.store, [model]);
@@ -132,7 +127,7 @@ export const builtinToolbarConfig = {
label: 'Duplicate', label: 'Duplicate',
icon: DuplicateIcon(), icon: DuplicateIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentModelByType(EmbedHtmlModel);
if (!model) return; if (!model) return;
const { flavour, parent } = model; const { flavour, parent } = model;
@@ -151,7 +146,7 @@ export const builtinToolbarConfig = {
icon: DeleteIcon(), icon: DeleteIcon(),
variant: 'destructive', variant: 'destructive',
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentModelByType(EmbedHtmlModel);
if (!model) return; if (!model) return;
ctx.store.deleteBlock(model); ctx.store.deleteBlock(model);

View File

@@ -40,10 +40,7 @@ export const builtinToolbarConfig = {
id: 'inline', id: 'inline',
label: 'Inline view', label: 'Inline view',
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(EmbedIframeBlockModel);
BlockSelection,
EmbedIframeBlockModel
);
if (!model) return; if (!model) return;
const { title, caption, url } = model.props; const { title, caption, url } = model.props;
@@ -76,10 +73,7 @@ export const builtinToolbarConfig = {
id: 'card', id: 'card',
label: 'Card view', label: 'Card view',
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(EmbedIframeBlockModel);
BlockSelection,
EmbedIframeBlockModel
);
if (!model) return; if (!model) return;
const { url, caption } = model.props; const { url, caption } = model.props;
@@ -119,10 +113,7 @@ export const builtinToolbarConfig = {
}, },
], ],
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(EmbedIframeBlockModel);
BlockSelection,
EmbedIframeBlockModel
);
if (!model) return null; if (!model) return null;
const actions = this.actions.map(action => ({ ...action })); const actions = this.actions.map(action => ({ ...action }));
@@ -152,9 +143,7 @@ export const builtinToolbarConfig = {
tooltip: 'Caption', tooltip: 'Caption',
icon: CaptionIcon(), icon: CaptionIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const component = ctx.getCurrentBlockByType(EmbedIframeBlockComponent);
EmbedIframeBlockComponent
);
component?.captionEditor?.show(); component?.captionEditor?.show();
ctx.track('OpenedCaptionEditor', { ctx.track('OpenedCaptionEditor', {
@@ -172,7 +161,7 @@ export const builtinToolbarConfig = {
label: 'Copy', label: 'Copy',
icon: CopyIcon(), icon: CopyIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model; const model = ctx.getCurrentModelByType(EmbedIframeBlockModel);
if (!model) return; if (!model) return;
const slice = Slice.fromModels(ctx.store, [model]); const slice = Slice.fromModels(ctx.store, [model]);
@@ -187,7 +176,7 @@ export const builtinToolbarConfig = {
label: 'Duplicate', label: 'Duplicate',
icon: DuplicateIcon(), icon: DuplicateIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model; const model = ctx.getCurrentModelByType(EmbedIframeBlockModel);
if (!model) return; if (!model) return;
const { flavour, parent } = model; const { flavour, parent } = model;
@@ -205,9 +194,7 @@ export const builtinToolbarConfig = {
label: 'Reload', label: 'Reload',
icon: ResetIcon(), icon: ResetIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const component = ctx.getCurrentBlockByType(EmbedIframeBlockComponent);
EmbedIframeBlockComponent
);
component?.refreshData().catch(console.error); component?.refreshData().catch(console.error);
}, },
}, },
@@ -218,7 +205,7 @@ export const builtinToolbarConfig = {
icon: DeleteIcon(), icon: DeleteIcon(),
variant: 'destructive', variant: 'destructive',
run(ctx) { run(ctx) {
const model = ctx.getCurrentBlockBy(BlockSelection)?.model; const model = ctx.getCurrentModelByType(EmbedIframeBlockModel);
if (!model) return; if (!model) return;
ctx.store.deleteBlock(model); ctx.store.deleteBlock(model);

View File

@@ -10,7 +10,6 @@ import {
getBlockProps, getBlockProps,
referenceToNode, referenceToNode,
} from '@blocksuite/affine-shared/utils'; } from '@blocksuite/affine-shared/utils';
import { BlockSelection } from '@blocksuite/block-std';
import { import {
CaptionIcon, CaptionIcon,
CopyIcon, CopyIcon,
@@ -37,12 +36,10 @@ export const builtinToolbarConfig = {
{ {
id: 'a.doc-title', id: 'a.doc-title',
content(ctx) { content(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
EmbedLinkedDocBlockComponent if (!block) return null;
);
if (!component) return null;
const model = component.model; const model = block.model;
if (!model.props.title) return null; if (!model.props.title) return null;
const originalTitle = const originalTitle =
@@ -50,7 +47,7 @@ export const builtinToolbarConfig = {
return html`<affine-linked-doc-title return html`<affine-linked-doc-title
.title=${originalTitle} .title=${originalTitle}
.open=${(event: MouseEvent) => component.open({ event })} .open=${(event: MouseEvent) => block.open({ event })}
></affine-linked-doc-title>`; ></affine-linked-doc-title>`;
}, },
}, },
@@ -61,10 +58,10 @@ export const builtinToolbarConfig = {
id: 'inline', id: 'inline',
label: 'Inline view', label: 'Inline view',
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(
EmbedLinkedDocBlockComponent EmbedLinkedDocBlockComponent
); );
component?.covertToInline(); block?.covertToInline();
// Clears // Clears
ctx.select('note'); ctx.select('note');
@@ -86,14 +83,14 @@ export const builtinToolbarConfig = {
id: 'embed', id: 'embed',
label: 'Embed view', label: 'Embed view',
disabled(ctx) { disabled(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(
EmbedLinkedDocBlockComponent EmbedLinkedDocBlockComponent
); );
if (!component) return true; if (!block) return true;
if (component.closest('affine-embed-synced-doc-block')) return true; if (block.closest('affine-embed-synced-doc-block')) return true;
const model = component.model; const model = block.model;
// same doc // same doc
if (model.props.pageId === ctx.store.id) return true; if (model.props.pageId === ctx.store.id) return true;
@@ -104,10 +101,10 @@ export const builtinToolbarConfig = {
return false; return false;
}, },
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(
EmbedLinkedDocBlockComponent EmbedLinkedDocBlockComponent
); );
component?.convertToEmbed(); block?.convertToEmbed();
ctx.track('SelectedView', { ctx.track('SelectedView', {
...trackBaseProps, ...trackBaseProps,
@@ -118,10 +115,7 @@ export const builtinToolbarConfig = {
}, },
], ],
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
BlockSelection,
EmbedLinkedDocModel
);
if (!model) return null; if (!model) return null;
const actions = this.actions.map(action => ({ ...action })); const actions = this.actions.map(action => ({ ...action }));
@@ -159,10 +153,7 @@ export const builtinToolbarConfig = {
}, },
], ],
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
BlockSelection,
EmbedLinkedDocModel
);
if (!model) return null; if (!model) return null;
const actions = this.actions.map(action => ({ const actions = this.actions.map(action => ({
@@ -203,10 +194,8 @@ export const builtinToolbarConfig = {
tooltip: 'Caption', tooltip: 'Caption',
icon: CaptionIcon(), icon: CaptionIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
EmbedLinkedDocBlockComponent block?.captionEditor?.show();
);
component?.captionEditor?.show();
ctx.track('OpenedCaptionEditor', { ctx.track('OpenedCaptionEditor', {
...trackBaseProps, ...trackBaseProps,
@@ -223,7 +212,7 @@ export const builtinToolbarConfig = {
label: 'Copy', label: 'Copy',
icon: CopyIcon(), icon: CopyIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
if (!model) return; if (!model) return;
const slice = Slice.fromModels(ctx.store, [model]); const slice = Slice.fromModels(ctx.store, [model]);
@@ -238,7 +227,7 @@ export const builtinToolbarConfig = {
label: 'Duplicate', label: 'Duplicate',
icon: DuplicateIcon(), icon: DuplicateIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
if (!model) return; if (!model) return;
const { flavour, parent } = model; const { flavour, parent } = model;
@@ -257,7 +246,7 @@ export const builtinToolbarConfig = {
icon: DeleteIcon(), icon: DeleteIcon(),
variant: 'destructive', variant: 'destructive',
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
if (!model) return; if (!model) return;
ctx.store.deleteBlock(model); ctx.store.deleteBlock(model);

View File

@@ -9,7 +9,6 @@ import {
type ToolbarModuleConfig, type ToolbarModuleConfig,
} 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 { import {
ArrowDownSmallIcon, ArrowDownSmallIcon,
CaptionIcon, CaptionIcon,
@@ -49,10 +48,8 @@ export const builtinToolbarConfig = {
}, },
], ],
content(ctx) { content(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent);
EmbedSyncedDocBlockComponent if (!block) return null;
);
if (!component) return null;
const actions = this.actions const actions = this.actions
.map<ToolbarAction>(action => { .map<ToolbarAction>(action => {
@@ -64,11 +61,11 @@ export const builtinToolbarConfig = {
return { return {
...action, ...action,
disabled: shouldOpenInActiveView disabled: shouldOpenInActiveView
? component.model.props.pageId === ctx.store.id ? block.model.props.pageId === ctx.store.id
: false, : false,
when: allowed, when: allowed,
run: (_ctx: ToolbarContext) => run: (_ctx: ToolbarContext) =>
component.open({ block.open({
openMode: action.id as OpenDocMode, openMode: action.id as OpenDocMode,
}), }),
}; };
@@ -115,10 +112,10 @@ export const builtinToolbarConfig = {
id: 'inline', id: 'inline',
label: 'Inline view', label: 'Inline view',
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(
EmbedSyncedDocBlockComponent EmbedSyncedDocBlockComponent
); );
component?.covertToInline(); block?.covertToInline();
// Clears // Clears
ctx.select('note'); ctx.select('note');
@@ -135,10 +132,10 @@ export const builtinToolbarConfig = {
id: 'card', id: 'card',
label: 'Card view', label: 'Card view',
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(
EmbedSyncedDocBlockComponent EmbedSyncedDocBlockComponent
); );
component?.convertToCard(); block?.convertToCard();
ctx.track('SelectedView', { ctx.track('SelectedView', {
...trackBaseProps, ...trackBaseProps,
@@ -154,10 +151,7 @@ export const builtinToolbarConfig = {
}, },
], ],
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(EmbedSyncedDocModel);
BlockSelection,
EmbedSyncedDocModel
);
if (!model) return null; if (!model) return null;
const actions = this.actions.map(action => ({ ...action })); const actions = this.actions.map(action => ({ ...action }));
@@ -188,10 +182,8 @@ export const builtinToolbarConfig = {
tooltip: 'Caption', tooltip: 'Caption',
icon: CaptionIcon(), icon: CaptionIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent);
EmbedSyncedDocBlockComponent block?.captionEditor?.show();
);
component?.captionEditor?.show();
ctx.track('OpenedCaptionEditor', { ctx.track('OpenedCaptionEditor', {
...trackBaseProps, ...trackBaseProps,
control: 'add caption', control: 'add caption',
@@ -207,7 +199,7 @@ export const builtinToolbarConfig = {
label: 'Copy', label: 'Copy',
icon: CopyIcon(), icon: CopyIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentModelByType(EmbedSyncedDocModel);
if (!model) return; if (!model) return;
const slice = Slice.fromModels(ctx.store, [model]); const slice = Slice.fromModels(ctx.store, [model]);
@@ -222,7 +214,7 @@ export const builtinToolbarConfig = {
label: 'Duplicate', label: 'Duplicate',
icon: DuplicateIcon(), icon: DuplicateIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentModelByType(EmbedSyncedDocModel);
if (!model) return; if (!model) return;
const { flavour, parent } = model; const { flavour, parent } = model;
@@ -241,7 +233,7 @@ export const builtinToolbarConfig = {
icon: DeleteIcon(), icon: DeleteIcon(),
variant: 'destructive', variant: 'destructive',
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentModelByType(EmbedSyncedDocModel);
if (!model) return; if (!model) return;
ctx.store.deleteBlock(model); ctx.store.deleteBlock(model);

View File

@@ -109,7 +109,7 @@ export class EdgelessChangeAttachmentButton extends WithDisposable(LitElement) {
`, `,
// TODO(@fundon): should remove it when refactoring the element toolbar // TODO(@fundon): should remove it when refactoring the element toolbar
attachmentViewDropdownMenu(new ToolbarContext(this.std)), attachmentViewDropdownMenu.content(new ToolbarContext(this.std)),
html` html`
<editor-icon-button <editor-icon-button

View File

@@ -1,9 +1,12 @@
import type { Placement } from '@floating-ui/dom'; import type { Placement } from '@floating-ui/dom';
import type { ToolbarActions } from './action'; import type { ToolbarActions } from './action';
import type { ToolbarContext } from './context';
export type ToolbarModuleConfig = { export type ToolbarModuleConfig = {
actions: ToolbarActions; actions: ToolbarActions;
when?: ((ctx: ToolbarContext) => boolean) | boolean;
placement?: Extract<Placement, 'top' | 'top-start'>; placement?: Extract<Placement, 'top' | 'top-start'>;
}; };

View File

@@ -4,15 +4,18 @@ import {
type BlockStdScope, type BlockStdScope,
SurfaceSelection, SurfaceSelection,
} from '@blocksuite/block-std'; } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; import {
GfxControllerIdentifier,
type GfxElementModelView,
type GfxModel,
} from '@blocksuite/block-std/gfx';
import { nextTick } from '@blocksuite/global/utils'; import { nextTick } from '@blocksuite/global/utils';
import type { import type {
BaseSelection, BaseSelection,
Block, BlockModel,
SelectionConstructor, SelectionConstructor,
} from '@blocksuite/store'; } from '@blocksuite/store';
import { matchModels } from '../../utils';
import { DocModeProvider } from '../doc-mode-service'; import { DocModeProvider } from '../doc-mode-service';
import { TelemetryProvider, type TelemetryService } from '../telemetry-service'; import { TelemetryProvider, type TelemetryService } from '../telemetry-service';
import { ThemeProvider } from '../theme-service'; import { ThemeProvider } from '../theme-service';
@@ -106,60 +109,99 @@ abstract class ToolbarContextBase {
return this.toolbarRegistry.flags; return this.toolbarRegistry.flags;
} }
get flavour$() {
return this.toolbarRegistry.flavour$;
}
get message$() { get message$() {
return this.toolbarRegistry.message$; return this.toolbarRegistry.message$;
} }
getCurrentElement() { get elementsMap$() {
const selection = this.selection.find(SurfaceSelection); return this.toolbarRegistry.elementsMap$;
return selection?.elements.length
? this.gfx.getElementById(selection.elements[0])
: null;
} }
getCurrentBlock(): Block | null { get hasSelectedSurfaceModels() {
return this.getCurrentBlockBy(); return (
this.flavour$.peek().includes('surface') &&
this.elementsMap$.peek().size > 0
);
} }
getCurrentBlockComponent(): BlockComponent | null { getSurfaceElementsByType<M extends abstract new (...args: any) => any>(
klass: M
) {
if (this.hasSelectedSurfaceModels) {
const elements = this.elementsMap$.peek().get(this.flavour$.peek());
if (elements?.length) {
return elements.filter(e => this.matchModel(e, klass));
}
}
return [];
}
getCurrentBlockBy<T extends SelectionConstructor>(type: T) {
const selection = this.selection.find(type);
if (!selection) return null;
if (selection.is(SurfaceSelection)) {
const elementId = selection.elements[0];
const model = this.gfx.getElementById(elementId);
if (!model) return null;
return this.gfx.view.get(model.id) ?? null;
}
const model = this.store.getBlock(selection.blockId);
if (!model) return null;
return this.view.getBlock(model.id);
}
getCurrentBlock() {
return this.hasSelectedSurfaceModels
? this.getCurrentBlockBy(SurfaceSelection)
: this.getCurrentBlockBy(BlockSelection);
}
getCurrentBlockByType<K extends abstract new (...args: any) => any>(
klass: K
) {
const block = this.getCurrentBlock(); const block = this.getCurrentBlock();
return block && this.view.getBlock(block.id); return this.matchBlock(block, klass) ? block : null;
} }
getCurrentModel() { matchBlock<K extends abstract new (...args: any) => any>(
return this.getCurrentBlock()?.model ?? null; component: GfxElementModelView | BlockComponent | null,
} klass: K
): component is InstanceType<K> {
getCurrentBlockBy<T extends SelectionConstructor>(type?: T): Block | null { return component instanceof klass;
const selection = this.selection.find(type ?? BlockSelection);
return (selection && this.store.getBlock(selection.blockId)) ?? null;
} }
getCurrentModelBy<T extends SelectionConstructor>(type: T) { getCurrentModelBy<T extends SelectionConstructor>(type: T) {
return this.getCurrentBlockBy<T>(type)?.model ?? null; const selection = this.selection.find(type);
if (!selection) return null;
if (selection.is(SurfaceSelection)) {
const elementId = selection.elements[0];
return elementId ? this.gfx.getElementById(elementId) : null;
}
return this.store.getBlock(selection.blockId)?.model ?? null;
} }
getCurrentModelByType< getCurrentModel() {
T extends SelectionConstructor, return this.hasSelectedSurfaceModels
M extends Parameters<typeof matchModels>[1][number], ? this.getCurrentModelBy(SurfaceSelection)
>(type: T, klass: M) { : this.getCurrentModelBy(BlockSelection);
const model = this.getCurrentModelBy(type);
return matchModels(model, [klass]) ? model : null;
} }
getCurrentBlockComponentBy<K extends abstract new (...args: any) => any>( getCurrentModelByType<M extends abstract new (...args: any) => any>(
klass: M
) {
const model = this.getCurrentModel();
return this.matchModel(model, klass) ? model : null;
}
matchModel<K extends abstract new (...args: any) => any>(
model: GfxModel | BlockModel | null,
klass: K klass: K
): InstanceType<K> | null { ): model is InstanceType<K> {
const block = this.getCurrentBlockBy(); return model instanceof klass;
const component = block && this.view.getBlock(block.id);
return this.blockComponentIs(component, klass) ? component : null;
}
blockComponentIs<K extends abstract new (...args: any) => any>(
component: BlockComponent | null,
...classes: K[]
): component is InstanceType<K> {
return classes.some(k => component instanceof k);
} }
select(group: string, selections: BaseSelection[] = []) { select(group: string, selections: BaseSelection[] = []) {

View File

@@ -1,4 +1,5 @@
import { type BlockStdScope, StdIdentifier } from '@blocksuite/block-std'; import { type BlockStdScope, StdIdentifier } from '@blocksuite/block-std';
import type { GfxModel } from '@blocksuite/block-std/gfx';
import { type Container, createIdentifier } from '@blocksuite/global/di'; import { type Container, createIdentifier } from '@blocksuite/global/di';
import { Extension, type ExtensionType } from '@blocksuite/store'; import { Extension, type ExtensionType } from '@blocksuite/store';
import { signal } from '@preact/signals-core'; import { signal } from '@preact/signals-core';
@@ -22,12 +23,16 @@ export function ToolbarModuleExtension(module: ToolbarModule): ExtensionType {
} }
export class ToolbarRegistryExtension extends Extension { export class ToolbarRegistryExtension extends Extension {
flavour$ = signal<string>('affine:note');
message$ = signal<{ message$ = signal<{
flavour: string; flavour: string;
element: Element; element: Element;
setFloating: (element?: Element) => void; setFloating: (element?: Element) => void;
} | null>(null); } | null>(null);
elementsMap$ = signal<Map<string, GfxModel[]>>(new Map());
flags = new Flags(); flags = new Flags();
constructor(readonly std: BlockStdScope) { constructor(readonly std: BlockStdScope) {

View File

@@ -72,8 +72,6 @@ export class AffineToolbarWidget extends WidgetComponent {
} }
`; `;
flavour$ = signal('affine:note');
placement$ = signal<Placement>('top'); placement$ = signal<Placement>('top');
sideOptions$ = signal<Partial<SideObject> | null>(null); sideOptions$ = signal<Partial<SideObject> | null>(null);
@@ -139,7 +137,6 @@ export class AffineToolbarWidget extends WidgetComponent {
super.connectedCallback(); super.connectedCallback();
const { const {
flavour$,
placement$, placement$,
sideOptions$, sideOptions$,
referenceElement$, referenceElement$,
@@ -149,7 +146,7 @@ export class AffineToolbarWidget extends WidgetComponent {
host, host,
std, std,
} = this; } = this;
const { flags, message$ } = toolbarRegistry; const { flags, elementsMap$, flavour$, message$ } = toolbarRegistry;
const context = new ToolbarContext(std); const context = new ToolbarContext(std);
// TODO(@fundon): fix toolbar position shaking when the wheel scrolls // TODO(@fundon): fix toolbar position shaking when the wheel scrolls
@@ -303,6 +300,7 @@ export class AffineToolbarWidget extends WidgetComponent {
let elements: GfxModel[] = []; let elements: GfxModel[] = [];
let hasLocked = false; let hasLocked = false;
let sideOptions = null; let sideOptions = null;
let paired: [string, GfxModel[]][] = [];
if (activated && surface) { if (activated && surface) {
elements = elementIds elements = elementIds
@@ -326,7 +324,10 @@ export class AffineToolbarWidget extends WidgetComponent {
e => e.flavour e => e.flavour
); );
const paired = toPairs(grouped); paired = toPairs(grouped).map(([flavour, items]) => [
flavour,
items.map(({ model }) => model),
]);
if (paired.length === 1) { if (paired.length === 1) {
flavour = paired[0][0]; flavour = paired[0][0];
@@ -350,6 +351,8 @@ export class AffineToolbarWidget extends WidgetComponent {
batch(() => { batch(() => {
flags.toggle(Flag.Surface, activated); flags.toggle(Flag.Surface, activated);
elementsMap$.value = new Map(paired);
if (!activated || !flavour) return; if (!activated || !flavour) return;
this.setReferenceElementWithElements(gfx, elements); this.setReferenceElementWithElements(gfx, elements);

View File

@@ -42,7 +42,7 @@ export const sideMap = new Map([
['affine:surface:frame', { top: 28 }], ['affine:surface:frame', { top: 28 }],
// includes group element // includes group element
['affine:surface:group', { top: 20 }], ['affine:surface:group', { top: 20 }],
// only one shape element // has only one shape element
['affine:surface:shape', { top: 26, bottom: -26 }], ['affine:surface:shape', { top: 26, bottom: -26 }],
]); ]);
@@ -196,6 +196,11 @@ export function renderToolbar(
.flat() .flat()
.map(key => toolbarRegistry.modules.get(key)) .map(key => toolbarRegistry.modules.get(key))
.filter(module => !!module) .filter(module => !!module)
.filter(module =>
typeof module.config.when === 'function'
? module.config.when(context)
: (module.config.when ?? true)
)
.map<ToolbarActions>(module => module.config.actions) .map<ToolbarActions>(module => module.config.actions)
.flat(); .flat();
@@ -220,7 +225,7 @@ export function renderToolbar(
); );
// if (moreMenuItems.length) { // if (moreMenuItems.length) {
// TODO(@fundon): edgeless case needs to be considered // TODO(@fundon): edgeless case needs to be considered
const key = `${context.getCurrentModel()?.id ?? context.getCurrentElement()?.id}`; const key = `${context.getCurrentModel()?.id}`;
primaryActionGroup.push({ primaryActionGroup.push({
id: 'more', id: 'more',

View File

@@ -313,33 +313,34 @@ function createToolbarMoreMenuConfigV2(baseUrl?: string) {
actions: [ actions: [
{ {
id: 'block-meta-display', id: 'block-meta-display',
when: cx => { when: ctx => {
const featureFlag = cx.std.get(FeatureFlagService); const featureFlag = ctx.std.get(FeatureFlagService);
const isEnabled = featureFlag.getFlag('enable_block_meta'); const isEnabled = featureFlag.getFlag('enable_block_meta');
if (!isEnabled) return false; if (!isEnabled) return false;
// only display when one block is selected by block selection // only display when one block is selected by block selection
const { selection, getCurrentBlockBy } = cx;
const hasBlockSelection = const hasBlockSelection =
selection.filter(BlockSelection).length === 1; ctx.selection.filter(BlockSelection).length === 1;
if (!hasBlockSelection) return false; if (!hasBlockSelection) return false;
const block = getCurrentBlockBy.call(cx, BlockSelection); const model = ctx.getCurrentModelBy(BlockSelection);
if (!block) return false; if (!model) return false;
const createdAt = 'meta:createdAt'; const createdAt = 'meta:createdAt';
const createdBy = 'meta:createdBy'; const createdBy = 'meta:createdBy';
return ( return (
createdAt in block.model.props && 'props' in model &&
block.model.props[createdAt] !== undefined && createdAt in model.props &&
createdBy in block.model.props && model.props[createdAt] !== undefined &&
block.model.props[createdBy] !== undefined && createdBy in model.props &&
typeof block.model.props[createdBy] === 'string' && model.props[createdBy] !== undefined &&
typeof block.model.props[createdAt] === 'number' typeof model.props[createdBy] === 'string' &&
typeof model.props[createdAt] === 'number'
); );
}, },
content: cx => { content: ctx => {
const model = cx.getCurrentModelBy(BlockSelection); const model = ctx.getCurrentModelBy(BlockSelection);
if (!model) return null; if (!model) return null;
if (!('props' in model)) return null;
const createdAt = 'meta:createdAt'; const createdAt = 'meta:createdAt';
if (!(createdAt in model.props)) return null; if (!(createdAt in model.props)) return null;
const createdBy = 'meta:createdBy'; const createdBy = 'meta:createdBy';
@@ -347,7 +348,7 @@ function createToolbarMoreMenuConfigV2(baseUrl?: string) {
const createdByUserId = model.props[createdBy] as string; const createdByUserId = model.props[createdBy] as string;
const createdAtTimestamp = model.props[createdAt] as number; const createdAtTimestamp = model.props[createdAt] as number;
const date = new Date(createdAtTimestamp); const date = new Date(createdAtTimestamp);
const userProvider = cx.std.get(UserProvider); const userProvider = ctx.std.get(UserProvider);
userProvider.revalidateUserInfo(createdByUserId); userProvider.revalidateUserInfo(createdByUserId);
const userSignal = userProvider.userInfo$(createdByUserId); const userSignal = userProvider.userInfo$(createdByUserId);
const isLoadingSignal = userProvider.isLoading$(createdByUserId); const isLoadingSignal = userProvider.isLoading$(createdByUserId);
@@ -415,7 +416,7 @@ function createExternalLinkableToolbarConfig(
tooltip: 'Copy link', tooltip: 'Copy link',
icon: CopyIcon(), icon: CopyIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentBlockComponentBy(klass)?.model; const model = ctx.getCurrentBlockByType(klass)?.model;
if (!model) return; if (!model) return;
const { url } = model.props; const { url } = model.props;
@@ -440,12 +441,12 @@ function createExternalLinkableToolbarConfig(
tooltip: 'Edit', tooltip: 'Edit',
icon: EditIcon(), icon: EditIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy(klass); const block = ctx.getCurrentBlockByType(klass);
if (!component) return; if (!block) return;
ctx.hide(); ctx.hide();
const model = component.model; const model = block.model;
const abortController = new AbortController(); const abortController = new AbortController();
abortController.signal.onabort = () => ctx.show(); abortController.signal.onabort = () => ctx.show();
@@ -457,7 +458,7 @@ function createExternalLinkableToolbarConfig(
undefined, undefined,
(_std, _component, props) => { (_std, _component, props) => {
ctx.store.updateBlock(model, props); ctx.store.updateBlock(model, props);
component.requestUpdate(); block.requestUpdate();
}, },
abortController abortController
); );
@@ -514,8 +515,8 @@ function createOpenDocActionGroup(
id: 'A.open-doc', id: 'A.open-doc',
actions: openDocActions, actions: openDocActions,
content(ctx) { content(ctx) {
const component = ctx.getCurrentBlockComponentBy(klass); const block = ctx.getCurrentBlockByType(klass);
if (!component) return null; if (!block) return null;
const actions = this.actions const actions = this.actions
.map<ToolbarAction>(action => { .map<ToolbarAction>(action => {
@@ -528,15 +529,14 @@ function createOpenDocActionGroup(
return { return {
...action, ...action,
disabled: shouldOpenInActiveView disabled: shouldOpenInActiveView
? component.model.props.pageId === ctx.store.id ? block.model.props.pageId === ctx.store.id
: false, : false,
when: when:
allowed && allowed && (shouldOpenInCenterPeek ? isPeekable(block) : true),
(shouldOpenInCenterPeek ? isPeekable(component) : true),
run: shouldOpenInCenterPeek run: shouldOpenInCenterPeek
? (_ctx: ToolbarContext) => peek(component) ? (_ctx: ToolbarContext) => peek(block)
: (_ctx: ToolbarContext) => : (_ctx: ToolbarContext) =>
component.open({ block.open({
openMode: action.id as OpenDocMode, openMode: action.id as OpenDocMode,
}), }),
}; };
@@ -589,10 +589,7 @@ const embedLinkedDocToolbarConfig = {
tooltip: 'Copy link', tooltip: 'Copy link',
icon: CopyIcon(), icon: CopyIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
BlockSelection,
EmbedLinkedDocModel
);
if (!model) return; if (!model) return;
const { pageId, params } = model.props; const { pageId, params } = model.props;
@@ -621,21 +618,21 @@ const embedLinkedDocToolbarConfig = {
tooltip: 'Edit', tooltip: 'Edit',
icon: EditIcon(), icon: EditIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(
EmbedLinkedDocBlockComponent EmbedLinkedDocBlockComponent
); );
if (!component) return; if (!block) return;
ctx.hide(); ctx.hide();
const model = component.model; const model = block.model;
const doc = ctx.workspace.getDoc(model.props.pageId); const doc = ctx.workspace.getDoc(model.props.pageId);
const abortController = new AbortController(); const abortController = new AbortController();
abortController.signal.onabort = () => ctx.show(); abortController.signal.onabort = () => ctx.show();
toggleEmbedCardEditModal( toggleEmbedCardEditModal(
ctx.host, ctx.host,
component.model, model,
'card', 'card',
doc doc
? { ? {
@@ -644,12 +641,12 @@ const embedLinkedDocToolbarConfig = {
} }
: undefined, : undefined,
std => { std => {
component.refreshData(); block.refreshData();
notifyLinkedDocClearedAliases(std); notifyLinkedDocClearedAliases(std);
}, },
(_std, _component, props) => { (_std, _component, props) => {
ctx.store.updateBlock(model, props); ctx.store.updateBlock(model, props);
component.requestUpdate(); block.requestUpdate();
}, },
abortController abortController
); );
@@ -681,10 +678,7 @@ const embedSyncedDocToolbarConfig = {
tooltip: 'Copy link', tooltip: 'Copy link',
icon: CopyIcon(), icon: CopyIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentModelByType( const model = ctx.getCurrentModelByType(EmbedSyncedDocModel);
BlockSelection,
EmbedSyncedDocModel
);
if (!model) return; if (!model) return;
const { pageId, params } = model.props; const { pageId, params } = model.props;
@@ -713,14 +707,14 @@ const embedSyncedDocToolbarConfig = {
tooltip: 'Edit', tooltip: 'Edit',
icon: EditIcon(), icon: EditIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const block = ctx.getCurrentBlockByType(
EmbedSyncedDocBlockComponent EmbedSyncedDocBlockComponent
); );
if (!component) return; if (!block) return;
ctx.hide(); ctx.hide();
const model = component.model; const model = block.model;
const doc = ctx.workspace.getDoc(model.props.pageId); const doc = ctx.workspace.getDoc(model.props.pageId);
const abortController = new AbortController(); const abortController = new AbortController();
abortController.signal.onabort = () => ctx.show(); abortController.signal.onabort = () => ctx.show();
@@ -732,7 +726,7 @@ const embedSyncedDocToolbarConfig = {
doc ? { title: doc.meta?.title } : undefined, doc ? { title: doc.meta?.title } : undefined,
undefined, undefined,
(std, _component, props) => { (std, _component, props) => {
component.convertToCard(props); block.convertToCard(props);
notifyLinkedDocSwitchedToCard(std); notifyLinkedDocSwitchedToCard(std);
}, },
@@ -761,8 +755,7 @@ const inlineReferenceToolbarConfig = {
id: 'A.open-doc', id: 'A.open-doc',
actions: openDocActions, actions: openDocActions,
content(ctx) { content(ctx) {
const registry = ctx.toolbarRegistry; const target = ctx.message$.peek()?.element;
const target = registry.message$.peek()?.element;
if (!(target instanceof AffineReference)) return null; if (!(target instanceof AffineReference)) return null;
const actions = this.actions const actions = this.actions
@@ -917,7 +910,7 @@ const embedIframeToolbarConfig = {
tooltip: 'Copy link', tooltip: 'Copy link',
icon: CopyIcon(), icon: CopyIcon(),
run(ctx) { run(ctx) {
const model = ctx.getCurrentBlockComponentBy( const model = ctx.getCurrentBlockByType(
EmbedIframeBlockComponent EmbedIframeBlockComponent
)?.model; )?.model;
if (!model) return; if (!model) return;
@@ -928,9 +921,6 @@ const embedIframeToolbarConfig = {
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: matchModels(model, [BookmarkBlockModel]) category: matchModels(model, [BookmarkBlockModel])
? 'bookmark' ? 'bookmark'
: 'link', : 'link',
@@ -944,7 +934,7 @@ const embedIframeToolbarConfig = {
tooltip: 'Edit', tooltip: 'Edit',
icon: EditIcon(), icon: EditIcon(),
run(ctx) { run(ctx) {
const component = ctx.getCurrentBlockComponentBy( const component = ctx.getCurrentBlockByType(
EmbedIframeBlockComponent EmbedIframeBlockComponent
); );
if (!component) return; if (!component) return;
@@ -969,9 +959,6 @@ const embedIframeToolbarConfig = {
); );
ctx.track('OpenedAliasPopup', { ctx.track('OpenedAliasPopup', {
segment: 'doc',
page: 'doc editor',
module: 'toolbar',
category: matchModels(model, [BookmarkBlockModel]) category: matchModels(model, [BookmarkBlockModel])
? 'bookmark' ? 'bookmark'
: 'link', : 'link',