import { Node as PMNode } from 'prosemirror-model'; import { EditorView, NodeView } from 'prosemirror-view'; import { attachResizeHandles, isResizeEvent } from './image-view'; import { EMBED_SANDBOX, EMBED_REFERRER_POLICY } from './embeds'; /** Below this a 16:9 player is unusable and its own controls stop fitting. */ const MIN_EMBED_WIDTH = 160; /** * Video embed (YouTube/Vimeo/Loom) with the same corner drag-resize handles * as images. Only the width is dragged; the 16:9 aspect ratio comes from CSS, * so the height follows automatically. */ export class EmbedView implements NodeView { dom: HTMLDivElement; private iframe: HTMLIFrameElement; constructor( private node: PMNode, view: EditorView, getPos: () => number | undefined ) { this.dom = document.createElement('div'); this.dom.className = 'nile-wysiwyg__embed'; this.iframe = document.createElement('iframe'); this.iframe.setAttribute('frameborder', '0'); this.iframe.setAttribute('allowfullscreen', 'true'); this.iframe.setAttribute( 'allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' ); this.iframe.setAttribute('sandbox', EMBED_SANDBOX); this.iframe.setAttribute('referrerpolicy', EMBED_REFERRER_POLICY); this.dom.appendChild(this.iframe); this.applyAttrs(node); attachResizeHandles( this.dom, this.dom, width => { const pos = getPos(); if (pos === undefined || !view.editable) return; const current = view.state.doc.nodeAt(pos); if (!current) return; view.dispatch( view.state.tr.setNodeMarkup(pos, undefined, { ...current.attrs, width }) ); }, MIN_EMBED_WIDTH ); } private applyAttrs(node: PMNode): void { const { src, provider, title, width, align } = node.attrs; if (this.iframe.getAttribute('src') !== src) this.iframe.setAttribute('src', src); this.dom.setAttribute('data-embed-provider', provider ?? 'generic'); if (title) this.iframe.title = title; else this.iframe.removeAttribute('title'); if (width) { this.dom.setAttribute('data-width', String(width)); this.dom.style.width = `${width}px`; } else { this.dom.removeAttribute('data-width'); this.dom.style.width = ''; } // Block alignment via auto margins (mirrors the image figure). if (align) this.dom.setAttribute('data-align', align); else this.dom.removeAttribute('data-align'); this.dom.style.marginLeft = align === 'center' || align === 'right' ? 'auto' : ''; this.dom.style.marginRight = align === 'center' ? 'auto' : ''; } update(node: PMNode): boolean { if (node.type !== this.node.type) return false; this.node = node; this.applyAttrs(node); return true; } stopEvent(event: Event): boolean { return isResizeEvent(event); } ignoreMutation(): boolean { return true; } }