From c8f6b7e0d655562aa143ad7c3e82c560b376e74b Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Sat, 27 Jul 2024 17:25:30 +0100
Subject: [PATCH] Lexical: Got media node core work & form done

---
 resources/js/wysiwyg/nodes/media.ts           | 23 +++++++++-
 resources/js/wysiwyg/todo.md                  |  3 +-
 .../wysiwyg/ui/defaults/form-definitions.ts   | 43 +++++++++++++------
 resources/js/wysiwyg/ui/framework/forms.ts    |  2 +-
 4 files changed, 54 insertions(+), 17 deletions(-)

diff --git a/resources/js/wysiwyg/nodes/media.ts b/resources/js/wysiwyg/nodes/media.ts
index e0c1b3141..751f420fa 100644
--- a/resources/js/wysiwyg/nodes/media.ts
+++ b/resources/js/wysiwyg/nodes/media.ts
@@ -30,7 +30,7 @@ const attributeAllowList = [
 
 function filterAttributes(attributes: Record<string, string>): Record<string, string> {
     const filtered: Record<string, string> = {};
-    for (const key in Object.keys(attributes)) {
+    for (const key of Object.keys(attributes)) {
         if (attributeAllowList.includes(key)) {
             filtered[key] = attributes[key];
         }
@@ -170,7 +170,7 @@ export class MediaNode extends ElementNode {
     exportJSON(): SerializedMediaNode {
         return {
             ...super.exportJSON(),
-            type: 'callout',
+            type: 'media',
             version: 1,
             tag: this.__tag,
             attributes: this.__attributes,
@@ -206,6 +206,25 @@ export function $createMediaNodeFromHtml(html: string): MediaNode | null {
     return domElementToNode(tag as MediaNodeTag, el);
 }
 
+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 {
+    let nodeTag: MediaNodeTag = 'iframe';
+    const srcEnd = src.split('?')[0].split('/').pop() || '';
+    const extension = (srcEnd.split('.').pop() || '').toLowerCase();
+    if (videoExtensions.includes(extension)) {
+        nodeTag = 'video';
+    } else if (audioExtensions.includes(extension)) {
+        nodeTag = 'audio';
+    } else if (extension && !iframeExtensions.includes(extension)) {
+        nodeTag = 'embed';
+    }
+
+    return new MediaNode(nodeTag);
+}
+
 export function $isMediaNode(node: LexicalNode | null | undefined) {
     return node instanceof MediaNode;
 }
diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md
index cd36f359e..2125aa258 100644
--- a/resources/js/wysiwyg/todo.md
+++ b/resources/js/wysiwyg/todo.md
@@ -2,7 +2,8 @@
 
 ## In progress
 
-- Finish initial media node & form integration
+- Update forms to allow panels (Media)
+  - Will be used for table forms also. 
 
 ## Main Todo
 
diff --git a/resources/js/wysiwyg/ui/defaults/form-definitions.ts b/resources/js/wysiwyg/ui/defaults/form-definitions.ts
index e0459b5c5..a2242c338 100644
--- a/resources/js/wysiwyg/ui/defaults/form-definitions.ts
+++ b/resources/js/wysiwyg/ui/defaults/form-definitions.ts
@@ -1,15 +1,17 @@
 import {EditorFormDefinition, EditorSelectFormFieldDefinition} from "../framework/forms";
 import {EditorUiContext} from "../framework/core";
 import {$createLinkNode} from "@lexical/link";
-import {$createTextNode, $getSelection} from "lexical";
+import {$createTextNode, $getSelection, LexicalNode} from "lexical";
 import {$createImageNode} from "../../nodes/image";
 import {setEditorContentFromHtml} from "../../actions";
-import {$createMediaNodeFromHtml} from "../../nodes/media";
+import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "../../nodes/media";
+import {$getNodeFromSelection} from "../../helpers";
+import {$insertNodeToNearestRoot} from "@lexical/utils";
 
 
 export const link: EditorFormDefinition = {
     submitText: 'Apply',
-    action(formData, context: EditorUiContext) {
+    async action(formData, context: EditorUiContext) {
         context.editor.update(() => {
 
             const selection = $getSelection();
@@ -54,7 +56,7 @@ export const link: EditorFormDefinition = {
 
 export const image: EditorFormDefinition = {
     submitText: 'Apply',
-    action(formData, context: EditorUiContext) {
+    async action(formData, context: EditorUiContext) {
         context.editor.update(() => {
             const selection = $getSelection();
             const imageNode = $createImageNode(formData.get('src')?.toString() || '', {
@@ -92,25 +94,40 @@ export const image: EditorFormDefinition = {
 
 export const media: EditorFormDefinition = {
     submitText: 'Save',
-    action(formData, context: EditorUiContext) {
-
-        // TODO - Get media from selection
+    async action(formData, context: EditorUiContext) {
+        const selectedNode: MediaNode|null = await (new Promise((res, rej) => {
+            context.editor.getEditorState().read(() => {
+                const node = $getNodeFromSelection($getSelection(), $isMediaNode);
+                res(node as MediaNode|null);
+            });
+        }));
 
         const embedCode = (formData.get('embed') || '').toString().trim();
         if (embedCode) {
             context.editor.update(() => {
                 const node = $createMediaNodeFromHtml(embedCode);
-                // TODO - Replace existing or insert new
+                if (selectedNode && node) {
+                    selectedNode.replace(node)
+                } else if (node) {
+                    $insertNodeToNearestRoot(node);
+                }
             });
 
             return true;
         }
 
-        const src = (formData.get('src') || '').toString().trim();
-        const height = (formData.get('height') || '').toString().trim();
-        const width = (formData.get('width') || '').toString().trim();
+        context.editor.update(() => {
+            const src = (formData.get('src') || '').toString().trim();
+            const height = (formData.get('height') || '').toString().trim();
+            const width = (formData.get('width') || '').toString().trim();
 
-        // TODO - Update existing or insert new
+            const updateNode = selectedNode || $createMediaNodeFromSrc(src);
+            updateNode.setSrc(src);
+            updateNode.setWidthAndHeight(width, height);
+            if (!selectedNode) {
+                $insertNodeToNearestRoot(updateNode);
+            }
+        });
 
         return true;
     },
@@ -141,7 +158,7 @@ export const media: EditorFormDefinition = {
 
 export const source: EditorFormDefinition = {
     submitText: 'Save',
-    action(formData, context: EditorUiContext) {
+    async action(formData, context: EditorUiContext) {
         setEditorContentFromHtml(context.editor, formData.get('source')?.toString() || '');
         return true;
     },
diff --git a/resources/js/wysiwyg/ui/framework/forms.ts b/resources/js/wysiwyg/ui/framework/forms.ts
index 4fee787d3..b641f993b 100644
--- a/resources/js/wysiwyg/ui/framework/forms.ts
+++ b/resources/js/wysiwyg/ui/framework/forms.ts
@@ -14,7 +14,7 @@ export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefiniti
 
 export interface EditorFormDefinition {
     submitText: string;
-    action: (formData: FormData, context: EditorUiContext) => boolean;
+    action: (formData: FormData, context: EditorUiContext) => Promise<boolean>;
     fields: EditorFormFieldDefinition[];
 }