| 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 |