├── .gitignore
├── .npmignore
├── index.html
├── LICENSE.md
├── package.json
├── demo.js
├── README.md
└── index.js
/.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
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | touches
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # touches
2 |
3 | [](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 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------