feat(editor): add embed iframe provider miro (#10996)

This commit is contained in:
donteatfriedrice
2025-03-19 13:57:29 +00:00
parent ee65d66d67
commit f6e32d8894
5 changed files with 95 additions and 4 deletions
@@ -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);
@@ -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;
}
}