Skip to content

Commit

Permalink
feat: add PointerMoveTracker (#75)
Browse files Browse the repository at this point in the history
* feat: add PointerMoveTracker
* fix: update tests for PointerMoveTracker
  • Loading branch information
simonguo authored Feb 28, 2024
1 parent 8c0d762 commit 428aaa3
Show file tree
Hide file tree
Showing 9 changed files with 351 additions and 2 deletions.
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

0 comments on commit 428aaa3

Please sign in to comment.