refactor(editor): inner toolbar surface-ref block with extension (#11246)

This PR refactor `surface-ref` toolbar with `ToolbarExtension`
This commit is contained in:
L-Sun
2025-03-28 05:48:24 +00:00
parent 69f393fe2f
commit af91a0217f
22 changed files with 551 additions and 542 deletions

View File

@@ -0,0 +1 @@
export { SurfaceRefToolbarTitle } from './surface-ref-toolbar-title';

View File

@@ -0,0 +1,68 @@
import {
FrameBlockModel,
GroupElementModel,
MindmapElementModel,
ShapeElementModel,
} from '@blocksuite/affine-model';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { ShadowlessElement } from '@blocksuite/block-std';
import type { GfxModel } from '@blocksuite/block-std/gfx';
import {
EdgelessIcon,
FrameIcon,
GroupIcon,
MindmapIcon,
} from '@blocksuite/icons/lit';
import { css, html, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
export class SurfaceRefToolbarTitle extends ShadowlessElement {
static override styles = css`
surface-ref-toolbar-title {
display: flex;
padding: 2px 4px;
margin-right: auto;
align-items: center;
gap: 4px;
border-radius: 2px;
background: ${unsafeCSSVarV2('button/iconButtonSolid')};
svg {
color: ${unsafeCSSVarV2('icon/primary')};
width: 16px;
height: 16px;
}
span {
color: ${unsafeCSSVarV2('text/primary')};
font-size: 12px;
font-weight: 500;
line-height: 20px;
}
}
`;
@property({ attribute: false })
accessor referenceModel: GfxModel | null = null;
override render() {
const { referenceModel } = this;
let title = '';
let icon: TemplateResult = EdgelessIcon();
if (referenceModel instanceof GroupElementModel) {
title = referenceModel.title.toString();
icon = GroupIcon();
} else if (referenceModel instanceof FrameBlockModel) {
title = referenceModel.props.title.toString();
icon = FrameIcon();
} else if (referenceModel instanceof MindmapElementModel) {
const rootElement = referenceModel.tree.element;
if (rootElement instanceof ShapeElementModel) {
title = rootElement.text?.toString() ?? '';
}
icon = MindmapIcon();
}
return html`${icon}<span>${title}</span>`;
}
}

View File

@@ -0,0 +1,125 @@
import { toast } from '@blocksuite/affine-components/toast';
import {
copySelectedModelsCommand,
draftSelectedModelsCommand,
} from '@blocksuite/affine-shared/commands';
import {
ActionPlacement,
type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
import { html } from 'lit';
import { SurfaceRefBlockComponent } from '../surface-ref-block';
import { surfaceRefToBlob, writeImageBlobToClipboard } from '../utils';
export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
actions: [
{
id: 'a.surface-ref-title',
content: ctx => {
const surfaceRefBlock = ctx.getCurrentBlockByType(
SurfaceRefBlockComponent
);
if (!surfaceRefBlock) return null;
return html`<surface-ref-toolbar-title
.referenceModel=${surfaceRefBlock.referenceModel}
></surface-ref-toolbar-title>`;
},
},
{
id: 'c.copy-surface-ref',
label: 'Copy',
icon: CopyIcon(),
run: ctx => {
const surfaceRefBlock = ctx.getCurrentBlockByType(
SurfaceRefBlockComponent
);
if (!surfaceRefBlock) return;
ctx.chain
.pipe(draftSelectedModelsCommand, {
selectedModels: [surfaceRefBlock.model],
})
.pipe(copySelectedModelsCommand)
.run();
toast(surfaceRefBlock.std.host, 'Copied to clipboard');
},
},
{
id: 'd.surface-ref-caption',
icon: CaptionIcon(),
run: ctx => {
const surfaceRefBlock = ctx.getCurrentBlockByType(
SurfaceRefBlockComponent
);
if (!surfaceRefBlock) return;
surfaceRefBlock.captionElement.show();
},
},
{
id: 'a.clipboard',
placement: ActionPlacement.More,
when: ctx => {
const surfaceRefBlock = ctx.getCurrentBlock();
if (!(surfaceRefBlock instanceof SurfaceRefBlockComponent))
return false;
return !!surfaceRefBlock.referenceModel;
},
actions: [
{
id: 'a.surface-ref-copy-as-image',
label: 'Copy as Image',
icon: CopyIcon(),
placement: ActionPlacement.More,
when: ctx => {
const surfaceRefBlock = ctx.getCurrentBlockByType(
SurfaceRefBlockComponent
);
if (!surfaceRefBlock) return false;
return !!surfaceRefBlock.referenceModel;
},
run: ctx => {
const surfaceRefBlock = ctx.getCurrentBlockByType(
SurfaceRefBlockComponent
);
if (!surfaceRefBlock) return;
surfaceRefToBlob(surfaceRefBlock)
.then(async blob => {
if (!blob) {
toast(ctx.host, 'Failed to render surface-ref to image');
} else {
await writeImageBlobToClipboard(blob);
toast(ctx.host, 'Copied image to clipboard');
}
})
.catch(console.error);
},
},
// TODO(@L-Sun): add duplicate action after refactoring root-block/edgeless
],
},
{
id: 'g.surface-ref-deletion',
label: 'Delete',
icon: DeleteIcon(),
placement: ActionPlacement.More,
variant: 'destructive',
run: ctx => {
const surfaceRefBlock = ctx.getCurrentBlockByType(
SurfaceRefBlockComponent
);
if (!surfaceRefBlock) return;
ctx.store.deleteBlock(surfaceRefBlock.model);
},
},
],
placement: 'inner',
};

View File

@@ -1,11 +1,8 @@
import { SurfaceRefGenericBlockPortal } from './portal/generic-block.js';
import { SurfaceRefNotePortal } from './portal/note.js';
import { SurfaceRefBlockComponent } from './surface-ref-block.js';
import { EdgelessSurfaceRefBlockComponent } from './surface-ref-block-edgeless.js';
import {
AFFINE_SURFACE_REF_TOOLBAR,
AffineSurfaceRefToolbar,
} from './widgets/surface-ref-toolbar.js';
import { SurfaceRefToolbarTitle } from './components';
import { SurfaceRefGenericBlockPortal } from './portal/generic-block';
import { SurfaceRefNotePortal } from './portal/note';
import { SurfaceRefBlockComponent } from './surface-ref-block';
import { EdgelessSurfaceRefBlockComponent } from './surface-ref-block-edgeless';
export function effects() {
customElements.define(
@@ -18,5 +15,11 @@ export function effects() {
EdgelessSurfaceRefBlockComponent
);
customElements.define('surface-ref-note-portal', SurfaceRefNotePortal);
customElements.define(AFFINE_SURFACE_REF_TOOLBAR, AffineSurfaceRefToolbar);
customElements.define('surface-ref-toolbar-title', SurfaceRefToolbarTitle);
}
declare global {
interface HTMLElementTagNameMap {
'surface-ref-toolbar-title': SurfaceRefToolbarTitle;
}
}

View File

@@ -4,7 +4,9 @@ import {
getSurfaceBlock,
} from '@blocksuite/affine-block-surface';
import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption';
import { whenHover } from '@blocksuite/affine-components/hover';
import { Peekable } from '@blocksuite/affine-components/peek';
import { RefNodeSlotsProvider } from '@blocksuite/affine-inline-reference';
import {
FrameBlockModel,
type SurfaceRefBlockModel,
@@ -12,7 +14,9 @@ import {
import {
DocModeProvider,
EditPropsStore,
type OpenDocMode,
ThemeProvider,
ToolbarRegistryIdentifier,
ViewportElementExtension,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
@@ -424,6 +428,29 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
this._previewSpec.extend([SurfaceRefViewportWatcher]);
}
private _initHover() {
const { setReference, setFloating, dispose } = whenHover(
hovered => {
const message$ = this.std.get(ToolbarRegistryIdentifier).message$;
if (hovered) {
message$.value = {
flavour: this.model.flavour,
element: this,
setFloating,
};
return;
}
// Clears previous bindings
message$.value = null;
setFloating();
},
{ enterDelay: 500 }
);
setReference(this);
this._disposables.add(dispose);
}
private _renderRefContent(referencedModel: GfxModel) {
const [, , w, h] = deserializeXYWH(referencedModel.xywh);
const _previewSpec = this._previewSpec.value;
@@ -468,6 +495,28 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
</div>`;
}
readonly open = ({
openMode,
event,
}: {
openMode?: OpenDocMode;
event?: MouseEvent;
} = {}) => {
const pageId = this.referenceModel?.surface?.doc.id;
if (!pageId) return;
this.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.next({
pageId: pageId,
params: {
mode: 'edgeless',
elementIds: [this.model.props.reference],
},
openMode,
event,
host: this.host,
});
};
override connectedCallback() {
super.connectedCallback();
@@ -479,6 +528,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
this._initViewport();
this._initReferencedModel();
this._initSelection();
this._initHover();
}
override render() {

View File

@@ -1,30 +1,33 @@
import { SurfaceRefBlockSchema } from '@blocksuite/affine-model';
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
import {
BlockFlavourIdentifier,
BlockViewExtension,
FlavourExtension,
WidgetViewExtension,
} from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { SurfaceRefSlashMenuConfigExtension } from './configs/slash-menu';
export const surfaceRefToolbarWidget = WidgetViewExtension(
'affine:surface-ref',
'surfaceToolbar',
literal`affine-surface-ref-toolbar`
);
import { surfaceRefToolbarModuleConfig } from './configs/toolbar';
export const PageSurfaceRefBlockSpec: ExtensionType[] = [
FlavourExtension('affine:surface-ref'),
BlockViewExtension('affine:surface-ref', literal`affine-surface-ref`),
surfaceRefToolbarWidget,
FlavourExtension(SurfaceRefBlockSchema.model.flavour),
BlockViewExtension(
SurfaceRefBlockSchema.model.flavour,
literal`affine-surface-ref`
),
ToolbarModuleExtension({
id: BlockFlavourIdentifier(SurfaceRefBlockSchema.model.flavour),
config: surfaceRefToolbarModuleConfig,
}),
SurfaceRefSlashMenuConfigExtension,
];
export const EdgelessSurfaceRefBlockSpec: ExtensionType[] = [
FlavourExtension('affine:surface-ref'),
FlavourExtension(SurfaceRefBlockSchema.model.flavour),
BlockViewExtension(
'affine:surface-ref',
SurfaceRefBlockSchema.model.flavour,
literal`affine-edgeless-surface-ref`
),
SurfaceRefSlashMenuConfigExtension,

View File

@@ -1,3 +1,10 @@
import type { SurfaceBlockComponent } from '@blocksuite/affine-block-surface';
import { ExportManager } from '@blocksuite/affine-block-surface';
import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { BlockSuiteError } from '@blocksuite/global/exceptions';
import { Bound } from '@blocksuite/global/gfx';
import { assertType } from '@blocksuite/global/utils';
import { html } from 'lit';
export const noContentPlaceholder = html`
@@ -97,3 +104,43 @@ export const noContentPlaceholder = html`
/>
</svg>
`;
export const surfaceRefToBlob = async (
surfaceRefBlock: SurfaceRefBlockComponent
): Promise<Blob | null> => {
const { referenceModel, previewEditor } = surfaceRefBlock;
if (!referenceModel || !previewEditor) return null;
const exportManager = previewEditor.std.get(ExportManager);
const gfx = previewEditor.std.get(GfxControllerIdentifier);
const { surface } = gfx;
if (!surface) return null;
const surfaceBlock = previewEditor.std.view.getBlock(surface.id);
if (!surfaceBlock) return null;
assertType<SurfaceBlockComponent>(surfaceBlock);
const canvas = await exportManager.edgelessToCanvas(
surfaceBlock.renderer,
Bound.deserialize(referenceModel.xywh),
gfx,
undefined,
undefined,
{ zoom: surfaceBlock.renderer.viewport.zoom }
);
if (!canvas) {
throw new BlockSuiteError(
BlockSuiteError.ErrorCode.ValueNotExists,
'Failed to export edgeless to canvas'
);
}
return new Promise((resolve, reject) => {
canvas.toBlob(blob => (blob ? resolve(blob) : reject(null)), 'image/png');
});
};
export const writeImageBlobToClipboard = async (blob: Blob) => {
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
};

View File

@@ -1,122 +0,0 @@
import { type SurfaceBlockComponent } from '@blocksuite/affine-block-surface';
import {
CopyIcon,
DeleteIcon,
DownloadIcon,
} from '@blocksuite/affine-components/icons';
import { toast } from '@blocksuite/affine-components/toast';
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
import { downloadBlob } from '@blocksuite/affine-shared/utils';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import type { SurfaceRefToolbarContext } from './context.js';
import { edgelessToBlob, writeImageBlobToClipboard } from './utils.js';
export const BUILT_IN_GROUPS: MenuItemGroup<SurfaceRefToolbarContext>[] = [
{
type: 'clipboard',
when: ctx => !!(ctx.blockComponent.referenceModel && ctx.doc.root),
items: [
{
type: 'copy',
label: 'Copy as Image',
icon: CopyIcon,
action: ctx => {
if (!(ctx.blockComponent.referenceModel && ctx.doc.root?.id)) {
ctx.close();
return;
}
const referencedModel = ctx.blockComponent.referenceModel;
const editor = ctx.blockComponent.previewEditor;
const surfaceModel = editor?.std.get(GfxControllerIdentifier).surface;
if (!surfaceModel) {
ctx.close();
return;
}
const surfaceBlock = editor?.std.view.getBlock(
surfaceModel.id
) as SurfaceBlockComponent;
const surfaceRenderer = surfaceBlock.renderer;
if (!surfaceRenderer) {
ctx.close();
return;
}
edgelessToBlob(editor, {
surfaceRefBlock: ctx.blockComponent,
surfaceRenderer,
edgelessElement: referencedModel,
})
.then(blob => writeImageBlobToClipboard(blob))
.then(() => toast(ctx.host, 'Copied image to clipboard'))
.catch(console.error);
ctx.close();
},
},
// TODO(@L-Sun): add duplicate action after refactoring toolbar
{
type: 'download',
label: 'Download',
icon: DownloadIcon,
action: ctx => {
if (!(ctx.blockComponent.referenceModel && ctx.doc.root?.id)) {
ctx.close();
return;
}
const referencedModel = ctx.blockComponent.referenceModel;
const editor = ctx.blockComponent.previewEditor;
const surfaceModel = editor?.std.get(GfxControllerIdentifier).surface;
if (!surfaceModel) {
ctx.close();
return;
}
const surfaceBlock = editor?.std.view.getBlock(
surfaceModel.id
) as SurfaceBlockComponent;
const surfaceRenderer = surfaceBlock.renderer;
if (!surfaceRenderer) {
ctx.close();
return;
}
edgelessToBlob(editor, {
surfaceRefBlock: ctx.blockComponent,
surfaceRenderer,
edgelessElement: referencedModel,
})
.then(blob => {
const fileName =
'title' in referencedModel
? (referencedModel.title?.toString() ?? 'Edgeless Content')
: 'Edgeless Content';
downloadBlob(blob, fileName);
})
.catch(console.error);
ctx.close();
},
},
],
},
{
type: 'delete',
items: [
{
type: 'delete',
label: 'Delete',
icon: DeleteIcon,
disabled: ({ doc }) => doc.readonly,
action: ({ blockComponent, doc, close }) => {
doc.deleteBlock(blockComponent.model);
close();
},
},
],
},
];

View File

@@ -1,44 +0,0 @@
import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref';
import { MenuContext } from '@blocksuite/affine-components/toolbar';
export class SurfaceRefToolbarContext extends MenuContext {
override close = () => {
this.abortController.abort();
};
get doc() {
return this.blockComponent.doc;
}
get host() {
return this.blockComponent.host;
}
get selectedBlockModels() {
if (this.blockComponent) return [this.blockComponent.model];
return [];
}
get std() {
return this.host.std;
}
constructor(
public blockComponent: SurfaceRefBlockComponent,
public abortController: AbortController
) {
super();
}
isEmpty() {
return !this.blockComponent;
}
isMultiple() {
return false;
}
isSingle() {
return true;
}
}

View File

@@ -1,280 +0,0 @@
import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref';
import { peek } from '@blocksuite/affine-components/peek';
import { toast } from '@blocksuite/affine-components/toast';
import {
cloneGroups,
getMoreMenuConfig,
type MenuItem,
type MenuItemGroup,
renderGroups,
} from '@blocksuite/affine-components/toolbar';
import {
FrameBlockModel,
GroupElementModel,
MindmapElementModel,
ShapeElementModel,
type SurfaceRefBlockModel,
} from '@blocksuite/affine-model';
import {
copySelectedModelsCommand,
draftSelectedModelsCommand,
} from '@blocksuite/affine-shared/commands';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import type { ButtonPopperOptions } from '@blocksuite/affine-shared/utils';
import { WidgetComponent } from '@blocksuite/block-std';
import {
ArrowDownSmallIcon,
CaptionIcon,
CenterPeekIcon,
CopyIcon,
EdgelessIcon,
FrameIcon,
GroupIcon,
MindmapIcon,
MoreVerticalIcon,
OpenInNewIcon,
} from '@blocksuite/icons/lit';
import { css, html, nothing, type TemplateResult } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
import { BUILT_IN_GROUPS } from './config.js';
import { SurfaceRefToolbarContext } from './context.js';
export const AFFINE_SURFACE_REF_TOOLBAR = 'affine-surface-ref-toolbar';
export class AffineSurfaceRefToolbar extends WidgetComponent<
SurfaceRefBlockModel,
SurfaceRefBlockComponent
> {
static override styles = css`
:host {
position: absolute;
top: 0;
left: 0;
width: 100%;
gap: 4px;
padding: 4px;
margin: 0;
display: flex;
justify-content: flex-end;
editor-icon-button,
editor-menu-button {
background: ${unsafeCSSVarV2('button/iconButtonSolid')};
color: ${unsafeCSSVarV2('text/primary')};
box-shadow: ${unsafeCSSVar('shadow1')};
border-radius: 4px;
}
}
.surface-ref-toolbar-title {
display: flex;
padding: 2px 4px;
margin-right: auto;
align-items: center;
gap: 4px;
border-radius: 2px;
background: ${unsafeCSSVarV2('button/iconButtonSolid')};
svg {
color: ${unsafeCSSVarV2('icon/primary')};
width: 16px;
height: 16px;
}
span {
color: ${unsafeCSSVarV2('text/primary')};
font-size: 12px;
font-weight: 500;
line-height: 20px;
}
}
`;
private readonly _popoverOptions: Partial<ButtonPopperOptions> = {
mainAxis: 4,
stateUpdated: ({ display }) => {
this.dataset.openMenuDisplay = display;
},
};
/*
* Caches the more menu items.
* Currently only supports configuring more menu.
*/
moreGroups: MenuItemGroup<SurfaceRefToolbarContext>[] =
cloneGroups(BUILT_IN_GROUPS);
override connectedCallback() {
super.connectedCallback();
this.moreGroups = getMoreMenuConfig(this.std).configure(this.moreGroups);
this.disposables.addFromEvent(this, 'dblclick', e => {
e.stopPropagation();
});
}
private _renderTitle() {
if (!this.block) return nothing;
const { referenceModel } = this.block;
if (!referenceModel) return nothing;
let title = '';
let icon: TemplateResult<1> | null = null;
if (referenceModel instanceof GroupElementModel) {
title = referenceModel.title.toString();
icon = GroupIcon();
} else if (referenceModel instanceof FrameBlockModel) {
title = referenceModel.props.title.toString();
icon = FrameIcon();
} else if (referenceModel instanceof MindmapElementModel) {
const rootElement = referenceModel.tree.element;
if (rootElement instanceof ShapeElementModel) {
title = rootElement.text?.toString() ?? '';
}
icon = MindmapIcon();
}
return html`<div class="surface-ref-toolbar-title">
${icon}
<span>${title}</span>
</div>`;
}
private _renderOpenButton() {
const referenceModel = this.block?.referenceModel;
if (!referenceModel) return nothing;
const openMenuActions: MenuItem[] = [
{
type: 'open-in-edgeless',
label: 'Open in Edgeless',
icon: EdgelessIcon(),
action: () => this.block?.viewInEdgeless(),
disabled: this.block.model.doc.readonly,
},
{
type: 'open-in-center-peek',
label: 'Open in center peek',
icon: CenterPeekIcon(),
action: () => this.block && peek(this.block),
},
// TODO(@L-Sun): add split view and new tab
];
return html`<editor-menu-button
data-show
aria-label="Open"
style=${styleMap({
'--content-padding': '8px',
})}
.button=${html`
<editor-icon-button .iconContainerPadding=${4} .iconSize=${'16px'}>
${OpenInNewIcon()} ${ArrowDownSmallIcon()}
</editor-icon-button>
`}
.popperOptions=${this._popoverOptions}
>
<div data-orientation="vertical">
${repeat(
openMenuActions,
button => button.label,
({ label, icon, action, disabled }) => html`
<editor-menu-action
aria-label=${ifDefined(label)}
?disabled=${disabled}
@click=${action}
>
${icon}<span class="label">${label}</span>
</editor-menu-action>
`
)}
</div>
</editor-menu-button>`;
}
private _moreButton() {
if (!this.block) return nothing;
const moreMenuActions = renderGroups(
this.moreGroups,
new SurfaceRefToolbarContext(this.block, new AbortController())
);
return html`<editor-menu-button
data-show
style=${styleMap({
'--content-padding': '8px',
})}
.button=${html`
<editor-icon-button .iconContainerPadding=${4} .iconSize=${'16px'}>
${MoreVerticalIcon()}
</editor-icon-button>
`}
.popperOptions=${this._popoverOptions}
>
<div data-orientation="vertical">${moreMenuActions}</div>
</editor-menu-button>`;
}
private _renderButtons() {
if (!this.block) return nothing;
const readonly = this.block.model.doc.readonly;
const buttons = [
this._renderOpenButton(),
when(
!readonly,
() =>
html`<editor-icon-button
.iconContainerPadding=${4}
.iconSize=${'16px'}
@click=${() => {
if (!this.block) return;
this.std.command
.chain()
.pipe(draftSelectedModelsCommand, {
selectedModels: [this.block.model],
})
.pipe(copySelectedModelsCommand)
.run();
toast(this.block.std.host, 'Copied to clipboard');
}}
>
${CopyIcon()}
</editor-icon-button>`
),
when(
!readonly,
() =>
html`<editor-icon-button
.iconContainerPadding=${4}
.iconSize=${'16px'}
@click=${() => {
if (!this.block) return;
this.block.captionElement.show();
}}
>
${CaptionIcon()}
</editor-icon-button>`
),
this._moreButton(),
];
return buttons;
}
override render() {
if (!this.block) return nothing;
return html`${this._renderTitle()} ${this._renderButtons()}`;
}
}
declare global {
interface HTMLElementTagNameMap {
[AFFINE_SURFACE_REF_TOOLBAR]: AffineSurfaceRefToolbar;
}
}

View File

@@ -1,48 +0,0 @@
import type { CanvasRenderer } from '@blocksuite/affine-block-surface';
import { ExportManager } from '@blocksuite/affine-block-surface';
import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref';
import type { EditorHost } from '@blocksuite/block-std';
import {
GfxControllerIdentifier,
type GfxModel,
} from '@blocksuite/block-std/gfx';
import { BlockSuiteError } from '@blocksuite/global/exceptions';
import { Bound } from '@blocksuite/global/gfx';
export const edgelessToBlob = async (
host: EditorHost,
options: {
surfaceRefBlock: SurfaceRefBlockComponent;
surfaceRenderer: CanvasRenderer;
edgelessElement: GfxModel;
}
): Promise<Blob> => {
const { edgelessElement } = options;
const exportManager = host.std.get(ExportManager);
const bound = Bound.deserialize(edgelessElement.xywh);
const gfx = host.std.get(GfxControllerIdentifier);
const canvas = await exportManager.edgelessToCanvas(
options.surfaceRenderer,
bound,
gfx,
undefined,
undefined,
{ zoom: options.surfaceRenderer.viewport.zoom }
);
if (!canvas) {
throw new BlockSuiteError(
BlockSuiteError.ErrorCode.ValueNotExists,
'Failed to export edgeless to canvas'
);
}
return new Promise((resolve, reject) => {
canvas.toBlob(blob => (blob ? resolve(blob) : reject(null)), 'image/png');
});
};
export const writeImageBlobToClipboard = async (blob: Blob) => {
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
};