diff --git a/src/api/window.ts b/src/api/window.ts index 5809a61..ecc41a7 100644 --- a/src/api/window.ts +++ b/src/api/window.ts @@ -6,7 +6,10 @@ import { WindowSizeOptions } from '../types/api/window'; -const draggableRegions: WeakMap = new WeakMap(); +const draggableRegions: WeakMap void; + pointerup: (e: PointerEvent) => void; +}> = new WeakMap(); export function setTitle(title: string): Promise { return sendMessage('window.setTitle', { title }); @@ -72,15 +75,35 @@ export function center(): Promise { return sendMessage('window.center'); }; -export function setDraggableRegion(domElementOrId: string | HTMLElement): Promise { - return new Promise((resolve: any, reject: any) => { +export type DraggableRegionOptions = { + /** + * If set to `true`, the region will always capture the pointer, + * ensuring dragging doesn't break on fast pointer movement. + * Note that it prevents child elements from receiving any pointer events. + * Defaults to `false`. + */ + alwaysCapture?: boolean; + /** + * Minimum distance between cursor's starting and current position + * after which dragging is started. This helps prevent accidental dragging + * while interacting with child elements. + * Defaults to `10`. (In pixels.) + */ + dragMinDistance?: number; +} +export function setDraggableRegion(domElementOrId: string | HTMLElement, options: DraggableRegionOptions = {}): Promise<{ + success: true, + message: string +}> { + return new Promise>>((resolve, reject) => { const draggableRegion: HTMLElement = domElementOrId instanceof Element ? domElementOrId : document.getElementById(domElementOrId); let initialClientX: number = 0; let initialClientY: number = 0; let absDragMovementDistance: number = 0; - let isPointerCaptured = false; + let shouldReposition = false; let lastMoveTimestamp = performance.now(); + let isPointerCaptured = options.alwaysCapture; if (!draggableRegion) { return reject({ @@ -98,12 +121,26 @@ export function setDraggableRegion(domElementOrId: string | HTMLElement): Promis draggableRegion.addEventListener('pointerdown', startPointerCapturing); draggableRegion.addEventListener('pointerup', endPointerCapturing); + draggableRegion.addEventListener('pointercancel', endPointerCapturing); draggableRegions.set(draggableRegion, { pointerdown: startPointerCapturing, pointerup: endPointerCapturing }); async function onPointerMove(evt: PointerEvent) { + // Get absolute drag distance from the starting point + const dx = evt.clientX - initialClientX, + dy = evt.clientY - initialClientY; + absDragMovementDistance = Math.sqrt(dx * dx + dy * dy); + // Only start pointer capturing when the user dragged more than a certain amount of distance + // This ensures that the user can also click on the dragable area, e.g. if the area is menu / navbar + if (absDragMovementDistance >= (options.dragMinDistance ?? 10)) { + shouldReposition = true; + if (!isPointerCaptured) { + draggableRegion.setPointerCapture(evt.pointerId); + isPointerCaptured = true; + } + } - if (isPointerCaptured) { + if (shouldReposition) { const currentMilliseconds = performance.now(); const timeTillLastMove = currentMilliseconds - lastMoveTimestamp; @@ -123,15 +160,6 @@ export function setDraggableRegion(domElementOrId: string | HTMLElement): Promis return; } - - // Add absolute drag distance - absDragMovementDistance = Math.sqrt(evt.movementX * evt.movementX + evt.movementY * evt.movementY); - // Only start pointer capturing when the user dragged more than a certain amount of distance - // This ensures that the user can also click on the dragable area, e.g. if the area is menu / navbar - if (absDragMovementDistance >= 10) { // TODO: introduce constant instead of magic number? - isPointerCaptured = true; - draggableRegion.setPointerCapture(evt.pointerId); - } } function startPointerCapturing(evt: PointerEvent) { @@ -139,6 +167,9 @@ export function setDraggableRegion(domElementOrId: string | HTMLElement): Promis initialClientX = evt.clientX; initialClientY = evt.clientY; draggableRegion.addEventListener('pointermove', onPointerMove); + if (options.alwaysCapture) { + draggableRegion.setPointerCapture(evt.pointerId); + } } function endPointerCapturing(evt: PointerEvent) { @@ -153,8 +184,11 @@ export function setDraggableRegion(domElementOrId: string | HTMLElement): Promis }); }; -export function unsetDraggableRegion(domElementOrId: string | HTMLElement): Promise { - return new Promise((resolve: any, reject: any) => { +export function unsetDraggableRegion(domElementOrId: string | HTMLElement): Promise<{ + success: true, + message: string +}> { + return new Promise>>((resolve, reject) => { const draggableRegion: HTMLElement = domElementOrId instanceof Element ? domElementOrId : document.getElementById(domElementOrId); @@ -174,6 +208,7 @@ export function unsetDraggableRegion(domElementOrId: string | HTMLElement): Prom const { pointerdown, pointerup } = draggableRegions.get(draggableRegion); draggableRegion.removeEventListener('pointerdown', pointerdown); draggableRegion.removeEventListener('pointerup', pointerup); + draggableRegion.removeEventListener('pointercancel', pointerup); draggableRegions.delete(draggableRegion); resolve({