import { createLitPortal } from '@blocksuite/affine-components/portal'; import type { EmbedIframeBlockModel } from '@blocksuite/affine-model'; import { DocModeProvider, TelemetryProvider, } from '@blocksuite/affine-shared/services'; import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { WithDisposable } from '@blocksuite/global/lit'; import { EditIcon, InformationIcon, ResetIcon } from '@blocksuite/icons/lit'; import type { BlockStdScope } from '@blocksuite/std'; import { flip, offset } from '@floating-ui/dom'; import { baseTheme } from '@toeverything/theme'; import { css, html, LitElement, nothing, unsafeCSS } from 'lit'; import { property, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { styleMap } from 'lit/directives/style-map.js'; import { ERROR_CARD_DEFAULT_HEIGHT } from '../consts'; import type { EmbedIframeStatusCardOptions } from '../types'; const LINK_EDIT_POPUP_OFFSET = 12; export class EmbedIframeErrorCard extends WithDisposable(LitElement) { static override styles = css` :host { width: 100%; height: 100%; } .affine-embed-iframe-error-card { container: affine-embed-iframe-error-card / size; display: flex; box-sizing: border-box; user-select: none; padding: 12px; gap: 12px; overflow: hidden; border-radius: 8px; border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')}; background: ${unsafeCSSVarV2('layer/background/secondary')}; font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; user-select: none; .error-content { display: flex; flex-direction: column; gap: 4px; .error-title { display: flex; align-items: center; gap: 8px; overflow: hidden; .error-icon { display: flex; justify-content: center; align-items: center; color: ${unsafeCSSVarV2('status/error')}; } .error-title-text { color: ${unsafeCSSVarV2('text/primary')}; text-align: justify; /* Client/smBold */ font-size: var(--affine-font-sm); font-style: normal; font-weight: 600; line-height: 22px; /* 157.143% */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } } .error-message { display: flex; align-self: stretch; color: ${unsafeCSSVarV2('text/secondary')}; overflow: hidden; font-feature-settings: 'liga' off, 'clig' off; text-overflow: ellipsis; /* Client/xs */ font-size: var(--affine-font-xs); font-style: normal; font-weight: 400; line-height: 20px; /* 166.667% */ } .error-info { display: flex; align-items: center; gap: 8px; overflow: hidden; .button { display: flex; padding: 0px 4px; align-items: center; border-radius: 4px; cursor: pointer; .icon { display: flex; justify-content: center; align-items: center; } .text { padding: 0px 4px; font-size: var(--affine-font-xs); font-style: normal; font-weight: 500; line-height: 20px; /* 166.667% */ } } .button.edit { color: ${unsafeCSSVarV2('text/secondary')}; } .button.retry { color: ${unsafeCSSVarV2('text/emphasis')}; } } } } .affine-embed-iframe-error-card.horizontal { flex-direction: row; align-items: flex-start; .error-content { align-items: flex-start; flex: 1 0 0; .error-message { height: 40px; align-items: flex-start; } } @container affine-embed-iframe-error-card (width < 480px) { .error-banner { display: none; } } } .affine-embed-iframe-error-card.vertical { flex-direction: column-reverse; align-items: center; justify-content: center; .error-content { justify-content: center; align-items: center; .error-message { justify-content: center; align-items: center; } } .icon-box { svg { transform: scale(1.6) translateY(-14px); } } @container affine-embed-iframe-error-card (height < 300px) or (width < 300px) { .error-banner { display: none; } } } `; private _editAbortController: AbortController | null = null; private readonly _toggleEdit = (e: MouseEvent) => { e.stopPropagation(); if (!this._editButton || this.readonly) { return; } if (this._editAbortController) { this._editAbortController.abort(); } this._editAbortController = new AbortController(); createLitPortal({ template: html``, container: document.body, computePosition: { referenceElement: this._editButton, placement: 'bottom-start', middleware: [flip(), offset(LINK_EDIT_POPUP_OFFSET)], autoUpdate: { animationFrame: true }, }, abortController: this._editAbortController, closeOnClickAway: true, }); }; private readonly _handleRetry = async (e: MouseEvent) => { e.stopPropagation(); const success = await this.onRetry(); // track retry event this.telemetryService?.track('ReloadLink', { type: 'embed iframe block', page: this.editorMode === 'page' ? 'doc editor' : 'whiteboard editor', segment: 'editor', module: 'embed block', control: 'reload button', result: success ? 'success' : 'failure', }); }; override render() { const { layout, width, height } = this.options; const cardClasses = classMap({ 'affine-embed-iframe-error-card': true, horizontal: layout === 'horizontal', vertical: layout === 'vertical', }); const cardWidth = width ? `${width}px` : '100%'; const cardHeight = height ? `${height}px` : '100%'; const cardStyle = styleMap({ width: cardWidth, height: cardHeight, }); return html`
${InformationIcon({ width: '16px', height: '16px' })} This link couldn’t be loaded.
${this.error?.message || 'Failed to load embedded content'}
${this.readonly ? nothing : html`
${EditIcon({ width: '16px', height: '16px' })} Edit
`}
${ResetIcon({ width: '16px', height: '16px' })} Reload
${EmbedIframeErrorIcon}
`; } get host() { return this.std.host; } get readonly() { return this.model.store.readonly; } get telemetryService() { return this.std.getOptional(TelemetryProvider); } get editorMode() { const docModeService = this.std.get(DocModeProvider); const mode = docModeService.getEditorMode(); return mode ?? 'page'; } @query('.button.edit') accessor _editButton: HTMLElement | null = null; @property({ attribute: false }) accessor error: Error | null = null; @property({ attribute: false }) accessor onRetry!: () => Promise; @property({ attribute: false }) accessor model!: EmbedIframeBlockModel; @property({ attribute: false }) accessor std!: BlockStdScope; @property({ attribute: false }) accessor inSurface = false; @property({ attribute: false }) accessor options: EmbedIframeStatusCardOptions = { layout: 'horizontal', height: ERROR_CARD_DEFAULT_HEIGHT, }; } export const EmbedIframeErrorIcon = html` `;