/** * Utility Functions * Toast, loading, validation, drag-drop, modal */ // =============== Toast Notifications =============== const toastContainer = document.createElement('div'); toastContainer.className = 'toast-container'; document.body.appendChild(toastContainer); const toastIcons = { success: ``, error: ``, info: `` }; function showToast(title, message, type = 'info', duration = 4000) { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.innerHTML = `
${toastIcons[type]}
${title}
${message ? `
${message}
` : ''}
`; toast.querySelector('.toast-close').addEventListener('click', () => removeToast(toast)); toastContainer.appendChild(toast); if (duration > 0) { setTimeout(() => removeToast(toast), duration); } return toast; } function removeToast(toast) { if (!toast.parentNode) return; toast.classList.add('hiding'); setTimeout(() => toast.remove(), 300); } // =============== Loading States =============== function setButtonLoading(btn, loading, loadingText = 'Loading...') { if (loading) { btn.dataset.originalText = btn.innerHTML; btn.innerHTML = ` ${loadingText}`; btn.classList.add('loading'); btn.disabled = true; } else { btn.innerHTML = btn.dataset.originalText || btn.innerHTML; btn.classList.remove('loading'); btn.disabled = false; } } function showProgress(container, show = true, indeterminate = true) { if (!container) return; if (show) { container.classList.add('active'); const fill = container.querySelector('.progress-fill'); if (fill) { if (indeterminate) { fill.classList.add('indeterminate'); fill.style.width = ''; } else { fill.classList.remove('indeterminate'); } } } else { container.classList.remove('active'); } } // =============== Form Validation =============== function validateUrl(input, errorMsg = 'Invalid URL') { const value = input.value.trim(); const formGroup = input.closest('.form-group'); if (!value) { clearFieldError(formGroup); return true; } try { new URL(value); clearFieldError(formGroup); return true; } catch { setFieldError(formGroup, errorMsg); return false; } } function setFieldError(formGroup, message) { if (!formGroup) return; formGroup.classList.add('error'); let errorEl = formGroup.querySelector('.form-error'); if (!errorEl) { errorEl = document.createElement('div'); errorEl.className = 'form-error'; errorEl.innerHTML = ` `; formGroup.appendChild(errorEl); } errorEl.querySelector('span').textContent = message; } function clearFieldError(formGroup) { if (!formGroup) return; formGroup.classList.remove('error'); } // =============== Drag & Drop =============== function initDropZone(dropZone, fileInput, options = {}) { const { onFile = () => {}, maxSize = 50 * 1024 * 1024 } = options; const fileDisplay = dropZone.querySelector('.drop-zone-file'); const fileName = dropZone.querySelector('.drop-zone-file-name'); const fileSize = dropZone.querySelector('.drop-zone-file-size'); const removeBtn = dropZone.querySelector('.drop-zone-file-remove'); dropZone.addEventListener('click', (e) => { if (e.target === removeBtn) return; fileInput.click(); }); ['dragenter', 'dragover'].forEach(event => { dropZone.addEventListener(event, (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }); }); ['dragleave', 'drop'].forEach(event => { dropZone.addEventListener(event, (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); }); }); dropZone.addEventListener('drop', (e) => { const files = e.dataTransfer.files; if (files.length > 0) handleFile(files[0]); }); fileInput.addEventListener('change', () => { if (fileInput.files.length > 0) handleFile(fileInput.files[0]); }); if (removeBtn) { removeBtn.addEventListener('click', (e) => { e.stopPropagation(); clearFile(); }); } function handleFile(file) { if (file.size > maxSize) { showToast('File Too Large', `Max size: ${formatFileSize(maxSize)}`, 'error'); return; } if (fileDisplay) { fileDisplay.classList.add('active'); if (fileName) fileName.textContent = file.name; if (fileSize) fileSize.textContent = formatFileSize(file.size); } const dt = new DataTransfer(); dt.items.add(file); fileInput.files = dt.files; onFile(file); } function clearFile() { fileInput.value = ''; if (fileDisplay) fileDisplay.classList.remove('active'); onFile(null); } return { handleFile, clearFile }; } function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } // =============== Image Modal =============== let currentModal = null; function showImageModal(imageUrl, filename, dimensions, size) { closeImageModal(); const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.innerHTML = ` `; document.body.appendChild(modal); requestAnimationFrame(() => modal.classList.add('active')); modal.querySelector('.modal-close').addEventListener('click', closeImageModal); modal.addEventListener('click', (e) => { if (e.target === modal) closeImageModal(); }); document.addEventListener('keydown', handleModalEsc); currentModal = modal; } function closeImageModal() { if (currentModal) { currentModal.classList.remove('active'); setTimeout(() => currentModal?.remove(), 200); currentModal = null; document.removeEventListener('keydown', handleModalEsc); } } function handleModalEsc(e) { if (e.key === 'Escape') closeImageModal(); } // =============== Keyboard Shortcuts =============== const shortcuts = {}; function registerShortcut(key, callback, description = '') { shortcuts[key.toLowerCase()] = { callback, description }; } document.addEventListener('keydown', (e) => { if (e.target.matches('input, textarea, select')) return; let key = ''; if (e.ctrlKey || e.metaKey) key += 'ctrl+'; if (e.shiftKey) key += 'shift+'; if (e.altKey) key += 'alt+'; key += e.key.toLowerCase(); const shortcut = shortcuts[key]; if (shortcut) { e.preventDefault(); shortcut.callback(); } }); // =============== Success Animation =============== function showSuccessAnimation(title = 'Success!', message = 'Operation completed') { // Create overlay const overlay = document.createElement('div'); overlay.className = 'success-overlay'; overlay.innerHTML = `
${title}
${message}
`; document.body.appendChild(overlay); // Add confetti createConfetti(overlay); // Show overlay requestAnimationFrame(() => overlay.classList.add('active')); // Close handlers const closeBtn = overlay.querySelector('.btn'); closeBtn.addEventListener('click', () => closeSuccessAnimation(overlay)); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSuccessAnimation(overlay); }); // Auto close after 3 seconds setTimeout(() => closeSuccessAnimation(overlay), 3000); } function closeSuccessAnimation(overlay) { if (!overlay) return; overlay.style.opacity = '0'; setTimeout(() => overlay.remove(), 300); } function createConfetti(container) { const colors = ['#4caf50', '#66bb6a', '#81c784', '#a5d6a7', '#c8e6c9', '#2e7d32']; for (let i = 0; i < 50; i++) { const confetti = document.createElement('div'); confetti.className = 'confetti'; confetti.style.left = Math.random() * 100 + '%'; confetti.style.top = '-10px'; confetti.style.background = colors[Math.floor(Math.random() * colors.length)]; confetti.style.animationDelay = Math.random() * 0.5 + 's'; confetti.style.animationDuration = (2 + Math.random() * 2) + 's'; container.appendChild(confetti); } } // =============== Step Indicator =============== class StepIndicator { constructor(containerId, steps) { this.container = document.getElementById(containerId); this.steps = steps; this.currentStep = 0; this.render(); } render() { if (!this.container) return; let html = ''; this.steps.forEach((step, index) => { const isActive = index === this.currentStep; const isCompleted = index < this.currentStep; html += `
${isCompleted ? '' : index + 1}
${step}
`; if (index < this.steps.length - 1) { html += `
`; } }); this.container.innerHTML = html; } setStep(stepIndex) { this.currentStep = Math.max(0, Math.min(stepIndex, this.steps.length - 1)); this.render(); } next() { if (this.currentStep < this.steps.length - 1) { this.currentStep++; this.render(); } } prev() { if (this.currentStep > 0) { this.currentStep--; this.render(); } } complete() { this.currentStep = this.steps.length; this.render(); } reset() { this.currentStep = 0; this.render(); } } // =============== Error States =============== function showErrorState(container, title, message, actions = []) { const errorHtml = `
${title}
${message}
${actions.map(a => ``).join('')}
`; container.innerHTML = errorHtml; // Bind action handlers actions.forEach(action => { const btn = container.querySelector(`[data-action="${action.id}"]`); if (btn && action.handler) { btn.addEventListener('click', action.handler); } }); } function showInlineError(container, title, message, actionLabel, actionHandler) { const errorEl = document.createElement('div'); errorEl.className = 'inline-error'; errorEl.innerHTML = `
${title}
${message}
${actionLabel ? `` : ''} `; if (actionLabel && actionHandler) { errorEl.querySelector('.inline-error-action').addEventListener('click', () => { errorEl.remove(); actionHandler(); }); } container.appendChild(errorEl); return errorEl; } // =============== Empty States =============== function showEmptyState(container, icon, title, message, actions = []) { const emptyHtml = `
${icon}
${title}
${message}
${actions.length > 0 ? `
${actions.map(a => ``).join('')}
` : ''}
`; container.innerHTML = emptyHtml; // Bind action handlers actions.forEach(action => { const btn = container.querySelector(`[data-action="${action.id}"]`); if (btn && action.handler) { btn.addEventListener('click', action.handler); } }); } function showPreviewEmpty(container) { container.innerHTML = `
No PDF loaded
Upload a file or enter a URL to preview
`; } function showImageGridEmpty(container) { container.innerHTML = `
No images yet
Enter a webpage URL above and click "Fetch Images" to get started
`; } // =============== Feature Hints =============== function showFeatureHint(container, icon, title, message, dismissKey) { // Check if already dismissed if (localStorage.getItem(`hint_${dismissKey}`)) return; const hint = document.createElement('div'); hint.className = 'feature-hint'; hint.innerHTML = `
${icon}
${title}
${message}
`; hint.querySelector('.feature-hint-dismiss').addEventListener('click', () => { localStorage.setItem(`hint_${dismissKey}`, 'true'); hint.style.opacity = '0'; hint.style.transform = 'translateY(-10px)'; setTimeout(() => hint.remove(), 300); }); container.prepend(hint); } // =============== Tooltip Initialization =============== function initTooltips() { // Add tooltip class to elements with data-tooltip document.querySelectorAll('[data-tooltip]').forEach(el => { if (!el.classList.contains('tooltip')) { el.classList.add('tooltip'); } }); } // Initialize tooltips on load document.addEventListener('DOMContentLoaded', initTooltips);