feat(dashboard): implement movement threshold for widget dragging to allow clicks

- Add 5px mouse movement threshold before drag starts
- Prevent dragging when clicking interactive elements (contenteditable, input, button, etc.)
- Store pending drag state and wait for movement before preventing default behavior
- Allow normal click/edit interactions on widget content
- Touch behavior unchanged (existing 150ms delay still works)
- Fixes issue where contenteditable fields and buttons were not clickable
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-25 19:32:29 +11:00
parent 101404d617
commit d6c5101a7e
+72 -2
View File
@@ -30,6 +30,7 @@ export class DragDropHandler {
enableSnap: true,
ghostOpacity: 0.5,
touchDelay: 150, // Delay before touch drag starts (ms)
mouseMoveThreshold: 5, // Pixels mouse must move before drag starts
...options
};
@@ -37,6 +38,7 @@ export class DragDropHandler {
this.dragHandlers = new Map();
this.gridOverlay = null;
this.touchTimer = null;
this.mouseDragPending = null; // Tracks potential mouse drag before threshold
// Bound event handlers for cleanup
this.boundMouseMove = this.onMouseMove.bind(this);
@@ -44,6 +46,8 @@ export class DragDropHandler {
this.boundTouchMove = this.onTouchMove.bind(this);
this.boundTouchEnd = this.onTouchEnd.bind(this);
this.boundKeyDown = this.onKeyDown.bind(this);
this.boundPendingMouseMove = this.onPendingMouseMove.bind(this);
this.boundPendingMouseUp = this.onPendingMouseUp.bind(this);
}
/**
@@ -65,8 +69,26 @@ export class DragDropHandler {
return;
}
e.preventDefault();
this.startDrag(e, element, widget, onDragEnd, widgets);
// Don't drag if clicking on interactive elements
const interactiveElements = 'input, button, select, textarea, a, [contenteditable="true"]';
if (e.target.closest(interactiveElements)) {
return;
}
// Store pending drag info - wait for movement threshold before starting drag
this.mouseDragPending = {
startX: e.clientX,
startY: e.clientY,
element,
widget,
onDragEnd,
widgets,
event: e
};
// Add temporary listeners to detect movement or mouseup
document.addEventListener('mousemove', this.boundPendingMouseMove);
document.addEventListener('mouseup', this.boundPendingMouseUp);
};
const touchStartHandler = (e) => {
@@ -75,6 +97,12 @@ export class DragDropHandler {
return;
}
// Don't drag if touching interactive elements
const interactiveElements = 'input, button, select, textarea, a, [contenteditable="true"]';
if (e.target.closest(interactiveElements)) {
return;
}
// Delay touch drag to allow scrolling
this.touchTimer = setTimeout(() => {
e.preventDefault();
@@ -199,6 +227,43 @@ export class DragDropHandler {
this.updateDragPosition(touch.clientX, touch.clientY);
}
/**
* Handle mouse move before drag threshold is reached
* @param {MouseEvent} e - Mouse event
*/
onPendingMouseMove(e) {
if (!this.mouseDragPending) return;
const { startX, startY, element, widget, onDragEnd, widgets } = this.mouseDragPending;
const deltaX = Math.abs(e.clientX - startX);
const deltaY = Math.abs(e.clientY - startY);
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// Check if movement threshold exceeded
if (distance >= this.options.mouseMoveThreshold) {
// Clean up pending listeners
document.removeEventListener('mousemove', this.boundPendingMouseMove);
document.removeEventListener('mouseup', this.boundPendingMouseUp);
// Start actual drag
this.startDrag(this.mouseDragPending.event, element, widget, onDragEnd, widgets);
this.mouseDragPending = null;
}
}
/**
* Handle mouse up before drag threshold is reached (click, not drag)
* @param {MouseEvent} e - Mouse event
*/
onPendingMouseUp(e) {
if (!this.mouseDragPending) return;
// Clean up pending listeners - this was a click, not a drag
document.removeEventListener('mousemove', this.boundPendingMouseMove);
document.removeEventListener('mouseup', this.boundPendingMouseUp);
this.mouseDragPending = null;
}
/**
* Update drag position and visual feedback
* @param {number} clientX - Pointer X coordinate
@@ -338,6 +403,8 @@ export class DragDropHandler {
document.removeEventListener('touchmove', this.boundTouchMove);
document.removeEventListener('touchend', this.boundTouchEnd);
document.removeEventListener('keydown', this.boundKeyDown);
document.removeEventListener('mousemove', this.boundPendingMouseMove);
document.removeEventListener('mouseup', this.boundPendingMouseUp);
// Clear touch timer
if (this.touchTimer) {
@@ -345,6 +412,9 @@ export class DragDropHandler {
this.touchTimer = null;
}
// Clear pending drag state
this.mouseDragPending = null;
this.dragState = null;
}