From 428aaa33b2d49555af2a2b6c6524b2556d76e3ff Mon Sep 17 00:00:00 2001 From: Simon Guo Date: Wed, 28 Feb 2024 22:44:25 +0800 Subject: [PATCH] feat: add PointerMoveTracker (#75) * feat: add PointerMoveTracker * fix: update tests for PointerMoveTracker --- babel.config.js | 4 + package-lock.json | 22 +++ package.json | 5 +- src/PointerMoveTracker.ts | 157 +++++++++++++++++++++ src/index.ts | 1 + test/PointerMoveTrackerSpec.js | 45 ++++++ test/{wheelSpec.js => WheelHandlerSpec.js} | 1 + test/html/PointerMoveTracker.html | 50 +++++++ test/html/WheelHandler.html | 68 +++++++++ 9 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 src/PointerMoveTracker.ts create mode 100644 test/PointerMoveTrackerSpec.js rename test/{wheelSpec.js => WheelHandlerSpec.js} (99%) create mode 100644 test/html/PointerMoveTracker.html create mode 100644 test/html/WheelHandler.html diff --git a/babel.config.js b/babel.config.js index 0fcebeb..f0c0e72 100644 --- a/babel.config.js +++ b/babel.config.js @@ -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'); } diff --git a/package-lock.json b/package-lock.json index 8afcf99..4daa027 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,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", @@ -3363,6 +3364,18 @@ "semver": "bin/semver.js" } }, + "node_modules/babel-plugin-add-import-extension": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-add-import-extension/-/babel-plugin-add-import-extension-1.6.0.tgz", + "integrity": "sha512-JVSQPMzNzN/S4wPRoKQ7+u8PlkV//BPUMnfWVbr63zcE+6yHdU2Mblz10Vf7qe+6Rmu4svF5jG7JxdcPi9VvKg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0" + } + }, "node_modules/babel-plugin-add-module-exports": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-1.0.4.tgz", @@ -23187,6 +23200,15 @@ } } }, + "babel-plugin-add-import-extension": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-add-import-extension/-/babel-plugin-add-import-extension-1.6.0.tgz", + "integrity": "sha512-JVSQPMzNzN/S4wPRoKQ7+u8PlkV//BPUMnfWVbr63zcE+6yHdU2Mblz10Vf7qe+6Rmu4svF5jG7JxdcPi9VvKg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, "babel-plugin-add-module-exports": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-1.0.4.tgz", diff --git a/package.json b/package.json index a5a4caf..e886e08 100644 --- a/package.json +++ b/package.json @@ -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" }, @@ -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", diff --git a/src/PointerMoveTracker.ts b/src/PointerMoveTracker.ts new file mode 100644 index 0000000..ef17c9d --- /dev/null +++ b/src/PointerMoveTracker.ts @@ -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); + }; +} diff --git a/src/index.ts b/src/index.ts index 964b848..c6756d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/test/PointerMoveTrackerSpec.js b/test/PointerMoveTrackerSpec.js new file mode 100644 index 0000000..f738615 --- /dev/null +++ b/test/PointerMoveTrackerSpec.js @@ -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'); + }); +}); diff --git a/test/wheelSpec.js b/test/WheelHandlerSpec.js similarity index 99% rename from test/wheelSpec.js rename to test/WheelHandlerSpec.js index 5f88d84..2d64379 100644 --- a/test/wheelSpec.js +++ b/test/WheelHandlerSpec.js @@ -34,6 +34,7 @@ describe('WheelHandler', () => { true, true ); + wheelHandler.onWheel(mockEvent); }); diff --git a/test/html/PointerMoveTracker.html b/test/html/PointerMoveTracker.html new file mode 100644 index 0000000..1b870ca --- /dev/null +++ b/test/html/PointerMoveTracker.html @@ -0,0 +1,50 @@ + + + + + + + PointerMoveTracker + + +
+ +
+
+

drag me (fail)

+
+ +
+

touch me

+
+
+ + + + diff --git a/test/html/WheelHandler.html b/test/html/WheelHandler.html new file mode 100644 index 0000000..4c9f129 --- /dev/null +++ b/test/html/WheelHandler.html @@ -0,0 +1,68 @@ + + + + + + + WheelHandler + + +
+
+ πŸ˜€ 😁 πŸ˜‚ 🀣 πŸ˜ƒπŸ˜„ πŸ˜… πŸ˜† πŸ˜‰ 😊😫 😴 😌 πŸ˜› πŸ˜œπŸ‘†πŸ» πŸ˜’ πŸ˜“ πŸ˜” πŸ‘‡πŸ»πŸ˜‘ 😢 πŸ™„ 😏 😣 😞 😟 😀 😒 πŸ˜­πŸ€‘ 😲 + πŸ™„ πŸ™ πŸ˜–πŸ‘ πŸ‘Ž πŸ‘Š ✊ πŸ€›πŸ™„ βœ‹ 🀚 πŸ– πŸ––πŸ‘πŸΌ πŸ‘ŽπŸΌ πŸ‘ŠπŸΌ ✊🏼 πŸ€›πŸΌ ☝🏽 βœ‹πŸ½ 🀚🏽 πŸ–πŸ½ πŸ––πŸ½πŸŒ– πŸŒ— 🌘 πŸŒ‘ πŸŒ’πŸ’« πŸ’₯ πŸ’’ πŸ’¦ + πŸ’§πŸ  🐟 🐬 🐳 πŸ‹πŸ˜¬ 😐 πŸ˜• 😯 😢 πŸ˜‡ 😏 πŸ˜‘ πŸ˜“ 😡πŸ₯ 🐣 πŸ” πŸ› 🐀πŸ’ͺ ✨ πŸ”” ✊ βœ‹πŸ‘‡ πŸ‘Š πŸ‘ πŸ‘ˆ πŸ‘†πŸ’› πŸ‘ + πŸ‘Ž πŸ‘Œ πŸ’˜ πŸ‘πŸΌ πŸ‘ŽπŸΌ πŸ‘ŠπŸΌ ✊🏼 πŸ€›πŸΌβ˜πŸ½ βœ‹πŸ½ 🀚🏽 πŸ–πŸ½ πŸ––πŸ½πŸŒ– πŸŒ— 🌘 πŸŒ‘ πŸŒ’πŸ’« πŸ’₯ πŸ’’ πŸ’¦ πŸ’§πŸ  🐟 🐬 🐳 πŸ‹ 😬 😐 πŸ˜• 😯 + πŸ˜ΆπŸ˜‡ 😏 πŸ˜‘ πŸ˜“ 😡πŸ₯ 🐣 πŸ” πŸ› 🐀πŸ’ͺ ✨ πŸ”” ✊ βœ‹πŸ‘‡ πŸ‘Š πŸ‘ πŸ‘ˆ πŸ‘† πŸ’› πŸ‘ πŸ‘Ž πŸ‘Œ πŸ’˜ πŸ˜€ 😁 πŸ˜‚ 🀣 πŸ˜ƒπŸ˜„ + πŸ˜… πŸ˜† πŸ˜‰ 😊😫 😴 😌 πŸ˜› πŸ˜œπŸ‘†πŸ» πŸ˜’ πŸ˜“ πŸ˜” πŸ‘‡πŸ»πŸ˜‘ 😢 πŸ™„ 😏 😣 😞 😟 😀 😒 πŸ˜­πŸ€‘ 😲 πŸ™„ πŸ™ πŸ˜–πŸ‘ πŸ‘Ž πŸ‘Š + ✊ πŸ€›πŸ™„ βœ‹ 🀚 πŸ– πŸ––πŸ‘πŸΌ πŸ‘ŽπŸΌ πŸ‘ŠπŸΌ ✊🏼 πŸ€›πŸΌ ☝🏽 βœ‹πŸ½ 🀚🏽 πŸ–πŸ½ πŸ––πŸ½πŸŒ– πŸŒ— 🌘 πŸŒ‘ πŸŒ’πŸ’« πŸ’₯ πŸ’’ πŸ’¦ πŸ’§πŸ  🐟 🐬 🐳 πŸ‹πŸ˜¬ + 😐 πŸ˜• 😯 😢 πŸ˜‡ 😏 πŸ˜‘ πŸ˜“ 😡πŸ₯ 🐣 πŸ” πŸ› 🐀πŸ’ͺ ✨ πŸ”” ✊ βœ‹πŸ‘‡ πŸ‘Š πŸ‘ πŸ‘ˆ πŸ‘†πŸ’› πŸ‘ πŸ‘Ž πŸ‘Œ πŸ’˜ πŸ‘πŸΌ πŸ‘ŽπŸΌ πŸ‘ŠπŸΌ + ✊🏼 πŸ€›πŸΌβ˜πŸ½ βœ‹πŸ½ 🀚🏽 πŸ–πŸ½ πŸ––πŸ½πŸŒ– πŸŒ— 🌘 πŸŒ‘ πŸŒ’πŸ’« πŸ’₯ πŸ’’ πŸ’¦ πŸ’§πŸ  🐟 🐬 🐳 πŸ‹ 😬 😐 πŸ˜• 😯 πŸ˜ΆπŸ˜‡ 😏 πŸ˜‘ πŸ˜“ 😡πŸ₯ + 🐣 πŸ” πŸ› 🐀πŸ’ͺ ✨ πŸ”” ✊ βœ‹πŸ‘‡ πŸ‘Š πŸ‘ πŸ‘ˆ πŸ‘† πŸ’› πŸ‘ πŸ‘Ž πŸ‘Œ πŸ’˜ πŸ˜€ 😁 πŸ˜‚ 🀣 πŸ˜ƒπŸ˜„ πŸ˜… πŸ˜† πŸ˜‰ 😊😫 😴 😌 + πŸ˜› πŸ˜œπŸ‘†πŸ» πŸ˜’ πŸ˜“ πŸ˜” πŸ‘‡πŸ»πŸ˜‘ 😢 πŸ™„ 😏 😣 😞 😟 😀 😒 πŸ˜­πŸ€‘ 😲 πŸ™„ πŸ™ πŸ˜–πŸ‘ πŸ‘Ž πŸ‘Š ✊ πŸ€›πŸ™„ βœ‹ 🀚 πŸ– πŸ––πŸ‘πŸΌ + πŸ‘ŽπŸΌ πŸ‘ŠπŸΌ ✊🏼 πŸ€›πŸΌ ☝🏽 βœ‹πŸ½ 🀚🏽 πŸ–πŸ½ πŸ––πŸ½πŸŒ– πŸŒ— 🌘 πŸŒ‘ πŸŒ’πŸ’« πŸ’₯ πŸ’’ πŸ’¦ πŸ’§πŸ  🐟 🐬 🐳 πŸ‹πŸ˜¬ 😐 πŸ˜• 😯 😢 πŸ˜‡ 😏 πŸ˜‘ + πŸ˜“ 😡πŸ₯ 🐣 πŸ” πŸ› 🐀πŸ’ͺ ✨ πŸ”” ✊ βœ‹πŸ‘‡ πŸ‘Š πŸ‘ πŸ‘ˆ πŸ‘†πŸ’› πŸ‘ πŸ‘Ž πŸ‘Œ πŸ’˜ πŸ‘πŸΌ πŸ‘ŽπŸΌ πŸ‘ŠπŸΌ ✊🏼 πŸ€›πŸΌβ˜πŸ½ βœ‹πŸ½ 🀚🏽 πŸ–πŸ½ πŸ––πŸ½πŸŒ– + πŸŒ— 🌘 πŸŒ‘ πŸŒ’πŸ’« πŸ’₯ πŸ’’ πŸ’¦ πŸ’§πŸ  🐟 🐬 🐳 πŸ‹ 😬 😐 πŸ˜• 😯 πŸ˜ΆπŸ˜‡ 😏 πŸ˜‘ πŸ˜“ 😡πŸ₯ 🐣 πŸ” πŸ› 🐀πŸ’ͺ ✨ πŸ”” + ✊ βœ‹πŸ‘‡ πŸ‘Š πŸ‘ πŸ‘ˆ πŸ‘† πŸ’› πŸ‘ πŸ‘Ž πŸ‘Œ πŸ’˜ πŸ˜€ 😁 πŸ˜‚ 🀣 πŸ˜ƒπŸ˜„ πŸ˜… πŸ˜† πŸ˜‰ 😊😫 😴 😌 πŸ˜› πŸ˜œπŸ‘†πŸ» πŸ˜’ πŸ˜“ πŸ˜” + πŸ‘‡πŸ»πŸ˜‘ 😢 πŸ™„ 😏 😣 😞 😟 😀 😒 πŸ˜­πŸ€‘ 😲 πŸ™„ πŸ™ πŸ˜–πŸ‘ πŸ‘Ž πŸ‘Š ✊ πŸ€›πŸ™„ βœ‹ 🀚 πŸ– πŸ––πŸ‘πŸΌ πŸ‘ŽπŸΌ πŸ‘ŠπŸΌ ✊🏼 πŸ€›πŸΌ ☝🏽 + βœ‹πŸ½ 🀚🏽 πŸ–πŸ½ πŸ––πŸ½πŸŒ– πŸŒ— 🌘 πŸŒ‘ πŸŒ’πŸ’« πŸ’₯ πŸ’’ πŸ’¦ πŸ’§πŸ  🐟 🐬 🐳 πŸ‹πŸ˜¬ 😐 πŸ˜• 😯 😢 πŸ˜‡ 😏 πŸ˜‘ πŸ˜“ 😡πŸ₯ 🐣 πŸ” + πŸ› 🐀πŸ’ͺ ✨ πŸ”” ✊ βœ‹πŸ‘‡ πŸ‘Š πŸ‘ πŸ‘ˆ πŸ‘†πŸ’› πŸ‘ πŸ‘Ž πŸ‘Œ πŸ’˜ πŸ‘πŸΌ πŸ‘ŽπŸΌ πŸ‘ŠπŸΌ ✊🏼 πŸ€›πŸΌβ˜πŸ½ βœ‹πŸ½ 🀚🏽 πŸ–πŸ½ πŸ––πŸ½πŸŒ– πŸŒ— 🌘 πŸŒ‘ πŸŒ’πŸ’« + πŸ’₯ πŸ’’ πŸ’¦ πŸ’§πŸ  🐟 🐬 🐳 πŸ‹ 😬 😐 πŸ˜• 😯 πŸ˜ΆπŸ˜‡ 😏 πŸ˜‘ πŸ˜“ 😡πŸ₯ 🐣 πŸ” πŸ› 🐀πŸ’ͺ ✨ πŸ”” ✊ βœ‹πŸ‘‡ πŸ‘Š πŸ‘ + πŸ‘ˆ πŸ‘† πŸ’› πŸ‘ πŸ‘Ž πŸ‘Œ πŸ’˜ +
+
+ + + +