From be83b3b056ca262ba08906a82d3e70e52ac91440 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 10 Aug 2024 19:13:16 +1200 Subject: [PATCH 1/2] :bug: Fix window.setDraggableRegion not starting pointer capture when needed Closes https://github.com/neutralinojs/neutralinojs/issues/1301 --- src/api/window.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/api/window.ts b/src/api/window.ts index 5809a61..53f8173 100644 --- a/src/api/window.ts +++ b/src/api/window.ts @@ -79,7 +79,7 @@ export function setDraggableRegion(domElementOrId: string | HTMLElement): Promis let initialClientX: number = 0; let initialClientY: number = 0; let absDragMovementDistance: number = 0; - let isPointerCaptured = false; + let shouldReposition = false; let lastMoveTimestamp = performance.now(); if (!draggableRegion) { @@ -102,8 +102,17 @@ export function setDraggableRegion(domElementOrId: string | HTMLElement): Promis 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 >= 10) { // TODO: introduce constant instead of magic number? + shouldReposition = true; + } - if (isPointerCaptured) { + if (shouldReposition) { const currentMilliseconds = performance.now(); const timeTillLastMove = currentMilliseconds - lastMoveTimestamp; @@ -123,15 +132,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 +139,7 @@ export function setDraggableRegion(domElementOrId: string | HTMLElement): Promis initialClientX = evt.clientX; initialClientY = evt.clientY; draggableRegion.addEventListener('pointermove', onPointerMove); + draggableRegion.setPointerCapture(evt.pointerId); } function endPointerCapturing(evt: PointerEvent) { From 9ae64d045f00f31f0aed6ca5d42c5a64897af32c Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 10 Aug 2024 20:08:21 +1200 Subject: [PATCH 2/2] :bug: Add options. Change the logic so default values allow pointer events on children. --- src/api/window.ts | 48 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/api/window.ts b/src/api/window.ts index 53f8173..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,8 +75,27 @@ 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; @@ -81,6 +103,7 @@ export function setDraggableRegion(domElementOrId: string | HTMLElement): Promis let absDragMovementDistance: number = 0; let shouldReposition = false; let lastMoveTimestamp = performance.now(); + let isPointerCaptured = options.alwaysCapture; if (!draggableRegion) { return reject({ @@ -98,6 +121,7 @@ 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 }); @@ -108,8 +132,12 @@ export function setDraggableRegion(domElementOrId: string | HTMLElement): Promis 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 >= 10) { // TODO: introduce constant instead of magic number? + if (absDragMovementDistance >= (options.dragMinDistance ?? 10)) { shouldReposition = true; + if (!isPointerCaptured) { + draggableRegion.setPointerCapture(evt.pointerId); + isPointerCaptured = true; + } } if (shouldReposition) { @@ -139,7 +167,9 @@ export function setDraggableRegion(domElementOrId: string | HTMLElement): Promis initialClientX = evt.clientX; initialClientY = evt.clientY; draggableRegion.addEventListener('pointermove', onPointerMove); - draggableRegion.setPointerCapture(evt.pointerId); + if (options.alwaysCapture) { + draggableRegion.setPointerCapture(evt.pointerId); + } } function endPointerCapturing(evt: PointerEvent) { @@ -154,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); @@ -175,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({