Skip to content

Commit 23049c9

Browse files
committed
Merge pull request #17 from JakeSidSmith/refactor-for-0.14
Refactor to work with React 0.14 (and older)
2 parents a6b4d4a + 8aca188 commit 23049c9

3 files changed

Lines changed: 235 additions & 117 deletions

File tree

README.md

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
**Instantly make your desktop / hybrid apps more responsive on touch devices.**
55

6-
React Fastclick automatically adds fastclick touch events to elements with onClick attributes to prevent the delay that occurs on some touch devices.
6+
React Fastclick automatically adds fastclick touch events to elements with onClick attributes (and those that require special functionality, such as inputs) to prevent the delay that occurs on some touch devices.
77

88
## Installation
99

@@ -15,41 +15,45 @@ npm install react-fastclick
1515

1616
## Usage
1717

18-
Include react-fastclick in your main javascript file before any of your components are created, and you're done.
18+
Include `react-fastclick` in your main javascript file before any of your components are created, and you're done.
1919

20-
Now any calls to onClick will have fast touch events added automatically - no need to write any additional listeners.
20+
Now any calls to onClick or elements with special functionality, such as inputs, will have fast touch events added automatically - no need to write any additional listeners.
2121

22-
Example:
22+
**ES6**
2323

2424
```javascript
25-
'use strict';
25+
import 'react-fastclick';
26+
```
2627

27-
require('react-fastclick');
28-
var React = require('react');
29-
30-
var App = React.createClass({
31-
logEventType: function (event) {
32-
console.log(event.type);
33-
},
34-
render: function() {
35-
return (
36-
<p onClick={this.logEventType}>
37-
Hello, world!
38-
</p>
39-
);
40-
}
41-
});
28+
**ES5**
4229

43-
React.render(<App />, document.body);
30+
```javascript
31+
require('react-fastclick');
4432
```
4533

4634
## Notes
4735

48-
1. The event triggered on touch devices is currently the same event for `touchend`, and will have `event.type` `touchend`. This also means that it wont have any mouse / touch coordinates (e.g. `event.touches`, `clientX`, `pageX`).
49-
50-
I will be creating synthetic events for these shortly with the most recent touch / mouse coords.
51-
52-
See this [issue](https://github.com/JakeSidSmith/react-fastclick/issues/4)
36+
1. The event triggered on touch devices is a modified `touchend` event. This means that it may have some keys that are unusual for a click event.
37+
38+
In order to simulate a click as best as possible, this event is populated with the following keys / values. All positions are taken from the last know touch position.
39+
40+
```javascript
41+
{
42+
// Simulate left click
43+
button: 0,
44+
type: 'click',
45+
// Additional key to tell the difference between
46+
// a regular click and a flastclick
47+
fastclick: true,
48+
// From touch positions
49+
clientX,
50+
clientY,
51+
pageX,
52+
pageY,
53+
screenX,
54+
screenY
55+
}
56+
```
5357

5458
2. On some devices the elements flicker after being touched. This can be prevented by setting the css property `-webkit-tap-highlight-color` to transparent.
5559
Either target `html, body` (to prevent the flickering on all elements) or target the specific element you don't want to flicker e.g. `button`.
@@ -62,4 +66,4 @@ Either target `html, body` (to prevent the flickering on all elements) or target
6266

6367
## Support
6468

65-
Currently only tested with React 0.12.x & 0.13.x
69+
React Fastclick version 2.x.x has been tested with React 0.12, 0.13 and 0.14, and should work with even older versions.

index.js

Lines changed: 201 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,110 +3,224 @@
33
(function () {
44

55
var React = require('react');
6-
var EventListener = require('react/lib/EventListener');
76

8-
React.initializeTouchEvents(true);
7+
var originalCreateElement = React.createElement;
8+
9+
// Moved if Math.abs(downX - upX) > MOVE_THRESHOLD;
10+
var MOVE_THRESHOLD = 8;
11+
var TOUCH_DELAY = 1000;
12+
13+
var touchKeysToStore = [
14+
'clientX',
15+
'clientY',
16+
'pageX',
17+
'pageY',
18+
'screenX',
19+
'screenY',
20+
'radiusX',
21+
'radiusY'
22+
];
23+
24+
var touchEvents = {
25+
downPos: {},
26+
lastPos: {}
27+
};
28+
29+
var focusAndCheck = function (event, target) {
30+
var myTarget = target || event.currentTarget;
31+
myTarget.focus();
32+
33+
switch (myTarget.type) {
34+
case 'checkbox':
35+
myTarget.checked = !myTarget.checked;
36+
event.preventDefault();
37+
break;
38+
case 'radio':
39+
myTarget.checked = true;
40+
event.preventDefault();
41+
break;
42+
}
43+
};
944

10-
// Save original listen method
11-
var listen = EventListener.listen;
45+
var handleType = {
46+
input: function (event) {
47+
focusAndCheck(event);
48+
event.stopPropagation();
49+
},
50+
textarea: function (event) {
51+
focusAndCheck(event);
52+
event.stopPropagation();
53+
},
54+
select: function (event) {
55+
focusAndCheck(event);
56+
event.stopPropagation();
57+
},
58+
label: function (event) {
59+
var input;
60+
61+
var forTarget = event.currentTarget.getAttribute('for');
62+
63+
if (forTarget) {
64+
input = document.getElementById(forTarget);
65+
} else {
66+
input = event.currentTarget.querySelectorAll('input, textarea, select')[0];
67+
}
68+
69+
if (input) {
70+
focusAndCheck(event, input);
71+
}
72+
event.preventDefault();
73+
}
74+
};
1275

13-
var constants = {
14-
touchstart: 'touchstart',
15-
touchend: 'touchend',
16-
touchmove: 'touchmove',
17-
moveThreshold: 20
76+
var fakeClickEvent = function (event) {
77+
event.fastclick = true;
78+
event.type = 'click';
79+
event.button = 0;
1880
};
1981

20-
var addListener = function (target, eventType, callback) {
21-
if (target.addEventListener) {
22-
target.addEventListener(eventType, callback, false);
23-
} else if (target.attachEvent) {
24-
target.attachEvent('on' + eventType, callback, false);
82+
var copyTouchKeys = function (touch, target) {
83+
if (touch) {
84+
for (var i = 0; i < touchKeysToStore.length; i += 1) {
85+
var key = touchKeysToStore[i];
86+
target[key] = touch[key];
87+
}
2588
}
2689
};
2790

28-
var removeListener = function (target, eventType, callback) {
29-
if (target.removeEventListener) {
30-
target.removeEventListener(eventType, callback, false);
31-
} else if (target.detachEvent) {
32-
target.detachEvent('on' + eventType, callback, false);
91+
var noTouchHappened = function () {
92+
return !touchEvents.touched || new Date().getDate() > touchEvents.lastTouchDate + TOUCH_DELAY;
93+
};
94+
95+
var invalidateIfMoreThanOneTouch = function (event) {
96+
touchEvents.invalid = event.touches && event.touches.length > 1 || touchEvents.invalid;
97+
};
98+
99+
var onMouseEvent = function (callback, event) {
100+
// Prevent any mouse events if we touched recently
101+
if (typeof callback === 'function' && noTouchHappened()) {
102+
callback(event);
103+
}
104+
if (event.type === 'click') {
105+
touchEvents.invalid = false;
106+
touchEvents.touched = false;
107+
touchEvents.moved = false;
33108
}
34109
};
35110

36-
// Create new listen method
37-
EventListener.listen = function (target, eventType, callback) {
38-
if (eventType === 'click') {
39-
var downPos;
40-
var touchedTimeout;
41-
var moved = false;
42-
var touched = false;
43-
44-
var onTouchEnd = function (event) {
45-
// Remove touch listeners
46-
removeListener(window, constants.touchend, onTouchEnd);
47-
removeListener(window, constants.touchmove, onTouchMove);
48-
49-
if (!moved) {
50-
// If not moved - callback & prevent mouse events
51-
touched = true;
52-
// Reset touched flag after 500 millis
53-
touchedTimeout = setTimeout(function () {
54-
touched = false;
55-
}, 500);
56-
callback(event);
57-
}
58-
};
59-
60-
var onTouchMove = function (event) {
61-
var touch = event.touches[0];
62-
// Check if touch has moved
63-
if (Math.abs(downPos.clientX - touch.clientX) >= constants.moveThreshold ||
64-
Math.abs(downPos.clientY - touch.clientY) >= constants.moveThreshold) {
65-
moved = true;
111+
var onTouchStart = function (callback, event) {
112+
touchEvents.invalid = false;
113+
touchEvents.moved = false;
114+
touchEvents.touched = true;
115+
touchEvents.lastTouchDate = new Date().getTime();
116+
117+
copyTouchKeys(event.touches[0], touchEvents.downPos);
118+
copyTouchKeys(event.touches[0], touchEvents.lastPos);
119+
120+
invalidateIfMoreThanOneTouch(event);
121+
122+
if (typeof callback === 'function') {
123+
callback(event);
124+
}
125+
};
126+
127+
var onTouchMove = function (callback, event) {
128+
touchEvents.touched = true;
129+
touchEvents.lastTouchDate = new Date().getTime();
130+
131+
copyTouchKeys(event.touches[0], touchEvents.lastPos);
132+
133+
invalidateIfMoreThanOneTouch(event);
134+
135+
if (Math.abs(touchEvents.downPos.clientX - touchEvents.lastPos.clientX) > MOVE_THRESHOLD ||
136+
Math.abs(touchEvents.downPos.clientY - touchEvents.lastPos.clientY) > MOVE_THRESHOLD) {
137+
touchEvents.moved = true;
138+
}
139+
140+
if (typeof callback === 'function') {
141+
callback(event);
142+
}
143+
};
144+
145+
var onTouchEnd = function (callback, onClick, type, event) {
146+
touchEvents.touched = true;
147+
touchEvents.lastTouchDate = new Date().getTime();
148+
149+
invalidateIfMoreThanOneTouch(event);
150+
151+
if (typeof callback === 'function') {
152+
callback(event);
153+
}
154+
155+
if (!touchEvents.invalid && !touchEvents.moved) {
156+
var box = event.currentTarget.getBoundingClientRect();
157+
158+
if (touchEvents.lastPos.clientX - touchEvents.lastPos.radiusX <= box.right &&
159+
touchEvents.lastPos.clientX + touchEvents.lastPos.radiusX >= box.left &&
160+
touchEvents.lastPos.clientY - touchEvents.lastPos.radiusY <= box.bottom &&
161+
touchEvents.lastPos.clientY + touchEvents.lastPos.radiusY >= box.top) {
162+
163+
if (typeof onClick === 'function') {
164+
copyTouchKeys(touchEvents.lastPos, event);
165+
fakeClickEvent(event);
166+
onClick(event);
66167
}
67-
};
68-
69-
var onTouchStart = function (event) {
70-
clearTimeout(touchedTimeout);
71-
var touch = event.touches[0];
72-
// Store initial touch position
73-
downPos = {
74-
clientX: touch.clientX,
75-
clientY: touch.clientY
76-
};
77-
// Reset moved flag
78-
moved = false;
79-
80-
// Add touch listeners
81-
addListener(window, constants.touchend, onTouchEnd);
82-
addListener(window, constants.touchmove, onTouchMove);
83-
};
84-
85-
// Wrap click callback
86-
var onClick = function (event) {
87-
// Do not call if touch has fired or mouse moved
88-
if (!touched && !moved) {
89-
callback(event);
168+
169+
if (!event.defaultPrevented && handleType[type]) {
170+
handleType[type](event);
90171
}
91-
};
172+
}
173+
}
174+
};
92175

93-
// Get original click listener remove function
94-
var originalListener = listen(target, eventType, onClick);
176+
var propsWithFastclickEvents = function (type, props) {
177+
var newProps = {};
95178

96-
addListener(target, constants.touchstart, onTouchStart);
179+
// Loop over props
180+
for (var key in props) {
181+
// Copy props to newProps
182+
newProps[key] = props[key];
183+
}
97184

98-
// Return remove listener functions
99-
return {
100-
remove: function () {
101-
if (originalListener && typeof originalListener.remove === 'function') {
102-
originalListener.remove();
103-
}
104-
removeListener(target, constants.touchstart, onTouchStart);
105-
}
106-
};
107-
} else {
108-
return listen(target, eventType, callback);
185+
// Apply our wrapped mouse and touch handlers
186+
newProps.onClick = onMouseEvent.bind(null, props.onClick);
187+
newProps.onMouseDown = onMouseEvent.bind(null, props.onMouseDown);
188+
newProps.onMouseMove = onMouseEvent.bind(null, props.onMouseMove);
189+
newProps.onMouseUp = onMouseEvent.bind(null, props.onMouseUp);
190+
newProps.onTouchStart = onTouchStart.bind(null, props.onTouchStart);
191+
newProps.onTouchMove = onTouchMove.bind(null, props.onTouchMove);
192+
newProps.onTouchEnd = onTouchEnd.bind(null, props.onTouchEnd, props.onClick, type);
193+
194+
if (typeof Object.freeze === 'function') {
195+
Object.freeze(newProps);
109196
}
197+
198+
return newProps;
110199
};
111200

201+
React.createElement = function () {
202+
// Convert arguments to array
203+
var args = Array.prototype.slice.call(arguments);
204+
205+
var type = args[0];
206+
var props = args[1];
207+
208+
// Check if basic element & has onClick prop
209+
if (type && typeof type === 'string' && (
210+
(props && typeof props.onClick === 'function') || handleType[type]
211+
)) {
212+
// Add our own events to props
213+
args[1] = propsWithFastclickEvents(type, props || {});
214+
}
215+
216+
// Apply args to original createElement function
217+
return originalCreateElement.apply(null, args);
218+
};
219+
220+
if (typeof React.DOM === 'object') {
221+
for (var key in React.DOM) {
222+
React.DOM[key] = React.createElement.bind(null, key);
223+
}
224+
}
225+
112226
})();

0 commit comments

Comments
 (0)