- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
642 lines
16 KiB
Markdown
642 lines
16 KiB
Markdown
# JavaScript Module System
|
|
|
|
> Modern ES6+ modular JavaScript architecture with performance monitoring, state management, and component-based UI interactions.
|
|
|
|
## 📁 Module Structure
|
|
|
|
```
|
|
resources/js/
|
|
├── main.js # Entry point
|
|
├── core/ # Core framework modules
|
|
│ ├── index.js # Core exports
|
|
│ ├── init.js # Initialization
|
|
│ ├── router.js # SPA routing
|
|
│ ├── state.js # State management
|
|
│ ├── EventManager.js # Event system
|
|
│ ├── PerformanceMonitor.js # Performance tracking
|
|
│ └── logger.js # Logging utilities
|
|
├── modules/ # Feature modules
|
|
│ ├── index.js # Module registry
|
|
│ ├── ui/ # UI components
|
|
│ │ ├── UIManager.js # UI coordinator
|
|
│ │ └── components/ # Individual components
|
|
│ ├── scroll-fx/ # Scroll animations
|
|
│ ├── lightbox/ # Image lightbox
|
|
│ └── parallax/ # Parallax effects
|
|
├── utils/ # Utility functions
|
|
└── docs/ # Module documentation
|
|
```
|
|
|
|
## 🚀 Core System
|
|
|
|
### Application Initialization
|
|
|
|
```javascript
|
|
// main.js - Application entry point
|
|
import { init } from './core/init.js';
|
|
import { ModuleRegistry } from './modules/index.js';
|
|
|
|
// Initialize core systems
|
|
await init({
|
|
performance: true,
|
|
router: true,
|
|
state: true,
|
|
logging: 'development'
|
|
});
|
|
|
|
// Register and load modules
|
|
ModuleRegistry.register('ui', () => import('./modules/ui/index.js'));
|
|
ModuleRegistry.register('scrollfx', () => import('./modules/scrollfx/index.js'));
|
|
|
|
// Auto-load modules based on DOM attributes
|
|
ModuleRegistry.autoLoad();
|
|
```
|
|
|
|
### Router System
|
|
|
|
```javascript
|
|
// SPA routing with layout animations
|
|
import { Router } from './core/router.js';
|
|
|
|
const router = new Router({
|
|
mode: 'history',
|
|
base: '/',
|
|
transitions: true,
|
|
prefetch: true
|
|
});
|
|
|
|
// Route definitions
|
|
router.addRoute('/admin/:page?', async (ctx) => {
|
|
const { page = 'dashboard' } = ctx.params;
|
|
|
|
// Layout animation
|
|
if (ctx.isLayoutChange) {
|
|
await animateLayoutSwitch('admin');
|
|
}
|
|
|
|
// Load page content
|
|
return await loadAdminPage(page);
|
|
});
|
|
|
|
// Meta data extraction from HTML
|
|
router.onNavigate((ctx) => {
|
|
// Extract and apply meta data
|
|
const metaTitle = ctx.dom.querySelector('[data-meta-title]');
|
|
if (metaTitle) {
|
|
document.title = metaTitle.dataset.metaTitle;
|
|
}
|
|
|
|
const metaTheme = ctx.dom.querySelector('[data-meta-theme]');
|
|
if (metaTheme) {
|
|
document.documentElement.style.setProperty('--theme-color', metaTheme.dataset.metaTheme);
|
|
}
|
|
});
|
|
```
|
|
|
|
### State Management
|
|
|
|
```javascript
|
|
// Reactive state system
|
|
import { State } from './core/state.js';
|
|
|
|
// Global state
|
|
const appState = new State({
|
|
user: null,
|
|
theme: 'light',
|
|
admin: {
|
|
currentPage: 'dashboard',
|
|
notifications: []
|
|
}
|
|
});
|
|
|
|
// Reactive updates
|
|
appState.watch('theme', (newTheme, oldTheme) => {
|
|
document.documentElement.dataset.theme = newTheme;
|
|
localStorage.setItem('preferred-theme', newTheme);
|
|
});
|
|
|
|
// Component state binding
|
|
appState.bind('[data-user-name]', 'user.name');
|
|
appState.bind('[data-notification-count]', 'admin.notifications.length');
|
|
```
|
|
|
|
### Event System
|
|
|
|
```javascript
|
|
// Centralized event management
|
|
import { EventManager } from './core/EventManager.js';
|
|
|
|
const events = new EventManager();
|
|
|
|
// Global event delegation
|
|
events.delegate('click', '[data-action]', (event, element) => {
|
|
const action = element.dataset.action;
|
|
const target = element.dataset.target;
|
|
|
|
switch (action) {
|
|
case 'toggle-theme':
|
|
appState.set('theme', appState.get('theme') === 'light' ? 'dark' : 'light');
|
|
break;
|
|
case 'show-modal':
|
|
UIManager.showModal(target);
|
|
break;
|
|
}
|
|
});
|
|
|
|
// Custom events
|
|
events.on('admin:page-change', (data) => {
|
|
appState.set('admin.currentPage', data.page);
|
|
PerformanceMonitor.mark(`admin-${data.page}-loaded`);
|
|
});
|
|
```
|
|
|
|
## 🧩 UI Component System
|
|
|
|
### Component Architecture
|
|
|
|
```javascript
|
|
// Base component class
|
|
class BaseComponent {
|
|
constructor(element, options = {}) {
|
|
this.element = element;
|
|
this.options = { ...this.defaults, ...options };
|
|
this.state = new State(this.initialState);
|
|
|
|
this.init();
|
|
this.bindEvents();
|
|
}
|
|
|
|
init() {
|
|
// Override in subclasses
|
|
}
|
|
|
|
bindEvents() {
|
|
// Override in subclasses
|
|
}
|
|
|
|
destroy() {
|
|
this.state.destroy();
|
|
this.element.removeEventListener();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Modal Component
|
|
|
|
```javascript
|
|
// UI Modal component
|
|
class Modal extends BaseComponent {
|
|
defaults = {
|
|
closeOnOverlay: true,
|
|
closeOnEscape: true,
|
|
animation: 'fade'
|
|
};
|
|
|
|
initialState = {
|
|
isOpen: false,
|
|
content: null
|
|
};
|
|
|
|
init() {
|
|
this.overlay = this.element.querySelector('.modal-overlay');
|
|
this.content = this.element.querySelector('.modal-content');
|
|
this.closeBtn = this.element.querySelector('[data-modal-close]');
|
|
|
|
// State reactivity
|
|
this.state.watch('isOpen', (isOpen) => {
|
|
this.element.classList.toggle('is-open', isOpen);
|
|
this.element.setAttribute('aria-hidden', !isOpen);
|
|
|
|
if (isOpen) {
|
|
this.trapFocus();
|
|
} else {
|
|
this.releaseFocus();
|
|
}
|
|
});
|
|
}
|
|
|
|
bindEvents() {
|
|
if (this.closeBtn) {
|
|
this.closeBtn.addEventListener('click', () => this.close());
|
|
}
|
|
|
|
if (this.options.closeOnOverlay) {
|
|
this.overlay.addEventListener('click', () => this.close());
|
|
}
|
|
|
|
if (this.options.closeOnEscape) {
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && this.state.get('isOpen')) {
|
|
this.close();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
open(content = null) {
|
|
if (content) {
|
|
this.setContent(content);
|
|
}
|
|
this.state.set('isOpen', true);
|
|
events.emit('modal:opened', { modal: this });
|
|
}
|
|
|
|
close() {
|
|
this.state.set('isOpen', false);
|
|
events.emit('modal:closed', { modal: this });
|
|
}
|
|
|
|
setContent(content) {
|
|
if (typeof content === 'string') {
|
|
this.content.innerHTML = content;
|
|
} else if (content instanceof HTMLElement) {
|
|
this.content.innerHTML = '';
|
|
this.content.appendChild(content);
|
|
}
|
|
this.state.set('content', content);
|
|
}
|
|
}
|
|
|
|
// Auto-initialization
|
|
document.querySelectorAll('[data-modal]').forEach(element => {
|
|
new Modal(element);
|
|
});
|
|
```
|
|
|
|
### Admin-specific Components
|
|
|
|
```javascript
|
|
// Admin Stats Card
|
|
class AdminStatsCard extends BaseComponent {
|
|
defaults = {
|
|
updateInterval: 30000,
|
|
animateChanges: true
|
|
};
|
|
|
|
init() {
|
|
this.valueElement = this.element.querySelector('.stat-value');
|
|
this.labelElement = this.element.querySelector('.stat-label');
|
|
this.trendElement = this.element.querySelector('.stat-trend');
|
|
|
|
if (this.options.updateInterval) {
|
|
this.startPolling();
|
|
}
|
|
}
|
|
|
|
startPolling() {
|
|
this.pollInterval = setInterval(() => {
|
|
this.updateValue();
|
|
}, this.options.updateInterval);
|
|
}
|
|
|
|
async updateValue() {
|
|
const endpoint = this.element.dataset.endpoint;
|
|
if (!endpoint) return;
|
|
|
|
try {
|
|
const response = await fetch(endpoint);
|
|
const data = await response.json();
|
|
|
|
if (this.options.animateChanges) {
|
|
this.animateValueChange(data.value);
|
|
} else {
|
|
this.setValue(data.value);
|
|
}
|
|
|
|
if (data.trend) {
|
|
this.setTrend(data.trend);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to update stat:', error);
|
|
}
|
|
}
|
|
|
|
animateValueChange(newValue) {
|
|
const currentValue = parseInt(this.valueElement.textContent) || 0;
|
|
const duration = 1000;
|
|
const steps = 60;
|
|
const increment = (newValue - currentValue) / steps;
|
|
|
|
let step = 0;
|
|
const timer = setInterval(() => {
|
|
step++;
|
|
const value = Math.round(currentValue + (increment * step));
|
|
this.valueElement.textContent = value.toLocaleString();
|
|
|
|
if (step >= steps) {
|
|
clearInterval(timer);
|
|
this.valueElement.textContent = newValue.toLocaleString();
|
|
}
|
|
}, duration / steps);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 📊 Performance Monitoring
|
|
|
|
```javascript
|
|
// Performance tracking
|
|
import { PerformanceMonitor } from './core/PerformanceMonitor.js';
|
|
|
|
// Page load metrics
|
|
PerformanceMonitor.mark('page-start');
|
|
PerformanceMonitor.measure('page-load', 'page-start', 'page-end');
|
|
|
|
// Component performance
|
|
class ComponentWithMetrics extends BaseComponent {
|
|
init() {
|
|
PerformanceMonitor.mark(`${this.constructor.name}-init-start`);
|
|
|
|
// Component initialization
|
|
super.init();
|
|
|
|
PerformanceMonitor.mark(`${this.constructor.name}-init-end`);
|
|
PerformanceMonitor.measure(
|
|
`${this.constructor.name}-init`,
|
|
`${this.constructor.name}-init-start`,
|
|
`${this.constructor.name}-init-end`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Performance reporting
|
|
PerformanceMonitor.report((metrics) => {
|
|
// Send to analytics
|
|
if (window.gtag) {
|
|
gtag('event', 'performance_metric', {
|
|
custom_map: { metric_name: 'custom_metric_name' },
|
|
metric_name: metrics.name,
|
|
value: metrics.duration
|
|
});
|
|
}
|
|
});
|
|
```
|
|
|
|
## 🎨 Admin Interface Integration
|
|
|
|
### Theme System
|
|
|
|
```javascript
|
|
// Admin theme management
|
|
class AdminThemeManager {
|
|
constructor() {
|
|
this.themes = ['light', 'dark', 'auto'];
|
|
this.currentTheme = localStorage.getItem('admin-theme') || 'auto';
|
|
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.applyTheme(this.currentTheme);
|
|
this.bindEvents();
|
|
}
|
|
|
|
bindEvents() {
|
|
// Theme toggle button
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.matches('[data-theme-toggle]')) {
|
|
this.toggleTheme();
|
|
}
|
|
});
|
|
|
|
// System theme changes
|
|
this.mediaQuery.addEventListener('change', () => {
|
|
if (this.currentTheme === 'auto') {
|
|
this.applyTheme('auto');
|
|
}
|
|
});
|
|
}
|
|
|
|
applyTheme(theme) {
|
|
let resolvedTheme = theme;
|
|
|
|
if (theme === 'auto') {
|
|
resolvedTheme = this.mediaQuery.matches ? 'dark' : 'light';
|
|
}
|
|
|
|
document.documentElement.dataset.theme = resolvedTheme;
|
|
document.documentElement.style.setProperty('--theme-preference', theme);
|
|
|
|
// Update theme color meta tag
|
|
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
|
|
if (metaThemeColor) {
|
|
const color = resolvedTheme === 'dark' ? '#1e293b' : '#ffffff';
|
|
metaThemeColor.setAttribute('content', color);
|
|
}
|
|
}
|
|
|
|
toggleTheme() {
|
|
const currentIndex = this.themes.indexOf(this.currentTheme);
|
|
const nextIndex = (currentIndex + 1) % this.themes.length;
|
|
const nextTheme = this.themes[nextIndex];
|
|
|
|
this.setTheme(nextTheme);
|
|
}
|
|
|
|
setTheme(theme) {
|
|
this.currentTheme = theme;
|
|
localStorage.setItem('admin-theme', theme);
|
|
this.applyTheme(theme);
|
|
|
|
events.emit('theme:changed', { theme, resolvedTheme: this.getResolvedTheme() });
|
|
}
|
|
|
|
getResolvedTheme() {
|
|
if (this.currentTheme === 'auto') {
|
|
return this.mediaQuery.matches ? 'dark' : 'light';
|
|
}
|
|
return this.currentTheme;
|
|
}
|
|
}
|
|
|
|
// Initialize admin theme
|
|
new AdminThemeManager();
|
|
```
|
|
|
|
### Data Tables
|
|
|
|
```javascript
|
|
// Admin data table component
|
|
class AdminDataTable extends BaseComponent {
|
|
defaults = {
|
|
sortable: true,
|
|
filterable: true,
|
|
paginated: true,
|
|
pageSize: 25
|
|
};
|
|
|
|
init() {
|
|
this.table = this.element.querySelector('table');
|
|
this.tbody = this.table.querySelector('tbody');
|
|
this.headers = [...this.table.querySelectorAll('th[data-sort]')];
|
|
this.filterInput = this.element.querySelector('[data-table-filter]');
|
|
|
|
this.data = this.extractData();
|
|
this.filteredData = [...this.data];
|
|
this.currentSort = { column: null, direction: 'asc' };
|
|
this.currentPage = 1;
|
|
|
|
this.bindEvents();
|
|
this.render();
|
|
}
|
|
|
|
bindEvents() {
|
|
// Column sorting
|
|
this.headers.forEach(header => {
|
|
header.addEventListener('click', () => {
|
|
const column = header.dataset.sort;
|
|
this.sort(column);
|
|
});
|
|
});
|
|
|
|
// Filtering
|
|
if (this.filterInput) {
|
|
this.filterInput.addEventListener('input', debounce(() => {
|
|
this.filter(this.filterInput.value);
|
|
}, 300));
|
|
}
|
|
}
|
|
|
|
sort(column) {
|
|
if (this.currentSort.column === column) {
|
|
this.currentSort.direction = this.currentSort.direction === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
this.currentSort.column = column;
|
|
this.currentSort.direction = 'asc';
|
|
}
|
|
|
|
this.filteredData.sort((a, b) => {
|
|
const aVal = a[column];
|
|
const bVal = b[column];
|
|
const modifier = this.currentSort.direction === 'asc' ? 1 : -1;
|
|
|
|
if (aVal < bVal) return -1 * modifier;
|
|
if (aVal > bVal) return 1 * modifier;
|
|
return 0;
|
|
});
|
|
|
|
this.currentPage = 1;
|
|
this.render();
|
|
}
|
|
|
|
filter(query) {
|
|
if (!query) {
|
|
this.filteredData = [...this.data];
|
|
} else {
|
|
const searchTerm = query.toLowerCase();
|
|
this.filteredData = this.data.filter(row => {
|
|
return Object.values(row).some(value =>
|
|
String(value).toLowerCase().includes(searchTerm)
|
|
);
|
|
});
|
|
}
|
|
|
|
this.currentPage = 1;
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
// Update table body
|
|
this.renderTableBody();
|
|
|
|
// Update sort indicators
|
|
this.updateSortIndicators();
|
|
|
|
// Update pagination
|
|
if (this.options.paginated) {
|
|
this.renderPagination();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🔧 Utility Functions
|
|
|
|
```javascript
|
|
// Common utility functions
|
|
export const utils = {
|
|
// Debounce function calls
|
|
debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
},
|
|
|
|
// Throttle function calls
|
|
throttle(func, limit) {
|
|
let inThrottle;
|
|
return function(...args) {
|
|
if (!inThrottle) {
|
|
func.apply(this, args);
|
|
inThrottle = true;
|
|
setTimeout(() => inThrottle = false, limit);
|
|
}
|
|
};
|
|
},
|
|
|
|
// DOM manipulation helpers
|
|
dom: {
|
|
ready(fn) {
|
|
if (document.readyState !== 'loading') {
|
|
fn();
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', fn);
|
|
}
|
|
},
|
|
|
|
create(tag, attributes = {}, children = []) {
|
|
const element = document.createElement(tag);
|
|
|
|
Object.entries(attributes).forEach(([key, value]) => {
|
|
if (key === 'className') {
|
|
element.className = value;
|
|
} else if (key.startsWith('data-')) {
|
|
element.dataset[key.slice(5)] = value;
|
|
} else {
|
|
element.setAttribute(key, value);
|
|
}
|
|
});
|
|
|
|
children.forEach(child => {
|
|
if (typeof child === 'string') {
|
|
element.appendChild(document.createTextNode(child));
|
|
} else {
|
|
element.appendChild(child);
|
|
}
|
|
});
|
|
|
|
return element;
|
|
}
|
|
},
|
|
|
|
// Format utilities
|
|
format: {
|
|
bytes(bytes, decimals = 2) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
|
},
|
|
|
|
duration(ms) {
|
|
if (ms < 1000) return `${ms}ms`;
|
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
return `${(ms / 60000).toFixed(1)}m`;
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
*For specific module documentation, see individual files in `/resources/js/docs/`*
|
|
*For performance optimization, see [Performance Guidelines](../development/performance.md)*
|
|
*For component integration, see [UI Components](components.md)* |