Replace inconsistently-supported dblclick events with custom handler on platforms supporting pointer events

Fix issue where double-tap-to-zoom would not be properly disabled when drawing on touch devices (close #2128)
Support adding nodes to ways with double-tap with on touch devices (close #2677)
Support double-tap-to-zoom with styluses on touch devices
Don't accept double click/tap events if the taps are far apart
Don't re-enter modeSelect when clicking the selected feature again
This commit is contained in:
Quincy Morgan
2020-03-06 12:46:26 -08:00
parent 83a51a4192
commit f8f69a777a
9 changed files with 141 additions and 64 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ export function behaviorAddWay(context) {
.on('finish', behavior.cancel);
context.map()
.dblclickEnable(false);
.dblclickZoomEnable(false);
surface.call(draw);
}
@@ -30,7 +30,7 @@ export function behaviorAddWay(context) {
behavior.cancel = function() {
window.setTimeout(function() {
context.map().dblclickEnable(true);
context.map().dblclickZoomEnable(true);
}, 1000);
context.enter(modeBrowse(context));
+7 -7
View File
@@ -64,7 +64,7 @@ export function behaviorDrag() {
}
function dragstart() {
function pointerdown() {
_target = this;
_event = eventOf(_target, arguments);
@@ -75,8 +75,8 @@ export function behaviorDrag() {
var selectEnable = d3_event_userSelectSuppress();
d3_select(window)
.on(_pointerPrefix + 'move.drag', dragmove)
.on(_pointerPrefix + 'up.drag', dragend, true);
.on(_pointerPrefix + 'move.drag', pointermove)
.on(_pointerPrefix + 'up.drag', pointerup, true);
if (_origin) {
offset = _origin.apply(_target, arguments);
@@ -94,7 +94,7 @@ export function behaviorDrag() {
}
function dragmove() {
function pointermove() {
var p = point();
var dx = p[0] - startOrigin[0];
var dy = p[1] - startOrigin[1];
@@ -118,7 +118,7 @@ export function behaviorDrag() {
}
function dragend() {
function pointerup() {
if (started) {
_event({ type: 'end' });
@@ -147,7 +147,7 @@ export function behaviorDrag() {
function behavior(selection) {
var matchesSelector = utilPrefixDOMProperty('matchesSelector');
var delegate = dragstart;
var delegate = pointerdown;
if (_selector) {
delegate = function() {
@@ -160,7 +160,7 @@ export function behaviorDrag() {
: datum && datum.properties && datum.properties.entity;
if (entity && target[matchesSelector](_selector)) {
return dragstart.call(target, entity);
return pointerdown.call(target, entity);
}
}
};
+2 -2
View File
@@ -85,10 +85,10 @@ export function behaviorDraw(context) {
d3_event.stopPropagation();
}, true);
context.map().dblclickEnable(false);
context.map().dblclickZoomEnable(false);
window.setTimeout(function() {
context.map().dblclickEnable(true);
context.map().dblclickZoomEnable(true);
d3_select(window).on('click.draw-block', null);
}, 500);
+3 -3
View File
@@ -199,7 +199,7 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph, baselin
.on('keyup.drawWay', keyup);
context.map()
.dblclickEnable(false)
.dblclickZoomEnable(false)
.on('drawn.draw', setActiveElements);
setActiveElements();
@@ -347,7 +347,7 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph, baselin
context.resumeChangeDispatch();
window.setTimeout(function() {
context.map().dblclickEnable(true);
context.map().dblclickZoomEnable(true);
}, 1000);
var isNewFeature = !mode.isContinuing;
@@ -365,7 +365,7 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph, baselin
context.resumeChangeDispatch();
window.setTimeout(function() {
context.map().dblclickEnable(true);
context.map().dblclickZoomEnable(true);
}, 1000);
context.surface()
+5 -2
View File
@@ -7,6 +7,7 @@ import { modeSelectData } from '../modes/select_data';
import { modeSelectNote } from '../modes/select_note';
import { modeSelectError } from '../modes/select_error';
import { osmEntity, osmNote, QAItem } from '../osm';
import { utilArrayIdentical } from '../util/array';
export function behaviorSelect(context) {
@@ -136,8 +137,10 @@ export function behaviorSelect(context) {
// multiple things already selected, just show the menu...
mode.suppressMenu(false).reselect();
} else {
// select a single thing..
context.enter(modeSelect(context, [datum.id]).suppressMenu(_suppressMenu));
if (mode.id !== 'select' || !utilArrayIdentical(mode.selectedIDs(), [datum.id])) {
// select a single thing if it's not already selected
context.enter(modeSelect(context, [datum.id]).suppressMenu(_suppressMenu));
}
}
} else {
+4 -13
View File
@@ -325,8 +325,8 @@ export function modeSelect(context, selectedIDs) {
breatheBehavior.restartIfNeeded(context.surface());
});
context.surface()
.on('dblclick.select', dblclick);
context.map().doubleUpHandler()
.on('doubleUp.modeSelect', didDoubleUp);
selectElements();
@@ -359,7 +359,7 @@ export function modeSelect(context, selectedIDs) {
}
function dblclick() {
function didDoubleUp(loc) {
if (!context.map().withinEditableZoom()) return;
var target = d3_select(d3_event.target);
@@ -369,7 +369,7 @@ export function modeSelect(context, selectedIDs) {
if (!entity) return;
if (entity instanceof osmWay && target.classed('target')) {
var choice = geoChooseEdge(context.childNodes(entity), context.mouse(), context.projection);
var choice = geoChooseEdge(context.childNodes(entity), loc, context.projection);
var prev = entity.nodes[choice.index - 1];
var next = entity.nodes[choice.index];
@@ -378,16 +378,10 @@ export function modeSelect(context, selectedIDs) {
t('operations.add.annotation.vertex')
);
d3_event.preventDefault();
d3_event.stopPropagation();
} else if (entity.type === 'midpoint') {
context.perform(
actionAddMidpoint({ loc: entity.loc, edge: entity.edge }, osmNode()),
t('operations.add.annotation.vertex'));
d3_event.preventDefault();
d3_event.stopPropagation();
}
}
@@ -574,9 +568,6 @@ export function modeSelect(context, selectedIDs) {
var surface = context.surface();
surface
.on('dblclick.select', null);
surface
.selectAll('.selected-member')
.classed('selected-member', false);
+37 -12
View File
@@ -15,6 +15,7 @@ import { utilDetect } from '../util/detect';
import { utilGetDimensions } from '../util/dimensions';
import { utilRebind } from '../util/rebind';
import { utilZoomPan } from '../util/zoom_pan';
import { utilDoubleUp } from '../util/double_up';
// constants
var TILESIZE = 256;
@@ -50,7 +51,7 @@ export function rendererMap(context) {
var surface = d3_select(null);
var _dimensions = [1, 1];
var _dblClickEnabled = true;
var _dblClickZoomEnabled = true;
var _redrawEnabled = true;
var _gestureTransformStart;
var _transformStart = projection.transform();
@@ -81,6 +82,7 @@ export function rendererMap(context) {
.on('end.map', function() {
_pointerDown = false;
});
var _doubleUpHandler = utilDoubleUp();
var scheduleRedraw = _throttle(redraw, 750);
// var isRedrawScheduled = false;
@@ -147,9 +149,9 @@ export function rendererMap(context) {
});
selection
.on('dblclick.map', dblClick)
.call(_zoomerPanner)
.call(_zoomerPanner.transform, projection.transform());
.call(_zoomerPanner.transform, projection.transform())
.on('dblclick.zoom', null); // override d3-zoom dblclick handling
supersurface = selection.append('div')
.attr('id', 'supersurface')
@@ -168,6 +170,7 @@ export function rendererMap(context) {
surface
.call(drawLabels.observe)
.call(_doubleUpHandler)
.on('gesturestart.surface', function() {
_gestureTransformStart = projection.transform();
})
@@ -203,6 +206,28 @@ export function rendererMap(context) {
// must call after surface init
updateAreaFill();
_doubleUpHandler.on('doubleUp.map', function(p0) {
if (!_dblClickZoomEnabled) return;
// don't zoom if targeting something other than the map itself
if (typeof d3_event.target.__data__ === 'object' &&
// or area fills
!d3_select(d3_event.target).classed('fill')) return;
var zoomOut = d3_event.shiftKey;
var t = projection.transform();
var p1 = t.invert(p0);
t = t.scale(zoomOut ? 0.5 : 2);
t.x = p0[0] - p1[0] * t.k;
t.y = p0[1] - p1[1] * t.k;
map.transformEase(t);
});
context.on('enter.map', function() {
if (map.editableDataEnabled(true /* skip zoom check */) && !_isTransformed) {
// redraw immediately any objects affected by a change in selectedIDs.
@@ -367,12 +392,7 @@ export function rendererMap(context) {
}
function dblClick() {
if (!_dblClickEnabled) {
d3_event.preventDefault();
d3_event.stopImmediatePropagation();
}
}
function gestureChange() {
@@ -662,9 +682,9 @@ export function rendererMap(context) {
};
map.dblclickEnable = function(val) {
if (!arguments.length) return _dblClickEnabled;
_dblClickEnabled = val;
map.dblclickZoomEnable = function(val) {
if (!arguments.length) return _dblClickZoomEnabled;
_dblClickZoomEnabled = val;
return map;
};
@@ -1051,5 +1071,10 @@ export function rendererMap(context) {
map.layers = drawLayers;
map.doubleUpHandler = function() {
return _doubleUpHandler;
};
return utilRebind(map, dispatch, 'on');
}
+80
View File
@@ -0,0 +1,80 @@
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { mouse as d3_mouse } from 'd3-selection';
import { utilRebind } from './rebind';
import { geoVecLength } from '../geo/vector';
// a double-click / double-tap event detector with wider
export function utilDoubleUp() {
var dispatch = d3_dispatch('doubleUp');
var _maxTimespan = 500; // milliseconds
var _maxDistance = 20; // web pixels; be somewhat generous to account for touch devices
var _pointer; // object representing the pointer that could trigger double up
function pointerIsValidFor(loc) {
// second pointerup must occur within a small timeframe after the first pointerdown
return new Date().getTime() - _pointer.startTime <= _maxTimespan &&
// all pointer events must occur within a small distance of the first pointerdown
geoVecLength(_pointer.startLoc, loc) <= _maxDistance;
}
function pointerdown() {
// d3_mouse works since pointer events inherit from mouse events
var loc = d3_mouse(this);
if (_pointer && !pointerIsValidFor(loc)) {
// if this pointer is no longer valid, clear it so another can be started
_pointer = undefined;
}
if (!_pointer) {
// don't rely on the pointerId since it can change between down events on touch devices
_pointer = {
startLoc: loc,
startTime: new Date().getTime(),
upCount: 0
};
}
}
function pointerup() {
if (!_pointer) return;
_pointer.upCount += 1;
if (_pointer.upCount === 2) { // double up!
var loc = d3_mouse(this);
if (pointerIsValidFor(loc)) {
dispatch.call('doubleUp', this, loc);
}
// clear the pointer info in any case
_pointer = undefined;
}
}
function doubleUp(selection) {
if ('PointerEvent' in window) {
// dblclick isn't well supported on touch devices so manually use
// pointer events if they're available
selection
.on('pointerdown.doubleUp', pointerdown)
.on('pointerup.doubleUp', pointerup);
} else {
// fallback to dblclick
selection
.on('dblclick.doubleUp', function() {
dispatch.call('doubleUp', this, d3_mouse(this));
});
}
}
doubleUp.off = function(selection) {
selection
.on('pointerdown.doubleUp', null)
.on('pointerup.doubleUp', null)
.on('dblclick.doubleUp', null);
};
return utilRebind(doubleUp, dispatch, 'on');
}
+1 -23
View File
@@ -3,7 +3,7 @@
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { interpolateZoom } from 'd3-interpolate';
import { event as d3_event, customEvent as d3_customEvent, select as d3_select, mouse as d3_mouse } from 'd3-selection';
import { event as d3_event, customEvent as d3_customEvent, mouse as d3_mouse } from 'd3-selection';
import { interrupt as d3_interrupt } from 'd3-transition';
import constant from '../../node_modules/d3-zoom/src/constant.js';
import ZoomEvent from '../../node_modules/d3-zoom/src/event.js';
@@ -70,7 +70,6 @@ export function utilZoomPan() {
function zoom(selection) {
selection
.property('__zoom', defaultTransform)
.on('dblclick.zoom', dblclicked)
.on('pointerdown.zoom', pointerdown)
.on('pointermove.zoom', pointermove)
.on('pointerup.zoom pointercancel.zoom', pointerup)
@@ -247,20 +246,6 @@ export function utilZoomPan() {
}
}
function dblclicked() {
if (!filter.apply(this, arguments)) return;
var t0 = this.__zoom,
p0 = d3_mouse(this),
p1 = t0.invert(p0),
k1 = t0.k * (d3_event.shiftKey ? 0.5 : 2),
t1 = constrain(translate(scale(t0, k1), p0, p1), extent.apply(this, arguments), translateExtent);
d3_event.preventDefault();
d3_event.stopImmediatePropagation();
if (duration > 0) d3_select(this).transition().duration(duration).call(schedule, t1, p0);
else d3_select(this).call(zoom.transform, t1);
}
var downPointerIDs = new Set();
function pointerdown() {
@@ -347,13 +332,6 @@ export function utilZoomPan() {
if (g.touch0) g.touch0[1] = this.__zoom.invert(g.touch0[0]);
else {
g.end();
// If this was a dbltap, reroute to the (optional) dblclick.zoom handler.
if (g.taps === 2) {
// This currently never appears to be called but mobile Safari still
// seems to get regular dblclick events upon double-tapping.
var p = d3_select(this).on('dblclick.zoom');
if (p) p.apply(this, arguments);
}
}
}