donteatfriedrice committed 2025-03-26 08:38:06 +00:00
1 parent f5cc4b41cd
commit 39fa8e87cf
24 files changed
+741 -640

No files matched your search

@@ -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();
@@ -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;
}
}
@@ -1 +1,2 @@
export * from './insert-embed-iframe';
export * from './insert-embed-iframe-with-url';
export * from './insert-empty-embed-iframe';
@@ -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) => {
@@ -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<string>;
}
> = (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<EmbedIframeBlockProps> & {
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];
}),
});
};
@@ -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`
<div class="embed-iframe-create-modal">
<div
class="embed-iframe-create-modal-mask"
@click=${this._onClose}
></div>
<div class=${modalMainWrapperClass}>
${showCloseButton
? html`
<div class="modal-close-button" @click=${this._onClose}>
${CloseIcon({ width: '20', height: '20' })}
</div>
`
: nothing}
<div class="modal-content-wrapper">
<div class="modal-content-header">
<div class="icon-container">
<div class="icon-background">${EmbedIcon()}</div>
</div>
<div class="title">Embed Link</div>
</div>
<div class="description">
Works with links of PDFs, Google Drive, Google Maps, CodePen…
</div>
<div class="input-container">
<input
class="link-input"
type="text"
placeholder="Paste in https://…"
@input=${this._handleInput}
@keydown=${this._handleKeyDown}
/>
</div>
</div>
<div class="button-container">
<div
class="confirm-button"
@click=${this._onConfirm}
?disabled=${this._isInputEmpty()}
>
Confirm
</div>
</div>
</div>
</div>
`;
}
@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<void> {
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();
});
}
@@ -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'}
</div>
<div class="error-info">
<div class="button edit" @click=${this._toggleEdit}>
<span class="icon"
>${EditIcon({ width: '16px', height: '16px' })}</span
>
<span class="text">Edit</span>
</div>
${this.readonly
? nothing
: html`
<div class="button edit" @click=${this._toggleEdit}>
<span class="icon"
>${EditIcon({ width: '16px', height: '16px' })}</span
>
<span class="text">Edit</span>
</div>
`}
<div class="button retry" @click=${this._handleRetry}>
<span class="icon"
>${ResetIcon({ width: '16px', height: '16px' })}</span
@@ -251,6 +255,10 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
return this.std.host;
}
get readonly() {
return this.model.doc.readonly;
}
@query('.button.edit')
accessor _editButton: HTMLElement | null = null;
@@ -0,0 +1,66 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { WithDisposable } from '@blocksuite/global/lit';
import { EmbedIcon } from '@blocksuite/icons/lit';
import { baseTheme } from '@toeverything/theme';
import { css, html, LitElement, unsafeCSS } from 'lit';
export class EmbedIframeIdleCard extends WithDisposable(LitElement) {
static override styles = css`
:host {
width: 100%;
}
.affine-embed-iframe-idle-card {
width: 100%;
height: 48px;
box-sizing: border-box;
display: flex;
align-items: center;
padding: 12px;
gap: 8px;
border-radius: 8px;
border: 1px solid var(--affine-border-color);
background-color: ${unsafeCSSVarV2('layer/background/secondary')};
.icon {
display: flex;
width: 24px;
height: 24px;
justify-content: center;
align-items: center;
color: ${unsafeCSSVarV2('icon/secondary')};
flex-shrink: 0;
}
.text {
/* Client/base */
font-size: 15px;
font-style: normal;
font-weight: 400;
line-height: 24px; /* 160% */
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
color: ${unsafeCSSVarV2('text/secondary')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.affine-embed-iframe-idle-card:hover {
cursor: pointer;
}
`;
override render() {
return html`
<div class="affine-embed-iframe-idle-card">
<span class="icon">
${EmbedIcon({ width: '24px', height: '24px' })}
</span>
<span class="text">
Embed anything (Google Drive, Google Docs, Spotify, Miro…)
</span>
</div>
`;
}
}
@@ -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}
/>
</div>
<div
class="confirm-button"
?disabled=${isInputEmpty}
@click=${this._onConfirm}
@click=${this.onConfirm}
>
${DoneIcon({ width: '24px', height: '24px' })}
</div>
</div>
`;
}
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;
}
@@ -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;
}
@@ -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`
<div class=${modalMainWrapperClass}>
${showCloseButton
? html`
<div class="popup-close-button" @click=${this._onClose}>
${CloseIcon({ width: '20', height: '20' })}
</div>
`
: nothing}
<div class="link-input-popup-content-wrapper">
<div class="title">${title}</div>
<div class="description">${description}</div>
<div class="input-container">
<input
class="link-input"
type="text"
placeholder=${ifDefined(placeholder)}
@input=${this.handleInput}
@keydown=${this.handleKeyDown}
/>
</div>
</div>
<div class="button-container">
<div
class="confirm-button"
@click=${this.onConfirm}
?disabled=${this.isInputEmpty()}
>
Confirm
</div>
</div>
</div>
`;
}
@property({ attribute: false })
accessor options: EmbedLinkInputPopupOptions | undefined = undefined;
}
@@ -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,
},
};
@@ -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,
},
};
@@ -40,6 +40,7 @@ const spotifyConfig = {
allow: 'autoplay; clipboard-write; encrypted-media; picture-in-picture',
style: 'border-radius: 8px;',
allowFullscreen: true,
containerBorderRadius: 12,
},
};
@@ -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();
},
},
],
@@ -23,7 +23,7 @@ export const EmbedIframeTooltip = html`
</mask>
<g mask="url(#mask0_888_46686)">
<path
d="M10.8807 19V11.7273H15.2415V12.5085H11.7614V14.9659H14.9148V15.7472H11.7614V19H10.8807ZM18.4517 19.1136C17.9593 19.1136 17.5272 18.9964 17.1555 18.7621C16.7862 18.5277 16.4974 18.1998 16.2891 17.7784C16.0831 17.357 15.9801 16.8646 15.9801 16.3011C15.9801 15.733 16.0831 15.237 16.2891 14.8132C16.4974 14.3894 16.7862 14.0604 17.1555 13.826C17.5272 13.5916 17.9593 13.4744 18.4517 13.4744C18.9441 13.4744 19.375 13.5916 19.7443 13.826C20.116 14.0604 20.4048 14.3894 20.6108 14.8132C20.8191 15.237 20.9233 15.733 20.9233 16.3011C20.9233 16.8646 20.8191 17.357 20.6108 17.7784C20.4048 18.1998 20.116 18.5277 19.7443 18.7621C19.375 18.9964 18.9441 19.1136 18.4517 19.1136ZM18.4517 18.3608C18.8258 18.3608 19.1335 18.2649 19.375 18.0732C19.6165 17.8814 19.7952 17.6293 19.9112 17.3168C20.0272 17.0043 20.0852 16.6657 20.0852 16.3011C20.0852 15.9366 20.0272 15.5968 19.9112 15.282C19.7952 14.9671 19.6165 14.7126 19.375 14.5185C19.1335 14.3243 18.8258 14.2273 18.4517 14.2273C18.0777 14.2273 17.7699 14.3243 17.5284 14.5185C17.2869 14.7126 17.1082 14.9671 16.9922 15.282C16.8762 15.5968 16.8182 15.9366 16.8182 16.3011C16.8182 16.6657 16.8762 17.0043 16.9922 17.3168C17.1082 17.6293 17.2869 17.8814 17.5284 18.0732C17.7699 18.2649 18.0777 18.3608 18.4517 18.3608ZM22.2026 19V13.5455H23.0123V14.3693H23.0691C23.1685 14.0994 23.3484 13.8804 23.6088 13.7124C23.8693 13.5443 24.1628 13.4602 24.4895 13.4602C24.5511 13.4602 24.628 13.4614 24.7203 13.4638C24.8127 13.4661 24.8825 13.4697 24.9299 13.4744V14.3267C24.9015 14.3196 24.8364 14.3089 24.7346 14.2947C24.6351 14.2782 24.5298 14.2699 24.4185 14.2699C24.1534 14.2699 23.9166 14.3255 23.7083 14.4368C23.5023 14.5457 23.339 14.6972 23.2182 14.8913C23.0998 15.0831 23.0407 15.3021 23.0407 15.5483V19H22.2026ZM28.8494 19V11.7273H31.3068C31.8774 11.7273 32.3438 11.8303 32.706 12.0362C33.0705 12.2398 33.3404 12.5156 33.5156 12.8636C33.6908 13.2116 33.7784 13.5999 33.7784 14.0284C33.7784 14.4569 33.6908 14.8464 33.5156 15.1967C33.3428 15.5471 33.0753 15.8265 32.7131 16.0348C32.3509 16.2408 31.8868 16.3438 31.321 16.3438H29.5597V15.5625H31.2926C31.6832 15.5625 31.9969 15.495 32.2337 15.3601C32.4704 15.2251 32.642 15.0429 32.7486 14.8132C32.8575 14.5812 32.9119 14.3196 32.9119 14.0284C32.9119 13.7372 32.8575 13.4768 32.7486 13.2472C32.642 13.0175 32.4692 12.8376 32.2301 12.7074C31.991 12.5748 31.6738 12.5085 31.2784 12.5085H29.7301V19H28.8494ZM37.4414 19H35.1971V11.7273H37.5408C38.2463 11.7273 38.85 11.8729 39.3519 12.1641C39.8538 12.4529 40.2385 12.8684 40.506 13.4105C40.7736 13.9503 40.9073 14.5966 40.9073 15.3494C40.9073 16.107 40.7724 16.7592 40.5025 17.3061C40.2326 17.8506 39.8396 18.2696 39.3235 18.5632C38.8074 18.8544 38.18 19 37.4414 19ZM36.0778 18.2188H37.3846C37.9859 18.2188 38.4843 18.1027 38.8796 17.8707C39.275 17.6387 39.5697 17.3085 39.7638 16.88C39.958 16.4515 40.055 15.9413 40.055 15.3494C40.055 14.7623 39.9592 14.2569 39.7674 13.8331C39.5756 13.407 39.2892 13.0803 38.908 12.853C38.5269 12.6233 38.0522 12.5085 37.484 12.5085H36.0778V18.2188ZM42.3846 19V11.7273H46.7454V12.5085H43.2653V14.9659H46.4187V15.7472H43.2653V19H42.3846ZM52.0037 14.767L51.2509 14.9801C51.2035 14.8546 51.1337 14.7327 51.0414 14.6143C50.9514 14.4936 50.8283 14.3942 50.6721 14.3161C50.5158 14.2379 50.3158 14.1989 50.0719 14.1989C49.7381 14.1989 49.4599 14.2758 49.2374 14.4297C49.0172 14.5812 48.9071 14.7741 48.9071 15.0085C48.9071 15.2169 48.9829 15.3814 49.1344 15.5021C49.2859 15.6229 49.5227 15.7235 49.8446 15.804L50.6543 16.0028C51.142 16.1212 51.5054 16.3023 51.7445 16.5462C51.9836 16.7876 52.1032 17.099 52.1032 17.4801C52.1032 17.7926 52.0132 18.072 51.8333 18.3182C51.6557 18.5644 51.4071 18.7585 51.0875 18.9006C50.7679 19.0426 50.3962 19.1136 49.9725 19.1136C49.4161 19.1136 48.9557 18.9929 48.5911 18.7514C48.2265 18.5099 47.9957 18.1572 47.8986 17.6932L48.6941 17.4943C48.7698 17.7879 48.9131 18.008 49.1238 18.1548C49.3368 18.3016 49.615 18.375 49.9583 18.375C50.3489 18.375 50.659 18.2921 50.8887 18.1264C51.1207 17.9583 51.2367 17.7571 51.2367 17.5227C51.2367 17.3333 51.1704 17.1747 51.0378 17.0469C50.9052 16.9167 50.7016 16.8196 50.427 16.7557L49.5179 16.5426C49.0184 16.4242 48.6515 16.2408 48.4171 15.9922C48.1851 15.7412 48.0691 15.4276 48.0691 15.0511C48.0691 14.7434 48.1555 14.4711 48.3283 14.2344C48.5035 13.9976 48.7414 13.8118 49.0421 13.6768C49.3451 13.5419 49.6884 13.4744 50.0719 13.4744C50.6117 13.4744 51.0355 13.5928 51.3432 13.8295C51.6533 14.0663 51.8735 14.3788 52.0037 14.767ZM54.6147 18.0057L54.5579 18.3892C54.5176 18.6591 54.4561 18.9479 54.3732 19.2557C54.2927 19.5634 54.2087 19.8535 54.1211 20.1257C54.0335 20.398 53.9613 20.6146 53.9045 20.7756H53.2653C53.296 20.6241 53.3363 20.424 53.386 20.1754C53.4357 19.9268 53.4854 19.6487 53.5352 19.3409C53.5872 19.0355 53.6299 18.723 53.663 18.4034L53.7056 18.0057H54.6147ZM60.585 19.1278C60.2394 19.1278 59.9257 19.0627 59.644 18.9325C59.3623 18.8 59.1386 18.6094 58.9728 18.3608C58.8071 18.1098 58.7243 17.8068 58.7243 17.4517C58.7243 17.1392 5Line truncated
d="M10.8807 19V11.7273H15.2415V12.5085H11.7614V14.9659H14.9148V15.7472H11.7614V19H10.8807ZM18.4517 19.1136C17.9593 19.1136 17.5272 18.9964 17.1555 18.7621C16.7862 18.5277 16.4974 18.1998 16.2891 17.7784C16.0831 17.357 15.9801 16.8646 15.9801 16.3011C15.9801 15.733 16.0831 15.237 16.2891 14.8132C16.4974 14.3894 16.7862 14.0604 17.1555 13.826C17.5272 13.5916 17.9593 13.4744 18.4517 13.4744C18.9441 13.4744 19.375 13.5916 19.7443 13.826C20.116 14.0604 20.4048 14.3894 20.6108 14.8132C20.8191 15.237 20.9233 15.733 20.9233 16.3011C20.9233 16.8646 20.8191 17.357 20.6108 17.7784C20.4048 18.1998 20.116 18.5277 19.7443 18.7621C19.375 18.9964 18.9441 19.1136 18.4517 19.1136ZM18.4517 18.3608C18.8258 18.3608 19.1335 18.2649 19.375 18.0732C19.6165 17.8814 19.7952 17.6293 19.9112 17.3168C20.0272 17.0043 20.0852 16.6657 20.0852 16.3011C20.0852 15.9366 20.0272 15.5968 19.9112 15.282C19.7952 14.9671 19.6165 14.7126 19.375 14.5185C19.1335 14.3243 18.8258 14.2273 18.4517 14.2273C18.0777 14.2273 17.7699 14.3243 17.5284 14.5185C17.2869 14.7126 17.1082 14.9671 16.9922 15.282C16.8762 15.5968 16.8182 15.9366 16.8182 16.3011C16.8182 16.6657 16.8762 17.0043 16.9922 17.3168C17.1082 17.6293 17.2869 17.8814 17.5284 18.0732C17.7699 18.2649 18.0777 18.3608 18.4517 18.3608ZM22.2026 19V13.5455H23.0123V14.3693H23.0691C23.1685 14.0994 23.3484 13.8804 23.6088 13.7124C23.8693 13.5443 24.1628 13.4602 24.4895 13.4602C24.5511 13.4602 24.628 13.4614 24.7203 13.4638C24.8127 13.4661 24.8825 13.4697 24.9299 13.4744V14.3267C24.9015 14.3196 24.8364 14.3089 24.7346 14.2947C24.6351 14.2782 24.5298 14.2699 24.4185 14.2699C24.1534 14.2699 23.9166 14.3255 23.7083 14.4368C23.5023 14.5457 23.339 14.6972 23.2182 14.8913C23.0998 15.0831 23.0407 15.3021 23.0407 15.5483V19H22.2026ZM33.821 14C33.7429 13.7609 33.6399 13.5466 33.5121 13.3572C33.3866 13.1655 33.2363 13.0021 33.0611 12.8672C32.8883 12.7322 32.6918 12.6293 32.4716 12.5582C32.2514 12.4872 32.0099 12.4517 31.7472 12.4517C31.3163 12.4517 30.9245 12.563 30.5717 12.7855C30.219 13.008 29.9384 13.3359 29.7301 13.7692C29.5218 14.2024 29.4176 14.7339 29.4176 15.3636C29.4176 15.9934 29.523 16.5249 29.7337 16.9581C29.9444 17.3913 30.2296 17.7192 30.5895 17.9418C30.9493 18.1643 31.3542 18.2756 31.804 18.2756C32.2206 18.2756 32.5876 18.1868 32.9048 18.0092C33.2244 17.8293 33.473 17.576 33.6506 17.2493C33.8305 16.9202 33.9205 16.5331 33.9205 16.0881L34.1903 16.1449H32.0028V15.3636H34.7727V16.1449C34.7727 16.7438 34.6449 17.2647 34.3892 17.7074C34.1359 18.1501 33.7855 18.4934 33.3381 18.7372C32.893 18.9787 32.3816 19.0994 31.804 19.0994C31.16 19.0994 30.5942 18.9479 30.1065 18.6449C29.6212 18.3419 29.2424 17.911 28.9702 17.3523C28.7003 16.7936 28.5653 16.1307 28.5653 15.3636C28.5653 14.7884 28.6423 14.2711 28.7962 13.8118C28.9524 13.3501 29.1726 12.9571 29.4567 12.6328C29.7408 12.3085 30.0769 12.0599 30.4652 11.8871C30.8535 11.7143 31.2808 11.6278 31.7472 11.6278C32.1307 11.6278 32.4882 11.6858 32.8196 11.8018C33.1534 11.9155 33.4505 12.0777 33.7109 12.2884C33.9737 12.4967 34.1927 12.7464 34.3679 13.0376C34.5431 13.3265 34.6638 13.6473 34.7301 14H33.821ZM38.3833 19.1136C37.8909 19.1136 37.4589 18.9964 37.0872 18.7621C36.7179 18.5277 36.429 18.1998 36.2207 17.7784C36.0147 17.357 35.9118 16.8646 35.9118 16.3011C35.9118 15.733 36.0147 15.237 36.2207 14.8132C36.429 14.3894 36.7179 14.0604 37.0872 13.826C37.4589 13.5916 37.8909 13.4744 38.3833 13.4744C38.8758 13.4744 39.3066 13.5916 39.676 13.826C40.0476 14.0604 40.3365 14.3894 40.5424 14.8132C40.7508 15.237 40.8549 15.733 40.8549 16.3011C40.8549 16.8646 40.7508 17.357 40.5424 17.7784C40.3365 18.1998 40.0476 18.5277 39.676 18.7621C39.3066 18.9964 38.8758 19.1136 38.3833 19.1136ZM38.3833 18.3608C38.7574 18.3608 39.0652 18.2649 39.3066 18.0732C39.5481 17.8814 39.7269 17.6293 39.8429 17.3168C39.9589 17.0043 40.0169 16.6657 40.0169 16.3011C40.0169 15.9366 39.9589 15.5968 39.8429 15.282C39.7269 14.9671 39.5481 14.7126 39.3066 14.5185C39.0652 14.3243 38.7574 14.2273 38.3833 14.2273C38.0093 14.2273 37.7015 14.3243 37.46 14.5185C37.2186 14.7126 37.0398 14.9671 36.9238 15.282C36.8078 15.5968 36.7498 15.9366 36.7498 16.3011C36.7498 16.6657 36.8078 17.0043 36.9238 17.3168C37.0398 17.6293 37.2186 17.8814 37.46 18.0732C37.7015 18.2649 38.0093 18.3608 38.3833 18.3608ZM44.3501 19.1136C43.8577 19.1136 43.4257 18.9964 43.054 18.7621C42.6847 18.5277 42.3958 18.1998 42.1875 17.7784C41.9815 17.357 41.8786 16.8646 41.8786 16.3011C41.8786 15.733 41.9815 15.237 42.1875 14.8132C42.3958 14.3894 42.6847 14.0604 43.054 13.826C43.4257 13.5916 43.8577 13.4744 44.3501 13.4744C44.8426 13.4744 45.2734 13.5916 45.6428 13.826C46.0144 14.0604 46.3033 14.3894 46.5092 14.8132C46.7176 15.237 46.8217 15.733 46.8217 16.3011C46.8217 16.8646 46.7176 17.357 46.5092 17.7784C46.3033 18.1998 46.0144 18.5277 45.6428 18.7621C45.2734 18.9964 44.8426 19.1136 44.3501 19.1136ZM44.3501 18.3608C44.7242 18.3608 45.032 18.2649 45.2734 18.0732C45.5149 17.8814 45.6937 17.6293 45.8097 17.3168C45.9257 17.0043 45.9837 16.665Line truncated
fill="#7A7A7A"
/>
<rect
@@ -7,6 +7,7 @@ import {
ActionPlacement,
type ToolbarAction,
type ToolbarActionGroup,
type ToolbarContext,
type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import { getBlockProps } from '@blocksuite/affine-shared/utils';
@@ -35,6 +36,12 @@ export const builtinToolbarConfig = {
actions: [
{
id: 'b.conversions',
when: (ctx: ToolbarContext) => {
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<ToolbarAction>,
{
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) {
@@ -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;
@@ -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);
@@ -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<EmbedIframeBlockModel> {
selectedStyle$: ReadonlySignal<ClassInfo> | null = computed<ClassInfo>(
@@ -41,6 +51,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
readonly status$ = signal<EmbedIframeStatus>('idle');
readonly error$ = signal<Error | null>(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<EmbedIfra
!this.selected$.value)
);
private _iframeOptions: IframeOptions | undefined = undefined;
// since different providers have different border radius
// we need to update the selected border radius when the iframe is loaded
readonly selectedBorderRadius$ = computed(() => {
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<EmbedIfra
return this.std.get(LinkPreviewerService);
}
get notificationService() {
return this.std.getOptional(NotificationProvider);
}
get inSurface() {
return matchModels(this.model.parent, [SurfaceBlockModel]);
}
@@ -85,11 +112,26 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
open = () => {
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<EmbedIfra
);
}
const { url } = this.model.props;
if (!url) {
throw new BlockSuiteError(
ErrorCode.ValueNotExists,
'No original URL provided'
);
}
// get embed data and preview data in a promise
const [embedData, previewData] = await Promise.all([
embedIframeService.getEmbedIframeData(url),
@@ -148,6 +182,45 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
}
};
private _linkInputAbortController: AbortController | null = null;
toggleLinkInputPopup = (options?: EmbedLinkInputPopupOptions) => {
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`<embed-iframe-link-input-popup
.model=${this.model}
.abortController=${this._linkInputAbortController}
.std=${this.std}
.options=${options}
></embed-iframe-link-input-popup>`,
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<EmbedIfra
private readonly _updateIframeOptions = (url: string) => {
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<EmbedIfra
selectionManager.setGroup('note', [blockSelection]);
};
protected _handleClick = (event: MouseEvent) => {
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<EmbedIfra
referrerpolicy,
scrolling,
allowFullscreen,
} = this._iframeOptions ?? {};
} = this.iframeOptions ?? {};
const width = `${widthPercent}%`;
// if the block is in the surface, use 100% as the height
// otherwise, use the heightInNote
@@ -224,6 +299,10 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
};
private readonly _renderContent = () => {
if (this.isIdle$.value) {
return html`<embed-iframe-idle-card></embed-iframe-idle-card>`;
}
if (this.isLoading$.value) {
return html`<embed-iframe-loading-card
.std=${this.std}
@@ -286,6 +365,12 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
);
}
override disconnectedCallback() {
super.disconnectedCallback();
this._linkInputAbortController?.abort();
this._linkInputAbortController = null;
}
override renderBlock() {
if (!this.isEmbedIframeBlockEnabled) {
return nothing;
@@ -296,6 +381,10 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
...this.selectedStyle$?.value,
'in-surface': this.inSurface,
});
const containerStyles = styleMap({
borderRadius: `${this.selectedBorderRadius$.value}px`,
});
const overlayClasses = classMap({
'affine-embed-iframe-block-overlay': true,
show: this.showOverlay$.value,
@@ -305,6 +394,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
<div
draggable=${this.blockDraggable ? 'true' : 'false'}
class=${containerClasses}
style=${containerStyles}
@click=${this._handleClick}
@dblclick=${this._handleDoubleClick}
>
@@ -316,11 +406,21 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
`;
}
override accessor blockContainerStyles = { margin: '18px 0' };
override accessor blockContainerStyles = {
margin: '18px 0',
backgroundColor: 'transparent',
};
get readonly() {
return this.doc.readonly;
}
override accessor useCaptionEditor = true;
override accessor useZeroWidth = true;
override accessor selectedStyle = SelectedStyle.Border;
@query('.affine-embed-iframe-block-container')
accessor _blockContainer: HTMLElement | null = null;
}
@@ -1,6 +1,5 @@
export * from './adapters';
export * from './commands';
export * from './components/embed-iframe-create-modal';
export * from './configs';
export * from './embed-iframe-block';
export * from './embed-iframe-spec';
@@ -1,6 +1,6 @@
import { addSiblingAttachmentBlocks } from '@blocksuite/affine-block-attachment';
import { insertDatabaseBlockCommand } from '@blocksuite/affine-block-database';
import { toggleEmbedIframeCreateModal } from '@blocksuite/affine-block-embed';
import { insertEmptyEmbedIframeCommand } from '@blocksuite/affine-block-embed';
import { insertImagesCommand } from '@blocksuite/affine-block-image';
import { insertLatexBlockCommand } from '@blocksuite/affine-block-latex';
import {
@@ -484,24 +484,18 @@ const embedToolGroup: KeyboardToolPanelGroup = {
);
},
action: async ({ std }) => {
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();
},
},
{
@@ -26,6 +26,7 @@ export type IframeOptions = {
scrolling?: boolean;
allow?: string;
allowFullscreen?: boolean;
containerBorderRadius?: number;
};
/**
@@ -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',