feat: Phase 1 complete + Task 2.1 User Stats Widget
Phase 1 Foundation: - DashboardManager: Complete orchestrator for all Epic 1 systems (572 lines) - WidgetBase: Common utilities for widget development (498 lines) Task 2.1 User Stats Widget: - Extracted and refactored from renderUserStats() - Clean vanilla JS implementation (408 lines) - Editable stat values with live updates - Progress bars with configurable colors - User portrait, name, and level display - Classic D&D stats (STR/DEX/CON/INT/WIS/CHA) with +/- buttons - Fully configurable (show/hide sections, visible stats) - Mobile-responsive with layout adjustments - No jQuery dependencies Dashboard Manager features: - Widget lifecycle management (add/remove/update) - Tab coordination with TabManager - Drag/drop and resize integration - Edit mode management - Layout persistence (save/load/export/import) - Auto-save with debouncing - Event-driven architecture Epic 2 progress: 1/4 core widgets complete
This commit is contained in:
@@ -0,0 +1,773 @@
|
||||
/**
|
||||
* Dashboard Manager
|
||||
*
|
||||
* Orchestrates the complete dashboard system by integrating:
|
||||
* - GridEngine (positioning)
|
||||
* - WidgetRegistry (widget definitions)
|
||||
* - TabManager (multi-tab support)
|
||||
* - DragDropHandler (drag widgets)
|
||||
* - ResizeHandler (resize widgets)
|
||||
* - EditModeManager (edit/view modes)
|
||||
* - LayoutPersistence (save/load)
|
||||
*
|
||||
* Provides high-level API for widget and tab management.
|
||||
*/
|
||||
|
||||
import { GridEngine } from './gridEngine.js';
|
||||
import { WidgetRegistry } from './widgetRegistry.js';
|
||||
import { TabManager } from './tabManager.js';
|
||||
import { DragDropHandler } from './dragDrop.js';
|
||||
import { ResizeHandler } from './resizeHandler.js';
|
||||
import { EditModeManager } from './editModeManager.js';
|
||||
import { LayoutPersistence } from './layoutPersistence.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} DashboardConfig
|
||||
* @property {number} columns - Grid column count (default: 12)
|
||||
* @property {number} rowHeight - Grid row height in pixels (default: 80)
|
||||
* @property {number} gap - Gap between widgets in pixels (default: 12)
|
||||
* @property {number} debounceMs - Auto-save debounce delay (default: 500)
|
||||
* @property {Function} onSave - Callback when layout saved
|
||||
* @property {Function} onLoad - Callback when layout loaded
|
||||
* @property {Function} onError - Callback on errors
|
||||
*/
|
||||
|
||||
/**
|
||||
* DashboardManager - Complete dashboard system orchestrator
|
||||
*/
|
||||
export class DashboardManager {
|
||||
/**
|
||||
* @param {HTMLElement} container - Main dashboard container element
|
||||
* @param {DashboardConfig} config - Configuration options
|
||||
*/
|
||||
constructor(container, config = {}) {
|
||||
if (!container) {
|
||||
throw new Error('[DashboardManager] Container element is required');
|
||||
}
|
||||
|
||||
this.container = container;
|
||||
this.config = {
|
||||
columns: config.columns || 12,
|
||||
rowHeight: config.rowHeight || 80,
|
||||
gap: config.gap || 12,
|
||||
debounceMs: config.debounceMs || 500,
|
||||
onSave: config.onSave,
|
||||
onLoad: config.onLoad,
|
||||
onError: config.onError,
|
||||
...config
|
||||
};
|
||||
|
||||
// Dashboard state
|
||||
this.currentTabId = null;
|
||||
this.widgets = new Map(); // widgetId => { widget data, element, tab }
|
||||
this.defaultLayout = null;
|
||||
|
||||
// System instances
|
||||
this.gridEngine = null;
|
||||
this.registry = null;
|
||||
this.tabManager = null;
|
||||
this.dragHandler = null;
|
||||
this.resizeHandler = null;
|
||||
this.editManager = null;
|
||||
this.persistence = null;
|
||||
|
||||
// Container elements
|
||||
this.gridContainer = null;
|
||||
this.tabContainer = null;
|
||||
|
||||
this.changeListeners = new Set();
|
||||
|
||||
console.log('[DashboardManager] Initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all dashboard systems
|
||||
*/
|
||||
async init() {
|
||||
console.log('[DashboardManager] Initializing systems...');
|
||||
|
||||
// Create container structure
|
||||
this.createContainerStructure();
|
||||
|
||||
// Initialize Grid Engine
|
||||
this.gridEngine = new GridEngine({
|
||||
columns: this.config.columns,
|
||||
rowHeight: this.config.rowHeight,
|
||||
gap: this.config.gap,
|
||||
container: this.gridContainer
|
||||
});
|
||||
|
||||
// Initialize Widget Registry
|
||||
this.registry = new WidgetRegistry();
|
||||
|
||||
// Initialize Tab Manager
|
||||
this.tabManager = new TabManager({
|
||||
onTabChange: (tabId) => this.onTabChange(tabId),
|
||||
onTabCreate: (tab) => this.onTabCreate(tab),
|
||||
onTabDelete: (tabId) => this.onTabDelete(tabId),
|
||||
onTabRename: (tabId, newName) => this.onTabRename(tabId, newName),
|
||||
onTabReorder: (fromIndex, toIndex) => this.onTabReorder(fromIndex, toIndex)
|
||||
});
|
||||
|
||||
// Initialize Drag & Drop
|
||||
this.dragHandler = new DragDropHandler(this.gridEngine, {
|
||||
showGrid: true,
|
||||
enableSnap: true
|
||||
});
|
||||
|
||||
// Initialize Resize Handler
|
||||
this.resizeHandler = new ResizeHandler(this.gridEngine, {
|
||||
minWidth: 2,
|
||||
minHeight: 2,
|
||||
maxWidth: this.config.columns,
|
||||
maxHeight: 10
|
||||
});
|
||||
|
||||
// Initialize Edit Mode Manager
|
||||
this.editManager = new EditModeManager({
|
||||
container: this.container,
|
||||
onSave: () => this.handleEditSave(),
|
||||
onCancel: (originalLayout) => this.handleEditCancel(originalLayout),
|
||||
onWidgetAdd: (type) => this.addWidget(type),
|
||||
onWidgetDelete: (widgetId) => this.removeWidget(widgetId),
|
||||
onWidgetSettings: (widgetId) => this.openWidgetSettings(widgetId)
|
||||
});
|
||||
|
||||
// Initialize Layout Persistence
|
||||
this.persistence = new LayoutPersistence({
|
||||
debounceMs: this.config.debounceMs,
|
||||
onSave: (layout) => {
|
||||
console.log('[DashboardManager] Layout saved');
|
||||
if (this.config.onSave) this.config.onSave(layout);
|
||||
},
|
||||
onLoad: (layout) => {
|
||||
console.log('[DashboardManager] Layout loaded');
|
||||
if (this.config.onLoad) this.config.onLoad(layout);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('[DashboardManager] Error:', error);
|
||||
if (this.config.onError) this.config.onError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Try to load saved layout
|
||||
await this.loadLayout();
|
||||
|
||||
console.log('[DashboardManager] All systems initialized');
|
||||
this.notifyChange('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create dashboard container structure
|
||||
*/
|
||||
createContainerStructure() {
|
||||
// Clear container
|
||||
this.container.innerHTML = '';
|
||||
|
||||
// Create tab container
|
||||
this.tabContainer = document.createElement('div');
|
||||
this.tabContainer.className = 'rpg-dashboard-tabs';
|
||||
this.tabContainer.id = 'rpg-dashboard-tabs';
|
||||
this.container.appendChild(this.tabContainer);
|
||||
|
||||
// Create grid container
|
||||
this.gridContainer = document.createElement('div');
|
||||
this.gridContainer.className = 'rpg-dashboard-grid';
|
||||
this.gridContainer.id = 'rpg-dashboard-grid';
|
||||
this.gridContainer.style.position = 'relative';
|
||||
this.gridContainer.style.minHeight = '600px';
|
||||
this.container.appendChild(this.gridContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new widget to the dashboard
|
||||
* @param {string} type - Widget type (must be registered)
|
||||
* @param {string} [tabId] - Tab ID (default: current tab)
|
||||
* @param {Object} [config] - Widget configuration
|
||||
* @returns {string} Widget ID
|
||||
*/
|
||||
addWidget(type, tabId = null, config = {}) {
|
||||
const targetTabId = tabId || this.currentTabId;
|
||||
if (!targetTabId) {
|
||||
throw new Error('[DashboardManager] No tab selected');
|
||||
}
|
||||
|
||||
// Get widget definition from registry
|
||||
const definition = this.registry.get(type);
|
||||
if (!definition) {
|
||||
throw new Error(`[DashboardManager] Widget type "${type}" not registered`);
|
||||
}
|
||||
|
||||
// Generate unique widget ID
|
||||
const widgetId = `widget-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Find available position in grid
|
||||
const position = this.findAvailablePosition(definition.defaultSize);
|
||||
|
||||
// Create widget data
|
||||
const widget = {
|
||||
id: widgetId,
|
||||
type,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
w: definition.defaultSize.w,
|
||||
h: definition.defaultSize.h,
|
||||
config: config || {}
|
||||
};
|
||||
|
||||
// Add to tab
|
||||
const tab = this.tabManager.getTab(targetTabId);
|
||||
if (!tab) {
|
||||
throw new Error(`[DashboardManager] Tab "${targetTabId}" not found`);
|
||||
}
|
||||
|
||||
if (!tab.widgets) {
|
||||
tab.widgets = [];
|
||||
}
|
||||
tab.widgets.push(widget);
|
||||
|
||||
// Render widget if on current tab
|
||||
if (targetTabId === this.currentTabId) {
|
||||
this.renderWidget(widget, definition);
|
||||
}
|
||||
|
||||
// Trigger auto-save
|
||||
this.triggerAutoSave();
|
||||
|
||||
console.log(`[DashboardManager] Added widget: ${widgetId} (${type}) to tab: ${targetTabId}`);
|
||||
this.notifyChange('widgetAdded', { widgetId, type, tabId: targetTabId });
|
||||
|
||||
return widgetId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a widget from the dashboard
|
||||
* @param {string} widgetId - Widget ID to remove
|
||||
*/
|
||||
removeWidget(widgetId) {
|
||||
// Find widget in current tab
|
||||
const tab = this.tabManager.getTab(this.currentTabId);
|
||||
if (!tab || !tab.widgets) {
|
||||
console.warn(`[DashboardManager] Widget ${widgetId} not found in current tab`);
|
||||
return;
|
||||
}
|
||||
|
||||
const index = tab.widgets.findIndex(w => w.id === widgetId);
|
||||
if (index === -1) {
|
||||
console.warn(`[DashboardManager] Widget ${widgetId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get widget element and definition
|
||||
const widgetData = this.widgets.get(widgetId);
|
||||
if (widgetData) {
|
||||
// Call widget cleanup
|
||||
const definition = this.registry.get(widgetData.widget.type);
|
||||
if (definition && definition.onRemove) {
|
||||
definition.onRemove(widgetData.element, widgetData.widget.config);
|
||||
}
|
||||
|
||||
// Destroy drag/resize handlers
|
||||
this.dragHandler.destroyWidget(widgetData.element);
|
||||
this.resizeHandler.destroyWidget(widgetData.element);
|
||||
|
||||
// Remove element
|
||||
widgetData.element.remove();
|
||||
|
||||
// Remove from map
|
||||
this.widgets.delete(widgetId);
|
||||
}
|
||||
|
||||
// Remove from tab
|
||||
tab.widgets.splice(index, 1);
|
||||
|
||||
// Trigger auto-save
|
||||
this.triggerAutoSave();
|
||||
|
||||
console.log(`[DashboardManager] Removed widget: ${widgetId}`);
|
||||
this.notifyChange('widgetRemoved', { widgetId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a widget's configuration
|
||||
* @param {string} widgetId - Widget ID
|
||||
* @param {Object} updates - Configuration updates
|
||||
*/
|
||||
updateWidget(widgetId, updates) {
|
||||
const widgetData = this.widgets.get(widgetId);
|
||||
if (!widgetData) {
|
||||
console.warn(`[DashboardManager] Widget ${widgetId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update widget config
|
||||
Object.assign(widgetData.widget.config, updates);
|
||||
|
||||
// Get widget definition
|
||||
const definition = this.registry.get(widgetData.widget.type);
|
||||
|
||||
// Call onConfigChange if defined
|
||||
if (definition && definition.onConfigChange) {
|
||||
definition.onConfigChange(widgetData.element, widgetData.widget.config);
|
||||
}
|
||||
|
||||
// Re-render widget
|
||||
this.renderWidgetContent(widgetData.element, widgetData.widget, definition);
|
||||
|
||||
// Trigger auto-save
|
||||
this.triggerAutoSave();
|
||||
|
||||
console.log(`[DashboardManager] Updated widget: ${widgetId}`);
|
||||
this.notifyChange('widgetUpdated', { widgetId, updates });
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single widget
|
||||
* @param {Object} widget - Widget data
|
||||
* @param {Object} definition - Widget definition
|
||||
*/
|
||||
renderWidget(widget, definition) {
|
||||
// Create widget element
|
||||
const element = document.createElement('div');
|
||||
element.className = 'rpg-widget';
|
||||
element.id = `widget-${widget.id}`;
|
||||
element.dataset.widgetId = widget.id;
|
||||
element.dataset.widgetType = widget.type;
|
||||
|
||||
// Position widget using grid engine
|
||||
const pos = this.gridEngine.getPixelPosition(widget);
|
||||
element.style.position = 'absolute';
|
||||
element.style.left = `${pos.left}px`;
|
||||
element.style.top = `${pos.top}px`;
|
||||
element.style.width = `${pos.width}px`;
|
||||
element.style.height = `${pos.height}px`;
|
||||
|
||||
// Add to grid
|
||||
this.gridContainer.appendChild(element);
|
||||
|
||||
// Render widget content
|
||||
this.renderWidgetContent(element, widget, definition);
|
||||
|
||||
// Initialize drag & drop
|
||||
this.dragHandler.initWidget(element, widget, (updated, newX, newY) => {
|
||||
widget.x = newX;
|
||||
widget.y = newY;
|
||||
this.repositionWidget(element, widget);
|
||||
this.triggerAutoSave();
|
||||
});
|
||||
|
||||
// Initialize resize
|
||||
this.resizeHandler.initWidget(element, widget, (updated, newW, newH, newX, newY) => {
|
||||
widget.w = newW;
|
||||
widget.h = newH;
|
||||
widget.x = newX;
|
||||
widget.y = newY;
|
||||
this.repositionWidget(element, widget);
|
||||
|
||||
// Call onResize if defined
|
||||
if (definition.onResize) {
|
||||
definition.onResize(element, newW, newH);
|
||||
}
|
||||
|
||||
this.triggerAutoSave();
|
||||
}, {
|
||||
minW: definition.minSize.w,
|
||||
minH: definition.minSize.h
|
||||
});
|
||||
|
||||
// Add edit mode controls
|
||||
if (this.editManager) {
|
||||
this.editManager.addWidgetControls(element, widget.id);
|
||||
}
|
||||
|
||||
// Store widget data
|
||||
this.widgets.set(widget.id, {
|
||||
widget,
|
||||
element,
|
||||
definition,
|
||||
tabId: this.currentTabId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render widget content (called by widget render function)
|
||||
* @param {HTMLElement} element - Widget element
|
||||
* @param {Object} widget - Widget data
|
||||
* @param {Object} definition - Widget definition
|
||||
*/
|
||||
renderWidgetContent(element, widget, definition) {
|
||||
// Clear existing content (except resize handles and controls)
|
||||
const handles = element.querySelector('.resize-handles');
|
||||
const controls = element.querySelector('.widget-edit-controls');
|
||||
element.innerHTML = '';
|
||||
if (handles) element.appendChild(handles);
|
||||
if (controls) element.appendChild(controls);
|
||||
|
||||
// Call widget render function
|
||||
if (definition && definition.render) {
|
||||
definition.render(element, widget.config || {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reposition widget element
|
||||
* @param {HTMLElement} element - Widget element
|
||||
* @param {Object} widget - Widget data
|
||||
*/
|
||||
repositionWidget(element, widget) {
|
||||
const pos = this.gridEngine.getPixelPosition(widget);
|
||||
element.style.left = `${pos.left}px`;
|
||||
element.style.top = `${pos.top}px`;
|
||||
element.style.width = `${pos.width}px`;
|
||||
element.style.height = `${pos.height}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find available position for new widget
|
||||
* @param {Object} size - Widget size { w, h }
|
||||
* @returns {Object} Position { x, y }
|
||||
*/
|
||||
findAvailablePosition(size) {
|
||||
// Simple algorithm: try to place at top-left, move right, then down
|
||||
const tab = this.tabManager.getTab(this.currentTabId);
|
||||
const widgets = tab?.widgets || [];
|
||||
|
||||
for (let y = 0; y < 20; y++) {
|
||||
for (let x = 0; x <= this.config.columns - size.w; x++) {
|
||||
const position = { x, y };
|
||||
const testWidget = { ...position, w: size.w, h: size.h };
|
||||
|
||||
// Check if position is free
|
||||
const hasCollision = widgets.some(w =>
|
||||
this.gridEngine.detectCollision(testWidget, [w])
|
||||
);
|
||||
|
||||
if (!hasCollision) {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: place at bottom
|
||||
const maxY = Math.max(...widgets.map(w => w.y + w.h), 0);
|
||||
return { x: 0, y: maxY };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new tab
|
||||
* @param {string} name - Tab name
|
||||
* @returns {string} Tab ID
|
||||
*/
|
||||
createTab(name) {
|
||||
const tabId = this.tabManager.createTab(name);
|
||||
this.triggerAutoSave();
|
||||
return tabId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a different tab
|
||||
* @param {string} tabId - Tab ID to switch to
|
||||
*/
|
||||
switchTab(tabId) {
|
||||
this.tabManager.switchTab(tabId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tab change event
|
||||
* @param {string} tabId - New active tab ID
|
||||
*/
|
||||
onTabChange(tabId) {
|
||||
console.log(`[DashboardManager] Switching to tab: ${tabId}`);
|
||||
this.currentTabId = tabId;
|
||||
|
||||
// Clear grid
|
||||
this.clearGrid();
|
||||
|
||||
// Render all widgets in this tab
|
||||
const tab = this.tabManager.getTab(tabId);
|
||||
if (tab && tab.widgets) {
|
||||
tab.widgets.forEach(widget => {
|
||||
const definition = this.registry.get(widget.type);
|
||||
if (definition) {
|
||||
this.renderWidget(widget, definition);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.notifyChange('tabChanged', { tabId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tab creation
|
||||
*/
|
||||
onTabCreate(tab) {
|
||||
console.log(`[DashboardManager] Tab created: ${tab.id}`);
|
||||
this.triggerAutoSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tab deletion
|
||||
*/
|
||||
onTabDelete(tabId) {
|
||||
console.log(`[DashboardManager] Tab deleted: ${tabId}`);
|
||||
this.triggerAutoSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tab rename
|
||||
*/
|
||||
onTabRename(tabId, newName) {
|
||||
console.log(`[DashboardManager] Tab renamed: ${tabId} -> ${newName}`);
|
||||
this.triggerAutoSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tab reorder
|
||||
*/
|
||||
onTabReorder(fromIndex, toIndex) {
|
||||
console.log(`[DashboardManager] Tabs reordered: ${fromIndex} -> ${toIndex}`);
|
||||
this.triggerAutoSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all widgets from grid
|
||||
*/
|
||||
clearGrid() {
|
||||
// Destroy all widgets
|
||||
this.widgets.forEach((widgetData, widgetId) => {
|
||||
const definition = this.registry.get(widgetData.widget.type);
|
||||
if (definition && definition.onRemove) {
|
||||
definition.onRemove(widgetData.element, widgetData.widget.config);
|
||||
}
|
||||
this.dragHandler.destroyWidget(widgetData.element);
|
||||
this.resizeHandler.destroyWidget(widgetData.element);
|
||||
widgetData.element.remove();
|
||||
});
|
||||
|
||||
this.widgets.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter edit mode
|
||||
*/
|
||||
enterEditMode() {
|
||||
this.editManager.enterEditMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit edit mode
|
||||
* @param {boolean} save - Whether to save changes
|
||||
*/
|
||||
exitEditMode(save = false) {
|
||||
this.editManager.exitEditMode(save);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle edit mode save
|
||||
*/
|
||||
handleEditSave() {
|
||||
console.log('[DashboardManager] Edit mode saved');
|
||||
this.triggerAutoSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle edit mode cancel
|
||||
*/
|
||||
handleEditCancel(originalLayout) {
|
||||
console.log('[DashboardManager] Edit mode cancelled');
|
||||
// Could restore original layout here if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Open widget settings dialog
|
||||
* @param {string} widgetId - Widget ID
|
||||
*/
|
||||
openWidgetSettings(widgetId) {
|
||||
const widgetData = this.widgets.get(widgetId);
|
||||
if (!widgetData) return;
|
||||
|
||||
const definition = this.registry.get(widgetData.widget.type);
|
||||
if (definition && definition.getConfig) {
|
||||
// Get config schema
|
||||
const configSchema = definition.getConfig();
|
||||
// TODO: Show config dialog
|
||||
console.log('[DashboardManager] Widget settings:', widgetId, configSchema);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current dashboard configuration
|
||||
* @returns {Object} Dashboard configuration
|
||||
*/
|
||||
getDashboardConfig() {
|
||||
return {
|
||||
version: 2,
|
||||
gridConfig: {
|
||||
columns: this.config.columns,
|
||||
rowHeight: this.config.rowHeight,
|
||||
gap: this.config.gap
|
||||
},
|
||||
tabs: this.tabManager.getTabs().map(tab => ({
|
||||
id: tab.id,
|
||||
name: tab.name,
|
||||
widgets: tab.widgets || []
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply dashboard configuration
|
||||
* @param {Object} config - Dashboard configuration
|
||||
*/
|
||||
applyDashboardConfig(config) {
|
||||
console.log('[DashboardManager] Applying dashboard config');
|
||||
|
||||
// Clear existing
|
||||
this.clearGrid();
|
||||
this.tabManager.deleteAllTabs();
|
||||
|
||||
// Create tabs
|
||||
config.tabs.forEach(tabConfig => {
|
||||
this.tabManager.createTab(tabConfig.name, tabConfig.id);
|
||||
const tab = this.tabManager.getTab(tabConfig.id);
|
||||
tab.widgets = tabConfig.widgets || [];
|
||||
});
|
||||
|
||||
// Switch to first tab
|
||||
if (config.tabs.length > 0) {
|
||||
this.switchTab(config.tabs[0].id);
|
||||
}
|
||||
|
||||
this.notifyChange('configApplied', { config });
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current layout
|
||||
* @param {boolean} immediate - Skip debounce
|
||||
*/
|
||||
async saveLayout(immediate = false) {
|
||||
const config = this.getDashboardConfig();
|
||||
await this.persistence.saveLayout(config, immediate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load saved layout
|
||||
*/
|
||||
async loadLayout() {
|
||||
try {
|
||||
const saved = await this.persistence.loadLayout();
|
||||
if (saved) {
|
||||
this.applyDashboardConfig(saved);
|
||||
} else if (this.defaultLayout) {
|
||||
console.log('[DashboardManager] No saved layout, using default');
|
||||
this.applyDashboardConfig(this.defaultLayout);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[DashboardManager] Failed to load layout:', error);
|
||||
if (this.defaultLayout) {
|
||||
this.applyDashboardConfig(this.defaultLayout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export layout as JSON
|
||||
* @param {string} filename - Export filename
|
||||
*/
|
||||
exportLayout(filename = 'dashboard-layout.json') {
|
||||
const config = this.getDashboardConfig();
|
||||
this.persistence.exportLayout(config, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import layout from JSON file
|
||||
* @param {File} file - JSON file
|
||||
*/
|
||||
async importLayout(file) {
|
||||
const config = await this.persistence.importLayout(file);
|
||||
this.applyDashboardConfig(config);
|
||||
await this.saveLayout(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to default layout
|
||||
*/
|
||||
async resetLayout() {
|
||||
if (!this.defaultLayout) {
|
||||
console.warn('[DashboardManager] No default layout defined');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.persistence.resetToDefault(this.defaultLayout);
|
||||
this.applyDashboardConfig(this.defaultLayout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default layout
|
||||
* @param {Object} layout - Default layout configuration
|
||||
*/
|
||||
setDefaultLayout(layout) {
|
||||
this.defaultLayout = layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger auto-save
|
||||
*/
|
||||
triggerAutoSave() {
|
||||
const config = this.getDashboardConfig();
|
||||
this.persistence.saveLayout(config).catch(err => {
|
||||
console.error('[DashboardManager] Auto-save failed:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register change listener
|
||||
* @param {Function} callback - Callback function (event, data) => void
|
||||
*/
|
||||
onChange(callback) {
|
||||
this.changeListeners.add(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister change listener
|
||||
* @param {Function} callback - Callback to remove
|
||||
*/
|
||||
offChange(callback) {
|
||||
this.changeListeners.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify change listeners
|
||||
* @private
|
||||
*/
|
||||
notifyChange(event, data) {
|
||||
this.changeListeners.forEach(callback => {
|
||||
try {
|
||||
callback(event, data);
|
||||
} catch (error) {
|
||||
console.error('[DashboardManager] Error in change listener:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy dashboard and cleanup
|
||||
*/
|
||||
destroy() {
|
||||
console.log('[DashboardManager] Destroying dashboard');
|
||||
|
||||
// Clear grid
|
||||
this.clearGrid();
|
||||
|
||||
// Destroy systems
|
||||
if (this.editManager) this.editManager.destroy();
|
||||
if (this.dragHandler) this.dragHandler.destroy();
|
||||
if (this.persistence) this.persistence.destroy();
|
||||
|
||||
// Clear listeners
|
||||
this.changeListeners.clear();
|
||||
|
||||
// Clear container
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user