feat(editor): add embed iframe provider miro (#10996)
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import { GoogleDriveEmbedConfig } from './google-drive';
|
import { GoogleDriveEmbedConfig } from './google-drive';
|
||||||
|
import { MiroEmbedConfig } from './miro';
|
||||||
import { SpotifyEmbedConfig } from './spotify';
|
import { SpotifyEmbedConfig } from './spotify';
|
||||||
|
|
||||||
export const EmbedIframeConfigExtensions = [
|
export const EmbedIframeConfigExtensions = [
|
||||||
SpotifyEmbedConfig,
|
SpotifyEmbedConfig,
|
||||||
GoogleDriveEmbedConfig,
|
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,
|
heightInSurface: SPOTIFY_DEFAULT_HEIGHT_IN_SURFACE,
|
||||||
heightInNote: SPOTIFY_DEFAULT_HEIGHT_IN_NOTE,
|
heightInNote: SPOTIFY_DEFAULT_HEIGHT_IN_NOTE,
|
||||||
widthPercent: SPOTIFY_DEFAULT_WIDTH_PERCENT,
|
widthPercent: SPOTIFY_DEFAULT_WIDTH_PERCENT,
|
||||||
allow:
|
allow: 'autoplay; clipboard-write; encrypted-media; picture-in-picture',
|
||||||
'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture',
|
|
||||||
style: 'border-radius: 8px;',
|
style: 'border-radius: 8px;',
|
||||||
allowFullscreen: true,
|
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 { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
|
|
||||||
import type { IframeOptions } from './extension/embed-iframe-config.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 { embedIframeBlockStyles } from './style.js';
|
||||||
import type { EmbedIframeStatusCardOptions } from './types.js';
|
import type { EmbedIframeStatusCardOptions } from './types.js';
|
||||||
|
import { safeGetIframeSrc } from './utils.js';
|
||||||
|
|
||||||
export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error';
|
export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error';
|
||||||
const DEFAULT_IFRAME_HEIGHT = 152;
|
const DEFAULT_IFRAME_HEIGHT = 152;
|
||||||
@@ -126,8 +130,9 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update model
|
// update model
|
||||||
|
const iframeUrl = this._getIframeUrl(embedData) ?? currentIframeUrl;
|
||||||
this.doc.updateBlock(this.model, {
|
this.doc.updateBlock(this.model, {
|
||||||
iframeUrl: embedData?.iframe_url,
|
iframeUrl,
|
||||||
title: embedData?.title || previewData?.title,
|
title: embedData?.title || previewData?.title,
|
||||||
description: embedData?.description || previewData?.description,
|
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) => {
|
private readonly _updateIframeOptions = (url: string) => {
|
||||||
const config = this.embedIframeService?.getConfig(url);
|
const config = this.embedIframeService?.getConfig(url);
|
||||||
if (config) {
|
if (config) {
|
||||||
|
|||||||
@@ -29,3 +29,33 @@ export function validateEmbedIframeUrl(
|
|||||||
return false;
|
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