import { Node, mergeAttributes } from '@tiptap/core'; export interface VideoOptions { HTMLAttributes: Record; } export const Video = Node.create({ name: 'video', group: 'block', // Allows it to be selected and manipulated as a single block selectable: true, draggable: true, addOptions() { return { HTMLAttributes: { class: 'w-full rounded-lg border border-border/50 shadow-sm overflow-hidden my-4', controls: true, }, }; }, addAttributes() { return { src: { default: null, }, filename: { default: null, parseHTML: element => element.getAttribute('data-filename'), renderHTML: attributes => { if (!attributes.filename) return {}; return { 'data-filename': attributes.filename }; }, }, }; }, addCommands() { return { setVideo: (options: { src: string, filename?: string }) => ({ commands }) => { return commands.insertContent({ type: this.name, attrs: options, }); }, }; }, parseHTML() { return [ { tag: 'video', }, ]; }, renderHTML({ HTMLAttributes }) { return ['video', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; }, addKeyboardShortcuts() { return { Backspace: () => { const { selection } = this.editor.state; if (!selection.empty) { const selectedNode = this.editor.state.doc.nodeAt(selection.from); if (selectedNode?.type.name === this.name) { return true; // Prevent default deletion } } return false; }, Delete: () => { const { selection } = this.editor.state; if (!selection.empty) { const selectedNode = this.editor.state.doc.nodeAt(selection.from); if (selectedNode?.type.name === this.name) { return true; // Prevent default deletion } } return false; }, }; }, addNodeView() { return ({ node, getPos, editor }) => { const wrapper = document.createElement('div'); wrapper.classList.add('video-wrapper', 'relative', 'inline-block', 'my-4', 'group', 'w-full', 'max-w-full'); wrapper.style.display = 'block'; const video = document.createElement('video'); Object.entries(node.attrs).forEach(([key, value]) => { if (value !== null && value !== undefined) { if (key === 'filename') { video.setAttribute('data-filename', value); } else { video.setAttribute(key, value); } } }); // Apply default attributes if not present if (!video.hasAttribute('controls')) { video.setAttribute('controls', 'true'); } video.classList.add('w-full', 'rounded-lg', 'border', 'border-border/50', 'shadow-sm', 'overflow-hidden'); // Delete button overlay const overlay = document.createElement('div'); overlay.classList.add('absolute', 'top-2', 'right-2', 'hidden', 'group-hover:flex', 'bg-background/80', 'backdrop-blur-sm', 'p-1', 'rounded-lg', 'border', 'border-border', 'shadow-sm', 'z-10'); const deleteBtn = document.createElement('button'); deleteBtn.innerHTML = ''; deleteBtn.classList.add('p-1', 'cursor-pointer'); deleteBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (typeof getPos === 'function') { const pos = getPos(); if (typeof pos === 'number') { editor.commands.deleteRange({ from: pos, to: pos + node.nodeSize }); } } }; overlay.appendChild(deleteBtn); wrapper.appendChild(video); wrapper.appendChild(overlay); return { dom: wrapper, stopEvent: () => true, // Stops events from propagating to editor (makes the input usable) ignoreMutation: () => true, }; }; }, }); declare module '@tiptap/core' { interface Commands { video: { setVideo: (options: { src: string, filename?: string }) => ReturnType; }; } }