diff --git a/blocksuite/affine/blocks/block-bookmark/src/commands/insert-link-by-quick-search.ts b/blocksuite/affine/blocks/block-bookmark/src/commands/insert-link-by-quick-search.ts index 49764f31f9..5f2e6a69e9 100644 --- a/blocksuite/affine/blocks/block-bookmark/src/commands/insert-link-by-quick-search.ts +++ b/blocksuite/affine/blocks/block-bookmark/src/commands/insert-link-by-quick-search.ts @@ -1,6 +1,6 @@ import { type InsertedLinkType, - insertEmbedIframeCommand, + insertEmbedIframeWithUrlCommand, insertEmbedLinkedDocCommand, type LinkableFlavour, } from '@blocksuite/affine-block-embed'; @@ -50,7 +50,9 @@ export const insertLinkByQuickSearchCommand: Command< const [success, { flavour }] = std.command .chain() .try(chain => [ - chain.pipe(insertEmbedIframeCommand, { url: result.externalUrl }), + chain.pipe(insertEmbedIframeWithUrlCommand, { + url: result.externalUrl, + }), chain.pipe(insertBookmarkCommand, { url: result.externalUrl }), ]) .run(); diff --git a/blocksuite/affine/blocks/block-embed/src/effects.ts b/blocksuite/affine/blocks/block-embed/src/effects.ts index 51d4bd7621..a7b477cc67 100644 --- a/blocksuite/affine/blocks/block-embed/src/effects.ts +++ b/blocksuite/affine/blocks/block-embed/src/effects.ts @@ -5,9 +5,10 @@ import { EmbedEdgelessGithubBlockComponent } from './embed-github-block/embed-ed import { EmbedHtmlBlockComponent } from './embed-html-block'; import { EmbedHtmlFullscreenToolbar } from './embed-html-block/components/fullscreen-toolbar'; import { EmbedEdgelessHtmlBlockComponent } from './embed-html-block/embed-edgeless-html-block'; -import { EmbedIframeCreateModal } from './embed-iframe-block/components/embed-iframe-create-modal'; import { EmbedIframeErrorCard } from './embed-iframe-block/components/embed-iframe-error-card'; +import { EmbedIframeIdleCard } from './embed-iframe-block/components/embed-iframe-idle-card'; import { EmbedIframeLinkEditPopup } from './embed-iframe-block/components/embed-iframe-link-edit-popup'; +import { EmbedIframeLinkInputPopup } from './embed-iframe-block/components/embed-iframe-link-input-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'; @@ -85,11 +86,12 @@ export function effects() { ); customElements.define('affine-embed-iframe-block', EmbedIframeBlockComponent); customElements.define( - 'affine-embed-iframe-create-modal', - EmbedIframeCreateModal + 'embed-iframe-link-input-popup', + EmbedIframeLinkInputPopup ); customElements.define('embed-iframe-loading-card', EmbedIframeLoadingCard); customElements.define('embed-iframe-error-card', EmbedIframeErrorCard); + customElements.define('embed-iframe-idle-card', EmbedIframeIdleCard); customElements.define( 'embed-iframe-link-edit-popup', EmbedIframeLinkEditPopup @@ -115,9 +117,10 @@ declare global { 'affine-embed-linked-doc-block': EmbedLinkedDocBlockComponent; 'affine-embed-edgeless-linked-doc-block': EmbedEdgelessLinkedDocBlockComponent; 'affine-embed-iframe-block': EmbedIframeBlockComponent; - 'affine-embed-iframe-create-modal': EmbedIframeCreateModal; + 'embed-iframe-link-input-popup': EmbedIframeLinkInputPopup; 'embed-iframe-loading-card': EmbedIframeLoadingCard; 'embed-iframe-error-card': EmbedIframeErrorCard; + 'embed-iframe-idle-card': EmbedIframeIdleCard; 'embed-iframe-link-edit-popup': EmbedIframeLinkEditPopup; } } diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/index.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/index.ts index 2c69456c91..840a26d4c2 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/index.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/index.ts @@ -1 +1,2 @@ -export * from './insert-embed-iframe'; +export * from './insert-embed-iframe-with-url'; +export * from './insert-empty-embed-iframe'; diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/insert-embed-iframe.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/insert-embed-iframe-with-url.ts similarity index 98% rename from blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/insert-embed-iframe.ts rename to blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/insert-embed-iframe-with-url.ts index d24eeebf73..91f7513370 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/insert-embed-iframe.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/insert-embed-iframe-with-url.ts @@ -17,7 +17,7 @@ import { EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE, } from '../consts'; -export const insertEmbedIframeCommand: Command< +export const insertEmbedIframeWithUrlCommand: Command< { url: string }, { blockId: string; flavour: string } > = (ctx, next) => { diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/insert-empty-embed-iframe.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/insert-empty-embed-iframe.ts new file mode 100644 index 0000000000..cb48b925f4 --- /dev/null +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/commands/insert-empty-embed-iframe.ts @@ -0,0 +1,55 @@ +import type { EmbedIframeBlockProps } from '@blocksuite/affine-model'; +import type { Command } from '@blocksuite/block-std'; +import type { BlockModel } from '@blocksuite/store'; + +import type { EmbedLinkInputPopupOptions } from '../components/embed-iframe-link-input-popup'; +import { EmbedIframeBlockComponent } from '../embed-iframe-block'; + +export const insertEmptyEmbedIframeCommand: Command< + { + place?: 'after' | 'before'; + removeEmptyLine?: boolean; + selectedModels?: BlockModel[]; + linkInputPopupOptions?: EmbedLinkInputPopupOptions; + }, + { + insertedEmbedIframeBlockId: Promise; + } +> = (ctx, next) => { + const { selectedModels, place, removeEmptyLine, std, linkInputPopupOptions } = + ctx; + if (!selectedModels?.length) return; + + const targetModel = + place === 'before' + ? selectedModels[0] + : selectedModels[selectedModels.length - 1]; + + const embedIframeBlockProps: Partial & { + flavour: 'affine:embed-iframe'; + } = { + flavour: 'affine:embed-iframe', + }; + + const result = std.store.addSiblingBlocks( + targetModel, + [embedIframeBlockProps], + place + ); + if (result.length === 0) return; + + if (removeEmptyLine && targetModel.text?.length === 0) { + std.store.deleteBlock(targetModel); + } + + next({ + insertedEmbedIframeBlockId: std.host.updateComplete.then(async () => { + const blockComponent = std.view.getBlock(result[0]); + if (blockComponent instanceof EmbedIframeBlockComponent) { + await blockComponent.updateComplete; + blockComponent.toggleLinkInputPopup(linkInputPopupOptions); + } + return result[0]; + }), + }); +}; diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-create-modal.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-create-modal.ts deleted file mode 100644 index 128218e4e6..0000000000 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-create-modal.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { EmbedIframeService } from '@blocksuite/affine-shared/services'; -import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; -import { isValidUrl, stopPropagation } from '@blocksuite/affine-shared/utils'; -import type { BlockStdScope } from '@blocksuite/block-std'; -import { WithDisposable } from '@blocksuite/global/lit'; -import { CloseIcon, EmbedIcon } from '@blocksuite/icons/lit'; -import type { BlockModel } from '@blocksuite/store'; -import { baseTheme } from '@toeverything/theme'; -import { css, html, LitElement, nothing, unsafeCSS } from 'lit'; -import { property, query, state } from 'lit/decorators.js'; -import { classMap } from 'lit/directives/class-map.js'; - -type EmbedModalVariant = 'default' | 'compact'; - -export class EmbedIframeCreateModal extends WithDisposable(LitElement) { - static override styles = css` - .embed-iframe-create-modal { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - display: flex; - align-items: center; - justify-content: center; - z-index: 1; - } - - .embed-iframe-create-modal-mask { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - - .modal-main-wrapper { - position: relative; - box-sizing: border-box; - width: 340px; - padding: 0 24px; - border-radius: 12px; - background: ${unsafeCSSVarV2('layer/background/overlayPanel')}; - box-shadow: ${unsafeCSSVar('overlayPanelShadow')}; - z-index: var(--affine-z-index-modal); - font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; - } - - .modal-content-wrapper { - display: flex; - flex-direction: column; - } - - .modal-close-button { - position: absolute; - top: 12px; - right: 12px; - width: 24px; - height: 24px; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - color: var(--affine-icon-color); - border-radius: 4px; - } - .modal-close-button:hover { - background-color: var(--affine-hover-color); - } - - .modal-content-header { - display: flex; - flex-direction: column; - gap: 4px; - - .icon-container { - padding-top: 48px; - padding-bottom: 16px; - display: flex; - justify-content: center; - - .icon-background { - display: flex; - width: 64px; - height: 64px; - justify-content: center; - align-items: center; - border-radius: 50%; - background: var(--affine-background-secondary-color); - color: ${unsafeCSSVarV2('icon/primary')}; - - svg { - width: 32px; - height: 32px; - } - } - } - - .title { - /* Client/h6 */ - text-align: center; - font-size: 18px; - font-style: normal; - font-weight: 600; - line-height: 26px; /* 144.444% */ - letter-spacing: -0.24px; - color: ${unsafeCSSVarV2('text/primary')}; - } - } - - .description { - margin-top: 8px; - text-align: center; - font-feature-settings: - 'liga' off, - 'clig' off; - /* Client/xs */ - font-size: 12px; - font-style: normal; - font-weight: 400; - line-height: 20px; /* 166.667% */ - color: ${unsafeCSSVarV2('text/secondary')}; - } - - .input-container { - width: 100%; - margin-top: 24px; - - .link-input { - box-sizing: border-box; - width: 100%; - padding: 4px 10px; - border-radius: 8px; - border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')}; - background: ${unsafeCSSVarV2('input/background')}; - } - - .link-input:focus { - border-color: var(--affine-blue-700); - box-shadow: var(--affine-active-shadow); - outline: none; - } - .link-input::placeholder { - color: var(--affine-placeholder-color); - } - } - - .button-container { - display: flex; - justify-content: center; - padding: 20px 0px; - - .confirm-button { - width: 100%; - height: 32px; - line-height: 32px; - text-align: center; - justify-content: center; - align-items: center; - border-radius: 8px; - background: ${unsafeCSSVarV2('button/primary')}; - border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')}; - - color: ${unsafeCSSVarV2('button/pureWhiteText')}; - /* Client/xsMedium */ - font-size: 12px; - font-style: normal; - font-weight: 500; - cursor: pointer; - } - - .confirm-button[disabled] { - opacity: 0.5; - } - } - - .modal-main-wrapper.compact { - padding: 12px 16px; - - .modal-content-wrapper { - gap: 0; - - .icon-container { - padding: 0; - - .icon-background { - width: 56px; - height: 56px; - - svg { - width: 28px; - height: 28px; - } - } - } - - .title { - padding: 10px 0; - font-weight: 500; - } - - .link-input { - padding: 10px; - font-size: 17px; - font-style: normal; - font-weight: 400; - letter-spacing: -0.43px; - } - - .description, - .input-container { - margin-top: 0; - } - - .title, - .description { - font-size: 17px; - font-style: normal; - line-height: 22px; /* 129.412% */ - letter-spacing: -0.43px; - } - - .description { - font-weight: 400; - text-align: left; - order: 2; - padding: 10px 0; - color: ${unsafeCSSVarV2('text/secondary')}; - } - - .input-container { - order: 1; - } - } - - .button-container { - padding: 4px 0; - - .confirm-button { - height: 40px; - line-height: 40px; - font-size: 17px; - font-style: normal; - font-weight: 400; - letter-spacing: -0.43px; - } - } - } - `; - - private readonly _onClose = () => { - this.remove(); - }; - - private readonly _isInputEmpty = () => { - return this._linkInputValue.trim() === ''; - }; - - private readonly _addBookmark = (url: string) => { - if (!isValidUrl(url)) { - // notify user that the url is invalid - return; - } - - const blockId = this.std.store.addBlock( - 'affine:bookmark', - { - url, - }, - this.parentModel.id, - this.index - ); - - return blockId; - }; - - private readonly _onConfirm = async () => { - if (this._isInputEmpty()) { - return; - } - - try { - const embedIframeService = this.std.get(EmbedIframeService); - if (!embedIframeService) { - console.error('iframe EmbedIframeService not found'); - return; - } - - const url = this.input.value; - // check if the url can be embedded - const canEmbed = embedIframeService.canEmbed(url); - // if can not be embedded, try to add as a bookmark - if (!canEmbed) { - console.log('iframe can not be embedded, add as a bookmark', url); - this._addBookmark(url); - return; - } - - // create a new embed iframe block - const embedIframeBlock = embedIframeService.addEmbedIframeBlock( - { - url, - }, - this.parentModel.id, - this.index - ); - - return embedIframeBlock; - } catch (error) { - console.error('Error in embed iframe creation:', error); - return; - } finally { - this._onClose(); - } - }; - - private readonly _handleInput = (e: InputEvent) => { - const target = e.target as HTMLInputElement; - this._linkInputValue = target.value; - }; - - private readonly _handleKeyDown = async (e: KeyboardEvent) => { - e.stopPropagation(); - if (e.key === 'Enter' && !e.isComposing) { - await this._onConfirm(); - } - }; - - override connectedCallback() { - super.connectedCallback(); - - this.updateComplete - .then(() => { - requestAnimationFrame(() => { - this.input.focus(); - }); - }) - .catch(console.error); - this.disposables.addFromEvent(this, 'cut', stopPropagation); - this.disposables.addFromEvent(this, 'copy', stopPropagation); - this.disposables.addFromEvent(this, 'paste', stopPropagation); - } - - override render() { - const { showCloseButton } = this; - const { variant } = this; - - const modalMainWrapperClass = classMap({ - 'modal-main-wrapper': true, - compact: variant === 'compact', - }); - - return html` -
-
-
- ${showCloseButton - ? html` - - ` - : nothing} - -
-
- Confirm -
-
-
-
- `; - } - - @state() - private accessor _linkInputValue = ''; - - @query('input') - accessor input!: HTMLInputElement; - - @property({ attribute: false }) - accessor parentModel!: BlockModel; - - @property({ attribute: false }) - accessor index: number | undefined = undefined; - - @property({ attribute: false }) - accessor std!: BlockStdScope; - - @property({ attribute: false }) - accessor onConfirm: () => void = () => {}; - - @property({ attribute: false }) - accessor showCloseButton: boolean = true; - - @property({ attribute: false }) - accessor variant: EmbedModalVariant = 'default'; -} - -export async function toggleEmbedIframeCreateModal( - std: BlockStdScope, - createOptions: { - parentModel: BlockModel; - index?: number; - variant?: EmbedModalVariant; - } -): Promise { - std.selection.clear(); - - const embedIframeCreateModal = new EmbedIframeCreateModal(); - embedIframeCreateModal.std = std; - embedIframeCreateModal.parentModel = createOptions.parentModel; - embedIframeCreateModal.index = createOptions.index; - embedIframeCreateModal.variant = createOptions.variant ?? 'default'; - - document.body.append(embedIframeCreateModal); - - return new Promise(resolve => { - embedIframeCreateModal.onConfirm = () => resolve(); - }); -} diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-error-card.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-error-card.ts index 9cb48f6f1b..5c5cbaf397 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-error-card.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-error-card.ts @@ -6,7 +6,7 @@ import { WithDisposable } from '@blocksuite/global/lit'; import { EditIcon, InformationIcon, ResetIcon } from '@blocksuite/icons/lit'; import { flip, offset } from '@floating-ui/dom'; import { baseTheme } from '@toeverything/theme'; -import { css, html, LitElement, unsafeCSS } from 'lit'; +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'; @@ -161,7 +161,7 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) { private _editAbortController: AbortController | null = null; private readonly _toggleEdit = (e: MouseEvent) => { e.stopPropagation(); - if (!this._editButton) { + if (!this._editButton || this.readonly) { return; } @@ -225,12 +225,16 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) { ${this.error?.message || 'Failed to load embedded content'}
-
- ${EditIcon({ width: '16px', height: '16px' })} - Edit -
+ ${this.readonly + ? nothing + : html` +
+ ${EditIcon({ width: '16px', height: '16px' })} + Edit +
+ `}
${ResetIcon({ width: '16px', height: '16px' })} + + ${EmbedIcon({ width: '24px', height: '24px' })} + + + Embed anything (Google Drive, Google Docs, Spotify, Miro…) + +
+ `; + } +} diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-link-edit-popup.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-link-edit-popup.ts index 1b5b9e26a2..98592290ab 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-link-edit-popup.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-link-edit-popup.ts @@ -1,15 +1,12 @@ -import { type EmbedIframeBlockModel } from '@blocksuite/affine-model'; -import { EmbedIframeService } from '@blocksuite/affine-shared/services'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; -import { isValidUrl, stopPropagation } from '@blocksuite/affine-shared/utils'; -import { BlockSelection, type BlockStdScope } from '@blocksuite/block-std'; -import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; +import { SignalWatcher } from '@blocksuite/global/lit'; import { DoneIcon } from '@blocksuite/icons/lit'; -import { css, html, LitElement } from 'lit'; -import { property, query, state } from 'lit/decorators.js'; +import { css, html } from 'lit'; + +import { EmbedIframeLinkInputBase } from './embed-iframe-link-input-base'; export class EmbedIframeLinkEditPopup extends SignalWatcher( - WithDisposable(LitElement) + EmbedIframeLinkInputBase ) { static override styles = css` .embed-iframe-link-edit-popup { @@ -70,90 +67,8 @@ export class EmbedIframeLinkEditPopup extends SignalWatcher( } `; - /** - * Try to add a bookmark model and remove the current embed iframe model - * @param url The url to add as a bookmark - */ - private readonly _tryToAddBookmark = (url: string) => { - if (!isValidUrl(url)) { - // notify user that the url is invalid - console.warn('can not add bookmark', url); - return; - } - - const { model } = this; - const { parent } = model; - const index = parent?.children.indexOf(model); - const flavour = 'affine:bookmark'; - - this.store.transact(() => { - const blockId = this.store.addBlock(flavour, { url }, parent, index); - - this.store.deleteBlock(model); - this.std.selection.setGroup('note', [ - this.std.selection.create(BlockSelection, { blockId }), - ]); - }); - - this.abortController.abort(); - }; - - private readonly _onConfirm = () => { - if (this._isInputEmpty()) { - return; - } - - const canEmbed = this.EmbedIframeService.canEmbed(this._linkInputValue); - // If the url is not embeddable, try to add it as a bookmark - if (!canEmbed) { - console.warn('can not embed', this._linkInputValue); - this._tryToAddBookmark(this._linkInputValue); - return; - } - - // Update the embed iframe model - this.store.updateBlock(this.model, { - url: this._linkInputValue, - iframeUrl: '', - title: '', - description: '', - }); - - this.abortController.abort(); - }; - - private readonly _handleInput = (e: InputEvent) => { - const target = e.target as HTMLInputElement; - this._linkInputValue = target.value; - }; - - private readonly _isInputEmpty = () => { - return this._linkInputValue.trim() === ''; - }; - - private readonly _handleKeyDown = (e: KeyboardEvent) => { - e.stopPropagation(); - if (e.key === 'Enter' && !e.isComposing) { - this._onConfirm(); - } - }; - - override connectedCallback() { - super.connectedCallback(); - this.updateComplete - .then(() => { - requestAnimationFrame(() => { - this.input.focus(); - }); - }) - .catch(console.error); - this.disposables.addFromEvent(this, 'cut', stopPropagation); - this.disposables.addFromEvent(this, 'copy', stopPropagation); - this.disposables.addFromEvent(this, 'paste', stopPropagation); - } - override render() { - const isInputEmpty = this._isInputEmpty(); + const isInputEmpty = this.isInputEmpty(); const { url$ } = this.model.props; return html` @@ -165,41 +80,18 @@ export class EmbedIframeLinkEditPopup extends SignalWatcher( type="text" spellcheck="false" placeholder=${url$.value} - @input=${this._handleInput} - @keydown=${this._handleKeyDown} + @input=${this.handleInput} + @keydown=${this.handleKeyDown} />
${DoneIcon({ width: '24px', height: '24px' })}
`; } - - get store() { - return this.model.doc; - } - - get EmbedIframeService() { - return this.store.get(EmbedIframeService); - } - - @state() - private accessor _linkInputValue = ''; - - @query('input') - accessor input!: HTMLInputElement; - - @property({ attribute: false }) - accessor model!: EmbedIframeBlockModel; - - @property({ attribute: false }) - accessor abortController!: AbortController; - - @property({ attribute: false }) - accessor std!: BlockStdScope; } diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-link-input-base.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-link-input-base.ts new file mode 100644 index 0000000000..19a7426172 --- /dev/null +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-link-input-base.ts @@ -0,0 +1,131 @@ +import type { EmbedIframeBlockModel } from '@blocksuite/affine-model'; +import { + EmbedIframeService, + NotificationProvider, +} from '@blocksuite/affine-shared/services'; +import { isValidUrl, stopPropagation } from '@blocksuite/affine-shared/utils'; +import { BlockSelection, type BlockStdScope } from '@blocksuite/block-std'; +import { WithDisposable } from '@blocksuite/global/lit'; +import { LitElement } from 'lit'; +import { property, query, state } from 'lit/decorators.js'; + +export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) { + protected isInputEmpty() { + return this._linkInputValue.trim() === ''; + } + + protected tryToAddBookmark(url: string) { + if (!isValidUrl(url)) { + this.notificationService?.notify({ + title: 'Invalid URL', + message: 'Please enter a valid URL', + accent: 'error', + onClose: function (): void {}, + }); + return; + } + + const { model } = this; + const { parent } = model; + const index = parent?.children.indexOf(model); + const flavour = 'affine:bookmark'; + + this.store.transact(() => { + const blockId = this.store.addBlock(flavour, { url }, parent, index); + this.store.deleteBlock(model); + this.std.selection.setGroup('note', [ + this.std.selection.create(BlockSelection, { blockId }), + ]); + }); + + this.abortController?.abort(); + } + + protected async onConfirm() { + if (this.isInputEmpty()) { + return; + } + + try { + const embedIframeService = this.std.get(EmbedIframeService); + if (!embedIframeService) { + console.error('iframe EmbedIframeService not found'); + return; + } + + const url = this._linkInputValue; + const canEmbed = embedIframeService.canEmbed(url); + + if (!canEmbed) { + console.log('iframe can not be embedded, add as a bookmark', url); + this.tryToAddBookmark(url); + return; + } + + this.store.updateBlock(this.model, { + url: this._linkInputValue, + iframeUrl: '', + title: '', + description: '', + }); + } catch (error) { + this.notificationService?.notify({ + title: 'Error in embed iframe creation', + message: error instanceof Error ? error.message : 'Please try again', + accent: 'error', + onClose: function (): void {}, + }); + } finally { + this.abortController?.abort(); + } + } + + protected handleInput = (e: InputEvent) => { + const target = e.target as HTMLInputElement; + this._linkInputValue = target.value; + }; + + protected handleKeyDown = async (e: KeyboardEvent) => { + e.stopPropagation(); + if (e.key === 'Enter' && !e.isComposing) { + await this.onConfirm(); + } + }; + + override connectedCallback() { + super.connectedCallback(); + this.updateComplete + .then(() => { + requestAnimationFrame(() => { + this.input.focus(); + }); + }) + .catch(console.error); + this.disposables.addFromEvent(this, 'cut', stopPropagation); + this.disposables.addFromEvent(this, 'copy', stopPropagation); + this.disposables.addFromEvent(this, 'paste', stopPropagation); + } + + get store() { + return this.model.doc; + } + + get notificationService() { + return this.std.getOptional(NotificationProvider); + } + + @state() + protected accessor _linkInputValue = ''; + + @query('input') + accessor input!: HTMLInputElement; + + @property({ attribute: false }) + accessor model!: EmbedIframeBlockModel; + + @property({ attribute: false }) + accessor std!: BlockStdScope; + + @property({ attribute: false }) + accessor abortController: AbortController | undefined = undefined; +} diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-link-input-popup.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-link-input-popup.ts new file mode 100644 index 0000000000..1287457b8b --- /dev/null +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/components/embed-iframe-link-input-popup.ts @@ -0,0 +1,266 @@ +import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; +import { CloseIcon } from '@blocksuite/icons/lit'; +import { baseTheme } from '@toeverything/theme'; +import { css, html, nothing, unsafeCSS } from 'lit'; +import { property } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; + +import { EmbedIframeLinkInputBase } from './embed-iframe-link-input-base'; + +type EmbedLinkInputPopupVariant = 'default' | 'mobile'; + +export type EmbedLinkInputPopupOptions = { + showCloseButton?: boolean; + variant?: EmbedLinkInputPopupVariant; + title?: string; + description?: string; + placeholder?: string; +}; + +const DEFAULT_OPTIONS: EmbedLinkInputPopupOptions = { + showCloseButton: false, + variant: 'default', + title: 'Embed Link', + description: 'Works with links of Google Drive, Spotify…', + placeholder: 'Paste the Embed link...', +}; + +export class EmbedIframeLinkInputPopup extends EmbedIframeLinkInputBase { + static override styles = css` + .link-input-popup-main-wrapper { + box-sizing: border-box; + width: 340px; + padding: 12px; + border-radius: 8px; + background: ${unsafeCSSVarV2('layer/background/overlayPanel')}; + box-shadow: ${unsafeCSSVar('overlayPanelShadow')}; + z-index: var(--affine-z-index-modal); + font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; + } + + .link-input-popup-content-wrapper { + display: flex; + flex-direction: column; + } + + .popup-close-button { + position: absolute; + top: 12px; + right: 12px; + width: 24px; + height: 24px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + color: var(--affine-icon-color); + border-radius: 4px; + } + .popup-close-button:hover { + background-color: var(--affine-hover-color); + } + + .title { + /* Client/h6 */ + font-size: var(--affine-font-base); + font-style: normal; + font-weight: 500; + line-height: 24px; + color: ${unsafeCSSVarV2('text/primary')}; + } + + .description { + margin-top: 4px; + font-feature-settings: + 'liga' off, + 'clig' off; + font-size: var(--affine-font-sm); + font-style: normal; + font-weight: 400; + line-height: 22px; + color: ${unsafeCSSVarV2('text/secondary')}; + } + + .input-container { + width: 100%; + margin-top: 12px; + box-sizing: border-box; + + .link-input { + box-sizing: border-box; + width: 100%; + padding: 4px 10px; + border-radius: 8px; + border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')}; + background: ${unsafeCSSVarV2('input/background')}; + } + + .link-input:focus { + border-color: var(--affine-blue-700); + box-shadow: var(--affine-active-shadow); + outline: none; + } + .link-input::placeholder { + color: var(--affine-placeholder-color); + } + } + + .button-container { + display: flex; + justify-content: center; + margin-top: 12px; + + .confirm-button { + width: 100%; + height: 32px; + line-height: 32px; + text-align: center; + justify-content: center; + align-items: center; + border-radius: 8px; + background: ${unsafeCSSVarV2('button/primary')}; + border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')}; + + color: ${unsafeCSSVarV2('button/pureWhiteText')}; + /* Client/xsMedium */ + font-size: 12px; + font-style: normal; + font-weight: 500; + cursor: pointer; + } + + .confirm-button[disabled] { + opacity: 0.5; + } + } + + .link-input-popup-main-wrapper.mobile { + width: 360px; + border-radius: 22px; + padding: 12px 0; + + .popup-close-button { + top: 20px; + right: 16px; + } + + .link-input-popup-content-wrapper { + gap: 0; + + .title { + padding: 10px 16px; + font-weight: 500; + } + + .input-container { + padding: 4px 12px; + } + + .link-input { + padding: 11px 10px; + font-size: 17px; + font-style: normal; + font-weight: 400; + letter-spacing: -0.43px; + } + + .title, + .description { + font-size: 17px; + font-style: normal; + line-height: 22px; /* 129.412% */ + letter-spacing: -0.43px; + } + + .description { + font-weight: 400; + text-align: left; + order: 2; + padding: 11px 16px; + color: ${unsafeCSSVarV2('text/secondary')}; + } + + .input-container { + order: 1; + } + } + + .description, + .input-container, + .button-container { + margin-top: 0; + } + + .button-container { + padding: 4px 16px; + + .confirm-button { + height: 40px; + line-height: 40px; + font-size: 17px; + font-style: normal; + font-weight: 400; + letter-spacing: -0.43px; + } + + .confirm-button[disabled] { + opacity: 1; + background: ${unsafeCSSVarV2('button/disable')}; + } + } + } + `; + + private readonly _onClose = () => { + this.abortController?.abort(); + }; + + override render() { + const options = { ...DEFAULT_OPTIONS, ...this.options }; + const { showCloseButton, variant, title, description, placeholder } = + options; + + const modalMainWrapperClass = classMap({ + 'link-input-popup-main-wrapper': true, + mobile: variant === 'mobile', + }); + + return html` +
+ ${showCloseButton + ? html` + + ` + : nothing} + +
+
+ Confirm +
+
+
+ `; + } + + @property({ attribute: false }) + accessor options: EmbedLinkInputPopupOptions | undefined = undefined; +} diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/excalidraw.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/excalidraw.ts index 0d1e025424..4a80f36f8f 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/excalidraw.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/excalidraw.ts @@ -33,7 +33,7 @@ const excalidrawConfig = { heightInNote: EXCALIDRAW_DEFAULT_HEIGHT_IN_NOTE, widthPercent: EXCALIDRAW_DEFAULT_WIDTH_PERCENT, allow: 'clipboard-read; clipboard-write', - style: 'border: 0; border-radius: 8px;', + style: 'border: none; border-radius: 8px;', allowFullscreen: true, }, }; diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/miro.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/miro.ts index f91466f719..fa168f1768 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/miro.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/miro.ts @@ -37,8 +37,9 @@ const miroConfig = { heightInNote: MIRO_DEFAULT_HEIGHT_IN_NOTE, widthPercent: MIRO_DEFAULT_WIDTH_PERCENT, allow: 'clipboard-read; clipboard-write', - style: 'border: 0; border-radius: 8px;', + style: 'border: none;', allowFullscreen: true, + containerBorderRadius: 0, }, }; diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/spotify.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/spotify.ts index 289141305a..74a1c91e7e 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/spotify.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/providers/spotify.ts @@ -40,6 +40,7 @@ const spotifyConfig = { allow: 'autoplay; clipboard-write; encrypted-media; picture-in-picture', style: 'border-radius: 8px;', allowFullscreen: true, + containerBorderRadius: 12, }, }; diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/slash-menu/slash-menu.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/slash-menu/slash-menu.ts index cf4ec6e121..f5f6cbf067 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/slash-menu/slash-menu.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/slash-menu/slash-menu.ts @@ -1,15 +1,16 @@ +import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands'; import { FeatureFlagService } from '@blocksuite/affine-shared/services'; import type { SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu'; import { EmbedIcon } from '@blocksuite/icons/lit'; -import { toggleEmbedIframeCreateModal } from '../../components/embed-iframe-create-modal'; +import { insertEmptyEmbedIframeCommand } from '../../commands/insert-empty-embed-iframe'; import { EmbedIframeTooltip } from './tooltip'; export const embedIframeSlashMenuConfig: SlashMenuConfig = { items: [ { name: 'Embed', - description: 'For PDFs, and more.', + description: 'For Google Drive, and more.', icon: EmbedIcon(), tooltip: { figure: EmbedIframeTooltip, @@ -23,21 +24,15 @@ export const embedIframeSlashMenuConfig: SlashMenuConfig = { model.doc.schema.flavourSchemaMap.has('affine:embed-iframe') ); }, - action: ({ std, model }) => { - (async () => { - const { host } = std; - const parentModel = host.doc.getParent(model); - if (!parentModel) { - return; - } - const index = parentModel.children.indexOf(model) + 1; - await toggleEmbedIframeCreateModal(std, { - parentModel, - index, - variant: 'default', - }); - if (model.text?.length === 0) std.store.deleteBlock(model); - })().catch(console.error); + action: ({ std }) => { + std.command + .chain() + .pipe(getSelectedModelsCommand) + .pipe(insertEmptyEmbedIframeCommand, { + place: 'after', + removeEmptyLine: true, + }) + .run(); }, }, ], diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/slash-menu/tooltip.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/slash-menu/tooltip.ts index 589ba36739..0cff1953fd 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/slash-menu/tooltip.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/configs/slash-menu/tooltip.ts @@ -23,7 +23,7 @@ export const EmbedIframeTooltip = html` { + const model = ctx.getCurrentModelByType(EmbedIframeBlockModel); + if (!model) return false; + + return !!model.props.url; + }, actions: [ { id: 'inline', @@ -44,6 +51,8 @@ export const builtinToolbarConfig = { if (!model) return; const { title, caption, url } = model.props; + if (!url) return; + const { parent } = model; const index = parent?.children.indexOf(model); @@ -77,6 +86,8 @@ export const builtinToolbarConfig = { if (!model) return; const { url, caption } = model.props; + if (!url) return; + const { parent } = model; const index = parent?.children.indexOf(model); @@ -140,6 +151,12 @@ export const builtinToolbarConfig = { } satisfies ToolbarActionGroup, { id: 'c.caption', + when: (ctx: ToolbarContext) => { + const model = ctx.getCurrentModelByType(EmbedIframeBlockModel); + if (!model) return false; + + return !!model.props.url; + }, tooltip: 'Caption', icon: CaptionIcon(), run(ctx) { diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/consts.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/consts.ts index 8e3074eff9..97966c62b5 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/consts.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/consts.ts @@ -1,2 +1,8 @@ export const EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE = 752; export const EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE = 116; +export const EMBED_IFRAME_DEFAULT_CONTAINER_BORDER_RADIUS = 8; + +export const DEFAULT_IFRAME_HEIGHT = 152; +export const DEFAULT_IFRAME_WIDTH = '100%'; + +export const LINK_CREATE_POPUP_OFFSET = 4; diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/embed-edgeless-iframe-block.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/embed-edgeless-iframe-block.ts index 4298d8ac97..0b585d9de3 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/embed-edgeless-iframe-block.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/embed-edgeless-iframe-block.ts @@ -14,7 +14,10 @@ export class EmbedEdgelessIframeBlockComponent extends toGfxBlockComponent( override blockDraggable = false; - override accessor blockContainerStyles = { margin: '0' }; + override accessor blockContainerStyles = { + margin: '0', + backgroundColor: 'transparent', + }; get edgelessSlots() { return this.std.get(EdgelessLegacySlotIdentifier); diff --git a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/embed-iframe-block.ts b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/embed-iframe-block.ts index 421144109e..680c6d7bcf 100644 --- a/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/embed-iframe-block.ts +++ b/blocksuite/affine/blocks/block-embed/src/embed-iframe-block/embed-iframe-block.ts @@ -3,6 +3,7 @@ import { CaptionedBlockComponent, SelectedStyle, } from '@blocksuite/affine-components/caption'; +import { createLitPortal } from '@blocksuite/affine-components/portal'; import type { EmbedIframeBlockModel } from '@blocksuite/affine-model'; import { type EmbedIframeData, @@ -10,22 +11,31 @@ import { FeatureFlagService, type IframeOptions, LinkPreviewerService, + NotificationProvider, } 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 { flip, offset, shift } from '@floating-ui/dom'; import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; import { html, nothing } from 'lit'; +import { query } from 'lit/decorators.js'; 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 { EmbedLinkInputPopupOptions } from './components/embed-iframe-link-input-popup.js'; +import { + DEFAULT_IFRAME_HEIGHT, + DEFAULT_IFRAME_WIDTH, + EMBED_IFRAME_DEFAULT_CONTAINER_BORDER_RADIUS, + LINK_CREATE_POPUP_OFFSET, +} from './consts.js'; import { embedIframeBlockStyles } from './style.js'; import type { EmbedIframeStatusCardOptions } from './types.js'; import { safeGetIframeSrc } from './utils.js'; export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error'; -const DEFAULT_IFRAME_HEIGHT = 152; -const DEFAULT_IFRAME_WIDTH = '100%'; export class EmbedIframeBlockComponent extends CaptionedBlockComponent { selectedStyle$: ReadonlySignal | null = computed( @@ -41,6 +51,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent('idle'); readonly error$ = signal(null); + readonly isIdle$ = computed(() => this.status$.value === 'idle'); readonly isLoading$ = computed(() => this.status$.value === 'loading'); readonly hasError$ = computed(() => this.status$.value === 'error'); readonly isSuccess$ = computed(() => this.status$.value === 'success'); @@ -57,7 +68,19 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent { + if ( + this.status$.value === 'success' && + typeof this.iframeOptions?.containerBorderRadius === 'number' + ) { + return this.iframeOptions.containerBorderRadius; + } + return EMBED_IFRAME_DEFAULT_CONTAINER_BORDER_RADIUS; + }); + + protected iframeOptions: IframeOptions | undefined = undefined; get embedIframeService() { return this.std.get(EmbedIframeService); @@ -67,6 +90,10 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent { const link = this.model.props.url; + if (!link) { + this.notificationService?.notify({ + title: 'No link found', + message: 'Please set a link to the block', + accent: 'warning', + onClose: function (): void {}, + }); + return; + } window.open(link, '_blank'); }; refreshData = async () => { try { + const { url } = this.model.props; + if (!url) { + this.status$.value = 'idle'; + return; + } + // set loading status this.status$.value = 'loading'; this.error$.value = null; @@ -104,14 +146,6 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent { + if (this.readonly) { + return; + } + + // toggle create popup when ths block is in idle status and the url is not set + if (!this._blockContainer || !this.isIdle$.value || this.model.props.url) { + return; + } + + if (this._linkInputAbortController) { + this._linkInputAbortController.abort(); + } + + this._linkInputAbortController = new AbortController(); + + createLitPortal({ + template: html``, + portalStyles: { + zIndex: 'var(--affine-z-index-popover)', + }, + container: this.host, + computePosition: { + referenceElement: this._blockContainer, + placement: 'bottom', + middleware: [flip(), offset(LINK_CREATE_POPUP_OFFSET), shift()], + autoUpdate: { animationFrame: true }, + }, + abortController: this._linkInputAbortController, + closeOnClickAway: true, + }); + }; + /** * Get the iframe url from the embed data, first check if iframe_url is set, * if not, check if html is set and get the iframe src from html @@ -162,12 +235,11 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent { const config = this.embedIframeService?.getConfig(url); if (config) { - this._iframeOptions = config.options; + this.iframeOptions = config.options; } }; - private readonly _handleDoubleClick = (event: MouseEvent) => { - event.stopPropagation(); + private readonly _handleDoubleClick = () => { this.open(); }; @@ -179,13 +251,16 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent { - event.stopPropagation(); + protected _handleClick = () => { // We don't need to select the block when the block is in the surface if (this.inSurface) { return; } - this._selectBlock(); + if (this.isIdle$.value && !this.model.props.url) { + this.toggleLinkInputPopup(); + } else { + this._selectBlock(); + } }; private readonly _handleRetry = async () => { @@ -202,7 +277,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent { + if (this.isIdle$.value) { + return html``; + } + if (this.isLoading$.value) { return html` @@ -316,11 +406,21 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent { - const [_, { selectedModels }] = std.command.exec( - getSelectedModelsCommand - ); - const model = selectedModels?.[0]; - if (!model) return; - - const parentModel = std.store.getParent(model); - if (!parentModel) return; - - const index = parentModel.children.indexOf(model) + 1; - await toggleEmbedIframeCreateModal(std, { - parentModel, - index, - variant: 'compact', - }); - if (model.text?.length === 0) { - std.store.deleteBlock(model); - } + std.command + .chain() + .pipe(getSelectedModelsCommand) + .pipe(insertEmptyEmbedIframeCommand, { + place: 'after', + removeEmptyLine: true, + linkInputPopupOptions: { + showCloseButton: true, + variant: 'mobile', + }, + }) + .run(); }, }, { diff --git a/blocksuite/affine/shared/src/services/embed-iframe/embed-iframe-config.ts b/blocksuite/affine/shared/src/services/embed-iframe/embed-iframe-config.ts index ad3b3e0236..00b599c842 100644 --- a/blocksuite/affine/shared/src/services/embed-iframe/embed-iframe-config.ts +++ b/blocksuite/affine/shared/src/services/embed-iframe/embed-iframe-config.ts @@ -26,6 +26,7 @@ export type IframeOptions = { scrolling?: boolean; allow?: string; allowFullscreen?: boolean; + containerBorderRadius?: number; }; /** diff --git a/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts b/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts index 8ce143c943..44cb79c107 100644 --- a/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts +++ b/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts @@ -48,6 +48,7 @@ import { } from '@blocksuite/affine/inlines/reference'; import { BookmarkBlockModel, + EmbedIframeBlockModel, EmbedLinkedDocModel, EmbedSyncedDocModel, } from '@blocksuite/affine/model'; @@ -868,6 +869,12 @@ const inlineReferenceToolbarConfig = { } as const satisfies ToolbarModuleConfig; const embedIframeToolbarConfig = { + when: (ctx: ToolbarContext) => { + const model = ctx.getCurrentModelByType(EmbedIframeBlockModel); + if (!model) return false; + + return !!model.props.url; + }, actions: [ { id: 'a.copy-link-and-edit',