radiantrh/Bootstrap_new/js/ux-manager.js

1377 lines
41 KiB
JavaScript

/**
* UX Manager pour Portail Inter Santé
* Gestion avancée de l'expérience utilisateur
* Version: 2025.12.20.01
*/
// Configuration globale
const UXConfig = {
debug: false,
transitionSpeed: 300,
proximityRadius: 150,
notificationPollInterval: 30000,
saveState: true
};
// Module principal UX Manager
class UXManager {
constructor() {
this.contextPanelOpen = false;
this.sidebarOpen = false;
this.activeMenuId = window.appConfig?.activeParentId || null;
this.proximityDetectionActive = true;
this.modules = {
navigation: null,
context: null,
notifications: null,
accessibility: null,
performance: null
};
this.init();
}
/**
* Initialisation du système UX
*/
init() {
try {
console.log('🚀 UX Manager initialisation...');
// Initialiser les modules
this.modules.navigation = new NavigationManager();
this.modules.context = new ContextPanelManager();
this.modules.notifications = new NotificationManager();
this.modules.accessibility = new AccessibilityManager();
this.modules.performance = new PerformanceManager();
// Restaurer l'état précédent
this.restoreState();
// Configurer les événements globaux
this.setupGlobalEvents();
// Lancer les pollings
this.startPollings();
console.log('✅ UX Manager prêt');
} catch (error) {
console.error('❌ Erreur initialisation UX Manager:', error);
}
}
/**
* Restaurer l'état depuis localStorage
*/
restoreState() {
if (!UXConfig.saveState) return;
try {
const savedPanelState = localStorage.getItem('contextPanelOpen');
if (savedPanelState === 'true') {
// Ouvrir avec délai pour meilleure UX
setTimeout(() => {
this.modules.context.togglePanel();
}, 1000);
}
const savedMenuState = localStorage.getItem('expandedMenus');
if (savedMenuState) {
this.modules.navigation.restoreMenuState(JSON.parse(savedMenuState));
}
} catch (e) {
if (UXConfig.debug) console.warn('Impossible de restaurer l\'état:', e);
}
}
/**
* Configurer les événements globaux
*/
setupGlobalEvents() {
// Gestionnaire de redimensionnement avec debounce
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
this.handleResize();
}, 250);
});
// Navigation au clavier
document.addEventListener('keydown', (e) => {
this.handleKeyboardNavigation(e);
});
// Prévenir la fermeture accidentelle avec données non sauvegardées
window.addEventListener('beforeunload', (e) => {
// Ici vous pouvez ajouter une logique pour vérifier les données non sauvegardées
// if (this.hasUnsavedChanges()) {
// e.preventDefault();
// e.returnValue = '';
// }
});
}
/**
* Gérer le redimensionnement de la fenêtre
*/
handleResize() {
const isMobile = window.innerWidth < 768;
document.body.classList.toggle('mobile-view', isMobile);
// Ajuster le comportement du sidebar sur mobile
if (!isMobile && this.sidebarOpen) {
document.getElementById('sidebar')?.classList.remove('open');
this.sidebarOpen = false;
}
}
/**
* Gérer la navigation au clavier
*/
handleKeyboardNavigation(e) {
// Marquer la navigation clavier pour le CSS
if (e.key === 'Tab') {
document.body.classList.add('keyboard-navigation');
}
// Échap pour fermer les modales
if (e.key === 'Escape') {
if (this.contextPanelOpen) {
this.modules.context.togglePanel();
}
// Fermer toutes les modales Bootstrap ouvertes
const openModals = document.querySelectorAll('.modal.show');
openModals.forEach(modal => {
const bsModal = bootstrap.Modal.getInstance(modal);
if (bsModal) bsModal.hide();
});
}
// Navigation dans les menus avec flèches
if (e.target.closest('.sidebar-nav')) {
this.modules.navigation.handleKeyboardMenuNavigation(e);
}
}
/**
* Démarrer les pollings automatiques
*/
startPollings() {
// Polling des notifications
setInterval(() => {
this.modules.notifications.pollNotifications();
}, UXConfig.notificationPollInterval);
// Premier poll immédiat
setTimeout(() => {
this.modules.notifications.pollNotifications();
}, 2000);
}
/**
* Basculer le panneau contexte
*/
toggleContextPanel() {
return this.modules.context.togglePanel();
}
/**
* Afficher la modale photo
*/
openPhotoModal() {
return this.modules.context.openPhotoModal();
}
/**
* Basculer le sidebar mobile
*/
toggleSidebar() {
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
this.sidebarOpen = !this.sidebarOpen;
sidebar.classList.toggle('open');
const toggleBtn = document.querySelector('.sidebar-toggle');
if (toggleBtn) {
toggleBtn.setAttribute('aria-expanded', this.sidebarOpen);
}
// Fermer le sidebar si on clique à l'extérieur (mobile uniquement)
if (this.sidebarOpen && window.innerWidth < 768) {
setTimeout(() => {
document.addEventListener('click', this.closeSidebarOnClickOutside.bind(this), { once: true });
}, 100);
}
}
/**
* Fermer le sidebar en cliquant à l'extérieur
*/
closeSidebarOnClickOutside(e) {
const sidebar = document.getElementById('sidebar');
const toggleBtn = document.querySelector('.sidebar-toggle');
if (!sidebar?.contains(e.target) && !toggleBtn?.contains(e.target)) {
sidebar.classList.remove('open');
this.sidebarOpen = false;
if (toggleBtn) toggleBtn.setAttribute('aria-expanded', 'false');
}
}
}
// ============================================
// MODULE: Gestionnaire de Navigation
// ============================================
class NavigationManager {
constructor() {
this.expandedMenus = new Set();
this.activeMenuId = window.appConfig?.activeParentId || null;
this.init();
}
init() {
this.setupMenuAutoClose();
this.highlightActiveMenu();
this.setupMenuInteractions();
}
/**
* Fermer automatiquement les menus inactifs
*/
setupMenuAutoClose() {
// Fermer tous les sous-menus sauf celui actif
document.querySelectorAll('.nav-submenu.show').forEach(submenu => {
const menuId = submenu.id;
const parentLink = document.querySelector(`[href="#${menuId}"]`);
if (!parentLink?.classList.contains('active')) {
this.collapseMenu(submenu.id);
}
});
// Fermer les menus quand on clique ailleurs
document.addEventListener('click', (e) => {
if (!e.target.closest('.nav-item')) {
this.collapseAllExceptActive();
}
});
}
/**
* Mettre en évidence le menu actif
*/
highlightActiveMenu() {
if (!this.activeMenuId) return;
const activeLink = document.querySelector(`[href="#submenu${this.activeMenuId}"]`);
if (activeLink) {
activeLink.classList.add('active');
this.expandMenu(`submenu${this.activeMenuId}`);
}
// Mettre en évidence les enfants actifs
document.querySelectorAll('.nav-submenu .nav-link').forEach(link => {
const href = link.getAttribute('href');
if (href && href.includes(window.appConfig.activeLink)) {
link.classList.add('active');
link.setAttribute('aria-current', 'page');
}
});
}
/**
* Configurer les interactions des menus
*/
setupMenuInteractions() {
document.querySelectorAll('.nav-link[data-bs-toggle="collapse"]').forEach(link => {
link.addEventListener('click', (e) => {
const menuId = link.getAttribute('href').substring(1);
// Si on clique sur un menu déjà actif, ne rien faire
if (link.classList.contains('active')) return;
// Fermer tous les autres menus
this.collapseAllExcept(menuId);
// Sauvegarder l'état
this.saveMenuState();
});
});
}
/**
* Développer un menu
*/
expandMenu(menuId) {
const submenu = document.getElementById(menuId);
const parentLink = document.querySelector(`[href="#${menuId}"]`);
if (submenu && parentLink) {
submenu.classList.add('show');
parentLink.setAttribute('aria-expanded', 'true');
parentLink.classList.add('active');
const arrow = parentLink.querySelector('.nav-arrow');
if (arrow) arrow.style.transform = 'rotate(90deg)';
this.expandedMenus.add(menuId);
}
}
/**
* Réduire un menu
*/
collapseMenu(menuId) {
const submenu = document.getElementById(menuId);
const parentLink = document.querySelector(`[href="#${menuId}"]`);
if (submenu && parentLink) {
submenu.classList.remove('show');
parentLink.setAttribute('aria-expanded', 'false');
parentLink.classList.remove('active');
const arrow = parentLink.querySelector('.nav-arrow');
if (arrow) arrow.style.transform = 'rotate(0deg)';
this.expandedMenus.delete(menuId);
}
}
/**
* Réduire tous les menus sauf un
*/
collapseAllExcept(exceptMenuId) {
document.querySelectorAll('.nav-submenu.show').forEach(submenu => {
if (submenu.id !== exceptMenuId) {
this.collapseMenu(submenu.id);
}
});
}
/**
* Réduire tous les menus sauf l'actif
*/
collapseAllExceptActive() {
const activeMenuId = `submenu${this.activeMenuId}`;
this.collapseAllExcept(activeMenuId);
}
/**
* Gérer la navigation clavier dans les menus
*/
handleKeyboardMenuNavigation(e) {
const currentItem = e.target.closest('.nav-link');
if (!currentItem) return;
const items = Array.from(document.querySelectorAll('.nav-link'));
const currentIndex = items.indexOf(currentItem);
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (currentIndex < items.length - 1) {
items[currentIndex + 1].focus();
}
break;
case 'ArrowUp':
e.preventDefault();
if (currentIndex > 0) {
items[currentIndex - 1].focus();
}
break;
case 'ArrowRight':
e.preventDefault();
if (currentItem.hasAttribute('data-bs-toggle')) {
const menuId = currentItem.getAttribute('href').substring(1);
this.expandMenu(menuId);
}
break;
case 'ArrowLeft':
e.preventDefault();
if (currentItem.hasAttribute('data-bs-toggle')) {
const menuId = currentItem.getAttribute('href').substring(1);
this.collapseMenu(menuId);
}
break;
case 'Enter':
case ' ':
e.preventDefault();
if (currentItem.hasAttribute('data-bs-toggle')) {
const menuId = currentItem.getAttribute('href').substring(1);
if (this.expandedMenus.has(menuId)) {
this.collapseMenu(menuId);
} else {
this.expandMenu(menuId);
}
} else if (currentItem.href) {
window.location.href = currentItem.href;
}
break;
}
}
/**
* Sauvegarder l'état des menus
*/
saveMenuState() {
if (!UXConfig.saveState) return;
try {
const state = Array.from(this.expandedMenus);
localStorage.setItem('expandedMenus', JSON.stringify(state));
} catch (e) {
if (UXConfig.debug) console.warn('Impossible de sauvegarder l\'état des menus:', e);
}
}
/**
* Restaurer l'état des menus
*/
restoreMenuState(state) {
if (!Array.isArray(state)) return;
state.forEach(menuId => {
this.expandMenu(menuId);
});
}
}
// ============================================
// MODULE: Gestionnaire du Panneau Contexte
// ============================================
class ContextPanelManager {
constructor() {
this.panel = document.getElementById('contextPanel');
this.toggleButton = document.querySelector('.context-toggle');
this.proximityArea = document.querySelector('.proximity-hover-area');
this.isOpen = false;
this.init();
}
init() {
if (!this.panel || !this.toggleButton) {
console.warn('❌ Éléments du panneau contexte non trouvés');
return;
}
this.setupProximityDetection();
this.setupAnimations();
this.setupCloseBehavior();
}
/**
* Détection de proximité (style QuillBot)
*/
setupProximityDetection() {
if (!this.proximityArea) return;
let proximityTimeout;
// Détection de la souris
document.addEventListener('mousemove', (e) => {
if (this.isOpen) return;
const rect = this.proximityArea.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const distance = Math.sqrt(
Math.pow(e.clientX - centerX, 2) +
Math.pow(e.clientY - centerY, 2)
);
clearTimeout(proximityTimeout);
if (distance < UXConfig.proximityRadius) {
// Souris proche - montrer le bouton
this.showButton();
} else {
// Souris éloignée - cacher progressivement
proximityTimeout = setTimeout(() => {
this.hideButton();
}, 1000);
}
});
// Toujours montrer le bouton quand le panel est ouvert
this.panel.addEventListener('transitionend', () => {
if (this.isOpen) {
this.showButton();
}
});
}
/**
* Afficher le bouton contexte
*/
showButton() {
if (this.toggleButton) {
this.toggleButton.style.opacity = '0.8';
this.toggleButton.classList.add('active');
}
}
/**
* Cacher le bouton contexte
*/
hideButton() {
if (this.toggleButton && !this.isOpen) {
this.toggleButton.style.opacity = '0.2';
this.toggleButton.classList.remove('active');
}
}
/**
* Configurer les animations
*/
setupAnimations() {
// Observer pour animations d'entrée
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate__animated', 'animate__fadeInUp');
}
});
}, { threshold: 0.1 });
// Observer les sections du panneau
document.querySelectorAll('.context-section').forEach(section => {
observer.observe(section);
});
}
/**
* Configurer le comportement de fermeture
*/
setupCloseBehavior() {
// Fermer avec Échap
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isOpen) {
this.togglePanel();
}
});
// Fermer en cliquant à l'extérieur
document.addEventListener('click', (e) => {
if (this.isOpen &&
!this.panel.contains(e.target) &&
!this.toggleButton.contains(e.target)) {
this.togglePanel();
}
});
}
/**
* Basculer l'état du panneau
*/
togglePanel() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.openPanel();
} else {
this.closePanel();
}
// Mettre à jour les attributs ARIA
this.toggleButton.setAttribute('aria-expanded', this.isOpen);
// Sauvegarder l'état
if (UXConfig.saveState) {
localStorage.setItem('contextPanelOpen', this.isOpen.toString());
}
return this.isOpen;
}
/**
* Ouvrir le panneau
*/
openPanel() {
this.panel.classList.add('open');
this.toggleButton.classList.add('panel-open');
// Focus sur le premier élément focusable
setTimeout(() => {
const firstFocusable = this.panel.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (firstFocusable) firstFocusable.focus();
}, UXConfig.transitionSpeed);
// Émettre un événement personnalisé
this.dispatchEvent('contextpanel:open');
}
/**
* Fermer le panneau
*/
closePanel() {
this.panel.classList.remove('open');
this.toggleButton.classList.remove('panel-open');
// Retourner le focus au bouton toggle
this.toggleButton.focus();
// Cacher progressivement le bouton
setTimeout(() => {
if (!this.isOpen) this.hideButton();
}, 1000);
// Émettre un événement personnalisé
this.dispatchEvent('contextpanel:close');
}
/**
* Ouvrir la modale photo
*/
openPhotoModal() {
const modalElement = document.getElementById('photoModal');
if (modalElement) {
const modal = new bootstrap.Modal(modalElement);
modal.show();
}
}
/**
* Émettre un événement personnalisé
*/
dispatchEvent(eventName, detail = {}) {
const event = new CustomEvent(eventName, {
detail: { ...detail, timestamp: Date.now() }
});
document.dispatchEvent(event);
}
}
// ============================================
// MODULE: Gestionnaire de Notifications
// ============================================
class NotificationManager {
constructor() {
this.badge = document.getElementById('notificationCount');
this.lastCount = 0;
this.unreadMessages = [];
this.init();
}
init() {
this.setupBadgeAnimation();
this.setupMessagePolling();
}
/**
* Configurer l'animation du badge
*/
setupBadgeAnimation() {
if (!this.badge) return;
// Animation de pulse pour nouveaux messages
this.badge.addEventListener('animationend', () => {
this.badge.classList.remove('pulse');
});
}
/**
* Configurer le polling des messages
*/
setupMessagePolling() {
// Poll initial
this.pollNotifications();
// Écouter les événements de nouvelle notification
document.addEventListener('newnotification', (e) => {
this.handleNewNotification(e.detail);
});
}
/**
* Poller les notifications
*/
async pollNotifications() {
try {
// Remplacer par votre appel API réel
const response = await this.fetchNotifications();
const data = await response.json();
this.updateBadge(data.count);
if (data.newMessages > 0) {
this.showNewNotificationAlert(data.newMessages);
this.unreadMessages = data.messages || [];
}
} catch (error) {
if (UXConfig.debug) console.warn('Erreur polling notifications:', error);
}
}
/**
* Récupérer les notifications
*/
async fetchNotifications() {
// Simuler une API - remplacer par votre endpoint réel
return {
ok: true,
json: async () => ({
count: Math.floor(Math.random() * 10),
newMessages: Math.random() > 0.7 ? 1 : 0,
messages: []
})
};
// En production, utiliser :
// return fetch('api/notifications/count', {
// headers: { 'X-Requested-With': 'XMLHttpRequest' }
// });
}
/**
* Mettre à jour le badge
*/
updateBadge(count) {
if (!this.badge) return;
const hasNew = count > this.lastCount;
this.lastCount = count;
this.badge.textContent = count;
this.badge.style.display = count > 0 ? 'flex' : 'none';
if (hasNew && count > 0) {
this.badge.classList.add('pulse');
// Notification système si autorisée
if (Notification.permission === 'granted' && document.hidden) {
this.showSystemNotification(count);
}
}
}
/**
* Afficher une alerte pour nouvelles notifications
*/
showNewNotificationAlert(count) {
// Créer un toast Bootstrap
const toastHTML = `
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1060">
<div id="notificationToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header bg-primary text-white">
<i class="bi bi-bell me-2"></i>
<strong class="me-auto">Nouvelle notification</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Fermer"></button>
</div>
<div class="toast-body">
${count} nouvelle${count > 1 ? 's' : ''} notification${count > 1 ? 's' : ''} reçue${count > 1 ? 's' : ''}
</div>
</div>
</div>
`;
const container = document.createElement('div');
container.innerHTML = toastHTML;
document.body.appendChild(container.firstElementChild);
const toastElement = document.getElementById('notificationToast');
if (toastElement) {
const toast = new bootstrap.Toast(toastElement, { autohide: true, delay: 5000 });
toast.show();
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
}
/**
* Afficher une notification système
*/
showSystemNotification(count) {
const notification = new Notification('Inter Santé', {
body: `${count} nouvelle${count > 1 ? 's' : ''} notification${count > 1 ? 's' : ''}`,
icon: 'Bootstrap_new/images/new/favicon.png',
tag: 'notification'
});
notification.onclick = () => {
window.focus();
this.showMessagesModal();
notification.close();
};
setTimeout(() => notification.close(), 5000);
}
/**
* Demander la permission pour les notifications
*/
async requestNotificationPermission() {
if (!('Notification' in window)) {
console.log('Notifications non supportées');
return false;
}
if (Notification.permission === 'granted') {
return true;
}
if (Notification.permission !== 'denied') {
const permission = await Notification.requestPermission();
return permission === 'granted';
}
return false;
}
/**
* Afficher la modale des messages
*/
showMessagesModal() {
const modalElement = document.getElementById('messagesModal');
if (!modalElement) return;
const modal = new bootstrap.Modal(modalElement);
// Charger les messages
this.loadMessages().then(messages => {
const container = document.getElementById('div_messagerie');
if (container) {
container.innerHTML = this.renderMessages(messages);
}
});
modal.show();
// Réinitialiser le compteur
this.updateBadge(0);
}
/**
* Charger les messages
*/
async loadMessages() {
try {
// Remplacer par votre appel API réel
const response = await fetch('api/messages/unread');
return await response.json();
} catch (error) {
console.error('Erreur chargement messages:', error);
return [];
}
}
/**
* Rendre les messages en HTML
*/
renderMessages(messages) {
if (!messages.length) {
return '<div class="text-center text-muted py-5">Aucun message non lu</div>';
}
return `
<div class="message-list">
${messages.map(msg => `
<div class="message-item p-3 border-bottom">
<div class="d-flex justify-content-between align-items-start mb-2">
<strong>${msg.sender}</strong>
<small class="text-muted">${this.formatDate(msg.date)}</small>
</div>
<div class="message-content">${msg.content}</div>
</div>
`).join('')}
</div>
`;
}
/**
* Formater une date
*/
formatDate(dateString) {
const date = new Date(dateString);
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
};
return date.toLocaleDateString(window.appConfig.isAnglophone ? 'en-US' : 'fr-FR', options);
}
/**
* Gérer une nouvelle notification
*/
handleNewNotification(detail) {
this.updateBadge(this.lastCount + 1);
this.showNewNotificationAlert(1);
// Ajouter au tableau des messages non lus
if (detail.message) {
this.unreadMessages.unshift(detail.message);
}
}
}
// ============================================
// MODULE: Gestionnaire d'Accessibilité
// ============================================
class AccessibilityManager {
constructor() {
this.keyboardNavigation = false;
this.init();
}
init() {
this.setupKeyboardNavigationDetection();
this.setupFocusManagement();
this.setupSkipLinks();
this.setupAriaLiveRegions();
}
/**
* Détecter la navigation clavier
*/
setupKeyboardNavigationDetection() {
document.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
this.keyboardNavigation = true;
document.body.classList.add('keyboard-navigation');
}
});
document.addEventListener('mousedown', () => {
this.keyboardNavigation = false;
document.body.classList.remove('keyboard-navigation');
});
}
/**
* Gérer le focus
*/
setupFocusManagement() {
// Garder le focus dans les modales
document.addEventListener('focusin', (e) => {
const modal = e.target.closest('.modal');
if (modal && modal.classList.contains('show')) {
this.trapFocus(modal);
}
});
// Focus sur le contenu principal après navigation
document.addEventListener('pjax:success', () => {
this.focusMainContent();
});
}
/**
* Piéger le focus dans un élément
*/
trapFocus(element) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length === 0) return;
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
element.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
});
}
/**
* Focus sur le contenu principal
*/
focusMainContent() {
const mainContent = document.getElementById('mainContent');
if (mainContent) {
mainContent.setAttribute('tabindex', '-1');
mainContent.focus();
setTimeout(() => {
mainContent.removeAttribute('tabindex');
}, 100);
}
}
/**
* Configurer les liens de saut
*/
setupSkipLinks() {
// Ajouter un lien pour sauter au contenu principal
const skipLink = document.createElement('a');
skipLink.href = '#mainContent';
skipLink.className = 'skip-link visually-hidden';
skipLink.textContent = 'Aller au contenu principal';
skipLink.addEventListener('click', (e) => {
e.preventDefault();
this.focusMainContent();
});
document.body.insertBefore(skipLink, document.body.firstChild);
}
/**
* Configurer les régions ARIA Live
*/
setupAriaLiveRegions() {
// Région pour les notifications
const notificationRegion = document.createElement('div');
notificationRegion.setAttribute('aria-live', 'polite');
notificationRegion.setAttribute('aria-atomic', 'true');
notificationRegion.className = 'visually-hidden';
notificationRegion.id = 'aria-live-notifications';
document.body.appendChild(notificationRegion);
}
/**
* Annoncer un message pour les lecteurs d'écran
*/
announce(message, priority = 'polite') {
const region = document.getElementById('aria-live-notifications');
if (region) {
region.textContent = '';
setTimeout(() => {
region.textContent = message;
}, 100);
}
}
}
// ============================================
// MODULE: Gestionnaire de Performance
// ============================================
class PerformanceManager {
constructor() {
this.metrics = {
fcp: null,
lcp: null,
fid: null,
cls: 0
};
this.init();
}
init() {
this.setupPerformanceMonitoring();
this.setupLazyLoading();
this.setupConnectionAwareLoading();
this.setupServiceWorker();
}
/**
* Surveiller les métriques de performance
*/
setupPerformanceMonitoring() {
if ('PerformanceObserver' in window) {
// Core Web Vitals
this.observeLCP();
this.observeFID();
this.observeCLS();
}
// First Contentful Paint
if ('PerformancePaintTiming' in performance) {
performance.getEntriesByType('paint').forEach(entry => {
if (entry.name === 'first-contentful-paint') {
this.metrics.fcp = entry.startTime;
}
});
}
}
/**
* Observer Largest Contentful Paint
*/
observeLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.lcp = lastEntry.renderTime || lastEntry.loadTime;
if (UXConfig.debug) {
console.log('LCP:', this.metrics.lcp);
}
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
}
/**
* Observer First Input Delay
*/
observeFID() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
this.metrics.fid = entry.processingStart - entry.startTime;
if (UXConfig.debug) {
console.log('FID:', this.metrics.fid);
}
});
});
observer.observe({ entryTypes: ['first-input'] });
}
/**
* Observer Cumulative Layout Shift
*/
observeCLS() {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (!entry.hadRecentInput) {
this.metrics.cls += entry.value;
}
if (UXConfig.debug) {
console.log('CLS:', this.metrics.cls);
}
});
});
observer.observe({ entryTypes: ['layout-shift'] });
}
/**
* Configurer le lazy loading
*/
setupLazyLoading() {
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
observer.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
}
/**
* Chargement adaptatif selon la connexion
*/
setupConnectionAwareLoading() {
if ('connection' in navigator) {
const connection = navigator.connection;
// Adapter la qualité des images
if (connection.saveData === true || connection.effectiveType.includes('2g')) {
this.enableDataSavingMode();
}
// Prévenir l'utilisateur si connexion lente
connection.addEventListener('change', () => {
if (connection.effectiveType.includes('2g') || connection.effectiveType.includes('3g')) {
this.showConnectionWarning();
}
});
}
}
/**
* Activer le mode économie de données
*/
enableDataSavingMode() {
// Réduire la qualité des images
document.querySelectorAll('img[data-src-low]').forEach(img => {
img.dataset.src = img.dataset.srcLow;
});
// Désactiver les animations non essentielles
document.body.classList.add('data-saving-mode');
}
/**
* Afficher un avertissement de connexion lente
*/
showConnectionWarning() {
const warning = document.createElement('div');
warning.className = 'connection-warning alert alert-warning alert-dismissible fade show';
warning.innerHTML = `
<i class="bi bi-wifi-off me-2"></i>
<span>Connexion lente détectée. Certaines fonctionnalités peuvent être limitées.</span>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Fermer"></button>
`;
document.body.prepend(warning);
// Auto-remove after 10 seconds
setTimeout(() => {
warning.remove();
}, 10000);
}
/**
* Configurer le Service Worker
*/
setupServiceWorker() {
if ('serviceWorker' in navigator && window.location.protocol === 'https:') {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
if (UXConfig.debug) {
console.log('Service Worker enregistré:', registration.scope);
}
})
.catch(error => {
if (UXConfig.debug) {
console.log('Échec enregistrement Service Worker:', error);
}
});
});
}
}
/**
* Optimiser les animations pour la performance
*/
optimizeAnimations() {
// Utiliser will-change pour les éléments animés
document.querySelectorAll('.nav-link, .action-btn, .context-toggle').forEach(el => {
el.style.willChange = 'transform, opacity';
});
}
}
// ============================================
// EXPORT ET INITIALISATION GLOBALE
// ============================================
// Exposer les fonctions globales
window.appUX = {
toggleContextPanel: () => window.uxManager?.toggleContextPanel(),
openPhotoModal: () => window.uxManager?.openPhotoModal(),
toggleSidebar: () => window.uxManager?.toggleSidebar()
};
window.appNotifications = {
showMessagesModal: () => window.uxManager?.modules.notifications?.showMessagesModal(),
requestNotificationPermission: () => window.uxManager?.modules.notifications?.requestNotificationPermission()
};
window.appLanguage = {
changeLanguage: () => {
Swal.fire({
title: 'Changer de langue',
text: 'Sélectionnez la langue de l\'interface :',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Français',
cancelButtonText: 'English',
reverseButtons: true,
customClass: { popup: 'animate__animated animate__fadeIn' }
}).then((result) => {
if (result.isConfirmed) {
window.location.href = '?lang=fr_FR';
} else if (result.dismiss === Swal.DismissReason.cancel) {
window.location.href = '?lang=en_US';
}
});
}
};
// Initialiser au chargement du DOM
document.addEventListener('DOMContentLoaded', () => {
// Attendre que les ressources critiques soient chargées
if (document.readyState === 'complete') {
window.uxManager = new UXManager();
} else {
window.addEventListener('load', () => {
window.uxManager = new UXManager();
});
}
// Initialiser les composants Bootstrap
initializeBootstrapComponents();
});
/**
* Initialiser les composants Bootstrap
*/
function initializeBootstrapComponents() {
// Tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(tooltipTriggerEl => {
return new bootstrap.Tooltip(tooltipTriggerEl, {
trigger: 'hover focus',
animation: true
});
});
// Select2
if (typeof $.fn.select2 === 'function') {
$('.select2').select2({
theme: 'bootstrap-5',
width: '100%',
placeholder: 'Sélectionnez...',
allowClear: true,
dropdownParent: $('body')
});
}
// DataTables
if (typeof $.fn.DataTable === 'function') {
$('.datatable').DataTable({
responsive: true,
language: {
url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/fr-FR.json'
},
dom: '<"row"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>' +
'<"row"<"col-sm-12"tr>>' +
'<"row"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>',
pageLength: 25,
stateSave: true,
stateDuration: 60 * 60 * 24 // 24 heures
});
}
}
// Exporter pour les tests
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
UXManager,
NavigationManager,
ContextPanelManager,
NotificationManager,
AccessibilityManager,
PerformanceManager
};
}