387 lines
12 KiB
JavaScript
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; |