UNPKG

js-draw

Version:

Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.

126 lines (125 loc) 4.83 kB
import { Vec2 } from '@js-draw/math'; const makeDraggable = (dragElement, options) => { const dragElements = [...options.draggableChildElements, dragElement]; let lastX = 0; let lastY = 0; let startX = 0; let startY = 0; let pointerDown = false; let capturedPointerId = null; const isDraggableElement = (element) => { if (!element) { return false; } if (dragElements.includes(element)) { return true; } // Some inputs handle dragging themselves. Don't also interpret such gestures // as dragging the dropdown. const undraggableElementTypes = ['INPUT', 'SELECT', 'IMG']; let hasSuitableAncestors = false; let ancestor = element.parentElement; while (ancestor) { if (undraggableElementTypes.includes(ancestor.tagName)) { break; } if (dragElements.includes(ancestor)) { hasSuitableAncestors = true; break; } ancestor = ancestor.parentElement; } return !undraggableElementTypes.includes(element.tagName) && hasSuitableAncestors; }; const removeEventListenerCallbacks = []; const addEventListener = (listenerType, listener, options) => { dragElement.addEventListener(listenerType, listener, options); removeEventListenerCallbacks.push(() => { dragElement.removeEventListener(listenerType, listener); }); }; const clickThreshold = 5; // Returns whether the current (or if no current, **the last**) gesture is roughly a click. // Because this can be called **after** a gesture has just ended, it should not require // the gesture to be in progress. const isRoughlyClick = () => { return Math.hypot(lastX - startX, lastY - startY) < clickThreshold; }; let startedDragging = false; addEventListener('pointerdown', (event) => { if (event.defaultPrevented || !isDraggableElement(event.target)) { return; } if (event.isPrimary) { startedDragging = false; lastX = event.clientX; lastY = event.clientY; startX = event.clientX; startY = event.clientY; capturedPointerId = null; pointerDown = true; } }, { passive: true }); const onGestureEnd = (_event) => { // If the pointerup/pointercancel event was for a pointer not being tracked, if (!pointerDown) { return; } if (capturedPointerId !== null) { dragElement.releasePointerCapture(capturedPointerId); capturedPointerId = null; } options.onDragEnd({ roughlyClick: isRoughlyClick(), endTimestamp: performance.now(), displacement: Vec2.of(lastX - startX, lastY - startY), }); pointerDown = false; startedDragging = false; }; addEventListener('pointermove', (event) => { if (!event.isPrimary || !pointerDown) { return undefined; } // Mouse event and no buttons pressed? Cancel the event. // This can happen if the event was canceled by a focus change (e.g. by opening a // right-click menu). if (event.pointerType === 'mouse' && event.buttons === 0) { onGestureEnd(event); return undefined; } // Only capture after motion -- capturing early prevents click events in Chrome. if (capturedPointerId === null && !isRoughlyClick()) { dragElement.setPointerCapture(event.pointerId); capturedPointerId = event.pointerId; } const x = event.clientX; const y = event.clientY; const dx = x - lastX; const dy = y - lastY; const isClick = Math.abs(x - startX) <= clickThreshold && Math.abs(y - startY) <= clickThreshold; if (!isClick || startedDragging) { options.onDrag(dx, dy, Vec2.of(x - startX, y - startY)); lastX = x; lastY = y; startedDragging = true; } }); addEventListener('pointerleave', (event) => { // Capture the pointer if it exits the container while dragging. if (capturedPointerId === null && pointerDown && event.isPrimary) { dragElement.setPointerCapture(event.pointerId); capturedPointerId = event.pointerId; } }); addEventListener('pointerup', onGestureEnd); addEventListener('pointercancel', onGestureEnd); return { removeListeners: () => { for (const removeListenerCallback of removeEventListenerCallbacks) { removeListenerCallback(); } }, }; }; export default makeDraggable;