diff --git a/modules/behavior/select.js b/modules/behavior/select.js index c3d9c2e78..72c28fcc0 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -1,10 +1,15 @@ import * as d3 from 'd3'; import _ from 'lodash'; -import { modeBrowse, modeSelect } from '../modes/index'; -import { osmEntity } from '../osm/index'; +import { geoEuclideanDistance } from '../geo'; +import { modeBrowse, modeSelect } from '../modes'; +import { osmEntity } from '../osm'; export function behaviorSelect(context) { + var suppressMenu = true, + tolerance = 4, + p1 = null; + function keydown() { if (d3.event && d3.event.shiftKey) { @@ -22,46 +27,92 @@ export function behaviorSelect(context) { } - function click() { - var rtClick = d3.event.type === 'contextmenu'; + function point() { + return d3.mouse(context.container().node()); + } - if (rtClick) { - d3.event.preventDefault(); + + function contextmenu() { + if (!p1) p1 = point(); + d3.event.preventDefault(); + suppressMenu = false; + click(); + } + + + function mousedown() { + if (!p1) p1 = point(); + d3.select(window) + .on('mouseup.select', mouseup, true); + } + + + function mouseup() { + click(); + } + + + function click() { + d3.select(window) + .on('mouseup.select', null, true); + + if (!p1) return; + var p2 = point(), + dist = geoEuclideanDistance(p1, p2); + + p1 = null; + if (dist > tolerance) { + return; } var datum = d3.event.target.__data__, - lasso = d3.select('#surface .lasso').node(), + isMultiselect = d3.event.shiftKey || d3.select('#surface .lasso').node(), mode = context.mode(); - if (datum.type === 'midpoint') { - // do nothing - } else if (!(datum instanceof osmEntity)) { - if (!d3.event.shiftKey && !lasso && mode.id !== 'browse') - context.enter(modeBrowse(context)); - } else if (!d3.event.shiftKey && !lasso) { - // Reselect when 'rtClick on one of the selectedIDs' - // OR 'leftClick on the same singular selected entity' - // Explanation: leftClick should discard any multiple - // selection of entities and make the selection singlular. - // Whereas rtClick should preserve multiple selection of - // entities if and only if it clicks on one of the selectedIDs. - if (context.selectedIDs().indexOf(datum.id) >= 0 && - (rtClick || context.selectedIDs().length === 1)) { - mode.suppressMenu(false).reselect(); - } else { - context.enter(modeSelect(context, [datum.id])); - } - } else if (context.selectedIDs().indexOf(datum.id) >= 0) { - if (rtClick) { // To prevent datum.id from being removed when rtClick - mode.suppressMenu(false).reselect(); - } else { - var selectedIDs = _.without(context.selectedIDs(), datum.id); - context.enter(selectedIDs.length ? modeSelect(context, selectedIDs) : modeBrowse(context)); + if (datum.type === 'midpoint') { + // clicked midpoint, do nothing.. + + } else if (!(datum instanceof osmEntity)) { + // clicked nothing.. + if (!isMultiselect && mode.id !== 'browse') { + context.enter(modeBrowse(context)); } + } else { - context.enter(modeSelect(context, context.selectedIDs().concat([datum.id]))); + // clicked an entity.. + var selectedIDs = context.selectedIDs(); + + if (!isMultiselect) { + if (selectedIDs.length > 1 && !suppressMenu) { + // 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)); + } + + } else { + if (selectedIDs.indexOf(datum.id) !== -1) { + // clicked entity is already in the selectedIDs list.. + if (!suppressMenu) { + // don't deselect clicked entity, just show the menu. + mode.suppressMenu(false).reselect(); + } else { + // deselect clicked entity, then reenter select mode or return to browse mode.. + selectedIDs = _.without(selectedIDs, datum.id); + context.enter(selectedIDs.length ? modeSelect(context, selectedIDs) : modeBrowse(context)); + } + } else { + // clicked entity is not in the selected list, add it.. + selectedIDs = selectedIDs.concat([datum.id]); + context.enter(modeSelect(context, selectedIDs).suppressMenu(suppressMenu)); + } + } } + + // reset for next time.. + suppressMenu = true; } @@ -70,8 +121,9 @@ export function behaviorSelect(context) { .on('keydown.select', keydown) .on('keyup.select', keyup); - selection.on('click.select', click); - selection.on('contextmenu.select', click); + selection + .on('mousedown.select', mousedown) + .on('contextmenu.select', contextmenu); keydown(); }; @@ -80,9 +132,12 @@ export function behaviorSelect(context) { behavior.off = function(selection) { d3.select(window) .on('keydown.select', null) - .on('keyup.select', null); + .on('keyup.select', null) + .on('mouseup.select', null, true); - selection.on('click.select', null); + selection + .on('mousedown.select', null) + .on('contextmenu.select', null); keyup(); }; diff --git a/modules/modes/select.js b/modules/modes/select.js index afe12def3..2aa34a793 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -193,7 +193,7 @@ export function modeSelect(context, selectedIDs) { } positionMenu(); - if (d3.event && d3.event.type === 'contextmenu') { + if (!suppressMenu) { showMenu(); } }; @@ -438,13 +438,15 @@ export function modeSelect(context, selectedIDs) { .on('move.select', closeMenu) .on('drawn.select', selectElements); + context.surface() + .on('dblclick.select', dblclick); + + selectElements(); - var show = d3.event; - var rtClick = d3.event && d3.event.type === 'contextmenu'; - - if (show) { - positionMenu(); + if (selectedIDs.length > 1) { + var entities = uiSelectionList(context, selectedIDs); + context.ui().sidebar.show(entities); } if (follow) { @@ -460,18 +462,12 @@ export function modeSelect(context, selectedIDs) { } timeout = window.setTimeout(function() { - if (!suppressMenu && rtClick) { + positionMenu(); + if (!suppressMenu) { showMenu(); } + }, 270); /* after any centerEase completes */ - context.surface() - .on('dblclick.select', dblclick); - }, 200); - - if (selectedIDs.length > 1) { - var entities = uiSelectionList(context, selectedIDs); - context.ui().sidebar.show(entities); - } }; diff --git a/test/spec/behavior/select.js b/test/spec/behavior/select.js index 5deddc548..bcd837040 100644 --- a/test/spec/behavior/select.js +++ b/test/spec/behavior/select.js @@ -44,37 +44,49 @@ describe('iD.behaviorSelect', function() { }); specify('click on entity selects the entity', function() { - happen.click(context.surface().selectAll('.' + a.id).node()); + var el = context.surface().selectAll('.' + a.id).node(); + happen.mousedown(el); + happen.mouseup(el); expect(context.selectedIDs()).to.eql([a.id]); }); specify('click on empty space clears the selection', function() { context.enter(iD.modeSelect(context, [a.id])); - happen.click(context.surface().node()); + var el = context.surface().node(); + happen.mousedown(el); + happen.mouseup(el); expect(context.mode().id).to.eql('browse'); }); specify('shift-click on unselected entity adds it to the selection', function() { context.enter(iD.modeSelect(context, [a.id])); - happen.click(context.surface().selectAll('.' + b.id).node(), {shiftKey: true}); + var el = context.surface().selectAll('.' + b.id).node(); + happen.mousedown(el, { shiftKey: true }); + happen.mouseup(el, { shiftKey: true }); expect(context.selectedIDs()).to.eql([a.id, b.id]); }); specify('shift-click on selected entity removes it from the selection', function() { context.enter(iD.modeSelect(context, [a.id, b.id])); - happen.click(context.surface().selectAll('.' + b.id).node(), {shiftKey: true}); + var el = context.surface().selectAll('.' + b.id).node(); + happen.mousedown(el, { shiftKey: true }); + happen.mouseup(el, { shiftKey: true }); expect(context.selectedIDs()).to.eql([a.id]); }); specify('shift-click on last selected entity clears the selection', function() { context.enter(iD.modeSelect(context, [a.id])); - happen.click(context.surface().selectAll('.' + a.id).node(), {shiftKey: true}); + var el = context.surface().selectAll('.' + a.id).node(); + happen.mousedown(el, { shiftKey: true }); + happen.mouseup(el, { shiftKey: true }); expect(context.mode().id).to.eql('browse'); }); specify('shift-click on empty space leaves the selection unchanged', function() { context.enter(iD.modeSelect(context, [a.id])); - happen.click(context.surface().node(), {shiftKey: true}); + var el = context.surface().node(); + happen.mousedown(el, { shiftKey: true }); + happen.mouseup(el, { shiftKey: true }); expect(context.selectedIDs()).to.eql([a.id]); }); });