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

View File

@@ -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>
`;
}