158 lines
6.1 KiB
TypeScript
158 lines
6.1 KiB
TypeScript
import { Node, mergeAttributes } from '@tiptap/core';
|
|
import { render } from 'solid-js/web';
|
|
import { JobFileSelector } from '@/components/JobFileSelector';
|
|
|
|
export interface FileViewerOptions {
|
|
HTMLAttributes: Record<string, any>;
|
|
}
|
|
|
|
declare module '@tiptap/core' {
|
|
interface Commands<ReturnType> {
|
|
fileViewer: {
|
|
insertFileViewer: (options?: { src?: string }) => ReturnType;
|
|
};
|
|
}
|
|
}
|
|
|
|
export const FileViewer = Node.create<FileViewerOptions>({
|
|
name: 'fileViewer',
|
|
|
|
group: 'block',
|
|
|
|
selectable: true,
|
|
|
|
draggable: true,
|
|
|
|
addOptions() {
|
|
return {
|
|
HTMLAttributes: {
|
|
class: 'file-viewer-node group relative',
|
|
},
|
|
};
|
|
},
|
|
|
|
addAttributes() {
|
|
return {
|
|
jobId: {
|
|
default: null,
|
|
parseHTML: element => element.getAttribute('data-job-id'),
|
|
renderHTML: attributes => {
|
|
if (!attributes.jobId) return {};
|
|
return { 'data-job-id': attributes.jobId };
|
|
},
|
|
},
|
|
collectionId: {
|
|
default: 'Job_Info_Prod',
|
|
parseHTML: element => element.getAttribute('data-collection-id') || 'Job_Info_Prod',
|
|
renderHTML: attributes => {
|
|
return { 'data-collection-id': attributes.collectionId || 'Job_Info_Prod' };
|
|
},
|
|
},
|
|
filename: {
|
|
default: null,
|
|
parseHTML: element => element.getAttribute('data-filename'),
|
|
renderHTML: attributes => {
|
|
if (!attributes.filename) return {};
|
|
return { 'data-filename': attributes.filename };
|
|
},
|
|
},
|
|
};
|
|
},
|
|
|
|
parseHTML() {
|
|
return [
|
|
{
|
|
tag: 'div[data-type="file-viewer-wrapper"]',
|
|
}
|
|
];
|
|
},
|
|
|
|
renderHTML({ HTMLAttributes }) {
|
|
const { jobId, collectionId, filename, ...rest } = HTMLAttributes;
|
|
let src = '';
|
|
if (jobId && filename) {
|
|
src = `https://pocketbase.ccllc.pro/api/files/${collectionId}/${jobId}/${filename}`;
|
|
}
|
|
return ['div', mergeAttributes(this.options.HTMLAttributes, rest, { 'data-type': 'file-viewer-wrapper', class: 'file-viewer-wrapper relative w-full my-4 rounded-xl overflow-hidden border border-border flex min-h-[500px] bg-muted/20' }),
|
|
['iframe', { 'data-type': 'file-viewer', src: src, class: 'w-full h-full border-0 absolute inset-0' }]
|
|
];
|
|
},
|
|
|
|
addNodeView() {
|
|
return ({ node, getPos, editor }) => {
|
|
const wrapper = document.createElement('div');
|
|
wrapper.classList.add('file-viewer-wrapper', 'relative', 'w-full', 'my-4', 'rounded-xl', 'overflow-hidden', 'border', 'border-border', 'flex', 'bg-muted/10', 'group');
|
|
|
|
const hasFile = node.attrs.jobId && node.attrs.filename;
|
|
|
|
if (hasFile) {
|
|
// Viewer mode
|
|
wrapper.style.minHeight = '500px';
|
|
|
|
const iframe = document.createElement('iframe');
|
|
iframe.src = `https://pocketbase.ccllc.pro/api/files/${node.attrs.collectionId}/${node.attrs.jobId}/${node.attrs.filename}`;
|
|
iframe.classList.add('w-full', 'h-full', 'border-0', 'absolute', 'inset-0');
|
|
|
|
// Delete button overlay
|
|
const overlay = document.createElement('div');
|
|
overlay.classList.add('absolute', 'top-2', 'left-2', 'flex', '[@media(hover:hover)]:hidden', '[@media(hover:hover)]: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();
|
|
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(iframe);
|
|
wrapper.appendChild(overlay);
|
|
|
|
return {
|
|
dom: wrapper,
|
|
stopEvent: () => true,
|
|
ignoreMutation: () => true,
|
|
};
|
|
} else {
|
|
render(() => JobFileSelector({
|
|
onSelectFile: (jobId: string, filename: string) => {
|
|
if (typeof getPos === 'function') {
|
|
editor.commands.command(({ tr }) => {
|
|
const pos = getPos();
|
|
if (typeof pos === 'number') {
|
|
tr.setNodeMarkup(pos, undefined, { jobId, collectionId: 'Job_Info_Prod', filename });
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
}
|
|
}), wrapper);
|
|
|
|
return {
|
|
dom: wrapper,
|
|
stopEvent: () => true,
|
|
ignoreMutation: () => true,
|
|
};
|
|
}
|
|
};
|
|
},
|
|
|
|
addCommands() {
|
|
return {
|
|
insertFileViewer: (options?: { src?: string }) => ({ commands }) => {
|
|
return commands.insertContent({
|
|
type: this.name,
|
|
attrs: options || {},
|
|
});
|
|
},
|
|
};
|
|
},
|
|
});
|