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:
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
========================================
|
========================================
|
||||||
|
|||||||
Reference in New Issue
Block a user