Files
dees-catalog/ts_web/elements/wysiwyg/blocks/media/attachment.block.ts
Juergen Kunz 342bd7d7c2 update
2025-06-26 13:18:34 +00:00

477 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element';
/**
* AttachmentBlockHandler - Handles file attachments
*
* Features:
* - Multiple file upload support
* - Click to upload or drag and drop
* - File type icons
* - Remove individual files
* - Base64 encoding (TODO: server upload in production)
*/
export class AttachmentBlockHandler extends BaseBlockHandler {
type = 'attachment';
render(block: IBlock, isSelected: boolean): string {
const files = block.metadata?.files || [];
return `
<div class="attachment-block-container${isSelected ? ' selected' : ''}"
data-block-id="${block.id}"
tabindex="0">
<div class="attachment-header">
<div class="attachment-icon">📎</div>
<div class="attachment-title">File Attachments</div>
</div>
<div class="attachment-list">
${files.length > 0 ? this.renderFiles(files) : this.renderPlaceholder()}
</div>
<input type="file"
class="attachment-file-input"
multiple
style="display: none;" />
${files.length > 0 ? '<button class="add-more-files">Add More Files</button>' : ''}
</div>
`;
}
private renderPlaceholder(): string {
return `
<div class="attachment-placeholder">
<div class="placeholder-text">Click to add files</div>
<div class="placeholder-hint">or drag and drop</div>
</div>
`;
}
private renderFiles(files: any[]): string {
return files.map((file: any) => `
<div class="attachment-item" data-file-id="${file.id}">
<div class="file-icon">${this.getFileIcon(file.type)}</div>
<div class="file-info">
<div class="file-name">${this.escapeHtml(file.name)}</div>
<div class="file-size">${this.formatFileSize(file.size)}</div>
</div>
<button class="remove-file" data-file-id="${file.id}">×</button>
</div>
`).join('');
}
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.attachment-block-container') as HTMLElement;
const fileInput = element.querySelector('.attachment-file-input') as HTMLInputElement;
if (!container || !fileInput) {
console.error('AttachmentBlockHandler: Could not find required elements');
return;
}
// Initialize files array if needed
if (!block.metadata) block.metadata = {};
if (!block.metadata.files) block.metadata.files = [];
// Click to upload on placeholder
const placeholder = container.querySelector('.attachment-placeholder');
if (placeholder) {
placeholder.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
fileInput.click();
});
}
// Add more files button
const addMoreBtn = container.querySelector('.add-more-files') as HTMLButtonElement;
if (addMoreBtn) {
addMoreBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
fileInput.click();
});
}
// File input change
fileInput.addEventListener('change', async (e) => {
const input = e.target as HTMLInputElement;
const files = input.files;
if (files && files.length > 0) {
await this.handleFileAttachments(files, block, handlers);
input.value = ''; // Clear input for next selection
}
});
// Remove file buttons
container.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.classList.contains('remove-file')) {
e.preventDefault();
e.stopPropagation();
const fileId = target.getAttribute('data-file-id');
if (fileId) {
this.removeFile(fileId, block, handlers);
}
}
});
// Drag and drop
container.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.add('drag-over');
});
container.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.remove('drag-over');
});
container.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.remove('drag-over');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
await this.handleFileAttachments(files, block, handlers);
}
});
// Focus/blur
container.addEventListener('focus', () => handlers.onFocus());
container.addEventListener('blur', () => handlers.onBlur());
// Keyboard navigation
container.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
// Only remove all files if container is focused, not when removing individual files
if (document.activeElement === container && block.metadata?.files?.length > 0) {
e.preventDefault();
block.metadata.files = [];
handlers.onRequestUpdate?.();
return;
}
}
handlers.onKeyDown(e);
});
}
private async handleFileAttachments(
files: FileList,
block: IBlock,
handlers: IBlockEventHandlers
): Promise<void> {
if (!block.metadata) block.metadata = {};
if (!block.metadata.files) block.metadata.files = [];
for (const file of Array.from(files)) {
try {
const dataUrl = await this.fileToDataUrl(file);
const fileData = {
id: this.generateId(),
name: file.name,
size: file.size,
type: file.type,
data: dataUrl
};
block.metadata.files.push(fileData);
} catch (error) {
console.error('Failed to attach file:', file.name, error);
}
}
// Update block content with file count
block.content = `${block.metadata.files.length} file${block.metadata.files.length !== 1 ? 's' : ''} attached`;
// Request UI update
handlers.onRequestUpdate?.();
}
private removeFile(fileId: string, block: IBlock, handlers: IBlockEventHandlers): void {
if (!block.metadata?.files) return;
block.metadata.files = block.metadata.files.filter((f: any) => f.id !== fileId);
// Update content
block.content = block.metadata.files.length > 0
? `${block.metadata.files.length} file${block.metadata.files.length !== 1 ? 's' : ''} attached`
: '';
// Request UI update
handlers.onRequestUpdate?.();
}
private fileToDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target?.result;
if (typeof result === 'string') {
resolve(result);
} else {
reject(new Error('Failed to read file'));
}
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
private getFileIcon(mimeType: string): string {
if (mimeType.startsWith('image/')) return '🖼️';
if (mimeType.startsWith('video/')) return '🎥';
if (mimeType.startsWith('audio/')) return '🎵';
if (mimeType.includes('pdf')) return '📄';
if (mimeType.includes('zip') || mimeType.includes('rar') || mimeType.includes('tar')) return '🗄️';
if (mimeType.includes('sheet')) return '📊';
if (mimeType.includes('document') || mimeType.includes('msword')) return '📝';
if (mimeType.includes('presentation')) return '📋';
if (mimeType.includes('text')) return '📃';
return '📁';
}
private formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
private generateId(): string {
return `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
// Content is the description of attached files
const block = this.getBlockFromElement(element);
return block?.content || '';
}
setContent(element: HTMLElement, content: string): void {
// Content is the description of attached files
const block = this.getBlockFromElement(element);
if (block) {
block.content = content;
}
}
private getBlockFromElement(element: HTMLElement): IBlock | null {
const container = element.querySelector('.attachment-block-container');
const blockId = container?.getAttribute('data-block-id');
if (!blockId) return null;
// Simplified version - in real implementation would need access to block data
return {
id: blockId,
type: 'attachment',
content: '',
metadata: {}
};
}
getCursorPosition(element: HTMLElement): number | null {
return null; // Attachment blocks don't have cursor position
}
setCursorToStart(element: HTMLElement): void {
this.focus(element);
}
setCursorToEnd(element: HTMLElement): void {
this.focus(element);
}
focus(element: HTMLElement): void {
const container = element.querySelector('.attachment-block-container') as HTMLElement;
container?.focus();
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
this.focus(element);
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
return null; // Attachment blocks can't be split
}
getStyles(): string {
return `
/* Attachment Block Container */
.attachment-block-container {
position: relative;
margin: 12px 0;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 6px;
overflow: hidden;
transition: all 0.15s ease;
outline: none;
background: ${cssManager.bdTheme('#ffffff', '#111827')};
}
.attachment-block-container.selected {
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
.attachment-block-container.drag-over {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: ${cssManager.bdTheme('#6366f1', '#818cf8')};
}
/* Header */
.attachment-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
}
.attachment-icon {
font-size: 18px;
opacity: 0.8;
}
.attachment-title {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
/* File List */
.attachment-list {
padding: 8px;
min-height: 80px;
display: flex;
flex-direction: column;
gap: 4px;
}
/* Placeholder */
.attachment-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
cursor: pointer;
transition: all 0.15s ease;
}
.attachment-placeholder:hover {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
}
.placeholder-text {
font-size: 14px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
margin-bottom: 4px;
}
.placeholder-hint {
font-size: 12px;
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* File Items */
.attachment-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px;
transition: all 0.15s ease;
}
.attachment-item:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
}
.file-icon {
font-size: 20px;
flex-shrink: 0;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-size {
font-size: 11px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
margin-top: 2px;
}
.remove-file {
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-size: 18px;
line-height: 1;
cursor: pointer;
transition: all 0.15s ease;
padding: 0;
}
.remove-file:hover {
background: ${cssManager.bdTheme('#fee2e2', '#991b1b')};
border-color: ${cssManager.bdTheme('#fca5a5', '#dc2626')};
color: ${cssManager.bdTheme('#dc2626', '#fca5a5')};
}
/* Add More Files Button */
.add-more-files {
margin: 8px;
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px;
font-size: 13px;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
cursor: pointer;
transition: all 0.15s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.add-more-files:hover {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
}
/* Hidden file input */
.attachment-file-input {
display: none !important;
}
`;
}
}