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
+321
View File
@@ -0,0 +1,321 @@
# Resize Handle Overlay Issue - Investigation Report
## Problem Summary
The resize handles in edit mode are being rendered **INSIDE the widget container DOM**, causing:
- Widgets to stretch and overflow their grid bounds
- Scrollbars to appear unexpectedly
- Edit/delete buttons to be hidden or inconsistently visible
- Layout overflow issues
The handles use negative positioning (`top: -6px`, `left: -3px`) to extend outside widget bounds, but being children of the widget element causes them to contribute to the widget's `offsetHeight` and `offsetWidth`, which creates unwanted scrollbars and overflow.
---
## Investigation Findings
### 1. Where Resize Handles Are Created and Appended
**File:** `src/systems/dashboard/resizeHandler.js`
**Key Code (Lines 172-215):**
```javascript
createResizeHandles() {
const container = document.createElement('div');
container.className = 'resize-handles';
container.style.position = 'absolute';
container.style.inset = '0';
container.style.pointerEvents = 'none';
// Create 8 handles (4 corners + 4 edges)
Object.entries(this.handleTypes).forEach(([handleType, cursor]) => {
const handle = document.createElement('div');
handle.className = `resize-handle resize-handle-${handleType}`;
// ... positioning ...
handle.style.top = '-6px'; // Negative positioning
handle.style.left = '-3px'; // Negative positioning
handle.style.zIndex = '100';
container.appendChild(handle);
});
return container;
}
```
**Appended At (Line 77):**
```javascript
initWidget(element, widget, onResizeEnd, constraints = {}) {
const handles = this.createResizeHandles();
element.appendChild(handles); // <-- APPENDED INSIDE WIDGET
// ...
}
```
**Problem:** The handles container is appended directly to the widget element (`element.appendChild(handles)`), making it a child of `.rpg-widget`.
---
### 2. Where Edit/Delete Buttons Are Created
**File:** `src/systems/dashboard/editModeManager.js`
**Key Code (Lines 325-373):**
```javascript
addWidgetControls(element, widgetId) {
const controls = document.createElement('div');
controls.className = 'widget-edit-controls';
controls.style.position = 'absolute';
controls.style.top = '4px';
controls.style.right = '4px';
controls.style.display = 'flex';
controls.style.gap = '4px';
controls.style.zIndex = '100';
controls.style.opacity = '0';
controls.style.transition = 'opacity 0.2s';
// Create settings and delete buttons
const settingsBtn = this.createControlButton('⚙', 'Settings');
const deleteBtn = this.createControlButton('×', 'Delete');
controls.appendChild(settingsBtn);
controls.appendChild(deleteBtn);
element.appendChild(controls); // <-- APPENDED INSIDE WIDGET
// ...
}
```
**Problem:** Like the resize handles, the edit controls are appended inside the widget element as a child.
---
### 3. Current DOM Structure
```
<div class="rpg-widget" id="widget-widget-usermood">
<!-- Widget content (rendered by widget definition) -->
<div class="widget-content">...</div>
<!-- Resize handles INSIDE widget (PROBLEM) -->
<div class="resize-handles" style="position: absolute; inset: 0; pointer-events: none;">
<div class="resize-handle resize-handle-nw" style="top: -6px; left: -3px; ..."></div>
<!-- 7 more handles... -->
</div>
<!-- Edit controls INSIDE widget (PROBLEM) -->
<div class="widget-edit-controls" style="position: absolute; top: 4px; right: 4px; ...">
<button>⚙</button>
<button>×</button>
</div>
</div>
```
**Why This Causes Issues:**
- Even though handles have `position: absolute`, they're still part of the DOM flow calculation
- Negative positioning extends them outside the widget visually, but the browser still includes them in overflow calculations
- This causes scrollbars when the widget container has `overflow: auto` or `overflow: scroll`
- The controls at `top: 4px; right: 4px` with `z-index: 100` can be covered or hidden by other elements
---
### 4. CSS Widget Styling
**File:** `style.css`
**Key Widget CSS:**
```css
.rpg-widget {
box-sizing: border-box;
overflow: visible; /* Allow resize handles to extend beyond widget bounds */
display: flex;
flex-direction: column;
max-height: 100%; /* Prevent content from overflowing grid cell */
/* ... other styles ... */
}
/* Hide resize handles by default */
.resize-handles {
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
/* Show resize handles in edit mode */
.edit-mode .resize-handles {
opacity: 1;
pointer-events: auto;
}
/* Hide resize handles when widgets are locked */
.widgets-locked .resize-handles {
opacity: 0 !important;
pointer-events: none !important;
}
```
**Current State:**
- Widget has `overflow: visible` - correct for allowing handles to show
- But the negative positioning of handles inside the widget still causes layout issues
- The `max-height: 100%` on flex column can cause scrollbars if child heights exceed parent
---
### 5. Why Buttons Are Inconsistently Visible
The edit/delete buttons are positioned inside the widget at `top: 4px; right: 4px;` with `z-index: 100`. Issues arise:
1. **Scrollbars Overlap:** If the widget develops a scrollbar, the buttons are positioned relative to the widget's content box, not the visible area, so they can be hidden by the scrollbar.
2. **Parent Stacking Context:** The widget element's positioning and z-index hierarchy may cause the buttons to be layered differently depending on scroll state.
3. **Hover State Lost:** When scrollbars appear, the widget's visual bounds change, and hover detection may fail to show/hide buttons consistently.
4. **Absolute Positioning Within Scrollable Parent:** Buttons positioned absolutely within a widget that can scroll create unpredictable rendering.
---
## Recommended Approach: Make Handles & Buttons True Overlays
### Strategy
**Move resize handles and edit controls outside the widget DOM to a shared overlay container at the dashboard/grid level.**
**Current (Problematic) Structure:**
```
<div class="rpg-widget">
<widget-content/>
<resize-handles/>
<edit-controls/>
</div>
```
**Target (Fixed) Structure:**
```
<div id="rpg-dashboard-grid">
<div class="rpg-widget">
<widget-content/>
</div>
<div id="rpg-widget-overlays">
<resize-handles for="widget-1"/>
<resize-handles for="widget-2"/>
<edit-controls for="widget-1"/>
<edit-controls for="widget-2"/>
</div>
</div>
```
### Benefits
1. **No DOM Overflow:** Handles and controls are outside widgets, don't contribute to widget dimensions
2. **Clean Widget DOM:** Widgets only contain their actual content
3. **Consistent Visibility:** Overlays are positioned relative to grid container, not affected by widget scrolling
4. **Proper Z-stacking:** True layers with proper z-index control
5. **Easier Positioning:** Overlay containers can be precisely positioned relative to grid, and handles/controls positioned relative to overlay
6. **No Scrollbar Interference:** Buttons and handles won't be hidden by scrollbars
### Implementation Plan
1. **Create overlay container management in DashboardManager:**
- Create and maintain `#rpg-resize-handles-overlay` container
- Create and maintain `#rpg-edit-controls-overlay` container
- Both positioned absolutely, covering entire grid, `pointer-events: none` by default
2. **Modify ResizeHandler:**
- Change `initWidget()` to NOT append handles to widget element
- Instead, create handles and append to `#rpg-resize-handles-overlay`
- Position handles using absolute positioning relative to overlay container
- Calculate positions based on widget's grid position + negative offsets
3. **Modify EditModeManager:**
- Change `addWidgetControls()` to NOT append controls to widget element
- Instead, create controls and append to `#rpg-edit-controls-overlay`
- Position controls using absolute positioning relative to overlay container
- Calculate positions based on widget's grid position
4. **Update repositioning logic:**
- When widgets are repositioned (drag/resize), update overlay child positions
- On tab switch, show/hide overlay child elements for that tab's widgets
- On widget removal, remove corresponding overlay children
5. **CSS updates:**
- Add styles for overlay containers
- Add positioning rules for handles and controls within overlays
---
## Key Files Needing Changes
| File | Change | Impact |
|------|--------|--------|
| `src/systems/dashboard/resizeHandler.js` | Don't append handles to widget; append to overlay instead | Prevents widget overflow |
| `src/systems/dashboard/editModeManager.js` | Don't append controls to widget; append to overlay instead | Fixes button visibility |
| `src/systems/dashboard/dashboardManager.js` | Create/maintain overlay containers; manage overlay children on reposition | Coordinates layout |
| `style.css` | Add overlay container styles; update handle/control positioning | Visual presentation |
---
## CSS Overflow Issue Analysis
**Current `.rpg-widget` CSS:**
```css
.rpg-widget {
overflow: visible; /* Good - allows content overflow */
max-height: 100%; /* Can cause scrollbars if flex children exceed 100% */
display: flex;
flex-direction: column;
}
```
**Why Scrollbars Appear:**
1. Widget has `display: flex; flex-direction: column`
2. Widget has `max-height: 100%`
3. If flex children (content + handles + controls) exceed max-height, scrollbars appear
4. The `overflow: visible` doesn't prevent scrollbars - `max-height` triggers them
**Solution:**
- Moving handles/controls outside widget DOM solves the flex child height problem
- Keep `overflow: visible` for clean content overflow
- Remove or adjust `max-height` constraint
---
## Event Handler Interaction Points
### Resize Handler
- **Source:** `resizeHandler.js`, line 77 in `initWidget()`
- **Current:** Appends handles to widget element
- **Change:** Accept overlay container reference, append there instead
### Drag/Drop Handler
- **Source:** `dragDrop.js`, line 76 checks for `.resize-handle` with `closest()`
- **Impact:** Still works (CSS class-based detection)
- **Change:** None needed - will still detect overlaid handles
### Edit Mode Manager
- **Source:** `editModeManager.js`, lines 325-373 in `addWidgetControls()`
- **Current:** Appends controls to widget element
- **Change:** Accept overlay container reference, append there instead
### Dashboard Manager
- **Source:** `dashboardManager.js`, lines 631-703 in `renderWidget()`
- **Current:** Calls `resizeHandler.initWidget()` and `editManager.addWidgetControls()` with widget element
- **Change:** Pass overlay containers, handle repositioning on layout changes
---
## Summary
The resize handles are being rendered **inside the widget container**, causing them to:
1. Contribute to widget dimensions via negative positioning tricks
2. Trigger scrollbars when combined with flex layout and `max-height`
3. Cause edit/delete buttons to be hidden or inaccessible
4. Create inconsistent UI behavior
**Solution:** Create true overlay containers at the grid level, position handles and controls outside the widget DOM, and coordinate their positioning through the DashboardManager lifecycle.
This approach is used in many professional UI frameworks and provides:
- Clean separation of concerns
- Better visual control
- Elimination of overflow/scrollbar issues
- Consistent button visibility
- Proper z-index layering
+68 -11
View File
@@ -306,14 +306,21 @@ function setupDashboardEventListeners(dependencies) {
});
}
// Add widget button
// Add widget button - supports both desktop click and mobile touch
const addWidgetBtn = document.querySelector('#rpg-dashboard-add-widget');
if (addWidgetBtn) {
addWidgetBtn.addEventListener('click', () => {
// Use pointerdown for universal desktop/mobile support
const openAddWidget = (e) => {
e.preventDefault();
e.stopPropagation();
if (dashboardManager) {
showAddWidgetDialog(dashboardManager);
}
});
};
// Listen to both click (desktop) and pointerdown (mobile) for maximum compatibility
addWidgetBtn.addEventListener('click', openAddWidget);
addWidgetBtn.addEventListener('pointerdown', openAddWidget, { once: true });
}
// Export layout button
@@ -326,22 +333,51 @@ function setupDashboardEventListeners(dependencies) {
});
}
// Import layout button
// Import layout button - trigger file input on click
const importBtn = document.querySelector('#rpg-dashboard-import-layout');
const importFile = document.querySelector('#rpg-dashboard-import-file');
if (importBtn && importFile) {
importBtn.addEventListener('click', () => {
importFile.click();
console.log('[RPG Companion] Import button and file input initialized');
// Trigger file picker on button click
importBtn.addEventListener('click', (e) => {
console.log('[RPG Companion] Import button clicked, triggering file picker');
console.log('[RPG Companion] File input element:', importFile);
console.log('[RPG Companion] File input visible:', importFile.offsetParent !== null);
try {
// Direct click works on desktop and mobile when input is properly positioned
importFile.click();
console.log('[RPG Companion] File input click() called successfully');
} catch (err) {
console.error('[RPG Companion] Error triggering file input:', err);
}
});
// Handle file selection
importFile.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file && dashboardManager) {
dashboardManager.importLayout(file);
console.log('[RPG Companion] File input change event fired');
console.log('[RPG Companion] Selected file:', file);
if (file) {
if (dashboardManager) {
console.log('[RPG Companion] Importing layout from:', file.name);
dashboardManager.importLayout(file);
} else {
console.error('[RPG Companion] Dashboard manager not available');
}
importFile.value = ''; // Reset file input
} else {
console.warn('[RPG Companion] No file selected');
}
});
} else {
console.error('[RPG Companion] Import button or file input not found!', {
importBtn,
importFile
});
}
}
@@ -354,7 +390,8 @@ function showAddWidgetDialog(manager) {
const widgets = registry.getAll();
// Create widget cards HTML
const widgetCardsHtml = widgets.map(([type, definition]) => `
// Note: registry.getAll() returns [{type, definition}, ...] not [[type, definition], ...]
const widgetCardsHtml = widgets.map(({type, definition}) => `
<div class="rpg-widget-card" data-widget-type="${type}">
<div class="rpg-widget-card-icon">${definition.icon}</div>
<div class="rpg-widget-card-name">${definition.name}</div>
@@ -372,6 +409,22 @@ function showAddWidgetDialog(manager) {
return;
}
// CRITICAL: Move modal to document.body on first use to escape panel constraints
// The panel has transform in its transition which creates a containing block,
// constraining position:fixed children to the panel instead of viewport
if (modal.parentElement?.id !== 'document-body-modals') {
// Create container for modals at body level (only once)
let bodyModalsContainer = document.getElementById('document-body-modals');
if (!bodyModalsContainer) {
bodyModalsContainer = document.createElement('div');
bodyModalsContainer.id = 'document-body-modals';
bodyModalsContainer.style.cssText = 'position: fixed; inset: 0; pointer-events: none; z-index: 10000; display: flex; align-items: center; justify-content: center;';
document.body.appendChild(bodyModalsContainer);
}
bodyModalsContainer.appendChild(modal);
console.log('[RPG Companion] Moved Add Widget modal to document.body for proper viewport positioning');
}
const widgetSelector = modal.querySelector('#rpg-widget-selector');
if (widgetSelector) {
widgetSelector.innerHTML = widgetCardsHtml;
@@ -380,7 +433,8 @@ function showAddWidgetDialog(manager) {
widgetSelector.querySelectorAll('.rpg-widget-card-add').forEach(btn => {
btn.addEventListener('click', () => {
const widgetType = btn.dataset.widgetType;
const activeTab = manager.tabManager.getActiveTabId();
// Use activeTabId property instead of getActiveTabId() method
const activeTab = manager.tabManager.activeTabId;
manager.addWidget(widgetType, activeTab);
hideModal('rpg-add-widget-modal');
@@ -388,7 +442,9 @@ function showAddWidgetDialog(manager) {
});
}
// Show modal with proper pointer events (parent has pointer-events: none)
modal.style.display = 'flex';
modal.style.pointerEvents = 'auto';
// Set up modal close handlers
modal.querySelectorAll('[data-close="add-widget"]').forEach(btn => {
@@ -424,7 +480,8 @@ export function createDefaultLayout(manager) {
console.log('[RPG Companion] Creating default dashboard layout with modular widgets...');
const mainTab = manager.tabManager.getActiveTabId();
// Use activeTabId property instead of getActiveTabId() method
const mainTab = manager.tabManager.activeTabId;
// Add modular user widgets
// Row 0: User Info (avatar, name, level) - full width
+61 -6
View File
@@ -20,6 +20,7 @@ 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
@@ -86,6 +87,8 @@ export class DashboardManager {
// Container elements
this.gridContainer = null;
this.tabContainer = null;
this.resizeHandlesOverlay = null;
this.editControlsOverlay = null;
this.changeListeners = new Set();
@@ -159,6 +162,7 @@ export class DashboardManager {
// 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),
@@ -174,13 +178,14 @@ export class DashboardManager {
dashboardManager: this
});
// Initialize Resize Handler (with editManager reference)
// 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
editManager: this.editManager,
resizeHandlesOverlay: this.resizeHandlesOverlay
});
// Initialize Layout Persistence
@@ -240,7 +245,21 @@ export class DashboardManager {
this.container.appendChild(this.gridContainer);
}
console.log('[DashboardManager] Container structure ready');
// 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)');
}
/**
@@ -735,8 +754,8 @@ export class DashboardManager {
el.contentEditable = 'false';
});
// Also disable input fields
const inputElements = element.querySelectorAll('input, textarea');
// Also disable input fields (except file inputs which should remain functional)
const inputElements = element.querySelectorAll('input:not([type="file"]), textarea');
inputElements.forEach(el => {
el.dataset.wasEnabled = el.disabled ? 'false' : 'true';
el.disabled = true;
@@ -755,6 +774,32 @@ export class DashboardManager {
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);
}
}
}
/**
@@ -1098,6 +1143,11 @@ export class DashboardManager {
* 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);
@@ -1351,8 +1401,13 @@ export class DashboardManager {
* 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();
if (!this.defaultLayout) {
console.warn('[DashboardManager] No default layout defined');
console.warn('[DashboardManager] Failed to generate default layout');
return;
}
+3 -1
View File
@@ -64,7 +64,9 @@
<!-- Menu items added dynamically -->
</div>
<input type="file" id="rpg-dashboard-import-file" accept=".json" style="display: none;" />
<!-- File input: visually hidden but accessible for mobile compatibility -->
<!-- Use 1px size for better browser compatibility while keeping hidden -->
<input type="file" id="rpg-dashboard-import-file" accept=".json" style="position: absolute; width: 1px; height: 1px; opacity: 0; overflow: hidden; z-index: -1; pointer-events: auto;" />
</div>
</div>
+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
+45 -1
View File
@@ -28,6 +28,7 @@ export class ResizeHandler {
constructor(gridEngine, options = {}) {
this.gridEngine = gridEngine;
this.editManager = options.editManager || null; // Reference to EditModeManager for lock state
this.resizeHandlesOverlay = options.resizeHandlesOverlay || null; // Overlay container for handles
this.options = {
showDimensions: true,
showGrid: true,
@@ -74,7 +75,19 @@ export class ResizeHandler {
initWidget(element, widget, onResizeEnd, constraints = {}) {
// Create resize handles
const handles = this.createResizeHandles();
element.appendChild(handles);
// Store reference to widget element for positioning
handles.dataset.widgetId = element.id;
// Append to overlay instead of widget to prevent overflow/scrollbar issues
if (this.resizeHandlesOverlay) {
this.resizeHandlesOverlay.appendChild(handles);
// Position handles to match widget bounds
this.updateHandlePosition(handles, element);
} else {
// Fallback to old behavior if overlay not available
element.appendChild(handles);
}
// Store constraints
const widgetConstraints = {
@@ -215,6 +228,25 @@ export class ResizeHandler {
return container;
}
/**
* Update handle container position to match widget bounds
* @param {HTMLElement} handles - Resize handles container
* @param {HTMLElement} element - Widget element
*/
updateHandlePosition(handles, element) {
if (!handles || !element) return;
const overlay = this.resizeHandlesOverlay;
if (!overlay) return;
// Use offset properties for parent-relative positioning
// Both widget and overlay are children of the same grid container
handles.style.left = `${element.offsetLeft}px`;
handles.style.top = `${element.offsetTop}px`;
handles.style.width = `${element.offsetWidth}px`;
handles.style.height = `${element.offsetHeight}px`;
}
/**
* Start resize operation
* @param {MouseEvent|Touch} e - Pointer event
@@ -416,6 +448,12 @@ export class ResizeHandler {
onResizeEnd(widget, widget.w, widget.h, widget.x, widget.y);
}
// Update handle positions to match new widget size
const handlerData = this.resizeHandlers.get(element);
if (handlerData && handlerData.handles) {
this.updateHandlePosition(handlerData.handles, element);
}
this.cleanup();
console.log('[ResizeHandler] Resize completed:', widget.id, `${widget.w}×${widget.h} at (${widget.x}, ${widget.y})`);
}
@@ -445,6 +483,12 @@ export class ResizeHandler {
// Remove resizing class
element.classList.remove('resizing');
// Update handle positions to match restored widget size
const handlerData = this.resizeHandlers.get(element);
if (handlerData && handlerData.handles) {
this.updateHandlePosition(handlerData.handles, element);
}
this.cleanup();
console.log('[ResizeHandler] Resize cancelled');
}
+29
View File
@@ -1748,6 +1748,35 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
padding: 0.75rem 1rem;
font-size: 1.05rem; /* Touch-friendly size for mobile readability */
}
/* Add Widget dialog mobile optimizations */
.rpg-widget-grid {
grid-template-columns: 1fr; /* Single column on mobile for better readability */
gap: 0.75rem; /* Slightly tighter spacing */
}
.rpg-widget-card {
padding: 0.875rem; /* Slightly less padding on mobile */
}
.rpg-widget-card-icon {
font-size: 2rem; /* Scale down icon for mobile */
}
.rpg-widget-card-name {
font-size: 0.9rem; /* Slightly smaller name */
}
.rpg-widget-card-description {
font-size: 0.7rem; /* Compact description */
line-height: 1.25;
}
.rpg-widget-card-add {
min-height: 44px; /* Touch-friendly button size */
padding: 0.75rem 1rem;
font-size: 0.95rem;
}
}
.rpg-dashboard-grid {