mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-11 23:26:30 +08:00
feat(editor): support embed iframe block in edgeless (#10830)
To close: [BS-2665](https://linear.app/affine-design/issue/BS-2665/iframe-embed-block-edgeless-mode-支持) [BS-2666](https://linear.app/affine-design/issue/BS-2666/iframe-embed-block-edgeless-toolbar) [BS-2667](https://linear.app/affine-design/issue/BS-2667/iframe-embed-block-edgeless-mode-拖拽调整支持) [BS-2789](https://linear.app/affine-design/issue/BS-2789/iframe-embed-block-edgeless-block-component)
This commit is contained in:
@@ -5,7 +5,10 @@ import type { EmbedCardStyle } from '@blocksuite/affine-model';
|
||||
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';
|
||||
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 embedOptions = std.get(EmbedOptionProvider).getEmbedBlockOptions(url);
|
||||
|
||||
@@ -16,6 +19,7 @@ export const insertBookmarkCommand: Command<{ url: string }> = (ctx, next) => {
|
||||
flavour = embedOptions.flavour;
|
||||
targetStyle = embedOptions.styles[0];
|
||||
}
|
||||
insertEmbedCard(std, { flavour, targetStyle, props });
|
||||
next();
|
||||
const blockId = insertEmbedCard(std, { flavour, targetStyle, props });
|
||||
if (!blockId) return;
|
||||
next({ blockId, flavour });
|
||||
};
|
||||
|
||||
+33
-5
@@ -1,8 +1,13 @@
|
||||
import {
|
||||
type InsertedLinkType,
|
||||
insertEmbedIframeCommand,
|
||||
insertEmbedLinkedDocCommand,
|
||||
type LinkableFlavour,
|
||||
} 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 { insertBookmarkCommand } from './insert-bookmark';
|
||||
@@ -36,10 +41,33 @@ export const insertLinkByQuickSearchCommand: Command<
|
||||
|
||||
// add normal link;
|
||||
if ('externalUrl' in result) {
|
||||
std.command.exec(insertBookmarkCommand, { url: result.externalUrl });
|
||||
return {
|
||||
flavour: 'affine:bookmark',
|
||||
};
|
||||
const featureFlagService = std.get(FeatureFlagService);
|
||||
const enableEmbedIframeBlock = featureFlagService.getFlag(
|
||||
'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;
|
||||
|
||||
@@ -48,7 +48,13 @@ export function insertEmbedCard(
|
||||
const parent = host.doc.getParent(block.model);
|
||||
if (!parent) return;
|
||||
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 {
|
||||
const rootId = std.store.root?.id;
|
||||
if (!rootId) return;
|
||||
@@ -85,5 +91,7 @@ export function insertEmbedCard(
|
||||
// @ts-expect-error FIXME: resolve after gfx tool refactor
|
||||
'default'
|
||||
);
|
||||
|
||||
return cardId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { EmbedIframeLinkEditPopup } from './embed-iframe-block/components/embed-iframe-link-edit-popup';
|
||||
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 { EmbedLinkedDocBlockComponent } from './embed-linked-doc-block';
|
||||
import { EmbedEdgelessLinkedDocBlockComponent } from './embed-linked-doc-block/embed-edgeless-linked-doc-block';
|
||||
@@ -78,6 +79,10 @@ export function effects() {
|
||||
EmbedSyncedDocBlockComponent
|
||||
);
|
||||
|
||||
customElements.define(
|
||||
'affine-embed-edgeless-iframe-block',
|
||||
EmbedEdgelessIframeBlockComponent
|
||||
);
|
||||
customElements.define('affine-embed-iframe-block', EmbedIframeBlockComponent);
|
||||
customElements.define(
|
||||
'affine-embed-iframe-create-modal',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './insert-embed-iframe';
|
||||
+109
@@ -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 });
|
||||
};
|
||||
+7
@@ -19,6 +19,7 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
|
||||
}
|
||||
|
||||
.affine-embed-iframe-error-card {
|
||||
container: affine-embed-iframe-error-card / inline-size;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
@@ -124,6 +125,12 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
|
||||
width: 204px;
|
||||
height: 102px;
|
||||
}
|
||||
|
||||
@container affine-embed-iframe-error-card (width < 480px) {
|
||||
.error-banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
+7
@@ -14,6 +14,7 @@ export class EmbedIframeLoadingCard extends LitElement {
|
||||
}
|
||||
|
||||
.affine-embed-iframe-loading-card {
|
||||
container: affine-embed-iframe-loading-card / inline-size;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
@@ -86,6 +87,12 @@ export class EmbedIframeLoadingCard extends LitElement {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container affine-embed-iframe-loading-card (width < 480px) {
|
||||
.loading-banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
+8
-4
@@ -4,8 +4,10 @@ import {
|
||||
validateEmbedIframeUrl,
|
||||
} from '../../utils';
|
||||
|
||||
const GOOGLE_DRIVE_DEFAULT_WIDTH = '100%';
|
||||
const GOOGLE_DRIVE_DEFAULT_HEIGHT = '480px';
|
||||
const GOOGLE_DRIVE_DEFAULT_WIDTH_IN_SURFACE = 640;
|
||||
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 =
|
||||
'https://drive.google.com/embeddedfolderview';
|
||||
const GOOGLE_DRIVE_EMBED_FILE_URL = 'https://drive.google.com/file/d/';
|
||||
@@ -181,8 +183,10 @@ const googleDriveConfig = {
|
||||
},
|
||||
useOEmbedUrlDirectly: true,
|
||||
options: {
|
||||
defaultWidth: GOOGLE_DRIVE_DEFAULT_WIDTH,
|
||||
defaultHeight: GOOGLE_DRIVE_DEFAULT_HEIGHT,
|
||||
widthInSurface: GOOGLE_DRIVE_DEFAULT_WIDTH_IN_SURFACE,
|
||||
heightInSurface: GOOGLE_DRIVE_DEFAULT_HEIGHT_IN_SURFACE,
|
||||
widthPercent: GOOGLE_DRIVE_DEFAULT_WIDTH_PERCENT,
|
||||
heightInNote: GOOGLE_DRIVE_DEFAULT_HEIGHT_IN_NOTE,
|
||||
allowFullscreen: true,
|
||||
style: 'border: none; border-radius: 8px;',
|
||||
},
|
||||
|
||||
+8
-4
@@ -4,8 +4,10 @@ import {
|
||||
validateEmbedIframeUrl,
|
||||
} from '../../utils';
|
||||
|
||||
const SPOTIFY_DEFAULT_WIDTH = '100%';
|
||||
const SPOTIFY_DEFAULT_HEIGHT = '152px';
|
||||
const SPOTIFY_DEFAULT_WIDTH_IN_SURFACE = 640;
|
||||
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
|
||||
const spotifyEndpoint = 'https://open.spotify.com/oembed';
|
||||
@@ -30,8 +32,10 @@ const spotifyConfig = {
|
||||
},
|
||||
useOEmbedUrlDirectly: false,
|
||||
options: {
|
||||
defaultWidth: SPOTIFY_DEFAULT_WIDTH,
|
||||
defaultHeight: SPOTIFY_DEFAULT_HEIGHT,
|
||||
widthInSurface: SPOTIFY_DEFAULT_WIDTH_IN_SURFACE,
|
||||
heightInSurface: SPOTIFY_DEFAULT_HEIGHT_IN_SURFACE,
|
||||
heightInNote: SPOTIFY_DEFAULT_HEIGHT_IN_NOTE,
|
||||
widthPercent: SPOTIFY_DEFAULT_WIDTH_PERCENT,
|
||||
allow:
|
||||
'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture',
|
||||
style: 'border-radius: 8px;',
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE = 752;
|
||||
export const EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE = 116;
|
||||
+57
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
CaptionedBlockComponent,
|
||||
SelectedStyle,
|
||||
@@ -7,13 +8,13 @@ import {
|
||||
FeatureFlagService,
|
||||
LinkPreviewerService,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import { BlockSelection } from '@blocksuite/block-std';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
|
||||
import { html, nothing } from 'lit';
|
||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.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 { EmbedIframeService } from './extension/embed-iframe-service.js';
|
||||
@@ -21,6 +22,7 @@ import { embedIframeBlockStyles } from './style.js';
|
||||
|
||||
export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error';
|
||||
const DEFAULT_IFRAME_HEIGHT = 152;
|
||||
const DEFAULT_IFRAME_WIDTH = '100%';
|
||||
|
||||
export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIframeBlockModel> {
|
||||
selectedStyle$: ReadonlySignal<ClassInfo> | null = computed<ClassInfo>(
|
||||
@@ -40,6 +42,18 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
readonly hasError$ = computed(() => this.status$.value === 'error');
|
||||
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;
|
||||
|
||||
get embedIframeService() {
|
||||
@@ -50,6 +64,16 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
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 = () => {
|
||||
const link = this.model.url;
|
||||
window.open(link, '_blank');
|
||||
@@ -134,8 +158,12 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
selectionManager.setGroup('note', [blockSelection]);
|
||||
};
|
||||
|
||||
protected readonly _handleClick = (event: MouseEvent) => {
|
||||
protected _handleClick = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
// We don't need to select the block when the block is in the surface
|
||||
if (this.inSurface) {
|
||||
return;
|
||||
}
|
||||
this._selectBlock();
|
||||
};
|
||||
|
||||
@@ -143,27 +171,25 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
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 = () => {
|
||||
const { iframeUrl } = this.model;
|
||||
const {
|
||||
defaultWidth,
|
||||
defaultHeight,
|
||||
widthPercent,
|
||||
heightInNote,
|
||||
style,
|
||||
allow,
|
||||
referrerpolicy,
|
||||
scrolling,
|
||||
allowFullscreen,
|
||||
} = 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`
|
||||
<iframe
|
||||
width=${defaultWidth ?? '100%'}
|
||||
height=${defaultHeight ?? DEFAULT_IFRAME_HEIGHT}
|
||||
width=${width ?? DEFAULT_IFRAME_WIDTH}
|
||||
height=${height ?? DEFAULT_IFRAME_HEIGHT}
|
||||
?allowfullscreen=${allowFullscreen}
|
||||
loading="lazy"
|
||||
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() {
|
||||
if (!this._embedIframeBlockEnabled$.value) {
|
||||
if (!this.isEmbedIframeBlockEnabled) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const classes = classMap({
|
||||
'affine-embed-iframe-block': true,
|
||||
const containerClasses = classMap({
|
||||
'affine-embed-iframe-block-container': true,
|
||||
...this.selectedStyle$?.value,
|
||||
'in-surface': this.inSurface,
|
||||
});
|
||||
|
||||
const style = styleMap({
|
||||
width: '100%',
|
||||
const overlayClasses = classMap({
|
||||
'affine-embed-iframe-block-overlay': true,
|
||||
show: this.showOverlay$.value,
|
||||
});
|
||||
|
||||
return html`
|
||||
<div
|
||||
draggable=${this.blockDraggable ? 'true' : 'false'}
|
||||
class=${classes}
|
||||
style=${style}
|
||||
class=${containerClasses}
|
||||
@click=${this._handleClick}
|
||||
@dblclick=${this._handleDoubleClick}
|
||||
>
|
||||
${this._renderContent()}
|
||||
|
||||
<!-- overlay to prevent the iframe from capturing pointer events -->
|
||||
<div class=${overlayClasses}></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
+8
-10
@@ -8,21 +8,19 @@ import type { ExtensionType } from '@blocksuite/store';
|
||||
* The options for the iframe
|
||||
* @example
|
||||
* {
|
||||
* defaultWidth: '100%',
|
||||
* defaultHeight: '152px',
|
||||
* widthInSurface: 640,
|
||||
* heightInSurface: 152,
|
||||
* heightInNote: 152,
|
||||
* widthPercent: 100,
|
||||
* style: 'border-radius: 12px;',
|
||||
* 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 = {
|
||||
defaultWidth?: string;
|
||||
defaultHeight?: string;
|
||||
widthInSurface: number; // the default width of embed iframe in surface, in pixels
|
||||
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;
|
||||
referrerpolicy?: string;
|
||||
scrolling?: boolean;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './adapters';
|
||||
export * from './commands';
|
||||
export * from './components/embed-iframe-create-modal';
|
||||
export * from './configs';
|
||||
export * from './embed-iframe-block';
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import { css } from 'lit';
|
||||
|
||||
export const embedIframeBlockStyles = css`
|
||||
.affine-embed-iframe-block {
|
||||
.affine-embed-iframe-block-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
align-items: 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;
|
||||
}
|
||||
`;
|
||||
|
||||
+20
-7
@@ -3,19 +3,32 @@ import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
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 = {
|
||||
flavour?: 'affine:bookmark' | 'affine:embed-linked-doc';
|
||||
flavour: LinkableFlavour;
|
||||
} | null;
|
||||
|
||||
export const insertEmbedLinkedDocCommand: Command<{
|
||||
docId: string;
|
||||
params?: ReferenceParams;
|
||||
}> = (ctx, next) => {
|
||||
export const insertEmbedLinkedDocCommand: Command<
|
||||
{
|
||||
docId: string;
|
||||
params?: ReferenceParams;
|
||||
},
|
||||
{ blockId: string }
|
||||
> = (ctx, next) => {
|
||||
const { docId, params, std } = ctx;
|
||||
const flavour = 'affine:embed-linked-doc';
|
||||
const targetStyle: EmbedCardStyle = 'vertical';
|
||||
const props: Record<string, unknown> = { pageId: docId };
|
||||
if (params) props.params = params;
|
||||
insertEmbedCard(std, { flavour, targetStyle, props });
|
||||
next();
|
||||
const blockId = insertEmbedCard(std, { flavour, targetStyle, props });
|
||||
if (!blockId) return;
|
||||
next({ blockId });
|
||||
};
|
||||
|
||||
@@ -441,6 +441,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
this.registerBlock('affine:embed-html', this._createHtmlEmbedBlock);
|
||||
this.registerBlock('affine:embed-loom', this._createLoomEmbedBlock);
|
||||
this.registerBlock('affine:embed-youtube', this._createYoutubeEmbedBlock);
|
||||
this.registerBlock('affine:embed-iframe', this._createIframeEmbedBlock);
|
||||
|
||||
// internal links
|
||||
this.registerBlock(
|
||||
@@ -881,6 +882,36 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
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(
|
||||
edgeless: EdgelessRootBlockComponent,
|
||||
bound: IBound,
|
||||
|
||||
Reference in New Issue
Block a user