refactor(editor): unify directories naming (#11516)

**Directory Structure Changes**

- Renamed multiple block-related directories by removing the "block-" prefix:
  - `block-attachment` → `attachment`
  - `block-bookmark` → `bookmark`
  - `block-callout` → `callout`
  - `block-code` → `code`
  - `block-data-view` → `data-view`
  - `block-database` → `database`
  - `block-divider` → `divider`
  - `block-edgeless-text` → `edgeless-text`
  - `block-embed` → `embed`
This commit is contained in:
Saul-Mirone
2025-04-07 12:34:40 +00:00
parent e1bd2047c4
commit 1f45cc5dec
893 changed files with 439 additions and 460 deletions
@@ -0,0 +1,342 @@
import { createLitPortal } from '@blocksuite/affine-components/portal';
import type { EmbedIframeBlockModel } from '@blocksuite/affine-model';
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { WithDisposable } from '@blocksuite/global/lit';
import { EditIcon, InformationIcon, ResetIcon } from '@blocksuite/icons/lit';
import type { BlockStdScope } from '@blocksuite/std';
import { flip, offset } from '@floating-ui/dom';
import { baseTheme } from '@toeverything/theme';
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { ERROR_CARD_DEFAULT_HEIGHT } from '../consts';
import type { EmbedIframeStatusCardOptions } from '../types';
const LINK_EDIT_POPUP_OFFSET = 12;
export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
static override styles = css`
:host {
width: 100%;
height: 100%;
}
.affine-embed-iframe-error-card {
container: affine-embed-iframe-error-card / size;
display: flex;
box-sizing: border-box;
user-select: none;
padding: 12px;
gap: 12px;
overflow: hidden;
border-radius: 8px;
border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
background: ${unsafeCSSVarV2('layer/background/secondary')};
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
user-select: none;
.error-content {
display: flex;
flex-direction: column;
gap: 4px;
.error-title {
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
.error-icon {
display: flex;
justify-content: center;
align-items: center;
color: ${unsafeCSSVarV2('status/error')};
}
.error-title-text {
color: ${unsafeCSSVarV2('text/primary')};
text-align: justify;
/* Client/smBold */
font-size: var(--affine-font-sm);
font-style: normal;
font-weight: 600;
line-height: 22px; /* 157.143% */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.error-message {
display: flex;
align-self: stretch;
color: ${unsafeCSSVarV2('text/secondary')};
overflow: hidden;
font-feature-settings:
'liga' off,
'clig' off;
text-overflow: ellipsis;
/* Client/xs */
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
}
.error-info {
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
.button {
display: flex;
padding: 0px 4px;
align-items: center;
border-radius: 4px;
cursor: pointer;
.icon {
display: flex;
justify-content: center;
align-items: center;
}
.text {
padding: 0px 4px;
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
}
}
.button.edit {
color: ${unsafeCSSVarV2('text/secondary')};
}
.button.retry {
color: ${unsafeCSSVarV2('text/emphasis')};
}
}
}
}
.affine-embed-iframe-error-card.horizontal {
flex-direction: row;
align-items: flex-start;
.error-content {
align-items: flex-start;
flex: 1 0 0;
.error-message {
height: 40px;
align-items: flex-start;
}
}
@container affine-embed-iframe-error-card (width < 480px) {
.error-banner {
display: none;
}
}
}
.affine-embed-iframe-error-card.vertical {
flex-direction: column-reverse;
align-items: center;
justify-content: center;
.error-content {
justify-content: center;
align-items: center;
.error-message {
justify-content: center;
align-items: center;
}
}
.icon-box {
svg {
transform: scale(1.6) translateY(-14px);
}
}
@container affine-embed-iframe-error-card (height < 300px) or (width < 300px) {
.error-banner {
display: none;
}
}
}
`;
private _editAbortController: AbortController | null = null;
private readonly _toggleEdit = (e: MouseEvent) => {
e.stopPropagation();
if (!this._editButton || this.readonly) {
return;
}
if (this._editAbortController) {
this._editAbortController.abort();
}
this._editAbortController = new AbortController();
createLitPortal({
template: html`<embed-iframe-link-edit-popup
.model=${this.model}
.abortController=${this._editAbortController}
.std=${this.std}
.inSurface=${this.inSurface}
></embed-iframe-link-edit-popup>`,
container: document.body,
computePosition: {
referenceElement: this._editButton,
placement: 'bottom-start',
middleware: [flip(), offset(LINK_EDIT_POPUP_OFFSET)],
autoUpdate: { animationFrame: true },
},
abortController: this._editAbortController,
closeOnClickAway: true,
});
};
private readonly _handleRetry = (e: MouseEvent) => {
e.stopPropagation();
this.onRetry();
// track retry event
this.telemetryService?.track('ReloadLink', {
type: 'embed iframe block',
page: this.editorMode === 'page' ? 'doc editor' : 'whiteboard editor',
segment: 'editor',
module: 'embed block',
control: 'reload button',
});
};
override render() {
const { layout, width, height } = this.options;
const cardClasses = classMap({
'affine-embed-iframe-error-card': true,
horizontal: layout === 'horizontal',
vertical: layout === 'vertical',
});
const cardWidth = width ? `${width}px` : '100%';
const cardHeight = height ? `${height}px` : '100%';
const cardStyle = styleMap({
width: cardWidth,
height: cardHeight,
});
return html`
<div class=${cardClasses} style=${cardStyle}>
<div class="error-content">
<div class="error-title">
<span class="error-icon">
${InformationIcon({ width: '16px', height: '16px' })}
</span>
<span class="error-title-text">This link couldnt be loaded.</span>
</div>
<div class="error-message">
${this.error?.message || 'Failed to load embedded content'}
</div>
<div class="error-info">
${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
>
<span class="text">Reload</span>
</div>
</div>
</div>
<div class="error-banner">
<div class="icon-box">${EmbedIframeErrorIcon}</div>
</div>
</div>
`;
}
get host() {
return this.std.host;
}
get readonly() {
return this.model.doc.readonly;
}
get telemetryService() {
return this.std.getOptional(TelemetryProvider);
}
get editorMode() {
const docModeService = this.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
return mode ?? 'page';
}
@query('.button.edit')
accessor _editButton: HTMLElement | null = null;
@property({ attribute: false })
accessor error: Error | null = null;
@property({ attribute: false })
accessor onRetry!: () => void;
@property({ attribute: false })
accessor model!: EmbedIframeBlockModel;
@property({ attribute: false })
accessor std!: BlockStdScope;
@property({ attribute: false })
accessor inSurface = false;
@property({ attribute: false })
accessor options: EmbedIframeStatusCardOptions = {
layout: 'horizontal',
height: ERROR_CARD_DEFAULT_HEIGHT,
};
}
export const EmbedIframeErrorIcon = html`<svg
width="204"
height="102"
viewBox="0 0 204 102"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_2676_106795)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M94.6838 8.45092L106.173 31.9276L84.6593 57.0514L90.5888 64.9202C88.6083 64.6092 86.5089 65.0701 84.7813 66.3719L78.4802 71.1202C75.0967 73.6698 74.4207 78.4796 76.9704 81.8631C79.5201 85.2467 84.3299 85.9227 87.7134 83.373L89.4487 82.0654C90.3714 81.37 90.5558 80.0582 89.8604 79.1354C89.1651 78.2127 87.8533 78.0283 86.9305 78.7237L85.1952 80.0313C83.6573 81.1902 81.471 80.883 80.3121 79.345C79.1531 77.807 79.4604 75.6208 80.9984 74.4618L87.2995 69.7136C88.8375 68.5547 91.0237 68.8619 92.1827 70.3999C92.8645 71.3047 94.1389 71.4996 95.0582 70.8513L95.8982 71.966L94.6469 72.9089C93.109 74.0679 90.9227 73.7606 89.7638 72.2227C89.0684 71.2999 87.7566 71.1155 86.8339 71.8109C85.9111 72.5062 85.7267 73.818 86.4221 74.7408C88.9718 78.1243 93.7816 78.8003 97.1651 76.2506L98.4164 75.3077L99.8156 77.1646L86.8434 102.707L89.291 114.735L42.1397 108.108L56.3354 7.10072C56.6429 4.91308 58.6655 3.38889 60.8532 3.69634L94.6838 8.45092ZM122.987 12.4287L119.974 33.8672L95.4607 58.4925C98.7006 56.8928 102.722 57.7678 104.976 60.7594C107.526 64.1429 106.85 68.9527 103.466 71.5024L102.718 72.0665L105.949 78.0266L92.2105 103.461L92.9872 115.254L147.108 122.86L161.304 21.8531C161.611 19.6654 160.087 17.6428 157.899 17.3353L122.987 12.4287ZM100.701 68.3471L100.948 68.1607C102.486 67.0018 102.793 64.8155 101.634 63.2775C100.625 61.9381 98.8364 61.5321 97.3755 62.2152L100.701 68.3471ZM88.8231 36.502C84.6277 35.9124 80.7486 38.8354 80.159 43.0308L79.1885 49.9367C79.0277 51.0809 79.8249 52.1388 80.9691 52.2996C82.1133 52.4604 83.1712 51.6632 83.332 50.519L84.3025 43.6132C84.5705 41.7062 86.3337 40.3775 88.2407 40.6455L95.1466 41.6161C96.2908 41.7769 97.3487 40.9797 97.5095 39.8355C97.6703 38.6913 96.8731 37.6334 95.7289 37.4726L88.8231 36.502ZM115.065 40.1901C113.921 40.0293 112.863 40.8265 112.702 41.9707C112.542 43.1149 113.339 44.1728 114.483 44.3336L121.389 45.3042C123.296 45.5722 124.625 47.3354 124.357 49.2424L123.386 56.1483C123.225 57.2925 124.022 58.3504 125.167 58.5112C126.311 58.672 127.369 57.8748 127.529 56.7306L128.5 49.8247C129.09 45.6293 126.167 41.7503 121.971 41.1607L115.065 40.1901ZM123.031 73.7041C124.176 73.8649 124.973 74.9228 124.812 76.067L123.841 82.9728C123.252 87.1682 119.373 90.0913 115.177 89.5017L106.89 88.337C105.746 88.1762 104.949 87.1183 105.11 85.9741C105.27 84.8299 106.328 84.0327 107.473 84.1935L115.76 85.3582C117.667 85.6262 119.43 84.2975 119.698 82.3905L120.668 75.4847C120.829 74.3405 121.887 73.5433 123.031 73.7041Z"
fill="#E6E6E6"
/>
</g>
<defs>
<clipPath id="clip0_2676_106795">
<rect width="204" height="102" fill="white" />
</clipPath>
</defs>
</svg>`;
@@ -0,0 +1,132 @@
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';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { IDLE_CARD_DEFAULT_HEIGHT } from '../consts';
import type { EmbedIframeStatusCardOptions } from '../types';
export class EmbedIframeIdleCard extends WithDisposable(LitElement) {
static override styles = css`
:host {
width: 100%;
height: 100%;
}
.affine-embed-iframe-idle-card {
container: affine-embed-iframe-idle-card / size;
box-sizing: border-box;
display: flex;
align-items: center;
padding: 12px;
gap: 8px;
border-radius: 8px;
background-color: ${unsafeCSSVarV2('layer/background/secondary')};
.icon {
display: flex;
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;
}
.affine-embed-iframe-idle-card.horizontal {
flex-direction: row;
.icon {
width: 24px;
height: 24px;
svg {
width: 24px;
height: 24px;
}
}
}
.affine-embed-iframe-idle-card.vertical {
flex-direction: column;
justify-content: center;
overflow: hidden;
gap: 12px;
.icon {
width: 176px;
height: 112px;
overflow-y: hidden;
svg {
width: 112px;
height: 112px;
transform: rotate(12deg) translateY(18%);
}
}
.text {
text-align: center;
white-space: normal;
word-break: break-word;
}
@container affine-embed-iframe-idle-card (height < 180px) {
.icon {
display: none;
}
}
}
`;
override render() {
const { layout, width, height } = this.options;
const cardClasses = classMap({
'affine-embed-iframe-idle-card': true,
horizontal: layout === 'horizontal',
vertical: layout === 'vertical',
});
const cardWidth = width ? `${width}px` : '100%';
const cardHeight = height ? `${height}px` : '100%';
const cardStyle = styleMap({
width: cardWidth,
height: cardHeight,
});
return html`
<div class=${cardClasses} style=${cardStyle}>
<span class="icon"> ${EmbedIcon()} </span>
<span class="text">
Embed anything (Google Drive, Google Docs, Spotify, Miro…)
</span>
</div>
`;
}
@property({ attribute: false })
accessor options: EmbedIframeStatusCardOptions = {
layout: 'horizontal',
height: IDLE_CARD_DEFAULT_HEIGHT,
};
}
@@ -0,0 +1,122 @@
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { SignalWatcher } from '@blocksuite/global/lit';
import { DoneIcon } from '@blocksuite/icons/lit';
import { css, html } from 'lit';
import { EmbedIframeLinkInputBase } from './embed-iframe-link-input-base';
export class EmbedIframeLinkEditPopup extends SignalWatcher(
EmbedIframeLinkInputBase
) {
static override styles = css`
.embed-iframe-link-edit-popup {
display: flex;
padding: 12px;
align-items: center;
gap: 12px;
color: var(--affine-text-primary-color);
border-radius: 8px;
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
background: ${unsafeCSSVarV2('layer/background/primary')};
box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
.input-container {
display: flex;
width: 280px;
align-items: center;
border: 1px solid var(--affine-border-color);
border-radius: 4px;
padding: 0 8px;
background-color: var(--affine-background-color);
gap: 8px;
.input-label {
color: var(--affine-text-secondary-color);
font-size: 14px;
margin-right: 8px;
white-space: nowrap;
}
.link-input {
flex: 1;
border: none;
outline: none;
padding: 8px 0;
background: transparent;
font-size: 14px;
}
}
.input-container:focus-within {
border-color: var(--affine-blue-700);
outline: none;
}
.confirm-button {
cursor: pointer;
display: flex;
padding: 2px;
justify-content: center;
align-items: center;
color: ${unsafeCSSVarV2('icon/activated')};
}
.confirm-button[disabled] {
color: ${unsafeCSSVarV2('icon/primary')};
}
}
`;
protected override track(status: 'success' | 'failure') {
this.telemetryService?.track('EditLink', {
type: 'embed iframe block',
page: this.editorMode === 'page' ? 'doc editor' : 'whiteboard editor',
segment: 'editor',
module: 'embed block',
control: 'edit button',
other: status,
});
}
override render() {
const isInputEmpty = this.isInputEmpty();
const { url$ } = this.model.props;
return html`
<div class="embed-iframe-link-edit-popup">
<div class="input-container">
<span class="input-label">Link</span>
<input
class="link-input"
type="text"
spellcheck="false"
placeholder=${url$.value}
@input=${this.handleInput}
@keydown=${this.handleKeyDown}
/>
</div>
<div
class="confirm-button"
?disabled=${isInputEmpty}
@click=${this.onConfirm}
>
${DoneIcon({ width: '24px', height: '24px' })}
</div>
</div>
`;
}
get telemetryService() {
return this.std.getOptional(TelemetryProvider);
}
get editorMode() {
const docModeService = this.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
return mode ?? 'page';
}
}
@@ -0,0 +1,160 @@
import type { EmbedIframeBlockModel } from '@blocksuite/affine-model';
import {
EmbedIframeService,
NotificationProvider,
} from '@blocksuite/affine-shared/services';
import { isValidUrl, stopPropagation } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/lit';
import { noop } from '@blocksuite/global/utils';
import {
BlockSelection,
type BlockStdScope,
SurfaceSelection,
} from '@blocksuite/std';
import { LitElement } from 'lit';
import { property, query, state } from 'lit/decorators.js';
export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
// this method is used to track the event when the user inputs the link
// it should be overridden by the subclass
protected track(status: 'success' | 'failure') {
noop(status);
}
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);
if (this.inSurface) {
this.std.selection.setGroup('gfx', [
this.std.selection.create(
SurfaceSelection,
blockId,
[blockId],
false
),
]);
} else {
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');
this.track('failure');
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: '',
});
this.track('success');
} catch (error) {
this.track('failure');
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);
this.disposables.addFromEvent(this, 'pointerdown', 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;
@property({ attribute: false })
accessor inSurface = false;
}
@@ -0,0 +1,292 @@
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
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;
telemetrySegment?: 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...',
telemetrySegment: 'editor',
};
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')};
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: ${unsafeCSSVarV2('text/placeholder')};
}
}
.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();
};
protected override track(status: 'success' | 'failure') {
this.telemetryService?.track('CreateEmbedBlock', {
type: 'embed iframe block',
page: this.editorMode === 'page' ? 'doc editor' : 'whiteboard editor',
segment: this.options?.telemetrySegment ?? 'editor',
module: 'embed block',
control: 'confirm embed link',
other: status,
});
}
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>
`;
}
get telemetryService() {
return this.std.getOptional(TelemetryProvider);
}
get editorMode() {
const docModeService = this.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
return mode ?? 'page';
}
@property({ attribute: false })
accessor options: EmbedLinkInputPopupOptions | undefined = undefined;
}
@@ -0,0 +1,197 @@
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { EmbedIcon } from '@blocksuite/icons/lit';
import { type BlockStdScope } from '@blocksuite/std';
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { getEmbedCardIcons } from '../../common/utils';
import { LOADING_CARD_DEFAULT_HEIGHT } from '../consts';
import type { EmbedIframeStatusCardOptions } from '../types';
export class EmbedIframeLoadingCard extends LitElement {
static override styles = css`
:host {
width: 100%;
height: 100%;
}
.affine-embed-iframe-loading-card {
container: affine-embed-iframe-loading-card / size;
display: flex;
box-sizing: border-box;
border-radius: 8px;
user-select: none;
padding: 12px;
gap: 12px;
overflow: hidden;
border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
background: ${unsafeCSSVarV2('layer/white')};
.loading-content {
display: flex;
gap: 8px;
align-self: stretch;
.loading-spinner {
display: flex;
width: 24px;
height: 24px;
justify-content: center;
align-items: center;
}
.loading-text {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
color: ${unsafeCSSVarV2('text/primary')};
text-overflow: ellipsis;
/* Client/smMedium */
font-family: Inter;
font-size: var(--affine-font-sm);
font-style: normal;
font-weight: 500;
line-height: 22px; /* 157.143% */
}
}
.loading-banner {
display: flex;
box-sizing: border-box;
justify-content: center;
align-items: center;
flex-shrink: 0;
.icon-box {
display: flex;
transform: rotate(8deg);
justify-content: center;
align-items: center;
flex-shrink: 0;
border-radius: 4px 4px 0px 0px;
background: ${unsafeCSSVarV2('slashMenu/background')};
box-shadow: 0px 0px 5px 0px rgba(66, 65, 73, 0.17);
svg {
fill: black;
fill-opacity: 0.07;
}
}
}
@container affine-embed-iframe-loading-card (width < 360px) {
.loading-banner {
display: none;
}
}
}
.affine-embed-iframe-loading-card.horizontal {
flex-direction: row;
align-items: flex-start;
.loading-content {
flex: 1 0 0;
align-items: flex-start;
.loading-text {
flex: 1 0 0;
}
}
.loading-banner {
width: 204px;
padding: 3.139px 42.14px 0px 42.14px;
.icon-box {
width: 106px;
height: 106px;
svg {
width: 66px;
height: 66px;
}
}
}
}
.affine-embed-iframe-loading-card.vertical {
flex-direction: column-reverse;
align-items: center;
justify-content: center;
.loading-content {
justify-content: center;
font-size: 14px;
transform: translateX(-2%);
}
.loading-banner {
width: 340px;
padding: 5.23px 70.234px 0px 70.232px;
overflow-y: hidden;
.icon-box {
width: 176px;
height: 176px;
transform: rotate(8deg) translateY(15%);
svg {
width: 112px;
height: 112px;
}
}
}
@container affine-embed-iframe-loading-card (height < 240px) {
.loading-banner {
display: none;
}
}
}
`;
override render() {
const theme = this.std.get(ThemeProvider).theme;
const { LoadingIcon } = getEmbedCardIcons(theme);
const { layout, width, height } = this.options;
const cardClasses = classMap({
'affine-embed-iframe-loading-card': true,
horizontal: layout === 'horizontal',
vertical: layout === 'vertical',
});
const cardWidth = width ? `${width}px` : '100%';
const cardHeight = height ? `${height}px` : '100%';
const cardStyle = styleMap({
width: cardWidth,
height: cardHeight,
});
return html`
<div class=${cardClasses} style=${cardStyle}>
<div class="loading-content">
<div class="loading-spinner">${LoadingIcon}</div>
<div class="loading-text">Loading...</div>
</div>
<div class="loading-banner">
<div class="icon-box">${EmbedIcon()}</div>
</div>
</div>
`;
}
@property({ attribute: false })
accessor std!: BlockStdScope;
@property({ attribute: false })
accessor options: EmbedIframeStatusCardOptions = {
layout: 'horizontal',
height: LOADING_CARD_DEFAULT_HEIGHT,
};
}