496 lines
15 KiB
JavaScript
496 lines
15 KiB
JavaScript
/**
|
|
* NOTYTEX CORE - Système JavaScript Unifié
|
|
* Architecture moderne ES6 pour l'application Notytex
|
|
* Version 2.0 - Refactoring complet pour uniformisation
|
|
*/
|
|
|
|
class NotytexCore {
|
|
constructor() {
|
|
this.modules = new Map();
|
|
this.components = new Map();
|
|
this.observers = new Map();
|
|
this.config = this.getDefaultConfig();
|
|
this.state = this.createReactiveState();
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Configuration par défaut unifiée
|
|
*/
|
|
getDefaultConfig() {
|
|
return {
|
|
// Design tokens JavaScript
|
|
designTokens: {
|
|
transitions: {
|
|
fast: 150,
|
|
normal: 300,
|
|
slow: 500,
|
|
easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
|
|
},
|
|
breakpoints: {
|
|
mobile: '(max-width: 767px)',
|
|
tablet: '(min-width: 768px) and (max-width: 1023px)',
|
|
desktop: '(min-width: 1024px)'
|
|
},
|
|
colors: {
|
|
primary: '#3b82f6',
|
|
success: '#10b981',
|
|
warning: '#f59e0b',
|
|
danger: '#ef4444'
|
|
}
|
|
},
|
|
|
|
// Configuration des modules
|
|
modules: {
|
|
animations: { enabled: true, performanceMode: false },
|
|
filters: { autoSubmit: true, debounceTime: 300 },
|
|
notifications: { position: 'top-right', duration: 3000 },
|
|
modals: { backdrop: true, keyboard: true }
|
|
},
|
|
|
|
// Performance
|
|
performance: {
|
|
lazyLoading: true,
|
|
bundleOptimization: true,
|
|
cacheStrategy: 'aggressive'
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Créer un état réactif avec Proxy
|
|
*/
|
|
createReactiveState() {
|
|
const state = {};
|
|
const self = this;
|
|
|
|
return new Proxy(state, {
|
|
set(target, property, value) {
|
|
const oldValue = target[property];
|
|
target[property] = value;
|
|
|
|
// Notifier les observateurs (vérifier que observers existe)
|
|
if (self.observers && self.observers.has(property)) {
|
|
self.observers.get(property).forEach(callback => {
|
|
callback(value, oldValue, property);
|
|
});
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
get(target, property) {
|
|
return target[property];
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialisation du core
|
|
*/
|
|
init() {
|
|
this.setupEventListeners();
|
|
this.detectEnvironment();
|
|
this.loadCoreModules();
|
|
this.initializeAnimations();
|
|
|
|
// Déclenchement de l'événement de prêt
|
|
this.emit('core:ready');
|
|
|
|
}
|
|
|
|
/**
|
|
* Enregistrer un module
|
|
*/
|
|
registerModule(name, moduleClass, config = {}) {
|
|
try {
|
|
const moduleInstance = new moduleClass(this, config);
|
|
this.modules.set(name, moduleInstance);
|
|
|
|
// Auto-init si le module le support
|
|
if (typeof moduleInstance.init === 'function') {
|
|
moduleInstance.init();
|
|
}
|
|
|
|
this.emit('module:registered', { name, module: moduleInstance });
|
|
return moduleInstance;
|
|
} catch (error) {
|
|
console.error(`Failed to register module ${name}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtenir un module
|
|
*/
|
|
getModule(name) {
|
|
return this.modules.get(name);
|
|
}
|
|
|
|
/**
|
|
* Chargement paresseux d'un module
|
|
*/
|
|
async loadModule(name, path) {
|
|
if (this.modules.has(name)) {
|
|
return this.modules.get(name);
|
|
}
|
|
|
|
try {
|
|
const moduleFile = await import(path);
|
|
const ModuleClass = moduleFile.default || moduleFile[name];
|
|
|
|
return this.registerModule(name, ModuleClass);
|
|
} catch (error) {
|
|
console.error(`Failed to load module ${name} from ${path}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gestionnaire d'état réactif
|
|
*/
|
|
setState(updates) {
|
|
Object.assign(this.state, updates);
|
|
}
|
|
|
|
getState(key) {
|
|
return key ? this.state[key] : this.state;
|
|
}
|
|
|
|
subscribe(key, callback) {
|
|
if (!this.observers.has(key)) {
|
|
this.observers.set(key, new Set());
|
|
}
|
|
this.observers.get(key).add(callback);
|
|
|
|
// Retourner fonction de désabonnement
|
|
return () => {
|
|
this.observers.get(key).delete(callback);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Système d'événements unifié
|
|
*/
|
|
emit(eventName, data = null) {
|
|
const event = new CustomEvent(eventName, {
|
|
detail: data,
|
|
bubbles: true,
|
|
cancelable: true
|
|
});
|
|
|
|
document.dispatchEvent(event);
|
|
return event;
|
|
}
|
|
|
|
on(eventName, callback) {
|
|
document.addEventListener(eventName, callback);
|
|
return () => document.removeEventListener(eventName, callback);
|
|
}
|
|
|
|
/**
|
|
* Utilitaires unifiés
|
|
*/
|
|
utils = {
|
|
// Debounce optimisé
|
|
debounce: (func, wait, immediate = false) => {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
timeout = null;
|
|
if (!immediate) func(...args);
|
|
};
|
|
const callNow = immediate && !timeout;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
if (callNow) func(...args);
|
|
};
|
|
},
|
|
|
|
// Throttle pour performance
|
|
throttle: (func, limit) => {
|
|
let inThrottle;
|
|
return function(...args) {
|
|
if (!inThrottle) {
|
|
func.apply(this, args);
|
|
inThrottle = true;
|
|
setTimeout(() => inThrottle = false, limit);
|
|
}
|
|
};
|
|
},
|
|
|
|
// Création d'éléments sécurisée
|
|
createElement: (tag, attributes = {}, children = []) => {
|
|
const element = document.createElement(tag);
|
|
|
|
Object.entries(attributes).forEach(([key, value]) => {
|
|
if (key === 'className') {
|
|
element.className = value;
|
|
} else if (key === 'innerHTML') {
|
|
element.innerHTML = value;
|
|
} else {
|
|
element.setAttribute(key, value);
|
|
}
|
|
});
|
|
|
|
children.forEach(child => {
|
|
if (typeof child === 'string') {
|
|
element.appendChild(document.createTextNode(child));
|
|
} else {
|
|
element.appendChild(child);
|
|
}
|
|
});
|
|
|
|
return element;
|
|
},
|
|
|
|
// Sélecteur sécurisé
|
|
query: (selector, context = document) => {
|
|
try {
|
|
return context.querySelector(selector);
|
|
} catch (error) {
|
|
console.warn(`Invalid selector: ${selector}`, error);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
queryAll: (selector, context = document) => {
|
|
try {
|
|
return Array.from(context.querySelectorAll(selector));
|
|
} catch (error) {
|
|
console.warn(`Invalid selector: ${selector}`, error);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
// Copie dans le presse-papiers
|
|
copyToClipboard: async (text) => {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
NotytexCore.instance.components.get('notifications')?.show({
|
|
type: 'success',
|
|
message: 'Copié dans le presse-papiers'
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Clipboard error:', error);
|
|
NotytexCore.instance.components.get('notifications')?.show({
|
|
type: 'error',
|
|
message: 'Erreur lors de la copie'
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Détection de l'environnement
|
|
*/
|
|
detectEnvironment() {
|
|
this.state.environment = {
|
|
isMobile: window.matchMedia(this.config.designTokens.breakpoints.mobile).matches,
|
|
isTablet: window.matchMedia(this.config.designTokens.breakpoints.tablet).matches,
|
|
isDesktop: window.matchMedia(this.config.designTokens.breakpoints.desktop).matches,
|
|
hasTouch: 'ontouchstart' in window,
|
|
supportsClipboard: !!navigator.clipboard,
|
|
supportsIntersectionObserver: 'IntersectionObserver' in window,
|
|
prefersReducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
};
|
|
|
|
// Observer les changements de breakpoints
|
|
Object.entries(this.config.designTokens.breakpoints).forEach(([name, query]) => {
|
|
const mediaQuery = window.matchMedia(query);
|
|
mediaQuery.addListener(() => {
|
|
this.state.environment[`is${name.charAt(0).toUpperCase() + name.slice(1)}`] = mediaQuery.matches;
|
|
this.emit('breakpoint:change', { name, matches: mediaQuery.matches });
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Chargement des modules essentiels
|
|
*/
|
|
loadCoreModules() {
|
|
// Ces modules seront chargés automatiquement
|
|
const coreModules = [
|
|
'NotificationSystem',
|
|
'ModalManager',
|
|
'AnimationController',
|
|
'FilterManager'
|
|
];
|
|
|
|
// Pour l'instant, on enregistre des placeholders
|
|
// qui seront remplacés par les vrais modules
|
|
coreModules.forEach(moduleName => {
|
|
this.modules.set(moduleName.toLowerCase(), {
|
|
name: moduleName,
|
|
loaded: false,
|
|
placeholder: true
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialisation des animations
|
|
*/
|
|
initializeAnimations() {
|
|
if (!this.config.modules.animations.enabled) return;
|
|
if (this.state.environment.prefersReducedMotion) return;
|
|
|
|
// Observer d'intersection pour animations au scroll
|
|
if (this.state.environment.supportsIntersectionObserver) {
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('in-view');
|
|
// Désactiver l'observation une fois animé
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.1,
|
|
rootMargin: '0px 0px -50px 0px'
|
|
});
|
|
|
|
// Observer tous les éléments avec animation
|
|
this.utils.queryAll('.animate-on-scroll').forEach(el => {
|
|
observer.observe(el);
|
|
});
|
|
|
|
this.state.scrollObserver = observer;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configuration des événements globaux
|
|
*/
|
|
setupEventListeners() {
|
|
// Gestion des clics globaux
|
|
document.addEventListener('click', this.handleGlobalClick.bind(this));
|
|
|
|
// Gestion des touches globales
|
|
document.addEventListener('keydown', this.handleGlobalKeydown.bind(this));
|
|
|
|
// Gestion des liens de confirmation
|
|
document.addEventListener('click', this.handleConfirmationLinks.bind(this));
|
|
|
|
// Gestion du resize window
|
|
window.addEventListener('resize',
|
|
this.utils.throttle(() => {
|
|
this.emit('window:resize', {
|
|
width: window.innerWidth,
|
|
height: window.innerHeight
|
|
});
|
|
}, 250)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Gestionnaire de clics globaux
|
|
*/
|
|
handleGlobalClick(event) {
|
|
// Fermeture des dropdowns
|
|
const openDropdowns = this.utils.queryAll('.dropdown-menu.show');
|
|
openDropdowns.forEach(dropdown => {
|
|
if (!dropdown.contains(event.target)) {
|
|
dropdown.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// Gestion des boutons de copie
|
|
const copyButton = event.target.closest('[data-copy]');
|
|
if (copyButton) {
|
|
event.preventDefault();
|
|
const textToCopy = copyButton.dataset.copy;
|
|
this.utils.copyToClipboard(textToCopy);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gestionnaire de touches globales
|
|
*/
|
|
handleGlobalKeydown(event) {
|
|
// Fermeture des modales avec Escape
|
|
if (event.key === 'Escape') {
|
|
const openModal = this.utils.query('.modal:not(.hidden)');
|
|
if (openModal && this.components.has('modals')) {
|
|
this.components.get('modals').close(openModal.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gestionnaire des liens de confirmation
|
|
*/
|
|
handleConfirmationLinks(event) {
|
|
const confirmLink = event.target.closest('[data-confirm]');
|
|
if (!confirmLink) return;
|
|
|
|
event.preventDefault();
|
|
const message = confirmLink.dataset.confirm;
|
|
const title = confirmLink.dataset.confirmTitle || 'Confirmation';
|
|
|
|
this.showConfirmation(message, title).then(confirmed => {
|
|
if (confirmed) {
|
|
if (confirmLink.tagName === 'A') {
|
|
window.location.href = confirmLink.href;
|
|
} else if (confirmLink.tagName === 'BUTTON') {
|
|
confirmLink.click();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Système de confirmation unifié
|
|
*/
|
|
showConfirmation(message, title = 'Confirmation') {
|
|
return new Promise((resolve) => {
|
|
// Si SweetAlert2 est disponible
|
|
if (typeof Swal !== 'undefined') {
|
|
Swal.fire({
|
|
title,
|
|
text: message,
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Confirmer',
|
|
cancelButtonText: 'Annuler',
|
|
confirmButtonColor: this.config.designTokens.colors.danger,
|
|
cancelButtonColor: '#6b7280'
|
|
}).then((result) => {
|
|
resolve(result.isConfirmed);
|
|
});
|
|
} else {
|
|
// Fallback natif
|
|
resolve(confirm(`${title}\n\n${message}`));
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* API publique unifiée
|
|
*/
|
|
static getInstance() {
|
|
if (!NotytexCore.instance) {
|
|
NotytexCore.instance = new NotytexCore();
|
|
}
|
|
return NotytexCore.instance;
|
|
}
|
|
}
|
|
|
|
// Instance globale singleton
|
|
NotytexCore.instance = null;
|
|
|
|
// Export pour utilisation
|
|
window.NotytexCore = NotytexCore;
|
|
window.Notytex = NotytexCore.getInstance();
|
|
|
|
// Auto-initialisation
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
if (!NotytexCore.instance) {
|
|
window.Notytex = NotytexCore.getInstance();
|
|
}
|
|
});
|
|
|
|
export default NotytexCore; |