Files
TasGrid/src/lib/extensions/video.ts
T
2026-03-12 17:23:28 -05:00

150 lines
5.2 KiB
TypeScript

import { Node, mergeAttributes } from '@tiptap/core';
export interface VideoOptions {
HTMLAttributes: Record<string, any>;
}
export const Video = Node.create<VideoOptions>({
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 = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground hover:text-destructive transition-colors"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>';
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<ReturnType> {
video: {
setVideo: (options: { src: string, filename?: string }) => ReturnType;
};
}
}