8dc07a938a
**Status Tab Layout Changes:** - User Info widget: 1x2 vertical (left column) instead of 2x1 horizontal - User Stats widget: scales from 1x3 (narrow) to 2x3 (wide) - User Mood widget: 1x1 positioned below User Info - User Attributes widget: scales from 2x4 (narrow) to 3x4 (wide), full width **Technical Changes:** - Update widget definitions to use column-aware defaultSize() functions - userInfoWidget: Returns 1x2 for desktop, 1x1 for mobile - userStatsWidget: Returns 1x3 for 2 cols, 2x3 for 3+ cols - userAttributesWidget: Returns 2x4 for 2 cols, 3x4 for 3+ cols - Remove autoLayout from resetLayout() to preserve default positions - Add resetWidgetSizesToDefault() to apply column-aware sizes - Update CSS for 1x1 compact avatar (round) and 1x2 wide avatar layouts **User Info Widget Improvements:** - 1x2 layout: Horizontal split with name left, level right over avatar - 1x1 layout: Round avatar with bottom nameplate (flush positioning) - Transparent glass-style backgrounds for better avatar visibility - Proper aspect-ratio for circular avatar in compact mode **Result:** - Widgets scale intelligently based on panel width (2-4 columns) - Desktop users get larger, more spacious layouts - Mobile/narrow screens get efficient vertical stacking - Reset Layout respects custom positions while applying responsive sizes - Window resize triggers autoLayout via ResizeObserver for reflow
2171 lines
81 KiB
JavaScript
2171 lines
81 KiB
JavaScript
/**
|
||
* 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.
|
||
*/
|
||
|
||
// Performance: Disable console logging (console.error still active)
|
||
// Temporarily enabled for debugging auto-arrange onResize issue
|
||
const DEBUG = true;
|
||
const console = DEBUG ? window.console : {
|
||
log: () => {},
|
||
warn: () => {},
|
||
error: window.console.error.bind(window.console)
|
||
};
|
||
|
||
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';
|
||
import { generateDefaultDashboard } from './defaultLayout.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 || 5, // rem units for responsive scaling
|
||
gap: config.gap || 0.75, // rem units for responsive scaling
|
||
debounceMs: config.debounceMs || 500,
|
||
onSave: config.onSave,
|
||
onLoad: config.onLoad,
|
||
onError: config.onError,
|
||
...config
|
||
};
|
||
|
||
console.log('[DashboardManager] Constructor config:', {
|
||
rowHeight: this.config.rowHeight,
|
||
gap: this.config.gap,
|
||
columns: this.config.columns
|
||
});
|
||
|
||
// Dashboard state
|
||
this.currentTabId = null;
|
||
this.widgets = new Map(); // widgetId => { widget data, element, tab }
|
||
this.defaultLayout = null;
|
||
this.previousTrackerConfig = null; // For detecting config changes
|
||
this.resizeTimeout = null; // For debouncing resize events
|
||
|
||
// Dashboard data structure (for TabManager)
|
||
this.dashboard = {
|
||
tabs: [],
|
||
defaultTab: 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.resizeHandlesOverlay = null;
|
||
this.editControlsOverlay = 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 Widget Registry (use provided registry or create new one)
|
||
this.registry = this.config.registry || new WidgetRegistry();
|
||
|
||
// Initialize Grid Engine (columns calculated dynamically)
|
||
this.gridEngine = new GridEngine({
|
||
rowHeight: this.config.rowHeight,
|
||
gap: this.config.gap,
|
||
container: this.gridContainer,
|
||
registry: this.registry, // Pass registry for maxAutoSize lookups
|
||
onColumnsChange: (newCols, oldCols) => {
|
||
console.log('[DashboardManager] Grid columns changed:', oldCols, '→', newCols);
|
||
|
||
// Update ALL tabs to keep them synchronized with new column count
|
||
// This prevents layout issues when switching to hidden tabs after resize
|
||
let totalWidgetsUpdated = 0;
|
||
const currentTab = this.tabManager.getTab(this.currentTabId);
|
||
|
||
this.dashboard.tabs.forEach(tab => {
|
||
if (!tab.widgets || tab.widgets.length === 0) return;
|
||
|
||
const isCurrentTab = tab.id === this.currentTabId;
|
||
console.log(`[DashboardManager] Updating tab "${tab.name}" (${tab.widgets.length} widgets, ${isCurrentTab ? 'visible' : 'hidden'})`);
|
||
|
||
// Store dimensions before resize (only for current tab, for onResize detection)
|
||
const dimensionsBefore = new Map();
|
||
if (isCurrentTab) {
|
||
tab.widgets.forEach(widget => {
|
||
dimensionsBefore.set(widget.id, { w: widget.w, h: widget.h });
|
||
});
|
||
}
|
||
|
||
// Reset widget sizes to column-aware defaults before auto-layout
|
||
// This ensures widgets adopt their appropriate sizes for the new column count
|
||
// (e.g., userInfo expands from 1×1 to 2×1 when going from 2→3 columns)
|
||
this.resetWidgetSizesToDefault(tab.widgets);
|
||
|
||
// Run auto-layout to reflow and expand widgets for new grid
|
||
// This prevents overlap and optimizes space usage
|
||
// Works on widget data arrays - no DOM access required
|
||
this.gridEngine.autoLayout(tab.widgets, { preserveOrder: true });
|
||
|
||
// Call onResize handlers ONLY for currently visible widgets (DOM exists)
|
||
// Hidden tab widgets don't have DOM elements, so skip their onResize handlers
|
||
if (isCurrentTab) {
|
||
tab.widgets.forEach(widget => {
|
||
const before = dimensionsBefore.get(widget.id);
|
||
if (before && (before.w !== widget.w || before.h !== widget.h)) {
|
||
const widgetData = this.widgets.get(widget.id);
|
||
if (widgetData?.definition?.onResize && widgetData.element) {
|
||
console.log(`[DashboardManager] Calling onResize for ${widget.type} (${before.w}x${before.h} → ${widget.w}x${widget.h})`);
|
||
widgetData.definition.onResize(widgetData.element, widget.w, widget.h);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
totalWidgetsUpdated += tab.widgets.length;
|
||
});
|
||
|
||
console.log(`[DashboardManager] Updated ${totalWidgetsUpdated} widgets across ${this.dashboard.tabs.length} tabs`);
|
||
|
||
// Save changes
|
||
this.triggerAutoSave();
|
||
|
||
// Re-render current tab widgets with new layout
|
||
this.renderAllWidgets();
|
||
}
|
||
});
|
||
|
||
// Initialize Tab Manager with dashboard data structure
|
||
// Create default tab if no tabs exist
|
||
if (this.dashboard.tabs.length === 0) {
|
||
this.dashboard.tabs.push({
|
||
id: 'main',
|
||
name: 'Main',
|
||
icon: 'fa-solid fa-house',
|
||
order: 0,
|
||
widgets: []
|
||
});
|
||
this.dashboard.defaultTab = 'main';
|
||
}
|
||
|
||
this.tabManager = new TabManager(this.dashboard);
|
||
|
||
// Set current tab to active tab from TabManager
|
||
this.currentTabId = this.tabManager.activeTabId;
|
||
|
||
// Register tab change listener
|
||
this.tabManager.onChange((event, data) => {
|
||
if (event === 'activeTabChanged') {
|
||
this.onTabChange(data.tabId);
|
||
}
|
||
});
|
||
|
||
// Initialize Edit Mode Manager first (needed by drag/resize handlers)
|
||
this.editManager = new EditModeManager({
|
||
container: this.container,
|
||
editControlsOverlay: this.editControlsOverlay,
|
||
onSave: () => this.handleEditSave(),
|
||
onCancel: (originalLayout) => this.handleEditCancel(originalLayout),
|
||
onWidgetAdd: (type) => this.addWidget(type),
|
||
onWidgetDelete: (widgetId) => this.removeWidget(widgetId),
|
||
onWidgetSettings: (widgetId) => this.openWidgetSettings(widgetId)
|
||
});
|
||
|
||
// Initialize Drag & Drop (with editManager and dashboardManager references)
|
||
this.dragHandler = new DragDropHandler(this.gridEngine, {
|
||
showGrid: true,
|
||
enableSnap: true,
|
||
editManager: this.editManager,
|
||
dashboardManager: this
|
||
});
|
||
|
||
// Initialize Resize Handler (with editManager and overlay references)
|
||
this.resizeHandler = new ResizeHandler(this.gridEngine, {
|
||
minWidth: 1,
|
||
minHeight: 2,
|
||
maxWidth: 4, // Max 4 columns (will be clamped to actual column count)
|
||
maxHeight: 10,
|
||
editManager: this.editManager,
|
||
resizeHandlesOverlay: this.resizeHandlesOverlay
|
||
});
|
||
|
||
// 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();
|
||
|
||
// Measure container width and set up responsive sizing
|
||
this.setupContainerSizing();
|
||
|
||
// Listen for tracker config changes (reactive integration)
|
||
document.addEventListener('rpg:trackerConfigChanged', (e) => {
|
||
console.log('[DashboardManager] Tracker config changed, refreshing widgets');
|
||
this.onTrackerConfigChanged(e.detail.config);
|
||
});
|
||
|
||
// Render tab navigation
|
||
this.renderTabs();
|
||
|
||
console.log('[DashboardManager] All systems initialized');
|
||
this.notifyChange('initialized');
|
||
}
|
||
|
||
/**
|
||
* Create dashboard container structure
|
||
*/
|
||
createContainerStructure() {
|
||
// Check if tabs and grid containers already exist (from template)
|
||
this.tabContainer = this.container.querySelector('#rpg-dashboard-tabs');
|
||
this.gridContainer = this.container.querySelector('#rpg-dashboard-grid');
|
||
|
||
// If they don't exist, create them (fallback for legacy/minimal setup)
|
||
if (!this.tabContainer) {
|
||
console.warn('[DashboardManager] Tab container not found in template, creating...');
|
||
this.tabContainer = document.createElement('div');
|
||
this.tabContainer.className = 'rpg-dashboard-tabs';
|
||
this.tabContainer.id = 'rpg-dashboard-tabs';
|
||
this.container.appendChild(this.tabContainer);
|
||
}
|
||
|
||
if (!this.gridContainer) {
|
||
console.warn('[DashboardManager] Grid container not found in template, creating...');
|
||
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);
|
||
}
|
||
|
||
// Create overlay containers for resize handles and edit controls
|
||
// These are positioned outside the widget DOM to prevent overflow/scrollbar issues
|
||
this.resizeHandlesOverlay = document.createElement('div');
|
||
this.resizeHandlesOverlay.id = 'rpg-resize-handles-overlay';
|
||
this.resizeHandlesOverlay.className = 'rpg-overlay-container';
|
||
this.resizeHandlesOverlay.style.cssText = 'position: absolute; inset: 0; pointer-events: none; z-index: 9999;';
|
||
this.gridContainer.appendChild(this.resizeHandlesOverlay);
|
||
|
||
this.editControlsOverlay = document.createElement('div');
|
||
this.editControlsOverlay.id = 'rpg-edit-controls-overlay';
|
||
this.editControlsOverlay.className = 'rpg-overlay-container';
|
||
this.editControlsOverlay.style.cssText = 'position: absolute; inset: 0; pointer-events: none; z-index: 10000;';
|
||
this.gridContainer.appendChild(this.editControlsOverlay);
|
||
|
||
console.log('[DashboardManager] Container structure ready (including overlays)');
|
||
}
|
||
|
||
/**
|
||
* Set up container sizing and responsive behavior
|
||
* Measures container width and sets up ResizeObserver
|
||
* Also listens for viewport resize to recalculate vw/vh positions
|
||
*/
|
||
setupContainerSizing() {
|
||
// Measure actual container width
|
||
const width = this.gridContainer.clientWidth || this.gridContainer.offsetWidth || 350;
|
||
console.log('[DashboardManager] Measured container width:', width);
|
||
|
||
// Set container width in GridEngine (triggers column calculation)
|
||
this.gridEngine.setContainerWidth(width);
|
||
|
||
// Set up ResizeObserver to track container width changes
|
||
if (typeof ResizeObserver !== 'undefined') {
|
||
this.resizeObserver = new ResizeObserver((entries) => {
|
||
for (const entry of entries) {
|
||
const newWidth = entry.contentRect.width;
|
||
|
||
// setContainerWidth returns true if columns changed
|
||
const columnsChanged = this.gridEngine.setContainerWidth(newWidth);
|
||
|
||
// If columns changed, onColumnsChange already handled full reflow
|
||
if (columnsChanged) {
|
||
console.log('[DashboardManager] Container resized, columns changed. Full reflow handled.');
|
||
// Clear any pending lightweight refresh to avoid conflicts
|
||
clearTimeout(this.resizeTimeout);
|
||
return;
|
||
}
|
||
|
||
// If columns did NOT change, trigger debounced lightweight refresh
|
||
// This handles resizing within the same column count
|
||
clearTimeout(this.resizeTimeout);
|
||
this.resizeTimeout = setTimeout(() => {
|
||
console.log('[DashboardManager] Container resized, no column change. Triggering lightweight refresh.');
|
||
this.refreshWidgetsAfterResize();
|
||
}, 150); // Using shorter 150ms debounce for better UX
|
||
}
|
||
});
|
||
|
||
this.resizeObserver.observe(this.gridContainer);
|
||
console.log('[DashboardManager] ResizeObserver set up');
|
||
} else {
|
||
console.warn('[DashboardManager] ResizeObserver not supported, responsive sizing disabled');
|
||
}
|
||
|
||
// Listen for window resize to recalculate vh positions
|
||
// Viewport height changes affect vh calculations for vertical positioning
|
||
// Horizontal (%) automatically adapts to container width changes via ResizeObserver
|
||
this.viewportResizeHandler = () => {
|
||
console.log('[DashboardManager] Viewport resized, recalculating vh positions');
|
||
this.renderAllWidgets(); // Re-render with new vh values
|
||
};
|
||
window.addEventListener('resize', this.viewportResizeHandler);
|
||
console.log('[DashboardManager] Viewport resize listener added');
|
||
}
|
||
|
||
/**
|
||
* Migrate old 12-column layouts to new responsive grid
|
||
* Detects if any widgets have widths exceeding current column count
|
||
* and automatically runs auto-layout to fix them
|
||
*/
|
||
migrateOldLayouts() {
|
||
console.log('[DashboardManager] Checking for old layouts to migrate...');
|
||
|
||
let needsMigration = false;
|
||
|
||
// Check all tabs
|
||
this.dashboard.tabs.forEach(tab => {
|
||
if (!tab.widgets || tab.widgets.length === 0) return;
|
||
|
||
// Check if any widget has width exceeding current column count
|
||
tab.widgets.forEach(widget => {
|
||
if (widget.w > this.gridEngine.columns) {
|
||
console.warn(`[DashboardManager] Widget ${widget.id} has width ${widget.w} exceeding column count ${this.gridEngine.columns}`);
|
||
needsMigration = true;
|
||
}
|
||
});
|
||
|
||
if (needsMigration) {
|
||
console.log(`[DashboardManager] Migrating tab ${tab.id} to new responsive grid...`);
|
||
// Run auto-layout on this tab's widgets
|
||
this.gridEngine.autoLayout(tab.widgets, { preferFullWidth: true });
|
||
console.log(`[DashboardManager] Tab ${tab.id} migrated successfully`);
|
||
}
|
||
});
|
||
|
||
if (needsMigration) {
|
||
// Save migrated layout
|
||
this.triggerAutoSave();
|
||
|
||
// Re-render current tab with new positions
|
||
this.clearGrid();
|
||
const currentTab = this.tabManager.getTab(this.currentTabId);
|
||
if (currentTab && currentTab.widgets) {
|
||
currentTab.widgets.forEach(widget => {
|
||
const definition = this.registry.get(widget.type);
|
||
if (definition) {
|
||
this.renderWidget(widget, definition);
|
||
}
|
||
});
|
||
}
|
||
|
||
console.log('[DashboardManager] Old layouts migrated, saved, and re-rendered');
|
||
} else {
|
||
console.log('[DashboardManager] No migration needed');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Render tab navigation UI
|
||
*/
|
||
renderTabs() {
|
||
if (!this.tabContainer) {
|
||
console.warn('[DashboardManager] Tab container not found');
|
||
return;
|
||
}
|
||
|
||
// Clear existing tabs
|
||
this.tabContainer.innerHTML = '';
|
||
|
||
// Get all tabs sorted by order
|
||
const tabs = this.tabManager.getTabs();
|
||
|
||
if (tabs.length === 0) {
|
||
console.warn('[DashboardManager] No tabs to render');
|
||
return;
|
||
}
|
||
|
||
// Create tab buttons
|
||
tabs.forEach(tab => {
|
||
const button = document.createElement('button');
|
||
button.className = 'rpg-dashboard-tab';
|
||
button.dataset.tabId = tab.id;
|
||
button.innerHTML = `
|
||
<span class="rpg-tab-icon"><i class="${tab.icon}"></i></span>
|
||
<span class="rpg-tab-name">${tab.name}</span>
|
||
`;
|
||
|
||
// Mark active tab
|
||
if (tab.id === this.currentTabId) {
|
||
button.classList.add('active');
|
||
}
|
||
|
||
// Tab click handler
|
||
button.addEventListener('click', () => {
|
||
this.switchTab(tab.id);
|
||
});
|
||
|
||
this.tabContainer.appendChild(button);
|
||
});
|
||
|
||
// Icon-only mode when 4+ tabs to prevent header wrapping on hover
|
||
if (tabs.length > 3) {
|
||
this.tabContainer.classList.add('rpg-tabs-icon-only');
|
||
} else {
|
||
this.tabContainer.classList.remove('rpg-tabs-icon-only');
|
||
}
|
||
|
||
console.log(`[DashboardManager] Rendered ${tabs.length} tabs`);
|
||
}
|
||
|
||
/**
|
||
* 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 });
|
||
}
|
||
|
||
/**
|
||
* Move a widget from one tab to another
|
||
* @param {string} widgetId - Widget ID to move
|
||
* @param {string} targetTabId - Target tab ID
|
||
*/
|
||
moveWidgetToTab(widgetId, targetTabId) {
|
||
console.log(`[DashboardManager] Moving widget ${widgetId} to tab ${targetTabId}`);
|
||
|
||
// Find which tab currently contains the widget
|
||
let sourceTab = null;
|
||
let widgetData = null;
|
||
|
||
for (const tab of this.dashboard.tabs) {
|
||
if (tab.widgets) {
|
||
const index = tab.widgets.findIndex(w => w.id === widgetId);
|
||
if (index !== -1) {
|
||
sourceTab = tab;
|
||
widgetData = tab.widgets[index];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!sourceTab || !widgetData) {
|
||
console.warn(`[DashboardManager] Widget ${widgetId} not found in any tab`);
|
||
return;
|
||
}
|
||
|
||
// Get target tab
|
||
const targetTab = this.tabManager.getTab(targetTabId);
|
||
if (!targetTab) {
|
||
console.warn(`[DashboardManager] Target tab ${targetTabId} not found`);
|
||
return;
|
||
}
|
||
|
||
// Don't move if already in target tab
|
||
if (sourceTab.id === targetTabId) {
|
||
console.log(`[DashboardManager] Widget ${widgetId} already in tab ${targetTabId}`);
|
||
return;
|
||
}
|
||
|
||
// Remove from source tab
|
||
const index = sourceTab.widgets.findIndex(w => w.id === widgetId);
|
||
sourceTab.widgets.splice(index, 1);
|
||
|
||
// Find available position in target tab (collision detection)
|
||
if (!targetTab.widgets) {
|
||
targetTab.widgets = [];
|
||
}
|
||
|
||
// Find available position explicitly checking against target tab widgets
|
||
const availablePosition = this.findAvailablePositionInWidgets(
|
||
{ w: widgetData.w, h: widgetData.h },
|
||
targetTab.widgets
|
||
);
|
||
|
||
widgetData.x = availablePosition.x;
|
||
widgetData.y = availablePosition.y;
|
||
|
||
console.log(`[DashboardManager] Found available position in target tab: (${availablePosition.x}, ${availablePosition.y})`);
|
||
|
||
// Add to target tab
|
||
targetTab.widgets.push(widgetData);
|
||
|
||
// Update runtime widget data if it exists
|
||
const runtimeData = this.widgets.get(widgetId);
|
||
if (runtimeData) {
|
||
runtimeData.tabId = targetTabId;
|
||
}
|
||
|
||
// Update DOM if source or target is current tab
|
||
if (sourceTab.id === this.currentTabId || targetTabId === this.currentTabId) {
|
||
// If widget is being moved from current tab, remove its element
|
||
if (sourceTab.id === this.currentTabId && runtimeData) {
|
||
const definition = this.registry.get(widgetData.type);
|
||
if (definition && definition.onRemove) {
|
||
definition.onRemove(runtimeData.element, widgetData.config);
|
||
}
|
||
this.dragHandler.destroyWidget(runtimeData.element);
|
||
this.resizeHandler.destroyWidget(runtimeData.element);
|
||
runtimeData.element.remove();
|
||
this.widgets.delete(widgetId);
|
||
}
|
||
|
||
// If widget is being moved to current tab, render it
|
||
if (targetTabId === this.currentTabId) {
|
||
const definition = this.registry.get(widgetData.type);
|
||
if (definition) {
|
||
this.renderWidget(widgetData, definition);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Trigger auto-save
|
||
this.triggerAutoSave();
|
||
|
||
console.log(`[DashboardManager] Moved widget ${widgetId} from ${sourceTab.id} to ${targetTabId} at position (${widgetData.x}, ${widgetData.y})`);
|
||
this.notifyChange('widgetMoved', { widgetId, sourceTabId: sourceTab.id, targetTabId });
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
|
||
// Validate widget dimensions (defensive check - shouldn't be needed if onColumnsChange works)
|
||
const validated = this.gridEngine.validateWidget(widget, definition.minSize || { w: 1, h: 1 });
|
||
|
||
// Position widget using validated dimensions
|
||
const pos = this.gridEngine.getWidgetPosition(validated);
|
||
element.style.position = 'absolute';
|
||
element.style.left = pos.left; // % of container (e.g., "5.23%")
|
||
element.style.top = pos.top; // vh units (e.g., "10.45vh")
|
||
element.style.width = pos.width; // % of container (e.g., "45.67%")
|
||
element.style.height = pos.height; // vh units (e.g., "20.12vh")
|
||
|
||
// Add to grid
|
||
this.gridContainer.appendChild(element);
|
||
|
||
// Render widget content
|
||
this.renderWidgetContent(element, widget, definition);
|
||
|
||
// Get current tab's widgets for collision detection
|
||
const currentTab = this.tabManager.getTab(this.currentTabId);
|
||
const allWidgets = currentTab ? currentTab.widgets : [];
|
||
|
||
// Initialize drag & drop
|
||
this.dragHandler.initWidget(element, widget, (updated, newX, newY) => {
|
||
widget.x = newX;
|
||
widget.y = newY;
|
||
|
||
// After drag (which may have triggered reflow), reposition ALL widgets
|
||
// because reflow may have moved other widgets
|
||
this.repositionAllWidgetsInCurrentTab();
|
||
|
||
this.triggerAutoSave();
|
||
}, allWidgets);
|
||
|
||
// 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
|
||
}, allWidgets);
|
||
|
||
// 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) {
|
||
console.log(`[DashboardManager] renderWidgetContent called for ${widget.type}`);
|
||
|
||
// 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) {
|
||
console.log(`[DashboardManager] Calling render for ${widget.type}`, element);
|
||
// Pass widget dimensions along with config for layout calculations
|
||
definition.render(element, {
|
||
...widget.config,
|
||
_width: widget.w,
|
||
_height: widget.h
|
||
});
|
||
console.log(`[DashboardManager] After render, element children:`, element.children.length);
|
||
} else {
|
||
console.warn(`[DashboardManager] No render function for ${widget.type}`);
|
||
}
|
||
|
||
// Note: Content editing will be disabled in bulk after all widgets are rendered
|
||
// (see onTabChange for global disable pass)
|
||
}
|
||
|
||
/**
|
||
* Reposition widget element
|
||
* @param {HTMLElement} element - Widget element
|
||
* @param {Object} widget - Widget data
|
||
*/
|
||
repositionWidget(element, widget) {
|
||
const pos = this.gridEngine.getWidgetPosition(widget);
|
||
element.style.left = pos.left;
|
||
element.style.top = pos.top;
|
||
element.style.width = pos.width;
|
||
element.style.height = pos.height;
|
||
|
||
// Update overlay positions (resize handles and edit controls) to match new widget position
|
||
this.syncOverlaysForWidget(element, widget.id);
|
||
}
|
||
|
||
/**
|
||
* Sync overlay elements (handles and controls) for a specific widget
|
||
* @param {HTMLElement} element - Widget element
|
||
* @param {string} widgetId - Widget ID
|
||
*/
|
||
syncOverlaysForWidget(element, widgetId) {
|
||
// Update resize handles position
|
||
if (this.resizeHandler) {
|
||
const handlerData = this.resizeHandler.resizeHandlers.get(element);
|
||
if (handlerData && handlerData.handles) {
|
||
this.resizeHandler.updateHandlePosition(handlerData.handles, element);
|
||
}
|
||
}
|
||
|
||
// Update edit controls position
|
||
if (this.editManager && this.editManager.isEditMode) {
|
||
const controlData = this.editManager.widgetControlsMap.get(widgetId);
|
||
if (controlData && controlData.controls) {
|
||
this.editManager.updateControlPosition(controlData.controls, element);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Re-render all widgets (repositions all widgets with current grid calculations)
|
||
*/
|
||
renderAllWidgets() {
|
||
this.widgets.forEach((widgetData) => {
|
||
this.repositionWidget(widgetData.element, widgetData.widget);
|
||
});
|
||
console.log('[DashboardManager] Repositioned all widgets');
|
||
}
|
||
|
||
/**
|
||
* Lightweight refresh of widgets after container resize without column change
|
||
* Repositions widgets to apply new CSS dimensions and calls onResize handlers
|
||
* Does NOT change widget grid positions (x, y, w, h)
|
||
*/
|
||
refreshWidgetsAfterResize() {
|
||
// 1. Reposition all widgets to apply new CSS width/height percentages
|
||
this.renderAllWidgets();
|
||
|
||
// 2. Call onResize handlers for each widget to allow internal layout updates
|
||
this.widgets.forEach((widgetData) => {
|
||
if (widgetData?.definition?.onResize && widgetData.element) {
|
||
const widget = widgetData.widget;
|
||
// Pass grid units (w, h) for consistency with other onResize calls
|
||
widgetData.definition.onResize(widgetData.element, widget.w, widget.h);
|
||
}
|
||
});
|
||
|
||
console.log('[DashboardManager] Lightweight widget refresh complete');
|
||
}
|
||
|
||
/**
|
||
* Reposition all widgets in the current tab
|
||
* Used after drag/drop reflow to update positions of all affected widgets
|
||
*/
|
||
repositionAllWidgetsInCurrentTab() {
|
||
const currentTab = this.tabManager.getTab(this.currentTabId);
|
||
if (!currentTab) return;
|
||
|
||
// Reposition each widget in the current tab
|
||
currentTab.widgets.forEach((widget) => {
|
||
const widgetData = this.widgets.get(widget.id);
|
||
if (widgetData && widgetData.element) {
|
||
this.repositionWidget(widgetData.element, widget);
|
||
}
|
||
});
|
||
|
||
console.log('[DashboardManager] Repositioned all widgets in current tab after reflow');
|
||
}
|
||
|
||
/**
|
||
* Estimate total height needed for widgets if laid out
|
||
* Simple estimation: sum all widget heights + gaps
|
||
*
|
||
* @param {Array<Object>} widgets - Widgets to estimate
|
||
* @returns {number} Estimated height in rem
|
||
*/
|
||
estimateLayoutHeight(widgets) {
|
||
if (widgets.length === 0) return 0;
|
||
|
||
// Sum all heights (widgets are already in rem units)
|
||
const totalHeight = widgets.reduce((sum, w) => sum + w.h, 0);
|
||
|
||
// Add gaps (rowHeight + gap between each widget)
|
||
const gaps = (widgets.length - 1) * this.gridEngine.gap;
|
||
|
||
return totalHeight * this.gridEngine.rowHeight + gaps;
|
||
}
|
||
|
||
/**
|
||
* Distribute widgets across multiple tabs by category
|
||
* Creates category-based tabs: Status, Social, Inventory
|
||
*
|
||
* @param {Array<Object>} widgets - All widgets to distribute
|
||
*/
|
||
distributeWidgetsByCategory(widgets) {
|
||
console.log('[DashboardManager] ===== DISTRIBUTE WIDGETS BY CATEGORY CALLED =====');
|
||
console.log('[DashboardManager] Distributing widgets across multiple tabs');
|
||
|
||
// Group widgets by category
|
||
const groups = {
|
||
user: [],
|
||
scene: [],
|
||
social: [],
|
||
inventory: [],
|
||
quests: []
|
||
};
|
||
|
||
widgets.forEach(widget => {
|
||
const def = this.registry.get(widget.type);
|
||
const category = def?.category || 'user';
|
||
if (groups[category]) {
|
||
groups[category].push(widget);
|
||
} else {
|
||
groups.user.push(widget); // Fallback to user
|
||
}
|
||
});
|
||
|
||
// Clear existing tabs
|
||
this.dashboard.tabs = [];
|
||
|
||
// Create Status tab (user widgets ONLY - prioritized)
|
||
if (groups.user.length > 0) {
|
||
this.dashboard.tabs.push({
|
||
id: 'tab-status',
|
||
name: 'Status',
|
||
icon: 'fa-solid fa-user',
|
||
order: 0,
|
||
widgets: groups.user
|
||
});
|
||
|
||
// Auto-layout status widgets
|
||
this.gridEngine.autoLayout(groups.user, { preserveOrder: true });
|
||
}
|
||
|
||
// Create Scene/Info tab if there are scene widgets (overflow from Status)
|
||
if (groups.scene.length > 0) {
|
||
this.dashboard.tabs.push({
|
||
id: 'tab-scene',
|
||
name: 'Scene',
|
||
icon: 'fa-solid fa-map',
|
||
order: 1,
|
||
widgets: groups.scene
|
||
});
|
||
|
||
this.gridEngine.autoLayout(groups.scene, { preserveOrder: true });
|
||
}
|
||
|
||
// Create Social tab if there are social widgets
|
||
if (groups.social.length > 0) {
|
||
this.dashboard.tabs.push({
|
||
id: 'tab-social',
|
||
name: 'Social',
|
||
icon: 'fa-solid fa-users',
|
||
order: 2,
|
||
widgets: groups.social
|
||
});
|
||
|
||
this.gridEngine.autoLayout(groups.social, { preserveOrder: true });
|
||
}
|
||
|
||
// Create Inventory tab if there are inventory widgets
|
||
if (groups.inventory.length > 0) {
|
||
this.dashboard.tabs.push({
|
||
id: 'tab-inventory',
|
||
name: 'Inventory',
|
||
icon: 'fa-solid fa-bag-shopping',
|
||
order: 3,
|
||
widgets: groups.inventory
|
||
});
|
||
|
||
this.gridEngine.autoLayout(groups.inventory, { preserveOrder: true });
|
||
}
|
||
|
||
// Create Quests tab if there are quest widgets
|
||
if (groups.quests.length > 0) {
|
||
this.dashboard.tabs.push({
|
||
id: 'tab-quests',
|
||
name: 'Quests',
|
||
icon: 'fa-solid fa-scroll',
|
||
order: 4,
|
||
widgets: groups.quests
|
||
});
|
||
|
||
this.gridEngine.autoLayout(groups.quests, { preserveOrder: true });
|
||
}
|
||
|
||
console.log('[DashboardManager] Created', this.dashboard.tabs.length, 'tabs');
|
||
|
||
// Re-render tabs and switch to first tab
|
||
this.renderTabs();
|
||
if (this.dashboard.tabs.length > 0) {
|
||
this.switchTab(this.dashboard.tabs[0].id);
|
||
}
|
||
|
||
// After rendering, call onResize for all currently rendered widgets to update internal layouts
|
||
// This ensures widgets like User Attributes recalculate their grid columns
|
||
// Note: Only iterate over this.widgets (currently rendered), not all tabs (includes non-rendered widgets)
|
||
console.log(`[DashboardManager] Calling onResize for ${this.widgets.size} rendered widgets after auto-layout`);
|
||
this.widgets.forEach(widgetData => {
|
||
if (widgetData?.definition?.onResize && widgetData.element) {
|
||
console.log(`[DashboardManager] Calling onResize for ${widgetData.widget.type} (${widgetData.widget.w}x${widgetData.widget.h})`);
|
||
widgetData.definition.onResize(widgetData.element, widgetData.widget.w, widgetData.widget.h);
|
||
}
|
||
});
|
||
|
||
// Save layout
|
||
this.triggerAutoSave();
|
||
}
|
||
|
||
/**
|
||
* Sort widgets by category for logical auto-layout
|
||
* Groups: user → scene → social → inventory
|
||
* Within groups, maintains smart ordering (e.g., userInfo before userStats)
|
||
*
|
||
* @param {Array<Object>} widgets - Widgets to sort
|
||
* @returns {Array<Object>} Sorted widgets
|
||
*/
|
||
sortWidgetsByCategory(widgets) {
|
||
// Category priority order
|
||
const categoryOrder = {
|
||
'user': 1,
|
||
'scene': 2,
|
||
'social': 3,
|
||
'inventory': 4,
|
||
'quests': 5,
|
||
'other': 6
|
||
};
|
||
|
||
// Specific widget type ordering within user category
|
||
const userWidgetOrder = {
|
||
'userInfo': 1, // Name/level at top-left
|
||
'userMood': 2, // Mood at top-right (before stats so it sits beside userInfo)
|
||
'userStats': 3, // Health/energy bars (after mood, goes below userInfo+mood)
|
||
'userAttributes': 4 // STR/DEX/etc
|
||
};
|
||
|
||
return [...widgets].sort((a, b) => {
|
||
// Get widget definitions from registry
|
||
const defA = this.registry.get(a.type);
|
||
const defB = this.registry.get(b.type);
|
||
|
||
const catA = defA?.category || 'other';
|
||
const catB = defB?.category || 'other';
|
||
|
||
// Sort by category first
|
||
const catOrderA = categoryOrder[catA] || 999;
|
||
const catOrderB = categoryOrder[catB] || 999;
|
||
|
||
if (catOrderA !== catOrderB) {
|
||
return catOrderA - catOrderB;
|
||
}
|
||
|
||
// Within user category, use specific ordering
|
||
if (catA === 'user' && catB === 'user') {
|
||
const orderA = userWidgetOrder[a.type] || 999;
|
||
const orderB = userWidgetOrder[b.type] || 999;
|
||
if (orderA !== orderB) {
|
||
return orderA - orderB;
|
||
}
|
||
}
|
||
|
||
// Otherwise maintain original order
|
||
return 0;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 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 || [];
|
||
return this.findAvailablePositionInWidgets(size, widgets);
|
||
}
|
||
|
||
/**
|
||
* Find available position for widget in a specific widgets array
|
||
* @param {Object} size - Widget size { w, h }
|
||
* @param {Array<Object>} widgets - Array of existing widgets to check against
|
||
* @returns {Object} Position { x, y }
|
||
*/
|
||
findAvailablePositionInWidgets(size, widgets) {
|
||
console.log(`[DashboardManager] Finding available position for ${size.w}x${size.h} widget among ${widgets.length} existing widgets`);
|
||
|
||
// Try to place at top-left, move right, then down
|
||
for (let y = 0; y < 20; y++) {
|
||
for (let x = 0; x <= this.gridEngine.columns - size.w; x++) {
|
||
const testWidget = { x, y, w: size.w, h: size.h };
|
||
|
||
// Check if position overlaps with any existing widget
|
||
const hasCollision = widgets.some(existingWidget => {
|
||
const overlapsX = testWidget.x < existingWidget.x + existingWidget.w &&
|
||
testWidget.x + testWidget.w > existingWidget.x;
|
||
const overlapsY = testWidget.y < existingWidget.y + existingWidget.h &&
|
||
testWidget.y + testWidget.h > existingWidget.y;
|
||
return overlapsX && overlapsY;
|
||
});
|
||
|
||
if (!hasCollision) {
|
||
console.log(`[DashboardManager] Found available position: (${x}, ${y})`);
|
||
return { x, y };
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fallback: place at bottom
|
||
const maxY = widgets.length > 0
|
||
? Math.max(...widgets.map(w => w.y + w.h))
|
||
: 0;
|
||
console.log(`[DashboardManager] No free space found, placing at bottom: (0, ${maxY})`);
|
||
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.setActiveTab(tabId);
|
||
}
|
||
|
||
/**
|
||
* Handle tab change event
|
||
* @param {string} tabId - New active tab ID
|
||
*/
|
||
onTabChange(tabId) {
|
||
console.log(`[DashboardManager] Switching to tab: ${tabId}`);
|
||
this.currentTabId = tabId;
|
||
|
||
// Re-render tabs to update active state
|
||
this.renderTabs();
|
||
|
||
// Clear grid
|
||
this.clearGrid();
|
||
|
||
// Render all widgets in this tab
|
||
const tab = this.tabManager.getTab(tabId);
|
||
console.log(`[DashboardManager] Tab data:`, tab);
|
||
console.log(`[DashboardManager] Tab has ${tab?.widgets?.length || 0} widgets`);
|
||
|
||
if (tab && tab.widgets) {
|
||
tab.widgets.forEach(widget => {
|
||
console.log(`[DashboardManager] Rendering widget:`, widget.type, widget.id);
|
||
const definition = this.registry.get(widget.type);
|
||
if (definition) {
|
||
this.renderWidget(widget, definition);
|
||
} else {
|
||
console.warn(`[DashboardManager] Widget type "${widget.type}" not found in registry`);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Call onResize handlers for all rendered widgets to apply responsive styling
|
||
// This ensures compact classes are applied based on widget dimensions
|
||
console.log(`[DashboardManager] Calling onResize for ${this.widgets.size} widgets after tab switch`);
|
||
this.widgets.forEach(widgetData => {
|
||
if (widgetData?.definition?.onResize && widgetData.element) {
|
||
const widget = widgetData.widget;
|
||
console.log(`[DashboardManager] Calling onResize for ${widget.type} (${widget.w}x${widget.h})`);
|
||
widgetData.definition.onResize(widgetData.element, widget.w, widget.h);
|
||
}
|
||
});
|
||
|
||
// Disable content editing once for all widgets if in edit mode
|
||
// (More efficient than per-widget queries - 2 queries vs 2N queries)
|
||
if (this.editManager && this.editManager.isEditMode) {
|
||
this.editManager.disableContentEditing();
|
||
}
|
||
|
||
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() {
|
||
// Clean up edit controls overlay first
|
||
if (this.editManager) {
|
||
this.editManager.removeAllControls();
|
||
}
|
||
|
||
// 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() {
|
||
const config = {
|
||
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,
|
||
icon: tab.icon,
|
||
order: tab.order,
|
||
widgets: tab.widgets || []
|
||
})),
|
||
defaultTab: this.dashboard.defaultTab
|
||
};
|
||
console.log('[DashboardManager] getDashboardConfig() returning:', {
|
||
rowHeight: config.gridConfig.rowHeight,
|
||
gap: config.gridConfig.gap,
|
||
columns: config.gridConfig.columns
|
||
});
|
||
return config;
|
||
}
|
||
|
||
/**
|
||
* Migrate emoji icons to Font Awesome
|
||
* @param {Object} config - Dashboard configuration
|
||
* @returns {Object} Migrated configuration
|
||
*/
|
||
migrateEmojiIcons(config) {
|
||
// Map of common emojis to Font Awesome classes
|
||
const emojiToFontAwesome = {
|
||
'📊': 'fa-solid fa-chart-line',
|
||
'🌍': 'fa-solid fa-map',
|
||
'🎒': 'fa-solid fa-bag-shopping',
|
||
'🏠': 'fa-solid fa-house',
|
||
'📄': 'fa-solid fa-file',
|
||
'⚙️': 'fa-solid fa-gear',
|
||
'👤': 'fa-solid fa-user',
|
||
'📝': 'fa-solid fa-note-sticky',
|
||
'🗂️': 'fa-solid fa-folder',
|
||
'📁': 'fa-solid fa-folder-open'
|
||
};
|
||
|
||
if (config && config.tabs) {
|
||
config.tabs.forEach(tab => {
|
||
// Check if icon is an emoji (contains emoji characters)
|
||
if (tab.icon && /[\u{1F300}-\u{1F9FF}]/u.test(tab.icon)) {
|
||
// Convert to Font Awesome if we have a mapping
|
||
const faIcon = emojiToFontAwesome[tab.icon];
|
||
if (faIcon) {
|
||
console.log(`[DashboardManager] Migrating emoji icon "${tab.icon}" → "${faIcon}" for tab "${tab.name}"`);
|
||
tab.icon = faIcon;
|
||
} else {
|
||
// Fallback to generic file icon
|
||
console.warn(`[DashboardManager] Unknown emoji icon "${tab.icon}", using fa-solid fa-file for tab "${tab.name}"`);
|
||
tab.icon = 'fa-solid fa-file';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
return config;
|
||
}
|
||
|
||
/**
|
||
* Apply dashboard configuration
|
||
* @param {Object} config - Dashboard configuration
|
||
* @param {Object} options - Optional parameters
|
||
* @param {boolean} options.skipInitialSwitch - Skip switching to first tab (caller will handle)
|
||
*/
|
||
applyDashboardConfig(config, options = {}) {
|
||
console.log('[DashboardManager] Applying dashboard config');
|
||
|
||
// Migrate emoji icons to Font Awesome
|
||
config = this.migrateEmojiIcons(config);
|
||
|
||
// Update grid config from dashboard config
|
||
if (config.gridConfig) {
|
||
this.config.rowHeight = config.gridConfig.rowHeight || this.config.rowHeight;
|
||
this.config.gap = config.gridConfig.gap || this.config.gap;
|
||
|
||
// Update gridEngine with new config
|
||
if (this.gridEngine) {
|
||
this.gridEngine.rowHeight = this.config.rowHeight;
|
||
this.gridEngine.gap = this.config.gap;
|
||
console.log('[DashboardManager] Updated grid config:', {
|
||
rowHeight: this.config.rowHeight + 'rem',
|
||
gap: this.config.gap + 'rem'
|
||
});
|
||
}
|
||
}
|
||
|
||
// Clear existing
|
||
this.clearGrid();
|
||
|
||
// Clear tabs directly (we have access to shared dashboard object)
|
||
this.dashboard.tabs = [];
|
||
|
||
// Recreate tabs from config (preserve IDs and widgets)
|
||
config.tabs.forEach(tabConfig => {
|
||
this.dashboard.tabs.push({
|
||
id: tabConfig.id,
|
||
name: tabConfig.name,
|
||
icon: tabConfig.icon || 'fa-solid fa-file',
|
||
order: tabConfig.order || 0,
|
||
widgets: tabConfig.widgets || []
|
||
});
|
||
});
|
||
|
||
// Update default tab
|
||
if (config.defaultTab) {
|
||
this.dashboard.defaultTab = config.defaultTab;
|
||
} else if (this.dashboard.tabs.length > 0) {
|
||
this.dashboard.defaultTab = this.dashboard.tabs[0].id;
|
||
}
|
||
|
||
// Switch to first tab (unless caller will handle it)
|
||
if (!options.skipInitialSwitch && 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 with auto-layout');
|
||
this.applyDashboardConfig(this.defaultLayout);
|
||
|
||
// Auto-layout each tab to prevent overlap (default positions may not fit screen)
|
||
this.dashboard.tabs.forEach(tab => {
|
||
if (tab.widgets && tab.widgets.length > 0) {
|
||
console.log(`[DashboardManager] Auto-laying out default tab "${tab.name}" (${tab.widgets.length} widgets)`);
|
||
this.gridEngine.autoLayout(tab.widgets, { preserveOrder: true });
|
||
}
|
||
});
|
||
|
||
// Save the auto-laid-out default as the initial saved layout
|
||
await this.saveLayout(true);
|
||
}
|
||
} 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() {
|
||
// Regenerate fresh default layout to ensure all original widgets are restored
|
||
// This ensures deleted widgets come back on reset
|
||
console.log('[DashboardManager] Regenerating fresh default layout...');
|
||
this.defaultLayout = generateDefaultDashboard();
|
||
|
||
// Reset previousTrackerConfig for fresh widget detection
|
||
// This ensures the comparison logic works correctly after reset
|
||
this.previousTrackerConfig = null;
|
||
console.log('[DashboardManager] Reset previousTrackerConfig for fresh widget detection');
|
||
|
||
if (!this.defaultLayout) {
|
||
console.warn('[DashboardManager] Failed to generate default layout');
|
||
return;
|
||
}
|
||
|
||
console.log('[DashboardManager] Resetting to default layout...');
|
||
console.log('[DashboardManager] Default layout has:', this.defaultLayout.tabs.length, 'tabs');
|
||
this.defaultLayout.tabs.forEach(tab => {
|
||
console.log(`[DashboardManager] Tab "${tab.name}" (${tab.id}):`, tab.widgets.length, 'widgets');
|
||
});
|
||
|
||
await this.persistence.resetToDefault(this.defaultLayout);
|
||
// Skip initial switch in applyDashboardConfig since we'll switch after layout calculations
|
||
this.applyDashboardConfig(this.defaultLayout, { skipInitialSwitch: true });
|
||
|
||
// Apply column-aware widget sizes from widget definitions
|
||
// This makes widgets scale properly based on screen width (2-4 columns)
|
||
const allWidgets = [];
|
||
this.dashboard.tabs.forEach(tab => {
|
||
if (tab.widgets && tab.widgets.length > 0) {
|
||
allWidgets.push(...tab.widgets);
|
||
}
|
||
});
|
||
this.resetWidgetSizesToDefault(allWidgets);
|
||
|
||
// Don't call autoLayout - preserve positions from defaultLayout.js
|
||
// Widget definitions now have column-aware sizes (defaultSize returns correct size for column count)
|
||
// ResizeObserver will handle column changes and trigger autoLayout when screen resizes
|
||
console.log('[DashboardManager] Using column-aware sizes from widget definitions, preserving positions from defaultLayout.js');
|
||
|
||
// Force re-render tabs
|
||
this.renderTabs();
|
||
|
||
// Re-render current tab's widgets
|
||
if (this.currentTabId) {
|
||
this.switchTab(this.currentTabId);
|
||
} else if (this.dashboard.tabs.length > 0) {
|
||
this.switchTab(this.dashboard.tabs[0].id);
|
||
}
|
||
|
||
console.log('[DashboardManager] Reset complete with auto-layout');
|
||
}
|
||
|
||
/**
|
||
* Set default layout
|
||
* @param {Object} layout - Default layout configuration
|
||
*/
|
||
setDefaultLayout(layout) {
|
||
this.defaultLayout = layout;
|
||
}
|
||
|
||
/**
|
||
* Reset all widgets to their default sizes
|
||
* @param {Array} widgets - Widgets to reset
|
||
*/
|
||
resetWidgetSizesToDefault(widgets) {
|
||
let resetCount = 0;
|
||
widgets.forEach(widget => {
|
||
const definition = this.registry.get(widget.type);
|
||
if (definition && definition.defaultSize) {
|
||
const oldSize = `${widget.w}x${widget.h}`;
|
||
|
||
// Support defaultSize as function (column-aware sizing)
|
||
let defaultSize;
|
||
if (typeof definition.defaultSize === 'function') {
|
||
defaultSize = definition.defaultSize(this.gridEngine.columns);
|
||
} else {
|
||
defaultSize = definition.defaultSize;
|
||
}
|
||
|
||
widget.w = defaultSize.w;
|
||
widget.h = defaultSize.h;
|
||
const newSize = `${widget.w}x${widget.h}`;
|
||
if (oldSize !== newSize) {
|
||
console.log(`[DashboardManager] Reset ${widget.type} from ${oldSize} to ${newSize}`);
|
||
resetCount++;
|
||
}
|
||
}
|
||
});
|
||
console.log(`[DashboardManager] Reset ${resetCount} widgets to default sizes`);
|
||
}
|
||
|
||
/**
|
||
* Auto-layout widgets on current tab only
|
||
* Sorts and arranges widgets on the current tab to maximize space usage
|
||
*
|
||
* @param {Object} options - Layout options
|
||
* @param {boolean} [options.preserveOrder=true] - Maintain widget order during layout
|
||
* @param {boolean} [options.resetSizes=true] - Reset widgets to default sizes before layout
|
||
*/
|
||
autoLayoutCurrentTab(options = {}) {
|
||
console.log('[DashboardManager] Auto-layout current tab requested');
|
||
|
||
// Get current tab
|
||
const currentTab = this.tabManager.getTab(this.currentTabId);
|
||
if (!currentTab) {
|
||
console.warn('[DashboardManager] No current tab found');
|
||
return;
|
||
}
|
||
|
||
if (!currentTab.widgets || currentTab.widgets.length === 0) {
|
||
console.warn('[DashboardManager] Current tab has no widgets to layout');
|
||
return;
|
||
}
|
||
|
||
console.log(`[DashboardManager] Laying out ${currentTab.widgets.length} widgets on tab "${currentTab.name}"`);
|
||
|
||
// Reset widget sizes to defaults (unless explicitly disabled)
|
||
if (options.resetSizes !== false) {
|
||
this.resetWidgetSizesToDefault(currentTab.widgets);
|
||
}
|
||
|
||
// Sort widgets by category for better organization
|
||
const sortedWidgets = this.sortWidgetsByCategory(currentTab.widgets);
|
||
|
||
// Update tab's widgets array with sorted order
|
||
currentTab.widgets = sortedWidgets;
|
||
|
||
// Store current widget dimensions before auto-layout
|
||
const dimensionsBefore = new Map();
|
||
currentTab.widgets.forEach(widget => {
|
||
dimensionsBefore.set(widget.id, { w: widget.w, h: widget.h });
|
||
});
|
||
|
||
// Auto-layout widgets on the current tab
|
||
this.gridEngine.autoLayout(currentTab.widgets, {
|
||
preserveOrder: options.preserveOrder !== false
|
||
});
|
||
|
||
// Call onResize handlers for widgets whose dimensions changed
|
||
// This allows widgets to update internal layouts (e.g., User Attributes grid columns)
|
||
currentTab.widgets.forEach(widget => {
|
||
const before = dimensionsBefore.get(widget.id);
|
||
if (before && (before.w !== widget.w || before.h !== widget.h)) {
|
||
const widgetData = this.widgets.get(widget.id);
|
||
if (widgetData?.definition?.onResize && widgetData.element) {
|
||
console.log(`[DashboardManager] Calling onResize for ${widget.type} (${before.w}x${before.h} → ${widget.w}x${widget.h})`);
|
||
widgetData.definition.onResize(widgetData.element, widget.w, widget.h);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Re-render all widgets with new positions
|
||
this.clearGrid();
|
||
currentTab.widgets.forEach(widget => {
|
||
const definition = this.registry.get(widget.type);
|
||
if (definition) {
|
||
this.renderWidget(widget, definition);
|
||
}
|
||
});
|
||
|
||
// Save layout
|
||
this.triggerAutoSave();
|
||
|
||
console.log('[DashboardManager] Current tab layout complete');
|
||
}
|
||
|
||
/**
|
||
* Auto-layout widgets on current tab to efficiently use all available space
|
||
*
|
||
* Sorts and packs widgets to maximize space usage with no gaps.
|
||
* Respects current panel width (responsive column count).
|
||
* Re-renders all widgets after repositioning.
|
||
*
|
||
* @param {Object} options - Layout options
|
||
* @param {boolean} [options.preferFullWidth=true] - Prefer full-width widgets when possible
|
||
* @param {boolean} [options.resetSizes=true] - Reset widgets to default sizes before layout
|
||
*/
|
||
autoLayoutWidgets(options = {}) {
|
||
console.log('[DashboardManager] ===== AUTO-LAYOUT WIDGETS CALLED =====');
|
||
console.log('[DashboardManager] Auto-layout widgets requested');
|
||
|
||
// Gather ALL widgets from ALL tabs (don't lose inventory, social, etc.)
|
||
const allWidgets = [];
|
||
this.dashboard.tabs.forEach(tab => {
|
||
if (tab.widgets && tab.widgets.length > 0) {
|
||
console.log(`[DashboardManager] Gathering ${tab.widgets.length} widgets from tab "${tab.name}"`);
|
||
allWidgets.push(...tab.widgets);
|
||
}
|
||
});
|
||
|
||
if (allWidgets.length === 0) {
|
||
console.warn('[DashboardManager] No widgets to auto-layout');
|
||
return;
|
||
}
|
||
|
||
console.log(`[DashboardManager] Total widgets to layout: ${allWidgets.length}`);
|
||
|
||
// Reset widget sizes to defaults (unless explicitly disabled)
|
||
if (options.resetSizes !== false) {
|
||
this.resetWidgetSizesToDefault(allWidgets);
|
||
}
|
||
|
||
// Smart category-aware sorting BEFORE auto-layout
|
||
const widgetsToLayout = this.sortWidgetsByCategory(allWidgets);
|
||
|
||
// Calculate estimated height to determine if multi-tab distribution is needed
|
||
const estimatedHeight = this.estimateLayoutHeight(widgetsToLayout);
|
||
const heightThreshold = 80; // rem - reasonable max height for single tab
|
||
|
||
console.log('[DashboardManager] Estimated height:', estimatedHeight + 'rem', 'Threshold:', heightThreshold + 'rem');
|
||
|
||
// Always use multi-tab distribution when we have many widgets
|
||
// This preserves all widgets (inventory, social, etc.)
|
||
console.log('[DashboardManager] Using multi-tab distribution to preserve all widgets');
|
||
this.distributeWidgetsByCategory(widgetsToLayout);
|
||
|
||
// distributeWidgetsByCategory handles rendering and tab switching
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Widget-to-tab mapping for smart widget placement
|
||
* Maps widget types to their preferred tab IDs
|
||
*/
|
||
static WIDGET_TO_TAB_MAP = {
|
||
'calendar': 'tab-scene',
|
||
'weather': 'tab-scene',
|
||
'temperature': 'tab-scene',
|
||
'clock': 'tab-scene',
|
||
'location': 'tab-scene',
|
||
'recentEvents': 'tab-scene',
|
||
'presentCharacters': 'tab-scene',
|
||
'userStats': 'tab-status',
|
||
'userInfo': 'tab-status',
|
||
'userMood': 'tab-status',
|
||
'userAttributes': 'tab-status',
|
||
'inventory': 'tab-inventory',
|
||
'quests': 'tab-quests'
|
||
};
|
||
|
||
/**
|
||
* Detect config changes between old and new tracker configs
|
||
* Identifies fields that transitioned from disabled to enabled
|
||
* @param {Object} oldConfig - Previous tracker configuration
|
||
* @param {Object} newConfig - New tracker configuration
|
||
* @returns {Array<string>} Array of widget types that should be re-added
|
||
*/
|
||
detectConfigChanges(oldConfig, newConfig) {
|
||
if (!oldConfig) {
|
||
// First run, no changes to detect
|
||
return [];
|
||
}
|
||
|
||
const widgetsToAdd = [];
|
||
|
||
// Check infoBox widgets (calendar, weather, temperature, clock, location, recentEvents)
|
||
const infoBoxWidgetMap = {
|
||
'date': 'calendar',
|
||
'weather': 'weather',
|
||
'temperature': 'temperature',
|
||
'time': 'clock',
|
||
'location': 'location',
|
||
'recentEvents': 'recentEvents'
|
||
};
|
||
|
||
Object.entries(infoBoxWidgetMap).forEach(([fieldKey, widgetType]) => {
|
||
const wasDisabled = oldConfig.infoBox?.widgets?.[fieldKey]?.enabled === false;
|
||
const isNowEnabled = newConfig.infoBox?.widgets?.[fieldKey]?.enabled !== false;
|
||
|
||
if (wasDisabled && isNowEnabled) {
|
||
widgetsToAdd.push(widgetType);
|
||
console.log(`[DashboardManager] Detected re-enabled field: ${fieldKey} → widget: ${widgetType}`);
|
||
}
|
||
});
|
||
|
||
// Check userStats widget (enabled when at least one stat is enabled)
|
||
const oldStatsEnabled = oldConfig.userStats?.customStats?.filter(s => s.enabled).length > 0;
|
||
const newStatsEnabled = newConfig.userStats?.customStats?.filter(s => s.enabled).length > 0;
|
||
|
||
if (!oldStatsEnabled && newStatsEnabled) {
|
||
widgetsToAdd.push('userStats');
|
||
console.log('[DashboardManager] Detected re-enabled userStats widget');
|
||
}
|
||
|
||
// Check userAttributes widget (enabled when RPG Attributes section is enabled AND at least one attribute is enabled)
|
||
const oldAttrsDisabled = oldConfig.userStats?.showRPGAttributes === false ||
|
||
(oldConfig.userStats?.rpgAttributes?.filter(a => a.enabled).length || 0) === 0;
|
||
const newAttrsEnabled = newConfig.userStats?.showRPGAttributes !== false &&
|
||
(newConfig.userStats?.rpgAttributes?.filter(a => a.enabled).length || 0) > 0;
|
||
|
||
if (oldAttrsDisabled && newAttrsEnabled) {
|
||
widgetsToAdd.push('userAttributes');
|
||
console.log('[DashboardManager] Detected re-enabled userAttributes widget');
|
||
}
|
||
|
||
// Check presentCharacters widget
|
||
const wasThoughtsDisabled = oldConfig.presentCharacters?.thoughts?.enabled === false;
|
||
const isThoughtsEnabled = newConfig.presentCharacters?.thoughts?.enabled !== false;
|
||
|
||
if (wasThoughtsDisabled && isThoughtsEnabled) {
|
||
widgetsToAdd.push('presentCharacters');
|
||
console.log('[DashboardManager] Detected re-enabled presentCharacters widget');
|
||
}
|
||
|
||
return widgetsToAdd;
|
||
}
|
||
|
||
/**
|
||
* Add widgets that were re-enabled in tracker config
|
||
* @param {Array<string>} widgetTypes - Array of widget types to add
|
||
*/
|
||
addEnabledWidgets(widgetTypes) {
|
||
if (widgetTypes.length === 0) {
|
||
return;
|
||
}
|
||
|
||
console.log(`[DashboardManager] Adding ${widgetTypes.length} re-enabled widgets:`, widgetTypes);
|
||
|
||
const addedWidgets = [];
|
||
|
||
widgetTypes.forEach(widgetType => {
|
||
// Get widget definition
|
||
const definition = this.registry.get(widgetType);
|
||
if (!definition) {
|
||
console.warn(`[DashboardManager] Widget type "${widgetType}" not found in registry`);
|
||
return;
|
||
}
|
||
|
||
// Determine target tab using mapping
|
||
const preferredTabId = DashboardManager.WIDGET_TO_TAB_MAP[widgetType] || 'tab-status';
|
||
const targetTab = this.tabManager.getTab(preferredTabId);
|
||
|
||
// Fallback to first tab if preferred tab doesn't exist
|
||
const tab = targetTab || this.dashboard.tabs[0];
|
||
if (!tab) {
|
||
console.warn(`[DashboardManager] No tab available to add widget ${widgetType}`);
|
||
return;
|
||
}
|
||
|
||
// Check for duplicates - don't add if widget type already exists in this tab
|
||
const alreadyExists = tab.widgets?.some(w => w.type === widgetType);
|
||
if (alreadyExists) {
|
||
console.log(`[DashboardManager] Widget ${widgetType} already exists in tab ${tab.id}, skipping`);
|
||
return;
|
||
}
|
||
|
||
// Generate unique widget ID
|
||
const widgetId = `widget-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||
|
||
// Find available position in the target tab
|
||
const position = this.findAvailablePositionInWidgets(
|
||
definition.defaultSize,
|
||
tab.widgets || []
|
||
);
|
||
|
||
// Create widget data
|
||
const widget = {
|
||
id: widgetId,
|
||
type: widgetType,
|
||
x: position.x,
|
||
y: position.y,
|
||
w: definition.defaultSize.w,
|
||
h: definition.defaultSize.h,
|
||
config: {}
|
||
};
|
||
|
||
// Add to tab
|
||
if (!tab.widgets) {
|
||
tab.widgets = [];
|
||
}
|
||
tab.widgets.push(widget);
|
||
|
||
console.log(`[DashboardManager] Added widget ${widgetType} (${widgetId}) to tab ${tab.id} at (${position.x}, ${position.y})`);
|
||
|
||
addedWidgets.push({
|
||
widgetId,
|
||
widgetType,
|
||
tabId: tab.id
|
||
});
|
||
});
|
||
|
||
// Auto-layout affected tabs to optimize positioning
|
||
if (addedWidgets.length > 0) {
|
||
const affectedTabs = new Set(addedWidgets.map(w => w.tabId));
|
||
affectedTabs.forEach(tabId => {
|
||
const tab = this.tabManager.getTab(tabId);
|
||
if (tab && tab.widgets && tab.widgets.length > 0) {
|
||
console.log(`[DashboardManager] Auto-layouting tab ${tabId} after widget addition`);
|
||
this.gridEngine.autoLayout(tab.widgets, { preserveOrder: true });
|
||
}
|
||
});
|
||
}
|
||
|
||
console.log(`[DashboardManager] Added ${addedWidgets.length} widgets`);
|
||
}
|
||
|
||
/**
|
||
* Handle tracker configuration changes from editor
|
||
* Removes disabled widgets and refreshes remaining widgets
|
||
* @param {Object} config - New tracker configuration
|
||
*/
|
||
onTrackerConfigChanged(config) {
|
||
console.log('[DashboardManager] Processing tracker config changes...');
|
||
|
||
// Step 1: Detect config changes (disabled → enabled)
|
||
const widgetsToAdd = this.detectConfigChanges(this.previousTrackerConfig, config);
|
||
|
||
// Step 2: Remove widgets that are now disabled
|
||
const removedWidgets = this.removeDisabledWidgets(config);
|
||
|
||
// Step 3: Add widgets that were re-enabled
|
||
this.addEnabledWidgets(widgetsToAdd);
|
||
|
||
// Step 4: If widgets were removed or added, auto-layout affected tabs
|
||
const allAffectedTabs = new Set([
|
||
...removedWidgets.map(w => w.tabId),
|
||
// Note: addEnabledWidgets already handles auto-layout for added widgets
|
||
]);
|
||
|
||
if (removedWidgets.length > 0) {
|
||
allAffectedTabs.forEach(tabId => {
|
||
const tab = this.tabManager.getTab(tabId);
|
||
if (tab && tab.widgets && tab.widgets.length > 0) {
|
||
console.log(`[DashboardManager] Auto-layouting tab ${tabId} after changes`);
|
||
this.gridEngine.autoLayout(tab.widgets, { preserveOrder: true });
|
||
}
|
||
});
|
||
}
|
||
|
||
// Step 5: Refresh all widgets (re-render with new config)
|
||
// This updates widget content (e.g., renamed stats) without repositioning
|
||
this.refreshAllWidgets();
|
||
|
||
// Step 6: If widgets were added to current tab, re-render to show them
|
||
if (widgetsToAdd.length > 0) {
|
||
const currentTab = this.tabManager.getTab(this.currentTabId);
|
||
if (currentTab) {
|
||
// Re-render current tab to show newly added widgets
|
||
this.clearGrid();
|
||
currentTab.widgets.forEach(widget => {
|
||
const definition = this.registry.get(widget.type);
|
||
if (definition) {
|
||
this.renderWidget(widget, definition);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Step 7: Store current config for next comparison
|
||
this.previousTrackerConfig = JSON.parse(JSON.stringify(config)); // Deep clone
|
||
|
||
// Step 8: Save layout changes
|
||
this.triggerAutoSave();
|
||
|
||
console.log('[DashboardManager] Tracker config refresh complete');
|
||
}
|
||
|
||
/**
|
||
* Remove widgets that should no longer be shown based on config
|
||
* @param {Object} config - Tracker configuration
|
||
* @returns {Array} Array of removed widget info {widgetId, tabId, type}
|
||
*/
|
||
removeDisabledWidgets(config) {
|
||
const removed = [];
|
||
|
||
// Iterate through all tabs
|
||
this.dashboard.tabs.forEach(tab => {
|
||
if (!tab.widgets) return;
|
||
|
||
// Find widgets to remove
|
||
const toRemove = tab.widgets.filter(widget =>
|
||
this.shouldWidgetBeRemoved(widget.type, config)
|
||
);
|
||
|
||
// Remove each widget
|
||
toRemove.forEach(widget => {
|
||
console.log(`[DashboardManager] Removing disabled widget: ${widget.type} (${widget.id})`);
|
||
|
||
// If widget is in current tab and rendered, clean it up
|
||
if (tab.id === this.currentTabId) {
|
||
const widgetData = this.widgets.get(widget.id);
|
||
if (widgetData) {
|
||
const definition = this.registry.get(widget.type);
|
||
if (definition && definition.onRemove) {
|
||
definition.onRemove(widgetData.element, widget.config);
|
||
}
|
||
this.dragHandler.destroyWidget(widgetData.element);
|
||
this.resizeHandler.destroyWidget(widgetData.element);
|
||
widgetData.element.remove();
|
||
this.widgets.delete(widget.id);
|
||
}
|
||
}
|
||
|
||
removed.push({
|
||
widgetId: widget.id,
|
||
tabId: tab.id,
|
||
type: widget.type
|
||
});
|
||
});
|
||
|
||
// Remove from tab's widget array
|
||
tab.widgets = tab.widgets.filter(widget =>
|
||
!toRemove.some(r => r.id === widget.id)
|
||
);
|
||
});
|
||
|
||
console.log(`[DashboardManager] Removed ${removed.length} disabled widgets`);
|
||
return removed;
|
||
}
|
||
|
||
/**
|
||
* Determine if widget should be removed based on tracker config
|
||
* @param {string} widgetType - Widget type
|
||
* @param {Object} config - Tracker configuration
|
||
* @returns {boolean} True if widget should be removed
|
||
*/
|
||
shouldWidgetBeRemoved(widgetType, config) {
|
||
const rules = {
|
||
'calendar': () => config.infoBox?.widgets?.date?.enabled === false,
|
||
'weather': () => config.infoBox?.widgets?.weather?.enabled === false,
|
||
'temperature': () => config.infoBox?.widgets?.temperature?.enabled === false,
|
||
'clock': () => config.infoBox?.widgets?.time?.enabled === false,
|
||
'location': () => config.infoBox?.widgets?.location?.enabled === false,
|
||
'recentEvents': () => config.infoBox?.widgets?.recentEvents?.enabled === false,
|
||
'userStats': () => {
|
||
const customStats = config.userStats?.customStats || [];
|
||
return customStats.filter(s => s.enabled).length === 0;
|
||
},
|
||
'userAttributes': () => {
|
||
// Remove if RPG Attributes section is disabled
|
||
if (config.userStats?.showRPGAttributes === false) {
|
||
return true;
|
||
}
|
||
// Remove if all attributes are disabled
|
||
const rpgAttrs = config.userStats?.rpgAttributes || [];
|
||
return rpgAttrs.filter(attr => attr.enabled).length === 0;
|
||
},
|
||
'presentCharacters': () => config.presentCharacters?.thoughts?.enabled === false
|
||
};
|
||
|
||
const rule = rules[widgetType];
|
||
return rule ? rule() : false;
|
||
}
|
||
|
||
/**
|
||
* Refresh all rendered widgets (re-render with current data)
|
||
*/
|
||
refreshAllWidgets() {
|
||
console.log('[DashboardManager] Refreshing all widgets...');
|
||
this.widgets.forEach((widgetData) => {
|
||
const definition = this.registry.get(widgetData.widget.type);
|
||
if (definition && widgetData.element) {
|
||
this.renderWidgetContent(widgetData.element, widgetData.widget, definition);
|
||
}
|
||
});
|
||
console.log('[DashboardManager] All widgets refreshed');
|
||
}
|
||
|
||
/**
|
||
* Destroy dashboard and cleanup
|
||
*/
|
||
destroy() {
|
||
console.log('[DashboardManager] Destroying dashboard');
|
||
|
||
// Clear grid
|
||
this.clearGrid();
|
||
|
||
// Disconnect ResizeObserver
|
||
if (this.resizeObserver) {
|
||
this.resizeObserver.disconnect();
|
||
this.resizeObserver = null;
|
||
}
|
||
|
||
// Remove viewport resize listener
|
||
if (this.viewportResizeHandler) {
|
||
window.removeEventListener('resize', this.viewportResizeHandler);
|
||
this.viewportResizeHandler = null;
|
||
}
|
||
|
||
// 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 = '';
|
||
}
|
||
}
|