493 lines
15 KiB
JavaScript
493 lines
15 KiB
JavaScript
/**
|
|
* Core Utilities Module for MYP Platform
|
|
* Consolidated utilities to eliminate redundancy and improve performance
|
|
*/
|
|
|
|
(function(window) {
|
|
'use strict';
|
|
|
|
// Module namespace
|
|
const MYP = window.MYP || {};
|
|
|
|
// ===== CONFIGURATION =====
|
|
const config = {
|
|
apiTimeout: 30000,
|
|
cacheExpiry: 5 * 60 * 1000, // 5 minutes
|
|
notificationDuration: 5000,
|
|
debounceDelay: 300,
|
|
throttleDelay: 100
|
|
};
|
|
|
|
// ===== CACHE MANAGEMENT =====
|
|
const cache = new Map();
|
|
const requestCache = new Map();
|
|
|
|
// ===== CSRF TOKEN HANDLING =====
|
|
const csrf = {
|
|
token: null,
|
|
|
|
get() {
|
|
if (!this.token) {
|
|
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
this.token = meta ? meta.getAttribute('content') : '';
|
|
}
|
|
return this.token;
|
|
},
|
|
|
|
headers() {
|
|
return {
|
|
'X-CSRFToken': this.get(),
|
|
'Content-Type': 'application/json'
|
|
};
|
|
}
|
|
};
|
|
|
|
// ===== DOM UTILITIES =====
|
|
const dom = {
|
|
// Cached selectors
|
|
selectors: new Map(),
|
|
|
|
// Get element with caching
|
|
get(selector, parent = document) {
|
|
const key = `${parent === document ? 'doc' : 'el'}_${selector}`;
|
|
if (!this.selectors.has(key)) {
|
|
this.selectors.set(key, parent.querySelector(selector));
|
|
}
|
|
return this.selectors.get(key);
|
|
},
|
|
|
|
// Get all elements
|
|
getAll(selector, parent = document) {
|
|
return parent.querySelectorAll(selector);
|
|
},
|
|
|
|
// Clear cache
|
|
clearCache() {
|
|
this.selectors.clear();
|
|
},
|
|
|
|
// Safe element creation
|
|
create(tag, attrs = {}, text = '') {
|
|
const el = document.createElement(tag);
|
|
Object.entries(attrs).forEach(([key, val]) => {
|
|
if (key === 'class') {
|
|
el.className = val;
|
|
} else if (key === 'dataset') {
|
|
Object.entries(val).forEach(([k, v]) => {
|
|
el.dataset[k] = v;
|
|
});
|
|
} else {
|
|
el.setAttribute(key, val);
|
|
}
|
|
});
|
|
if (text) el.textContent = text;
|
|
return el;
|
|
}
|
|
};
|
|
|
|
// ===== API REQUEST HANDLING =====
|
|
const api = {
|
|
// Request deduplication
|
|
pending: new Map(),
|
|
|
|
async request(url, options = {}) {
|
|
const key = `${options.method || 'GET'}_${url}`;
|
|
|
|
// Check for pending request
|
|
if (this.pending.has(key)) {
|
|
return this.pending.get(key);
|
|
}
|
|
|
|
// Check cache for GET requests
|
|
if (!options.method || options.method === 'GET') {
|
|
const cached = requestCache.get(key);
|
|
if (cached && Date.now() - cached.timestamp < config.cacheExpiry) {
|
|
return Promise.resolve(cached.data);
|
|
}
|
|
}
|
|
|
|
// Prepare request
|
|
const requestOptions = {
|
|
...options,
|
|
headers: {
|
|
...csrf.headers(),
|
|
...options.headers
|
|
}
|
|
};
|
|
|
|
// Create request promise
|
|
const promise = fetch(url, requestOptions)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
// Cache successful GET requests
|
|
if (!options.method || options.method === 'GET') {
|
|
requestCache.set(key, {
|
|
data,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
return data;
|
|
})
|
|
.finally(() => {
|
|
this.pending.delete(key);
|
|
});
|
|
|
|
this.pending.set(key, promise);
|
|
return promise;
|
|
},
|
|
|
|
get(url, options = {}) {
|
|
return this.request(url, { ...options, method: 'GET' });
|
|
},
|
|
|
|
post(url, data, options = {}) {
|
|
return this.request(url, {
|
|
...options,
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
});
|
|
},
|
|
|
|
put(url, data, options = {}) {
|
|
return this.request(url, {
|
|
...options,
|
|
method: 'PUT',
|
|
body: JSON.stringify(data)
|
|
});
|
|
},
|
|
|
|
delete(url, options = {}) {
|
|
return this.request(url, { ...options, method: 'DELETE' });
|
|
}
|
|
};
|
|
|
|
// ===== UNIFIED NOTIFICATION SYSTEM =====
|
|
const notifications = {
|
|
container: null,
|
|
queue: [],
|
|
|
|
init() {
|
|
if (this.container) return;
|
|
|
|
this.container = dom.create('div', {
|
|
id: 'myp-notifications',
|
|
class: 'fixed top-4 right-4 z-50 space-y-2'
|
|
});
|
|
document.body.appendChild(this.container);
|
|
},
|
|
|
|
show(message, type = 'info', duration = config.notificationDuration) {
|
|
this.init();
|
|
|
|
const notification = dom.create('div', {
|
|
class: `notification notification-${type} glass-navbar p-4 rounded-lg shadow-lg transform translate-x-full transition-transform duration-300`,
|
|
role: 'alert'
|
|
});
|
|
|
|
const content = dom.create('div', {
|
|
class: 'flex items-center space-x-3'
|
|
});
|
|
|
|
// Icon
|
|
const icon = dom.create('i', {
|
|
class: `fas ${this.getIcon(type)} text-lg`
|
|
});
|
|
|
|
// Message
|
|
const text = dom.create('span', {
|
|
class: 'flex-1'
|
|
}, message);
|
|
|
|
// Close button
|
|
const close = dom.create('button', {
|
|
class: 'ml-4 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200',
|
|
'aria-label': 'Close'
|
|
});
|
|
close.innerHTML = '×';
|
|
close.onclick = () => this.remove(notification);
|
|
|
|
content.appendChild(icon);
|
|
content.appendChild(text);
|
|
content.appendChild(close);
|
|
notification.appendChild(content);
|
|
|
|
this.container.appendChild(notification);
|
|
|
|
// Animate in
|
|
requestAnimationFrame(() => {
|
|
notification.classList.remove('translate-x-full');
|
|
});
|
|
|
|
// Auto remove
|
|
if (duration > 0) {
|
|
setTimeout(() => this.remove(notification), duration);
|
|
}
|
|
|
|
return notification;
|
|
},
|
|
|
|
remove(notification) {
|
|
notification.classList.add('translate-x-full');
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 300);
|
|
},
|
|
|
|
getIcon(type) {
|
|
const icons = {
|
|
success: 'fa-check-circle text-green-500',
|
|
error: 'fa-exclamation-circle text-red-500',
|
|
warning: 'fa-exclamation-triangle text-yellow-500',
|
|
info: 'fa-info-circle text-blue-500'
|
|
};
|
|
return icons[type] || icons.info;
|
|
},
|
|
|
|
success(message, duration) {
|
|
return this.show(message, 'success', duration);
|
|
},
|
|
|
|
error(message, duration) {
|
|
return this.show(message, 'error', duration);
|
|
},
|
|
|
|
warning(message, duration) {
|
|
return this.show(message, 'warning', duration);
|
|
},
|
|
|
|
info(message, duration) {
|
|
return this.show(message, 'info', duration);
|
|
}
|
|
};
|
|
|
|
// ===== PERFORMANCE UTILITIES =====
|
|
const performance = {
|
|
debounce(func, delay = config.debounceDelay) {
|
|
let timeoutId;
|
|
return function(...args) {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = setTimeout(() => func.apply(this, args), delay);
|
|
};
|
|
},
|
|
|
|
throttle(func, delay = config.throttleDelay) {
|
|
let lastCall = 0;
|
|
return function(...args) {
|
|
const now = Date.now();
|
|
if (now - lastCall >= delay) {
|
|
lastCall = now;
|
|
return func.apply(this, args);
|
|
}
|
|
};
|
|
},
|
|
|
|
memoize(func) {
|
|
const cache = new Map();
|
|
return function(...args) {
|
|
const key = JSON.stringify(args);
|
|
if (cache.has(key)) {
|
|
return cache.get(key);
|
|
}
|
|
const result = func.apply(this, args);
|
|
cache.set(key, result);
|
|
return result;
|
|
};
|
|
},
|
|
|
|
lazy(selector, callback) {
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
callback(entry.target);
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
});
|
|
|
|
dom.getAll(selector).forEach(el => observer.observe(el));
|
|
return observer;
|
|
}
|
|
};
|
|
|
|
// ===== STORAGE UTILITIES =====
|
|
const storage = {
|
|
set(key, value, expiry = null) {
|
|
const data = {
|
|
value,
|
|
timestamp: Date.now(),
|
|
expiry
|
|
};
|
|
try {
|
|
localStorage.setItem(`myp_${key}`, JSON.stringify(data));
|
|
return true;
|
|
} catch (e) {
|
|
console.error('Storage error:', e);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
get(key) {
|
|
try {
|
|
const item = localStorage.getItem(`myp_${key}`);
|
|
if (!item) return null;
|
|
|
|
const data = JSON.parse(item);
|
|
if (data.expiry && Date.now() - data.timestamp > data.expiry) {
|
|
this.remove(key);
|
|
return null;
|
|
}
|
|
return data.value;
|
|
} catch (e) {
|
|
console.error('Storage error:', e);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
remove(key) {
|
|
localStorage.removeItem(`myp_${key}`);
|
|
},
|
|
|
|
clear() {
|
|
Object.keys(localStorage)
|
|
.filter(key => key.startsWith('myp_'))
|
|
.forEach(key => localStorage.removeItem(key));
|
|
}
|
|
};
|
|
|
|
// ===== EVENT UTILITIES =====
|
|
const 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);
|
|
},
|
|
|
|
off(element, event, handler) {
|
|
const key = `${element}_${event}`;
|
|
if (this.listeners.has(key)) {
|
|
this.listeners.get(key).delete(handler);
|
|
if (this.listeners.get(key).size === 0) {
|
|
this.listeners.delete(key);
|
|
}
|
|
}
|
|
element.removeEventListener(event, handler);
|
|
},
|
|
|
|
once(element, event, handler, options = {}) {
|
|
const onceHandler = (e) => {
|
|
handler(e);
|
|
this.off(element, event, onceHandler);
|
|
};
|
|
this.on(element, event, onceHandler, options);
|
|
},
|
|
|
|
emit(name, detail = {}) {
|
|
const event = new CustomEvent(name, {
|
|
detail,
|
|
bubbles: true,
|
|
cancelable: true
|
|
});
|
|
document.dispatchEvent(event);
|
|
},
|
|
|
|
cleanup() {
|
|
this.listeners.forEach((handlers, key) => {
|
|
const [element, event] = key.split('_');
|
|
handlers.forEach(handler => {
|
|
element.removeEventListener(event, handler);
|
|
});
|
|
});
|
|
this.listeners.clear();
|
|
}
|
|
};
|
|
|
|
// ===== FORM UTILITIES =====
|
|
const forms = {
|
|
serialize(form) {
|
|
const data = new FormData(form);
|
|
const obj = {};
|
|
for (const [key, value] of data.entries()) {
|
|
if (obj[key]) {
|
|
if (!Array.isArray(obj[key])) {
|
|
obj[key] = [obj[key]];
|
|
}
|
|
obj[key].push(value);
|
|
} else {
|
|
obj[key] = value;
|
|
}
|
|
}
|
|
return obj;
|
|
},
|
|
|
|
validate(form) {
|
|
const inputs = form.querySelectorAll('[required]');
|
|
let valid = true;
|
|
|
|
inputs.forEach(input => {
|
|
if (!input.value.trim()) {
|
|
input.classList.add('border-red-500');
|
|
valid = false;
|
|
} else {
|
|
input.classList.remove('border-red-500');
|
|
}
|
|
});
|
|
|
|
return valid;
|
|
},
|
|
|
|
reset(form) {
|
|
form.reset();
|
|
form.querySelectorAll('.border-red-500').forEach(el => {
|
|
el.classList.remove('border-red-500');
|
|
});
|
|
}
|
|
};
|
|
|
|
// ===== INITIALIZATION =====
|
|
const init = () => {
|
|
// Initialize notifications
|
|
notifications.init();
|
|
|
|
// Clean up on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
events.cleanup();
|
|
dom.clearCache();
|
|
});
|
|
};
|
|
|
|
// ===== PUBLIC API =====
|
|
MYP.utils = {
|
|
csrf,
|
|
dom,
|
|
api,
|
|
notifications,
|
|
performance,
|
|
storage,
|
|
events,
|
|
forms,
|
|
init,
|
|
config
|
|
};
|
|
|
|
// Auto-initialize when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
// Export to global scope
|
|
window.MYP = MYP;
|
|
|
|
})(window); |