Files
notytex/static/js/notytex.js

387 lines
12 KiB
JavaScript

/**
* NOTYTEX - JavaScript Application Core
* Fonctionnalités JavaScript centralisées et réutilisables
*/
const Notytex = {
// Configuration globale
config: {
transitions: {
fast: 150,
normal: 300,
slow: 500
},
breakpoints: {
sm: 640,
md: 768,
lg: 1024,
xl: 1280
}
},
// Utilitaires généraux
utils: {
/**
* Débounce une fonction
*/
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
/**
* Vérifie si un élément est visible dans le viewport
*/
isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
},
/**
* Anime l'apparition des éléments
*/
animateOnScroll() {
const elements = document.querySelectorAll('[data-animate]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const animationType = entry.target.dataset.animate;
entry.target.classList.add(`animate-${animationType}`);
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
elements.forEach(el => observer.observe(el));
},
/**
* Copie du texte dans le presse-papiers
*/
async copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
this.showToast('Copié dans le presse-papiers', 'success');
} catch (err) {
console.error('Erreur lors de la copie:', err);
this.showToast('Erreur lors de la copie', 'error');
}
},
/**
* Affiche une notification toast
*/
showToast(message, type = 'info', duration = 3000) {
const toast = document.createElement('div');
const typeClasses = {
success: 'bg-green-500 text-white',
error: 'bg-red-500 text-white',
warning: 'bg-orange-500 text-white',
info: 'bg-blue-500 text-white'
};
toast.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ${typeClasses[type]} transition-all duration-300 transform translate-x-full`;
toast.textContent = message;
document.body.appendChild(toast);
// Animation d'entrée
setTimeout(() => toast.classList.remove('translate-x-full'), 100);
// Suppression automatique
setTimeout(() => {
toast.classList.add('translate-x-full');
setTimeout(() => document.body.removeChild(toast), 300);
}, duration);
}
},
// Gestion des filtres
filters: {
/**
* Initialise les filtres pour une page
*/
init(config = {}) {
const defaultConfig = {
autoSubmit: true,
debounceTime: 300,
preserveState: true
};
const settings = { ...defaultConfig, ...config };
const filterElements = document.querySelectorAll('[data-filter]');
const applyFilters = Notytex.utils.debounce(this.applyFilters, settings.debounceTime);
filterElements.forEach(element => {
element.addEventListener('change', applyFilters);
});
// Restaurer l'état des filtres depuis l'URL
if (settings.preserveState) {
this.restoreFromURL();
}
},
/**
* Applique les filtres en construisant l'URL
*/
applyFilters() {
const params = new URLSearchParams();
const filterElements = document.querySelectorAll('[data-filter]');
filterElements.forEach(element => {
const filterName = element.dataset.filter;
const value = element.value;
if (value && value !== '') {
params.set(filterName, value);
}
});
// Mise à jour de l'URL
const url = new URL(window.location);
url.search = params.toString();
// Navigation sans rechargement complet si possible
if (history.pushState) {
history.pushState(null, '', url.toString());
// Déclenchement d'un événement personnalisé pour les composants qui écoutent
window.dispatchEvent(new CustomEvent('filtersChanged', { detail: params }));
} else {
// Fallback pour les anciens navigateurs
window.location.href = url.toString();
}
},
/**
* Restaure les filtres depuis l'URL
*/
restoreFromURL() {
const params = new URLSearchParams(window.location.search);
params.forEach((value, key) => {
const element = document.querySelector(`[data-filter="${key}"]`);
if (element) {
element.value = value;
}
});
}
},
// Gestion des modales
modals: {
/**
* Ouvre une modale
*/
open(modalId) {
const modal = document.getElementById(modalId);
if (!modal) return;
modal.classList.remove('hidden');
modal.classList.add('flex');
// Animation d'entrée
setTimeout(() => {
modal.querySelector('.modal-content')?.classList.add('scale-100');
modal.querySelector('.modal-content')?.classList.remove('scale-95');
}, 10);
// Focus sur la modale pour l'accessibilité
modal.setAttribute('aria-hidden', 'false');
modal.focus();
// Écouter la touche Escape
document.addEventListener('keydown', this.handleEscapeKey);
},
/**
* Ferme une modale
*/
close(modalId) {
const modal = document.getElementById(modalId);
if (!modal) return;
// Animation de sortie
modal.querySelector('.modal-content')?.classList.add('scale-95');
modal.querySelector('.modal-content')?.classList.remove('scale-100');
setTimeout(() => {
modal.classList.add('hidden');
modal.classList.remove('flex');
modal.setAttribute('aria-hidden', 'true');
}, 150);
document.removeEventListener('keydown', this.handleEscapeKey);
},
/**
* Gère la touche Escape pour fermer les modales
*/
handleEscapeKey(event) {
if (event.key === 'Escape') {
const openModal = document.querySelector('.modal:not(.hidden)');
if (openModal) {
Notytex.modals.close(openModal.id);
}
}
}
},
// Gestion des données en temps réel
realtime: {
/**
* Met à jour les indicateurs de progression
*/
updateProgressIndicators() {
const indicators = document.querySelectorAll('[data-progress]');
indicators.forEach(indicator => {
const assessmentId = indicator.dataset.assessmentId;
// Ici on peut faire un appel AJAX pour récupérer les données actualisées
// Pour l'instant, on simule avec une animation
this.animateProgress(indicator);
});
},
/**
* Animation des cercles de progression
*/
animateProgress(element) {
const circle = element.querySelector('circle[stroke-dashoffset]');
if (!circle) return;
const currentOffset = parseFloat(circle.getAttribute('stroke-dashoffset'));
const circumference = 2 * Math.PI * parseFloat(circle.getAttribute('r'));
// Animation fluide
circle.style.transition = 'stroke-dashoffset 1s ease-in-out';
}
},
// Gestion des confirmations
confirmations: {
/**
* Affiche une confirmation avant une action
*/
show(message, callback, options = {}) {
const defaultOptions = {
title: 'Confirmation',
confirmText: 'Confirmer',
cancelText: 'Annuler',
type: 'warning'
};
const settings = { ...defaultOptions, ...options };
// Si on a SweetAlert2 disponible, on l'utilise
if (typeof Swal !== 'undefined') {
Swal.fire({
title: settings.title,
text: message,
icon: settings.type,
showCancelButton: true,
confirmButtonText: settings.confirmText,
cancelButtonText: settings.cancelText,
confirmButtonColor: settings.type === 'danger' ? '#dc2626' : '#3b82f6'
}).then((result) => {
if (result.isConfirmed) {
callback();
}
});
} else {
// Fallback avec confirm() natif
if (confirm(message)) {
callback();
}
}
}
},
// Initialisation de l'application
init() {
console.log('🎓 Notytex Application Initialized');
// Initialisation des fonctionnalités de base
this.utils.animateOnScroll();
// Initialisation des filtres si présents
if (document.querySelector('[data-filter]')) {
this.filters.init();
}
// Gestionnaires d'événements globaux
this.bindGlobalEvents();
// Mise à jour périodique des indicateurs (optionnel)
if (document.querySelector('[data-progress]')) {
setInterval(() => {
this.realtime.updateProgressIndicators();
}, 30000); // Toutes les 30 secondes
}
},
// Événements globaux
bindGlobalEvents() {
// Fermeture des dropdowns au clic extérieur
document.addEventListener('click', (event) => {
const dropdowns = document.querySelectorAll('.dropdown-menu.show');
dropdowns.forEach(dropdown => {
if (!dropdown.contains(event.target)) {
dropdown.classList.remove('show');
}
});
});
// Gestion des liens de confirmation
document.addEventListener('click', (event) => {
const confirmLink = event.target.closest('[data-confirm]');
if (confirmLink) {
event.preventDefault();
const message = confirmLink.dataset.confirm;
this.confirmations.show(message, () => {
if (confirmLink.tagName === 'A') {
window.location.href = confirmLink.href;
} else if (confirmLink.tagName === 'BUTTON') {
confirmLink.click();
}
});
}
});
// Gestion des boutons de copie
document.addEventListener('click', (event) => {
const copyButton = event.target.closest('[data-copy]');
if (copyButton) {
const textToCopy = copyButton.dataset.copy;
this.utils.copyToClipboard(textToCopy);
}
});
}
};
// Auto-initialisation au chargement du DOM
document.addEventListener('DOMContentLoaded', () => {
Notytex.init();
});
// Export pour utilisation dans d'autres scripts
window.Notytex = Notytex;