OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * Click buster implementation, which is a behavior that prevents native clicks | |
7 * from firing at undesirable times. There are two scenarios where we may want | |
8 * to 'bust' a click. | |
9 * | |
10 * Buttons implemented with touch events usually have click handlers as well. | |
11 * This is because sometimes touch events stop working, and the click handler | |
12 * serves as a fallback. Here we use a click buster to prevent the native click | |
13 * from firing if the touchend event was succesfully handled. | |
14 * | |
15 * When native scrolling behavior is disabled (see Scroller), click events will | |
16 * fire after the touchend event when the drag sequence is complete. The click | |
17 * event also happens to fire at the location of the touchstart event which can | |
18 * lead to some very strange behavior. | |
19 * | |
20 * This class puts a single click handler on the body, and calls preventDefault | |
21 * on the click event if we detect that there was a touchend event that already | |
22 * fired in the same spot recently. | |
23 */ | |
24 class ClickBuster { | |
25 /** | |
26 * The threshold for how long we allow a click to occur after a touchstart. | |
27 */ | |
28 static final _TIME_THRESHOLD = 2500; | |
29 | |
30 /** | |
31 * The threshold for how close a click has to be to the saved coordinate for | |
32 * us to allow it. | |
33 */ | |
34 static final _DISTANCE_THRESHOLD = 25; | |
35 | |
36 /** | |
37 * The list of coordinates that we use to measure the distance of clicks from. | |
38 * If a click is within the distance threshold of any of these coordinates | |
39 * then we allow the click. | |
40 * TODO(ngeoffray): Should be DoubleLinkedQueue<num> | |
41 */ | |
42 static var _coordinates; | |
43 | |
44 /** The last time preventGhostClick was called. */ | |
45 static int _lastPreventedTime; | |
46 | |
47 /** | |
48 * This handler will prevent the default behavior for any clicks unless the | |
49 * click is within the distance threshold of one of the temporary allowed | |
50 * coordinates. | |
51 */ | |
52 static void _onClick(Event e) { | |
53 if (TimeUtil.now() - _lastPreventedTime > _TIME_THRESHOLD) { | |
54 return; | |
55 } | |
56 final coord = new Coordinate.fromClient(e); | |
57 // TODO(rnystrom): On Android, we get spurious click events at (0, 0). We | |
58 // *do* want those clicks to be busted, so commenting this out fixes it. | |
59 // Leaving it commented out instead of just deleting it because I'm not sure | |
60 // what this code was intended to do to begin with. | |
61 /* | |
62 if (coord.x < 1 && coord.y < 1) { | |
63 // TODO(jacobr): implement a configurable logging framework. | |
64 // _logger.warning( | |
65 // "Not busting click on label elem at(${coord.x}, ${coord.y})"); | |
66 return; | |
67 } | |
68 */ | |
69 var entry = _coordinates.firstEntry(); | |
70 while (entry != null) { | |
71 if (_hitTest(entry.element, | |
72 entry.nextEntry().element, | |
73 coord.x, | |
74 coord.y)) { | |
75 entry.nextEntry().remove(); | |
76 entry.remove(); | |
77 return; | |
78 } else { | |
79 entry = entry.nextEntry().nextEntry(); | |
80 } | |
81 } | |
82 | |
83 // TODO(jacobr): implement a configurable logging framework. | |
84 // _logger.warning("busting click at ${coord.x}, ${coord.y}"); | |
85 e.stopPropagation(); | |
86 e.preventDefault(); | |
87 } | |
88 | |
89 /** | |
90 * This handler will temporarily allow a click to occur near the touch event's | |
91 * coordinates. | |
92 */ | |
93 static void _onTouchStart(Event e) { | |
94 TouchEvent te = e; | |
95 final coord = new Coordinate.fromClient(te.touches[0]); | |
96 _coordinates.add(coord.x); | |
97 _coordinates.add(coord.y); | |
98 window.setTimeout(() { | |
99 _removeCoordinate(coord.x, coord.y); | |
100 }, _TIME_THRESHOLD); | |
101 _toggleTapHighlights(true); | |
102 } | |
103 | |
104 /** | |
105 * Hit test for whether a coordinate is within the distance threshold of an | |
106 * event. | |
107 */ | |
108 static bool _hitTest(num x, num y, num eventX, num eventY) { | |
109 return (eventX - x).abs() < _DISTANCE_THRESHOLD && | |
110 (eventY - y).abs() < _DISTANCE_THRESHOLD; | |
111 } | |
112 | |
113 /** | |
114 * Remove one specified coordinate from the coordinates list. | |
115 */ | |
116 static void _removeCoordinate(num x, num y) { | |
117 var entry = _coordinates.firstEntry(); | |
118 while (entry != null) { | |
119 if (entry.element == x && entry.nextEntry().element == y) { | |
120 entry.nextEntry().remove(); | |
121 entry.remove(); | |
122 return; | |
123 } else { | |
124 entry = entry.nextEntry().nextEntry(); | |
125 } | |
126 } | |
127 } | |
128 | |
129 /** | |
130 * Enable or disable tap highlights. They are disabled when preventGhostClick | |
131 * is called so that the flicker on links is not invoked when the ghost click | |
132 * does fire. This is due to a bug: links get highlighted even if the click | |
133 * event has preventDefault called on it. | |
134 */ | |
135 static void _toggleTapHighlights(bool enable) { | |
136 document.body.style.setProperty( | |
137 "-webkit-tap-highlight-color", enable ? "" : "rgba(0,0,0,0)", ""); | |
138 } | |
139 | |
140 /** | |
141 * Registers new touches to create temporary "allowable zones" and registers | |
142 * new clicks to be prevented unless they fall in one of the current | |
143 * "allowable zones". Note that if the touchstart and touchend locations are | |
144 * different, it is still possible for a ghost click to be fired if you | |
145 * called preventDefault on all touchmove events. In this case the ghost | |
146 * click will be fired at the location of the touchstart event, so the | |
147 * coordinate you pass in should be the coordinate of the touchstart. | |
148 */ | |
149 static void preventGhostClick(num x, num y) { | |
150 // First time this is called the following occurs: | |
151 // 1) Attaches a handler to touchstart events so that each touch will | |
152 // temporarily create an "allowable zone" for clicks to occur in. | |
153 // 2) Attaches a handler to click events so that each click will be | |
154 // prevented unless it is in an "allowable zone". | |
155 // | |
156 // Every time this is called (including the first) the following occurs: | |
157 // 1) Removes an allowable zone that contains the specified coordinate. | |
158 // | |
159 // How this enables click busting: | |
160 // 1) User performs first click. | |
161 // - No attached touchstart handler yet. | |
162 // - preventGhostClick is called before the click event occurs, it | |
163 // attaches the touchstart and click handlers. | |
164 // - The click handler captures the user's click event and prevents it | |
165 // from propagating since there is no "allowable zone". | |
166 // | |
167 // 2) User performs subsequent, to-be-busted click. | |
168 // - touchstart event triggers the attached handler and creates a | |
169 // temporary "allowable zone". | |
170 // - preventGhostClick is called and removes the "allowable zone". | |
171 // - The click handler captures the user's click event and prevents it | |
172 // from propagating since there is no "allowable zone". | |
173 // | |
174 // 3) User performs a should-not-be-busted click. | |
175 // - touchstart event triggers the attached handler and creates a | |
176 // temporary "allowable zone". | |
177 // - The click handler captures the user's click event and allows it to | |
178 // propagate since the click falls in the "allowable zone". | |
179 if (_coordinates === null) { | |
180 // Listen to clicks on capture phase so they can be busted before anything | |
181 // else gets a chance to handle them. | |
182 document.on.click.add((e) { _onClick(e); }, true); | |
183 document.on.focus.add((e) { _lastPreventedTime = 0; }, true); | |
184 | |
185 // Listen to touchstart on capture phase since it must be called prior to | |
186 // every click or else we will accidentally prevent the click even if we | |
187 // don't call preventGhostClick. | |
188 Function startFn = (e) { _onTouchStart(e); }; | |
189 if (!Device.supportsTouch) { | |
190 startFn = mouseToTouchCallback(startFn); | |
191 } | |
192 EventUtil.observe(document, | |
193 Device.supportsTouch ? document.on.touchStart : document.on.mouseDown, | |
194 startFn, true, true); | |
195 _coordinates = new Queue<num>(); | |
196 } | |
197 | |
198 // Turn tap highlights off until we know the ghost click has fired. | |
199 _toggleTapHighlights(false); | |
200 | |
201 // Above all other rules, we won't bust any clicks if there wasn't some call | |
202 // to preventGhostClick in the last time threshold. | |
203 _lastPreventedTime = TimeUtil.now(); | |
204 var entry = _coordinates.firstEntry(); | |
205 while (entry != null) { | |
206 if (_hitTest(entry.element, entry.nextEntry().element, x, y)) { | |
207 entry.nextEntry().remove(); | |
208 entry.remove(); | |
209 return; | |
210 } else { | |
211 entry = entry.nextEntry().nextEntry(); | |
212 } | |
213 } | |
214 } | |
215 } | |
OLD | NEW |