From e686a6aeccf24d7a60178d4a8c2a089db7f090f7 Mon Sep 17 00:00:00 2001 From: fundon Date: Wed, 19 Mar 2025 12:34:17 +0000 Subject: [PATCH] refactor(editor): edgeless internal embed card toolbar config extension (#10717) --- .../embed-linked-doc-block/configs/toolbar.ts | 485 +++++++++++++----- .../embed-linked-doc-block.ts | 2 +- .../embed-linked-doc-spec.ts | 13 +- .../embed-synced-doc-block/configs/toolbar.ts | 386 ++++++++------ .../embed-synced-doc-block.ts | 2 +- .../embed-synced-doc-spec.ts | 14 +- .../change-embed-card-button.ts | 2 +- .../components/src/link-preview/link.ts | 10 +- .../src/linked-doc-title/doc-title.ts | 10 +- .../extensions/editor-config/toolbar/index.ts | 10 + .../blocksuite/e2e/utils/actions/edgeless.ts | 2 +- 11 files changed, 621 insertions(+), 315 deletions(-) diff --git a/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/configs/toolbar.ts b/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/configs/toolbar.ts index 47a76751de..4390513efe 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/configs/toolbar.ts @@ -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) => { + 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` block.open({ event })} + >`; + }, +} 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 & { + 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(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` + + ${OpenInNewIcon()} ${ArrowDownSmallIcon()} + + `} + > +
+ ${repeat( + actions, + action => action.id, + ({ label, icon, run, disabled }) => html` + run?.(ctx)} + > + ${icon}${label} + + ` + )} +
+
+ `; + }, +} 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` block.open({ event })} - >`; + 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) => { - const opened = e.detail; - if (!opened) return; - - ctx.track('OpenedViewSelector', { - ...trackBaseProps, - control: 'switch view', - }); - }; - - return html`${keyed( - model, - html`` - )}`; + return false; }, - } satisfies ToolbarActionGroup, + 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`` + )}`; + }, +} as const satisfies ToolbarActionGroup; + +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) => { - 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, - { - 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`` + )}`; + }, + } satisfies ToolbarActionGroup, + 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) => { + 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`` + )}`; + }, + }, + ], + + 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, + }), + ]; +}; diff --git a/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/embed-linked-doc-block.ts b/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/embed-linked-doc-block.ts index ed897421e1..80b8402f93 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/embed-linked-doc-block.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/embed-linked-doc-block.ts @@ -151,7 +151,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent { + convertToInline = () => { const { doc } = this.model; const parent = doc.getParent(this.model); if (!parent) { diff --git a/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/embed-linked-doc-spec.ts b/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/embed-linked-doc-spec.ts index 0b7d65ef44..239912b00d 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/embed-linked-doc-spec.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-linked-doc-block/embed-linked-doc-spec.ts @@ -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(); diff --git a/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/configs/toolbar.ts b/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/configs/toolbar.ts index 7ab2a51be0..b652e711a1 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/configs/toolbar.ts @@ -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) => { + 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 & { + 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(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` + + ${OpenInNewIcon()} ${ArrowDownSmallIcon()} + + `} + > +
+ ${repeat( + actions, + action => action.id, + ({ label, icon, run, disabled }) => html` + run?.(ctx)} + > + ${icon}${label} + + ` + )} +
+
+ `; + }, +} 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(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` - - ${OpenInNewIcon()} ${ArrowDownSmallIcon()} - - `} - > -
- ${repeat( - actions, - action => action.id, - ({ label, icon, run, disabled }) => html` - run?.(ctx)} - > - ${icon}${label} - - ` - )} -
-
- `; - }, - } satisfies ToolbarActionGroup, - { - 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) => { - const opened = e.detail; - if (!opened) return; - - ctx.track('OpenedViewSelector', { - ...trackBaseProps, - control: 'switch view', - }); - }; - - return html`${keyed( - model, - html`` - )}`; - }, - } satisfies ToolbarActionGroup, - { - 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`` + )}`; + }, +} as const satisfies ToolbarActionGroup; + +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) => { + 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`` + )}`; + }, + }, + ], + + 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, + }), + ]; +}; diff --git a/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/embed-synced-doc-block.ts b/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/embed-synced-doc-block.ts index bb8f544bb6..4f342cc329 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/embed-synced-doc-block.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/embed-synced-doc-block.ts @@ -306,7 +306,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent { + convertToInline = () => { const { doc } = this.model; const parent = doc.getParent(this.model); if (!parent) { diff --git a/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/embed-synced-doc-spec.ts b/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/embed-synced-doc-spec.ts index 3f07cf24a6..6d5e295fc3 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/embed-synced-doc-spec.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-synced-doc-block/embed-synced-doc-spec.ts @@ -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(); diff --git a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-embed-card-button.ts b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-embed-card-button.ts index f0a5db1b38..bbf335ec69 100644 --- a/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-embed-card-button.ts +++ b/blocksuite/affine/blocks/block-root/src/widgets/element-toolbar/change-embed-card-button.ts @@ -616,7 +616,7 @@ export class EdgelessChangeEmbedCardButton extends WithDisposable(LitElement) { .contentPadding=${'8px'} .button=${html` diff --git a/blocksuite/affine/components/src/link-preview/link.ts b/blocksuite/affine/components/src/link-preview/link.ts index f7e7f9f493..482b4b8f22 100644 --- a/blocksuite/affine/components/src/link-preview/link.ts +++ b/blocksuite/affine/components/src/link-preview/link.ts @@ -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; diff --git a/blocksuite/affine/components/src/linked-doc-title/doc-title.ts b/blocksuite/affine/components/src/linked-doc-title/doc-title.ts index aa920a0639..f48897c02c 100644 --- a/blocksuite/affine/components/src/linked-doc-title/doc-title.ts +++ b/blocksuite/affine/components/src/linked-doc-title/doc-title.ts @@ -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; diff --git a/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts b/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts index e1d8ccc7d1..52af85520d 100644 --- a/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts +++ b/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts @@ -1012,11 +1012,21 @@ export const createCustomToolbarExtension = ( config: embedLinkedDocToolbarConfig, }), + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('custom:affine:surface:embed-linked-doc'), + config: embedLinkedDocToolbarConfig, + }), + ToolbarModuleExtension({ id: BlockFlavourIdentifier('custom:affine:embed-synced-doc'), config: embedSyncedDocToolbarConfig, }), + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('custom:affine:surface:embed-synced-doc'), + config: embedSyncedDocToolbarConfig, + }), + ToolbarModuleExtension({ id: BlockFlavourIdentifier('custom:affine:reference'), config: inlineReferenceToolbarConfig, diff --git a/tests/blocksuite/e2e/utils/actions/edgeless.ts b/tests/blocksuite/e2e/utils/actions/edgeless.ts index 2e9d446487..0f688a6b62 100644 --- a/tests/blocksuite/e2e/utils/actions/edgeless.ts +++ b/tests/blocksuite/e2e/utils/actions/edgeless.ts @@ -1327,7 +1327,7 @@ export async function triggerComponentToolbarAction( } case 'openLinkedDoc': { const openButton = locatorComponentToolbar(page).getByRole('button', { - name: 'Open', + name: 'Open doc', }); await openButton.click();