3 DOMConversionMap, DOMConversionOutput,
7 SerializedElementNode, Spread
9 import type {EditorConfig} from "lexical/LexicalEditor";
10 import {el} from "../helpers";
12 export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
13 export type MediaNodeSource = {
18 export type SerializedMediaNode = Spread<{
20 attributes: Record<string, string>;
21 sources: MediaNodeSource[];
22 }, SerializedElementNode>
24 const attributeAllowList = [
25 'id', 'width', 'height', 'style', 'title', 'name',
26 'src', 'allow', 'allowfullscreen', 'loading', 'sandbox',
27 'type', 'data', 'controls', 'autoplay', 'controlslist', 'loop',
28 'muted', 'playsinline', 'poster', 'preload'
31 function filterAttributes(attributes: Record<string, string>): Record<string, string> {
32 const filtered: Record<string, string> = {};
33 for (const key in Object.keys(attributes)) {
34 if (attributeAllowList.includes(key)) {
35 filtered[key] = attributes[key];
41 function domElementToNode(tag: MediaNodeTag, element: Element): MediaNode {
42 const node = $createMediaNode(tag);
44 const attributes: Record<string, string> = {};
45 for (const attribute of element.attributes) {
46 attributes[attribute.name] = attribute.value;
48 node.setAttributes(attributes);
50 const sources: MediaNodeSource[] = [];
51 if (tag === 'video' || tag === 'audio') {
52 for (const child of element.children) {
53 if (child.tagName === 'SOURCE') {
54 const src = child.getAttribute('src');
55 const type = child.getAttribute('type');
57 sources.push({ src, type });
61 node.setSources(sources);
67 export class MediaNode extends ElementNode {
70 __attributes: Record<string, string> = {};
71 __sources: MediaNodeSource[] = [];
77 static clone(node: MediaNode) {
78 return new MediaNode(node.__tag, node.__key);
81 constructor(tag: MediaNodeTag, key?: string) {
86 setTag(tag: MediaNodeTag) {
87 const self = this.getWritable();
91 getTag(): MediaNodeTag {
92 const self = this.getLatest();
96 setAttributes(attributes: Record<string, string>) {
97 const self = this.getWritable();
98 self.__attributes = filterAttributes(attributes);
101 getAttributes(): Record<string, string> {
102 const self = this.getLatest();
103 return self.__attributes;
106 setSources(sources: MediaNodeSource[]) {
107 const self = this.getWritable();
108 self.__sources = sources;
111 getSources(): MediaNodeSource[] {
112 const self = this.getLatest();
113 return self.__sources;
116 setSrc(src: string): void {
117 const attrs = Object.assign({}, this.getAttributes());
118 if (this.__tag ==='object') {
123 this.setAttributes(attrs);
126 setWidthAndHeight(width: string, height: string): void {
127 const attrs = Object.assign(
129 this.getAttributes(),
132 this.setAttributes(attrs);
135 createDOM(_config: EditorConfig, _editor: LexicalEditor) {
136 const sources = (this.__tag === 'video' || this.__tag === 'audio') ? this.__sources : [];
137 const sourceEls = sources.map(source => el('source', source));
139 return el(this.__tag, this.__attributes, sourceEls);
142 updateDOM(prevNode: unknown, dom: HTMLElement) {
146 static importDOM(): DOMConversionMap|null {
148 const buildConverter = (tag: MediaNodeTag) => {
149 return (node: HTMLElement): DOMConversion|null => {
151 conversion: (element: HTMLElement): DOMConversionOutput|null => {
153 node: domElementToNode(tag, element),
162 iframe: buildConverter('iframe'),
163 embed: buildConverter('embed'),
164 object: buildConverter('object'),
165 video: buildConverter('video'),
166 audio: buildConverter('audio'),
170 exportJSON(): SerializedMediaNode {
172 ...super.exportJSON(),
176 attributes: this.__attributes,
177 sources: this.__sources,
181 static importJSON(serializedNode: SerializedMediaNode): MediaNode {
182 return $createMediaNode(serializedNode.tag);
187 export function $createMediaNode(tag: MediaNodeTag) {
188 return new MediaNode(tag);
191 export function $createMediaNodeFromHtml(html: string): MediaNode | null {
192 const parser = new DOMParser();
193 const doc = parser.parseFromString(`<body>${html}</body>`, 'text/html');
195 const el = doc.body.children[0];
200 const tag = el.tagName.toLowerCase();
201 const validTypes = ['embed', 'iframe', 'video', 'audio', 'object'];
202 if (!validTypes.includes(tag)) {
206 return domElementToNode(tag as MediaNodeTag, el);
209 export function $isMediaNode(node: LexicalNode | null | undefined) {
210 return node instanceof MediaNode;
213 export function $isMediaNodeOfTag(node: LexicalNode | null | undefined, tag: MediaNodeTag) {
214 return node instanceof MediaNode && (node as MediaNode).getTag() === tag;