├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist └── nanodrag.min.js ├── example.js ├── nanodrag.js ├── package.json └── test └── nanodrag.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Chrome 4 | Chrome* 5 | *swp 6 | coverage 7 | .nyc_output 8 | sauce_connect.log 9 | .sauce-cred 10 | .sauce-credentials.json 11 | .zuulrc 12 | coverage.json 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - '8' 3 | sudo: false 4 | language: node_js 5 | script: npm run test 6 | addons: 7 | apt: 8 | packages: 9 | - xvfb 10 | install: 11 | - export DISPLAY=':99.0' 12 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 13 | - npm install 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Todd Kennedy 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License.` 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nanodrag [![stability][0]][1] 2 | [![npm version][2]][3] [![build status][4]][5] 3 | [![downloads][6]][7] [![js-standard-style][8]][9] 4 | 5 | A small library to simplify the handling of drag events on mouse & touch devices. 6 | 7 | ## Usage 8 | 9 | ```js 10 | const Nanodrag = require('nanodrag') 11 | const div = document.createElement('div') 12 | const nd = Nanodrag(div) 13 | 14 | nd.on('start', (data) => { 15 | console.log('Drag started!', data) 16 | }) 17 | 18 | nd.on('move', (data) => { 19 | console.log('Dragging!', data) 20 | }) 21 | 22 | nd.on('end', (data) => { 23 | console.log('Drag finished!', data) 24 | }) 25 | 26 | nd.close() 27 | ``` 28 | 29 | ## API 30 | 31 | ### Properties 32 | 33 | #### preventDefault:boolean 34 | If this is set to true, it will call `event.preventDefault()` on the event 35 | provided by the `touchmove` or `mousemove` event. This property only works 36 | if you did **not** set `{passive: true}` in the constructor. 37 | 38 | Examples: 39 | 40 | This will attach the listener in passive mode and prevent the calling of 41 | `event.preventDefault` regardless of what value `Nanodrag.preventDefault` is. 42 | 43 | ```js 44 | const nd = new Nanodrag(element, {passive: true}) 45 | ``` 46 | 47 | This will attach the listener in active mode allowing you decide to call 48 | `event.preventDefault` at a later time. 49 | 50 | ```js 51 | const nd = new Nanodrag(element) 52 | nd.on('start', () => { 53 | nd.preventDefault = false 54 | }) 55 | ``` 56 | 57 | ### Methods 58 | 59 | #### new Nanodrag(selector:string|element:HTMLElement, options?:object(key:any)):nanodrag 60 | Create a new nanodrag instance. You can either pass in a valid selector for 61 | `.querySelector` or a reference to an HTML element. A nanodrag instance is also 62 | an instance of a [nanobus](https://github.com/choojs/nanobus) object. 63 | 64 | **options** 65 | * `trackingDelay`?:number - the delay (in nanoseconds) to wait before turning off 66 | the tracking mode if the mouse escapes the tracked element. Default: 300 67 | * `passive`?:boolean - attach `touchmove` and `mousemove` as passive listeners. 68 | This will prevent calling `event.preventDefault` on move events. 69 | 70 | #### nanodrag#on(event:string, listener:function) 71 | Provide a function to invoke when the specified event is triggered 72 | 73 | #### nanodrag#once(event:string, listener:function) 74 | Attach an event to invoke only once 75 | 76 | #### nanodrag#emit(event:string, data:any) 77 | Invoke an event with a specific payload of data 78 | 79 | #### nanodrag#removeListener(event:string, listener:function) 80 | Remove a specific listener 81 | 82 | #### nanodrag#removeAllListeners(event?:string) 83 | Remove all listeners for a given event; if no event is specified, removed all 84 | listeners. 85 | 86 | #### nanodrag#close() 87 | Remove all listeners on the DOM as well as on the nanobus instances and stop 88 | reporting any move events 89 | 90 | ### Events 91 | 92 | #### start 93 | Triggered when a touch start or mouse down event occur on the nanodrag element. 94 | 95 | **data** 96 | * `start`:object 97 | * `x`:number - the x coordinate of the touch instrument or mouse 98 | * `y`:number - the y coordinate of the touch instrument or mouse 99 | 100 | #### move 101 | Triggered when the mouse or touch instrument is moved after being started. For 102 | mouse-like devices, this means the button must be actively held down 103 | 104 | **data** 105 | * `start`:object 106 | * `x`:number - the starting x coordinate of the touch instrument or mouse 107 | * `y`:number - the starting y coordinate of the touch instrument or mouse 108 | * `current`:object 109 | * `x`:number - the current x coordinate of the touch instrument or mouse 110 | * `y`:number - the current y coordinate of the touch instrument or mouse 111 | * `direction`:object 112 | * `x`:string - either 'left' or 'right 113 | * `y`:string - either 'up' or 'down 114 | 115 | #### end 116 | Triggered when the touchend or mouseup event occurs. 117 | 118 | **data** 119 | * `start`:object 120 | * `x`:number - the starting x coordinate of the touch instrument or mouse 121 | * `y`:number - the starting y coordinate of the touch instrument or mouse 122 | * `end`:object 123 | * `x`:number - the end x coordinate of the touch instrument or mouse 124 | * `y`:number - the end y coordinate of the touch instrument or mouse 125 | 126 | 127 | 128 | ## License 129 | Copyright © 2018 Todd Kennedy, [Apache 2.0 License](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) 130 | 131 | [0]: https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square 132 | [1]: https://nodejs.org/api/documentation.html#documentation_stability_index 133 | [2]: https://img.shields.io/npm/v/nanodrag.svg?style=flat-square 134 | [3]: https://npmjs.org/package/nanodrag 135 | [4]: https://img.shields.io/travis/toddself/nanodrag/master.svg?style=flat-square 136 | [5]: https://travis-ci.org/toddself/nanodrag 137 | [6]: http://img.shields.io/npm/dm/nanodrag.svg?style=flat-square 138 | [7]: https://npmjs.org/package/nanodrag 139 | [8]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 140 | [9]: https://github.com/feross/standard 141 | -------------------------------------------------------------------------------- /dist/nanodrag.min.js: -------------------------------------------------------------------------------- 1 | !function(){var t={},e={};(function(i){var s,n=void 0!==i?i:"undefined"!=typeof window?window:{};"undefined"!=typeof document?s=document:(s=n["__GLOBAL_DOCUMENT_CACHE@4"])||(s=n["__GLOBAL_DOCUMENT_CACHE@4"]=t),e=s}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{});var i={};(function(t){var e;e="undefined"!=typeof window?window:void 0!==t?t:"undefined"!=typeof self?self:{},i=e}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{});var s,n={},r="undefined"!=typeof window,o=r&&window.requestIdleCallback,a=function t(e,i){i=i||n;var s;return o?(s=window.requestIdleCallback(function(s){if(s.timeRemaining()<=10&&!s.didTimeout)return t(e,i);e(s)},i),window.cancelIdleCallback.bind(window,s)):r?(s=setTimeout(e,0),clearTimeout.bind(window,s)):void 0},h=!0;try{s=window.performance,h="true"===window.localStorage.DISABLE_NANOTIMING||!s.mark}catch(t){}function u(t){t&&a(t)}var c=function(t,e,i){var s,n=t.length;if(!(e>=n||0===i)){var r=n-(i=e+i>n?n-e:i);for(s=e;s0&&this._emit(this._listeners[t],e),this._starListeners.length>0&&this._emit(this._starListeners,t,e,r.uuid),r(),this},p.prototype.on=p.prototype.addListener=function(t,e){return"*"===t?this._starListeners.push(e):(this._listeners[t]||(this._listeners[t]=[]),this._listeners[t].push(e)),this},p.prototype.prependListener=function(t,e){return"*"===t?this._starListeners.unshift(e):(this._listeners[t]||(this._listeners[t]=[]),this._listeners[t].unshift(e)),this},p.prototype.once=function(t,e){var i=this;return this.on(t,function s(){e.apply(i,arguments),i.removeListener(t,s)}),this},p.prototype.prependOnceListener=function(t,e){var i=this;return this.prependListener(t,function s(){e.apply(i,arguments),i.removeListener(t,s)}),this},p.prototype.removeListener=function(t,e){return"*"===t?(this._starListeners=this._starListeners.slice(),i(this._starListeners,e)):(void 0!==this._listeners[t]&&(this._listeners[t]=this._listeners[t].slice()),i(this._listeners[t],e));function i(t,e){if(t){var i=t.indexOf(e);return-1!==i?(c(t,i,1),!0):void 0}}},p.prototype.removeAllListeners=function(t){return t?"*"===t?this._starListeners=[]:this._listeners[t]=[]:(this._starListeners=[],this._listeners={}),this},p.prototype.listeners=function(t){var e="*"!==t?this._listeners[t]:this._starListeners,i=[];if(e)for(var s=e.length,n=0;nthis.targetEl.addEventListener(t,this,{passive:!0}))}y.prototype=Object.create(l.prototype),y.prototype.handleEvent=function(t){const e=f[t.type]||d[t.type],s=this._getPointerData(t),n=(this[`on${e}`]||(()=>{})).bind(this);t.type.startsWith("touch")&&(this._touchTriggered=!0),this._touchTriggered&&t.type.startsWith("mouse")||("mouseleave"===t.type&&this._active?this._leaveTimer=i.setTimeout(()=>n(t,s),this._trackingDelay):(null!==this._leaveTimer&&(i.clearTimeout(this._leaveTimer),this._leaveTimer=null),n(t,s)))},y.prototype._getPointerData=function(t){return t.touches&&t.touches.length>0?t.touches[0]:{pageX:t.screenX+t.currentTarget.scrollLeft,pageY:t.screenY+t.currentTarget.scrollTop}},y.prototype.onstart=function(t,e){this._active=!0,this._startX=e.pageX,this._startY=e.pageY,this._currentX=e.pageX,this._currentY=e.pageY,Object.keys(d).forEach(t=>{this.targetEl.addEventListener(t,this,{passive:this._passiveMove})}),this.emit("start",{start:{x:this._startX,y:this._startY}})},y.prototype.onend=function(t){if(this._active){const t={start:{x:this._startX,y:this._startY},end:{x:this._currentX,y:this._currentY}};Object.keys(d).forEach(t=>this.targetEl.removeEventListener(t,this)),this._active=!1,this.emit("end",t)}},y.prototype.onmove=function(t,e,i){this._active&&(!this._passiveMove&&this.preventDefault&&t.preventDefault(),v(()=>{this._currentX=e.pageX,this._currentY=e.pageY;const t={direction:this._getSwipeDirection(),start:{x:this._startX,y:this._startY},current:{x:this._currentX,y:this._currentY}};this.emit("move",t)}))},y.prototype.close=function(){Object.keys(f).forEach(t=>this.targetEl.removeEventListener(t,this)),this.removeAllListeners(),this._active=!1},y.prototype._getSwipeDirection=function(){const t=this._startX-this._currentX,e=this._startY-this._currentY;return{x:t>-1?"left":t<0?"right":"",y:e>-1?"up":e<0?"down":""}}}(); -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const Nanodrag = require('./nanodrag') 2 | 3 | if (typeof window !== 'undefined') { 4 | const d = document.createElement('div') 5 | d.style.cssText = 'border: 1px solid black; position: absolute; top: 100px; left: 100px; width: 200px; height: 200px;' 6 | document.body.appendChild(d) 7 | const n = new Nanodrag(d) 8 | let offsetX = 0 9 | let offsetY = 0 10 | 11 | n.on('start', (data) => { 12 | const rect = d.getBoundingClientRect() 13 | offsetY = rect.y - data.start.y 14 | offsetX = rect.x - data.start.x 15 | console.log('i have been started') 16 | }) 17 | 18 | n.on('move', (data) => { 19 | d.style.top = `${data.current.y + offsetY}px` 20 | d.style.left = `${data.current.x + offsetX}px` 21 | }) 22 | 23 | n.on('end', (data) => { 24 | console.log('i am over') 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /nanodrag.js: -------------------------------------------------------------------------------- 1 | const window = require('global/window') 2 | const document = require('global/document') 3 | const Nanobus = require('nanobus') 4 | const noop = () => {} 5 | 6 | const defaultTrackingDelay = 300 7 | 8 | const controlEvents = { 9 | touchstart: 'start', 10 | touchend: 'end', 11 | mousedown: 'start', 12 | mouseup: 'end', 13 | mouseleave: 'end', 14 | mouseover: '' 15 | } 16 | 17 | const moveEvents = { 18 | touchmove: 'move', 19 | mousemove: 'move' 20 | } 21 | 22 | const raf = window.requestAnimationFrame || window.setTimeout 23 | 24 | function Nanodrag (targetEl, options) { 25 | if (!(this instanceof Nanodrag)) return new Nanodrag(targetEl) 26 | Nanobus.call(this) 27 | if (typeof targetEl === 'string') { 28 | targetEl = document.querySelector(targetEl) 29 | } 30 | 31 | if (!targetEl) { 32 | throw new Error('You must supply a valid selector or DOM Node') 33 | } 34 | 35 | options = options || {} 36 | 37 | this.targetEl = targetEl 38 | this._active = false 39 | this._startX = null 40 | this._startY = null 41 | this._currentX = null 42 | this._currentY = null 43 | this._direction = {x: '', y: ''} 44 | this._trackingDelay = options.trackingDelay || defaultTrackingDelay 45 | this._passiveMove = options.passive || false 46 | this._leaveTimer = null 47 | this.preventDefault = true 48 | this._touchTriggered = false 49 | 50 | Object.keys(controlEvents).forEach((evt) => this.targetEl.addEventListener(evt, this, {passive: true})) 51 | } 52 | 53 | Nanodrag.prototype = Object.create(Nanobus.prototype) 54 | 55 | Nanodrag.prototype.handleEvent = function (evt) { 56 | const evtType = controlEvents[evt.type] || moveEvents[evt.type] 57 | const pointerData = this._getPointerData(evt) 58 | const evtMethod = (this[`on${evtType}`] || noop).bind(this) 59 | 60 | if (evt.type.startsWith('touch')) { 61 | this._touchTriggered = true 62 | } 63 | 64 | // iOS fires both `mousedown` and `touchstart`, so if we've gotten a touch 65 | // event, we ignore mouse events 66 | if (this._touchTriggered && evt.type.startsWith('mouse')) { 67 | return 68 | } 69 | 70 | if (evt.type === 'mouseleave' && this._active) { 71 | this._leaveTimer = window.setTimeout(() => evtMethod(evt, pointerData), this._trackingDelay) 72 | return 73 | } else if (this._leaveTimer !== null) { 74 | window.clearTimeout(this._leaveTimer) 75 | this._leaveTimer = null 76 | } 77 | 78 | evtMethod(evt, pointerData) 79 | } 80 | 81 | Nanodrag.prototype._getPointerData = function (evt) { 82 | if (evt.touches && evt.touches.length > 0) { 83 | return evt.touches[0] 84 | } 85 | 86 | const data = { 87 | pageX: evt.screenX + evt.currentTarget.scrollLeft, 88 | pageY: evt.screenY + evt.currentTarget.scrollTop 89 | } 90 | 91 | return data 92 | } 93 | 94 | Nanodrag.prototype.onstart = function (evt, pointerData) { 95 | this._active = true 96 | this._startX = pointerData.pageX 97 | this._startY = pointerData.pageY 98 | this._currentX = pointerData.pageX 99 | this._currentY = pointerData.pageY 100 | Object.keys(moveEvents).forEach((evt) => { 101 | this.targetEl.addEventListener(evt, this, {passive: this._passiveMove}) 102 | }) 103 | this.emit('start', {start: {x: this._startX, y: this._startY}}) 104 | } 105 | 106 | Nanodrag.prototype.onend = function (evt) { 107 | if (this._active) { 108 | const data = { 109 | start: { 110 | x: this._startX, 111 | y: this._startY 112 | }, 113 | end: { 114 | x: this._currentX, 115 | y: this._currentY 116 | } 117 | } 118 | Object.keys(moveEvents).forEach((evt) => this.targetEl.removeEventListener(evt, this)) 119 | this._active = false 120 | this.emit('end', data) 121 | } 122 | } 123 | 124 | Nanodrag.prototype.onmove = function (evt, pointerData, force) { 125 | const update = () => { 126 | this._currentX = pointerData.pageX 127 | this._currentY = pointerData.pageY 128 | const direction = this._getSwipeDirection() 129 | const data = { 130 | direction, 131 | start: { 132 | x: this._startX, 133 | y: this._startY 134 | }, 135 | current: { 136 | x: this._currentX, 137 | y: this._currentY} 138 | } 139 | this.emit('move', data) 140 | } 141 | 142 | if (this._active) { 143 | if (!this._passiveMove && this.preventDefault) evt.preventDefault() 144 | raf(update) 145 | } 146 | } 147 | 148 | Nanodrag.prototype.close = function () { 149 | Object.keys(controlEvents).forEach((evt) => this.targetEl.removeEventListener(evt, this)) 150 | this.removeAllListeners() 151 | this._active = false 152 | } 153 | 154 | Nanodrag.prototype._getSwipeDirection = function () { 155 | const diffX = this._startX - this._currentX 156 | const diffY = this._startY - this._currentY 157 | const x = diffX > -1 ? 'left' : diffX < 0 ? 'right' : '' 158 | const y = diffY > -1 ? 'up' : diffY < 0 ? 'down' : '' 159 | return {x, y} 160 | } 161 | 162 | module.exports = Nanodrag 163 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanodrag", 3 | "version": "2.0.0", 4 | "description": "A small touchdrag events library", 5 | "main": "nanodrag.js", 6 | "scripts": { 7 | "test": "standard && npm run dep && browserify test/*.spec.js | tape-run", 8 | "build": "npm test && mkdir -p dist || exit 0 && browserify nanodrag.js -p tinyify > dist/nanodrag.min.js", 9 | "start": "bankai start example.js", 10 | "dep": "dependency-check . --entry=nanodrag.js && dependency-check . --entry=nanodrag.js --no-dev --unused", 11 | "prepublish": "npm run build" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/toddself/nanodrag.git" 16 | }, 17 | "keywords": [ 18 | "drag", 19 | "touchevents", 20 | "touchdown", 21 | "touchstart", 22 | "touchend", 23 | "swipe", 24 | "touchdrag" 25 | ], 26 | "author": "Todd Kennedy ", 27 | "license": "Apache-2.0", 28 | "bugs": { 29 | "url": "https://github.com/toddself/nanodrag/issues" 30 | }, 31 | "homepage": "https://github.com/toddself/nanodrag#readme", 32 | "dependencies": { 33 | "global": "^4.3.2", 34 | "nanobus": "^4.3.1" 35 | }, 36 | "devDependencies": { 37 | "bankai": "^9.1.0", 38 | "browserify": "^14.5.0", 39 | "dependency-check": "^2.9.2", 40 | "standard": "^10.0.3", 41 | "tape": "^4.8.0", 42 | "tape-run": "^3.0.1", 43 | "tinyify": "^2.4.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/nanodrag.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const Nanodrag = require('../nanodrag') 3 | 4 | function makeEl () { 5 | const d = document.createElement('div') 6 | d.style.cssText = 'width: 3px; height: 3px; position: absolute;' 7 | return d 8 | } 9 | 10 | function createTouch (x, y, el, type) { 11 | const t = new window.Touch({ 12 | identifier: Date.now(), 13 | clientX: x, 14 | clientY: y, 15 | pageX: x, 16 | pageY: y, 17 | target: el 18 | }) 19 | 20 | const evt = new window.TouchEvent(`touch${type}`, { 21 | touches: [t], 22 | changedTouches: [t] 23 | }) 24 | 25 | return evt 26 | } 27 | 28 | test('init and close', (t) => { 29 | t.plan(4) 30 | const el = makeEl() 31 | document.body.appendChild(el) 32 | const nd = Nanodrag(el) 33 | nd.on('start', (data) => { 34 | t.equal(data.start.x, 2, 'touch start x') 35 | t.equal(data.start.y, 2, 'touch start y') 36 | t.ok(nd._active, 'is active') 37 | }) 38 | const evt = createTouch(2, 2, el, 'start') 39 | el.dispatchEvent(evt) 40 | nd.close() 41 | el.dispatchEvent(evt) 42 | t.ok(!nd._active, 'not active') 43 | el.remove() 44 | }) 45 | 46 | test('with a string', (t) => { 47 | t.plan(1) 48 | const el = makeEl() 49 | el.id = 'drag-tester' 50 | document.body.appendChild(el) 51 | const nd = new Nanodrag('#drag-tester') 52 | const evt = createTouch(2, 2, el, 'start') 53 | el.dispatchEvent(evt) 54 | t.ok(nd._active, 'active') 55 | nd.close() 56 | el.remove() 57 | }) 58 | 59 | test('throws', (t) => { 60 | t.plan(1) 61 | t.throws(() => { 62 | Nanodrag('boop') 63 | }, 'boop throws') 64 | }) 65 | 66 | test('tracks direction', (t) => { 67 | t.plan(12) 68 | const el = makeEl() 69 | document.body.appendChild(el) 70 | const nd = new Nanodrag(el) 71 | 72 | nd.on('start', () => { 73 | const dragEvent = createTouch(3, 3, el, 'move') 74 | t.ok(nd._active, 'is active') 75 | el.dispatchEvent(dragEvent) 76 | }) 77 | 78 | nd.on('move', (data) => { 79 | t.equal(data.start.x, 2, 'start at 2') 80 | t.equal(data.start.y, 2, 'start at 2') 81 | t.equal(data.current.x, 3, 'move to 3') 82 | t.equal(data.current.y, 3, 'move to 3') 83 | t.equal(data.direction.x, 'right') 84 | t.equal(data.direction.y, 'down') 85 | const endEvent = createTouch(3, 3, el, 'end') 86 | el.dispatchEvent(endEvent) 87 | }) 88 | 89 | nd.on('end', (data) => { 90 | t.ok(!nd._active, 'not active') 91 | t.equal(data.start.x, 2, 'start at 2') 92 | t.equal(data.start.y, 2, 'start at 2') 93 | t.equal(data.end.x, 3, 'end at 3') 94 | t.equal(data.end.y, 3, 'end at 3') 95 | nd.close() 96 | el.remove() 97 | }) 98 | 99 | const startEvent = createTouch(2, 2, el, 'start') 100 | el.dispatchEvent(startEvent) 101 | }) 102 | --------------------------------------------------------------------------------