refactor(dashboard): move controls to overlays and fix mobile/positioning issues

BREAKING CHANGES:
- Resize handles and edit controls now render in separate overlay containers
- This prevents widget overflow and scrollbar issues

Major Changes:

Overlay System Refactor:
- Create overlay containers for resize handles and edit controls
- Append handles/controls to overlays instead of widget DOM
- Fix position calculations using offsetLeft/Top instead of getBoundingClientRect
- Add position sync after resize/drag/reposition operations
- Add cleanup methods when switching tabs

Mobile Fixes:
- Add pointer/touch event support for Add Widget button
- Fix file upload trigger for iOS/Android compatibility
- Move modals to document.body with flex centering
- Fix modal button font-sizes (rem-based instead of vw)
- Add mobile-responsive styling for widget dialog

Bug Fixes:
- Fix getActiveTabId() calls (use activeTabId property instead)
- Fix file input disabled state (exclude type="file" from edit mode disable)
- Fix Add Widget modal registry.getAll() destructuring (objects not arrays)
- Fix edit/delete button hover (keep visible when hovering buttons)
- Fix reset layout to restore deleted widgets (regenerate default layout)

UI Improvements:
- No more scrollbars from resize handles
- Consistent button visibility in edit mode
- Touch-friendly button sizes on mobile (44px min-height)
- Single-column widget grid on mobile
- Proper z-index stacking for overlays

Files Changed:
- dashboardManager.js: Overlay container management, sync methods
- resizeHandler.js: Append to overlay, update positioning
- editModeManager.js: Append to overlay, hover behavior, sync/cleanup
- dashboardIntegration.js: Mobile touch events, file upload, modal fixes
- dashboardTemplate.html: File input accessibility
- style.css: Modal button font-sizes, mobile optimizations
- RESIZE_HANDLES_INVESTIGATION.md: Technical investigation documentation
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-29 19:07:01 +11:00
parent ddb2f8c222
commit 9fbc35dbd9
7 changed files with 651 additions and 28 deletions
+124 -9
View File
@@ -23,6 +23,7 @@ export class EditModeManager {
*/
constructor(config) {
this.container = config.container;
this.editControlsOverlay = config.editControlsOverlay || null; // Overlay container for edit controls
this.onSave = config.onSave;
this.onCancel = config.onCancel;
this.onWidgetAdd = config.onWidgetAdd;
@@ -69,6 +70,9 @@ export class EditModeManager {
// Add edit class to container
this.container.classList.add('edit-mode');
// Add controls to all currently rendered widgets
this.syncAllControls();
this.notifyChange('editModeEntered');
console.log('[EditModeManager] Entered edit mode');
}
@@ -180,8 +184,8 @@ export class EditModeManager {
element.contentEditable = 'false';
});
// Also disable input fields
const inputElements = this.container.querySelectorAll('input, textarea');
// Also disable input fields (except file inputs which should remain functional)
const inputElements = this.container.querySelectorAll('input:not([type="file"]), textarea');
inputElements.forEach(element => {
element.dataset.wasEnabled = element.disabled ? 'false' : 'true';
element.disabled = true;
@@ -356,20 +360,86 @@ export class EditModeManager {
controls.appendChild(settingsBtn);
controls.appendChild(deleteBtn);
element.appendChild(controls);
// Store reference to widget element for positioning
controls.dataset.widgetId = widgetId;
// Show controls on hover
// Append to overlay instead of widget to prevent overflow/scrollbar issues
if (this.editControlsOverlay) {
this.editControlsOverlay.appendChild(controls);
// Position controls to match widget bounds
this.updateControlPosition(controls, element);
} else {
// Fallback to old behavior if overlay not available
element.appendChild(controls);
}
// Show controls on hover - keep visible when hovering controls themselves
let isHoveringWidget = false;
let isHoveringControls = false;
let hideTimeout = null;
const checkAndHideControls = () => {
// Clear any existing timeout
if (hideTimeout) {
clearTimeout(hideTimeout);
}
// Add small delay to allow mouse to move between widget and controls
hideTimeout = setTimeout(() => {
if (!isHoveringWidget && !isHoveringControls) {
controls.style.opacity = '0';
}
}, 100);
};
// Widget hover
element.addEventListener('mouseenter', () => {
isHoveringWidget = true;
if (this.isEditMode) {
controls.style.opacity = '1';
}
});
element.addEventListener('mouseleave', () => {
controls.style.opacity = '0';
isHoveringWidget = false;
checkAndHideControls();
});
this.widgetControlsMap.set(widgetId, controls);
// Controls hover - keep visible when hovering the buttons
controls.addEventListener('mouseenter', () => {
isHoveringControls = true;
controls.style.opacity = '1';
});
controls.addEventListener('mouseleave', () => {
isHoveringControls = false;
checkAndHideControls();
});
this.widgetControlsMap.set(widgetId, { controls, element });
}
/**
* Update control position to match widget bounds
* @param {HTMLElement} controls - Edit controls container
* @param {HTMLElement} element - Widget element
*/
updateControlPosition(controls, element) {
if (!controls || !element) return;
const overlay = this.editControlsOverlay;
if (!overlay) return;
// Use offset properties for parent-relative positioning
// Both widget and overlay are children of the same grid container
const widgetLeft = element.offsetLeft;
const widgetTop = element.offsetTop;
const widgetWidth = element.offsetWidth;
// Position controls at top-right of widget (4px from top, 4px from right)
controls.style.left = `${widgetLeft + widgetWidth - 60}px`; // 60px approximate width of controls
controls.style.top = `${widgetTop + 4}px`;
controls.style.pointerEvents = 'auto'; // Ensure controls are clickable
}
/**
@@ -377,13 +447,58 @@ export class EditModeManager {
* @param {string} widgetId - Widget ID
*/
removeWidgetControls(widgetId) {
const controls = this.widgetControlsMap.get(widgetId);
if (controls) {
controls.remove();
const data = this.widgetControlsMap.get(widgetId);
if (data) {
if (data.controls) {
data.controls.remove();
}
this.widgetControlsMap.delete(widgetId);
}
}
/**
* Sync controls for all currently rendered widgets
* Adds controls to widgets that don't have them yet
*/
syncAllControls() {
// Find all widget elements in the grid
const gridContainer = this.container.querySelector('#rpg-dashboard-grid');
if (!gridContainer) return;
const widgets = gridContainer.querySelectorAll('.rpg-widget');
widgets.forEach(widgetElement => {
const widgetId = widgetElement.dataset.widgetId;
if (!widgetId) return;
// Add controls if they don't exist yet
if (!this.widgetControlsMap.has(widgetId)) {
this.addWidgetControls(widgetElement, widgetId);
} else {
// Update position if controls already exist
const data = this.widgetControlsMap.get(widgetId);
if (data && data.controls) {
this.updateControlPosition(data.controls, widgetElement);
}
}
});
console.log('[EditModeManager] Synced controls for', widgets.length, 'widgets');
}
/**
* Remove all widget controls
* Called when clearing the grid or switching tabs
*/
removeAllControls() {
this.widgetControlsMap.forEach((data, widgetId) => {
if (data.controls) {
data.controls.remove();
}
});
this.widgetControlsMap.clear();
console.log('[EditModeManager] Removed all widget controls');
}
/**
* Create a control button
* @param {string} icon - Button icon/text