Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add PointerMoveTracker #75

Merged
merged 3 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ module.exports = (api, options) => {
['@babel/plugin-transform-runtime', { useESModules: !modules }]
];

if (NODE_ENV !== 'test') {
plugins.push('babel-plugin-add-import-extension');
}

if (modules) {
plugins.push('add-module-exports');
}
Expand Down
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
"build": "npm run build:gulp && npm run build:types",
"build:gulp": "gulp build --gulpfile scripts/gulpfile.js",
"build:types": "npx tsc --emitDeclarationOnly --outDir lib/cjs && npx tsc --emitDeclarationOnly --outDir lib/esm",
"tdd": "NODE_ENV=test karma start",
"docs:generate": "typedoc src/index.ts",
"tdd": "karma start",
"lint": "eslint src/**/*.ts",
"test": "npm run lint && karma start --single-run",
"test": "npm run lint && NODE_ENV=test karma start --single-run",
"prepublishOnly": "npm run build",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
},
Expand Down Expand Up @@ -51,6 +51,7 @@
"@typescript-eslint/parser": "^4.11.1",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.0",
"babel-plugin-add-import-extension": "^1.6.0",
"babel-plugin-add-module-exports": "^1.0.4",
"brfs": "^1.5.0",
"chai": "^3.5.0",
Expand Down
157 changes: 157 additions & 0 deletions src/PointerMoveTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import on from './on';
import isEventSupported from './utils/isEventSupported';

interface PointerMoveTrackerOptions {
useTouchEvent: boolean;
onMove: (x: number, y: number, event: MouseEvent | TouchEvent) => void;
onMoveEnd: (event: MouseEvent | TouchEvent) => void;
}

/**
* Track mouse/touch events for a given element.
*/
export default class PointerMoveTracker {
isDragStatus = false;
useTouchEvent = true;
animationFrameID = null;
domNode: Element;
onMove = null;
onMoveEnd = null;
eventMoveToken = null;
eventUpToken = null;
moveEvent = null;
deltaX = 0;
deltaY = 0;
x = 0;
y = 0;

/**
* onMove is the callback that will be called on every mouse move.
* onMoveEnd is called on mouse up when movement has ended.
*/
constructor(
domNode: Element,
{ onMove, onMoveEnd, useTouchEvent = true }: PointerMoveTrackerOptions
) {
this.domNode = domNode;
this.onMove = onMove;
this.onMoveEnd = onMoveEnd;
this.useTouchEvent = useTouchEvent;
}

isSupportTouchEvent() {
return this.useTouchEvent && isEventSupported('touchstart');
}

getClientX(event: TouchEvent | MouseEvent) {
return this.isSupportTouchEvent()
? (event as TouchEvent).touches?.[0].clientX
: (event as MouseEvent).clientX;
}

getClientY(event: TouchEvent | MouseEvent) {
return this.isSupportTouchEvent()
? (event as TouchEvent).touches?.[0].clientY
: (event as MouseEvent).clientY;
}

/**
* This is to set up the listeners for listening to mouse move
* and mouse up signaling the movement has ended. Please note that these
* listeners are added at the document.body level. It takes in an event
* in order to grab inital state.
*/
captureMoves(event) {
if (!this.eventMoveToken && !this.eventUpToken) {
this.eventMoveToken = on(this.domNode, 'mousemove', this.onDragMove);
this.eventUpToken = on(this.domNode, 'mouseup', this.onDragUp);

if (this.isSupportTouchEvent()) {
this.eventMoveToken = on(this.domNode, 'touchmove', this.onDragMove, { passive: false });
this.eventUpToken = on(this.domNode, 'touchend', this.onDragUp, { passive: false });
on(this.domNode, 'touchcancel', this.releaseMoves);
}
}

if (!this.isDragStatus) {
this.deltaX = 0;
this.deltaY = 0;
this.isDragStatus = true;
this.x = this.getClientX(event);
this.y = this.getClientY(event);
}

event.preventDefault();
}

/**
* These releases all of the listeners on document.body.
*/
releaseMoves() {
if (this.eventMoveToken) {
this.eventMoveToken.off();
this.eventMoveToken = null;
}

if (this.eventUpToken) {
this.eventUpToken.off();
this.eventUpToken = null;
}

if (this.animationFrameID !== null) {
cancelAnimationFrame(this.animationFrameID);
this.animationFrameID = null;
}

if (this.isDragStatus) {
this.isDragStatus = false;
this.x = 0;
this.y = 0;
}
}

/**
* Returns whether or not if the mouse movement is being tracked.
*/
isDragging = () => this.isDragStatus;

/**
* Calls onMove passed into constructor and updates internal state.
*/
onDragMove = (event: MouseEvent | TouchEvent) => {
const x = this.getClientX(event);
const y = this.getClientY(event);

this.deltaX += x - this.x;
this.deltaY += x - this.y;

if (this.animationFrameID === null) {
// The mouse may move faster then the animation frame does.
// Use `requestAnimationFrame` to avoid over-updating.
this.animationFrameID = requestAnimationFrame(this.didDragMove);
}

this.x = x;
this.y = y;

this.moveEvent = event;
event.preventDefault();
};

didDragMove = () => {
this.animationFrameID = null;
this.onMove(this.deltaX, this.deltaY, this.moveEvent);

this.deltaX = 0;
this.deltaY = 0;
};
/**
* Calls onMoveEnd passed into constructor and updates internal state.
*/
onDragUp = event => {
if (this.animationFrameID) {
this.didDragMove();
}
this.onMoveEnd?.(event);
};
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { default as on } from './on';
export { default as off } from './off';
export { default as WheelHandler } from './WheelHandler';
export { default as DOMMouseMoveTracker } from './DOMMouseMoveTracker';
export { default as PointerMoveTracker } from './PointerMoveTracker';

/** classNames */
export { default as addClass } from './addClass';
Expand Down
45 changes: 45 additions & 0 deletions test/PointerMoveTrackerSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as lib from '../src';
import simulant from 'simulant';

describe('PointerMoveTracker', () => {
beforeEach(() => {
document.body.innerHTML = window.__html__['test/html/PointerMoveTracker.html'];
});

it('Should track for mouse events', done => {
const target = document.getElementById('drag-target');
let tracker = null;

const handleDragMove = (x, y, e) => {
if (e instanceof MouseEvent) {
if (x && y) {
expect(x).to.equal(100);
expect(y).to.equal(100);
}
}
};

const handleDragEnd = () => {
tracker.releaseMoves();
tracker = null;
done();
};

function handleStart(e) {
if (!tracker) {
tracker = new lib.PointerMoveTracker(document.body, {
onMove: handleDragMove,
onMoveEnd: handleDragEnd
});

tracker.captureMoves(e);
}
}

target.addEventListener('mousedown', handleStart);

simulant.fire(target, 'mousedown');
simulant.fire(document.body, 'mousemove', { clientX: 100, clientY: 100 });
simulant.fire(document.body, 'mouseup');
});
});
1 change: 1 addition & 0 deletions test/wheelSpec.js → test/WheelHandlerSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('WheelHandler', () => {
true,
true
);

wheelHandler.onWheel(mockEvent);
});

Expand Down
50 changes: 50 additions & 0 deletions test/html/PointerMoveTracker.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PointerMoveTracker</title>
</head>
<body>
<div style="height: 1000px">
<button id="btn">drag me</button>
<hr />
<div id="drag-target">
<p>drag me (fail)</p>
</div>

<div id="touch-target">
<p>touch me</p>
</div>
</div>

<script type="module">

import PointerMoveTracker from '../../lib/esm/PointerMoveTracker.js';
const handleDragMove = (x, y, e) => {
console.log(e instanceof TouchEvent ? 'TouchEvent:' : 'MouseEvent:', x, y);
};
let tracker = null;
const handleDragEnd = e => {
console.log('end');
tracker.releaseMoves();
tracker = null;
};

function handleStart(e) {
console.log('start');
if (!tracker) {
tracker = new PointerMoveTracker(document.body, {
onMove: handleDragMove,
onMoveEnd: handleDragEnd
});
tracker.captureMoves(e);
}
}

document.getElementById('btn').addEventListener('touchstart', handleStart);
document.getElementById('btn').addEventListener('mousedown', handleStart);
</script>
</body>
</html>
Loading
Loading