mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-23 21:06:22 +08:00
feat(editor): add embed iframe provider miro (#10996)
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { GoogleDriveEmbedConfig } from './google-drive';
|
||||
import { MiroEmbedConfig } from './miro';
|
||||
import { SpotifyEmbedConfig } from './spotify';
|
||||
|
||||
export const EmbedIframeConfigExtensions = [
|
||||
SpotifyEmbedConfig,
|
||||
GoogleDriveEmbedConfig,
|
||||
MiroEmbedConfig,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { EmbedIframeConfigExtension } from '../../extension/embed-iframe-config';
|
||||
import {
|
||||
type EmbedIframeUrlValidationOptions,
|
||||
validateEmbedIframeUrl,
|
||||
} from '../../utils';
|
||||
|
||||
const MIRO_DEFAULT_WIDTH_IN_SURFACE = 640;
|
||||
const MIRO_DEFAULT_HEIGHT_IN_SURFACE = 480;
|
||||
const MIRO_DEFAULT_HEIGHT_IN_NOTE = 480;
|
||||
const MIRO_DEFAULT_WIDTH_PERCENT = 100;
|
||||
|
||||
// https://developers.miro.com/reference/getembeddata
|
||||
const miroEndpoint = 'https://miro.com/api/v1/oembed';
|
||||
|
||||
const miroUrlValidationOptions: EmbedIframeUrlValidationOptions = {
|
||||
protocols: ['https:'],
|
||||
hostnames: ['miro.com'],
|
||||
};
|
||||
|
||||
const miroConfig = {
|
||||
name: 'miro',
|
||||
match: (url: string) => validateEmbedIframeUrl(url, miroUrlValidationOptions),
|
||||
buildOEmbedUrl: (url: string) => {
|
||||
const match = validateEmbedIframeUrl(url, miroUrlValidationOptions);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const encodedUrl = encodeURIComponent(url);
|
||||
const oEmbedUrl = `${miroEndpoint}?url=${encodedUrl}`;
|
||||
return oEmbedUrl;
|
||||
},
|
||||
useOEmbedUrlDirectly: false,
|
||||
options: {
|
||||
widthInSurface: MIRO_DEFAULT_WIDTH_IN_SURFACE,
|
||||
heightInSurface: MIRO_DEFAULT_HEIGHT_IN_SURFACE,
|
||||
heightInNote: MIRO_DEFAULT_HEIGHT_IN_NOTE,
|
||||
widthPercent: MIRO_DEFAULT_WIDTH_PERCENT,
|
||||
allow: 'fullscreen; clipboard-read; clipboard-write',
|
||||
style: 'border: 0; border-radius: 8px;',
|
||||
allowFullscreen: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const MiroEmbedConfig = EmbedIframeConfigExtension(miroConfig);
|
||||
+1
-2
@@ -36,8 +36,7 @@ const spotifyConfig = {
|
||||
heightInSurface: SPOTIFY_DEFAULT_HEIGHT_IN_SURFACE,
|
||||
heightInNote: SPOTIFY_DEFAULT_HEIGHT_IN_NOTE,
|
||||
widthPercent: SPOTIFY_DEFAULT_WIDTH_PERCENT,
|
||||
allow:
|
||||
'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture',
|
||||
allow: 'autoplay; clipboard-write; encrypted-media; picture-in-picture',
|
||||
style: 'border-radius: 8px;',
|
||||
allowFullscreen: true,
|
||||
},
|
||||
|
||||
@@ -17,9 +17,13 @@ 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 { EmbedIframeService } from './extension/embed-iframe-service.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';
|
||||
|
||||
export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error';
|
||||
const DEFAULT_IFRAME_HEIGHT = 152;
|
||||
@@ -126,8 +130,9 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
}
|
||||
|
||||
// update model
|
||||
const iframeUrl = this._getIframeUrl(embedData) ?? currentIframeUrl;
|
||||
this.doc.updateBlock(this.model, {
|
||||
iframeUrl: embedData?.iframe_url,
|
||||
iframeUrl,
|
||||
title: embedData?.title || previewData?.title,
|
||||
description: embedData?.description || previewData?.description,
|
||||
});
|
||||
@@ -145,6 +150,17 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param embedData - The embed data
|
||||
* @returns The iframe url
|
||||
*/
|
||||
private readonly _getIframeUrl = (embedData: EmbedIframeData | null) => {
|
||||
const { iframe_url, html } = embedData ?? {};
|
||||
return iframe_url ?? (html && safeGetIframeSrc(html));
|
||||
};
|
||||
|
||||
private readonly _updateIframeOptions = (url: string) => {
|
||||
const config = this.embedIframeService?.getConfig(url);
|
||||
if (config) {
|
||||
|
||||
@@ -29,3 +29,33 @@ export function validateEmbedIframeUrl(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely extracts the src URL from an iframe HTML string
|
||||
* @param htmlString The iframe HTML string to parse
|
||||
* @param options Optional validation configuration
|
||||
* @returns The validated src URL or undefined if validation fails
|
||||
*/
|
||||
export function safeGetIframeSrc(htmlString: string): string | undefined {
|
||||
try {
|
||||
// Create a DOMParser instance
|
||||
const parser = new DOMParser();
|
||||
// Parse the HTML string
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
// Get the iframe element
|
||||
const iframe = doc.querySelector('iframe');
|
||||
if (!iframe) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Get the src attribute
|
||||
const src = iframe.getAttribute('src');
|
||||
if (!src) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return src;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user