Files
notytex/static/js/notytex-core.js

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;