mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00: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:
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user