feat(dashboard): add lock button to prevent accidental widget movement

- Add lock/unlock button to dashboard header (always visible)
- Lock state prevents dragging in both normal and edit modes
- Lock state prevents resizing in edit mode
- Icon changes: lock-open (unlocked) ↔ lock (locked)
- Hide resize handles and prevent grab cursor when locked
- Lock state persists across edit mode toggles
- Integrate lock checks in DragDropHandler and ResizeHandler
- Pass editManager reference to drag/resize handlers for lock state access
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-25 19:50:17 +11:00
parent d6c5101a7e
commit f84cbf794a
7 changed files with 112 additions and 66 deletions
@@ -238,6 +238,17 @@ function setupDashboardEventListeners(dependencies) {
}); });
} }
// Lock/unlock widgets button
const lockWidgetsBtn = document.querySelector('#rpg-dashboard-lock-widgets');
if (lockWidgetsBtn) {
lockWidgetsBtn.addEventListener('click', () => {
if (dashboardManager && dashboardManager.editManager) {
console.log('[RPG Companion] Lock button clicked');
dashboardManager.editManager.toggleLock();
}
});
}
// Add widget button // Add widget button
const addWidgetBtn = document.querySelector('#rpg-dashboard-add-widget'); const addWidgetBtn = document.querySelector('#rpg-dashboard-add-widget');
if (addWidgetBtn) { if (addWidgetBtn) {
+17 -15
View File
@@ -156,21 +156,7 @@ export class DashboardManager {
} }
}); });
// Initialize Drag & Drop // Initialize Edit Mode Manager first (needed by drag/resize handlers)
this.dragHandler = new DragDropHandler(this.gridEngine, {
showGrid: true,
enableSnap: true
});
// Initialize Resize Handler
this.resizeHandler = new ResizeHandler(this.gridEngine, {
minWidth: 1,
minHeight: 2,
maxWidth: 4, // Max 4 columns (will be clamped to actual column count)
maxHeight: 10
});
// Initialize Edit Mode Manager
this.editManager = new EditModeManager({ this.editManager = new EditModeManager({
container: this.container, container: this.container,
onSave: () => this.handleEditSave(), onSave: () => this.handleEditSave(),
@@ -180,6 +166,22 @@ export class DashboardManager {
onWidgetSettings: (widgetId) => this.openWidgetSettings(widgetId) onWidgetSettings: (widgetId) => this.openWidgetSettings(widgetId)
}); });
// Initialize Drag & Drop (with editManager reference)
this.dragHandler = new DragDropHandler(this.gridEngine, {
showGrid: true,
enableSnap: true,
editManager: this.editManager
});
// Initialize Resize Handler (with editManager reference)
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
});
// Initialize Layout Persistence // Initialize Layout Persistence
this.persistence = new LayoutPersistence({ this.persistence = new LayoutPersistence({
debounceMs: this.config.debounceMs, debounceMs: this.config.debounceMs,
@@ -18,6 +18,11 @@
<i class="fa-solid fa-table-cells-large"></i> <i class="fa-solid fa-table-cells-large"></i>
</button> </button>
<!-- Lock/Unlock Button (always visible) -->
<button id="rpg-dashboard-lock-widgets" class="rpg-dashboard-btn rpg-lock-widgets-btn" title="Lock Widgets">
<i class="fa-solid fa-lock-open"></i>
</button>
<!-- Edit Mode Toggle --> <!-- Edit Mode Toggle -->
<button id="rpg-dashboard-edit-mode" class="rpg-dashboard-btn rpg-edit-mode-btn" title="Toggle Edit Mode"> <button id="rpg-dashboard-edit-mode" class="rpg-dashboard-btn rpg-edit-mode-btn" title="Toggle Edit Mode">
<i class="fa-solid fa-pen-to-square"></i> <i class="fa-solid fa-pen-to-square"></i>
+15 -2
View File
@@ -24,6 +24,7 @@ export class DragDropHandler {
*/ */
constructor(gridEngine, options = {}) { constructor(gridEngine, options = {}) {
this.gridEngine = gridEngine; this.gridEngine = gridEngine;
this.editManager = options.editManager || null; // Reference to EditModeManager for lock state
this.options = { this.options = {
showGrid: true, showGrid: true,
showCollisions: true, showCollisions: true,
@@ -64,6 +65,11 @@ export class DragDropHandler {
const mouseDownHandler = (e) => { const mouseDownHandler = (e) => {
if (e.button !== 0) return; // Only left mouse button if (e.button !== 0) return; // Only left mouse button
// Don't drag if widgets are locked
if (this.editManager?.isWidgetsLocked()) {
return;
}
// Don't drag if clicking on resize handle or widget controls // Don't drag if clicking on resize handle or widget controls
if (e.target.closest('.resize-handle') || e.target.closest('.widget-edit-controls')) { if (e.target.closest('.resize-handle') || e.target.closest('.widget-edit-controls')) {
return; return;
@@ -92,6 +98,11 @@ export class DragDropHandler {
}; };
const touchStartHandler = (e) => { const touchStartHandler = (e) => {
// Don't drag if widgets are locked
if (this.editManager?.isWidgetsLocked()) {
return;
}
// Don't drag if touching resize handle or widget controls // Don't drag if touching resize handle or widget controls
if (e.target.closest('.resize-handle') || e.target.closest('.widget-edit-controls')) { if (e.target.closest('.resize-handle') || e.target.closest('.widget-edit-controls')) {
return; return;
@@ -130,8 +141,10 @@ export class DragDropHandler {
dragHandle dragHandle
}); });
// Add draggable cursor // Add draggable cursor (unless locked)
dragHandle.style.cursor = 'grab'; if (!this.editManager?.isWidgetsLocked()) {
dragHandle.style.cursor = 'grab';
}
} }
/** /**
+44 -49
View File
@@ -28,8 +28,8 @@ export class EditModeManager {
this.onWidgetSettings = config.onWidgetSettings; this.onWidgetSettings = config.onWidgetSettings;
this.isEditMode = false; this.isEditMode = false;
this.isLocked = false; // Lock state prevents dragging/resizing
this.originalLayout = null; this.originalLayout = null;
this.editControls = null;
this.gridOverlay = null; this.gridOverlay = null;
this.widgetLibrary = null; this.widgetLibrary = null;
this.widgetControlsMap = new Map(); this.widgetControlsMap = new Map();
@@ -48,14 +48,14 @@ export class EditModeManager {
// Store original layout for cancel // Store original layout for cancel
this.originalLayout = this.captureLayout(); this.originalLayout = this.captureLayout();
// Create edit controls // Show edit mode buttons (lock button is always visible)
this.createEditControls(); const addWidgetBtn = document.querySelector('#rpg-dashboard-add-widget');
const exportBtn = document.querySelector('#rpg-dashboard-export-layout');
const importBtn = document.querySelector('#rpg-dashboard-import-layout');
// Show grid overlay if (addWidgetBtn) addWidgetBtn.style.display = '';
this.showGridOverlay(); if (exportBtn) exportBtn.style.display = '';
if (importBtn) importBtn.style.display = '';
// Show widget library
this.showWidgetLibrary();
// Add edit class to container // Add edit class to container
this.container.classList.add('edit-mode'); this.container.classList.add('edit-mode');
@@ -88,14 +88,14 @@ export class EditModeManager {
this.isEditMode = false; this.isEditMode = false;
this.originalLayout = null; this.originalLayout = null;
// Remove edit controls // Hide edit mode buttons (lock button stays visible)
this.removeEditControls(); const addWidgetBtn = document.querySelector('#rpg-dashboard-add-widget');
const exportBtn = document.querySelector('#rpg-dashboard-export-layout');
const importBtn = document.querySelector('#rpg-dashboard-import-layout');
// Hide grid overlay if (addWidgetBtn) addWidgetBtn.style.display = 'none';
this.hideGridOverlay(); if (exportBtn) exportBtn.style.display = 'none';
if (importBtn) importBtn.style.display = 'none';
// Hide widget library
this.hideWidgetLibrary();
// Remove edit class from container // Remove edit class from container
this.container.classList.remove('edit-mode'); this.container.classList.remove('edit-mode');
@@ -115,50 +115,45 @@ export class EditModeManager {
} }
/** /**
* Create edit control buttons * Toggle lock state
*/ */
createEditControls() { toggleLock() {
if (this.editControls) return; this.isLocked = !this.isLocked;
this.editControls = document.createElement('div'); // Update button appearance
this.editControls.className = 'edit-controls'; const lockBtn = document.querySelector('#rpg-dashboard-lock-widgets');
this.editControls.style.position = 'absolute'; if (lockBtn) {
this.editControls.style.top = '10px'; const icon = lockBtn.querySelector('i');
this.editControls.style.right = '10px'; if (this.isLocked) {
this.editControls.style.display = 'flex'; icon.className = 'fa-solid fa-lock';
this.editControls.style.gap = '8px'; lockBtn.title = 'Unlock Widgets';
this.editControls.style.zIndex = '10000'; } else {
icon.className = 'fa-solid fa-lock-open';
lockBtn.title = 'Lock Widgets';
}
}
// Save button // Add/remove locked class to container for CSS styling
const saveBtn = document.createElement('button'); if (this.isLocked) {
saveBtn.className = 'edit-btn edit-btn-save'; this.container.classList.add('widgets-locked');
saveBtn.textContent = '💾 Save'; } else {
saveBtn.onclick = () => this.exitEditMode(true); this.container.classList.remove('widgets-locked');
this.styleButton(saveBtn, '#4ecca3', '#1a1a2e'); }
// Cancel button // Notify listeners
const cancelBtn = document.createElement('button'); this.notifyChange('lockStateChanged', { locked: this.isLocked });
cancelBtn.className = 'edit-btn edit-btn-cancel'; console.log('[EditModeManager] Lock state:', this.isLocked ? 'LOCKED' : 'UNLOCKED');
cancelBtn.textContent = '✖ Cancel';
cancelBtn.onclick = () => this.confirmCancel(() => this.exitEditMode(false));
this.styleButton(cancelBtn, '#e94560', 'white');
this.editControls.appendChild(saveBtn);
this.editControls.appendChild(cancelBtn);
this.container.appendChild(this.editControls);
} }
/** /**
* Remove edit control buttons * Check if widgets are currently locked
* @returns {boolean} True if locked
*/ */
removeEditControls() { isWidgetsLocked() {
if (this.editControls) { return this.isLocked;
this.editControls.remove();
this.editControls = null;
}
} }
/** /**
* Show grid overlay (now handled via CSS on container) * Show grid overlay (now handled via CSS on container)
*/ */
+9
View File
@@ -27,6 +27,7 @@ export class ResizeHandler {
*/ */
constructor(gridEngine, options = {}) { constructor(gridEngine, options = {}) {
this.gridEngine = gridEngine; this.gridEngine = gridEngine;
this.editManager = options.editManager || null; // Reference to EditModeManager for lock state
this.options = { this.options = {
showDimensions: true, showDimensions: true,
showGrid: true, showGrid: true,
@@ -92,12 +93,20 @@ export class ResizeHandler {
const mouseDownHandler = (e) => { const mouseDownHandler = (e) => {
if (e.button !== 0) return; if (e.button !== 0) return;
// Don't resize if widgets are locked
if (this.editManager?.isWidgetsLocked()) {
return;
}
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.startResize(e, handleType, element, widget, onResizeEnd, widgetConstraints); this.startResize(e, handleType, element, widget, onResizeEnd, widgetConstraints);
}; };
const touchStartHandler = (e) => { const touchStartHandler = (e) => {
// Don't resize if widgets are locked
if (this.editManager?.isWidgetsLocked()) {
return;
}
this.touchTimer = setTimeout(() => { this.touchTimer = setTimeout(() => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
+11
View File
@@ -1172,6 +1172,17 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
pointer-events: auto; pointer-events: auto;
} }
/* Hide resize handles when widgets are locked */
.widgets-locked .resize-handles {
opacity: 0 !important;
pointer-events: none !important;
}
/* Prevent grab cursor when widgets are locked */
.widgets-locked .rpg-widget {
cursor: default !important;
}
/* ======================================== /* ========================================
DASHBOARD V2 WIDGET STYLES DASHBOARD V2 WIDGET STYLES
======================================== ========================================