From 62defcde1d54821023d6ddafba7c6a69e7e2f1f8 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 23 Oct 2025 10:16:46 +1100 Subject: [PATCH] fix(dashboard): prevent drag when clicking resize handles or controls - Add event target check in DragDropHandler to ignore resize handles - Add event target check to ignore widget edit controls - Use e.target.closest() to check parent elements - Add e.stopPropagation() in resize handle event handlers - Replace simplified ResizeHandler with fully functional version - Now resize handles work correctly without triggering drag - Both mouse and touch events properly handled - Fixes integration issue where resizing always triggered dragging --- src/systems/dashboard/dragDrop.js | 11 ++ .../dashboard/editMode.standalone.test.html | 181 ++++++++++++++++-- 2 files changed, 179 insertions(+), 13 deletions(-) diff --git a/src/systems/dashboard/dragDrop.js b/src/systems/dashboard/dragDrop.js index 961f50d..a17fed4 100644 --- a/src/systems/dashboard/dragDrop.js +++ b/src/systems/dashboard/dragDrop.js @@ -58,11 +58,22 @@ export class DragDropHandler { const mouseDownHandler = (e) => { if (e.button !== 0) return; // Only left mouse button + + // Don't drag if clicking on resize handle or widget controls + if (e.target.closest('.resize-handle') || e.target.closest('.widget-edit-controls')) { + return; + } + e.preventDefault(); this.startDrag(e, element, widget, onDragEnd); }; const touchStartHandler = (e) => { + // Don't drag if touching resize handle or widget controls + if (e.target.closest('.resize-handle') || e.target.closest('.widget-edit-controls')) { + return; + } + // Delay touch drag to allow scrolling this.touchTimer = setTimeout(() => { e.preventDefault(); diff --git a/src/systems/dashboard/editMode.standalone.test.html b/src/systems/dashboard/editMode.standalone.test.html index 5f5bf97..dc00b4d 100644 --- a/src/systems/dashboard/editMode.standalone.test.html +++ b/src/systems/dashboard/editMode.standalone.test.html @@ -352,10 +352,18 @@ const dragHandle = element; const mouseDownHandler = (e) => { if (e.button !== 0) return; + // Don't drag if clicking on resize handle or widget controls + if (e.target.closest('.resize-handle') || e.target.closest('.widget-edit-controls')) { + return; + } e.preventDefault(); this.startDrag(e, element, widget, onDragEnd); }; const touchStartHandler = (e) => { + // Don't drag if touching resize handle or widget controls + if (e.target.closest('.resize-handle') || e.target.closest('.widget-edit-controls')) { + return; + } this.touchTimer = setTimeout(() => { e.preventDefault(); this.startDrag(e.touches[0], element, widget, onDragEnd); @@ -484,25 +492,40 @@ } } - // ResizeHandler (simplified for demo) + // ResizeHandler (functional version) class ResizeHandler { - constructor(gridEngine) { + constructor(gridEngine, options = {}) { this.gridEngine = gridEngine; + this.options = { minWidth: 2, minHeight: 2, maxWidth: 12, maxHeight: 10, ...options }; this.resizeHandlers = new Map(); + this.resizeState = null; + this.boundMouseMove = this.onMouseMove.bind(this); + this.boundMouseUp = this.onMouseUp.bind(this); + this.boundTouchMove = this.onTouchMove.bind(this); + this.boundTouchEnd = this.onTouchEnd.bind(this); } - initWidget(element, widget, onResizeEnd) { - // Simplified - just create handles + initWidget(element, widget, onResizeEnd, constraints = {}) { const handles = document.createElement('div'); handles.className = 'resize-handles'; handles.style.position = 'absolute'; handles.style.inset = '0'; handles.style.pointerEvents = 'none'; - const handleTypes = ['nw', 'ne', 'se', 'sw']; - handleTypes.forEach(type => { + const widgetConstraints = { + minW: constraints.minW || this.options.minWidth, + minH: constraints.minH || this.options.minHeight, + maxW: constraints.maxW || this.options.maxWidth, + maxH: constraints.maxH || this.options.maxHeight + }; + + const handleTypes = { nw: 'nwse-resize', ne: 'nesw-resize', se: 'nwse-resize', sw: 'nesw-resize', n: 'ns-resize', s: 'ns-resize', e: 'ew-resize', w: 'ew-resize' }; + const handleListeners = []; + + Object.entries(handleTypes).forEach(([type, cursor]) => { const handle = document.createElement('div'); handle.className = `resize-handle resize-handle-${type}`; + handle.dataset.handle = type; handle.style.position = 'absolute'; handle.style.width = '12px'; handle.style.height = '12px'; @@ -510,24 +533,156 @@ handle.style.border = '2px solid white'; handle.style.borderRadius = '3px'; handle.style.pointerEvents = 'auto'; - handle.style.cursor = type + '-resize'; + handle.style.cursor = cursor; + handle.style.zIndex = '101'; + if (type.includes('n')) handle.style.top = '-6px'; if (type.includes('s')) handle.style.bottom = '-6px'; if (type.includes('w')) handle.style.left = '-6px'; if (type.includes('e')) handle.style.right = '-6px'; + if (type === 'n' || type === 's') { + handle.style.left = '50%'; + handle.style.transform = 'translateX(-50%)'; + } + if (type === 'w' || type === 'e') { + handle.style.top = '50%'; + handle.style.transform = 'translateY(-50%)'; + } + + const mouseDownHandler = (e) => { + if (e.button !== 0) return; + e.preventDefault(); + e.stopPropagation(); + this.startResize(e, type, element, widget, onResizeEnd, widgetConstraints); + }; + + const touchStartHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + this.startResize(e.touches[0], type, element, widget, onResizeEnd, widgetConstraints); + }; + + handle.addEventListener('mousedown', mouseDownHandler); + handle.addEventListener('touchstart', touchStartHandler, { passive: false }); + handleListeners.push({ element: handle, mouseDownHandler, touchStartHandler }); handles.appendChild(handle); }); element.appendChild(handles); - this.resizeHandlers.set(element, handles); + this.resizeHandlers.set(element, { handles, handleListeners }); + } + + startResize(e, handleType, element, widget, onResizeEnd, constraints) { + this.resizeState = { + element, + widget: { ...widget }, + handle: handleType, + startX: e.clientX, + startY: e.clientY, + startWidth: widget.w, + startHeight: widget.h, + startGridX: widget.x, + startGridY: widget.y, + onResizeEnd, + constraints + }; + + document.addEventListener('mousemove', this.boundMouseMove); + document.addEventListener('mouseup', this.boundMouseUp); + document.addEventListener('touchmove', this.boundTouchMove, { passive: false }); + document.addEventListener('touchend', this.boundTouchEnd); + element.classList.add('resizing'); + } + + onMouseMove(e) { + if (!this.resizeState) return; + e.preventDefault(); + this.updateResizeSize(e.clientX, e.clientY); + } + + onTouchMove(e) { + if (!this.resizeState) return; + e.preventDefault(); + this.updateResizeSize(e.touches[0].clientX, e.touches[0].clientY); + } + + updateResizeSize(clientX, clientY) { + const { widget, handle, startX, startY, startWidth, startHeight, startGridX, startGridY, constraints, element } = this.resizeState; + const deltaX = clientX - startX; + const deltaY = clientY - startY; + + this.gridEngine.updateContainerWidth(); + const totalGaps = this.gridEngine.gap * (this.gridEngine.columns + 1); + const colWidth = (this.gridEngine.containerWidth - totalGaps) / this.gridEngine.columns; + const rowHeight = this.gridEngine.rowHeight; + + const deltaGridX = Math.round(deltaX / (colWidth + this.gridEngine.gap)); + const deltaGridY = Math.round(deltaY / (rowHeight + this.gridEngine.gap)); + + let newW = startWidth, newH = startHeight, newX = startGridX, newY = startGridY; + + if (handle.includes('e')) newW = startWidth + deltaGridX; + else if (handle.includes('w')) { newW = startWidth - deltaGridX; newX = startGridX + deltaGridX; } + if (handle.includes('s')) newH = startHeight + deltaGridY; + else if (handle.includes('n')) { newH = startHeight - deltaGridY; newY = startGridY + deltaGridY; } + + newW = Math.max(constraints.minW, Math.min(newW, constraints.maxW)); + newH = Math.max(constraints.minH, Math.min(newH, constraints.maxH)); + newW = Math.min(newW, this.gridEngine.columns - newX); + + if (handle.includes('w') && newW === constraints.minW) newX = startGridX + startWidth - constraints.minW; + if (handle.includes('n') && newH === constraints.minH) newY = startGridY + startHeight - constraints.minH; + + this.resizeState.widget.w = newW; + this.resizeState.widget.h = newH; + this.resizeState.widget.x = newX; + this.resizeState.widget.y = newY; + + const pos = this.gridEngine.getPixelPosition(this.resizeState.widget); + element.style.width = pos.width + 'px'; + element.style.height = pos.height + 'px'; + element.style.left = pos.left + 'px'; + element.style.top = pos.top + 'px'; + } + + onMouseUp(e) { + if (!this.resizeState) return; + e.preventDefault(); + this.endResize(); + } + + onTouchEnd(e) { + if (!this.resizeState) return; + e.preventDefault(); + this.endResize(); + } + + endResize() { + if (!this.resizeState) return; + const { element, widget, onResizeEnd } = this.resizeState; + element.classList.remove('resizing'); + if (onResizeEnd) onResizeEnd(widget, widget.w, widget.h, widget.x, widget.y); + this.cleanup(); + } + + cleanup() { + document.removeEventListener('mousemove', this.boundMouseMove); + document.removeEventListener('mouseup', this.boundMouseUp); + document.removeEventListener('touchmove', this.boundTouchMove); + document.removeEventListener('touchend', this.boundTouchEnd); + this.resizeState = null; } destroyWidget(element) { - const handles = this.resizeHandlers.get(element); - if (handles) { - handles.remove(); - this.resizeHandlers.delete(element); - } + const data = this.resizeHandlers.get(element); + if (!data) return; + const { handles, handleListeners } = data; + handleListeners.forEach(({ element: h, mouseDownHandler, touchStartHandler }) => { + h.removeEventListener('mousedown', mouseDownHandler); + h.removeEventListener('touchstart', touchStartHandler); + }); + handles.remove(); + this.resizeHandlers.delete(element); } }