import { DeesElement, html, customElement, type TemplateResult, property, state, cssManager, } from '@design.estate/dees-element'; import '../dees-image-viewer/component.js'; import '../dees-audio-viewer/component.js'; import '../dees-video-viewer/component.js'; import '../../00group-dataview/dees-dataview-codebox/dees-dataview-codebox.js'; import '../dees-pdf-viewer/component.js'; import '../../00group-utility/dees-icon/dees-icon.js'; import { demoFunc } from './dees-preview.demo.js'; export type TPreviewContentType = 'image' | 'pdf' | 'audio' | 'video' | 'code' | 'text' | 'unknown'; declare global { interface HTMLElementTagNameMap { 'dees-preview': DeesPreview; } } const EXTENSION_MAP: Record = { // Image jpg: 'image', jpeg: 'image', png: 'image', gif: 'image', webp: 'image', svg: 'image', bmp: 'image', avif: 'image', ico: 'image', // PDF pdf: 'pdf', // Audio mp3: 'audio', wav: 'audio', ogg: 'audio', flac: 'audio', aac: 'audio', m4a: 'audio', opus: 'audio', weba: 'audio', // Video mp4: 'video', webm: 'video', mov: 'video', avi: 'video', mkv: 'video', ogv: 'video', // Code ts: 'code', js: 'code', jsx: 'code', tsx: 'code', json: 'code', html: 'code', css: 'code', scss: 'code', less: 'code', py: 'code', java: 'code', go: 'code', rs: 'code', yaml: 'code', yml: 'code', xml: 'code', sql: 'code', sh: 'code', bash: 'code', zsh: 'code', md: 'code', c: 'code', cpp: 'code', h: 'code', hpp: 'code', rb: 'code', php: 'code', swift: 'code', kt: 'code', // Text txt: 'text', log: 'text', csv: 'text', env: 'text', }; const MIME_PREFIX_MAP: Record = { 'image/': 'image', 'audio/': 'audio', 'video/': 'video', 'application/pdf': 'pdf', }; const EXTENSION_LANG_MAP: Record = { ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript', json: 'json', html: 'xml', xml: 'xml', css: 'css', scss: 'scss', less: 'less', py: 'python', java: 'java', go: 'go', rs: 'rust', yaml: 'yaml', yml: 'yaml', sql: 'sql', sh: 'bash', bash: 'bash', zsh: 'bash', c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', rb: 'ruby', php: 'php', swift: 'swift', kt: 'kotlin', md: 'markdown', }; const TYPE_ICONS: Record = { image: 'lucide:Image', pdf: 'lucide:FileText', audio: 'lucide:Music', video: 'lucide:Video', code: 'lucide:Code', text: 'lucide:FileText', unknown: 'lucide:File', }; @customElement('dees-preview') export class DeesPreview extends DeesElement { public static demo = demoFunc; public static demoGroups = ['Media', 'Data View']; // Content sources (use one) @property() accessor url: string = ''; @property({ attribute: false }) accessor file: File | undefined = undefined; @property() accessor base64: string = ''; @property() accessor textContent: string = ''; // Hints & overrides @property() accessor contentType: TPreviewContentType | undefined = undefined; @property() accessor language: string = ''; @property() accessor mimeType: string = ''; @property() accessor filename: string = ''; // UI @property({ type: Boolean }) accessor showToolbar: boolean = true; @property({ type: Boolean }) accessor showFilename: boolean = true; // Internal @state() accessor resolvedType: TPreviewContentType = 'unknown'; @state() accessor resolvedSrc: string = ''; @state() accessor resolvedText: string = ''; @state() accessor resolvedLang: string = 'text'; @state() accessor loading: boolean = false; @state() accessor error: string = ''; private objectUrl: string = ''; public render(): TemplateResult { const displayName = this.filename || this.file?.name || this.getFilenameFromUrl() || ''; return html`
${this.showFilename && displayName ? html`
${displayName} ${this.resolvedType}
` : ''}
${this.error ? html`
${this.error}
` : this.loading ? html`
` : this.renderContent()}
`; } private renderContent(): TemplateResult { switch (this.resolvedType) { case 'image': return html` `; case 'pdf': return html` `; case 'audio': return html` `; case 'video': return html` `; case 'code': return html` `; case 'text': return html`
${this.resolvedText}
`; default: return html`
Preview not available
`; } } public async updated(changedProperties: Map): Promise { super.updated(changedProperties); const relevant = ['url', 'file', 'base64', 'textContent', 'contentType', 'language', 'mimeType', 'filename']; const needsResolve = relevant.some((key) => changedProperties.has(key)); if (needsResolve) { await this.resolveContent(); } } public async disconnectedCallback(): Promise { await super.disconnectedCallback(); this.revokeObjectUrl(); } private async resolveContent(): Promise { this.error = ''; this.revokeObjectUrl(); // Detect type this.resolvedType = this.detectType(); // Resolve source try { if (this.url) { this.resolvedSrc = this.url; if (this.resolvedType === 'code' || this.resolvedType === 'text') { if (!this.textContent) { this.loading = true; const response = await fetch(this.url); this.resolvedText = await response.text(); this.loading = false; } else { this.resolvedText = this.textContent; } } } else if (this.file) { this.objectUrl = URL.createObjectURL(this.file); this.resolvedSrc = this.objectUrl; if (this.resolvedType === 'code' || this.resolvedType === 'text') { this.loading = true; this.resolvedText = await this.file.text(); this.loading = false; } } else if (this.base64) { const mime = this.mimeType || 'application/octet-stream'; this.resolvedSrc = `data:${mime};base64,${this.base64}`; } else if (this.textContent) { this.resolvedText = this.textContent; } } catch { this.error = 'Failed to load content'; this.loading = false; } // Resolve language for code this.resolvedLang = this.resolveLanguage(); } private detectType(): TPreviewContentType { // 1. Explicit override if (this.contentType) return this.contentType; // 2. MIME type const mime = this.mimeType || this.file?.type || ''; if (mime) { if (mime === 'application/pdf') return 'pdf'; for (const [prefix, type] of Object.entries(MIME_PREFIX_MAP)) { if (mime.startsWith(prefix)) return type; } if (mime.startsWith('text/')) return 'text'; } // 3. File extension const ext = this.getExtension(); if (ext && EXTENSION_MAP[ext]) return EXTENSION_MAP[ext]; // 4. If textContent is provided, assume code or text if (this.textContent) { return this.language ? 'code' : 'text'; } return 'unknown'; } private getExtension(): string { const name = this.filename || this.file?.name || ''; if (name) { const parts = name.split('.'); if (parts.length > 1) return parts.pop()!.toLowerCase(); } if (this.url) { try { const pathname = new URL(this.url, 'https://placeholder.com').pathname; const parts = pathname.split('.'); if (parts.length > 1) return parts.pop()!.toLowerCase(); } catch { // Invalid URL } } return ''; } private getFilenameFromUrl(): string { if (!this.url) return ''; try { const pathname = new URL(this.url, 'https://placeholder.com').pathname; return pathname.split('/').pop() || ''; } catch { return ''; } } private resolveLanguage(): string { if (this.language) return this.language; const ext = this.getExtension(); return EXTENSION_LANG_MAP[ext] || 'text'; } private revokeObjectUrl(): void { if (this.objectUrl) { URL.revokeObjectURL(this.objectUrl); this.objectUrl = ''; } } }