feat: frontend optimization, refactoring
This commit is contained in:
@@ -1,5 +1,16 @@
|
||||
// SPORE UI 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() {
|
||||
@@ -99,24 +110,25 @@ class ViewModel {
|
||||
// Set multiple properties at once with change detection
|
||||
setMultiple(properties) {
|
||||
const changedProperties = {};
|
||||
const unchangedProperties = {};
|
||||
|
||||
// Determine changes and update previousData snapshot per key
|
||||
Object.keys(properties).forEach(key => {
|
||||
if (this._data[key] !== properties[key]) {
|
||||
changedProperties[key] = properties[key];
|
||||
} else {
|
||||
unchangedProperties[key] = properties[key];
|
||||
const newValue = properties[key];
|
||||
const oldValue = this._data[key];
|
||||
if (oldValue !== newValue) {
|
||||
this._previousData[key] = oldValue;
|
||||
changedProperties[key] = newValue;
|
||||
}
|
||||
});
|
||||
|
||||
// Set all properties
|
||||
// Apply all properties
|
||||
Object.keys(properties).forEach(key => {
|
||||
this._data[key] = properties[key];
|
||||
});
|
||||
|
||||
// Notify listeners only for changed properties
|
||||
// Notify listeners only for changed properties with accurate previous values
|
||||
Object.keys(changedProperties).forEach(key => {
|
||||
this._notifyListeners(key, changedProperties[key], this._previousData[key]);
|
||||
this._notifyListeners(key, this._data[key], this._previousData[key]);
|
||||
});
|
||||
|
||||
if (Object.keys(changedProperties).length > 0) {
|
||||
@@ -215,28 +227,33 @@ class ViewModel {
|
||||
batchUpdate(updates, options = {}) {
|
||||
const { preserveUIState = true, notifyChanges = true } = options;
|
||||
|
||||
if (preserveUIState) {
|
||||
// Store current UI state
|
||||
const currentUIState = new Map(this._uiState);
|
||||
|
||||
// Apply updates
|
||||
Object.keys(updates).forEach(key => {
|
||||
this._data[key] = updates[key];
|
||||
});
|
||||
|
||||
// Restore UI state
|
||||
// Optionally preserve UI state snapshot
|
||||
const currentUIState = preserveUIState ? new Map(this._uiState) : null;
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
|
||||
// Restore UI state if requested
|
||||
if (preserveUIState && currentUIState) {
|
||||
this._uiState = currentUIState;
|
||||
} else {
|
||||
// Apply updates normally
|
||||
Object.keys(updates).forEach(key => {
|
||||
this._data[key] = updates[key];
|
||||
});
|
||||
}
|
||||
|
||||
// Notify listeners if requested
|
||||
// Notify listeners for changed keys
|
||||
if (notifyChanges) {
|
||||
Object.keys(updates).forEach(key => {
|
||||
this._notifyListeners(key, updates[key], this._previousData[key]);
|
||||
changedKeys.forEach(key => {
|
||||
this._notifyListeners(key, this._data[key], this._previousData[key]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -521,6 +538,66 @@ class Component {
|
||||
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 = 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);
|
||||
}
|
||||
|
||||
// Tab helpers
|
||||
setupTabs(container = this.container) {
|
||||
const tabButtons = container.querySelectorAll('.tab-button');
|
||||
const tabContents = container.querySelectorAll('.tab-content');
|
||||
tabButtons.forEach(button => {
|
||||
this.addEventListener(button, 'click', (e) => {
|
||||
e.stopPropagation();
|
||||
const targetTab = button.dataset.tab;
|
||||
this.setActiveTab(targetTab, container);
|
||||
});
|
||||
});
|
||||
tabContents.forEach(content => {
|
||||
this.addEventListener(content, 'click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setActiveTab(tabName, container = this.container) {
|
||||
const tabButtons = container.querySelectorAll('.tab-button');
|
||||
const tabContents = container.querySelectorAll('.tab-content');
|
||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||
tabContents.forEach(content => content.classList.remove('active'));
|
||||
const activeButton = container.querySelector(`[data-tab="${tabName}"]`);
|
||||
const activeContent = container.querySelector(`#${tabName}-tab`);
|
||||
if (activeButton) activeButton.classList.add('active');
|
||||
if (activeContent) activeContent.classList.add('active');
|
||||
logger.debug(`${this.constructor.name}: Active tab set to '${tabName}'`);
|
||||
}
|
||||
}
|
||||
|
||||
// Application class to manage components and routing
|
||||
|
||||
Reference in New Issue
Block a user