Enable multiselection on touch devices when tapping on features with a pointer already down on a selected feature (close #7590)

Reuse modeSelect when changing entity selection if possible
This commit is contained in:
Quincy Morgan
2020-05-26 17:21:48 -04:00
parent 5f13be88dc
commit 5b399623e9
2 changed files with 144 additions and 102 deletions

View File

@@ -12,32 +12,15 @@ import { utilFastMouse } from '../util/util';
export function behaviorSelect(context) {
var _tolerancePx = 4; // see also behaviorDrag
var _lastPointerEvent = null;
var _lastMouseEvent = null;
var _showMenu = false;
var _p1 = null;
var _downPointerId = null;
var _downPointers = {};
var _longPressTimeout = null;
var _lastInteractionType = null;
// use pointer events on supported platforms; fallback to mouse events
var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';
function point(event) {
// Don't use map().mouse() since additional pointers unrelated to selection can
// move between pointerdown and pointerup. Use the `main-map` coordinate system
// since the surface and supersurface are transformed when drag-panning.
return utilFastMouse(context.container().select('.main-map').node())(event || d3_event);
}
function mapContains(event) {
var rect = context.container().select('.main-map').node().getBoundingClientRect();
return event.clientX >= rect.left &&
event.clientX <= rect.right &&
event.clientY >= rect.top &&
event.clientY <= rect.bottom;
}
function keydown() {
@@ -55,7 +38,7 @@ export function behaviorSelect(context) {
if (d3_event.repeat) return; // ignore repeated events for held keys
// if any key is pressed the user is probably doing something other than long-pressing
if (_longPressTimeout) window.clearTimeout(_longPressTimeout);
cancelLongPress();
if (d3_event.shiftKey) {
context.surface()
@@ -63,17 +46,21 @@ export function behaviorSelect(context) {
}
if (d3_event.keyCode === 32) { // spacebar
if (!_p1) {
_p1 = point(_lastPointerEvent);
if (_longPressTimeout) window.clearTimeout(_longPressTimeout);
_longPressTimeout = window.setTimeout(didLongPress, 500, 'spacebar');
if (!_downPointers.spacebar && _lastMouseEvent) {
cancelLongPress();
_longPressTimeout = window.setTimeout(didLongPress, 500, 'spacebar', 'spacebar');
_downPointers.spacebar = {
firstEvent: _lastMouseEvent,
lastEvent: _lastMouseEvent
};
}
}
}
function keyup() {
if (_longPressTimeout) window.clearTimeout(_longPressTimeout);
cancelLongPress();
if (!d3_event.shiftKey) {
context.surface()
@@ -85,52 +72,75 @@ export function behaviorSelect(context) {
_lastInteractionType = 'menukey';
contextmenu();
} else if (d3_event.keyCode === 32) { // spacebar
d3_event.preventDefault();
_lastInteractionType = 'spacebar';
click();
var pointer = _downPointers.spacebar;
if (pointer) {
delete _downPointers.spacebar;
if (pointer.longPressed) return;
d3_event.preventDefault();
_lastInteractionType = 'spacebar';
click(pointer.firstEvent, pointer.lastEvent);
}
}
}
function pointerdown() {
if (_p1) return;
_p1 = point();
_downPointerId = d3_event.pointerId || 'mouse';
var id = d3_event.pointerId || 'mouse';
if (_longPressTimeout) window.clearTimeout(_longPressTimeout);
_longPressTimeout = window.setTimeout(didLongPress, 500, 'longdown-' + (d3_event.pointerType || 'mouse'));
cancelLongPress();
_lastPointerEvent = d3_event;
if (d3_event.buttons && d3_event.buttons !== 1) return;
d3_select(window)
.on(_pointerPrefix + 'up.select', pointerup, true);
_longPressTimeout = window.setTimeout(didLongPress, 500, id, 'longdown-' + (d3_event.pointerType || 'mouse'));
_downPointers[id] = {
firstEvent: d3_event,
lastEvent: d3_event
};
}
function didLongPress(iType) {
function didLongPress(id, interactionType) {
var pointer = _downPointers[id];
if (!pointer) return;
pointer.longPressed = true;
// treat long presses like right-clicks
_longPressTimeout = null;
_lastInteractionType = iType;
_lastInteractionType = interactionType;
_showMenu = true;
click();
click(pointer.firstEvent, pointer.lastEvent);
}
function pointermove() {
if (_downPointerId && _downPointerId !== (d3_event.pointerId || 'mouse')) return;
_lastPointerEvent = d3_event;
var id = d3_event.pointerId || 'mouse';
if (_downPointers[id]) {
_downPointers[id].lastEvent = d3_event;
}
if (!d3_event.pointerType || d3_event.pointerType === 'mouse') {
_lastMouseEvent = d3_event;
if (_downPointers.spacebar) {
_downPointers.spacebar.lastEvent = d3_event;
}
}
}
function pointerup() {
if (_downPointerId !== (d3_event.pointerId || 'mouse')) return;
_downPointerId = null;
var id = d3_event.pointerId || 'mouse';
var pointer = _downPointers[id];
if (!pointer) return;
d3_select(window)
.on(_pointerPrefix + 'up.select', null, true);
delete _downPointers[id];
click();
if (pointer.longPressed) return;
click(pointer.firstEvent, d3_event);
}
@@ -139,48 +149,74 @@ export function behaviorSelect(context) {
e.preventDefault();
if (!+e.clientX && !+e.clientY) {
if (_lastPointerEvent) {
e.sourceEvent = _lastPointerEvent;
if (_lastMouseEvent) {
e.sourceEvent = _lastMouseEvent;
} else {
return;
}
} else {
_lastPointerEvent = d3_event;
_lastMouseEvent = d3_event;
_lastInteractionType = 'rightclick';
}
if (!_p1) {
_p1 = point();
}
_showMenu = true;
click();
click(d3_event, d3_event);
}
function click() {
if (_longPressTimeout) window.clearTimeout(_longPressTimeout);
function click(firstEvent, lastEvent) {
cancelLongPress();
var mapNode = context.container().select('.main-map').node();
// Use the `main-map` coordinate system since the surface and supersurface
// are transformed when drag-panning.
var pointGetter = utilFastMouse(mapNode);
var p1 = pointGetter(firstEvent);
var p2 = pointGetter(lastEvent);
var dist = geoVecLength(p1, p2);
if (dist > _tolerancePx ||
!mapContains(lastEvent)) {
if (!_p1) {
resetProperties();
return;
}
var p2 = point(_lastPointerEvent);
var dist = geoVecLength(_p1, p2);
_p1 = null;
if (dist > _tolerancePx || !mapContains(_lastPointerEvent)) {
resetProperties();
return;
}
var datum = (d3_event && d3_event.target.__data__) || (_lastPointerEvent && _lastPointerEvent.target.__data__);
var isMultiselect = (d3_event && d3_event.shiftKey) || context.surface().select('.lasso').node();
var datum = lastEvent.target.__data__;
// only support multiselect if data is already selected
var isMultiselect = context.mode().id === 'select' &&
((d3_event && d3_event.shiftKey) || context.surface().select('.lasso').node() || isPointerDownOnSelection());
processClick(datum, isMultiselect, p2);
function mapContains(event) {
var rect = mapNode.getBoundingClientRect();
return event.clientX >= rect.left &&
event.clientX <= rect.right &&
event.clientY >= rect.top &&
event.clientY <= rect.bottom;
}
}
function isPointerDownOnSelection() {
var selectedIds = context.mode().id === 'select' && context.mode().selectedIDs();
for (var id in _downPointers) {
if (id === 'spacebar') continue;
var datum = _downPointers[id].firstEvent.target.__data__;
var entity = (datum && datum.properties && datum.properties.entity) || datum;
if (selectedIds.indexOf(entity.id) !== -1) return true;
}
return false;
}
function processClick(datum, isMultiselect, point) {
var mode = context.mode();
var showMenu = _showMenu;
var interactionType = _lastInteractionType;
var entity = datum && datum.properties && datum.properties.entity;
if (entity) datum = entity;
@@ -198,26 +234,27 @@ export function behaviorSelect(context) {
context.selectedErrorID(null);
if (!isMultiselect) {
if (selectedIDs.length <= 1 || !_showMenu) {
if (selectedIDs.length <= 1 || !showMenu) {
// always enter modeSelect even if the entity is already
// selected since listeners may expect `context.enter` events,
// e.g. in the walkthrough
newMode = modeSelect(context, [datum.id]);
newMode = mode.id === 'select' ? mode.selectedIDs([datum.id]) : modeSelect(context, [datum.id]);
context.enter(newMode);
}
} else {
if (selectedIDs.indexOf(datum.id) !== -1) {
// clicked entity is already in the selectedIDs list..
if (!_showMenu) {
if (!showMenu) {
// deselect clicked entity, then reenter select mode or return to browse mode..
selectedIDs = selectedIDs.filter(function(id) { return id !== datum.id; });
context.enter(selectedIDs.length ? modeSelect(context, selectedIDs) : modeBrowse(context));
newMode = selectedIDs.length ? mode.selectedIDs(selectedIDs) : modeBrowse(context);
context.enter(newMode);
}
} else {
// clicked entity is not in the selected list, add it..
selectedIDs = selectedIDs.concat([datum.id]);
newMode = modeSelect(context, selectedIDs);
newMode = mode.selectedIDs(selectedIDs);
context.enter(newMode);
}
}
@@ -248,31 +285,35 @@ export function behaviorSelect(context) {
context.ui().closeEditMenu();
// always request to show the edit menu in case the mode needs it
if (_showMenu) context.ui().showEditMenu(point, _lastInteractionType);
if (showMenu) context.ui().showEditMenu(point, interactionType);
resetProperties();
}
function resetProperties() {
_showMenu = false;
_p1 = null;
_downPointerId = null;
function cancelLongPress() {
if (_longPressTimeout) window.clearTimeout(_longPressTimeout);
_longPressTimeout = null;
}
function resetProperties() {
cancelLongPress();
_showMenu = false;
_lastInteractionType = null;
// don't reset _lastPointerEvent since it might still be useful
// don't reset _lastMouseEvent since it might still be useful
}
function behavior(selection) {
resetProperties();
_lastPointerEvent = context.map().lastPointerEvent();
_lastMouseEvent = context.map().lastPointerEvent();
d3_select(window)
.on('keydown.select', keydown)
.on('keyup.select', keyup)
.on(_pointerPrefix + 'move.select', pointermove, true)
.on(_pointerPrefix + 'up.select', pointerup, true)
.on('contextmenu.select-window', function() {
// Edge and IE really like to show the contextmenu on the
// menubar when user presses a keyboard menu button
@@ -295,7 +336,7 @@ export function behaviorSelect(context) {
behavior.off = function(selection) {
if (_longPressTimeout) window.clearTimeout(_longPressTimeout);
cancelLongPress();
d3_select(window)
.on('keydown.select', null)

View File

@@ -37,25 +37,24 @@ export function modeSelect(context, selectedIDs) {
};
var keybinding = utilKeybinding('select');
var breatheBehavior = behaviorBreathe(context);
var behaviors = [
var _breatheBehavior = behaviorBreathe(context);
var _modeDragNode = modeDragNode(context);
var _behaviors = [
behaviorPaste(context),
breatheBehavior,
_breatheBehavior,
behaviorHover(context).on('hover', context.ui().sidebar.hoverModeSelect),
behaviorSelect(context),
behaviorLasso(context),
modeDragNode(context).restoreSelectedIDs(selectedIDs).behavior,
_modeDragNode.behavior,
modeDragNote(context).behavior
];
var inspector; // unused?
var _operations = [];
var _newFeature = false;
var _follow = false;
var wrap = context.container()
.select('.inspector-wrap');
function singular() {
if (selectedIDs && selectedIDs.length === 1) {
return context.hasEntity(selectedIDs[0]);
@@ -142,8 +141,10 @@ export function modeSelect(context, selectedIDs) {
}
mode.selectedIDs = function() {
return selectedIDs;
mode.selectedIDs = function(val) {
if (!arguments.length) return selectedIDs;
selectedIDs = val;
return mode;
};
@@ -165,17 +166,15 @@ export function modeSelect(context, selectedIDs) {
return mode;
};
var operations = [];
function loadOperations() {
operations.forEach(function(operation) {
_operations.forEach(function(operation) {
if (operation.behavior) {
context.uninstall(operation.behavior);
}
});
operations = Object.values(Operations)
_operations = Object.values(Operations)
.map(function(o) { return o(context, selectedIDs); })
.filter(function(o) { return o.available() && o.id !== 'delete' && o.id !== 'downgrade'; });
@@ -183,9 +182,9 @@ export function modeSelect(context, selectedIDs) {
// don't allow delete if downgrade is available
var lastOperation = !context.inIntro() && downgradeOperation.available() ? downgradeOperation : Operations.operationDelete(context, selectedIDs);
operations.push(lastOperation);
_operations.push(lastOperation);
operations.forEach(function(operation) {
_operations.forEach(function(operation) {
if (operation.behavior) {
context.install(operation.behavior);
}
@@ -196,7 +195,7 @@ export function modeSelect(context, selectedIDs) {
}
mode.operations = function() {
return operations;
return _operations;
};
@@ -205,9 +204,11 @@ export function modeSelect(context, selectedIDs) {
context.features().forceVisible(selectedIDs);
_modeDragNode.restoreSelectedIDs(selectedIDs);
loadOperations();
behaviors.forEach(context.install);
_behaviors.forEach(context.install);
keybinding
.on(t('inspector.zoom_to.key'), mode.zoomToSelected)
@@ -245,7 +246,7 @@ export function modeSelect(context, selectedIDs) {
.on('drawn.select', selectElements)
.on('crossEditableZoom.select', function() {
selectElements();
breatheBehavior.restartIfNeeded(context.surface());
_breatheBehavior.restartIfNeeded(context.surface());
});
context.map().doubleUpHandler()
@@ -468,15 +469,15 @@ export function modeSelect(context, selectedIDs) {
mode.exit = function() {
if (inspector) wrap.call(inspector.close);
operations.forEach(function(operation) {
_operations.forEach(function(operation) {
if (operation.behavior) {
context.uninstall(operation.behavior);
}
});
_operations = [];
behaviors.forEach(context.uninstall);
_behaviors.forEach(context.uninstall);
d3_select(document)
.call(keybinding.unbind);