745f112ef6
Opt an initial batch of files under samples, samples-dev, utils, and runtime/tools/dartfuzz out of null safety in preparation for switching the flag on by default. Change-Id: Icdfd52a5a969e678a7205903332f73fe3841c223 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/166960 Reviewed-by: Daco Harkes <dacoharkes@google.com> Commit-Queue: Leaf Petersen <leafp@google.com>
227 lines
8.6 KiB
Dart
227 lines
8.6 KiB
Dart
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
// @dart = 2.9
|
|
|
|
part of touch;
|
|
|
|
/**
|
|
* Click buster implementation, which is a behavior that prevents native clicks
|
|
* from firing at undesirable times. There are two scenarios where we may want
|
|
* to 'bust' a click.
|
|
*
|
|
* Buttons implemented with touch events usually have click handlers as well.
|
|
* This is because sometimes touch events stop working, and the click handler
|
|
* serves as a fallback. Here we use a click buster to prevent the native click
|
|
* from firing if the touchend event was successfully handled.
|
|
*
|
|
* When native scrolling behavior is disabled (see Scroller), click events will
|
|
* fire after the touchend event when the drag sequence is complete. The click
|
|
* event also happens to fire at the location of the touchstart event which can
|
|
* lead to some very strange behavior.
|
|
*
|
|
* This class puts a single click handler on the body, and calls preventDefault
|
|
* on the click event if we detect that there was a touchend event that already
|
|
* fired in the same spot recently.
|
|
*/
|
|
class ClickBuster {
|
|
/**
|
|
* The threshold for how long we allow a click to occur after a touchstart.
|
|
*/
|
|
static const _TIME_THRESHOLD = 2500;
|
|
|
|
/**
|
|
* The threshold for how close a click has to be to the saved coordinate for
|
|
* us to allow it.
|
|
*/
|
|
static const _DISTANCE_THRESHOLD = 25;
|
|
|
|
/**
|
|
* The list of coordinates that we use to measure the distance of clicks from.
|
|
* If a click is within the distance threshold of any of these coordinates
|
|
* then we allow the click.
|
|
*/
|
|
static DoubleLinkedQueue<num> _coordinates;
|
|
|
|
/** The last time preventGhostClick was called. */
|
|
static int _lastPreventedTime;
|
|
|
|
/**
|
|
* This handler will prevent the default behavior for any clicks unless the
|
|
* click is within the distance threshold of one of the temporary allowed
|
|
* coordinates.
|
|
*/
|
|
static void _onClick(Event e) {
|
|
if (TimeUtil.now() - _lastPreventedTime > _TIME_THRESHOLD) {
|
|
return;
|
|
}
|
|
final coord = new Coordinate.fromClient(e);
|
|
// TODO(rnystrom): On Android, we get spurious click events at (0, 0). We
|
|
// *do* want those clicks to be busted, so commenting this out fixes it.
|
|
// Leaving it commented out instead of just deleting it because I'm not sure
|
|
// what this code was intended to do to begin with.
|
|
/*
|
|
if (coord.x < 1 && coord.y < 1) {
|
|
// TODO(jacobr): implement a configurable logging framework.
|
|
// _logger.warning(
|
|
// "Not busting click on label elem at(${coord.x}, ${coord.y})");
|
|
return;
|
|
}
|
|
*/
|
|
DoubleLinkedQueueEntry<num> entry = _coordinates.firstEntry();
|
|
while (entry != null) {
|
|
if (_hitTest(
|
|
entry.element, entry.nextEntry().element, coord.x, coord.y)) {
|
|
entry.nextEntry().remove();
|
|
entry.remove();
|
|
return;
|
|
} else {
|
|
entry = entry.nextEntry().nextEntry();
|
|
}
|
|
}
|
|
|
|
// TODO(jacobr): implement a configurable logging framework.
|
|
// _logger.warning("busting click at ${coord.x}, ${coord.y}");
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
}
|
|
|
|
/**
|
|
* This handler will temporarily allow a click to occur near the touch event's
|
|
* coordinates.
|
|
*/
|
|
static void _onTouchStart(Event e) {
|
|
TouchEvent te = e;
|
|
final coord = new Coordinate.fromClient(te.touches[0]);
|
|
_coordinates.add(coord.x);
|
|
_coordinates.add(coord.y);
|
|
new Timer(const Duration(milliseconds: _TIME_THRESHOLD), () {
|
|
_removeCoordinate(coord.x, coord.y);
|
|
});
|
|
_toggleTapHighlights(true);
|
|
}
|
|
|
|
/**
|
|
* Hit test for whether a coordinate is within the distance threshold of an
|
|
* event.
|
|
*/
|
|
static bool _hitTest(num x, num y, num eventX, num eventY) {
|
|
return (eventX - x).abs() < _DISTANCE_THRESHOLD &&
|
|
(eventY - y).abs() < _DISTANCE_THRESHOLD;
|
|
}
|
|
|
|
/**
|
|
* Remove one specified coordinate from the coordinates list.
|
|
*/
|
|
static void _removeCoordinate(num x, num y) {
|
|
DoubleLinkedQueueEntry<num> entry = _coordinates.firstEntry();
|
|
while (entry != null) {
|
|
if (entry.element == x && entry.nextEntry().element == y) {
|
|
entry.nextEntry().remove();
|
|
entry.remove();
|
|
return;
|
|
} else {
|
|
entry = entry.nextEntry().nextEntry();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable or disable tap highlights. They are disabled when preventGhostClick
|
|
* is called so that the flicker on links is not invoked when the ghost click
|
|
* does fire. This is due to a bug: links get highlighted even if the click
|
|
* event has preventDefault called on it.
|
|
*/
|
|
static void _toggleTapHighlights(bool enable) {
|
|
document.body.style.setProperty(
|
|
"-webkit-tap-highlight-color", enable ? "" : "rgba(0,0,0,0)", "");
|
|
}
|
|
|
|
/**
|
|
* Registers new touches to create temporary "allowable zones" and registers
|
|
* new clicks to be prevented unless they fall in one of the current
|
|
* "allowable zones". Note that if the touchstart and touchend locations are
|
|
* different, it is still possible for a ghost click to be fired if you
|
|
* called preventDefault on all touchmove events. In this case the ghost
|
|
* click will be fired at the location of the touchstart event, so the
|
|
* coordinate you pass in should be the coordinate of the touchstart.
|
|
*/
|
|
static void preventGhostClick(num x, num y) {
|
|
// First time this is called the following occurs:
|
|
// 1) Attaches a handler to touchstart events so that each touch will
|
|
// temporarily create an "allowable zone" for clicks to occur in.
|
|
// 2) Attaches a handler to click events so that each click will be
|
|
// prevented unless it is in an "allowable zone".
|
|
//
|
|
// Every time this is called (including the first) the following occurs:
|
|
// 1) Removes an allowable zone that contains the specified coordinate.
|
|
//
|
|
// How this enables click busting:
|
|
// 1) User performs first click.
|
|
// - No attached touchstart handler yet.
|
|
// - preventGhostClick is called before the click event occurs, it
|
|
// attaches the touchstart and click handlers.
|
|
// - The click handler captures the user's click event and prevents it
|
|
// from propagating since there is no "allowable zone".
|
|
//
|
|
// 2) User performs subsequent, to-be-busted click.
|
|
// - touchstart event triggers the attached handler and creates a
|
|
// temporary "allowable zone".
|
|
// - preventGhostClick is called and removes the "allowable zone".
|
|
// - The click handler captures the user's click event and prevents it
|
|
// from propagating since there is no "allowable zone".
|
|
//
|
|
// 3) User performs a should-not-be-busted click.
|
|
// - touchstart event triggers the attached handler and creates a
|
|
// temporary "allowable zone".
|
|
// - The click handler captures the user's click event and allows it to
|
|
// propagate since the click falls in the "allowable zone".
|
|
if (_coordinates == null) {
|
|
// Listen to clicks on capture phase so they can be busted before anything
|
|
// else gets a chance to handle them.
|
|
Element.clickEvent.forTarget(document, useCapture: true).listen((e) {
|
|
_onClick(e);
|
|
});
|
|
Element.focusEvent.forTarget(document, useCapture: true).listen((e) {
|
|
_lastPreventedTime = 0;
|
|
});
|
|
|
|
// Listen to touchstart on capture phase since it must be called prior to
|
|
// every click or else we will accidentally prevent the click even if we
|
|
// don't call preventGhostClick.
|
|
Function startFn = (e) {
|
|
_onTouchStart(e);
|
|
};
|
|
if (!Device.supportsTouch) {
|
|
startFn = mouseToTouchCallback(startFn);
|
|
}
|
|
var stream;
|
|
if (Device.supportsTouch) {
|
|
stream = Element.touchStartEvent.forTarget(document, useCapture: true);
|
|
} else {
|
|
stream = Element.mouseDownEvent.forTarget(document, useCapture: true);
|
|
}
|
|
EventUtil.observe(document, stream, startFn, true);
|
|
_coordinates = new DoubleLinkedQueue<num>();
|
|
}
|
|
|
|
// Turn tap highlights off until we know the ghost click has fired.
|
|
_toggleTapHighlights(false);
|
|
|
|
// Above all other rules, we won't bust any clicks if there wasn't some call
|
|
// to preventGhostClick in the last time threshold.
|
|
_lastPreventedTime = TimeUtil.now();
|
|
DoubleLinkedQueueEntry<num> entry = _coordinates.firstEntry();
|
|
while (entry != null) {
|
|
if (_hitTest(entry.element, entry.nextEntry().element, x, y)) {
|
|
entry.nextEntry().remove();
|
|
entry.remove();
|
|
return;
|
|
} else {
|
|
entry = entry.nextEntry().nextEntry();
|
|
}
|
|
}
|
|
}
|
|
}
|