From d89a2fdb150880bf98292bff3e16083179709ffb Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 27 Jan 2025 14:28:27 +0000 Subject: [PATCH] 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. --- .../lexical/rich-text/LexicalMediaNode.ts | 49 ++++++++++++++++++- resources/js/wysiwyg/todo.md | 1 - .../js/wysiwyg/ui/defaults/buttons/objects.ts | 17 +------ .../js/wysiwyg/ui/defaults/forms/objects.ts | 34 +++++++++++-- 4 files changed, 79 insertions(+), 22 deletions(-) diff --git a/resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts b/resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts index a675665ac..81fb96a93 100644 --- a/resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts +++ b/resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts @@ -16,6 +16,7 @@ import { } from "lexical/nodes/common"; import {$selectSingleNode} from "../../utils/selection"; import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode"; +import * as url from "node:url"; export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio'; export type MediaNodeSource = { @@ -343,11 +344,55 @@ export function $createMediaNodeFromHtml(html: string): MediaNode | null { 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 audioExtensions = ['3gp', 'aac', 'flac', 'mp3', 'm4a', 'ogg', 'wav', 'webm']; const iframeExtensions = ['html', 'htm', 'php', 'asp', 'aspx', '']; 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'; const srcEnd = src.split('?')[0].split('/').pop() || ''; const srcEndSplit = srcEnd.split('.'); @@ -360,7 +405,9 @@ export function $createMediaNodeFromSrc(src: string): MediaNode { 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 { diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index 1d42ba3e4..94ae0e144 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -10,7 +10,6 @@ ## 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 ## Bugs diff --git a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts index 6612c0dc4..63df4fea8 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts @@ -32,7 +32,7 @@ import { } from "../../../utils/selection"; import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams"; 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"; export const link: EditorButtonDefinition = { @@ -168,24 +168,11 @@ export const media: EditorButtonDefinition = { label: 'Insert/edit Media', icon: mediaIcon, action(context: EditorUiContext) { - const mediaModal = context.manager.createModal('media'); - context.editor.getEditorState().read(() => { const selection = $getSelection(); const selectedNode = $getNodeFromSelection(selection, $isMediaNode) as MediaNode | null; - let formDefaults = {}; - if (selectedNode) { - const nodeAttrs = selectedNode.getAttributes(); - formDefaults = { - src: nodeAttrs.src || nodeAttrs.data || '', - width: nodeAttrs.width, - height: nodeAttrs.height, - embed: '', - } - } - - mediaModal.show(formDefaults); + $showMediaForm(selectedNode, context); }); }, isActive(selection: BaseSelection | null): boolean { diff --git a/resources/js/wysiwyg/ui/defaults/forms/objects.ts b/resources/js/wysiwyg/ui/defaults/forms/objects.ts index 21d333c3a..0effdc171 100644 --- a/resources/js/wysiwyg/ui/defaults/forms/objects.ts +++ b/resources/js/wysiwyg/ui/defaults/forms/objects.ts @@ -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 = { submitText: 'Save', async action(formData, context: EditorUiContext) { @@ -215,12 +232,19 @@ export const media: EditorFormDefinition = { const height = (formData.get('height') || '').toString().trim(); const width = (formData.get('width') || '').toString().trim(); - const updateNode = selectedNode || $createMediaNodeFromSrc(src); - updateNode.setSrc(src); - updateNode.setWidthAndHeight(width, height); - if (!selectedNode) { - $insertNodes([updateNode]); + // Update existing + if (selectedNode) { + selectedNode.setSrc(src); + selectedNode.setWidthAndHeight(width, height); + return; } + + // Insert new + const node = $createMediaNodeFromSrc(src); + if (width || height) { + node.setWidthAndHeight(width, height); + } + $insertNodes([node]); }); return true;