mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
refactor(editor): move embed iframe config and service extension to shared (#11029)
This commit is contained in:
@@ -2,6 +2,7 @@ import {
|
||||
EdgelessCRUDIdentifier,
|
||||
SurfaceBlockComponent,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { EmbedIframeService } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
BlockSelection,
|
||||
type Command,
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE,
|
||||
EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE,
|
||||
} from '../consts';
|
||||
import { EmbedIframeService } from '../extension/embed-iframe-service';
|
||||
|
||||
export const insertEmbedIframeCommand: Command<
|
||||
{ url: string },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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';
|
||||
@@ -9,8 +10,6 @@ import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import { EmbedIframeService } from '../extension/embed-iframe-service';
|
||||
|
||||
type EmbedModalVariant = 'default' | 'compact';
|
||||
|
||||
export class EmbedIframeCreateModal extends WithDisposable(LitElement) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -7,8 +8,6 @@ import { DoneIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
|
||||
import { EmbedIframeService } from '../extension/embed-iframe-service';
|
||||
|
||||
export class EmbedIframeLinkEditPopup extends SignalWatcher(
|
||||
WithDisposable(LitElement)
|
||||
) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EmbedIframeConfigExtension } from '../../extension/embed-iframe-config';
|
||||
import { EmbedIframeConfigExtension } from '@blocksuite/affine-shared/services';
|
||||
|
||||
import {
|
||||
type EmbedIframeUrlValidationOptions,
|
||||
validateEmbedIframeUrl,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EmbedIframeConfigExtension } from '../../extension/embed-iframe-config';
|
||||
import { EmbedIframeConfigExtension } from '@blocksuite/affine-shared/services';
|
||||
|
||||
import {
|
||||
type EmbedIframeUrlValidationOptions,
|
||||
validateEmbedIframeUrl,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EmbedIframeConfigExtension } from '../../extension/embed-iframe-config';
|
||||
import { EmbedIframeConfigExtension } from '@blocksuite/affine-shared/services';
|
||||
|
||||
import {
|
||||
type EmbedIframeUrlValidationOptions,
|
||||
validateEmbedIframeUrl,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EmbedIframeConfigExtension } from '../../extension/embed-iframe-config';
|
||||
import { EmbedIframeConfigExtension } from '@blocksuite/affine-shared/services';
|
||||
|
||||
import {
|
||||
type EmbedIframeUrlValidationOptions,
|
||||
validateEmbedIframeUrl,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EmbedIframeConfigExtension } from '../../extension/embed-iframe-config';
|
||||
import { EmbedIframeConfigExtension } from '@blocksuite/affine-shared/services';
|
||||
|
||||
import {
|
||||
type EmbedIframeUrlValidationOptions,
|
||||
validateEmbedIframeUrl,
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
} from '@blocksuite/affine-components/caption';
|
||||
import type { EmbedIframeBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
type EmbedIframeData,
|
||||
EmbedIframeService,
|
||||
FeatureFlagService,
|
||||
type IframeOptions,
|
||||
LinkPreviewerService,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
@@ -16,11 +19,6 @@ import { html, nothing } from 'lit';
|
||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
|
||||
import type { IframeOptions } from './extension/embed-iframe-config.js';
|
||||
import {
|
||||
type EmbedIframeData,
|
||||
EmbedIframeService,
|
||||
} from './extension/embed-iframe-service.js';
|
||||
import { embedIframeBlockStyles } from './style.js';
|
||||
import type { EmbedIframeStatusCardOptions } from './types.js';
|
||||
import { safeGetIframeSrc } from './utils.js';
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import {
|
||||
createIdentifier,
|
||||
type ServiceIdentifier,
|
||||
} from '@blocksuite/global/di';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
/**
|
||||
* The options for the iframe
|
||||
* @example
|
||||
* {
|
||||
* widthInSurface: 640,
|
||||
* heightInSurface: 152,
|
||||
* heightInNote: 152,
|
||||
* widthPercent: 100,
|
||||
* style: 'border-radius: 12px;',
|
||||
* allow: 'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture',
|
||||
* }
|
||||
*/
|
||||
export type IframeOptions = {
|
||||
widthInSurface: number; // the default width of embed iframe in surface, in pixels
|
||||
heightInSurface: number; // the default height of embed iframe in surface, in pixels
|
||||
heightInNote: number; // the default height of embed iframe in note, in pixels
|
||||
widthPercent: number; // the width percentage of embed iframe relative to parent container width, normalized to 0-100
|
||||
style?: string;
|
||||
referrerpolicy?: string;
|
||||
scrolling?: boolean;
|
||||
allow?: string;
|
||||
allowFullscreen?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Define the config of an embed iframe block
|
||||
* @example
|
||||
* {
|
||||
* name: 'spotify',
|
||||
* match: (url: string) => spotifyRegex.test(url),
|
||||
* buildOEmbedUrl: (url: string) => {
|
||||
* const match = url.match(spotifyRegex);
|
||||
* if (!match) {
|
||||
* return undefined;
|
||||
* }
|
||||
* const encodedUrl = encodeURIComponent(url);
|
||||
* const oEmbedUrl = `${spotifyEndpoint}?url=${encodedUrl}`;
|
||||
* return oEmbedUrl;
|
||||
* },
|
||||
* useOEmbedUrlDirectly: false,
|
||||
* options: {
|
||||
* defaultWidth: '100%',
|
||||
* defaultHeight: '152px',
|
||||
* allow: 'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture'
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export type EmbedIframeConfig = {
|
||||
/**
|
||||
* The name of the embed iframe block
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The function to match the url
|
||||
*/
|
||||
match: (url: string) => boolean;
|
||||
/**
|
||||
* The function to build the oEmbed URL for fetching embed data
|
||||
*/
|
||||
buildOEmbedUrl: (url: string) => string | undefined;
|
||||
/**
|
||||
* Use oEmbed URL directly as iframe src without fetching oEmbed data
|
||||
*/
|
||||
useOEmbedUrlDirectly: boolean;
|
||||
/**
|
||||
* The options for the iframe
|
||||
*/
|
||||
options?: IframeOptions;
|
||||
};
|
||||
|
||||
export const EmbedIframeConfigIdentifier =
|
||||
createIdentifier<EmbedIframeConfig>('EmbedIframeConfig');
|
||||
|
||||
export function EmbedIframeConfigExtension(
|
||||
config: EmbedIframeConfig
|
||||
): ExtensionType & {
|
||||
identifier: ServiceIdentifier<EmbedIframeConfig>;
|
||||
} {
|
||||
const identifier = EmbedIframeConfigIdentifier(config.name);
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(identifier, () => config);
|
||||
},
|
||||
identifier,
|
||||
};
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import type { EmbedIframeBlockProps } from '@blocksuite/affine-model';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import { type Store, StoreExtension } from '@blocksuite/store';
|
||||
|
||||
import {
|
||||
type EmbedIframeConfig,
|
||||
EmbedIframeConfigIdentifier,
|
||||
} from './embed-iframe-config';
|
||||
|
||||
export type EmbedIframeData = {
|
||||
html?: string;
|
||||
iframe_url?: string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
provider_name?: string;
|
||||
provider_url?: string;
|
||||
version?: string;
|
||||
thumbnail_url?: string;
|
||||
thumbnail_width?: number;
|
||||
thumbnail_height?: number;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Service for handling embeddable URLs
|
||||
*/
|
||||
export interface EmbedIframeProvider {
|
||||
/**
|
||||
* Check if a URL can be embedded
|
||||
* @param url URL to check
|
||||
* @returns true if the URL can be embedded, false otherwise
|
||||
*/
|
||||
canEmbed: (url: string) => boolean;
|
||||
|
||||
/**
|
||||
* Build a API URL for fetching embed data
|
||||
* @param url URL to build API URL
|
||||
* @returns API URL if the URL can be embedded, undefined otherwise
|
||||
*/
|
||||
buildOEmbedUrl: (url: string) => string | undefined;
|
||||
|
||||
/**
|
||||
* Get the embed iframe config
|
||||
* @param url URL to get embed iframe config
|
||||
* @returns Embed iframe config if the URL can be embedded, undefined otherwise
|
||||
*/
|
||||
getConfig: (url: string) => EmbedIframeConfig | undefined;
|
||||
|
||||
/**
|
||||
* Get embed iframe data
|
||||
* @param url URL to get embed iframe data
|
||||
* @returns Embed iframe data if the URL can be embedded, undefined otherwise
|
||||
*/
|
||||
getEmbedIframeData: (url: string) => Promise<EmbedIframeData | null>;
|
||||
|
||||
/**
|
||||
* Parse an embeddable URL and add an EmbedIframeBlock to doc
|
||||
* @param url Original url to embed
|
||||
* @param parentId Parent block ID
|
||||
* @param index Optional index to insert at
|
||||
* @returns Created block id if successful, undefined if the URL cannot be embedded
|
||||
*/
|
||||
addEmbedIframeBlock: (
|
||||
props: Partial<EmbedIframeBlockProps>,
|
||||
parentId: string,
|
||||
index?: number
|
||||
) => string | undefined;
|
||||
}
|
||||
|
||||
export const EmbedIframeProvider = createIdentifier<EmbedIframeProvider>(
|
||||
'EmbedIframeProvider'
|
||||
);
|
||||
|
||||
export class EmbedIframeService
|
||||
extends StoreExtension
|
||||
implements EmbedIframeProvider
|
||||
{
|
||||
static override key = 'embed-iframe-service';
|
||||
|
||||
private readonly _configs: EmbedIframeConfig[];
|
||||
|
||||
constructor(store: Store) {
|
||||
super(store);
|
||||
this._configs = Array.from(
|
||||
store.provider.getAll(EmbedIframeConfigIdentifier).values()
|
||||
);
|
||||
}
|
||||
|
||||
canEmbed = (url: string): boolean => {
|
||||
return this._configs.some(config => config.match(url));
|
||||
};
|
||||
|
||||
buildOEmbedUrl = (url: string): string | undefined => {
|
||||
return this._configs.find(config => config.match(url))?.buildOEmbedUrl(url);
|
||||
};
|
||||
|
||||
getConfig = (url: string): EmbedIframeConfig | undefined => {
|
||||
return this._configs.find(config => config.match(url));
|
||||
};
|
||||
|
||||
getEmbedIframeData = async (
|
||||
url: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<EmbedIframeData | null> => {
|
||||
try {
|
||||
const config = this._configs.find(config => config.match(url));
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const oEmbedUrl = config.buildOEmbedUrl(url);
|
||||
if (!oEmbedUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if the config useOEmbedUrlDirectly is true, return the url directly as iframe_url
|
||||
if (config.useOEmbedUrlDirectly) {
|
||||
return {
|
||||
iframe_url: oEmbedUrl,
|
||||
};
|
||||
}
|
||||
|
||||
// otherwise, fetch the oEmbed data
|
||||
const response = await fetch(oEmbedUrl, { signal });
|
||||
if (!response.ok) {
|
||||
console.warn(
|
||||
`Failed to fetch oEmbed data: ${response.status} ${response.statusText}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data as EmbedIframeData;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name !== 'AbortError') {
|
||||
console.error('Error fetching embed iframe data:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
addEmbedIframeBlock = (
|
||||
props: Partial<EmbedIframeBlockProps>,
|
||||
parentId: string,
|
||||
index?: number
|
||||
): string | undefined => {
|
||||
const blockId = this.store.addBlock(
|
||||
'affine:embed-iframe',
|
||||
props,
|
||||
parentId,
|
||||
index
|
||||
);
|
||||
return blockId;
|
||||
};
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './embed-iframe-config';
|
||||
export * from './embed-iframe-service';
|
||||
@@ -4,4 +4,3 @@ export * from './components/embed-iframe-create-modal';
|
||||
export * from './configs';
|
||||
export * from './embed-iframe-block';
|
||||
export * from './embed-iframe-spec';
|
||||
export * from './extension';
|
||||
|
||||
Reference in New Issue
Block a user