donteatfriedrice
2025-03-15 09:23:02 +00:00
parent b4f49a234f
commit 1d4ee1e383
18 changed files with 397 additions and 55 deletions

View File

@@ -5,7 +5,10 @@ import type { EmbedCardStyle } from '@blocksuite/affine-model';
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services'; import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';
import type { Command } from '@blocksuite/block-std'; import type { Command } from '@blocksuite/block-std';
export const insertBookmarkCommand: Command<{ url: string }> = (ctx, next) => { export const insertBookmarkCommand: Command<
{ url: string },
{ blockId: string; flavour: string }
> = (ctx, next) => {
const { url, std } = ctx; const { url, std } = ctx;
const embedOptions = std.get(EmbedOptionProvider).getEmbedBlockOptions(url); const embedOptions = std.get(EmbedOptionProvider).getEmbedBlockOptions(url);
@@ -16,6 +19,7 @@ export const insertBookmarkCommand: Command<{ url: string }> = (ctx, next) => {
flavour = embedOptions.flavour; flavour = embedOptions.flavour;
targetStyle = embedOptions.styles[0]; targetStyle = embedOptions.styles[0];
} }
insertEmbedCard(std, { flavour, targetStyle, props }); const blockId = insertEmbedCard(std, { flavour, targetStyle, props });
next(); if (!blockId) return;
next({ blockId, flavour });
}; };

View File

@@ -1,8 +1,13 @@
import { import {
type InsertedLinkType, type InsertedLinkType,
insertEmbedIframeCommand,
insertEmbedLinkedDocCommand, insertEmbedLinkedDocCommand,
type LinkableFlavour,
} from '@blocksuite/affine-block-embed'; } from '@blocksuite/affine-block-embed';
import { QuickSearchProvider } from '@blocksuite/affine-shared/services'; import {
FeatureFlagService,
QuickSearchProvider,
} from '@blocksuite/affine-shared/services';
import type { Command } from '@blocksuite/block-std'; import type { Command } from '@blocksuite/block-std';
import { insertBookmarkCommand } from './insert-bookmark'; import { insertBookmarkCommand } from './insert-bookmark';
@@ -36,10 +41,33 @@ export const insertLinkByQuickSearchCommand: Command<
// add normal link; // add normal link;
if ('externalUrl' in result) { if ('externalUrl' in result) {
std.command.exec(insertBookmarkCommand, { url: result.externalUrl }); const featureFlagService = std.get(FeatureFlagService);
return { const enableEmbedIframeBlock = featureFlagService.getFlag(
flavour: 'affine:bookmark', 'enable_embed_iframe_block'
}; );
if (enableEmbedIframeBlock) {
// try to insert embed iframe block first
const [success, { flavour }] = std.command
.chain()
.try(chain => [
chain.pipe(insertEmbedIframeCommand, { url: result.externalUrl }),
chain.pipe(insertBookmarkCommand, { url: result.externalUrl }),
])
.run();
if (!success || !flavour) return null;
return {
flavour: flavour as LinkableFlavour,
};
} else {
const [success, { flavour }] = std.command.exec(
insertBookmarkCommand,
{ url: result.externalUrl }
);
if (!success || !flavour) return null;
return {
flavour: flavour as LinkableFlavour,
};
}
} }
return null; return null;

View File

@@ -48,7 +48,13 @@ export function insertEmbedCard(
const parent = host.doc.getParent(block.model); const parent = host.doc.getParent(block.model);
if (!parent) return; if (!parent) return;
const index = parent.children.indexOf(block.model); const index = parent.children.indexOf(block.model);
host.doc.addBlock(flavour as never, props, parent, index + 1); const cardId = host.doc.addBlock(
flavour as never,
props,
parent,
index + 1
);
return cardId;
} else { } else {
const rootId = std.store.root?.id; const rootId = std.store.root?.id;
if (!rootId) return; if (!rootId) return;
@@ -85,5 +91,7 @@ export function insertEmbedCard(
// @ts-expect-error FIXME: resolve after gfx tool refactor // @ts-expect-error FIXME: resolve after gfx tool refactor
'default' 'default'
); );
return cardId;
} }
} }

View File

@@ -9,6 +9,7 @@ import { EmbedIframeCreateModal } from './embed-iframe-block/components/embed-if
import { EmbedIframeErrorCard } from './embed-iframe-block/components/embed-iframe-error-card'; import { EmbedIframeErrorCard } from './embed-iframe-block/components/embed-iframe-error-card';
import { EmbedIframeLinkEditPopup } from './embed-iframe-block/components/embed-iframe-link-edit-popup'; import { EmbedIframeLinkEditPopup } from './embed-iframe-block/components/embed-iframe-link-edit-popup';
import { EmbedIframeLoadingCard } from './embed-iframe-block/components/embed-iframe-loading-card'; import { EmbedIframeLoadingCard } from './embed-iframe-block/components/embed-iframe-loading-card';
import { EmbedEdgelessIframeBlockComponent } from './embed-iframe-block/embed-edgeless-iframe-block';
import { EmbedIframeBlockComponent } from './embed-iframe-block/embed-iframe-block'; import { EmbedIframeBlockComponent } from './embed-iframe-block/embed-iframe-block';
import { EmbedLinkedDocBlockComponent } from './embed-linked-doc-block'; import { EmbedLinkedDocBlockComponent } from './embed-linked-doc-block';
import { EmbedEdgelessLinkedDocBlockComponent } from './embed-linked-doc-block/embed-edgeless-linked-doc-block'; import { EmbedEdgelessLinkedDocBlockComponent } from './embed-linked-doc-block/embed-edgeless-linked-doc-block';
@@ -78,6 +79,10 @@ export function effects() {
EmbedSyncedDocBlockComponent EmbedSyncedDocBlockComponent
); );
customElements.define(
'affine-embed-edgeless-iframe-block',
EmbedEdgelessIframeBlockComponent
);
customElements.define('affine-embed-iframe-block', EmbedIframeBlockComponent); customElements.define('affine-embed-iframe-block', EmbedIframeBlockComponent);
customElements.define( customElements.define(
'affine-embed-iframe-create-modal', 'affine-embed-iframe-create-modal',

View File

@@ -0,0 +1 @@
export * from './insert-embed-iframe';

View File

@@ -0,0 +1,109 @@
import {
EdgelessCRUDIdentifier,
SurfaceBlockComponent,
} from '@blocksuite/affine-block-surface';
import {
BlockSelection,
type Command,
SurfaceSelection,
TextSelection,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound, Vec } from '@blocksuite/global/gfx';
import {
EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE,
EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE,
} from '../consts';
import { EmbedIframeService } from '../extension/embed-iframe-service';
export const insertEmbedIframeCommand: Command<
{ url: string },
{ blockId: string; flavour: string }
> = (ctx, next) => {
const { url, std } = ctx;
const embedIframeService = std.get(EmbedIframeService);
if (!embedIframeService || !embedIframeService.canEmbed(url)) {
return;
}
const config = embedIframeService.getConfig(url);
if (!config) {
return;
}
const { host } = std;
const selectionManager = host.selection;
let selectedBlockId: string | undefined;
const textSelection = selectionManager.find(TextSelection);
const blockSelection = selectionManager.find(BlockSelection);
const surfaceSelection = selectionManager.find(SurfaceSelection);
if (textSelection) {
selectedBlockId = textSelection.blockId;
} else if (blockSelection) {
selectedBlockId = blockSelection.blockId;
} else if (surfaceSelection && surfaceSelection.editing) {
selectedBlockId = surfaceSelection.blockId;
}
const flavour = 'affine:embed-iframe';
const props: Record<string, unknown> = { url };
// When there is a selected block, it means that the selection is in note or edgeless text
// we should insert the embed iframe block after the selected block and only need the url prop
let newBlockId: string | undefined;
if (selectedBlockId) {
const block = host.view.getBlock(selectedBlockId);
if (!block) return;
const parent = host.doc.getParent(block.model);
if (!parent) return;
const index = parent.children.indexOf(block.model);
newBlockId = host.doc.addBlock(flavour, props, parent, index + 1);
} else {
// When there is no selected block and in edgeless mode
// We should insert the embed iframe block to surface
// It means that not only the url prop but also the xywh prop is needed
const rootId = std.store.root?.id;
if (!rootId) return;
const edgelessRoot = std.view.getBlock(rootId);
if (!edgelessRoot) return;
const gfx = std.get(GfxControllerIdentifier);
const crud = std.get(EdgelessCRUDIdentifier);
gfx.viewport.smoothZoom(1);
const surfaceBlock = gfx.surfaceComponent;
if (!(surfaceBlock instanceof SurfaceBlockComponent)) return;
const options = config.options;
const { widthInSurface, heightInSurface } = options ?? {};
const width = widthInSurface ?? EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE;
const height = heightInSurface ?? EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE;
const center = Vec.toVec(surfaceBlock.renderer.viewport.center);
const xywh = Bound.fromCenter(center, width, height).serialize();
newBlockId = crud.addBlock(
flavour,
{
...props,
xywh,
},
surfaceBlock.model
);
gfx.selection.set({
elements: [newBlockId],
editing: false,
});
gfx.tool.setTool(
// @ts-expect-error FIXME: resolve after gfx tool refactor
'default'
);
}
if (!newBlockId) {
return;
}
next({ blockId: newBlockId, flavour });
};

View File

@@ -19,6 +19,7 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
} }
.affine-embed-iframe-error-card { .affine-embed-iframe-error-card {
container: affine-embed-iframe-error-card / inline-size;
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
@@ -124,6 +125,12 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
width: 204px; width: 204px;
height: 102px; height: 102px;
} }
@container affine-embed-iframe-error-card (width < 480px) {
.error-banner {
display: none;
}
}
} }
`; `;

View File

@@ -14,6 +14,7 @@ export class EmbedIframeLoadingCard extends LitElement {
} }
.affine-embed-iframe-loading-card { .affine-embed-iframe-loading-card {
container: affine-embed-iframe-loading-card / inline-size;
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
@@ -86,6 +87,12 @@ export class EmbedIframeLoadingCard extends LitElement {
} }
} }
} }
@container affine-embed-iframe-loading-card (width < 480px) {
.loading-banner {
display: none;
}
}
} }
`; `;

View File

@@ -4,8 +4,10 @@ import {
validateEmbedIframeUrl, validateEmbedIframeUrl,
} from '../../utils'; } from '../../utils';
const GOOGLE_DRIVE_DEFAULT_WIDTH = '100%'; const GOOGLE_DRIVE_DEFAULT_WIDTH_IN_SURFACE = 640;
const GOOGLE_DRIVE_DEFAULT_HEIGHT = '480px'; const GOOGLE_DRIVE_DEFAULT_HEIGHT_IN_SURFACE = 480;
const GOOGLE_DRIVE_DEFAULT_WIDTH_PERCENT = 100;
const GOOGLE_DRIVE_DEFAULT_HEIGHT_IN_NOTE = 480;
const GOOGLE_DRIVE_EMBED_FOLDER_URL = const GOOGLE_DRIVE_EMBED_FOLDER_URL =
'https://drive.google.com/embeddedfolderview'; 'https://drive.google.com/embeddedfolderview';
const GOOGLE_DRIVE_EMBED_FILE_URL = 'https://drive.google.com/file/d/'; const GOOGLE_DRIVE_EMBED_FILE_URL = 'https://drive.google.com/file/d/';
@@ -181,8 +183,10 @@ const googleDriveConfig = {
}, },
useOEmbedUrlDirectly: true, useOEmbedUrlDirectly: true,
options: { options: {
defaultWidth: GOOGLE_DRIVE_DEFAULT_WIDTH, widthInSurface: GOOGLE_DRIVE_DEFAULT_WIDTH_IN_SURFACE,
defaultHeight: GOOGLE_DRIVE_DEFAULT_HEIGHT, heightInSurface: GOOGLE_DRIVE_DEFAULT_HEIGHT_IN_SURFACE,
widthPercent: GOOGLE_DRIVE_DEFAULT_WIDTH_PERCENT,
heightInNote: GOOGLE_DRIVE_DEFAULT_HEIGHT_IN_NOTE,
allowFullscreen: true, allowFullscreen: true,
style: 'border: none; border-radius: 8px;', style: 'border: none; border-radius: 8px;',
}, },

View File

@@ -4,8 +4,10 @@ import {
validateEmbedIframeUrl, validateEmbedIframeUrl,
} from '../../utils'; } from '../../utils';
const SPOTIFY_DEFAULT_WIDTH = '100%'; const SPOTIFY_DEFAULT_WIDTH_IN_SURFACE = 640;
const SPOTIFY_DEFAULT_HEIGHT = '152px'; const SPOTIFY_DEFAULT_HEIGHT_IN_SURFACE = 152;
const SPOTIFY_DEFAULT_HEIGHT_IN_NOTE = 152;
const SPOTIFY_DEFAULT_WIDTH_PERCENT = 100;
// https://developer.spotify.com/documentation/embeds/reference/oembed // https://developer.spotify.com/documentation/embeds/reference/oembed
const spotifyEndpoint = 'https://open.spotify.com/oembed'; const spotifyEndpoint = 'https://open.spotify.com/oembed';
@@ -30,8 +32,10 @@ const spotifyConfig = {
}, },
useOEmbedUrlDirectly: false, useOEmbedUrlDirectly: false,
options: { options: {
defaultWidth: SPOTIFY_DEFAULT_WIDTH, widthInSurface: SPOTIFY_DEFAULT_WIDTH_IN_SURFACE,
defaultHeight: SPOTIFY_DEFAULT_HEIGHT, heightInSurface: SPOTIFY_DEFAULT_HEIGHT_IN_SURFACE,
heightInNote: SPOTIFY_DEFAULT_HEIGHT_IN_NOTE,
widthPercent: SPOTIFY_DEFAULT_WIDTH_PERCENT,
allow: allow:
'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture', 'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture',
style: 'border-radius: 8px;', style: 'border-radius: 8px;',

View File

@@ -0,0 +1,2 @@
export const EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE = 752;
export const EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE = 116;

View File

@@ -0,0 +1,57 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import { toGfxBlockComponent } from '@blocksuite/block-std';
import { Bound } from '@blocksuite/global/gfx';
import { nothing } from 'lit';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import { EmbedIframeBlockComponent } from './embed-iframe-block';
export class EmbedEdgelessIframeBlockComponent extends toGfxBlockComponent(
EmbedIframeBlockComponent
) {
override selectedStyle$ = null;
override blockDraggable = false;
override accessor blockContainerStyles = { margin: '0' };
get edgelessSlots() {
return this.std.get(EdgelessLegacySlotIdentifier);
}
override connectedCallback() {
super.connectedCallback();
this.edgelessSlots.elementResizeStart.subscribe(() => {
this.isResizing$.value = true;
});
this.edgelessSlots.elementResizeEnd.subscribe(() => {
this.isResizing$.value = false;
});
}
override renderGfxBlock() {
if (!this.isEmbedIframeBlockEnabled) {
return nothing;
}
const bound = Bound.deserialize(this.model.xywh$.value);
const scale = this.model.scale$.value;
const width = bound.w / scale;
const height = bound.h / scale;
const style = {
width: `${width}px`,
height: `${height}px`,
transformOrigin: '0 0',
transform: `scale(${scale})`,
};
return html`
<div class="edgeless-embed-iframe-block" style=${styleMap(style)}>
${this.renderPageContent()}
</div>
`;
}
}

View File

@@ -1,3 +1,4 @@
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import { import {
CaptionedBlockComponent, CaptionedBlockComponent,
SelectedStyle, SelectedStyle,
@@ -7,13 +8,13 @@ import {
FeatureFlagService, FeatureFlagService,
LinkPreviewerService, LinkPreviewerService,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { matchModels } from '@blocksuite/affine-shared/utils';
import { BlockSelection } from '@blocksuite/block-std'; import { BlockSelection } from '@blocksuite/block-std';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import { html, nothing } from 'lit'; import { html, nothing } from 'lit';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js'; import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { IframeOptions } from './extension/embed-iframe-config.js'; import type { IframeOptions } from './extension/embed-iframe-config.js';
import { EmbedIframeService } from './extension/embed-iframe-service.js'; import { EmbedIframeService } from './extension/embed-iframe-service.js';
@@ -21,6 +22,7 @@ import { embedIframeBlockStyles } from './style.js';
export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error'; export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error';
const DEFAULT_IFRAME_HEIGHT = 152; const DEFAULT_IFRAME_HEIGHT = 152;
const DEFAULT_IFRAME_WIDTH = '100%';
export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIframeBlockModel> { export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIframeBlockModel> {
selectedStyle$: ReadonlySignal<ClassInfo> | null = computed<ClassInfo>( selectedStyle$: ReadonlySignal<ClassInfo> | null = computed<ClassInfo>(
@@ -40,6 +42,18 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
readonly hasError$ = computed(() => this.status$.value === 'error'); readonly hasError$ = computed(() => this.status$.value === 'error');
readonly isSuccess$ = computed(() => this.status$.value === 'success'); readonly isSuccess$ = computed(() => this.status$.value === 'success');
readonly isDraggingOnHost$ = signal(false);
readonly isResizing$ = signal(false);
// show overlay to prevent the iframe from capturing pointer events
// when the block is dragging, resizing, or not selected
readonly showOverlay$ = computed(
() =>
this.isSuccess$.value &&
(this.isDraggingOnHost$.value ||
this.isResizing$.value ||
!this.selected$.value)
);
private _iframeOptions: IframeOptions | undefined = undefined; private _iframeOptions: IframeOptions | undefined = undefined;
get embedIframeService() { get embedIframeService() {
@@ -50,6 +64,16 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
return this.std.get(LinkPreviewerService); return this.std.get(LinkPreviewerService);
} }
get inSurface() {
return matchModels(this.model.parent, [SurfaceBlockModel]);
}
get isEmbedIframeBlockEnabled() {
const featureFlagService = this.doc.get(FeatureFlagService);
const flag = featureFlagService.getFlag('enable_embed_iframe_block');
return flag ?? false;
}
open = () => { open = () => {
const link = this.model.url; const link = this.model.url;
window.open(link, '_blank'); window.open(link, '_blank');
@@ -134,8 +158,12 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
selectionManager.setGroup('note', [blockSelection]); selectionManager.setGroup('note', [blockSelection]);
}; };
protected readonly _handleClick = (event: MouseEvent) => { protected _handleClick = (event: MouseEvent) => {
event.stopPropagation(); event.stopPropagation();
// We don't need to select the block when the block is in the surface
if (this.inSurface) {
return;
}
this._selectBlock(); this._selectBlock();
}; };
@@ -143,27 +171,25 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
await this.refreshData(); await this.refreshData();
}; };
private readonly _embedIframeBlockEnabled$: ReadonlySignal = computed(() => {
const featureFlagService = this.doc.get(FeatureFlagService);
const flag = featureFlagService.getFlag('enable_embed_iframe_block');
return flag ?? false;
});
private readonly _renderIframe = () => { private readonly _renderIframe = () => {
const { iframeUrl } = this.model; const { iframeUrl } = this.model;
const { const {
defaultWidth, widthPercent,
defaultHeight, heightInNote,
style, style,
allow, allow,
referrerpolicy, referrerpolicy,
scrolling, scrolling,
allowFullscreen, allowFullscreen,
} = this._iframeOptions ?? {}; } = this._iframeOptions ?? {};
const width = `${widthPercent}%`;
// if the block is in the surface, use 100% as the height
// otherwise, use the heightInNote
const height = this.inSurface ? '100%' : heightInNote;
return html` return html`
<iframe <iframe
width=${defaultWidth ?? '100%'} width=${width ?? DEFAULT_IFRAME_WIDTH}
height=${defaultHeight ?? DEFAULT_IFRAME_HEIGHT} height=${height ?? DEFAULT_IFRAME_HEIGHT}
?allowfullscreen=${allowFullscreen} ?allowfullscreen=${allowFullscreen}
loading="lazy" loading="lazy"
frameborder="0" frameborder="0"
@@ -218,31 +244,51 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
} }
}) })
); );
// subscribe the editor host global dragging event
// to show the overlay for the dragging area or other pointer events
this.handleEvent(
'dragStart',
() => {
this.isDraggingOnHost$.value = true;
},
{ global: true }
);
this.handleEvent(
'dragEnd',
() => {
this.isDraggingOnHost$.value = false;
},
{ global: true }
);
} }
override renderBlock() { override renderBlock() {
if (!this._embedIframeBlockEnabled$.value) { if (!this.isEmbedIframeBlockEnabled) {
return nothing; return nothing;
} }
const classes = classMap({ const containerClasses = classMap({
'affine-embed-iframe-block': true, 'affine-embed-iframe-block-container': true,
...this.selectedStyle$?.value, ...this.selectedStyle$?.value,
'in-surface': this.inSurface,
}); });
const overlayClasses = classMap({
const style = styleMap({ 'affine-embed-iframe-block-overlay': true,
width: '100%', show: this.showOverlay$.value,
}); });
return html` return html`
<div <div
draggable=${this.blockDraggable ? 'true' : 'false'} draggable=${this.blockDraggable ? 'true' : 'false'}
class=${classes} class=${containerClasses}
style=${style}
@click=${this._handleClick} @click=${this._handleClick}
@dblclick=${this._handleDoubleClick} @dblclick=${this._handleDoubleClick}
> >
${this._renderContent()} ${this._renderContent()}
<!-- overlay to prevent the iframe from capturing pointer events -->
<div class=${overlayClasses}></div>
</div> </div>
`; `;
} }

View File

@@ -8,21 +8,19 @@ import type { ExtensionType } from '@blocksuite/store';
* The options for the iframe * The options for the iframe
* @example * @example
* { * {
* defaultWidth: '100%', * widthInSurface: 640,
* defaultHeight: '152px', * heightInSurface: 152,
* heightInNote: 152,
* widthPercent: 100,
* style: 'border-radius: 12px;', * style: 'border-radius: 12px;',
* allow: 'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture', * allow: 'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture',
* } * }
* <iframe
* width="100%"
* height="152px"
* style="border-radius: 12px;"
* allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
* ></iframe>
*/ */
export type IframeOptions = { export type IframeOptions = {
defaultWidth?: string; widthInSurface: number; // the default width of embed iframe in surface, in pixels
defaultHeight?: string; heightInSurface: number; // the default height of embed iframe in surface, in pixels
heightInNote: number; // the default height of embed iframe in note, in pixels
widthPercent: number; // the width percentage of embed iframe relative to parent container width, normalized to 0-100
style?: string; style?: string;
referrerpolicy?: string; referrerpolicy?: string;
scrolling?: boolean; scrolling?: boolean;

View File

@@ -1,4 +1,5 @@
export * from './adapters'; export * from './adapters';
export * from './commands';
export * from './components/embed-iframe-create-modal'; export * from './components/embed-iframe-create-modal';
export * from './configs'; export * from './configs';
export * from './embed-iframe-block'; export * from './embed-iframe-block';

View File

@@ -1,12 +1,29 @@
import { css } from 'lit'; import { css } from 'lit';
export const embedIframeBlockStyles = css` export const embedIframeBlockStyles = css`
.affine-embed-iframe-block { .affine-embed-iframe-block-container {
display: flex; display: flex;
width: 100%; width: 100%;
border-radius: 8px; border-radius: 8px;
user-select: none; user-select: none;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative;
}
.affine-embed-iframe-block-container.in-surface {
height: 100%;
}
.affine-embed-iframe-block-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
}
.affine-embed-iframe-block-overlay.show {
display: block;
} }
`; `;

View File

@@ -3,19 +3,32 @@ import type { Command } from '@blocksuite/block-std';
import { insertEmbedCard } from '../../common/insert-embed-card.js'; import { insertEmbedCard } from '../../common/insert-embed-card.js';
export type LinkableFlavour =
| 'affine:bookmark'
| 'affine:embed-linked-doc'
| 'affine:embed-iframe'
| 'affine:embed-figma'
| 'affine:embed-github'
| 'affine:embed-loom'
| 'affine:embed-youtube';
export type InsertedLinkType = { export type InsertedLinkType = {
flavour?: 'affine:bookmark' | 'affine:embed-linked-doc'; flavour: LinkableFlavour;
} | null; } | null;
export const insertEmbedLinkedDocCommand: Command<{ export const insertEmbedLinkedDocCommand: Command<
docId: string; {
params?: ReferenceParams; docId: string;
}> = (ctx, next) => { params?: ReferenceParams;
},
{ blockId: string }
> = (ctx, next) => {
const { docId, params, std } = ctx; const { docId, params, std } = ctx;
const flavour = 'affine:embed-linked-doc'; const flavour = 'affine:embed-linked-doc';
const targetStyle: EmbedCardStyle = 'vertical'; const targetStyle: EmbedCardStyle = 'vertical';
const props: Record<string, unknown> = { pageId: docId }; const props: Record<string, unknown> = { pageId: docId };
if (params) props.params = params; if (params) props.params = params;
insertEmbedCard(std, { flavour, targetStyle, props }); const blockId = insertEmbedCard(std, { flavour, targetStyle, props });
next(); if (!blockId) return;
next({ blockId });
}; };

View File

@@ -441,6 +441,7 @@ export class EdgelessClipboardController extends PageClipboard {
this.registerBlock('affine:embed-html', this._createHtmlEmbedBlock); this.registerBlock('affine:embed-html', this._createHtmlEmbedBlock);
this.registerBlock('affine:embed-loom', this._createLoomEmbedBlock); this.registerBlock('affine:embed-loom', this._createLoomEmbedBlock);
this.registerBlock('affine:embed-youtube', this._createYoutubeEmbedBlock); this.registerBlock('affine:embed-youtube', this._createYoutubeEmbedBlock);
this.registerBlock('affine:embed-iframe', this._createIframeEmbedBlock);
// internal links // internal links
this.registerBlock( this.registerBlock(
@@ -881,6 +882,36 @@ export class EdgelessClipboardController extends PageClipboard {
return embedYoutubeId; return embedYoutubeId;
} }
private _createIframeEmbedBlock(embedIframe: BlockSnapshot) {
const {
xywh,
caption,
url,
title,
description,
iframeUrl,
scale,
width,
height,
} = embedIframe.props;
return this.crud.addBlock(
'affine:embed-iframe',
{
url,
iframeUrl,
xywh,
caption,
title,
description,
scale,
width,
height,
},
this.surface.model.id
);
}
private async _edgelessToCanvas( private async _edgelessToCanvas(
edgeless: EdgelessRootBlockComponent, edgeless: EdgelessRootBlockComponent,
bound: IBound, bound: IBound,