From eb7c56d310c524cf869c8a5ab4c8cceefd4385e6 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 1 Nov 2016 21:43:26 -0400 Subject: [PATCH 1/6] Add vertex keyboard navigation (closes #1917) --- modules/modes/select.js | 149 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/modules/modes/select.js b/modules/modes/select.js index 064f197df..55cff105e 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -32,6 +32,9 @@ import { uiRadialMenu, uiSelectionList } from '../ui/index'; import { utilEntityOrMemberSelector } from '../util/index'; +var selectedLastParent; + + export function modeSelect(context, selectedIDs) { var mode = { id: 'select', @@ -52,7 +55,9 @@ export function modeSelect(context, selectedIDs) { inspector, radialMenu, newFeature = false, - suppressMenu = false; + suppressMenu = false, + follow = false; + var wrap = context.container() .select('.inspector-wrap'); @@ -65,6 +70,53 @@ export function modeSelect(context, selectedIDs) { } + // find the common parent ways for nextNode, previousNode + function commonParents() { + var graph = context.graph(), + commonParents = []; + + for (var i = 0; i < selectedIDs.length; i++) { + var entity = context.hasEntity(selectedIDs[i]); + if (!entity || entity.geometry(graph) !== 'vertex') { + return []; // selection includes some not vertexes + } + + var currParents = _.map(graph.parentWays(entity), 'id'); + if (!commonParents.length) { + commonParents = currParents; + continue; + } + + commonParents = _.intersection(commonParents, currParents); + if (!commonParents.length) { + return []; + } + } + + return commonParents; + } + + + function singularParent() { + var parents = commonParents(); + if (!parents) return; + + // selectedLastParent is used when we visit a vertex with multiple + // parents, and we want to remember which parent line we started on. + + if (parents.length === 1) { + selectedLastParent = parents[0]; // remember this parent for later + return selectedLastParent; + } + + if (parents.indexOf(selectedLastParent) !== -1) { + return selectedLastParent; // prefer the previously seen parent + } + + return parents[0]; + } + + function closeMenu() { if (radialMenu) { context.surface().call(radialMenu.close); @@ -139,6 +191,13 @@ export function modeSelect(context, selectedIDs) { }; + mode.follow = function(_) { + if (!arguments.length) return follow; + follow = _; + return mode; + }; + + mode.enter = function() { function update() { @@ -200,6 +259,78 @@ export function modeSelect(context, selectedIDs) { } + function firstNode() { + d3.event.preventDefault(); + var parent = singularParent(); + if (parent) { + var way = context.entity(parent); + context.enter( + modeSelect(context, [way.first()]).follow(true) + ); + } + } + + + function lastNode() { + d3.event.preventDefault(); + var parent = singularParent(); + if (parent) { + var way = context.entity(parent); + context.enter( + modeSelect(context, [way.last()]).follow(true) + ); + } + } + + + function previousNode() { + d3.event.preventDefault(); + var parent = singularParent(); + if (!parent) return; + + var way = context.entity(parent), + length = way.nodes.length, + curr = way.nodes.indexOf(selectedIDs[0]), + index = -1; + + if (curr > 0) { + index = curr - 1; + } else if (way.isClosed()) { + index = length - 2; + } + + if (index !== -1) { + context.enter( + modeSelect(context, [way.nodes[index]]).follow(true) + ); + } + } + + + function nextNode() { + d3.event.preventDefault(); + var parent = singularParent(); + if (!parent) return; + + var way = context.entity(parent), + length = way.nodes.length, + curr = way.nodes.indexOf(selectedIDs[0]), + index = -1; + + if (curr < length - 1) { + index = curr + 1; + } else if (way.isClosed()) { + index = 0; + } + + if (index !== -1) { + context.enter( + modeSelect(context, [way.nodes[index]]).follow(true) + ); + } + } + + behaviors.forEach(function(behavior) { context.install(behavior); }); @@ -211,6 +342,10 @@ export function modeSelect(context, selectedIDs) { operations.unshift(Operations.operationDelete(selectedIDs, context)); keybinding + .on('[', previousNode) + .on(']', nextNode) + .on('⌘[', firstNode) + .on('⌘]', lastNode) .on('⎋', esc, true) .on('space', toggleMenu); @@ -248,6 +383,18 @@ export function modeSelect(context, selectedIDs) { positionMenu(); } + if (follow) { + var extent = geoExtent(), + graph = context.graph(); + selectedIDs.forEach(function(id) { + var entity = context.entity(id); + extent._extend(entity.extent(graph)); + }); + + var loc = extent.center(); + context.map().centerEase(loc); + } + timeout = window.setTimeout(function() { if (show) { showMenu(); From 2273a6ed43626e65cfa5908557da24827f5cef59 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 4 Nov 2016 09:41:44 -0400 Subject: [PATCH 2/6] Allow multiple key bindings to be specified in an array --- modules/lib/d3.keybinding.js | 49 +++++++++++++++++++--------------- test/spec/lib/d3.keybinding.js | 10 +++++++ 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/modules/lib/d3.keybinding.js b/modules/lib/d3.keybinding.js index c152f6d97..cc98ab242 100644 --- a/modules/lib/d3.keybinding.js +++ b/modules/lib/d3.keybinding.js @@ -58,33 +58,37 @@ export function d3keybinding(namespace) { return keybinding; }; - keybinding.on = function(code, callback, capture) { - var binding = { - event: { - keyCode: 0, - shiftKey: false, - ctrlKey: false, - altKey: false, - metaKey: false - }, - capture: capture, - callback: callback - }; + keybinding.on = function(codes, callback, capture) { + var arr = [].concat(codes); + for (var i = 0; i < arr.length; i++) { + var code = arr[i]; + var binding = { + event: { + keyCode: 0, + shiftKey: false, + ctrlKey: false, + altKey: false, + metaKey: false + }, + capture: capture, + callback: callback + }; - code = code.toLowerCase().match(/(?:(?:[^+⇧⌃⌥⌘])+|[⇧⌃⌥⌘]|\+\+|^\+$)/g); + code = code.toLowerCase().match(/(?:(?:[^+⇧⌃⌥⌘])+|[⇧⌃⌥⌘]|\+\+|^\+$)/g); - for (var i = 0; i < code.length; i++) { - // Normalise matching errors - if (code[i] === '++') code[i] = '+'; + for (var j = 0; j < code.length; j++) { + // Normalise matching errors + if (code[j] === '++') code[i] = '+'; - if (code[i] in d3keybinding.modifierCodes) { - binding.event[d3keybinding.modifierProperties[d3keybinding.modifierCodes[code[i]]]] = true; - } else if (code[i] in d3keybinding.keyCodes) { - binding.event.keyCode = d3keybinding.keyCodes[code[i]]; + if (code[j] in d3keybinding.modifierCodes) { + binding.event[d3keybinding.modifierProperties[d3keybinding.modifierCodes[code[j]]]] = true; + } else if (code[j] in d3keybinding.keyCodes) { + binding.event.keyCode = d3keybinding.keyCodes[code[j]]; + } } - } - bindings.push(binding); + bindings.push(binding); + } return keybinding; }; @@ -92,6 +96,7 @@ export function d3keybinding(namespace) { return keybinding; } + d3keybinding.modifierCodes = { // Shift key, ⇧ '⇧': 16, shift: 16, diff --git a/test/spec/lib/d3.keybinding.js b/test/spec/lib/d3.keybinding.js index 79a43a225..62f45118b 100644 --- a/test/spec/lib/d3.keybinding.js +++ b/test/spec/lib/d3.keybinding.js @@ -38,6 +38,16 @@ describe('d3.keybinding', function() { expect(spy).to.have.been.calledOnce; }); + it('adds multiple bindings given an array of keys', function () { + d3.select(document).call(keybinding.on(['A','B'], spy)); + + happen.keydown(document, {keyCode: 65}); + expect(spy).to.have.been.calledOnce; + + happen.keydown(document, {keyCode: 66}); + expect(spy).to.have.been.calledTwice; + }); + it('does not dispatch when focus is in input elements by default', function () { d3.select(document).call(keybinding.on('A', spy)); From d1c7b5c8a24a2e841eac2b86ce3eabf646c6b05e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 4 Nov 2016 09:49:26 -0400 Subject: [PATCH 3/6] Add alternate vertex navigation keys pgup/pdgown/home/end --- modules/modes/select.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/modes/select.js b/modules/modes/select.js index 55cff105e..70149a958 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -342,10 +342,10 @@ export function modeSelect(context, selectedIDs) { operations.unshift(Operations.operationDelete(selectedIDs, context)); keybinding - .on('[', previousNode) - .on(']', nextNode) - .on('⌘[', firstNode) - .on('⌘]', lastNode) + .on(['[','pgup'], previousNode) + .on([']', 'pgdown'], nextNode) + .on(['⌘[', 'home'], firstNode) + .on(['⌘]', 'end'], lastNode) .on('⎋', esc, true) .on('space', toggleMenu); From 3224130821cdd36222aa9ed5157062b55b24baf8 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 4 Nov 2016 10:17:30 -0400 Subject: [PATCH 4/6] Add uiCmd to fix modifier, add keybind arrays to simplify code --- modules/modes/select.js | 5 +++-- modules/ui/init.js | 12 ++++-------- modules/ui/zoom.js | 12 ++++-------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/modules/modes/select.js b/modules/modes/select.js index 70149a958..94eacaeec 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -29,6 +29,7 @@ import { modeBrowse } from './browse'; import { modeDragNode } from './drag_node'; import * as Operations from '../operations/index'; import { uiRadialMenu, uiSelectionList } from '../ui/index'; +import { uiCmd } from '../ui/cmd'; import { utilEntityOrMemberSelector } from '../util/index'; @@ -344,8 +345,8 @@ export function modeSelect(context, selectedIDs) { keybinding .on(['[','pgup'], previousNode) .on([']', 'pgdown'], nextNode) - .on(['⌘[', 'home'], firstNode) - .on(['⌘]', 'end'], lastNode) + .on([uiCmd('⌘['), 'home'], firstNode) + .on([uiCmd('⌘]'), 'end'], lastNode) .on('⎋', esc, true) .on('space', toggleMenu); diff --git a/modules/ui/init.js b/modules/ui/init.js index 801e0c461..4f9d682ac 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -256,14 +256,10 @@ export function uiInit(context) { .on('↑', pan([0, pa])) .on('→', pan([-pa, 0])) .on('↓', pan([0, -pa])) - .on('⇧←', pan([mapDimensions[0], 0])) - .on('⇧↑', pan([0, mapDimensions[1]])) - .on('⇧→', pan([-mapDimensions[0], 0])) - .on('⇧↓', pan([0, -mapDimensions[1]])) - .on(uiCmd('⌘←'), pan([mapDimensions[0], 0])) - .on(uiCmd('⌘↑'), pan([0, mapDimensions[1]])) - .on(uiCmd('⌘→'), pan([-mapDimensions[0], 0])) - .on(uiCmd('⌘↓'), pan([0, -mapDimensions[1]])); + .on(['⇧←', uiCmd('⌘←')], pan([mapDimensions[0], 0])) + .on(['⇧↑', uiCmd('⌘↑')], pan([0, mapDimensions[1]])) + .on(['⇧→', uiCmd('⌘→')], pan([-mapDimensions[0], 0])) + .on(['⇧↓', uiCmd('⌘↓')], pan([0, -mapDimensions[1]])); d3.select(document) .call(keybinding); diff --git a/modules/ui/zoom.js b/modules/ui/zoom.js index ae0527693..685671d93 100644 --- a/modules/ui/zoom.js +++ b/modules/ui/zoom.js @@ -72,16 +72,12 @@ export function uiZoom(context) { var keybinding = d3keybinding('zoom'); _.each(['=','ffequals','plus','ffplus'], function(key) { - keybinding.on(key, zoomIn); - keybinding.on('⇧' + key, zoomIn); - keybinding.on(uiCmd('⌘' + key), zoomInFurther); - keybinding.on(uiCmd('⌘⇧' + key), zoomInFurther); + keybinding.on([key, '⇧' + key], zoomIn); + keybinding.on([uiCmd('⌘' + key), uiCmd('⌘⇧' + key)], zoomInFurther); }); _.each(['-','ffminus','_','dash'], function(key) { - keybinding.on(key, zoomOut); - keybinding.on('⇧' + key, zoomOut); - keybinding.on(uiCmd('⌘' + key), zoomOutFurther); - keybinding.on(uiCmd('⌘⇧' + key), zoomOutFurther); + keybinding.on([key, '⇧' + key], zoomOut); + keybinding.on([uiCmd('⌘' + key), uiCmd('⌘⇧' + key)], zoomOutFurther); }); d3.select(document) From 25624609622d5b847ea365235d65f048f0c4fa36 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 7 Nov 2016 11:46:38 -0500 Subject: [PATCH 5/6] Exit to browse mode if selected items gone, only in response to user drags --- modules/modes/select.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/modes/select.js b/modules/modes/select.js index 94eacaeec..658814d69 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -243,7 +243,10 @@ export function modeSelect(context, selectedIDs) { .selectAll(utilEntityOrMemberSelector(selectedIDs, context.graph())); if (selection.empty()) { - if (drawn) { // Exit mode if selected DOM elements have disappeared.. + // Return to browse mode if selected DOM elements have + // disappeared because the user moved them out of view.. + var source = d3.event && d3.event.type === 'zoom' && d3.event.sourceEvent; + if (drawn && source && (source.type === 'mousemove' || source.type === 'touchmove')) { context.enter(modeBrowse(context)); } } else { From 774b3a0ccd89a23da3448d38dbcf8a56f68b7779 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 7 Nov 2016 13:44:25 -0500 Subject: [PATCH 6/6] Add '\' key to toggle related parent, add .related class (like hover) --- css/map.css | 5 +++ modules/modes/select.js | 84 ++++++++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/css/map.css b/css/map.css index 250f23aed..c0d5f42ec 100644 --- a/css/map.css +++ b/css/map.css @@ -37,6 +37,7 @@ g.point .shadow { stroke-opacity: 0; } +g.point.related:not(.selected) .shadow, g.point.hover:not(.selected) .shadow { stroke-opacity: 0.5; } @@ -104,7 +105,9 @@ g.vertex.vertex-hover { display: block; } +g.vertex.related:not(.selected) .shadow, g.vertex.hover:not(.selected) .shadow, +g.midpoint.related:not(.selected) .shadow, g.midpoint.hover:not(.selected) .shadow { fill-opacity: 0.5; } @@ -144,6 +147,7 @@ path.shadow { stroke-opacity: 0; } +path.shadow.related:not(.selected), path.shadow.hover:not(.selected) { stroke-opacity: 0.4; } @@ -1600,6 +1604,7 @@ text.gpx { stroke-width: 8; } +.fill-wireframe path.shadow.related:not(.selected), .fill-wireframe path.shadow.hover:not(.selected) { stroke-opacity: 0.4; } diff --git a/modules/modes/select.js b/modules/modes/select.js index 658814d69..06d88b544 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -30,10 +30,10 @@ import { modeDragNode } from './drag_node'; import * as Operations from '../operations/index'; import { uiRadialMenu, uiSelectionList } from '../ui/index'; import { uiCmd } from '../ui/cmd'; -import { utilEntityOrMemberSelector } from '../util/index'; +import { utilEntityOrMemberSelector, utilEntitySelector } from '../util/index'; -var selectedLastParent; +var relatedParent; export function modeSelect(context, selectedIDs) { @@ -71,7 +71,7 @@ export function modeSelect(context, selectedIDs) { } - // find the common parent ways for nextNode, previousNode + // find the common parent ways for nextVertex, previousVertex function commonParents() { var graph = context.graph(), commonParents = []; @@ -100,18 +100,21 @@ export function modeSelect(context, selectedIDs) { function singularParent() { var parents = commonParents(); - if (!parents) return; + if (!parents) { + relatedParent = null; + return null; + } - // selectedLastParent is used when we visit a vertex with multiple + // relatedParent is used when we visit a vertex with multiple // parents, and we want to remember which parent line we started on. if (parents.length === 1) { - selectedLastParent = parents[0]; // remember this parent for later - return selectedLastParent; + relatedParent = parents[0]; // remember this parent for later + return relatedParent; } - if (parents.indexOf(selectedLastParent) !== -1) { - return selectedLastParent; // prefer the previously seen parent + if (parents.indexOf(relatedParent) !== -1) { + return relatedParent; // prefer the previously seen parent } return parents[0]; @@ -233,14 +236,24 @@ export function modeSelect(context, selectedIDs) { function selectElements(drawn) { - var entity = singular(); + var surface = context.surface(), + entity = singular(); + if (entity && context.geometry(entity.id) === 'relation') { suppressMenu = true; return; } + var parent = singularParent(); + if (parent) { + surface.selectAll('.related') + .classed('related', false); + surface.selectAll(utilEntitySelector([relatedParent])) + .classed('related', true); + } + var selection = context.surface() - .selectAll(utilEntityOrMemberSelector(selectedIDs, context.graph())); + .selectAll(utilEntityOrMemberSelector(selectedIDs, context.graph())); if (selection.empty()) { // Return to browse mode if selected DOM elements have @@ -263,7 +276,7 @@ export function modeSelect(context, selectedIDs) { } - function firstNode() { + function firstVertex() { d3.event.preventDefault(); var parent = singularParent(); if (parent) { @@ -275,7 +288,7 @@ export function modeSelect(context, selectedIDs) { } - function lastNode() { + function lastVertex() { d3.event.preventDefault(); var parent = singularParent(); if (parent) { @@ -287,7 +300,7 @@ export function modeSelect(context, selectedIDs) { } - function previousNode() { + function previousVertex() { d3.event.preventDefault(); var parent = singularParent(); if (!parent) return; @@ -311,7 +324,7 @@ export function modeSelect(context, selectedIDs) { } - function nextNode() { + function nextVertex() { d3.event.preventDefault(); var parent = singularParent(); if (!parent) return; @@ -335,6 +348,26 @@ export function modeSelect(context, selectedIDs) { } + function nextParent() { + d3.event.preventDefault(); + var parents = _.uniq(commonParents()); + if (!parents || parents.length < 2) return; + + var index = parents.indexOf(relatedParent); + if (index < 0 || index > parents.length - 2) { + relatedParent = parents[0]; + } else { + relatedParent = parents[index + 1]; + } + + var surface = context.surface(); + surface.selectAll('.related') + .classed('related', false); + surface.selectAll(utilEntitySelector([relatedParent])) + .classed('related', true); + } + + behaviors.forEach(function(behavior) { context.install(behavior); }); @@ -346,10 +379,11 @@ export function modeSelect(context, selectedIDs) { operations.unshift(Operations.operationDelete(selectedIDs, context)); keybinding - .on(['[','pgup'], previousNode) - .on([']', 'pgdown'], nextNode) - .on([uiCmd('⌘['), 'home'], firstNode) - .on([uiCmd('⌘]'), 'end'], lastNode) + .on(['[','pgup'], previousVertex) + .on([']', 'pgdown'], nextVertex) + .on([uiCmd('⌘['), 'home'], firstVertex) + .on([uiCmd('⌘]'), 'end'], lastVertex) + .on(['\\', 'pause'], nextParent) .on('⎋', esc, true) .on('space', toggleMenu); @@ -432,11 +466,19 @@ export function modeSelect(context, selectedIDs) { .on('undone.select', null) .on('redone.select', null); - context.surface() - .on('dblclick.select', null) + var surface = context.surface(); + + surface + .on('dblclick.select', null); + + surface .selectAll('.selected') .classed('selected', false); + surface + .selectAll('.related') + .classed('related', false); + context.map().on('drawn.select', null); context.ui().sidebar.hide(); };