/** * MYP Platform Advanced UI Components * Erweiterte Komponenten: Progress-Bars, File-Upload, Datepicker * Version: 2.0.0 */ (function() { 'use strict'; // Namespace erweitern window.MYP = window.MYP || {}; window.MYP.Advanced = window.MYP.Advanced || {}; /** * Progress Bar Component */ class ProgressBar { constructor(container, options = {}) { this.container = typeof container === 'string' ? document.querySelector(container) : container; this.options = { value: 0, max: 100, showLabel: true, showPercentage: true, animated: true, color: 'blue', size: 'md', striped: false, ...options }; this.currentValue = this.options.value; this.init(); } init() { if (!this.container) { console.error('ProgressBar: Container nicht gefunden'); return; } this.render(); } render() { const percentage = Math.round((this.currentValue / this.options.max) * 100); const sizeClass = this.getSizeClass(); const colorClass = this.getColorClass(); this.container.innerHTML = `
${this.options.showLabel ? `
${this.options.label || 'Fortschritt'} ${this.options.showPercentage ? ` ${percentage}% ` : ''}
` : ''}
`; } getSizeClass() { const sizes = { 'sm': 'h-2', 'md': 'h-3', 'lg': 'h-4', 'xl': 'h-6' }; return sizes[this.options.size] || sizes.md; } getColorClass() { const colors = { 'blue': 'bg-blue-500', 'green': 'bg-green-500', 'red': 'bg-red-500', 'yellow': 'bg-yellow-500', 'purple': 'bg-purple-500', 'indigo': 'bg-indigo-500' }; return colors[this.options.color] || colors.blue; } setValue(value, animate = true) { const oldValue = this.currentValue; this.currentValue = Math.max(0, Math.min(this.options.max, value)); if (animate) { this.animateToValue(oldValue, this.currentValue); } else { this.render(); } } animateToValue(from, to) { const duration = 500; // ms const steps = 60; const stepValue = (to - from) / steps; let currentStep = 0; const animate = () => { if (currentStep < steps) { this.currentValue = from + (stepValue * currentStep); this.render(); currentStep++; requestAnimationFrame(animate); } else { this.currentValue = to; this.render(); } }; animate(); } increment(amount = 1) { this.setValue(this.currentValue + amount); } decrement(amount = 1) { this.setValue(this.currentValue - amount); } reset() { this.setValue(0); } complete() { this.setValue(this.options.max); } } /** * Advanced File Upload Component */ class FileUpload { constructor(container, options = {}) { this.container = typeof container === 'string' ? document.querySelector(container) : container; this.options = { multiple: false, accept: '*/*', maxSize: 50 * 1024 * 1024, // 50MB maxFiles: 10, dragDrop: true, showProgress: true, showPreview: true, uploadUrl: '/api/upload', chunkSize: 1024 * 1024, // 1MB chunks ...options }; this.files = []; this.uploads = new Map(); this.init(); } init() { if (!this.container) { console.error('FileUpload: Container nicht gefunden'); return; } this.render(); this.setupEventListeners(); } render() { this.container.innerHTML = `

${this.getFileTypeText()} • Max. ${this.formatFileSize(this.options.maxSize)}

`; } setupEventListeners() { const fileInput = this.container.querySelector('#fileInput'); const dropzone = this.container.querySelector('#dropzone'); // File Input Change fileInput.addEventListener('change', (e) => { this.handleFiles(Array.from(e.target.files)); }); if (this.options.dragDrop) { // Drag and Drop Events dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('drag-over'); }); dropzone.addEventListener('dragleave', (e) => { e.preventDefault(); dropzone.classList.remove('drag-over'); }); dropzone.addEventListener('drop', (e) => { e.preventDefault(); dropzone.classList.remove('drag-over'); this.handleFiles(Array.from(e.dataTransfer.files)); }); } } handleFiles(fileList) { for (const file of fileList) { if (this.validateFile(file)) { this.addFile(file); } } this.renderFileList(); } validateFile(file) { // Dateigröße prüfen if (file.size > this.options.maxSize) { this.showError(`Datei "${file.name}" ist zu groß. Maximum: ${this.formatFileSize(this.options.maxSize)}`); return false; } // Anzahl Dateien prüfen if (!this.options.multiple && this.files.length > 0) { this.files = []; // Ersetze einzelne Datei } else if (this.files.length >= this.options.maxFiles) { this.showError(`Maximal ${this.options.maxFiles} Dateien erlaubt`); return false; } return true; } addFile(file) { const fileData = { id: this.generateId(), file: file, name: file.name, size: file.size, type: file.type, status: 'pending', progress: 0, error: null }; this.files.push(fileData); // Preview generieren if (this.options.showPreview && file.type.startsWith('image/')) { this.generatePreview(fileData); } } generatePreview(fileData) { const reader = new FileReader(); reader.onload = (e) => { fileData.preview = e.target.result; this.renderFileList(); }; reader.readAsDataURL(fileData.file); } renderFileList() { const fileListContainer = this.container.querySelector('#fileList'); if (this.files.length === 0) { fileListContainer.innerHTML = ''; return; } fileListContainer.innerHTML = this.files.map(fileData => `
${fileData.preview ? ` Preview ` : `
`}

${fileData.name}

${this.formatFileSize(fileData.size)} • ${this.getStatusText(fileData.status)}

${this.options.showProgress && fileData.status === 'uploading' ? `
` : ''} ${fileData.error ? `

${fileData.error}

` : ''}
`).join(''); // Event Listeners für Remove-Buttons fileListContainer.querySelectorAll('.remove-file').forEach(button => { button.addEventListener('click', (e) => { const fileId = e.target.closest('.remove-file').dataset.fileId; this.removeFile(fileId); }); }); } removeFile(fileId) { this.files = this.files.filter(f => f.id !== fileId); this.renderFileList(); } async uploadFiles() { const pendingFiles = this.files.filter(f => f.status === 'pending'); for (const fileData of pendingFiles) { await this.uploadFile(fileData); } } async uploadFile(fileData) { fileData.status = 'uploading'; fileData.progress = 0; this.renderFileList(); try { const formData = new FormData(); formData.append('file', fileData.file); const response = await fetch(this.options.uploadUrl, { method: 'POST', body: formData, headers: { 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' } }); if (response.ok) { fileData.status = 'completed'; fileData.progress = 100; const result = await response.json(); fileData.url = result.url; } else { throw new Error(`Upload fehlgeschlagen: ${response.status}`); } } catch (error) { fileData.status = 'error'; fileData.error = error.message; } this.renderFileList(); } getFileTypeText() { if (this.options.accept === '*/*') return 'Alle Dateitypen'; if (this.options.accept.includes('image/')) return 'Bilder'; if (this.options.accept.includes('.pdf')) return 'PDF-Dateien'; return 'Spezifische Dateitypen'; } getStatusText(status) { const statusTexts = { 'pending': 'Wartend', 'uploading': 'Wird hochgeladen...', 'completed': 'Abgeschlossen', 'error': 'Fehler' }; return statusTexts[status] || status; } formatFileSize(bytes) { 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]; } generateId() { return 'file_' + Math.random().toString(36).substr(2, 9); } showError(message) { if (window.showToast) { window.showToast(message, 'error'); } else { alert(message); } } getFiles() { return this.files; } getCompletedFiles() { return this.files.filter(f => f.status === 'completed'); } clear() { this.files = []; this.renderFileList(); } } /** * Simple Datepicker Component */ class DatePicker { constructor(input, options = {}) { this.input = typeof input === 'string' ? document.querySelector(input) : input; this.options = { format: 'dd.mm.yyyy', minDate: null, maxDate: null, disabledDates: [], language: 'de', closeOnSelect: true, showWeekNumbers: false, ...options }; this.isOpen = false; this.currentDate = new Date(); this.selectedDate = null; this.init(); } init() { if (!this.input) { console.error('DatePicker: Input-Element nicht gefunden'); return; } this.setupInput(); this.createCalendar(); this.setupEventListeners(); } setupInput() { this.input.setAttribute('readonly', 'true'); this.input.classList.add('datepicker-input'); // Container für Input und Calendar this.container = document.createElement('div'); this.container.className = 'datepicker-container relative'; this.input.parentNode.insertBefore(this.container, this.input); this.container.appendChild(this.input); } createCalendar() { this.calendar = document.createElement('div'); this.calendar.className = 'datepicker-calendar absolute top-full left-0 mt-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-600 rounded-lg shadow-lg z-50 hidden'; this.calendar.innerHTML = this.renderCalendar(); this.container.appendChild(this.calendar); } renderCalendar() { const year = this.currentDate.getFullYear(); const month = this.currentDate.getMonth(); const monthName = this.getMonthName(month); return `
${monthName} ${year}
${this.getWeekdayHeaders()}
${this.getDaysOfMonth(year, month)}
`; } getWeekdayHeaders() { const weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; return weekdays.map(day => `
${day}
` ).join(''); } getDaysOfMonth(year, month) { const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startDate = new Date(firstDay); startDate.setDate(startDate.getDate() - ((firstDay.getDay() + 6) % 7)); const days = []; const current = new Date(startDate); while (current <= lastDay || current.getMonth() === month) { const isCurrentMonth = current.getMonth() === month; const isToday = this.isToday(current); const isSelected = this.isSelectedDate(current); const isDisabled = this.isDisabledDate(current); const classes = [ 'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center transition-colors', isCurrentMonth ? 'text-slate-900 dark:text-white' : 'text-slate-400 dark:text-slate-600', isToday ? 'bg-blue-100 dark:bg-blue-900 text-blue-900 dark:text-blue-100' : '', isSelected ? 'bg-blue-500 text-white' : '', !isDisabled && isCurrentMonth ? 'hover:bg-slate-100 dark:hover:bg-slate-700' : '', isDisabled ? 'cursor-not-allowed opacity-50' : '' ].filter(Boolean); days.push(`
${current.getDate()}
`); current.setDate(current.getDate() + 1); if (days.length >= 42) break; // Max 6 Wochen } return days.join(''); } setupEventListeners() { // Input click this.input.addEventListener('click', () => { this.toggle(); }); // Calendar clicks this.calendar.addEventListener('click', (e) => { if (e.target.classList.contains('prev-month')) { this.previousMonth(); } else if (e.target.classList.contains('next-month')) { this.nextMonth(); } else if (e.target.dataset.selectable) { this.selectDate(new Date(e.target.dataset.date)); } }); // Click outside document.addEventListener('click', (e) => { if (!this.container.contains(e.target)) { this.close(); } }); } toggle() { if (this.isOpen) { this.close(); } else { this.open(); } } open() { this.calendar.classList.remove('hidden'); this.isOpen = true; this.updateCalendar(); } close() { this.calendar.classList.add('hidden'); this.isOpen = false; } selectDate(date) { this.selectedDate = new Date(date); this.input.value = this.formatDate(date); // Custom Event this.input.dispatchEvent(new CustomEvent('dateselected', { detail: { date: new Date(date) } })); if (this.options.closeOnSelect) { this.close(); } } previousMonth() { this.currentDate.setMonth(this.currentDate.getMonth() - 1); this.updateCalendar(); } nextMonth() { this.currentDate.setMonth(this.currentDate.getMonth() + 1); this.updateCalendar(); } updateCalendar() { this.calendar.innerHTML = this.renderCalendar(); } isToday(date) { const today = new Date(); return date.toDateString() === today.toDateString(); } isSelectedDate(date) { return this.selectedDate && date.toDateString() === this.selectedDate.toDateString(); } isDisabledDate(date) { if (this.options.minDate && date < this.options.minDate) return true; if (this.options.maxDate && date > this.options.maxDate) return true; return this.options.disabledDates.some(disabled => date.toDateString() === disabled.toDateString() ); } formatDate(date) { const day = date.getDate().toString().padStart(2, '0'); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const year = date.getFullYear(); return this.options.format .replace('dd', day) .replace('mm', month) .replace('yyyy', year); } formatDateForData(date) { return date.toISOString().split('T')[0]; } getMonthName(monthIndex) { const months = [ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' ]; return months[monthIndex]; } setValue(date) { if (date) { this.selectDate(new Date(date)); } } getValue() { return this.selectedDate; } clear() { this.selectedDate = null; this.input.value = ''; } } // Globale API window.MYP.Advanced = { ProgressBar, FileUpload, DatePicker, // Convenience Functions createProgressBar: (container, options) => new ProgressBar(container, options), createFileUpload: (container, options) => new FileUpload(container, options), createDatePicker: (input, options) => new DatePicker(input, options) }; // Auto-Initialize document.addEventListener('DOMContentLoaded', function() { // Auto-initialize datepickers document.querySelectorAll('[data-datepicker]').forEach(input => { const options = JSON.parse(input.dataset.datepicker || '{}'); new DatePicker(input, options); }); // Auto-initialize file uploads document.querySelectorAll('[data-file-upload]').forEach(container => { const options = JSON.parse(container.dataset.fileUpload || '{}'); new FileUpload(container, options); }); console.log('🚀 MYP Advanced Components geladen'); }); })();