Skip to content

Commit

Permalink
⚡ Improve window.setDraggableRegion and add options to allow native-f…
Browse files Browse the repository at this point in the history
…eeling behavior. (#120)

* 🐛 Fix window.setDraggableRegion not starting pointer capture when needed

Closes neutralinojs/neutralinojs#1301

* 🐛 Add options. Change the logic so default values allow pointer events on children.
  • Loading branch information
CosmoMyzrailGorynych authored Aug 10, 2024
1 parent e274a9b commit 6c1fef9
Showing 1 changed file with 51 additions and 16 deletions.
67 changes: 51 additions & 16 deletions src/api/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
WindowSizeOptions
} from '../types/api/window';

const draggableRegions: WeakMap<HTMLElement, any> = new WeakMap();
const draggableRegions: WeakMap<HTMLElement, {
pointerdown: (e: PointerEvent) => void;
pointerup: (e: PointerEvent) => void;
}> = new WeakMap();

export function setTitle(title: string): Promise<void> {
return sendMessage('window.setTitle', { title });
Expand Down Expand Up @@ -72,15 +75,35 @@ export function center(): Promise<void> {
return sendMessage('window.center');
};

export function setDraggableRegion(domElementOrId: string | HTMLElement): Promise<void> {
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<Awaited<ReturnType<typeof setDraggableRegion>>>((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({
Expand All @@ -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;
Expand All @@ -123,22 +160,16 @@ 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) {
if (evt.button !== 0) return;
initialClientX = evt.clientX;
initialClientY = evt.clientY;
draggableRegion.addEventListener('pointermove', onPointerMove);
if (options.alwaysCapture) {
draggableRegion.setPointerCapture(evt.pointerId);
}
}

function endPointerCapturing(evt: PointerEvent) {
Expand All @@ -153,8 +184,11 @@ export function setDraggableRegion(domElementOrId: string | HTMLElement): Promis
});
};

export function unsetDraggableRegion(domElementOrId: string | HTMLElement): Promise<void> {
return new Promise((resolve: any, reject: any) => {
export function unsetDraggableRegion(domElementOrId: string | HTMLElement): Promise<{
success: true,
message: string
}> {
return new Promise<Awaited<ReturnType<typeof unsetDraggableRegion>>>((resolve, reject) => {
const draggableRegion: HTMLElement = domElementOrId instanceof Element ?
domElementOrId : document.getElementById(domElementOrId);

Expand All @@ -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({
Expand Down

0 comments on commit 6c1fef9

Please sign in to comment.