feat(editor): support add embed iframe block in mobile (#10955)

To close [BS-2664](https://linear.app/affine-design/issue/BS-2664/iframe-embed-block-移动端支持)
This commit is contained in:
donteatfriedrice
2025-03-19 02:58:51 +00:00
parent 6b73e8ebb5
commit 3a2d386275
3 changed files with 179 additions and 37 deletions
@@ -4,11 +4,15 @@ import type { BlockStdScope } from '@blocksuite/block-std';
import { WithDisposable } from '@blocksuite/global/lit'; import { WithDisposable } from '@blocksuite/global/lit';
import { CloseIcon, EmbedIcon } from '@blocksuite/icons/lit'; import { CloseIcon, EmbedIcon } from '@blocksuite/icons/lit';
import type { BlockModel } from '@blocksuite/store'; import type { BlockModel } from '@blocksuite/store';
import { css, html, LitElement, nothing } from 'lit'; import { baseTheme } from '@toeverything/theme';
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
import { property, query, state } from 'lit/decorators.js'; import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { EmbedIframeService } from '../extension/embed-iframe-service'; import { EmbedIframeService } from '../extension/embed-iframe-service';
type EmbedModalVariant = 'default' | 'compact';
export class EmbedIframeCreateModal extends WithDisposable(LitElement) { export class EmbedIframeCreateModal extends WithDisposable(LitElement) {
static override styles = css` static override styles = css`
.embed-iframe-create-modal { .embed-iframe-create-modal {
@@ -40,12 +44,12 @@ export class EmbedIframeCreateModal extends WithDisposable(LitElement) {
background: ${unsafeCSSVarV2('layer/background/overlayPanel')}; background: ${unsafeCSSVarV2('layer/background/overlayPanel')};
box-shadow: ${unsafeCSSVar('overlayPanelShadow')}; box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
z-index: var(--affine-z-index-modal); z-index: var(--affine-z-index-modal);
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
} }
.modal-content-wrapper { .modal-content-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px;
} }
.modal-close-button { .modal-close-button {
@@ -85,17 +89,17 @@ export class EmbedIframeCreateModal extends WithDisposable(LitElement) {
border-radius: 50%; border-radius: 50%;
background: var(--affine-background-secondary-color); background: var(--affine-background-secondary-color);
color: ${unsafeCSSVarV2('icon/primary')}; color: ${unsafeCSSVarV2('icon/primary')};
}
}
.title, svg {
.description { width: 32px;
text-align: center; height: 32px;
}
}
} }
.title { .title {
/* Client/h6 */ /* Client/h6 */
font-family: Inter; text-align: center;
font-size: 18px; font-size: 18px;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
@@ -103,32 +107,30 @@ export class EmbedIframeCreateModal extends WithDisposable(LitElement) {
letter-spacing: -0.24px; letter-spacing: -0.24px;
color: ${unsafeCSSVarV2('text/primary')}; color: ${unsafeCSSVarV2('text/primary')};
} }
}
.description { .description {
font-feature-settings: margin-top: 8px;
'liga' off, text-align: center;
'clig' off; font-feature-settings:
/* Client/xs */ 'liga' off,
font-family: Inter; 'clig' off;
font-size: 12px; /* Client/xs */
font-style: normal; font-size: 12px;
font-weight: 400; font-style: normal;
line-height: 20px; /* 166.667% */ font-weight: 400;
color: ${unsafeCSSVarV2('text/secondary')}; line-height: 20px; /* 166.667% */
} color: ${unsafeCSSVarV2('text/secondary')};
} }
.input-container { .input-container {
width: 100%; width: 100%;
margin-top: 24px;
.link-input { .link-input {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
display: flex;
padding: 4px 10px; padding: 4px 10px;
align-items: center;
gap: 8px;
align-self: stretch;
border-radius: 8px; border-radius: 8px;
border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')}; border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
background: ${unsafeCSSVarV2('input/background')}; background: ${unsafeCSSVarV2('input/background')};
@@ -148,7 +150,6 @@ export class EmbedIframeCreateModal extends WithDisposable(LitElement) {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 20px 0px; padding: 20px 0px;
cursor: pointer;
.confirm-button { .confirm-button {
width: 100%; width: 100%;
@@ -163,16 +164,89 @@ export class EmbedIframeCreateModal extends WithDisposable(LitElement) {
color: ${unsafeCSSVarV2('button/pureWhiteText')}; color: ${unsafeCSSVarV2('button/pureWhiteText')};
/* Client/xsMedium */ /* Client/xsMedium */
font-family: Inter;
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
cursor: pointer;
} }
.confirm-button[disabled] { .confirm-button[disabled] {
opacity: 0.5; 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 = () => { private readonly _onClose = () => {
@@ -270,13 +344,20 @@ export class EmbedIframeCreateModal extends WithDisposable(LitElement) {
override render() { override render() {
const { showCloseButton } = this; const { showCloseButton } = this;
const { variant } = this;
const modalMainWrapperClass = classMap({
'modal-main-wrapper': true,
compact: variant === 'compact',
});
return html` return html`
<div class="embed-iframe-create-modal"> <div class="embed-iframe-create-modal">
<div <div
class="embed-iframe-create-modal-mask" class="embed-iframe-create-modal-mask"
@click=${this._onClose} @click=${this._onClose}
></div> ></div>
<div class="modal-main-wrapper"> <div class=${modalMainWrapperClass}>
${showCloseButton ${showCloseButton
? html` ? html`
<div class="modal-close-button" @click=${this._onClose}> <div class="modal-close-button" @click=${this._onClose}>
@@ -287,16 +368,12 @@ export class EmbedIframeCreateModal extends WithDisposable(LitElement) {
<div class="modal-content-wrapper"> <div class="modal-content-wrapper">
<div class="modal-content-header"> <div class="modal-content-header">
<div class="icon-container"> <div class="icon-container">
<div class="icon-background"> <div class="icon-background">${EmbedIcon()}</div>
${EmbedIcon({ width: '32px', height: '32px' })}
</div>
</div>
<div>
<div class="title">Embed Link</div>
<div class="description">
Works with links of PDFs, Google Drive, Google Maps, CodePen…
</div>
</div> </div>
<div class="title">Embed Link</div>
</div>
<div class="description">
Works with links of PDFs, Google Drive, Google Maps, CodePen…
</div> </div>
<div class="input-container"> <div class="input-container">
<input <input
@@ -342,6 +419,9 @@ export class EmbedIframeCreateModal extends WithDisposable(LitElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor showCloseButton: boolean = true; accessor showCloseButton: boolean = true;
@property({ attribute: false })
accessor variant: EmbedModalVariant = 'default';
} }
export async function toggleEmbedIframeCreateModal( export async function toggleEmbedIframeCreateModal(
@@ -349,6 +429,7 @@ export async function toggleEmbedIframeCreateModal(
createOptions: { createOptions: {
parentModel: BlockModel; parentModel: BlockModel;
index?: number; index?: number;
variant?: EmbedModalVariant;
} }
): Promise<void> { ): Promise<void> {
std.selection.clear(); std.selection.clear();
@@ -357,6 +438,7 @@ export async function toggleEmbedIframeCreateModal(
embedIframeCreateModal.std = std; embedIframeCreateModal.std = std;
embedIframeCreateModal.parentModel = createOptions.parentModel; embedIframeCreateModal.parentModel = createOptions.parentModel;
embedIframeCreateModal.index = createOptions.index; embedIframeCreateModal.index = createOptions.index;
embedIframeCreateModal.variant = createOptions.variant ?? 'default';
document.body.append(embedIframeCreateModal); document.body.append(embedIframeCreateModal);
@@ -34,6 +34,7 @@ export const embedIframeSlashMenuConfig: SlashMenuConfig = {
await toggleEmbedIframeCreateModal(std, { await toggleEmbedIframeCreateModal(std, {
parentModel, parentModel,
index, index,
variant: 'default',
}); });
if (model.text?.length === 0) std.store.deleteBlock(model); if (model.text?.length === 0) std.store.deleteBlock(model);
})().catch(console.error); })().catch(console.error);
@@ -1,5 +1,6 @@
import { addSiblingAttachmentBlocks } from '@blocksuite/affine-block-attachment'; import { addSiblingAttachmentBlocks } from '@blocksuite/affine-block-attachment';
import { insertDatabaseBlockCommand } from '@blocksuite/affine-block-database'; import { insertDatabaseBlockCommand } from '@blocksuite/affine-block-database';
import { toggleEmbedIframeCreateModal } from '@blocksuite/affine-block-embed';
import { insertImagesCommand } from '@blocksuite/affine-block-image'; import { insertImagesCommand } from '@blocksuite/affine-block-image';
import { insertLatexBlockCommand } from '@blocksuite/affine-block-latex'; import { insertLatexBlockCommand } from '@blocksuite/affine-block-latex';
import { import {
@@ -45,7 +46,10 @@ import {
getTextSelectionCommand, getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands'; } from '@blocksuite/affine-shared/commands';
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts'; import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import { FileSizeLimitService } from '@blocksuite/affine-shared/services'; import {
FeatureFlagService,
FileSizeLimitService,
} from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { import {
createDefaultDoc, createDefaultDoc,
@@ -70,6 +74,7 @@ import {
DeleteIcon, DeleteIcon,
DividerIcon, DividerIcon,
DuplicateIcon, DuplicateIcon,
EmbedIcon,
FontIcon, FontIcon,
FrameIcon, FrameIcon,
GithubIcon, GithubIcon,
@@ -444,6 +449,59 @@ const contentMediaToolGroup: KeyboardToolPanelGroup = {
} }
}, },
}, },
{
name: 'Equation',
icon: TeXIcon(),
showWhen: ({ std }) =>
std.store.schema.flavourSchemaMap.has('affine:latex'),
action: ({ std }) => {
std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(insertLatexBlockCommand, {
place: 'after',
removeEmptyLine: true,
})
.run();
},
},
],
};
const embedToolGroup: KeyboardToolPanelGroup = {
name: 'Embeds',
items: [
{
name: 'Embed',
icon: EmbedIcon({ style: `color: black` }),
showWhen: ({ std }) => {
const featureFlagService = std.get(FeatureFlagService);
return (
featureFlagService.getFlag('enable_embed_iframe_block') &&
std.store.schema.flavourSchemaMap.has('affine:embed-iframe')
);
},
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);
}
},
},
{ {
name: 'Youtube', name: 'Youtube',
icon: YoutubeDuotoneIcon({ icon: YoutubeDuotoneIcon({
@@ -744,6 +802,7 @@ const moreToolPanel: KeyboardToolPanelConfig = {
{ name: 'List', items: listToolActionItems }, { name: 'List', items: listToolActionItems },
pageToolGroup, pageToolGroup,
contentMediaToolGroup, contentMediaToolGroup,
embedToolGroup,
documentGroupFrameToolGroup, documentGroupFrameToolGroup,
dateToolGroup, dateToolGroup,
databaseToolGroup, databaseToolGroup,