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