├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demo ├── demo.js └── index.html ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | demo/demo-bundle.js 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.0 4 | 5 | - Add `useCapture` argument to `createTapListener`. 6 | 7 | ## 0.1.0 8 | 9 | - Initial release. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 David Clark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # teeny-tap 2 | 3 | Listen for both clicks and click-like touches (not scrolls or drags). 4 | 5 | This library is very small and simple and focused, without any dependencies. 6 | It is not a touch gesture library or a complete fastclick-style solution. 7 | Plenty of similar libraries exist, but none of them seemed exactly right for 8 | my simple needs. For more about the purpose of the library, read ["Why?"](#why). 9 | 10 | This library is heavily inspired by [tap.js](https://github.com/alexgibson/tap.js), 11 | which is no longer maintained. 12 | 13 | **[Check out the demo](http://davidtheclark.github.io/teeny-tap/demo/).** 14 | 15 | (If the demo works on your phone, would you mind tweeting me your specs, 16 | `@davidtheclark`? That way I can list the devices where we know it works. 17 | And of course let me know if you find any bugs!) 18 | 19 | ## Installation 20 | 21 | ``` 22 | npm install teeny-tap 23 | ``` 24 | 25 | You will need to be compiling CommonJS modules (browserify or webpack). 26 | 27 | ### Browser Support 28 | 29 | IE9+ and everything help, I hope. Testing underway. 30 | (If you can help out by trying the demo on your mobile device, please do!) 31 | 32 | ## Usage 33 | 34 | ```js 35 | var createTapListener = require('teeny-tap'); 36 | 37 | var docTapListener = createTapListener(document.documentElement, function(e) { 38 | console.log('A tap happened!'); 39 | }); 40 | 41 | // ... 42 | docTapListener.remove(); 43 | ``` 44 | 45 | That's it. Very simple. You create and remove listeners. 46 | 47 | ### API 48 | 49 | #### `var tapListenerInstance = createTapListener(element, callback[, useCapture])` 50 | 51 | Adds a tap listener on `element`, using `addEventListener()`. 52 | When there's a tap, `callback` is invoked with 53 | the relevant `event` as its argument (either a `click` or `touchend` event). 54 | 55 | **Returns an object with a `remove` function, for removing the listener.** 56 | 57 | #### `tapListenerInstance.remove()` 58 | 59 | Remove the listener that you added when you created `tapListenerInstance`. 60 | 61 | ## Why? 62 | 63 | For 64 | [react-aria-menubutton](https://github.com/davidtheclark/react-aria-menubutton), 65 | I need to close an open menu when the user taps/clicks outside of it. 66 | 67 | The click event wasn't reliable: mobile Safari screws it up. 68 | So I needed to **distinguish, on touch devices, between *touches 69 | that are scrolling or dragging* and *touches that are clicking*, 70 | in situations where the regular `click` event doesn't work**. 71 | Existing solutions that I found weren't satisfactory for a few reasons: 72 | 73 | - jQuery or other dependencies I didn't want or need. 74 | 75 | - Many just threw in a `touchstart` or `touchend` 76 | listener, in addition to the click; but that alone won't distinguish 77 | between tapping and scrolling/dragging, so it's insufficient. 78 | The menu might close when you, say, scroll down in order to see more of it. 79 | Egad! 80 | 81 | - One solution to the detect-clicks-outside-an-element problem is to add an 82 | underlay that covers the page, beneath the element, and listen for clicks on that. 83 | However, this prevents the click from getting through and actually 84 | *doing* something; so if I click a link outside the open menu, 85 | that click only closes the menu, and I have to click *again* to 86 | go where I wanted to go. Non-optimal. 87 | 88 | - A number of solutions just use `click` events, which won't 89 | work on most browsers on most iPhones and iPads, due to 90 | mobile Safari's unique and unpleasant handling of click events. 91 | 92 | - Some went the other way, and had more fine-tuning than I want or need. 93 | 94 | [tap.js](https://github.com/alexgibson/tap.js) pretty much provided exactly what I needed, 95 | but that library is no longer maintained so I made this one to carry the torch, 96 | to fill the need. 97 | 98 | But know that *I do not like that this exists, do not want it to need to exist*. 99 | If you know of a way to accomplish the same goal without this library, 100 | please let me know! 101 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | var tapListener = require('..'); 2 | var count = 0; 3 | var docTapListener; 4 | 5 | document.getElementById('init').addEventListener('click', function(e) { 6 | e.stopPropagation(); 7 | if (docTapListener) return; 8 | docTapListener = true; 9 | docTapListener = tapListener(document.documentElement, registerTap); 10 | }); 11 | 12 | document.getElementById('remove').addEventListener('click', function(e) { 13 | e.stopPropagation(); 14 | if (docTapListener) docTapListener.remove(); 15 | docTapListener = null; 16 | }); 17 | 18 | function registerTap(e) { 19 | console.log(e.type); 20 | count++; 21 | document.getElementById('count').innerHTML = count; 22 | } 23 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | tap-listener demo 8 | 9 | 30 | 31 | 32 |

tap-listener demo

33 | 34 |

35 | This library's purpose is to distinguish on touch devices between 36 | taps and non-taps (e.g. scrolls and drags) in situations where 37 | the "click" event will not work. 38 |

39 | 40 |

41 | If you find anything wrong in this demo, please file a bugs 42 | back in the repo. 43 |

44 | 45 |

46 | Click this button to initiate the tap listener: 47 |

48 | 49 |

50 | 53 |

54 | 55 |

56 | Every time you tap (or click), the number at the bottom of the screen should increment. 57 | When you touch-scroll or -drag, it should not. 58 |

59 | 60 |

61 | Also, try clicking this button to remove the tap listener: 62 |

63 | 64 |

65 | 68 |

69 | 70 |

71 | And here is a lot of space to try scrolling in. 72 | (Scrolls should not cause tap events.) 73 |

74 | 75 |
76 | tap count: 77 | 78 | 0 79 | 80 |
81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = function createTapListener(el, callback, useCapture) { 2 | var startX = 0; 3 | var startY = 0; 4 | var touchStarted = false; 5 | var touchMoved = false; 6 | // Assume that if a touchstart event initiates, the user is 7 | // using touch and click events should be ignored. 8 | // If this isn't done, touch-clicks will fire the callback 9 | // twice: once on touchend, once on the subsequent "click". 10 | var usingTouch = false; 11 | 12 | el.addEventListener('click', handleClick, useCapture); 13 | el.addEventListener('touchstart', handleTouchstart, useCapture); 14 | 15 | function handleClick(e) { 16 | if (usingTouch) return; 17 | callback(e); 18 | } 19 | 20 | function handleTouchstart(e) { 21 | usingTouch = true; 22 | 23 | if (touchStarted) return; 24 | touchStarted = true; 25 | 26 | el.addEventListener('touchmove', handleTouchmove, useCapture); 27 | el.addEventListener('touchend', handleTouchend, useCapture); 28 | el.addEventListener('touchcancel', handleTouchcancel, useCapture); 29 | 30 | touchMoved = false; 31 | startX = e.touches[0].clientX; 32 | startY = e.touches[0].clientY; 33 | } 34 | 35 | function handleTouchmove(e) { 36 | if (touchMoved) return; 37 | 38 | if ( 39 | Math.abs(e.touches[0].clientX - startX) <= 10 40 | && Math.abs(e.touches[0].clientY - startY) <= 10 41 | ) return; 42 | 43 | touchMoved = true; 44 | } 45 | 46 | function handleTouchend(e) { 47 | touchStarted = false; 48 | removeSecondaryTouchListeners(); 49 | if (!touchMoved) { 50 | callback(e); 51 | } 52 | } 53 | 54 | function handleTouchcancel() { 55 | touchStarted = false; 56 | touchMoved = false; 57 | startX = 0; 58 | startY = 0; 59 | } 60 | 61 | function removeSecondaryTouchListeners() { 62 | el.removeEventListener('touchmove', handleTouchmove, useCapture); 63 | el.removeEventListener('touchend', handleTouchend, useCapture); 64 | el.removeEventListener('touchcancel', handleTouchcancel, useCapture); 65 | } 66 | 67 | function removeTapListener() { 68 | el.removeEventListener('click', handleClick, useCapture); 69 | el.removeEventListener('touchstart', handleTouchstart, useCapture); 70 | removeSecondaryTouchListeners(); 71 | } 72 | 73 | return { 74 | remove: removeTapListener, 75 | }; 76 | }; 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teeny-tap", 3 | "version": "0.2.0", 4 | "description": "Listen for both clicks and click-like touches (not scrolls or drags)", 5 | "main": "index.js", 6 | "scripts": { 7 | "demo-dev": "watchify demo/demo.js -o demo/demo-bundle.js -v & http-server demo", 8 | "demo-bundle": "browserify demo/demo.js > demo/demo-bundle.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/davidtheclark/teeny-tap.git" 14 | }, 15 | "keywords": [ 16 | "tap", 17 | "click", 18 | "mobile", 19 | "touch", 20 | "event", 21 | "listener" 22 | ], 23 | "author": "David Clark", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/davidtheclark/teeny-tap/issues" 27 | }, 28 | "homepage": "https://github.com/davidtheclark/teeny-tap#readme", 29 | "devDependencies": { 30 | "browserify": "13.0.0", 31 | "http-server": "0.8.5", 32 | "watchify": "3.7.0" 33 | } 34 | } 35 | --------------------------------------------------------------------------------