pdftools / static /js /utils.js
Shivakafle038's picture
PDF Tools Web App - compress, convert, watermark removal
32a841c
/**
* 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: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`,
error: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`,
info: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`
};
function showToast(title, message, type = 'info', duration = 4000) {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `
<div class="toast-icon">${toastIcons[type]}</div>
<div class="toast-content">
<div class="toast-title">${title}</div>
${message ? `<div class="toast-message">${message}</div>` : ''}
</div>
<button class="toast-close">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
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 = `<span class="spinner"></span> ${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 = `
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span></span>
`;
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 = `
<div class="modal-content">
<button class="modal-close">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
<img class="modal-image" src="${imageUrl}" alt="${filename}" />
<div class="modal-footer">
<div class="modal-info">
<strong>${filename}</strong><br>
${dimensions || 'Unknown'}${size || ''}
</div>
<a href="${imageUrl}" target="_blank" class="btn btn-secondary">Open Original</a>
</div>
</div>
`;
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 = `
<div class="success-content">
<div class="success-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
</svg>
</div>
<div class="success-title">${title}</div>
<div class="success-message">${message}</div>
<button class="btn btn-primary">Continue</button>
</div>
`;
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 += `
<div class="step ${isActive ? 'active' : ''} ${isCompleted ? 'completed' : ''}">
<div class="step-number ${isActive ? 'pulse' : ''}">
${isCompleted ? '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>' : index + 1}
</div>
<span class="step-label">${step}</span>
</div>
`;
if (index < this.steps.length - 1) {
html += `<div class="step-connector ${isCompleted ? 'completed' : ''}"></div>`;
}
});
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 = `
<div class="error-state">
<div class="error-state-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div class="error-state-title">${title}</div>
<div class="error-state-message">${message}</div>
<div class="error-state-actions">
${actions.map(a => `<button class="btn ${a.primary ? 'btn-primary' : 'btn-secondary'}" data-action="${a.id}">${a.label}</button>`).join('')}
</div>
</div>
`;
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 = `
<div class="inline-error-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="inline-error-content">
<div class="inline-error-title">${title}</div>
<div class="inline-error-message">${message}</div>
</div>
${actionLabel ? `<button class="inline-error-action">${actionLabel}</button>` : ''}
`;
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 = `
<div class="empty-state">
<div class="empty-state-illustration">
${icon}
</div>
<div class="empty-state-title">${title}</div>
<div class="empty-state-message">${message}</div>
${actions.length > 0 ? `
<div class="empty-state-actions">
${actions.map(a => `<button class="btn ${a.primary ? 'btn-primary' : 'btn-secondary'}" data-action="${a.id}">${a.label}</button>`).join('')}
</div>
` : ''}
</div>
`;
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 = `
<div class="preview-empty">
<div class="preview-empty-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<div class="preview-empty-title">No PDF loaded</div>
<div class="preview-empty-message">Upload a file or enter a URL to preview</div>
</div>
`;
}
function showImageGridEmpty(container) {
container.innerHTML = `
<div class="image-grid-empty">
<div class="image-grid-empty-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
<div class="image-grid-empty-title">No images yet</div>
<div class="image-grid-empty-message">Enter a webpage URL above and click "Fetch Images" to get started</div>
</div>
`;
}
// =============== 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 = `
<div class="feature-hint-icon">${icon}</div>
<div class="feature-hint-content">
<div class="feature-hint-title">${title}</div>
<div class="feature-hint-message">${message}</div>
</div>
<button class="feature-hint-dismiss">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
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);