rhsaas/Bootstrap_new/js/ux-manager.js

1420 lines
48 KiB
JavaScript
Executable File

/**
* UX Manager - Gestionnaire d'expérience utilisateur modulaire
* Version: 3.0.0 - Corrigée et Optimisée
* Description: Gère tous les aspects de l'UX : navigation, panels contextuels, notifications, accessibilité
* Auteur: Portail RH Inter Santé
* Date: 2025.12.22 - Version finale
*/
// ============================================================================
// CLASSE PRINCIPALE - UXManager
// ============================================================================
/**
* Classe principale qui orchestre tous les gestionnaires d'expérience utilisateur
* Elle initialise et coordonne les différents sous-managers
*/
class UXManager {
/**
* Constructeur principal - Initialise tous les sous-managers
*/
constructor() {
// Initialisation des sous-managers
this.navigation = new NavigationManager();
this.contextPanel = new ContextPanelManager();
this.notifications = new NotificationManager();
this.accessibility = new AccessibilityManager();
this.performance = new PerformanceManager();
this.language = new LanguageManager();
// Lancement de l'initialisation
this.init();
}
/**
* Méthode d'initialisation principale
* Appelée automatiquement par le constructeur
*/
init() {
//console.log('[UX Manager] Initialisation du système d\'expérience utilisateur...');
// Initialisation séquentielle des sous-managers
this.navigation.init();
this.contextPanel.init();
this.notifications.init();
this.accessibility.init();
this.performance.init();
this.language.init();
// Gestion de session utilisateur
this.checkSession();
// Initialisation du Service Worker
this.initServiceWorker();
//console.log('[UX Manager] Initialisation terminée avec succès');
}
/**
* Basculer l'état du panneau contextuel
* @public - Méthode accessible globalement
*/
toggleContextPanel() {
this.contextPanel.toggle();
}
/**
* Vérifier et gérer l'expiration de session utilisateur
* @private - Méthode interne de gestion de session
*/
checkSession() {
// Récupérer la durée de session depuis le DOM
const dureeSession = parseInt(document.getElementById('dureeSession')?.value) || 30;
const dureeMinutes = dureeSession * 60 * 1000; // Conversion en millisecondes
// Vérifier régulièrement l'activité de l'utilisateur
setInterval(() => {
const derniereAction = sessionStorage.getItem('derniere_action');
const maintenant = Date.now();
// Si inactivité dépassant la durée autorisée
if (derniereAction && (maintenant - derniereAction > dureeMinutes)) {
this.showSessionWarning();
}
}, 60000); // Vérification toutes les minutes
}
/**
* Afficher un avertissement d'expiration de session
* @private - Méthode d'affichage de notification
*/
showSessionWarning() {
const isAnglophone = window.appConfig?.isAnglophone || false;
const messages = {
title: isAnglophone ? 'Session Warning' : 'Avertissement de session',
text: isAnglophone
? 'Your session will expire soon. Do you want to extend it?'
: 'Votre session va bientôt expirer. Souhaitez-vous la prolonger?',
confirm: isAnglophone ? 'Extend' : 'Prolonger',
cancel: isAnglophone ? 'Logout' : 'Déconnexion'
};
// Utiliser SweetAlert pour une notification élégante
Swal.fire({
title: messages.title,
text: messages.text,
icon: 'warning',
showCancelButton: true,
confirmButtonText: messages.confirm,
cancelButtonText: messages.cancel,
allowOutsideClick: false,
allowEscapeKey: false
}).then((result) => {
if (result.isConfirmed) {
// Prolonger la session
sessionStorage.setItem('derniere_action', Date.now());
this.accessibility.announce(
isAnglophone ? 'Session extended' : 'Session prolongée'
);
} else {
// Déconnexion
window.location.href = window.appConfig?.racineWeb + 'Connexion/deconnecter';
}
});
}
/**
* Initialiser le Service Worker pour le mode hors-ligne
* @private - Méthode d'initialisation PWA
*/
initServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
//console.log('[UX Manager] Service Worker actif:', registration.scope);
}).catch(error => {
//console.warn('[UX Manager] Service Worker non disponible:', error);
});
}
}
}
// ============================================================================
// SOUS-CLASSE - NavigationManager (GESTION DES MENUS)
// ============================================================================
/**
* Gestionnaire spécialisé pour la navigation et les menus
* Contrôle l'ouverture/fermeture des menus dépliables
*/
class NavigationManager {
/**
* Constructeur - Initialise l'état de navigation
*/
constructor() {
this.currentOpenMenu = null; // Menu actuellement ouvert
this.activeMenuId = null; // Menu actif basé sur la page courante
this.isInitialized = false; // État d'initialisation
}
/**
* Initialiser le système de navigation
* @public - Méthode appelée par UXManager
*/
init() {
if (this.isInitialized) return;
// console.log('[Navigation] Initialisation du système de menus...');
// Appliquer le correctif d'urgence pour les menus multiples ouverts
this.applyEmergencyFix();
// Configuration des comportements
this.setupActiveMenu();
this.setupMenuBehavior();
this.setupKeyboardNavigation();
// Vérification finale après initialisation
setTimeout(() => {
this.validateMenuConsistency();
}, 300);
this.isInitialized = true;
//console.log('[Navigation] Système de menus initialisé');
}
/**
* Correctif d'urgence - Force un seul menu ouvert au chargement
* @private - Méthode de correction initiale
*/
applyEmergencyFix() {
// Détecter le menu actif depuis la configuration
const activeParentId = window.appConfig?.activeParentId;
if (activeParentId !== null && activeParentId !== '') {
this.activeMenuId = `submenu${activeParentId}`;
//console.log('[Navigation] Correctif: Menu actif détecté:', this.activeMenuId);
// Forcer la fermeture de tous les autres menus
this.forceSingleMenu();
}
}
/**
* Forcer l'ouverture d'un seul menu (celui actif)
* @private - Méthode de correction forcée
*/
forceSingleMenu() {
if (!this.activeMenuId) return;
//console.log('[Navigation] Forçage: Un seul menu ouvert ->', this.activeMenuId);
// Étape 1: Fermer TOUS les menus
document.querySelectorAll('.nav-submenu').forEach(menu => {
if (menu.id !== this.activeMenuId) {
this.closeMenu(menu.id, true); // true = force close
}
});
// Étape 2: Ouvrir le menu actif
setTimeout(() => {
this.openMenu(this.activeMenuId);
this.currentOpenMenu = this.activeMenuId;
}, 50);
}
/**
* Configurer le menu actif basé sur la page courante
* @private - Méthode de configuration initiale
*/
setupActiveMenu() {
const activeParentId = window.appConfig?.activeParentId;
const activeLink = window.appConfig?.activeLink;
//console.log('[Navigation] Configuration:', { activeParentId, activeLink });
// CORRECTION : Si "Accueil" mais activeParentId != 0, corriger
if (activeLink === 'Accueil' && activeParentId !== '0') {
//console.warn('[Navigation] Correction: Accueil devrait être menu 0, pas', activeParentId);
this.activeMenuId = 'submenu0';
// Mettre à jour la config
if (window.appConfig) {
window.appConfig.activeParentId = '0';
}
}
// Cas normal
else if (activeParentId !== null && activeParentId !== '') {
this.activeMenuId = `submenu${activeParentId}`;
} else {
this.activeMenuId = null;
}
//console.log('[Navigation] Menu actif final:', this.activeMenuId);
// Appliquer
if (this.activeMenuId) {
setTimeout(() => {
this.openMenu(this.activeMenuId);
this.currentOpenMenu = this.activeMenuId;
}, 100);
}
}
/**
* Configurer les comportements interactifs des menus
* @private - Méthode d'attachement des événements
*/
setupMenuBehavior() {
// Pour chaque lien de menu avec sous-menu (dépliable)
document.querySelectorAll('.nav-link[data-bs-toggle="collapse"]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const targetId = link.getAttribute('href').substring(1); // Enlever le #
//console.log('[Navigation] Clic détecté sur menu:', targetId);
// Logique de basculement intelligent
if (this.currentOpenMenu === targetId) {
// Si on clique sur le menu déjà ouvert, on le ferme
this.closeMenu(targetId);
this.currentOpenMenu = null;
} else {
// Fermer le menu précédent si différent
if (this.currentOpenMenu && this.currentOpenMenu !== targetId) {
this.closeMenu(this.currentOpenMenu);
}
// Ouvrir le nouveau menu
this.openMenu(targetId);
this.currentOpenMenu = targetId;
}
});
});
// Fermer les menus en cliquant en dehors de la sidebar
document.addEventListener('click', (e) => {
if (!e.target.closest('.app-sidebar')) {
// Ne pas fermer le menu actif s'il correspond à la page courante
if (this.currentOpenMenu !== this.activeMenuId) {
this.closeAllMenus();
}
}
});
// Empêcher la fermeture accidentelle en cliquant dans le sous-menu
document.querySelectorAll('.nav-submenu').forEach(menu => {
menu.addEventListener('click', (e) => {
e.stopPropagation();
});
});
}
/**
* Ouvrir un menu spécifique
* @param {string} menuId - ID du menu à ouvrir
* @private - Méthode d'ouverture de menu
*/
openMenu(menuId) {
const menu = document.getElementById(menuId);
const link = document.querySelector(`[href="#${menuId}"]`);
if (menu && link) {
// Vérifier si le menu est déjà ouvert
if (menu.classList.contains('show')) {
//console.log('[Navigation] Menu déjà ouvert:', menuId);
return;
}
//console.log('[Navigation] Ouverture du menu:', menuId);
// 1. Fermer tous les autres menus
document.querySelectorAll('.nav-submenu.show').forEach(otherMenu => {
if (otherMenu.id !== menuId) {
otherMenu.classList.remove('show');
const otherLink = document.querySelector(`[href="#${otherMenu.id}"]`);
if (otherLink) {
otherLink.setAttribute('aria-expanded', 'false');
otherLink.classList.remove('active');
const arrow = otherLink.querySelector('.nav-arrow');
if (arrow) arrow.style.transform = 'rotate(0deg)';
}
}
});
// 2. Ouvrir le menu sélectionné avec animations
menu.classList.add('show');
link.setAttribute('aria-expanded', 'true');
link.classList.add('active');
// 3. Animer la flèche
const arrow = link.querySelector('.nav-arrow');
if (arrow) arrow.style.transform = 'rotate(90deg)';
// 4. Annoncer pour l'accessibilité
const menuName = link.querySelector('.nav-text')?.textContent || 'Menu';
window.appUX?.accessibility.announce(`${menuName} ouvert`);
}
}
/**
* Fermer un menu spécifique
* @param {string} menuId - ID du menu à fermer
* @param {boolean} force - Forcer la fermeture même pour le menu actif
* @private - Méthode de fermeture de menu
*/
closeMenu(menuId, force = false) {
const menu = document.getElementById(menuId);
const link = document.querySelector(`[href="#${menuId}"]`);
if (menu && link) {
// Ne pas fermer le menu actif s'il correspond à la page courante (sauf si forcé)
if (!force && menuId === this.activeMenuId) {
//console.log('[Navigation] Menu actif, fermeture bloquée:', menuId);
return;
}
//console.log('[Navigation] Fermeture du menu:', menuId);
// Appliquer les changements visuels
menu.classList.remove('show');
link.setAttribute('aria-expanded', 'false');
link.classList.remove('active');
// Réinitialiser la flèche
const arrow = link.querySelector('.nav-arrow');
if (arrow) arrow.style.transform = 'rotate(0deg)';
}
}
/**
* Fermer tous les menus (sauf le menu actif)
* @public - Méthode accessible globalement
*/
closeAllMenus() {
//console.log('[Navigation] Fermeture de tous les menus (sauf actif)');
document.querySelectorAll('.nav-submenu.show').forEach(menu => {
if (menu.id !== this.activeMenuId) {
this.closeMenu(menu.id);
}
});
// Réinitialiser l'état si aucun menu actif
if (!this.activeMenuId) {
this.currentOpenMenu = null;
}
}
/**
* Fermer tous les menus sauf celui actif
* @public - Alias pour closeAllMenus avec nom plus explicite
*/
closeAllExceptActive() {
this.closeAllMenus();
}
/**
* Configurer la navigation au clavier
* @private - Méthode d'accessibilité clavier
*/
setupKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
// Échap: fermer tous les menus
if (e.key === 'Escape') {
this.closeAllMenus();
}
// Flèches: navigation dans les menus
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
const focused = document.activeElement;
if (focused.classList.contains('nav-link')) {
e.preventDefault();
this.navigateMenu(focused, e.key);
}
}
});
}
/**
* Naviguer dans les menus avec les flèches
* @param {HTMLElement} currentElement - Élément actuellement focus
* @param {string} direction - Direction de navigation ('ArrowDown' ou 'ArrowUp')
* @private - Méthode de navigation clavier
*/
navigateMenu(currentElement, direction) {
const allLinks = Array.from(document.querySelectorAll('.nav-link'));
const currentIndex = allLinks.indexOf(currentElement);
if (direction === 'ArrowDown' && currentIndex < allLinks.length - 1) {
allLinks[currentIndex + 1].focus();
} else if (direction === 'ArrowUp' && currentIndex > 0) {
allLinks[currentIndex - 1].focus();
}
}
/**
* Vérifier la cohérence des menus
* @private - Méthode de validation
*/
validateMenuConsistency() {
//console.log('[Navigation] Vérification de cohérence...');
// 1. Vérifier que le menu actif est ouvert
if (this.activeMenuId) {
const activeMenu = document.getElementById(this.activeMenuId);
if (activeMenu && !activeMenu.classList.contains('show')) {
//console.warn('[Navigation] Menu actif non ouvert, correction...');
this.openMenu(this.activeMenuId);
}
}
// 2. Vérifier qu'un seul menu est ouvert
const openMenus = document.querySelectorAll('.nav-submenu.show');
if (openMenus.length > 1) {
//console.warn(`[Navigation] ${openMenus.length} menus ouverts, correction...`);
this.forceSingleMenu();
}
// 3. Vérifier la correspondance page/menu
this.validatePageMenuMatch();
}
/**
* Valider la correspondance entre la page et le menu ouvert
* @private - Méthode de validation avancée
*/
validatePageMenuMatch() {
const activeLink = window.appConfig?.activeLink;
if (!activeLink) return;
// Chercher si la page active est dans le menu ouvert
const openMenu = document.querySelector('.nav-submenu.show');
if (openMenu) {
const hasActiveLink = openMenu.querySelector(`a[href*="${activeLink}"]`);
if (!hasActiveLink) {
//console.warn(`[Navigation] Page "${activeLink}" non trouvée dans le menu ouvert ${openMenu.id}`);
// Essayer de trouver le bon menu
this.autoDetectCorrectMenu();
}
}
}
/**
* Détecter automatiquement le menu correct
* @private - Méthode d'auto-détection
*/
autoDetectCorrectMenu() {
const activeLink = window.appConfig?.activeLink;
if (!activeLink) return;
//console.log(`[Navigation] Auto-détection du menu pour "${activeLink}"...`);
// Chercher dans tous les menus
document.querySelectorAll('.nav-submenu').forEach(menu => {
const link = menu.querySelector(`a[href*="${activeLink}"]`);
if (link) {
const correctMenuId = menu.id;
//console.log(`[Navigation] Trouvé dans ${correctMenuId}`);
// Mettre à jour
this.activeMenuId = correctMenuId;
if (window.appConfig) {
const menuNum = correctMenuId.replace('submenu', '');
window.appConfig.activeParentId = menuNum;
}
// Ouvrir le bon menu
setTimeout(() => {
this.openMenu(correctMenuId);
this.currentOpenMenu = correctMenuId;
}, 100);
}
});
}
/**
* Ouvrir un menu par son ID (méthode publique)
* @param {string} menuId - ID du menu à ouvrir
* @public - Méthode accessible globalement
*/
openMenuById(menuId) {
this.openMenu(menuId);
this.currentOpenMenu = menuId;
}
}
// ============================================================================
// SOUS-CLASSE - ContextPanelManager (PANEL CONTEXTUEL)
// ============================================================================
/**
* Gestionnaire du panneau contextuel latéral
* Contrôle l'affichage et les interactions du panel d'outils
*/
class ContextPanelManager {
/**
* Constructeur - Initialise les références DOM
*/
constructor() {
this.panel = document.getElementById('contextPanel');
this.toggleButton = document.querySelector('.context-toggle');
this.proximityArea = document.querySelector('.proximity-hover-area');
this.isOpen = false;
this.isInitialized = false;
}
/**
* Initialiser le panel contextuel
* @public - Méthode appelée par UXManager
*/
init() {
if (this.isInitialized || !this.panel) return;
//console.log('[Context Panel] Initialisation...');
this.setupProximityDetection();
this.setupKeyboardControls();
this.isInitialized = true;
}
/**
* Configurer la détection de proximité (hover)
* @private - Méthode d'interaction au survol
*/
setupProximityDetection() {
if (!this.proximityArea || !this.toggleButton) return;
// Animation au survol de la zone de proximité
this.proximityArea.addEventListener('mouseenter', () => {
this.toggleButton.style.opacity = '1';
this.toggleButton.style.transform = 'scale(1.1)';
});
this.proximityArea.addEventListener('mouseleave', () => {
if (!this.isOpen) {
this.toggleButton.style.opacity = '0.2';
this.toggleButton.style.transform = 'scale(1)';
}
});
// Opacité initiale
this.toggleButton.style.opacity = '0.2';
this.toggleButton.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
}
/**
* Configurer les contrôles clavier
* @private - Méthode d'accessibilité clavier
*/
setupKeyboardControls() {
document.addEventListener('keydown', (e) => {
// Ctrl+Shift+C : basculer le panel
if (e.ctrlKey && e.shiftKey && e.key === 'C') {
e.preventDefault();
this.toggle();
}
// Échap : fermer le panel si ouvert
if (e.key === 'Escape' && this.isOpen) {
this.close();
}
});
}
/**
* Basculer l'état du panel (ouvrir/fermer)
* @public - Méthode accessible globalement
*/
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
/**
* Ouvrir le panel contextuel
* @private - Méthode d'ouverture
*/
open() {
this.panel.classList.add('open');
this.toggleButton.setAttribute('aria-expanded', 'true');
this.toggleButton.style.opacity = '1';
this.isOpen = true;
// Focus sur le bouton de fermeture pour l'accessibilité
setTimeout(() => {
const closeBtn = this.panel.querySelector('.context-close');
if (closeBtn) closeBtn.focus();
}, 300);
//console.log('[Context Panel] Ouvert');
window.appUX?.accessibility.announce('Panneau contextuel ouvert');
}
/**
* Fermer le panel contextuel
* @private - Méthode de fermeture
*/
close() {
this.panel.classList.remove('open');
this.toggleButton.setAttribute('aria-expanded', 'false');
this.toggleButton.style.opacity = '0.2';
this.isOpen = false;
// Retourner le focus au bouton toggle
this.toggleButton.focus();
//console.log('[Context Panel] Fermé');
window.appUX?.accessibility.announce('Panneau contextuel fermé');
}
}
// ============================================================================
// SOUS-CLASSE - NotificationManager (GESTION DES NOTIFICATIONS)
// ============================================================================
/**
* Gestionnaire des notifications et messages système
*/
class NotificationManager {
/**
* Constructeur - Initialise le compteur de notifications
*/
constructor() {
this.countElement = document.getElementById('notificationCount');
this.unreadCount = 0;
this.pollingInterval = null;
}
/**
* Initialiser le système de notifications
* @public - Méthode appelée par UXManager
*/
init() {
//console.log('[Notifications] Initialisation...');
this.loadNotifications();
this.setupPolling();
}
/**
* Charger les notifications depuis le serveur
* @private - Méthode de chargement initial
*/
loadNotifications() {
// Simulation - À remplacer par un appel API réel
const mockCount = 3; // Exemple : 3 notifications non lues
this.updateCount(mockCount);
//console.log('[Notifications] Chargement simulé:', mockCount, 'notification(s)');
}
/**
* Mettre à jour le compteur de notifications
* @param {number} count - Nouveau nombre de notifications
* @private - Méthode de mise à jour d'affichage
*/
updateCount(count) {
this.unreadCount = count;
if (this.countElement) {
// Mettre à jour l'élément visuel
this.countElement.textContent = count;
this.countElement.style.display = count > 0 ? 'flex' : 'none';
// Mettre à jour le titre de la page
const baseTitle = document.title.replace(/^\(\d+\)\s*/, '');
document.title = count > 0 ? `(${count}) ${baseTitle}` : baseTitle;
// Annoncer pour l'accessibilité si nouveau message
if (count > 0) {
const message = window.appConfig?.isAnglophone
? `${count} new notification${count > 1 ? 's' : ''}`
: `${count} nouvelle${count > 1 ? 's' : ''} notification${count > 1 ? 's' : ''}`;
window.appUX?.accessibility.announce(message);
}
}
}
/**
* Configurer le polling pour les nouvelles notifications
* @private - Méthode de vérification périodique
*/
setupPolling() {
// Vérifier les nouvelles notifications toutes les 30 secondes
this.pollingInterval = setInterval(() => {
this.checkNewNotifications();
}, 30000);
}
/**
* Vérifier les nouvelles notifications (méthode à implémenter)
* @private - Méthode d'appel API
*/
checkNewNotifications() {
// À implémenter : appel AJAX pour vérifier les nouvelles notifications
// console.log('[Notifications] Vérification des nouvelles notifications...');
}
/**
* Afficher la modale des messages
* @public - Méthode accessible globalement
*/
showMessagesModal() {
const modalElement = document.getElementById('messagesModal');
if (!modalElement) {
//console.error('[Notifications] Modale des messages non trouvée');
return;
}
const modal = new bootstrap.Modal(modalElement);
modal.show();
// Charger les messages
this.loadMessages();
//console.log('[Notifications] Affichage de la modale des messages');
}
/**
* Charger les messages dans la modale
* @private - Méthode de chargement de contenu
*/
loadMessages() {
const container = document.getElementById('div_messagerie');
if (!container) return;
// Simulation de chargement - À remplacer par un appel API réel
const messages = window.appConfig?.isAnglophone
? [
{ title: 'New prescription', time: '5 minutes ago', content: 'A new prescription has been created for the beneficiary.' },
{ title: 'Exemption approved', time: '2 hours ago', content: 'Your exemption request has been approved.' }
]
: [
{ title: 'Nouvelle prescription', time: 'Il y a 5 minutes', content: 'Une nouvelle prescription a été créée pour le bénéficiaire.' },
{ title: 'Dérogation approuvée', time: 'Il y a 2 heures', content: 'Votre demande de dérogation a été approuvée.' }
];
// Générer le HTML des messages
let messagesHTML = '<div class="list-group">';
messages.forEach(msg => {
messagesHTML += `
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">${msg.title}</h6>
<small>${msg.time}</small>
</div>
<p class="mb-1">${msg.content}</p>
</div>
`;
});
messagesHTML += '</div>';
container.innerHTML = messagesHTML;
}
}
// ============================================================================
// SOUS-CLASSE - AccessibilityManager (ACCESSIBILITÉ)
// ============================================================================
/**
* Gestionnaire d'accessibilité (WCAG, ARIA, etc.)
*/
class AccessibilityManager {
/**
* Constructeur - Initialise les fonctionnalités d'accessibilité
*/
constructor() {
this.liveRegion = null;
this.reducedMotion = false;
}
/**
* Initialiser les fonctionnalités d'accessibilité
* @public - Méthode appelée par UXManager
*/
init() {
//console.log('[Accessibility] Initialisation des fonctionnalités d\'accessibilités...');
this.setupFocusManagement();
this.setupAriaLiveRegions();
this.detectReducedMotion();
this.setupSkipLinks();
//console.log('[Accessibility] Fonctionnalités d\'accessibilité activées');
}
/**
* Configurer la gestion du focus (modales, etc.)
* @private - Méthode de gestion du focus
*/
setupFocusManagement() {
// Gérer le focus pour les modales Bootstrap
document.addEventListener('shown.bs.modal', (e) => {
const modal = e.target;
const focusable = modal.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable) focusable.focus();
});
// Piéger le focus dans les modales
document.addEventListener('keydown', (e) => {
if (e.key === 'Tab' && document.querySelector('.modal.show')) {
this.trapFocusInModal(e);
}
});
}
/**
* Piéger le focus dans une modale
* @param {KeyboardEvent} e - Événement clavier
* @private - Méthode de piégeage de focus
*/
trapFocusInModal(e) {
const modal = document.querySelector('.modal.show');
if (!modal) return;
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusable[0];
const lastFocusable = focusable[focusable.length - 1];
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
} else {
// Tab seul
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
/**
* Configurer les régions ARIA live (annonces)
* @private - Méthode de création de régions ARIA
*/
setupAriaLiveRegions() {
// Créer une région ARIA live pour les annonces
this.liveRegion = document.createElement('div');
this.liveRegion.setAttribute('aria-live', 'polite');
this.liveRegion.setAttribute('aria-atomic', 'true');
this.liveRegion.className = 'visually-hidden';
this.liveRegion.setAttribute('role', 'status');
document.body.appendChild(this.liveRegion);
//console.log('[Accessibility] Région ARIA live créée');
}
/**
* Annoncer un message pour les lecteurs d'écran
* @param {string} message - Message à annoncer
* @public - Méthode accessible globalement
*/
announce(message) {
if (!this.liveRegion || !message) return;
// Effacer l'ancien message
this.liveRegion.textContent = '';
// Ajouter le nouveau message après un délai
setTimeout(() => {
this.liveRegion.textContent = message;
//console.log('[Accessibility] Annonce:', message);
}, 100);
}
/**
* Détecter la préférence de réduction des animations
* @private - Méthode de détection de préférence utilisateur
*/
detectReducedMotion() {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
this.reducedMotion = mediaQuery.matches;
if (this.reducedMotion) {
document.documentElement.classList.add('reduced-motion');
//console.log('[Accessibility] Réduction des mouvements activée');
}
// Écouter les changements de préférence
mediaQuery.addEventListener('change', (e) => {
this.reducedMotion = e.matches;
if (this.reducedMotion) {
document.documentElement.classList.add('reduced-motion');
} else {
document.documentElement.classList.remove('reduced-motion');
}
//console.log('[Accessibility] Préférence de mouvement mise à jour:', this.reducedMotion);
});
}
/**
* Configurer les liens d'évitement (skip links)
* @private - Méthode d'accessibilité navigation
*/
setupSkipLinks() {
// Vérifier si les skip links existent déjà
const existingSkipLinks = document.querySelector('.skip-links');
if (existingSkipLinks) return;
// Créer les skip links
const skipLinks = document.createElement('nav');
skipLinks.className = 'skip-links';
skipLinks.setAttribute('aria-label', 'Liens d\'évitement');
skipLinks.innerHTML = `
<a href="#main-content" class="skip-link">Aller au contenu principal</a>
<a href="#navigation" class="skip-link">Aller à la navigation</a>
<a href="#footer" class="skip-link">Aller au pied de page</a>
`;
// Insérer au début du body
document.body.insertBefore(skipLinks, document.body.firstChild);
//console.log('[Accessibility] Liens d\'évitement créés');
}
}
// ============================================================================
// SOUS-CLASSE - PerformanceManager (PERFORMANCES)
// ============================================================================
/**
* Gestionnaire des performances et optimisation
*/
class PerformanceManager {
/**
* Constructeur - Initialise le monitoring de performance
*/
constructor() {
this.observers = [];
this.metrics = {};
}
/**
* Initialiser le monitoring de performance
* @public - Méthode appelée par UXManager
*/
init() {
//console.log('[Performance] Initialisation du monitoring...');
this.monitorPerformance();
this.setupLazyLoading();
this.setupIntersectionObserver();
this.setupIdleCallback();
//console.log('[Performance] Monitoring activé');
}
/**
* Monitorer les métriques de performance web vitales
* @private - Méthode de monitoring Core Web Vitals
*/
monitorPerformance() {
// Monitoring LCP (Largest Contentful Paint)
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
switch (entry.entryType) {
case 'largest-contentful-paint':
this.metrics.lcp = entry.startTime;
//console.log('[Performance] LCP:', entry.startTime.toFixed(2), 'ms');
break;
case 'first-input':
this.metrics.fid = entry.processingStart - entry.startTime;
//console.log('[Performance] FID:', this.metrics.fid.toFixed(2), 'ms');
break;
case 'layout-shift':
if (!entry.hadRecentInput) {
this.metrics.cls = (this.metrics.cls || 0) + entry.value;
//console.log('[Performance] CLS cumulé:', this.metrics.cls.toFixed(4));
}
break;
}
}
});
observer.observe({
entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift']
});
this.observers.push(observer);
}
}
/**
* Configurer le lazy loading pour les images
* @private - Méthode d'optimisation de chargement
*/
setupLazyLoading() {
// Utiliser le lazy loading natif si disponible
if ('loading' in HTMLImageElement.prototype) {
document.querySelectorAll('img[loading="lazy"]').forEach(img => {
img.addEventListener('load', () => {
img.classList.add('loaded');
//console.log('[Performance] Image chargée:', img.src);
});
img.addEventListener('error', () => {
//console.warn('[Performance] Erreur de chargement image:', img.src);
});
});
} else {
// Fallback pour les navigateurs plus anciens
this.setupIntersectionObserverForImages();
}
}
/**
* Configurer l'Intersection Observer pour les images (fallback)
* @private - Méthode de lazy loading alternatif
*/
setupIntersectionObserverForImages() {
const lazyImages = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
imageObserver.unobserve(img);
}
});
});
lazyImages.forEach(img => imageObserver.observe(img));
this.observers.push(imageObserver);
}
/**
* Configurer l'Intersection Observer pour les éléments
* @private - Méthode d'animation au défilement
*/
setupIntersectionObserver() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('in-view');
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.1,
rootMargin: '50px'
});
// Observer les cartes de contenu
document.querySelectorAll('.content-card').forEach(card => {
observer.observe(card);
});
this.observers.push(observer);
}
/**
* Configurer les callbacks idle (traitements en arrière-plan)
* @private - Méthode d'optimisation des ressources
*/
setupIdleCallback() {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
// Tâches de basse priorité à exécuter pendant les périodes d'inactivité
this.cleanupOldData();
this.prefetchImportantLinks();
}, { timeout: 2000 });
}
}
/**
* Nettoyer les données anciennes
* @private - Méthode de nettoyage
*/
cleanupOldData() {
// Exemple : nettoyer le cache local si trop volumineux
// À implémenter selon les besoins
}
/**
* Précharger les liens importants
* @private - Méthode de préchargement
*/
prefetchImportantLinks() {
// Exemple : précharger les pages fréquemment visitées
// À implémenter selon l'analyse d'usage
}
/**
* Nettoyer les observers (pour la mémoire)
* @public - Méthode de nettoyage
*/
cleanup() {
this.observers.forEach(observer => {
if (observer && typeof observer.disconnect === 'function') {
observer.disconnect();
}
});
this.observers = [];
}
}
// ============================================================================
// SOUS-CLASSE - LanguageManager (GESTION DES LANGUES)
// ============================================================================
/**
* Gestionnaire du changement de langue
*/
class LanguageManager {
/**
* Constructeur - Initialise les textes selon la langue
*/
constructor() {
this.currentLang = null;
}
/**
* Initialiser la gestion des langues
* @public - Méthode appelée par UXManager
*/
init() {
this.detectCurrentLanguage();
this.updateLanguageTexts();
//console.log('[Language] Gestionnaire de langue initialisé:', this.currentLang);
}
/**
* Détecter la langue actuelle depuis la configuration
* @private - Méthode de détection
*/
detectCurrentLanguage() {
this.currentLang = window.appConfig?.isAnglophone ? 'en_US' : 'fr_FR';
}
/**
* Changer la langue de l'interface
* @public - Méthode accessible globalement
*/
changeLanguage() {
const newLang = this.currentLang === 'en_US' ? 'fr_FR' : 'en_US';
const messages = window.appConfig?.isAnglophone
? {
title: 'Change Language',
text: 'Switch to French?',
confirm: 'Switch',
cancel: 'Cancel'
}
: {
title: 'Changer de langue',
text: 'Passer en Anglais?',
confirm: 'Changer',
cancel: 'Annuler'
};
// Demander confirmation à l'utilisateur
Swal.fire({
title: messages.title,
text: messages.text,
icon: 'question',
showCancelButton: true,
confirmButtonText: messages.confirm,
cancelButtonText: messages.cancel,
allowOutsideClick: true,
allowEscapeKey: true
}).then((result) => {
if (result.isConfirmed) {
// Rediriger vers la page de connexion avec le nouveau paramètre de langue
const redirectUrl = window.appConfig?.racineWeb +
'Connexion/index?langue=' + newLang;
//console.log('[Language] Changement de langue vers:', newLang);
window.location.href = redirectUrl;
}
});
}
/**
* Mettre à jour les textes dynamiques selon la langue
* @private - Méthode de mise à jour des textes
*/
updateLanguageTexts() {
// À implémenter: mise à jour des textes dynamiques non gérés par le serveur
// Cette méthode peut être étendue pour gérer les textes côté client
// Exemple: Mettre à jour le titre de la page en fonction de la langue
const pageTitle = document.title;
if (this.currentLang === 'en_US' && !pageTitle.includes('RH Portal')) {
// Traduire vers l'anglais si nécessaire
}
}
/**
* Obtenir la langue courante
* @returns {string} Code de langue (fr_FR ou en_US)
* @public - Méthode d'accès à la langue
*/
getCurrentLanguage() {
return this.currentLang;
}
}
// ============================================================================
// INITIALISATION ET EXPOSITION GLOBALE
// ============================================================================
/**
* Initialiser l'application après le chargement du DOM
* Point d'entrée principal de l'application
*/
document.addEventListener('DOMContentLoaded', () => {
//console.log('[App] DOM chargé, initialisation de l\'application...');
// Vérifier que la configuration est disponible
if (!window.appConfig) {
//console.warn('[App] Configuration non trouvée, création par défaut...');
window.appConfig = {
activeParentId: '',
activeChildId: '',
activeLink: '',
racineWeb: '/',
baseUrl: '/',
isAnglophone: false,
debugMode: false
};
}
// Créer l'instance principale
window.appUX = new UXManager();
// Marquer le chargement comme terminé
setTimeout(() => {
document.body.classList.add('loaded');
//console.log('[App] Portail RH Inter Santé initialisé avec succès');
// Émettre un événement personnalisé
document.dispatchEvent(new CustomEvent('app:initialized'));
}, 100);
});
// ============================================================================
// FONCTIONS GLOBALES D'ACCÈS RAPIDE
// ============================================================================
/**
* Obtenir le gestionnaire de notifications
* @returns {NotificationManager} Instance du gestionnaire de notifications
*/
function appNotifications() {
return window.appUX?.notifications;
}
/**
* Obtenir le gestionnaire de navigation
* @returns {NavigationManager} Instance du gestionnaire de navigation
*/
function appNavigation() {
return window.appUX?.navigation;
}
/**
* Obtenir le gestionnaire de langues
* @returns {LanguageManager} Instance du gestionnaire de langues
*/
function appLanguage() {
return window.appUX?.language;
}
/**
* Obtenir le gestionnaire d'accessibilité
* @returns {AccessibilityManager} Instance du gestionnaire d'accessibilité
*/
function appAccessibility() {
return window.appUX?.accessibility;
}
// ============================================================================
// API PUBLIQUE GLOBALE
// ============================================================================
/**
* API publique globale pour l'interaction avec le UX Manager
* Permet un accès simple aux fonctionnalités principales
*/
window.uxManager = {
/**
* Basculer le panneau contextuel
*/
toggleContextPanel: () => window.appUX?.toggleContextPanel(),
/**
* Afficher les notifications
*/
showNotifications: () => window.appUX?.notifications?.showMessagesModal(),
/**
* Changer la langue
*/
changeLanguage: () => window.appUX?.language?.changeLanguage(),
/**
* Annoncer un message pour l'accessibilité
* @param {string} message - Message à annoncer
*/
announce: (message) => window.appUX?.accessibility?.announce(message),
/**
* Ouvrir un menu spécifique
* @param {string} menuId - ID du menu à ouvrir
*/
openMenu: (menuId) => window.appUX?.navigation?.openMenuById(menuId),
/**
* Fermer tous les menus
*/
closeAllMenus: () => window.appUX?.navigation?.closeAllMenus(),
/**
* Tester le système (mode debug)
*/
testSystem: () => {
if (window.appConfig?.debugMode) {
/*console.group('[UX Manager] Test du système');
console.log('App UX:', window.appUX);
console.log('Navigation:', window.appUX?.navigation);
console.log('Config:', window.appConfig);
console.groupEnd();*/
}
}
};
// ============================================================================
// EXPORT POUR LES MODULES ES6 (si utilisé avec bundler)
// ============================================================================
// Note: Décommentez si vous utilisez un bundler moderne
/*
export {
UXManager,
NavigationManager,
ContextPanelManager,
NotificationManager,
AccessibilityManager,
PerformanceManager,
LanguageManager
};
*/
//console.log('[UX Manager] Module chargé et prêt à l\'emploi');