mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 14:56:59 +08:00
refactor(editor): edgeless internal embed card toolbar config extension (#10717)
This commit is contained in:
@@ -1,145 +1,249 @@
|
||||
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 {
|
||||
ActionPlacement,
|
||||
type LinkEventType,
|
||||
type OpenDocMode,
|
||||
type ToolbarAction,
|
||||
type ToolbarActionGroup,
|
||||
type ToolbarContext,
|
||||
type ToolbarModuleConfig,
|
||||
ToolbarModuleExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
getBlockProps,
|
||||
referenceToNode,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { BlockFlavourIdentifier } from '@blocksuite/block-std';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
CaptionIcon,
|
||||
CopyIcon,
|
||||
DeleteIcon,
|
||||
DuplicateIcon,
|
||||
ExpandFullIcon,
|
||||
OpenInNewIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { Slice } from '@blocksuite/store';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { type ExtensionType, Slice } from '@blocksuite/store';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { EmbedLinkedDocBlockComponent } from '../embed-linked-doc-block';
|
||||
|
||||
const trackBaseProps = {
|
||||
segment: 'doc',
|
||||
page: 'doc editor',
|
||||
module: 'toolbar',
|
||||
category: 'linked doc',
|
||||
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: [
|
||||
{
|
||||
id: 'a.doc-title',
|
||||
content(ctx) {
|
||||
id: 'inline',
|
||||
label: 'Inline view',
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
|
||||
if (!block) return null;
|
||||
block?.convertToInline();
|
||||
|
||||
const model = block.model;
|
||||
if (!model.props.title) return null;
|
||||
// Clears
|
||||
ctx.select('note');
|
||||
ctx.reset();
|
||||
|
||||
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>`;
|
||||
ctx.track('SelectedView', {
|
||||
...trackBaseProps,
|
||||
control: 'select view',
|
||||
type: 'inline view',
|
||||
});
|
||||
},
|
||||
when: ctx => !ctx.hasSelectedSurfaceModels,
|
||||
},
|
||||
{
|
||||
id: 'b.conversions',
|
||||
actions: [
|
||||
{
|
||||
id: 'inline',
|
||||
label: 'Inline view',
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(
|
||||
EmbedLinkedDocBlockComponent
|
||||
);
|
||||
block?.covertToInline();
|
||||
id: 'card',
|
||||
label: 'Card view',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'embed',
|
||||
label: 'Embed view',
|
||||
disabled(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
|
||||
if (!block) return true;
|
||||
|
||||
// Clears
|
||||
ctx.select('note');
|
||||
ctx.reset();
|
||||
if (block.closest('affine-embed-synced-doc-block')) return true;
|
||||
|
||||
ctx.track('SelectedView', {
|
||||
...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;
|
||||
const model = block.model;
|
||||
|
||||
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
|
||||
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>`
|
||||
)}`;
|
||||
return false;
|
||||
},
|
||||
} 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',
|
||||
actions: [
|
||||
@@ -151,7 +255,9 @@ export const builtinToolbarConfig = {
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
],
|
||||
].filter(action =>
|
||||
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return null;
|
||||
@@ -168,15 +274,11 @@ export const builtinToolbarConfig = {
|
||||
});
|
||||
},
|
||||
})) satisfies ToolbarAction[];
|
||||
const onToggle = (e: CustomEvent<boolean>) => {
|
||||
const opened = e.detail;
|
||||
if (!opened) return;
|
||||
|
||||
ctx.track('OpenedCardStyleSelector', {
|
||||
...trackBaseProps,
|
||||
control: 'switch card style',
|
||||
});
|
||||
};
|
||||
const onToggle = createOnToggleFn(
|
||||
ctx,
|
||||
'OpenedCardStyleSelector',
|
||||
'switch card style'
|
||||
);
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
@@ -189,20 +291,7 @@ export const builtinToolbarConfig = {
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
id: 'd.caption',
|
||||
tooltip: 'Caption',
|
||||
icon: CaptionIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
|
||||
block?.captionEditor?.show();
|
||||
|
||||
ctx.track('OpenedCaptionEditor', {
|
||||
...trackBaseProps,
|
||||
control: 'add caption',
|
||||
});
|
||||
},
|
||||
},
|
||||
captionAction,
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'a.clipboard',
|
||||
@@ -258,3 +347,149 @@ export const builtinToolbarConfig = {
|
||||
},
|
||||
],
|
||||
} 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,
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -151,7 +151,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
]);
|
||||
};
|
||||
|
||||
covertToInline = () => {
|
||||
convertToInline = () => {
|
||||
const { doc } = this.model;
|
||||
const parent = doc.getParent(this.model);
|
||||
if (!parent) {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
|
||||
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
BlockServiceIdentifier,
|
||||
BlockViewExtension,
|
||||
} from '@blocksuite/block-std';
|
||||
import { BlockViewExtension } from '@blocksuite/block-std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { EmbedLinkedDocBlockAdapterExtensions } from './adapters/extension';
|
||||
import { LinkedDocSlashMenuConfigExtension } from './configs/slash-menu';
|
||||
import { builtinToolbarConfig } from './configs/toolbar';
|
||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||
|
||||
const flavour = EmbedLinkedDocBlockSchema.model.flavour;
|
||||
|
||||
@@ -20,9 +16,6 @@ export const EmbedLinkedDocBlockSpec: ExtensionType[] = [
|
||||
: literal`affine-embed-linked-doc-block`;
|
||||
}),
|
||||
EmbedLinkedDocBlockAdapterExtensions,
|
||||
ToolbarModuleExtension({
|
||||
id: BlockServiceIdentifier(flavour),
|
||||
config: builtinToolbarConfig,
|
||||
}),
|
||||
createBuiltinToolbarConfigExtension(flavour),
|
||||
LinkedDocSlashMenuConfigExtension,
|
||||
].flat();
|
||||
|
||||
@@ -2,13 +2,17 @@ import { toast } from '@blocksuite/affine-components/toast';
|
||||
import { EmbedSyncedDocModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
ActionPlacement,
|
||||
type LinkEventType,
|
||||
type OpenDocMode,
|
||||
type ToolbarAction,
|
||||
type ToolbarActionGroup,
|
||||
type ToolbarContext,
|
||||
type ToolbarModuleConfig,
|
||||
ToolbarModuleExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { getBlockProps } from '@blocksuite/affine-shared/utils';
|
||||
import { BlockFlavourIdentifier } from '@blocksuite/block-std';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
CaptionIcon,
|
||||
@@ -18,8 +22,8 @@ import {
|
||||
ExpandFullIcon,
|
||||
OpenInNewIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { Slice } from '@blocksuite/store';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { type ExtensionType, Slice } from '@blocksuite/store';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.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';
|
||||
|
||||
const trackBaseProps = {
|
||||
segment: 'doc',
|
||||
page: 'doc editor',
|
||||
module: 'toolbar',
|
||||
category: 'linked doc',
|
||||
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: [
|
||||
{
|
||||
placement: ActionPlacement.Start,
|
||||
id: 'A.open-doc',
|
||||
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(),
|
||||
id: 'inline',
|
||||
label: 'Inline view',
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent);
|
||||
block?.captionEditor?.show();
|
||||
ctx.track('OpenedCaptionEditor', {
|
||||
block?.convertToInline();
|
||||
|
||||
// Clears
|
||||
ctx.select('note');
|
||||
ctx.reset();
|
||||
|
||||
ctx.track('SelectedView', {
|
||||
...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,
|
||||
id: 'a.clipboard',
|
||||
@@ -244,3 +252,79 @@ export const builtinToolbarConfig = {
|
||||
},
|
||||
],
|
||||
} 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,
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -306,7 +306,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
|
||||
]);
|
||||
};
|
||||
|
||||
covertToInline = () => {
|
||||
convertToInline = () => {
|
||||
const { doc } = this.model;
|
||||
const parent = doc.getParent(this.model);
|
||||
if (!parent) {
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { EmbedSyncedDocBlockSchema } from '@blocksuite/affine-model';
|
||||
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
BlockServiceIdentifier,
|
||||
BlockViewExtension,
|
||||
FlavourExtension,
|
||||
} from '@blocksuite/block-std';
|
||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { EmbedSyncedDocBlockAdapterExtensions } from './adapters/extension';
|
||||
import { builtinToolbarConfig } from './configs/toolbar';
|
||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||
import { EmbedSyncedDocBlockService } from './embed-synced-doc-service';
|
||||
|
||||
const flavour = EmbedSyncedDocBlockSchema.model.flavour;
|
||||
@@ -23,8 +18,5 @@ export const EmbedSyncedDocBlockSpec: ExtensionType[] = [
|
||||
: literal`affine-embed-synced-doc-block`;
|
||||
}),
|
||||
EmbedSyncedDocBlockAdapterExtensions,
|
||||
ToolbarModuleExtension({
|
||||
id: BlockServiceIdentifier(flavour),
|
||||
config: builtinToolbarConfig,
|
||||
}),
|
||||
createBuiltinToolbarConfigExtension(flavour),
|
||||
].flat();
|
||||
|
||||
@@ -616,7 +616,7 @@ export class EdgelessChangeEmbedCardButton extends WithDisposable(LitElement) {
|
||||
.contentPadding=${'8px'}
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Open"
|
||||
aria-label="Open doc"
|
||||
.justify=${'space-between'}
|
||||
.labelHeight=${'20px'}
|
||||
>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { getHostName } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
PropTypes,
|
||||
requiredProperties,
|
||||
ShadowlessElement,
|
||||
} from '@blocksuite/block-std';
|
||||
import { css } from 'lit';
|
||||
import { PropTypes, requiredProperties } from '@blocksuite/block-std';
|
||||
import { css, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { html } from 'lit-html';
|
||||
|
||||
@requiredProperties({
|
||||
url: PropTypes.string,
|
||||
})
|
||||
export class LinkPreview extends ShadowlessElement {
|
||||
export class LinkPreview extends LitElement {
|
||||
static override styles = css`
|
||||
.affine-link-preview {
|
||||
display: flex;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
PropTypes,
|
||||
requiredProperties,
|
||||
ShadowlessElement,
|
||||
} from '@blocksuite/block-std';
|
||||
import { css } from 'lit';
|
||||
import { PropTypes, requiredProperties } from '@blocksuite/block-std';
|
||||
import { css, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { html } from 'lit-html';
|
||||
|
||||
@@ -11,7 +7,7 @@ import { html } from 'lit-html';
|
||||
title: PropTypes.string,
|
||||
open: PropTypes.instanceOf(Function),
|
||||
})
|
||||
export class DocTitle extends ShadowlessElement {
|
||||
export class DocTitle extends LitElement {
|
||||
static override styles = css`
|
||||
editor-icon-button .label {
|
||||
min-width: 60px;
|
||||
|
||||
Reference in New Issue
Block a user