refactor(editor): edgeless internal embed card toolbar config extension (#10717)

This commit is contained in:
fundon
2025-03-19 12:34:17 +00:00
parent ddd6c97b08
commit e686a6aecc
11 changed files with 621 additions and 315 deletions

View File

@@ -1,145 +1,249 @@
import { toast } from '@blocksuite/affine-components/toast'; import { toast } from '@blocksuite/affine-components/toast';
import { EmbedLinkedDocModel } from '@blocksuite/affine-model'; import {
type EmbedCardStyle,
EmbedLinkedDocModel,
EmbedLinkedDocStyles,
} from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { import {
ActionPlacement, ActionPlacement,
type LinkEventType,
type OpenDocMode,
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 { import {
getBlockProps, getBlockProps,
referenceToNode, referenceToNode,
} from '@blocksuite/affine-shared/utils'; } from '@blocksuite/affine-shared/utils';
import { BlockFlavourIdentifier } from '@blocksuite/block-std';
import { Bound } from '@blocksuite/global/gfx';
import { import {
ArrowDownSmallIcon,
CaptionIcon, CaptionIcon,
CopyIcon, CopyIcon,
DeleteIcon, DeleteIcon,
DuplicateIcon, DuplicateIcon,
ExpandFullIcon,
OpenInNewIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { Slice } from '@blocksuite/store'; import { type ExtensionType, Slice } 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 { ifDefined } from 'lit/directives/if-defined.js';
import { keyed } from 'lit/directives/keyed.js'; import { keyed } from 'lit/directives/keyed.js';
import { repeat } from 'lit/directives/repeat.js';
import { EmbedLinkedDocBlockComponent } from '../embed-linked-doc-block'; import { EmbedLinkedDocBlockComponent } from '../embed-linked-doc-block';
const trackBaseProps = { const trackBaseProps = {
segment: 'doc',
page: 'doc editor',
module: 'toolbar',
category: 'linked doc', category: 'linked doc',
type: 'card view', type: 'card view',
}; };
export const builtinToolbarConfig = { 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 });
};
const docTitleAction = {
id: 'a.doc-title',
content(ctx) {
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
if (!block) return null;
const model = block.model;
if (!model.props.title) return null;
const originalTitle =
ctx.workspace.getDoc(model.props.pageId)?.meta?.title || 'Untitled';
return html`<affine-linked-doc-title
.title=${originalTitle}
.open=${(event: MouseEvent) => block.open({ event })}
></affine-linked-doc-title>`;
},
} as const satisfies ToolbarAction;
const captionAction = {
id: 'd.caption',
tooltip: 'Caption',
icon: CaptionIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
block?.captionEditor?.show();
ctx.track('OpenedCaptionEditor', {
...trackBaseProps,
control: 'add caption',
});
},
} as const satisfies ToolbarAction;
const openDocActions = [
{
mode: 'open-in-active-view',
id: 'a.open-in-active-view',
label: 'Open this doc',
icon: ExpandFullIcon(),
},
] as const satisfies (Pick<ToolbarAction, 'id' | 'label' | 'icon'> & {
mode: OpenDocMode;
})[];
const openDocActionGroup = {
placement: ActionPlacement.Start,
id: 'A.open-doc',
content(ctx) {
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
if (!block) return null;
const actions = openDocActions.map<ToolbarAction>(action => {
const openMode = action.mode;
const shouldOpenInActiveView = openMode === 'open-in-active-view';
return {
...action,
disabled: shouldOpenInActiveView
? block.model.props.pageId === ctx.store.id
: false,
when: true,
run: (_ctx: ToolbarContext) => block.open({ openMode }),
};
});
return html`
<editor-menu-button
.contentPadding="${'8px'}"
.button=${html`
<editor-icon-button aria-label="Open doc" .tooltip=${'Open doc'}>
${OpenInNewIcon()} ${ArrowDownSmallIcon()}
</editor-icon-button>
`}
>
<div data-size="small" data-orientation="vertical">
${repeat(
actions,
action => action.id,
({ label, icon, run, disabled }) => html`
<editor-menu-action
aria-label=${ifDefined(label)}
?disabled=${ifDefined(
typeof disabled === 'function' ? disabled(ctx) : disabled
)}
@click=${() => run?.(ctx)}
>
${icon}<span class="label">${label}</span>
</editor-menu-action>
`
)}
</div>
</editor-menu-button>
`;
},
} as const satisfies ToolbarAction;
const conversionsActionGroup = {
id: 'b.conversions',
actions: [ actions: [
{ {
id: 'a.doc-title', id: 'inline',
content(ctx) { label: 'Inline view',
run(ctx) {
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent); const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
if (!block) return null; block?.convertToInline();
const model = block.model; // Clears
if (!model.props.title) return null; ctx.select('note');
ctx.reset();
const originalTitle = ctx.track('SelectedView', {
ctx.workspace.getDoc(model.props.pageId)?.meta?.title || 'Untitled'; ...trackBaseProps,
control: 'select view',
return html`<affine-linked-doc-title type: 'inline view',
.title=${originalTitle} });
.open=${(event: MouseEvent) => block.open({ event })}
></affine-linked-doc-title>`;
}, },
when: ctx => !ctx.hasSelectedSurfaceModels,
}, },
{ {
id: 'b.conversions', id: 'card',
actions: [ label: 'Card view',
{ disabled: true,
id: 'inline', },
label: 'Inline view', {
run(ctx) { id: 'embed',
const block = ctx.getCurrentBlockByType( label: 'Embed view',
EmbedLinkedDocBlockComponent disabled(ctx) {
); const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
block?.covertToInline(); if (!block) return true;
// Clears if (block.closest('affine-embed-synced-doc-block')) return true;
ctx.select('note');
ctx.reset();
ctx.track('SelectedView', { const model = block.model;
...trackBaseProps,
control: 'select view',
type: 'inline view',
});
},
},
{
id: 'card',
label: 'Card view',
disabled: true,
},
{
id: 'embed',
label: 'Embed view',
disabled(ctx) {
const block = ctx.getCurrentBlockByType(
EmbedLinkedDocBlockComponent
);
if (!block) return true;
if (block.closest('affine-embed-synced-doc-block')) return true; // same doc
if (model.props.pageId === ctx.store.id) return true;
const model = block.model; // linking to block
if (referenceToNode(model.props)) return true;
// same doc return false;
if (model.props.pageId === ctx.store.id) return true;
// linking to block
if (referenceToNode(model.props)) return true;
return false;
},
run(ctx) {
const block = ctx.getCurrentBlockByType(
EmbedLinkedDocBlockComponent
);
block?.convertToEmbed();
ctx.track('SelectedView', {
...trackBaseProps,
control: 'select view',
type: 'embed view',
});
},
},
],
content(ctx) {
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
if (!model) return null;
const actions = this.actions.map(action => ({ ...action }));
const onToggle = (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
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.viewType$=${signal(actions[1].label)}
></affine-view-dropdown-menu>`
)}`;
}, },
} satisfies ToolbarActionGroup<ToolbarAction>, run(ctx) {
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
block?.convertToEmbed();
ctx.track('SelectedView', {
...trackBaseProps,
control: 'select view',
type: 'embed view',
});
},
},
],
content(ctx) {
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
if (!model) return null;
const actions = this.actions.map(action => ({ ...action }));
const viewType$ = signal('Card 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>`
)}`;
},
} as const satisfies ToolbarActionGroup<ToolbarAction>;
const builtinToolbarConfig = {
actions: [
docTitleAction,
conversionsActionGroup,
{ {
id: 'c.style', id: 'c.style',
actions: [ actions: [
@@ -151,7 +255,9 @@ export const builtinToolbarConfig = {
id: 'list', id: 'list',
label: 'Small horizontal style', label: 'Small horizontal style',
}, },
], ].filter(action =>
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
),
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel); const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
if (!model) return null; if (!model) return null;
@@ -168,15 +274,11 @@ export const builtinToolbarConfig = {
}); });
}, },
})) satisfies ToolbarAction[]; })) satisfies ToolbarAction[];
const onToggle = (e: CustomEvent<boolean>) => { const onToggle = createOnToggleFn(
const opened = e.detail; ctx,
if (!opened) return; 'OpenedCardStyleSelector',
'switch card style'
ctx.track('OpenedCardStyleSelector', { );
...trackBaseProps,
control: 'switch card style',
});
};
return html`${keyed( return html`${keyed(
model, model,
@@ -189,20 +291,7 @@ export const builtinToolbarConfig = {
)}`; )}`;
}, },
} satisfies ToolbarActionGroup<ToolbarAction>, } satisfies ToolbarActionGroup<ToolbarAction>,
{ captionAction,
id: 'd.caption',
tooltip: 'Caption',
icon: CaptionIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
block?.captionEditor?.show();
ctx.track('OpenedCaptionEditor', {
...trackBaseProps,
control: 'add caption',
});
},
},
{ {
placement: ActionPlacement.More, placement: ActionPlacement.More,
id: 'a.clipboard', id: 'a.clipboard',
@@ -258,3 +347,149 @@ export const builtinToolbarConfig = {
}, },
], ],
} as const satisfies ToolbarModuleConfig; } as const satisfies ToolbarModuleConfig;
const builtinSurfaceToolbarConfig = {
actions: [
openDocActionGroup,
docTitleAction,
conversionsActionGroup,
{
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 =>
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
),
content(ctx) {
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
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>,
captionAction,
{
id: 'e.scale',
content(ctx) {
const model = ctx.getCurrentBlockByType(
EmbedLinkedDocBlockComponent
)?.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.getSurfaceModelsByType(EmbedLinkedDocModel).length === 1,
} as const satisfies ToolbarModuleConfig;
export const createBuiltinToolbarConfigExtension = (
flavour: string
): ExtensionType[] => {
const name = flavour.split(':').pop();
return [
ToolbarModuleExtension({
id: BlockFlavourIdentifier(flavour),
config: builtinToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier(`affine:surface:${name}`),
config: builtinSurfaceToolbarConfig,
}),
];
};

View File

@@ -151,7 +151,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
]); ]);
}; };
covertToInline = () => { convertToInline = () => {
const { doc } = this.model; const { doc } = this.model;
const parent = doc.getParent(this.model); const parent = doc.getParent(this.model);
if (!parent) { if (!parent) {

View File

@@ -1,15 +1,11 @@
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model'; import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services'; import { BlockViewExtension } from '@blocksuite/block-std';
import {
BlockServiceIdentifier,
BlockViewExtension,
} 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 { EmbedLinkedDocBlockAdapterExtensions } from './adapters/extension'; import { EmbedLinkedDocBlockAdapterExtensions } from './adapters/extension';
import { LinkedDocSlashMenuConfigExtension } from './configs/slash-menu'; import { LinkedDocSlashMenuConfigExtension } from './configs/slash-menu';
import { builtinToolbarConfig } from './configs/toolbar'; import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
const flavour = EmbedLinkedDocBlockSchema.model.flavour; const flavour = EmbedLinkedDocBlockSchema.model.flavour;
@@ -20,9 +16,6 @@ export const EmbedLinkedDocBlockSpec: ExtensionType[] = [
: literal`affine-embed-linked-doc-block`; : literal`affine-embed-linked-doc-block`;
}), }),
EmbedLinkedDocBlockAdapterExtensions, EmbedLinkedDocBlockAdapterExtensions,
ToolbarModuleExtension({ createBuiltinToolbarConfigExtension(flavour),
id: BlockServiceIdentifier(flavour),
config: builtinToolbarConfig,
}),
LinkedDocSlashMenuConfigExtension, LinkedDocSlashMenuConfigExtension,
].flat(); ].flat();

View File

@@ -2,13 +2,17 @@ import { toast } from '@blocksuite/affine-components/toast';
import { EmbedSyncedDocModel } from '@blocksuite/affine-model'; import { EmbedSyncedDocModel } from '@blocksuite/affine-model';
import { import {
ActionPlacement, ActionPlacement,
type LinkEventType,
type OpenDocMode, type OpenDocMode,
type ToolbarAction, type ToolbarAction,
type ToolbarActionGroup, type ToolbarActionGroup,
type ToolbarContext, 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 { BlockFlavourIdentifier } from '@blocksuite/block-std';
import { Bound } from '@blocksuite/global/gfx';
import { import {
ArrowDownSmallIcon, ArrowDownSmallIcon,
CaptionIcon, CaptionIcon,
@@ -18,8 +22,8 @@ import {
ExpandFullIcon, ExpandFullIcon,
OpenInNewIcon, OpenInNewIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { Slice } from '@blocksuite/store'; import { type ExtensionType, Slice } 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 { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { keyed } from 'lit/directives/keyed.js'; import { keyed } from 'lit/directives/keyed.js';
@@ -28,167 +32,171 @@ import { repeat } from 'lit/directives/repeat.js';
import { EmbedSyncedDocBlockComponent } from '../embed-synced-doc-block'; import { EmbedSyncedDocBlockComponent } from '../embed-synced-doc-block';
const trackBaseProps = { const trackBaseProps = {
segment: 'doc',
page: 'doc editor',
module: 'toolbar',
category: 'linked doc', category: 'linked doc',
type: 'embed view', type: 'embed view',
}; };
export const builtinToolbarConfig = { const createOnToggleFn =
(
ctx: ToolbarContext,
name: Extract<
LinkEventType,
'OpenedViewSelector' | 'OpenedCardScaleSelector'
>,
control: 'switch view' | 'switch card scale'
) =>
(e: CustomEvent<boolean>) => {
e.stopPropagation();
const opened = e.detail;
if (!opened) return;
ctx.track(name, { ...trackBaseProps, control });
};
const openDocActions = [
{
mode: 'open-in-active-view',
id: 'a.open-in-active-view',
label: 'Open this doc',
icon: ExpandFullIcon(),
},
] as const satisfies (Pick<ToolbarAction, 'id' | 'label' | 'icon'> & {
mode: OpenDocMode;
})[];
const openDocActionGroup = {
placement: ActionPlacement.Start,
id: 'A.open-doc',
content(ctx) {
const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent);
if (!block) return null;
const actions = openDocActions.map<ToolbarAction>(action => {
const openMode = action.mode;
const shouldOpenInActiveView = openMode === 'open-in-active-view';
return {
...action,
disabled: shouldOpenInActiveView
? block.model.props.pageId === ctx.store.id
: false,
when: true,
run: (_ctx: ToolbarContext) => block.open({ openMode }),
};
});
return html`
<editor-menu-button
.contentPadding="${'8px'}"
.button=${html`
<editor-icon-button aria-label="Open doc" .tooltip=${'Open doc'}>
${OpenInNewIcon()} ${ArrowDownSmallIcon()}
</editor-icon-button>
`}
>
<div data-size="small" data-orientation="vertical">
${repeat(
actions,
action => action.id,
({ label, icon, run, disabled }) => html`
<editor-menu-action
aria-label=${ifDefined(label)}
?disabled=${ifDefined(
typeof disabled === 'function' ? disabled(ctx) : disabled
)}
@click=${() => run?.(ctx)}
>
${icon}<span class="label">${label}</span>
</editor-menu-action>
`
)}
</div>
</editor-menu-button>
`;
},
} as const satisfies ToolbarAction;
const conversionsActionGroup = {
id: 'a.conversions',
actions: [ actions: [
{ {
placement: ActionPlacement.Start, id: 'inline',
id: 'A.open-doc', label: 'Inline view',
actions: [
{
id: 'open-in-active-view',
label: 'Open this doc',
icon: ExpandFullIcon(),
},
],
content(ctx) {
const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent);
if (!block) return null;
const actions = this.actions
.map<ToolbarAction>(action => {
const shouldOpenInActiveView = action.id === 'open-in-active-view';
const allowed =
typeof action.when === 'function'
? action.when(ctx)
: (action.when ?? true);
return {
...action,
disabled: shouldOpenInActiveView
? block.model.props.pageId === ctx.store.id
: false,
when: allowed,
run: (_ctx: ToolbarContext) =>
block.open({
openMode: action.id as OpenDocMode,
}),
};
})
.filter(action => {
if (typeof action.when === 'function') return action.when(ctx);
return action.when ?? true;
});
return html`
<editor-menu-button
.contentPadding="${'8px'}"
.button=${html`
<editor-icon-button aria-label="Open doc" .tooltip=${'Open doc'}>
${OpenInNewIcon()} ${ArrowDownSmallIcon()}
</editor-icon-button>
`}
>
<div data-size="small" data-orientation="vertical">
${repeat(
actions,
action => action.id,
({ label, icon, run, disabled }) => html`
<editor-menu-action
aria-label=${ifDefined(label)}
?disabled=${ifDefined(
typeof disabled === 'function' ? disabled(ctx) : disabled
)}
@click=${() => run?.(ctx)}
>
${icon}<span class="label">${label}</span>
</editor-menu-action>
`
)}
</div>
</editor-menu-button>
`;
},
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'a.conversions',
actions: [
{
id: 'inline',
label: 'Inline view',
run(ctx) {
const block = ctx.getCurrentBlockByType(
EmbedSyncedDocBlockComponent
);
block?.covertToInline();
// Clears
ctx.select('note');
ctx.reset();
ctx.track('SelectedView', {
...trackBaseProps,
control: 'select view',
type: 'inline view',
});
},
},
{
id: 'card',
label: 'Card view',
run(ctx) {
const block = ctx.getCurrentBlockByType(
EmbedSyncedDocBlockComponent
);
block?.convertToCard();
ctx.track('SelectedView', {
...trackBaseProps,
control: 'select view',
type: 'card view',
});
},
},
{
id: 'embed',
label: 'Embed view',
disabled: true,
},
],
content(ctx) {
const model = ctx.getCurrentModelByType(EmbedSyncedDocModel);
if (!model) return null;
const actions = this.actions.map(action => ({ ...action }));
const onToggle = (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
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.viewType$=${signal(actions[2].label)}
></affine-view-dropdown-menu>`
)}`;
},
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'b.caption',
tooltip: 'Caption',
icon: CaptionIcon(),
run(ctx) { run(ctx) {
const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent); const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent);
block?.captionEditor?.show(); block?.convertToInline();
ctx.track('OpenedCaptionEditor', {
// Clears
ctx.select('note');
ctx.reset();
ctx.track('SelectedView', {
...trackBaseProps, ...trackBaseProps,
control: 'add caption', control: 'select view',
type: 'inline view',
});
},
when: ctx => !ctx.hasSelectedSurfaceModels,
},
{
id: 'card',
label: 'Card view',
run(ctx) {
const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent);
block?.convertToCard();
ctx.track('SelectedView', {
...trackBaseProps,
control: 'select view',
type: 'card view',
}); });
}, },
}, },
{
id: 'embed',
label: 'Embed view',
disabled: true,
},
],
content(ctx) {
const model = ctx.getCurrentModelByType(EmbedSyncedDocModel);
if (!model) return null;
const actions = this.actions.map(action => ({ ...action }));
const viewType$ = signal('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>`
)}`;
},
} as const satisfies ToolbarActionGroup<ToolbarAction>;
const captionAction = {
id: 'c.caption',
tooltip: 'Caption',
icon: CaptionIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent);
block?.captionEditor?.show();
ctx.track('OpenedCaptionEditor', {
...trackBaseProps,
control: 'add caption',
});
},
} as const satisfies ToolbarAction;
const builtinToolbarConfig = {
actions: [
openDocActionGroup,
conversionsActionGroup,
captionAction,
{ {
placement: ActionPlacement.More, placement: ActionPlacement.More,
id: 'a.clipboard', id: 'a.clipboard',
@@ -244,3 +252,79 @@ export const builtinToolbarConfig = {
}, },
], ],
} as const satisfies ToolbarModuleConfig; } as const satisfies ToolbarModuleConfig;
const builtinSurfaceToolbarConfig = {
actions: [
openDocActionGroup,
conversionsActionGroup,
captionAction,
{
id: 'd.scale',
content(ctx) {
const model = ctx.getCurrentBlockByType(
EmbedSyncedDocBlockComponent
)?.model;
if (!model) return null;
const scale$ = computed(() =>
Math.round(100 * (model.props.scale$.value ?? 1))
);
const onSelect = (e: CustomEvent<number>) => {
e.stopPropagation();
const scale = e.detail / 100;
const oldScale = model.props.scale ?? 1;
const ratio = scale / oldScale;
const bounds = Bound.deserialize(model.xywh);
bounds.h *= ratio;
bounds.w *= ratio;
const xywh = bounds.serialize();
ctx.store.updateBlock(model, { scale, 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.getSurfaceModelsByType(EmbedSyncedDocModel).length === 1,
} as const satisfies ToolbarModuleConfig;
export const createBuiltinToolbarConfigExtension = (
flavour: string
): ExtensionType[] => {
const name = flavour.split(':').pop();
return [
ToolbarModuleExtension({
id: BlockFlavourIdentifier(flavour),
config: builtinToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier(`affine:surface:${name}`),
config: builtinSurfaceToolbarConfig,
}),
];
};

View File

@@ -306,7 +306,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
]); ]);
}; };
covertToInline = () => { convertToInline = () => {
const { doc } = this.model; const { doc } = this.model;
const parent = doc.getParent(this.model); const parent = doc.getParent(this.model);
if (!parent) { if (!parent) {

View File

@@ -1,15 +1,10 @@
import { EmbedSyncedDocBlockSchema } from '@blocksuite/affine-model'; import { EmbedSyncedDocBlockSchema } from '@blocksuite/affine-model';
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services'; import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std';
import {
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 { EmbedSyncedDocBlockAdapterExtensions } from './adapters/extension'; import { EmbedSyncedDocBlockAdapterExtensions } from './adapters/extension';
import { builtinToolbarConfig } from './configs/toolbar'; import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import { EmbedSyncedDocBlockService } from './embed-synced-doc-service'; import { EmbedSyncedDocBlockService } from './embed-synced-doc-service';
const flavour = EmbedSyncedDocBlockSchema.model.flavour; const flavour = EmbedSyncedDocBlockSchema.model.flavour;
@@ -23,8 +18,5 @@ export const EmbedSyncedDocBlockSpec: ExtensionType[] = [
: literal`affine-embed-synced-doc-block`; : literal`affine-embed-synced-doc-block`;
}), }),
EmbedSyncedDocBlockAdapterExtensions, EmbedSyncedDocBlockAdapterExtensions,
ToolbarModuleExtension({ createBuiltinToolbarConfigExtension(flavour),
id: BlockServiceIdentifier(flavour),
config: builtinToolbarConfig,
}),
].flat(); ].flat();

View File

@@ -616,7 +616,7 @@ export class EdgelessChangeEmbedCardButton extends WithDisposable(LitElement) {
.contentPadding=${'8px'} .contentPadding=${'8px'}
.button=${html` .button=${html`
<editor-icon-button <editor-icon-button
aria-label="Open" aria-label="Open doc"
.justify=${'space-between'} .justify=${'space-between'}
.labelHeight=${'20px'} .labelHeight=${'20px'}
> >

View File

@@ -1,17 +1,13 @@
import { getHostName } from '@blocksuite/affine-shared/utils'; import { getHostName } from '@blocksuite/affine-shared/utils';
import { import { PropTypes, requiredProperties } from '@blocksuite/block-std';
PropTypes, import { css, LitElement } from 'lit';
requiredProperties,
ShadowlessElement,
} from '@blocksuite/block-std';
import { css } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { html } from 'lit-html'; import { html } from 'lit-html';
@requiredProperties({ @requiredProperties({
url: PropTypes.string, url: PropTypes.string,
}) })
export class LinkPreview extends ShadowlessElement { export class LinkPreview extends LitElement {
static override styles = css` static override styles = css`
.affine-link-preview { .affine-link-preview {
display: flex; display: flex;

View File

@@ -1,9 +1,5 @@
import { import { PropTypes, requiredProperties } from '@blocksuite/block-std';
PropTypes, import { css, LitElement } from 'lit';
requiredProperties,
ShadowlessElement,
} from '@blocksuite/block-std';
import { css } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { html } from 'lit-html'; import { html } from 'lit-html';
@@ -11,7 +7,7 @@ import { html } from 'lit-html';
title: PropTypes.string, title: PropTypes.string,
open: PropTypes.instanceOf(Function), open: PropTypes.instanceOf(Function),
}) })
export class DocTitle extends ShadowlessElement { export class DocTitle extends LitElement {
static override styles = css` static override styles = css`
editor-icon-button .label { editor-icon-button .label {
min-width: 60px; min-width: 60px;

View File

@@ -1012,11 +1012,21 @@ export const createCustomToolbarExtension = (
config: embedLinkedDocToolbarConfig, config: embedLinkedDocToolbarConfig,
}), }),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:surface:embed-linked-doc'),
config: embedLinkedDocToolbarConfig,
}),
ToolbarModuleExtension({ ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:embed-synced-doc'), id: BlockFlavourIdentifier('custom:affine:embed-synced-doc'),
config: embedSyncedDocToolbarConfig, config: embedSyncedDocToolbarConfig,
}), }),
ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:surface:embed-synced-doc'),
config: embedSyncedDocToolbarConfig,
}),
ToolbarModuleExtension({ ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:reference'), id: BlockFlavourIdentifier('custom:affine:reference'),
config: inlineReferenceToolbarConfig, config: inlineReferenceToolbarConfig,

View File

@@ -1327,7 +1327,7 @@ export async function triggerComponentToolbarAction(
} }
case 'openLinkedDoc': { case 'openLinkedDoc': {
const openButton = locatorComponentToolbar(page).getByRole('button', { const openButton = locatorComponentToolbar(page).getByRole('button', {
name: 'Open', name: 'Open doc',
}); });
await openButton.click(); await openButton.click();