├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── demo.js ├── index.html ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test 7 | demo.js 8 | test.js 9 | demo/ 10 | .npmignore 11 | index.html 12 | LICENSE.md -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 Jam3 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # touches 2 | 3 | [![stable](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) 4 | 5 | [(click for demo)](http://jam3.github.io/touches/) - [(source)](demo.js) 6 | 7 | Normalizes touch and mouse events to provide a simpler interface. Simplest case: 8 | 9 | ```js 10 | //get mouse/touch events on window 11 | require('touches')() 12 | .on('start', mouseDown) //-> mousedown / touchstart 13 | .on('move', mouseMove) //-> mousemove / touchmove 14 | .on('end', mouseEnd) //-> mouseup / touchend 15 | 16 | ... 17 | ``` 18 | 19 | A common pattern for drag events is to listen for events on a parent element (like the `window`), and use a different element as the `target` for client offset calculation. The second argument to the event listener is a `[x, y]` vector representing the calculated client offset (relative to top left of target element). 20 | 21 | ```js 22 | var touch = require('touches') 23 | touch(window, { target: button }) 24 | .on('move', function (ev, position) { 25 | console.log('relative pos', position[0], position[1]) 26 | }) 27 | ``` 28 | 29 | Another common pattern, especially with drag events, is filtering touch input to a single finger. Below; the events will only get fired for the first finger placed on the screen. Subsequent fingers will be ignored until after the first finger has been lifted. 30 | 31 | ```js 32 | touch(window, { target: button, filtered: true }) 33 | .on('start', dragStart) 34 | .on('move', dragMove) 35 | .on('end', dragEnd) 36 | ``` 37 | 38 | ## Usage 39 | 40 | [![NPM](https://nodei.co/npm/touches.png)](https://www.npmjs.com/package/touches) 41 | 42 | #### `emitter = require('touches')([element, opt])` 43 | 44 | Creates a new drag emitter by attaching listeners to `element`, which defaults to `window`. 45 | 46 | The `opt` options can be: 47 | 48 | - `target` the element to use when calculating the `position` parameter passed to event listeners. The clientX/clientY of the event will be relative to this target 49 | - `filtered` whether the touch events should be filtered to the first placed finger 50 | - `type` can be a string, either `"mouse"` or `"touch"` if listening to only one or the other event is desired. If any other value, will listen for both mouse and touch. 51 | - `preventSimulated` (default `true`) if true, prevents simulated touch events by running `ev.preventDefault()` on `'touchend'` events 52 | 53 | 54 | If the events are not filtered, the `position` for an event will be the first changed touch associated with the `target`. 55 | 56 | #### `emitter.disable()` 57 | 58 | Disables the events associated with this emitter by removing them from the DOM element. Returns the emitter for chaining. 59 | 60 | #### `emitter.enable()` 61 | 62 | Enables the events associated with this emitter by adding them to the DOM element. The emitter is enabled by default. Returns the emitter for chaining. 63 | 64 | #### `emitter.target` 65 | 66 | The current target for position offset calculation. 67 | 68 | #### `emitter.on('start', listener)` 69 | #### `emitter.on('move', listener)` 70 | #### `emitter.on('end', listener)` 71 | 72 | The mousedown/touchstart, mousemove/touchmove, and mouseup/touchend events, respectively. Listeners are called with two parameters: `(ev, position)` where `ev` is the event and `position` is an `[x, y]` array of the client offset, relative to the target's top left. 73 | 74 | ## demo 75 | 76 | To run the demo from source, first `git clone` this repo, then: 77 | 78 | ```sh 79 | cd touches 80 | npm install 81 | npm start 82 | ``` 83 | 84 | And open `localhost:9966` in your browser. 85 | 86 | To generate a distribution bundle: 87 | 88 | ```sh 89 | npm run build 90 | ``` 91 | 92 | ## License 93 | 94 | MIT, see [LICENSE.md](http://github.com/Jam3/touches/blob/master/LICENSE.md) for details. 95 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | var touches = require('./') 2 | var css = require('dom-css') 3 | var xtend = require('xtend') 4 | 5 | require('domready')(function () { 6 | var body = document.body 7 | 8 | // create a simple box element 9 | var div = create() 10 | div.textContent = 'drag me!' 11 | body.appendChild(div) 12 | 13 | var child = create({ left: 250, background: 'red' }) 14 | body.appendChild(child) 15 | 16 | // start listening for drag events on window 17 | // but use the div as our target element for position 18 | var dragging = false 19 | touches(window, { target: div, filtered: true }) 20 | .on('start', function (ev, pos) { 21 | ev.preventDefault() 22 | dragging = within(pos, div) 23 | if (dragging) write('start', pos) 24 | }) 25 | .on('end', function (ev, pos) { 26 | if (dragging) write('end', pos) 27 | dragging = false 28 | }) 29 | .on('move', function (ev, pos) { 30 | if (dragging) write('move', pos) 31 | }) 32 | 33 | function write (msg, pos) { 34 | div.textContent = [msg, pos.map(Math.round).join(', ')].join(' ') 35 | } 36 | }) 37 | 38 | function within (pos, element) { 39 | var rect = element.getBoundingClientRect() 40 | return pos[0] >= 0 && pos[1] >= 0 && 41 | pos[0] < rect.width && pos[1] < rect.height 42 | } 43 | 44 | function create (opt) { 45 | var div = document.createElement('div') 46 | css(div, xtend({ 47 | position: 'absolute', 48 | top: 20, 49 | left: 20, 50 | background: 'gray', 51 | width: 200, 52 | height: 200, 53 | color: 'white', 54 | fontSize: '30px', 55 | lineHeight: '200px', 56 | textAlign: 'center' 57 | }, opt)) 58 | return div 59 | } 60 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | touches 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Emitter = require('events/') 2 | 3 | var allEvents = [ 4 | 'touchstart', 'touchmove', 'touchend', 'touchcancel', 5 | 'mousedown', 'mousemove', 'mouseup' 6 | ] 7 | 8 | var ROOT = { left: 0, top: 0 } 9 | 10 | module.exports = function handler (element, opt) { 11 | opt = opt || {} 12 | element = element || window 13 | 14 | var emitter = new Emitter() 15 | emitter.target = opt.target || element 16 | 17 | var touch = null 18 | var filtered = opt.filtered 19 | 20 | var events = allEvents 21 | 22 | // only a subset of events 23 | if (typeof opt.type === 'string') { 24 | events = allEvents.filter(function (type) { 25 | return type.indexOf(opt.type) === 0 26 | }) 27 | } 28 | 29 | // grab the event functions 30 | var funcs = events.map(function (type) { 31 | var name = normalize(type) 32 | var fn = function (ev) { 33 | var client = ev 34 | if (/^touch/.test(type)) { 35 | if (/^touchend$/.test(type) && opt.preventSimulated !== false) { 36 | ev.preventDefault() 37 | } 38 | 39 | if (filtered) { 40 | client = getFilteredTouch(ev, type) 41 | } else { 42 | client = getTargetTouch(ev.changedTouches, emitter.target) 43 | } 44 | } 45 | 46 | if (!client) { 47 | return 48 | } 49 | 50 | // get 2D position 51 | var pos = offset(client, emitter.target) 52 | 53 | // dispatch the normalized event to our emitter 54 | emitter.emit(name, ev, pos) 55 | } 56 | return { type: type, listener: fn } 57 | }) 58 | 59 | emitter.enable = function enable () { 60 | funcs.forEach(listeners(element, true)) 61 | 62 | return emitter 63 | } 64 | 65 | emitter.disable = function dispose () { 66 | touch = null 67 | funcs.forEach(listeners(element, false)) 68 | 69 | return emitter 70 | } 71 | 72 | // initially enabled 73 | emitter.enable() 74 | return emitter 75 | 76 | function getFilteredTouch (ev, type) { 77 | var client 78 | 79 | // clear touch if it was lifted or canceled 80 | if (touch && /^touch(end|cancel)/.test(type)) { 81 | // allow end event to trigger on tracked touch 82 | client = getTouch(ev.changedTouches, touch.identifier || 0) 83 | if (client) { 84 | touch = null 85 | } 86 | } else if (!touch && /^touchstart/.test(type)) { 87 | // not yet tracking any touches, pick one from target 88 | touch = client = getTargetTouch(ev.changedTouches, emitter.target) 89 | } else if (touch) { 90 | // get the tracked touch 91 | client = getTouch(ev.changedTouches, touch.identifier || 0) 92 | } 93 | 94 | return client 95 | } 96 | } 97 | 98 | // get 2D client position of touch/mouse event 99 | function offset (ev, target) { 100 | var cx = ev.clientX || 0 101 | var cy = ev.clientY || 0 102 | var rect = bounds(target) 103 | return [ cx - rect.left, cy - rect.top ] 104 | } 105 | 106 | // since we are adding events to a parent we can't rely on targetTouches 107 | function getTargetTouch (touches, target) { 108 | return Array.prototype.slice.call(touches).filter(function (t) { 109 | return t.target === target 110 | })[0] || touches[0] 111 | } 112 | 113 | function getTouch (touches, id) { 114 | for (var i = 0; i < touches.length; i++) { 115 | if (touches[i].identifier === id) { 116 | return touches[i] 117 | } 118 | } 119 | return null 120 | } 121 | 122 | function listeners (e, enabled) { 123 | return function (data) { 124 | if (enabled) e.addEventListener(data.type, data.listener, { passive: false }) 125 | else e.removeEventListener(data.type, data.listener, { passive: false }) 126 | } 127 | } 128 | 129 | // normalize touchstart/mousedown to "start" etc 130 | function normalize (event) { 131 | return event.replace(/^(touch|mouse)/, '') 132 | .replace(/up$/, 'end') 133 | .replace(/down$/, 'start') 134 | } 135 | 136 | function bounds (element) { 137 | if (element === window || 138 | element === document || 139 | element === document.body) { 140 | return ROOT 141 | } else { 142 | return element.getBoundingClientRect() 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "touches", 3 | "version": "1.2.2", 4 | "description": "simplified touch/mouse events for flick and swipe", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Matt DesLauriers", 9 | "email": "dave.des@gmail.com", 10 | "url": "https://github.com/mattdesl" 11 | }, 12 | "dependencies": { 13 | "events": "^1.0.2" 14 | }, 15 | "devDependencies": { 16 | "browserify": "^8.1.3", 17 | "budo": "^5.0.0-beta", 18 | "dom-css": "^1.0.6", 19 | "domready": "^1.0.7", 20 | "garnish": "^3.0.0", 21 | "standard": "^5.2.2", 22 | "uglify-js": "^2.4.16", 23 | "wzrd": "^1.2.1", 24 | "xtend": "^4.0.0" 25 | }, 26 | "scripts": { 27 | "start": "budo demo.js:bundle.js --host=`internal-ip` --live | garnish", 28 | "build": "browserify demo.js | uglifyjs -cm > bundle.js", 29 | "test": "standard" 30 | }, 31 | "keywords": [ 32 | "touch", 33 | "drag", 34 | "unified", 35 | "mouse", 36 | "input", 37 | "pointer", 38 | "move", 39 | "mousemove", 40 | "touchmove", 41 | "touchstart", 42 | "touchend", 43 | "mouseup", 44 | "mousedown" 45 | ], 46 | "repository": { 47 | "type": "git", 48 | "url": "git://github.com/Jam3/touches.git" 49 | }, 50 | "homepage": "https://github.com/Jam3/touches", 51 | "bugs": { 52 | "url": "https://github.com/Jam3/touches/issues" 53 | } 54 | } 55 | --------------------------------------------------------------------------------