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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user