mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 17:23:02 +00:00
* render labels properly, fixes #9425 * always open the touch-specific edit menu (the contextmenu event is always triggered by the browser on long presses, where we need to make sure that this event doesn't override the `isTouchMenu` property of the edit menu)
438 lines
15 KiB
JavaScript
438 lines
15 KiB
JavaScript
import { select as d3_select } from 'd3-selection';
|
|
|
|
import { geoVecLength } from '../geo';
|
|
import { modeBrowse } from '../modes/browse';
|
|
import { modeSelect } from '../modes/select';
|
|
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 { utilFastMouse } from '../util/util';
|
|
|
|
|
|
export function behaviorSelect(context) {
|
|
var _tolerancePx = 4; // see also behaviorDrag
|
|
var _lastMouseEvent = null;
|
|
var _showMenu = false;
|
|
var _downPointers = {};
|
|
var _longPressTimeout = null;
|
|
var _lastInteractionType = null;
|
|
// the id of the down pointer that's enabling multiselection while down
|
|
var _multiselectionPointerId = null;
|
|
|
|
// use pointer events on supported platforms; fallback to mouse events
|
|
var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';
|
|
|
|
|
|
function keydown(d3_event) {
|
|
|
|
if (d3_event.keyCode === 32) {
|
|
// don't react to spacebar events during text input
|
|
var activeNode = document.activeElement;
|
|
if (activeNode && new Set(['INPUT', 'TEXTAREA']).has(activeNode.nodeName)) return;
|
|
}
|
|
|
|
if (d3_event.keyCode === 93 || // context menu key
|
|
d3_event.keyCode === 32) { // spacebar
|
|
d3_event.preventDefault();
|
|
}
|
|
|
|
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
|
|
cancelLongPress();
|
|
|
|
if (d3_event.shiftKey) {
|
|
context.surface()
|
|
.classed('behavior-multiselect', true);
|
|
}
|
|
|
|
if (d3_event.keyCode === 32) { // spacebar
|
|
if (!_downPointers.spacebar && _lastMouseEvent) {
|
|
cancelLongPress();
|
|
_longPressTimeout = window.setTimeout(didLongPress, 500, 'spacebar', 'spacebar');
|
|
|
|
_downPointers.spacebar = {
|
|
firstEvent: _lastMouseEvent,
|
|
lastEvent: _lastMouseEvent
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function keyup(d3_event) {
|
|
cancelLongPress();
|
|
|
|
if (!d3_event.shiftKey) {
|
|
context.surface()
|
|
.classed('behavior-multiselect', false);
|
|
}
|
|
|
|
if (d3_event.keyCode === 93) { // context menu key
|
|
d3_event.preventDefault();
|
|
_lastInteractionType = 'menukey';
|
|
contextmenu(d3_event);
|
|
} else if (d3_event.keyCode === 32) { // spacebar
|
|
var pointer = _downPointers.spacebar;
|
|
if (pointer) {
|
|
delete _downPointers.spacebar;
|
|
|
|
if (pointer.done) return;
|
|
|
|
d3_event.preventDefault();
|
|
_lastInteractionType = 'spacebar';
|
|
click(pointer.firstEvent, pointer.lastEvent, 'spacebar');
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function pointerdown(d3_event) {
|
|
var id = (d3_event.pointerId || 'mouse').toString();
|
|
|
|
cancelLongPress();
|
|
|
|
if (d3_event.buttons && d3_event.buttons !== 1) return;
|
|
|
|
context.ui().closeEditMenu();
|
|
|
|
if (d3_event.pointerType !== 'mouse') {
|
|
_longPressTimeout = window.setTimeout(didLongPress, 500, id, 'longdown-' + (d3_event.pointerType || 'mouse'));
|
|
}
|
|
|
|
_downPointers[id] = {
|
|
firstEvent: d3_event,
|
|
lastEvent: d3_event
|
|
};
|
|
}
|
|
|
|
|
|
function didLongPress(id, interactionType) {
|
|
var pointer = _downPointers[id];
|
|
if (!pointer) return;
|
|
|
|
for (var i in _downPointers) {
|
|
// don't allow this or any currently down pointer to trigger another click
|
|
_downPointers[i].done = true;
|
|
}
|
|
|
|
// treat long presses like right-clicks
|
|
_longPressTimeout = null;
|
|
_lastInteractionType = interactionType;
|
|
_showMenu = true;
|
|
|
|
click(pointer.firstEvent, pointer.lastEvent, id);
|
|
}
|
|
|
|
|
|
function pointermove(d3_event) {
|
|
var id = (d3_event.pointerId || 'mouse').toString();
|
|
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(d3_event) {
|
|
var id = (d3_event.pointerId || 'mouse').toString();
|
|
var pointer = _downPointers[id];
|
|
if (!pointer) return;
|
|
|
|
delete _downPointers[id];
|
|
|
|
if (_multiselectionPointerId === id) {
|
|
_multiselectionPointerId = null;
|
|
}
|
|
|
|
if (pointer.done) return;
|
|
|
|
click(pointer.firstEvent, d3_event, id);
|
|
}
|
|
|
|
|
|
function pointercancel(d3_event) {
|
|
var id = (d3_event.pointerId || 'mouse').toString();
|
|
if (!_downPointers[id]) return;
|
|
|
|
delete _downPointers[id];
|
|
|
|
if (_multiselectionPointerId === id) {
|
|
_multiselectionPointerId = null;
|
|
}
|
|
}
|
|
|
|
|
|
function contextmenu(d3_event) {
|
|
d3_event.preventDefault();
|
|
|
|
if (!+d3_event.clientX && !+d3_event.clientY) {
|
|
if (_lastMouseEvent) {
|
|
d3_event = _lastMouseEvent;
|
|
} else {
|
|
return;
|
|
}
|
|
} else {
|
|
_lastMouseEvent = d3_event;
|
|
if (d3_event.pointerType === 'touch' || d3_event.pointerType === 'pen' ||
|
|
d3_event.mozInputSource && ( // firefox doesn't give a pointerType on contextmenu events
|
|
d3_event.mozInputSource === MouseEvent.MOZ_SOURCE_TOUCH ||
|
|
d3_event.mozInputSource === MouseEvent.MOZ_SOURCE_PEN)) {
|
|
_lastInteractionType = 'touch';
|
|
} else {
|
|
_lastInteractionType = 'rightclick';
|
|
}
|
|
}
|
|
|
|
_showMenu = true;
|
|
click(d3_event, d3_event);
|
|
}
|
|
|
|
|
|
function click(firstEvent, lastEvent, pointerId) {
|
|
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)) {
|
|
|
|
resetProperties();
|
|
return;
|
|
}
|
|
|
|
var targetDatum = lastEvent.target.__data__;
|
|
|
|
var multiselectEntityId;
|
|
|
|
if (!_multiselectionPointerId) {
|
|
// If a different pointer than the one triggering this click is down on a
|
|
// feature, treat this and all future clicks as multiselection until that
|
|
// pointer is raised.
|
|
var selectPointerInfo = pointerDownOnSelection(pointerId);
|
|
if (selectPointerInfo) {
|
|
_multiselectionPointerId = selectPointerInfo.pointerId;
|
|
// if the other feature isn't selected yet, make sure we select it
|
|
multiselectEntityId = !selectPointerInfo.selected && selectPointerInfo.entityId;
|
|
_downPointers[selectPointerInfo.pointerId].done = true;
|
|
}
|
|
}
|
|
|
|
// support multiselect if data is already selected
|
|
var isMultiselect = context.mode().id === 'select' && (
|
|
// and shift key is down
|
|
(lastEvent && lastEvent.shiftKey) ||
|
|
// or we're lasso-selecting
|
|
context.surface().select('.lasso').node() ||
|
|
// or a pointer is down over a selected feature
|
|
(_multiselectionPointerId && !multiselectEntityId)
|
|
);
|
|
|
|
processClick(targetDatum, isMultiselect, p2, multiselectEntityId);
|
|
|
|
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 pointerDownOnSelection(skipPointerId) {
|
|
var mode = context.mode();
|
|
var selectedIDs = mode.id === 'select' ? mode.selectedIDs() : [];
|
|
for (var pointerId in _downPointers) {
|
|
if (pointerId === 'spacebar' || pointerId === skipPointerId) continue;
|
|
|
|
var pointerInfo = _downPointers[pointerId];
|
|
|
|
var p1 = pointGetter(pointerInfo.firstEvent);
|
|
var p2 = pointGetter(pointerInfo.lastEvent);
|
|
if (geoVecLength(p1, p2) > _tolerancePx) continue;
|
|
|
|
var datum = pointerInfo.firstEvent.target.__data__;
|
|
var entity = (datum && datum.properties && datum.properties.entity) || datum;
|
|
if (context.graph().hasEntity(entity.id)) {
|
|
return {
|
|
pointerId: pointerId,
|
|
entityId: entity.id,
|
|
selected: selectedIDs.indexOf(entity.id) !== -1
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
function processClick(datum, isMultiselect, point, alsoSelectId) {
|
|
var mode = context.mode();
|
|
var showMenu = _showMenu;
|
|
var interactionType = _lastInteractionType;
|
|
|
|
var entity = datum && datum.properties && datum.properties.entity;
|
|
if (entity) datum = entity;
|
|
|
|
if (datum && datum.type === 'midpoint') {
|
|
// treat targeting midpoints as if targeting the parent way
|
|
datum = datum.parents[0];
|
|
}
|
|
|
|
var newMode;
|
|
|
|
if (datum instanceof osmEntity) {
|
|
// targeting an entity
|
|
var selectedIDs = context.selectedIDs();
|
|
context.selectedNoteID(null);
|
|
context.selectedErrorID(null);
|
|
|
|
if (!isMultiselect) {
|
|
// don't change the selection if we're toggling the menu atop a multiselection
|
|
if (!showMenu ||
|
|
selectedIDs.length <= 1 ||
|
|
selectedIDs.indexOf(datum.id) === -1) {
|
|
|
|
if (alsoSelectId === datum.id) alsoSelectId = null;
|
|
|
|
selectedIDs = (alsoSelectId ? [alsoSelectId] : []).concat([datum.id]);
|
|
// always enter modeSelect even if the entity is already
|
|
// selected since listeners may expect `context.enter` events,
|
|
// e.g. in the walkthrough
|
|
newMode = mode.id === 'select' ? mode.selectedIDs(selectedIDs) : modeSelect(context, selectedIDs).selectBehavior(behavior);
|
|
context.enter(newMode);
|
|
}
|
|
|
|
} else {
|
|
if (selectedIDs.indexOf(datum.id) !== -1) {
|
|
// clicked entity is already in the selectedIDs list..
|
|
if (!showMenu) {
|
|
// deselect clicked entity, then reenter select mode or return to browse mode..
|
|
selectedIDs = selectedIDs.filter(function(id) { return id !== datum.id; });
|
|
newMode = selectedIDs.length ? mode.selectedIDs(selectedIDs) : modeBrowse(context).selectBehavior(behavior);
|
|
context.enter(newMode);
|
|
}
|
|
} else {
|
|
// clicked entity is not in the selected list, add it..
|
|
selectedIDs = selectedIDs.concat([datum.id]);
|
|
newMode = mode.selectedIDs(selectedIDs);
|
|
context.enter(newMode);
|
|
}
|
|
}
|
|
|
|
} else if (datum && datum.__featurehash__ && !isMultiselect) {
|
|
// targeting custom data
|
|
context
|
|
.selectedNoteID(null)
|
|
.enter(modeSelectData(context, datum));
|
|
|
|
} else if (datum instanceof osmNote && !isMultiselect) {
|
|
// targeting a note
|
|
context
|
|
.selectedNoteID(datum.id)
|
|
.enter(modeSelectNote(context, datum.id));
|
|
|
|
} else if (datum instanceof QAItem && !isMultiselect) {
|
|
// targeting an external QA issue
|
|
context
|
|
.selectedErrorID(datum.id)
|
|
.enter(modeSelectError(context, datum.id, datum.service));
|
|
|
|
} else {
|
|
// targeting nothing
|
|
context.selectedNoteID(null);
|
|
context.selectedErrorID(null);
|
|
if (!isMultiselect && mode.id !== 'browse') {
|
|
context.enter(modeBrowse(context));
|
|
}
|
|
}
|
|
|
|
context.ui().closeEditMenu();
|
|
|
|
// always request to show the edit menu in case the mode needs it
|
|
if (showMenu) context.ui().showEditMenu(point, interactionType);
|
|
|
|
resetProperties();
|
|
}
|
|
|
|
|
|
function cancelLongPress() {
|
|
if (_longPressTimeout) window.clearTimeout(_longPressTimeout);
|
|
_longPressTimeout = null;
|
|
}
|
|
|
|
|
|
function resetProperties() {
|
|
cancelLongPress();
|
|
_showMenu = false;
|
|
_lastInteractionType = null;
|
|
// don't reset _lastMouseEvent since it might still be useful
|
|
}
|
|
|
|
|
|
function behavior(selection) {
|
|
resetProperties();
|
|
_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('pointercancel.select', pointercancel, true)
|
|
.on('contextmenu.select-window', function(d3_event) {
|
|
// Edge and IE really like to show the contextmenu on the
|
|
// menubar when user presses a keyboard menu button
|
|
// even after we've already preventdefaulted the key event.
|
|
var e = d3_event;
|
|
if (+e.clientX === 0 && +e.clientY === 0) {
|
|
d3_event.preventDefault();
|
|
}
|
|
});
|
|
|
|
selection
|
|
.on(_pointerPrefix + 'down.select', pointerdown)
|
|
.on('contextmenu.select', contextmenu);
|
|
|
|
/*if (d3_event && d3_event.shiftKey) {
|
|
context.surface()
|
|
.classed('behavior-multiselect', true);
|
|
}*/
|
|
}
|
|
|
|
|
|
behavior.off = function(selection) {
|
|
cancelLongPress();
|
|
|
|
d3_select(window)
|
|
.on('keydown.select', null)
|
|
.on('keyup.select', null)
|
|
.on('contextmenu.select-window', null)
|
|
.on(_pointerPrefix + 'move.select', null, true)
|
|
.on(_pointerPrefix + 'up.select', null, true)
|
|
.on('pointercancel.select', null, true);
|
|
|
|
selection
|
|
.on(_pointerPrefix + 'down.select', null)
|
|
.on('contextmenu.select', null);
|
|
|
|
context.surface()
|
|
.classed('behavior-multiselect', false);
|
|
};
|
|
|
|
|
|
return behavior;
|
|
}
|