468 lines
17 KiB
JavaScript
468 lines
17 KiB
JavaScript
/**
|
||
* MYP Platform Core Utilities - Optimized Version
|
||
* Consolidates common functionality from multiple files
|
||
* Reduces redundancy and improves performance
|
||
*/
|
||
|
||
(function(window) {
|
||
'use strict';
|
||
|
||
// Single instance pattern for core utilities
|
||
const MYPCore = {
|
||
// Version for cache busting
|
||
version: '1.0.0',
|
||
|
||
// CSRF Token Management (replaces 22+ different implementations)
|
||
csrf: {
|
||
_token: null,
|
||
|
||
getToken() {
|
||
if (!this._token) {
|
||
// Try multiple sources in order of preference
|
||
this._token =
|
||
document.querySelector('meta[name="csrf-token"]')?.content ||
|
||
document.querySelector('input[name="csrf_token"]')?.value ||
|
||
this._getFromCookie('csrf_token') ||
|
||
'';
|
||
}
|
||
return this._token;
|
||
},
|
||
|
||
_getFromCookie(name) {
|
||
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||
return match ? match[2] : null;
|
||
},
|
||
|
||
clearCache() {
|
||
this._token = null;
|
||
}
|
||
},
|
||
|
||
// DOM Utilities with caching
|
||
dom: {
|
||
_cache: new Map(),
|
||
|
||
// Cached querySelector
|
||
get(selector, parent = document) {
|
||
const key = `${parent === document ? 'doc' : 'el'}_${selector}`;
|
||
if (!this._cache.has(key)) {
|
||
this._cache.set(key, parent.querySelector(selector));
|
||
}
|
||
return this._cache.get(key);
|
||
},
|
||
|
||
// Cached querySelectorAll
|
||
getAll(selector, parent = document) {
|
||
return parent.querySelectorAll(selector);
|
||
},
|
||
|
||
// Efficient HTML escaping
|
||
escapeHtml(text) {
|
||
const div = this._escapeDiv || (this._escapeDiv = document.createElement('div'));
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
},
|
||
|
||
// Clear cache when DOM changes
|
||
clearCache() {
|
||
this._cache.clear();
|
||
},
|
||
|
||
// Efficient element creation
|
||
create(tag, attrs = {}, children = []) {
|
||
const el = document.createElement(tag);
|
||
Object.entries(attrs).forEach(([key, value]) => {
|
||
if (key === 'class') {
|
||
el.className = value;
|
||
} else if (key === 'style' && typeof value === 'object') {
|
||
Object.assign(el.style, value);
|
||
} else if (key.startsWith('data-')) {
|
||
el.dataset[key.slice(5)] = value;
|
||
} else {
|
||
el.setAttribute(key, value);
|
||
}
|
||
});
|
||
children.forEach(child => {
|
||
if (typeof child === 'string') {
|
||
el.appendChild(document.createTextNode(child));
|
||
} else {
|
||
el.appendChild(child);
|
||
}
|
||
});
|
||
return el;
|
||
}
|
||
},
|
||
|
||
// Unified Notification System (replaces 3 separate systems)
|
||
notify: {
|
||
_container: null,
|
||
_queue: [],
|
||
_activeToasts: new Map(),
|
||
_toastId: 0,
|
||
|
||
show(message, type = 'info', duration = 5000, options = {}) {
|
||
// Check for DND mode
|
||
if (window.dndManager?.isEnabled && !options.force) {
|
||
console.log(`[DND] Suppressed ${type} notification:`, message);
|
||
return null;
|
||
}
|
||
|
||
const id = `toast-${++this._toastId}`;
|
||
const toast = this._createToast(id, message, type, duration, options);
|
||
|
||
this._ensureContainer();
|
||
this._container.appendChild(toast);
|
||
|
||
// Animate in
|
||
requestAnimationFrame(() => {
|
||
toast.classList.add('show');
|
||
});
|
||
|
||
// Auto remove
|
||
if (duration > 0 && !options.persistent) {
|
||
const timeout = setTimeout(() => this.close(id), duration);
|
||
this._activeToasts.set(id, { element: toast, timeout });
|
||
}
|
||
|
||
return id;
|
||
},
|
||
|
||
_createToast(id, message, type, duration, options) {
|
||
const toast = MYPCore.dom.create('div', {
|
||
id,
|
||
class: `myp-toast myp-toast-${type}`,
|
||
role: 'alert',
|
||
'aria-live': 'polite'
|
||
});
|
||
|
||
const icon = this._getIcon(type);
|
||
const content = MYPCore.dom.create('div', { class: 'toast-content' }, [
|
||
MYPCore.dom.create('span', { class: 'toast-icon' }, [icon]),
|
||
MYPCore.dom.create('span', { class: 'toast-message' }, [message])
|
||
]);
|
||
|
||
const closeBtn = MYPCore.dom.create('button', {
|
||
class: 'toast-close',
|
||
'aria-label': 'Close notification',
|
||
onclick: () => this.close(id)
|
||
}, ['×']);
|
||
|
||
toast.appendChild(content);
|
||
toast.appendChild(closeBtn);
|
||
|
||
if (duration > 0 && !options.persistent) {
|
||
const progress = MYPCore.dom.create('div', { class: 'toast-progress' });
|
||
const bar = MYPCore.dom.create('div', {
|
||
class: 'toast-progress-bar',
|
||
style: { animationDuration: `${duration}ms` }
|
||
});
|
||
progress.appendChild(bar);
|
||
toast.appendChild(progress);
|
||
}
|
||
|
||
return toast;
|
||
},
|
||
|
||
_getIcon(type) {
|
||
const icons = {
|
||
success: '✓',
|
||
error: '✗',
|
||
warning: '⚠',
|
||
info: 'ℹ'
|
||
};
|
||
return icons[type] || icons.info;
|
||
},
|
||
|
||
_ensureContainer() {
|
||
if (!this._container) {
|
||
this._container = MYPCore.dom.get('#myp-toast-container') ||
|
||
MYPCore.dom.create('div', { id: 'myp-toast-container', class: 'myp-toast-container' });
|
||
|
||
if (!this._container.parentNode) {
|
||
document.body.appendChild(this._container);
|
||
}
|
||
}
|
||
},
|
||
|
||
close(id) {
|
||
const toast = this._activeToasts.get(id);
|
||
if (toast) {
|
||
clearTimeout(toast.timeout);
|
||
toast.element.classList.remove('show');
|
||
setTimeout(() => {
|
||
toast.element.remove();
|
||
this._activeToasts.delete(id);
|
||
}, 300);
|
||
}
|
||
},
|
||
|
||
closeAll() {
|
||
this._activeToasts.forEach((_, id) => this.close(id));
|
||
}
|
||
},
|
||
|
||
// API Request Utilities
|
||
api: {
|
||
_cache: new Map(),
|
||
_pendingRequests: new Map(),
|
||
|
||
async request(url, options = {}) {
|
||
const config = {
|
||
...options,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': MYPCore.csrf.getToken(),
|
||
...options.headers
|
||
}
|
||
};
|
||
|
||
// Check cache for GET requests
|
||
if (options.method === 'GET' || !options.method) {
|
||
const cacheKey = this._getCacheKey(url, options);
|
||
const cached = this._cache.get(cacheKey);
|
||
|
||
if (cached && !options.noCache) {
|
||
const age = Date.now() - cached.timestamp;
|
||
const maxAge = options.cacheTime || 300000; // 5 minutes default
|
||
|
||
if (age < maxAge) {
|
||
return cached.data;
|
||
}
|
||
}
|
||
|
||
// Prevent duplicate requests
|
||
if (this._pendingRequests.has(cacheKey)) {
|
||
return this._pendingRequests.get(cacheKey);
|
||
}
|
||
}
|
||
|
||
// Make request
|
||
const requestPromise = fetch(url, config)
|
||
.then(async response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
// Cache successful GET requests
|
||
if (config.method === 'GET' || !config.method) {
|
||
const cacheKey = this._getCacheKey(url, options);
|
||
this._cache.set(cacheKey, {
|
||
data,
|
||
timestamp: Date.now()
|
||
});
|
||
this._pendingRequests.delete(cacheKey);
|
||
}
|
||
|
||
return data;
|
||
})
|
||
.catch(error => {
|
||
if (config.method === 'GET' || !config.method) {
|
||
this._pendingRequests.delete(this._getCacheKey(url, options));
|
||
}
|
||
throw error;
|
||
});
|
||
|
||
if (config.method === 'GET' || !config.method) {
|
||
this._pendingRequests.set(this._getCacheKey(url, options), requestPromise);
|
||
}
|
||
|
||
return requestPromise;
|
||
},
|
||
|
||
_getCacheKey(url, options) {
|
||
return `${url}_${JSON.stringify(options.params || {})}`;
|
||
},
|
||
|
||
clearCache(pattern) {
|
||
if (pattern) {
|
||
for (const [key] of this._cache) {
|
||
if (key.includes(pattern)) {
|
||
this._cache.delete(key);
|
||
}
|
||
}
|
||
} else {
|
||
this._cache.clear();
|
||
}
|
||
}
|
||
},
|
||
|
||
// Time/Date Utilities
|
||
time: {
|
||
formatAgo(timestamp) {
|
||
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
|
||
const now = new Date();
|
||
const seconds = Math.floor((now - date) / 1000);
|
||
|
||
const intervals = [
|
||
{ label: 'Jahr', seconds: 31536000, plural: 'Jahre' },
|
||
{ label: 'Monat', seconds: 2592000, plural: 'Monate' },
|
||
{ label: 'Woche', seconds: 604800, plural: 'Wochen' },
|
||
{ label: 'Tag', seconds: 86400, plural: 'Tage' },
|
||
{ label: 'Stunde', seconds: 3600, plural: 'Stunden' },
|
||
{ label: 'Minute', seconds: 60, plural: 'Minuten' }
|
||
];
|
||
|
||
for (const interval of intervals) {
|
||
const count = Math.floor(seconds / interval.seconds);
|
||
if (count >= 1) {
|
||
const label = count === 1 ? interval.label : interval.plural;
|
||
return `vor ${count} ${label}`;
|
||
}
|
||
}
|
||
|
||
return 'Gerade eben';
|
||
},
|
||
|
||
formatDuration(seconds) {
|
||
const hours = Math.floor(seconds / 3600);
|
||
const minutes = Math.floor((seconds % 3600) / 60);
|
||
const secs = seconds % 60;
|
||
|
||
const parts = [];
|
||
if (hours > 0) parts.push(`${hours}h`);
|
||
if (minutes > 0) parts.push(`${minutes}m`);
|
||
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
|
||
|
||
return parts.join(' ');
|
||
}
|
||
},
|
||
|
||
// Performance Utilities
|
||
perf: {
|
||
debounce(func, wait) {
|
||
let timeout;
|
||
return function debounced(...args) {
|
||
const context = this;
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(() => func.apply(context, args), wait);
|
||
};
|
||
},
|
||
|
||
throttle(func, limit) {
|
||
let inThrottle;
|
||
return function throttled(...args) {
|
||
const context = this;
|
||
if (!inThrottle) {
|
||
func.apply(context, args);
|
||
inThrottle = true;
|
||
setTimeout(() => inThrottle = false, limit);
|
||
}
|
||
};
|
||
},
|
||
|
||
memoize(func, resolver) {
|
||
const cache = new Map();
|
||
return function memoized(...args) {
|
||
const key = resolver ? resolver(...args) : args[0];
|
||
if (cache.has(key)) {
|
||
return cache.get(key);
|
||
}
|
||
const result = func.apply(this, args);
|
||
cache.set(key, result);
|
||
return result;
|
||
};
|
||
}
|
||
},
|
||
|
||
// Event Management
|
||
events: {
|
||
_listeners: new Map(),
|
||
|
||
on(element, event, handler, options = {}) {
|
||
const key = `${element}_${event}`;
|
||
if (!this._listeners.has(key)) {
|
||
this._listeners.set(key, new Set());
|
||
}
|
||
|
||
this._listeners.get(key).add(handler);
|
||
element.addEventListener(event, handler, options);
|
||
|
||
// Return cleanup function
|
||
return () => this.off(element, event, handler);
|
||
},
|
||
|
||
off(element, event, handler) {
|
||
const key = `${element}_${event}`;
|
||
const handlers = this._listeners.get(key);
|
||
|
||
if (handlers) {
|
||
handlers.delete(handler);
|
||
if (handlers.size === 0) {
|
||
this._listeners.delete(key);
|
||
}
|
||
}
|
||
|
||
element.removeEventListener(event, handler);
|
||
},
|
||
|
||
once(element, event, handler, options = {}) {
|
||
const wrappedHandler = (e) => {
|
||
handler(e);
|
||
this.off(element, event, wrappedHandler);
|
||
};
|
||
|
||
return this.on(element, event, wrappedHandler, options);
|
||
}
|
||
},
|
||
|
||
// Storage Utilities
|
||
storage: {
|
||
get(key, defaultValue = null) {
|
||
try {
|
||
const item = localStorage.getItem(`myp_${key}`);
|
||
return item ? JSON.parse(item) : defaultValue;
|
||
} catch (e) {
|
||
console.error('Storage get error:', e);
|
||
return defaultValue;
|
||
}
|
||
},
|
||
|
||
set(key, value) {
|
||
try {
|
||
localStorage.setItem(`myp_${key}`, JSON.stringify(value));
|
||
return true;
|
||
} catch (e) {
|
||
console.error('Storage set error:', e);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
remove(key) {
|
||
localStorage.removeItem(`myp_${key}`);
|
||
},
|
||
|
||
clear(prefix = 'myp_') {
|
||
const keys = Object.keys(localStorage);
|
||
keys.forEach(key => {
|
||
if (key.startsWith(prefix)) {
|
||
localStorage.removeItem(key);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
// Global shortcuts for backwards compatibility
|
||
window.MYPCore = MYPCore;
|
||
|
||
// Legacy function mappings
|
||
window.showToast = (msg, type, duration) => MYPCore.notify.show(msg, type, duration);
|
||
window.showNotification = window.showToast;
|
||
window.showFlashMessage = window.showToast;
|
||
window.showSuccessMessage = (msg) => MYPCore.notify.show(msg, 'success');
|
||
window.showErrorMessage = (msg) => MYPCore.notify.show(msg, 'error');
|
||
window.showWarningMessage = (msg) => MYPCore.notify.show(msg, 'warning');
|
||
window.showInfoMessage = (msg) => MYPCore.notify.show(msg, 'info');
|
||
|
||
// Auto-initialize on DOM ready
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
console.log('✅ MYP Core Utilities initialized');
|
||
});
|
||
} else {
|
||
console.log('✅ MYP Core Utilities initialized');
|
||
}
|
||
|
||
})(window); |