feat(editor): support convert edgeless bookmark to embed iframe block (#11237)

Close [BS-2941](https://linear.app/affine-design/issue/BS-2941/白板-surface-中-card-view-无法转为-embed-view)
This commit is contained in:
donteatfriedrice
2025-03-28 02:25:00 +00:00
parent c0c0e5ea8f
commit a459a00b21
3 changed files with 91 additions and 39 deletions
@@ -1,3 +1,8 @@
import {
canEmbedAsIframe,
EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE,
EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE,
} from '@blocksuite/affine-block-embed';
import { reassociateConnectorsCommand } from '@blocksuite/affine-block-surface';
import { toast } from '@blocksuite/affine-components/toast';
import {
@@ -13,7 +18,6 @@ import {
ActionPlacement,
EmbedIframeService,
EmbedOptionProvider,
FeatureFlagService,
type LinkEventType,
type ToolbarAction,
type ToolbarActionGroup,
@@ -145,26 +149,20 @@ const builtinToolbarConfig = {
if (!model) return true;
const url = model.props.url;
// check if the url can be embedded as iframe block
const featureFlag = ctx.std.get(FeatureFlagService);
const embedIframeService = ctx.std.get(EmbedIframeService);
const isEmbedIframeEnabled = featureFlag.getFlag(
'enable_embed_iframe_block'
);
const canEmbedAsIframe =
isEmbedIframeEnabled && embedIframeService.canEmbed(url);
// check if the url can be embedded as iframe block or other embed blocks
const options = ctx.std
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
return !canEmbedAsIframe && options?.viewType !== 'embed';
return (
!canEmbedAsIframe(ctx.std, url) && options?.viewType !== 'embed'
);
},
run(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return;
const { caption, url, style } = model.props;
const { caption, url, title, description, style } = model.props;
const { parent } = model;
const index = parent?.children.indexOf(model);
if (!parent) return;
@@ -172,14 +170,10 @@ const builtinToolbarConfig = {
let blockId: string | undefined;
// first try to embed as iframe block
const featureFlag = ctx.std.get(FeatureFlagService);
const isEmbedIframeEnabled = featureFlag.getFlag(
'enable_embed_iframe_block'
);
const embedIframeService = ctx.std.get(EmbedIframeService);
if (isEmbedIframeEnabled && embedIframeService.canEmbed(url)) {
if (canEmbedAsIframe(ctx.std, url)) {
const embedIframeService = ctx.std.get(EmbedIframeService);
blockId = embedIframeService.addEmbedIframeBlock(
{ url, caption },
{ url, caption, title, description },
parent.id,
index
);
@@ -201,6 +195,8 @@ const builtinToolbarConfig = {
{
url,
caption,
title,
description,
style: newStyle,
},
parent,
@@ -379,30 +375,62 @@ const builtinSurfaceToolbarConfig = {
if (!model) return;
const { id: oldId, xywh, parent } = model;
const { url, caption } = model.props;
const options = ctx.std
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
const { url, caption, title, description } = model.props;
if (options?.viewType !== 'embed') return;
let newId: string | undefined;
const { flavour, styles } = options;
let { style } = model.props;
// first try to embed as iframe block
if (canEmbedAsIframe(ctx.std, url)) {
const embedIframeService = ctx.std.get(EmbedIframeService);
const config = embedIframeService.getConfig(url);
if (!config) {
return;
}
if (!styles.includes(style)) {
style = styles[0];
const bound = Bound.deserialize(xywh);
const options = config.options;
const { widthInSurface, heightInSurface } = options ?? {};
bound.w = widthInSurface ?? EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE;
bound.h =
heightInSurface ?? EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE;
newId = ctx.store.addBlock(
'affine:embed-iframe',
{ url, caption, title, description, xywh: bound.serialize() },
parent
);
} else {
const options = ctx.std
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
if (options?.viewType !== 'embed') return;
const { flavour, styles } = options;
let { style } = model.props;
if (!styles.includes(style)) {
style = styles[0];
}
const bound = Bound.deserialize(xywh);
bound.w = EMBED_CARD_WIDTH[style];
bound.h = EMBED_CARD_HEIGHT[style];
newId = ctx.store.addBlock(
flavour,
{
url,
caption,
title,
description,
style,
xywh: bound.serialize(),
},
parent
);
}
const bounds = Bound.deserialize(xywh);
bounds.w = EMBED_CARD_WIDTH[style];
bounds.h = EMBED_CARD_HEIGHT[style];
const newId = ctx.store.addBlock(
flavour,
{ url, caption, style, xywh: bounds.serialize() },
parent
);
ctx.command.exec(reassociateConnectorsCommand, { oldId, newId });
ctx.store.deleteBlock(model);
@@ -427,7 +455,7 @@ const builtinSurfaceToolbarConfig = {
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
return options?.viewType === 'embed';
return canEmbedAsIframe(ctx.std, url) || options?.viewType === 'embed';
},
content(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
@@ -1,5 +1,10 @@
export * from './adapters';
export * from './commands';
export * from './configs';
export {
EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE,
EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE,
} from './consts';
export * from './embed-iframe-block';
export * from './embed-iframe-spec';
export { canEmbedAsIframe } from './utils';
@@ -1,3 +1,9 @@
import {
EmbedIframeService,
FeatureFlagService,
} from '@blocksuite/affine-shared/services';
import type { BlockStdScope } from '@blocksuite/block-std';
/**
* The options for the embed iframe url validation
*/
@@ -59,3 +65,16 @@ export function safeGetIframeSrc(htmlString: string): string | undefined {
return undefined;
}
}
/**
* Check if the url can be embedded as an iframe
* @param std The block std scope
* @param url The url to check
* @returns Whether the url can be embedded as an iframe
*/
export function canEmbedAsIframe(std: BlockStdScope, url: string) {
const featureFlag = std.get(FeatureFlagService);
const isEmbedIframeEnabled = featureFlag.getFlag('enable_embed_iframe_block');
const embedIframeService = std.get(EmbedIframeService);
return isEmbedIframeEnabled && embedIframeService.canEmbed(url);
}