feat: ledlab
This commit is contained in:
759
public/scripts/framework.js
Normal file
759
public/scripts/framework.js
Normal file
@@ -0,0 +1,759 @@
|
||||
// SPORE LEDLab Framework - Component-based architecture with pub/sub system
|
||||
|
||||
// Lightweight logger with level gating
|
||||
const logger = {
|
||||
debug: (...args) => { try { if (window && window.DEBUG) { console.debug(...args); } } catch (_) { /* no-op */ } },
|
||||
info: (...args) => console.info(...args),
|
||||
warn: (...args) => console.warn(...args),
|
||||
error: (...args) => console.error(...args),
|
||||
};
|
||||
if (typeof window !== 'undefined') {
|
||||
window.logger = window.logger || logger;
|
||||
}
|
||||
|
||||
// Event Bus for pub/sub communication
|
||||
class EventBus {
|
||||
constructor() {
|
||||
this.events = new Map();
|
||||
}
|
||||
|
||||
// Subscribe to an event
|
||||
subscribe(event, callback) {
|
||||
if (!this.events.has(event)) {
|
||||
this.events.set(event, []);
|
||||
}
|
||||
this.events.get(event).push(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const callbacks = this.events.get(event);
|
||||
if (callbacks) {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Publish an event
|
||||
publish(event, data) {
|
||||
if (this.events.has(event)) {
|
||||
this.events.get(event).forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`Error in event callback for ${event}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Unsubscribe from an event
|
||||
unsubscribe(event, callback) {
|
||||
if (this.events.has(event)) {
|
||||
const callbacks = this.events.get(event);
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all events
|
||||
clear() {
|
||||
this.events.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Base ViewModel class with enhanced state management
|
||||
class ViewModel {
|
||||
constructor() {
|
||||
this._data = {};
|
||||
this._listeners = new Map();
|
||||
this._eventBus = null;
|
||||
this._uiState = new Map(); // Store UI state like active tabs, expanded cards, etc.
|
||||
this._previousData = {}; // Store previous data for change detection
|
||||
}
|
||||
|
||||
// Set the event bus for this view model
|
||||
setEventBus(eventBus) {
|
||||
this._eventBus = eventBus;
|
||||
}
|
||||
|
||||
// Get data property
|
||||
get(property) {
|
||||
return this._data[property];
|
||||
}
|
||||
|
||||
// Set data property and notify listeners
|
||||
set(property, value) {
|
||||
logger.debug(`ViewModel: Setting property '${property}' to:`, value);
|
||||
|
||||
// Check if the value has actually changed
|
||||
const hasChanged = this._data[property] !== value;
|
||||
|
||||
if (hasChanged) {
|
||||
// Store previous value for change detection
|
||||
this._previousData[property] = this._data[property];
|
||||
|
||||
// Update the data
|
||||
this._data[property] = value;
|
||||
|
||||
logger.debug(`ViewModel: Property '${property}' changed, notifying listeners...`);
|
||||
this._notifyListeners(property, value, this._previousData[property]);
|
||||
} else {
|
||||
logger.debug(`ViewModel: Property '${property}' unchanged, skipping notification`);
|
||||
}
|
||||
}
|
||||
|
||||
// Set multiple properties at once with change detection
|
||||
setMultiple(properties) {
|
||||
const changedProperties = {};
|
||||
|
||||
// Determine changes and update previousData snapshot per key
|
||||
Object.keys(properties).forEach(key => {
|
||||
const newValue = properties[key];
|
||||
const oldValue = this._data[key];
|
||||
if (oldValue !== newValue) {
|
||||
this._previousData[key] = oldValue;
|
||||
changedProperties[key] = newValue;
|
||||
}
|
||||
});
|
||||
|
||||
// Apply all properties
|
||||
Object.keys(properties).forEach(key => {
|
||||
this._data[key] = properties[key];
|
||||
});
|
||||
|
||||
// Notify listeners only for changed properties with accurate previous values
|
||||
Object.keys(changedProperties).forEach(key => {
|
||||
this._notifyListeners(key, this._data[key], this._previousData[key]);
|
||||
});
|
||||
|
||||
if (Object.keys(changedProperties).length > 0) {
|
||||
logger.debug(`ViewModel: Updated ${Object.keys(changedProperties).length} changed properties:`, Object.keys(changedProperties));
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to property changes
|
||||
subscribe(property, callback) {
|
||||
if (!this._listeners.has(property)) {
|
||||
this._listeners.set(property, []);
|
||||
}
|
||||
this._listeners.get(property).push(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const callbacks = this._listeners.get(property);
|
||||
if (callbacks) {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Notify listeners of property changes
|
||||
_notifyListeners(property, value, previousValue) {
|
||||
logger.debug(`ViewModel: _notifyListeners called for property '${property}'`);
|
||||
if (this._listeners.has(property)) {
|
||||
const callbacks = this._listeners.get(property);
|
||||
logger.debug(`ViewModel: Found ${callbacks.length} listeners for property '${property}'`);
|
||||
callbacks.forEach((callback, index) => {
|
||||
try {
|
||||
logger.debug(`ViewModel: Calling listener ${index} for property '${property}'`);
|
||||
callback(value, previousValue);
|
||||
} catch (error) {
|
||||
console.error(`Error in property listener for ${property}:`, error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.debug(`ViewModel: No listeners found for property '${property}'`);
|
||||
}
|
||||
}
|
||||
|
||||
// Publish event to event bus
|
||||
publish(event, data) {
|
||||
if (this._eventBus) {
|
||||
this._eventBus.publish(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all data
|
||||
getAll() {
|
||||
return { ...this._data };
|
||||
}
|
||||
|
||||
// Clear all data
|
||||
clear() {
|
||||
this._data = {};
|
||||
this._listeners.clear();
|
||||
}
|
||||
|
||||
// UI State Management Methods
|
||||
setUIState(key, value) {
|
||||
this._uiState.set(key, value);
|
||||
}
|
||||
|
||||
getUIState(key) {
|
||||
return this._uiState.get(key);
|
||||
}
|
||||
|
||||
getAllUIState() {
|
||||
return new Map(this._uiState);
|
||||
}
|
||||
|
||||
clearUIState(key) {
|
||||
if (key) {
|
||||
this._uiState.delete(key);
|
||||
} else {
|
||||
this._uiState.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a property has changed
|
||||
hasChanged(property) {
|
||||
return this._data[property] !== this._previousData[property];
|
||||
}
|
||||
|
||||
// Get previous value of a property
|
||||
getPrevious(property) {
|
||||
return this._previousData[property];
|
||||
}
|
||||
|
||||
// Batch update with change detection
|
||||
batchUpdate(updates, options = {}) {
|
||||
const { notifyChanges = true } = options;
|
||||
|
||||
// Track which keys actually change and what the previous values were
|
||||
const changedKeys = [];
|
||||
Object.keys(updates).forEach(key => {
|
||||
const newValue = updates[key];
|
||||
const oldValue = this._data[key];
|
||||
if (oldValue !== newValue) {
|
||||
this._previousData[key] = oldValue;
|
||||
this._data[key] = newValue;
|
||||
changedKeys.push(key);
|
||||
} else {
|
||||
// Still apply to ensure consistency if needed
|
||||
this._data[key] = newValue;
|
||||
}
|
||||
});
|
||||
|
||||
// Notify listeners for changed keys
|
||||
if (notifyChanges) {
|
||||
changedKeys.forEach(key => {
|
||||
this._notifyListeners(key, this._data[key], this._previousData[key]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Base Component class
|
||||
class Component {
|
||||
constructor(container, viewModel, eventBus) {
|
||||
this.container = container;
|
||||
this.viewModel = viewModel;
|
||||
this.eventBus = eventBus;
|
||||
this.isMounted = false;
|
||||
this.unsubscribers = [];
|
||||
this.uiState = new Map(); // Local UI state for this component
|
||||
|
||||
// Set event bus on view model
|
||||
if (this.viewModel) {
|
||||
this.viewModel.setEventBus(eventBus);
|
||||
}
|
||||
|
||||
// Bind methods
|
||||
this.render = this.render.bind(this);
|
||||
this.mount = this.mount.bind(this);
|
||||
this.unmount = this.unmount.bind(this);
|
||||
this.updatePartial = this.updatePartial.bind(this);
|
||||
}
|
||||
|
||||
// Mount the component
|
||||
mount() {
|
||||
if (this.isMounted) return;
|
||||
|
||||
logger.debug(`${this.constructor.name}: Starting mount...`);
|
||||
this.isMounted = true;
|
||||
this.setupEventListeners();
|
||||
this.setupViewModelListeners();
|
||||
this.render();
|
||||
|
||||
logger.debug(`${this.constructor.name}: Mounted successfully`);
|
||||
}
|
||||
|
||||
// Unmount the component
|
||||
unmount() {
|
||||
if (!this.isMounted) return;
|
||||
|
||||
this.isMounted = false;
|
||||
this.cleanupEventListeners();
|
||||
this.cleanupViewModelListeners();
|
||||
|
||||
logger.debug(`${this.constructor.name} unmounted`);
|
||||
}
|
||||
|
||||
// Setup event listeners (override in subclasses)
|
||||
setupEventListeners() {
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
// Setup view model listeners (override in subclasses)
|
||||
setupViewModelListeners() {
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
// Cleanup event listeners (override in subclasses)
|
||||
cleanupEventListeners() {
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
// Cleanup view model listeners (override in subclasses)
|
||||
cleanupViewModelListeners() {
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
// Render the component (override in subclasses)
|
||||
render() {
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
// Partial update method for efficient data updates
|
||||
updatePartial(property, newValue, previousValue) {
|
||||
// Override in subclasses to implement partial updates
|
||||
logger.debug(`${this.constructor.name}: Partial update for '${property}':`, { newValue, previousValue });
|
||||
}
|
||||
|
||||
// UI State Management Methods
|
||||
setUIState(key, value) {
|
||||
this.uiState.set(key, value);
|
||||
// Also store in view model for persistence across refreshes
|
||||
if (this.viewModel) {
|
||||
this.viewModel.setUIState(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
getUIState(key) {
|
||||
// First try local state, then view model state
|
||||
return this.uiState.get(key) || (this.viewModel ? this.viewModel.getUIState(key) : null);
|
||||
}
|
||||
|
||||
getAllUIState() {
|
||||
const localState = new Map(this.uiState);
|
||||
const viewModelState = this.viewModel ? this.viewModel.getAllUIState() : new Map();
|
||||
|
||||
// Merge states, with local state taking precedence
|
||||
const mergedState = new Map(viewModelState);
|
||||
localState.forEach((value, key) => mergedState.set(key, value));
|
||||
|
||||
return mergedState;
|
||||
}
|
||||
|
||||
clearUIState(key) {
|
||||
if (key) {
|
||||
this.uiState.delete(key);
|
||||
if (this.viewModel) {
|
||||
this.viewModel.clearUIState(key);
|
||||
}
|
||||
} else {
|
||||
this.uiState.clear();
|
||||
if (this.viewModel) {
|
||||
this.viewModel.clearUIState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to add event listener and track for cleanup
|
||||
addEventListener(element, event, handler) {
|
||||
element.addEventListener(event, handler);
|
||||
this.unsubscribers.push(() => {
|
||||
element.removeEventListener(event, handler);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper method to subscribe to event bus and track for cleanup
|
||||
subscribeToEvent(event, handler) {
|
||||
const unsubscribe = this.eventBus.subscribe(event, handler);
|
||||
this.unsubscribers.push(unsubscribe);
|
||||
}
|
||||
|
||||
// Helper method to subscribe to view model property and track for cleanup
|
||||
subscribeToProperty(property, handler) {
|
||||
if (this.viewModel) {
|
||||
const unsubscribe = this.viewModel.subscribe(property, (newValue, previousValue) => {
|
||||
// Call handler with both new and previous values for change detection
|
||||
handler(newValue, previousValue);
|
||||
});
|
||||
this.unsubscribers.push(unsubscribe);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to find element within component container
|
||||
findElement(selector) {
|
||||
return this.container.querySelector(selector);
|
||||
}
|
||||
|
||||
// Helper method to find all elements within component container
|
||||
findAllElements(selector) {
|
||||
return this.container.querySelectorAll(selector);
|
||||
}
|
||||
|
||||
// Helper method to set innerHTML safely
|
||||
setHTML(selector, html) {
|
||||
logger.debug(`${this.constructor.name}: setHTML called with selector '${selector}', html length: ${html.length}`);
|
||||
|
||||
let element;
|
||||
if (selector === '') {
|
||||
// Empty selector means set HTML on the component's container itself
|
||||
element = this.container;
|
||||
logger.debug(`${this.constructor.name}: Using component container for empty selector`);
|
||||
} else {
|
||||
// Find element within the component's container
|
||||
element = this.findElement(selector);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
logger.debug(`${this.constructor.name}: Element found, setting innerHTML`);
|
||||
element.innerHTML = html;
|
||||
logger.debug(`${this.constructor.name}: innerHTML set successfully`);
|
||||
} else {
|
||||
console.error(`${this.constructor.name}: Element not found for selector '${selector}'`);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to set text content safely
|
||||
setText(selector, text) {
|
||||
const element = this.findElement(selector);
|
||||
if (element) {
|
||||
element.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to add/remove CSS classes
|
||||
setClass(selector, className, add = true) {
|
||||
const element = this.findElement(selector);
|
||||
if (element) {
|
||||
if (add) {
|
||||
element.classList.add(className);
|
||||
} else {
|
||||
element.classList.remove(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to set CSS styles
|
||||
setStyle(selector, property, value) {
|
||||
const element = this.findElement(selector);
|
||||
if (element) {
|
||||
element.style[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to show/hide elements
|
||||
setVisible(selector, visible) {
|
||||
const element = this.findElement(selector);
|
||||
if (element) {
|
||||
element.style.display = visible ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to enable/disable elements
|
||||
setEnabled(selector, enabled) {
|
||||
const element = this.findElement(selector);
|
||||
if (element) {
|
||||
element.disabled = !enabled;
|
||||
}
|
||||
}
|
||||
|
||||
// Reusable render helpers
|
||||
renderLoading(customHtml) {
|
||||
const html = customHtml || `
|
||||
<div class="loading">
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
`;
|
||||
this.setHTML('', html);
|
||||
}
|
||||
|
||||
renderError(message) {
|
||||
const safe = this.escapeHtml(String(message || 'An error occurred'));
|
||||
const html = `
|
||||
<div class="error">
|
||||
<strong>Error:</strong><br>
|
||||
${safe}
|
||||
</div>
|
||||
`;
|
||||
this.setHTML('', html);
|
||||
}
|
||||
|
||||
renderEmpty(customHtml) {
|
||||
const html = customHtml || `
|
||||
<div class="empty-state">
|
||||
<div>No data</div>
|
||||
</div>
|
||||
`;
|
||||
this.setHTML('', html);
|
||||
}
|
||||
|
||||
// Basic HTML escaping for dynamic values
|
||||
escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
}
|
||||
|
||||
// Application class to manage components and routing
|
||||
class App {
|
||||
constructor() {
|
||||
this.eventBus = new EventBus();
|
||||
this.components = new Map();
|
||||
this.currentView = null;
|
||||
this.routes = new Map();
|
||||
this.navigationInProgress = false;
|
||||
this.navigationQueue = [];
|
||||
this.lastNavigationTime = 0;
|
||||
this.navigationCooldown = (window.CONSTANTS && window.CONSTANTS.TIMING.NAV_COOLDOWN_MS) || 300;
|
||||
|
||||
// Component cache to keep components alive
|
||||
this.componentCache = new Map();
|
||||
this.cachedViews = new Set();
|
||||
}
|
||||
|
||||
// Register a route
|
||||
registerRoute(name, componentClass, containerId, viewModel = null) {
|
||||
this.routes.set(name, { componentClass, containerId, viewModel });
|
||||
}
|
||||
|
||||
// Navigate to a route
|
||||
navigateTo(routeName) {
|
||||
// Check cooldown period
|
||||
const now = Date.now();
|
||||
if (now - this.lastNavigationTime < this.navigationCooldown) {
|
||||
logger.debug(`App: Navigation cooldown active, skipping route '${routeName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If navigation is already in progress, queue this request
|
||||
if (this.navigationInProgress) {
|
||||
logger.debug(`App: Navigation in progress, queuing route '${routeName}'`);
|
||||
if (!this.navigationQueue.includes(routeName)) {
|
||||
this.navigationQueue.push(routeName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If trying to navigate to the same route, do nothing
|
||||
if (this.currentView && this.currentView.routeName === routeName) {
|
||||
logger.debug(`App: Already on route '${routeName}', skipping navigation`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastNavigationTime = now;
|
||||
this.performNavigation(routeName);
|
||||
}
|
||||
|
||||
// Perform the actual navigation
|
||||
async performNavigation(routeName) {
|
||||
this.navigationInProgress = true;
|
||||
|
||||
try {
|
||||
logger.debug(`App: Navigating to route '${routeName}'`);
|
||||
const route = this.routes.get(routeName);
|
||||
if (!route) {
|
||||
console.error(`Route '${routeName}' not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`App: Route found, component: ${route.componentClass.name}, container: ${route.containerId}`);
|
||||
|
||||
// Get or create component from cache
|
||||
let component = this.componentCache.get(routeName);
|
||||
if (!component) {
|
||||
logger.debug(`App: Component not in cache, creating new instance for '${routeName}'`);
|
||||
const container = document.getElementById(route.containerId);
|
||||
if (!container) {
|
||||
console.error(`Container '${route.containerId}' not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
component = new route.componentClass(container, route.viewModel, this.eventBus);
|
||||
component.routeName = routeName;
|
||||
component.isCached = true;
|
||||
this.componentCache.set(routeName, component);
|
||||
}
|
||||
|
||||
// Hide current view smoothly
|
||||
if (this.currentView) {
|
||||
logger.debug('App: Hiding current view');
|
||||
await this.hideCurrentView();
|
||||
}
|
||||
|
||||
// Show new view
|
||||
logger.debug(`App: Showing new view '${routeName}'`);
|
||||
await this.showView(routeName, component);
|
||||
|
||||
// Update navigation state
|
||||
this.updateNavigation(routeName);
|
||||
|
||||
// Set as current view
|
||||
this.currentView = component;
|
||||
|
||||
// Mark view as cached for future use
|
||||
this.cachedViews.add(routeName);
|
||||
|
||||
logger.debug(`App: Navigation to '${routeName}' completed`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('App: Navigation failed:', error);
|
||||
} finally {
|
||||
this.navigationInProgress = false;
|
||||
|
||||
// Process any queued navigation requests
|
||||
if (this.navigationQueue.length > 0) {
|
||||
const nextRoute = this.navigationQueue.shift();
|
||||
logger.debug(`App: Processing queued navigation to '${nextRoute}'`);
|
||||
setTimeout(() => this.navigateTo(nextRoute), 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide current view smoothly
|
||||
async hideCurrentView() {
|
||||
if (!this.currentView) return;
|
||||
|
||||
// If component is mounted, pause it instead of unmounting
|
||||
if (this.currentView.isMounted) {
|
||||
logger.debug('App: Pausing current view instead of unmounting');
|
||||
this.currentView.pause();
|
||||
}
|
||||
|
||||
// Fade out the container
|
||||
if (this.currentView.container) {
|
||||
this.currentView.container.style.opacity = '0';
|
||||
this.currentView.container.style.transition = `opacity ${(window.CONSTANTS && window.CONSTANTS.TIMING.VIEW_FADE_OUT_MS) || 150}ms ease-out`;
|
||||
}
|
||||
|
||||
// Wait for fade out to complete
|
||||
await new Promise(resolve => setTimeout(resolve, (window.CONSTANTS && window.CONSTANTS.TIMING.VIEW_FADE_OUT_MS) || 150));
|
||||
}
|
||||
|
||||
// Show view smoothly
|
||||
async showView(routeName, component) {
|
||||
const container = component.container;
|
||||
|
||||
// Ensure component is mounted (but not necessarily active); lazy-create now if needed
|
||||
if (!component) {
|
||||
const route = this.routes.get(routeName);
|
||||
const container = document.getElementById(route.containerId);
|
||||
component = new route.componentClass(container, route.viewModel, this.eventBus);
|
||||
component.routeName = routeName;
|
||||
component.isCached = true;
|
||||
this.componentCache.set(routeName, component);
|
||||
}
|
||||
if (!component.isMounted) {
|
||||
logger.debug(`App: Mounting component for '${routeName}'`);
|
||||
component.mount();
|
||||
} else {
|
||||
logger.debug(`App: Resuming component for '${routeName}'`);
|
||||
component.resume();
|
||||
}
|
||||
|
||||
// Fade in the container
|
||||
container.style.opacity = '0';
|
||||
container.style.transition = `opacity ${(window.CONSTANTS && window.CONSTANTS.TIMING.VIEW_FADE_IN_MS) || 200}ms ease-in`;
|
||||
|
||||
// Small delay to ensure smooth transition
|
||||
await new Promise(resolve => setTimeout(resolve, (window.CONSTANTS && window.CONSTANTS.TIMING.VIEW_FADE_DELAY_MS) || 50));
|
||||
|
||||
// Fade in
|
||||
container.style.opacity = '1';
|
||||
}
|
||||
|
||||
// Update navigation state
|
||||
updateNavigation(activeRoute) {
|
||||
// Remove active class from all nav tabs
|
||||
document.querySelectorAll((window.CONSTANTS && window.CONSTANTS.SELECTORS.NAV_TAB) || '.nav-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
|
||||
// Add active class to current route tab
|
||||
const activeTab = document.querySelector(`[data-view="${activeRoute}"]`);
|
||||
if (activeTab) {
|
||||
activeTab.classList.add('active');
|
||||
}
|
||||
|
||||
// Hide all view contents with smooth transition
|
||||
document.querySelectorAll((window.CONSTANTS && window.CONSTANTS.SELECTORS.VIEW_CONTENT) || '.view-content').forEach(view => {
|
||||
view.classList.remove('active');
|
||||
view.style.opacity = '0';
|
||||
view.style.transition = 'opacity 0.15s ease-out';
|
||||
});
|
||||
|
||||
// Show current view content with smooth transition
|
||||
const activeView = document.getElementById(`${activeRoute}-view`);
|
||||
if (activeView) {
|
||||
activeView.classList.add('active');
|
||||
// Small delay to ensure smooth transition
|
||||
setTimeout(() => {
|
||||
activeView.style.opacity = '1';
|
||||
activeView.style.transition = 'opacity 0.2s ease-in';
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
// Register a component
|
||||
registerComponent(name, component) {
|
||||
this.components.set(name, component);
|
||||
}
|
||||
|
||||
// Get a component by name
|
||||
getComponent(name) {
|
||||
return this.components.get(name);
|
||||
}
|
||||
|
||||
// Get the event bus
|
||||
getEventBus() {
|
||||
return this.eventBus;
|
||||
}
|
||||
|
||||
// Initialize the application
|
||||
init() {
|
||||
logger.debug('SPORE LEDLab Framework initialized');
|
||||
}
|
||||
|
||||
// Setup navigation
|
||||
setupNavigation() {
|
||||
document.querySelectorAll((window.CONSTANTS && window.CONSTANTS.SELECTORS.NAV_TAB) || '.nav-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const routeName = tab.dataset.view;
|
||||
this.navigateTo(routeName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up cached components (call when app is shutting down)
|
||||
cleanup() {
|
||||
logger.debug('App: Cleaning up cached components...');
|
||||
|
||||
this.componentCache.forEach((component, routeName) => {
|
||||
if (component.isMounted) {
|
||||
logger.debug(`App: Unmounting cached component '${routeName}'`);
|
||||
component.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
this.componentCache.clear();
|
||||
this.cachedViews.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Global app instance (disabled for LEDLab single-page app)
|
||||
// window.app = new App();
|
||||
Reference in New Issue
Block a user