Lexical: Media form improvements
Some checks failed
test-js / build (push) Has been cancelled

- Allowed re-editing of existing embed HTML code.
- Handled "src" form field when video is using child source tags.
This commit is contained in:
Dan Brown
2025-06-15 20:00:28 +01:00
parent 1611b0399f
commit b913ae703d
4 changed files with 46 additions and 6 deletions

View File

@ -14,7 +14,6 @@ import {
setCommonBlockPropsFromElement, setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps updateElementWithCommonBlockProps
} from "lexical/nodes/common"; } from "lexical/nodes/common";
import {$selectSingleNode} from "../../utils/selection";
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode"; import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio'; export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
@ -141,16 +140,26 @@ export class MediaNode extends ElementNode {
getSources(): MediaNodeSource[] { getSources(): MediaNodeSource[] {
const self = this.getLatest(); const self = this.getLatest();
return self.__sources; return self.__sources.map(s => Object.assign({}, s))
} }
setSrc(src: string): void { setSrc(src: string): void {
const attrs = this.getAttributes(); const attrs = this.getAttributes();
const sources = this.getSources();
if (this.__tag ==='object') { if (this.__tag ==='object') {
attrs.data = src; 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 { } else {
attrs.src = src; attrs.src = src;
} }
this.setAttributes(attrs); this.setAttributes(attrs);
} }

View 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');
});
});
}); });

View File

@ -192,11 +192,17 @@ export function $showMediaForm(media: MediaNode|null, context: EditorUiContext):
let formDefaults = {}; let formDefaults = {};
if (media) { if (media) {
const nodeAttrs = media.getAttributes(); const nodeAttrs = media.getAttributes();
const nodeDOM = media.exportDOM(context.editor).element;
const nodeHtml = (nodeDOM instanceof HTMLElement) ? nodeDOM.outerHTML : '';
formDefaults = { formDefaults = {
src: nodeAttrs.src || nodeAttrs.data || '', src: nodeAttrs.src || nodeAttrs.data || media.getSources()[0]?.src || '',
width: nodeAttrs.width, width: nodeAttrs.width,
height: nodeAttrs.height, 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(); const embedCode = (formData.get('embed') || '').toString().trim();
if (embedCode) { const embedCheck = (formData.get('embed_check') || '').toString().trim();
if (embedCode && embedCode !== embedCheck) {
context.editor.update(() => { context.editor.update(() => {
const node = $createMediaNodeFromHtml(embedCode); const node = $createMediaNodeFromHtml(embedCode);
if (selectedNode && node) { if (selectedNode && node) {
@ -236,6 +243,7 @@ export const media: EditorFormDefinition = {
if (selectedNode) { if (selectedNode) {
selectedNode.setSrc(src); selectedNode.setSrc(src);
selectedNode.setWidthAndHeight(width, height); selectedNode.setWidthAndHeight(width, height);
context.manager.triggerFutureStateRefresh();
return; return;
} }
@ -281,6 +289,11 @@ export const media: EditorFormDefinition = {
name: 'embed', name: 'embed',
type: 'textarea', type: 'textarea',
}, },
{
label: '',
name: 'embed_check',
type: 'hidden',
},
], ],
} }
]) ])

View File

@ -11,7 +11,7 @@ import {el} from "../../utils/dom";
export interface EditorFormFieldDefinition { export interface EditorFormFieldDefinition {
label: string; label: string;
name: string; name: string;
type: 'text' | 'select' | 'textarea' | 'checkbox'; type: 'text' | 'select' | 'textarea' | 'checkbox' | 'hidden';
} }
export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefinition { 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'}); input = el('textarea', {id, name: this.definition.name, class: 'editor-form-field-input'});
} else if (this.definition.type === 'checkbox') { } else if (this.definition.type === 'checkbox') {
input = el('input', {id, name: this.definition.name, type: 'checkbox', class: 'editor-form-field-input-checkbox', value: 'true'}); 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 { } else {
input = el('input', {id, name: this.definition.name, class: 'editor-form-field-input'}); input = el('input', {id, name: this.definition.name, class: 'editor-form-field-input'});
} }