Lexical: Added media src conversions

Only actuall added YT in the end.
Google had changed URL scheme, and Vimeo seems to just be something else
now, can't really browse video pages like before.
This commit is contained in:
Dan Brown 2025-01-27 14:28:27 +00:00
parent 958b537a49
commit d89a2fdb15
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
4 changed files with 79 additions and 22 deletions

View File

@ -16,6 +16,7 @@ import {
} from "lexical/nodes/common"; } from "lexical/nodes/common";
import {$selectSingleNode} from "../../utils/selection"; import {$selectSingleNode} from "../../utils/selection";
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode"; import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
import * as url from "node:url";
export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio'; export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
export type MediaNodeSource = { export type MediaNodeSource = {
@ -343,11 +344,55 @@ export function $createMediaNodeFromHtml(html: string): MediaNode | null {
return domElementToNode(tag as MediaNodeTag, el); return domElementToNode(tag as MediaNodeTag, el);
} }
interface UrlPattern {
readonly regex: RegExp;
readonly w: number;
readonly h: number;
readonly url: string;
}
/**
* These patterns originate from the tinymce/tinymce project.
* https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts
* License: MIT Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
* License Link: https://github.com/tinymce/tinymce/blob/584a150679669859a528828e5d2910a083b1d911/LICENSE.TXT
*/
const urlPatterns: UrlPattern[] = [
{
regex: /.*?youtu\.be\/([\w\-_\?&=.]+)/i,
w: 560, h: 314,
url: 'https://www.youtube.com/embed/$1',
},
{
regex: /.*youtube\.com(.+)v=([^&]+)(&([a-z0-9&=\-_]+))?.*/i,
w: 560, h: 314,
url: 'https://www.youtube.com/embed/$2?$4',
},
{
regex: /.*youtube.com\/embed\/([a-z0-9\?&=\-_]+).*/i,
w: 560, h: 314,
url: 'https://www.youtube.com/embed/$1',
},
];
const videoExtensions = ['mp4', 'mpeg', 'm4v', 'm4p', 'mov']; const videoExtensions = ['mp4', 'mpeg', 'm4v', 'm4p', 'mov'];
const audioExtensions = ['3gp', 'aac', 'flac', 'mp3', 'm4a', 'ogg', 'wav', 'webm']; const audioExtensions = ['3gp', 'aac', 'flac', 'mp3', 'm4a', 'ogg', 'wav', 'webm'];
const iframeExtensions = ['html', 'htm', 'php', 'asp', 'aspx', '']; const iframeExtensions = ['html', 'htm', 'php', 'asp', 'aspx', ''];
export function $createMediaNodeFromSrc(src: string): MediaNode { export function $createMediaNodeFromSrc(src: string): MediaNode {
for (const pattern of urlPatterns) {
const match = src.match(pattern.regex);
if (match) {
const newSrc = src.replace(pattern.regex, pattern.url);
const node = new MediaNode('iframe');
node.setSrc(newSrc);
node.setHeight(pattern.h);
node.setWidth(pattern.w);
return node;
}
}
let nodeTag: MediaNodeTag = 'iframe'; let nodeTag: MediaNodeTag = 'iframe';
const srcEnd = src.split('?')[0].split('/').pop() || ''; const srcEnd = src.split('?')[0].split('/').pop() || '';
const srcEndSplit = srcEnd.split('.'); const srcEndSplit = srcEnd.split('.');
@ -360,7 +405,9 @@ export function $createMediaNodeFromSrc(src: string): MediaNode {
nodeTag = 'embed'; nodeTag = 'embed';
} }
return new MediaNode(nodeTag); const node = new MediaNode(nodeTag);
node.setSrc(src);
return node;
} }
export function $isMediaNode(node: LexicalNode | null | undefined): node is MediaNode { export function $isMediaNode(node: LexicalNode | null | undefined): node is MediaNode {

View File

@ -10,7 +10,6 @@
## Secondary Todo ## Secondary Todo
- Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts)
- Deep check of translation coverage - Deep check of translation coverage
## Bugs ## Bugs

View File

@ -32,7 +32,7 @@ import {
} from "../../../utils/selection"; } from "../../../utils/selection";
import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams"; import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams";
import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images"; import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images";
import {$showDetailsForm, $showImageForm, $showLinkForm} from "../forms/objects"; import {$showDetailsForm, $showImageForm, $showLinkForm, $showMediaForm} from "../forms/objects";
import {formatCodeBlock} from "../../../utils/formats"; import {formatCodeBlock} from "../../../utils/formats";
export const link: EditorButtonDefinition = { export const link: EditorButtonDefinition = {
@ -168,24 +168,11 @@ export const media: EditorButtonDefinition = {
label: 'Insert/edit Media', label: 'Insert/edit Media',
icon: mediaIcon, icon: mediaIcon,
action(context: EditorUiContext) { action(context: EditorUiContext) {
const mediaModal = context.manager.createModal('media');
context.editor.getEditorState().read(() => { context.editor.getEditorState().read(() => {
const selection = $getSelection(); const selection = $getSelection();
const selectedNode = $getNodeFromSelection(selection, $isMediaNode) as MediaNode | null; const selectedNode = $getNodeFromSelection(selection, $isMediaNode) as MediaNode | null;
let formDefaults = {}; $showMediaForm(selectedNode, context);
if (selectedNode) {
const nodeAttrs = selectedNode.getAttributes();
formDefaults = {
src: nodeAttrs.src || nodeAttrs.data || '',
width: nodeAttrs.width,
height: nodeAttrs.height,
embed: '',
}
}
mediaModal.show(formDefaults);
}); });
}, },
isActive(selection: BaseSelection | null): boolean { isActive(selection: BaseSelection | null): boolean {

View File

@ -186,6 +186,23 @@ export const link: EditorFormDefinition = {
], ],
}; };
export function $showMediaForm(media: MediaNode|null, context: EditorUiContext): void {
const mediaModal = context.manager.createModal('media');
let formDefaults = {};
if (media) {
const nodeAttrs = media.getAttributes();
formDefaults = {
src: nodeAttrs.src || nodeAttrs.data || '',
width: nodeAttrs.width,
height: nodeAttrs.height,
embed: '',
}
}
mediaModal.show(formDefaults);
}
export const media: EditorFormDefinition = { export const media: EditorFormDefinition = {
submitText: 'Save', submitText: 'Save',
async action(formData, context: EditorUiContext) { async action(formData, context: EditorUiContext) {
@ -215,12 +232,19 @@ export const media: EditorFormDefinition = {
const height = (formData.get('height') || '').toString().trim(); const height = (formData.get('height') || '').toString().trim();
const width = (formData.get('width') || '').toString().trim(); const width = (formData.get('width') || '').toString().trim();
const updateNode = selectedNode || $createMediaNodeFromSrc(src); // Update existing
updateNode.setSrc(src); if (selectedNode) {
updateNode.setWidthAndHeight(width, height); selectedNode.setSrc(src);
if (!selectedNode) { selectedNode.setWidthAndHeight(width, height);
$insertNodes([updateNode]); return;
} }
// Insert new
const node = $createMediaNodeFromSrc(src);
if (width || height) {
node.setWidthAndHeight(width, height);
}
$insertNodes([node]);
}); });
return true; return true;