modules/behavior/hover.js (171 lines of code) (raw):

import { dispatch as d3_dispatch } from 'd3-dispatch'; import { select as d3_select } from 'd3-selection'; import { presetManager } from '../presets'; import { osmEntity, osmNote, QAItem } from '../osm'; import { utilKeybinding, utilRebind } from '../util'; /* The hover behavior adds the `.hover` class on pointerover to all elements to which the identical datum is bound, and removes it on pointerout. The :hover pseudo-class is insufficient for iD's purposes because a datum's visual representation may consist of several elements scattered throughout the DOM hierarchy. Only one of these elements can have the :hover pseudo-class, but all of them will have the .hover class. */ export function behaviorHover(context) { var dispatch = d3_dispatch('hover'); var _selection = d3_select(null); var _newNodeId = null; var _initialNodeID = null; var _altDisables; var _ignoreVertex; var _targets = []; // use pointer events on supported platforms; fallback to mouse events var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse'; function keydown(d3_event) { if (_altDisables && d3_event.keyCode === utilKeybinding.modifierCodes.alt) { _selection.selectAll('.hover') .classed('hover-suppressed', true) .classed('hover', false); _selection .classed('hover-disabled', true); dispatch.call('hover', this, null); } } function keyup(d3_event) { if (_altDisables && d3_event.keyCode === utilKeybinding.modifierCodes.alt) { _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false) .classed('hover', true); _selection .classed('hover-disabled', false); dispatch.call('hover', this, _targets); } } function behavior(selection) { _selection = selection; _targets = []; if (_initialNodeID) { _newNodeId = _initialNodeID; _initialNodeID = null; } else { _newNodeId = null; } _selection .on(_pointerPrefix + 'over.hover', pointerover) .on(_pointerPrefix + 'out.hover', pointerout) // treat pointerdown as pointerover for touch devices .on(_pointerPrefix + 'down.hover', pointerover); d3_select(window) .on(_pointerPrefix + 'up.hover pointercancel.hover', pointerout, true) .on('keydown.hover', keydown) .on('keyup.hover', keyup); function eventTarget(d3_event) { var datum = d3_event.target && d3_event.target.__data__; if (typeof datum !== 'object') return null; if (!(datum instanceof osmEntity) && datum.properties && (datum.properties.entity instanceof osmEntity)) { return datum.properties.entity; } return datum; } function pointerover(d3_event) { // ignore mouse hovers with buttons pressed unless dragging if (context.mode().id.indexOf('drag') === -1 && (!d3_event.pointerType || d3_event.pointerType === 'mouse') && d3_event.buttons) return; var target = eventTarget(d3_event); if (target && _targets.indexOf(target) === -1) { _targets.push(target); updateHover(d3_event, _targets); } } function pointerout(d3_event) { var target = eventTarget(d3_event); var index = _targets.indexOf(target); if (index !== -1) { _targets.splice(index); updateHover(d3_event, _targets); } } function allowsVertex(d) { return d.geometry(context.graph()) === 'vertex' || presetManager.allowsVertex(d, context.graph()); } function modeAllowsHover(target) { var mode = context.mode(); if (mode.id === 'add-point') { return mode.preset.matchGeometry('vertex') || (target.type !== 'way' && target.geometry(context.graph()) !== 'vertex'); } return true; } function updateHover(d3_event, targets) { _selection.selectAll('.hover') .classed('hover', false); _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); var mode = context.mode(); if (!_newNodeId && (mode.id === 'draw-line' || mode.id === 'draw-area')) { var node = targets.find(function(target) { return target instanceof osmEntity && target.type === 'node'; }); _newNodeId = node && node.id; } targets = targets.filter(function(datum) { if (datum instanceof osmEntity) { // If drawing a way, don't hover on a node that was just placed. #3974 return datum.id !== _newNodeId && (datum.type !== 'node' || !_ignoreVertex || allowsVertex(datum)) && modeAllowsHover(datum); } return true; }); var selector = ''; for (var i in targets) { var datum = targets[i]; // What are we hovering over? if (datum.__fbid__) { // hovering a RapiD feature selector += ', .data' + datum.__fbid__; } else if (datum.__featurehash__) { // hovering custom data selector += ', .data' + datum.__featurehash__; } else if (datum instanceof QAItem) { selector += ', .' + datum.service + '.itemId-' + datum.id; } else if (datum instanceof osmNote) { selector += ', .note-' + datum.id; } else if (datum instanceof osmEntity) { selector += ', .' + datum.id; if (datum.type === 'relation') { for (var j in datum.members) { selector += ', .' + datum.members[j].id; } } } } var suppressed = _altDisables && d3_event && d3_event.altKey; if (selector.trim().length) { // remove the first comma selector = selector.slice(1); _selection.selectAll(selector) .classed(suppressed ? 'hover-suppressed' : 'hover', true); } dispatch.call('hover', this, !suppressed && targets); } } behavior.off = function(selection) { selection.selectAll('.hover') .classed('hover', false); selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); selection .classed('hover-disabled', false); selection .on(_pointerPrefix + 'over.hover', null) .on(_pointerPrefix + 'out.hover', null) .on(_pointerPrefix + 'down.hover', null); d3_select(window) .on(_pointerPrefix + 'up.hover pointercancel.hover', null, true) .on('keydown.hover', null) .on('keyup.hover', null); }; behavior.altDisables = function(val) { if (!arguments.length) return _altDisables; _altDisables = val; return behavior; }; behavior.ignoreVertex = function(val) { if (!arguments.length) return _ignoreVertex; _ignoreVertex = val; return behavior; }; behavior.initialNodeID = function(nodeId) { _initialNodeID = nodeId; return behavior; }; return utilRebind(behavior, dispatch, 'on'); }