]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Media form improvements 5653/head
authorDan Brown <redacted>
Sun, 15 Jun 2025 19:00:28 +0000 (20:00 +0100)
committerDan Brown <redacted>
Sun, 15 Jun 2025 19:00:28 +0000 (20:00 +0100)
- Allowed re-editing of existing embed HTML code.
- Handled "src" form field when video is using child source tags.

resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts
resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalMediaNode.test.ts
resources/js/wysiwyg/ui/defaults/forms/objects.ts
resources/js/wysiwyg/ui/framework/forms.ts

index 4f4b1d82af89bc48643d6bb08c7b019e1a2cdb79..a7acc4ad3400522f2b4b7b347d6fcf9688589f87 100644 (file)
@@ -14,7 +14,6 @@ import {
     setCommonBlockPropsFromElement,
     updateElementWithCommonBlockProps
 } from "lexical/nodes/common";
-import {$selectSingleNode} from "../../utils/selection";
 import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
 
 export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
@@ -141,16 +140,26 @@ export class MediaNode extends ElementNode {
 
     getSources(): MediaNodeSource[] {
         const self = this.getLatest();
-        return self.__sources;
+        return self.__sources.map(s => Object.assign({}, s))
     }
 
     setSrc(src: string): void {
         const attrs = this.getAttributes();
+        const sources = this.getSources();
+
         if (this.__tag ==='object') {
             attrs.data = src;
+        } if (this.__tag === 'video' && sources.length > 0) {
+            sources[0].src = src;
+            delete attrs.src;
+            if (sources.length > 1) {
+                sources.splice(1, sources.length - 1);
+            }
+            this.setSources(sources);
         } else {
             attrs.src = src;
         }
+
         this.setAttributes(attrs);
     }
 
index c55ae669edd88f61b8b4307376f66a82a164f1e1..b142e95a08546424e3cf3b753f68b553c0096b36 100644 (file)
@@ -28,4 +28,19 @@ describe('LexicalMediaNode', () => {
         });
     });
 
+    test('setSrc on video uses sources if existing', () => {
+        const {editor} = createTestContext();
+        editor.updateAndCommit(() => {
+            const mediaMode = $createMediaNode('video');
+            mediaMode.setAttributes({src: 'z'});
+            mediaMode.setSources([{src: 'a', type: 'video'}, {src: 'b', type: 'video'}]);
+
+            mediaMode.setSrc('c');
+
+            expect(mediaMode.getAttributes().src).toBeUndefined();
+            expect(mediaMode.getSources()).toHaveLength(1);
+            expect(mediaMode.getSources()[0].src).toBe('c');
+        });
+    });
+
 });
\ No newline at end of file
index 0effdc1715ee7794af684079401ddab25ffde681..cdf464cb4f7910b479160adc9b21e0ad2c166aac 100644 (file)
@@ -192,11 +192,17 @@ export function $showMediaForm(media: MediaNode|null, context: EditorUiContext):
     let formDefaults = {};
     if (media) {
         const nodeAttrs = media.getAttributes();
+        const nodeDOM = media.exportDOM(context.editor).element;
+        const nodeHtml = (nodeDOM instanceof HTMLElement) ? nodeDOM.outerHTML : '';
+
         formDefaults = {
-            src: nodeAttrs.src || nodeAttrs.data || '',
+            src: nodeAttrs.src || nodeAttrs.data || media.getSources()[0]?.src || '',
             width: nodeAttrs.width,
             height: nodeAttrs.height,
-            embed: '',
+            embed: nodeHtml,
+
+            // This is used so we can check for edits against the embed field on submit
+            embed_check: nodeHtml,
         }
     }
 
@@ -214,7 +220,8 @@ export const media: EditorFormDefinition = {
         }));
 
         const embedCode = (formData.get('embed') || '').toString().trim();
-        if (embedCode) {
+        const embedCheck = (formData.get('embed_check') || '').toString().trim();
+        if (embedCode && embedCode !== embedCheck) {
             context.editor.update(() => {
                 const node = $createMediaNodeFromHtml(embedCode);
                 if (selectedNode && node) {
@@ -236,6 +243,7 @@ export const media: EditorFormDefinition = {
             if (selectedNode) {
                 selectedNode.setSrc(src);
                 selectedNode.setWidthAndHeight(width, height);
+                context.manager.triggerFutureStateRefresh();
                 return;
             }
 
@@ -281,6 +289,11 @@ export const media: EditorFormDefinition = {
                                 name: 'embed',
                                 type: 'textarea',
                             },
+                            {
+                                label: '',
+                                name: 'embed_check',
+                                type: 'hidden',
+                            },
                         ],
                     }
                 ])
index 08edb214e2a780f7854ba59f2b41ae02041bf098..b12d9f692fca9e1d4ca15070b160c5a1b21b50b9 100644 (file)
@@ -11,7 +11,7 @@ import {el} from "../../utils/dom";
 export interface EditorFormFieldDefinition {
     label: string;
     name: string;
-    type: 'text' | 'select' | 'textarea' | 'checkbox';
+    type: 'text' | 'select' | 'textarea' | 'checkbox' | 'hidden';
 }
 
 export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefinition {
@@ -67,6 +67,9 @@ export class EditorFormField extends EditorUiElement {
             input = el('textarea', {id, name: this.definition.name, class: 'editor-form-field-input'});
         } else if (this.definition.type === 'checkbox') {
             input = el('input', {id, name: this.definition.name, type: 'checkbox', class: 'editor-form-field-input-checkbox', value: 'true'});
+        } else if (this.definition.type === 'hidden') {
+            input = el('input', {id, name: this.definition.name, type: 'hidden'});
+            return el('div', {hidden: 'true'}, [input]);
         } else {
             input = el('input', {id, name: this.definition.name, class: 'editor-form-field-input'});
         }