diff --git a/modules/behavior/add_way.js b/modules/behavior/add_way.js index b5ba7784c7..a4b0461ae6 100644 --- a/modules/behavior/add_way.js +++ b/modules/behavior/add_way.js @@ -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)); diff --git a/modules/behavior/drag.js b/modules/behavior/drag.js index a92b457feb..93d42cfb32 100644 --- a/modules/behavior/drag.js +++ b/modules/behavior/drag.js @@ -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); } } }; diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 1011af090a..4122478e73 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -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); diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 47c955ad96..c70252b8cf 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -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() diff --git a/modules/behavior/select.js b/modules/behavior/select.js index a9676def7a..955721284a 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -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 { diff --git a/modules/modes/select.js b/modules/modes/select.js index 34b87d9877..2ecf843b1e 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -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); diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 0c77ba2cb1..81bd158e50 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -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'); } diff --git a/modules/util/double_up.js b/modules/util/double_up.js new file mode 100644 index 0000000000..d00da0f594 --- /dev/null +++ b/modules/util/double_up.js @@ -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'); +} diff --git a/modules/util/zoom_pan.js b/modules/util/zoom_pan.js index b89e2b8a4e..5d20255660 100644 --- a/modules/util/zoom_pan.js +++ b/modules/util/zoom_pan.js @@ -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); - } } }