Merge pull request #3539 from openstreetmap/vertex-keyboard-nav

Add vertex keyboard navigation
This commit is contained in:
Bryan Housel
2016-11-07 14:06:59 -05:00
committed by GitHub
6 changed files with 250 additions and 45 deletions
+5
View File
@@ -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;
}
@@ -1612,6 +1616,7 @@ text.gpx {
stroke-width: 8;
}
.fill-wireframe path.shadow.related:not(.selected),
.fill-wireframe path.shadow.hover:not(.selected) {
stroke-opacity: 0.4;
}
+27 -22
View File
@@ -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,
+200 -7
View File
@@ -29,7 +29,11 @@ import { modeBrowse } from './browse';
import { modeDragNode } from './drag_node';
import * as Operations from '../operations/index';
import { uiRadialMenu, uiSelectionList } from '../ui/index';
import { utilEntityOrMemberSelector } from '../util/index';
import { uiCmd } from '../ui/cmd';
import { utilEntityOrMemberSelector, utilEntitySelector } from '../util/index';
var relatedParent;
export function modeSelect(context, selectedIDs) {
@@ -52,7 +56,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 +71,56 @@ export function modeSelect(context, selectedIDs) {
}
// find the common parent ways for nextVertex, previousVertex
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) {
relatedParent = null;
return null;
}
// 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) {
relatedParent = parents[0]; // remember this parent for later
return relatedParent;
}
if (parents.indexOf(relatedParent) !== -1) {
return relatedParent; // prefer the previously seen parent
}
return parents[0];
}
function closeMenu() {
if (radialMenu) {
context.surface().call(radialMenu.close);
@@ -139,6 +195,13 @@ export function modeSelect(context, selectedIDs) {
};
mode.follow = function(_) {
if (!arguments.length) return follow;
follow = _;
return mode;
};
mode.enter = function() {
function update() {
@@ -173,17 +236,30 @@ 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()) {
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 {
@@ -200,6 +276,98 @@ export function modeSelect(context, selectedIDs) {
}
function firstVertex() {
d3.event.preventDefault();
var parent = singularParent();
if (parent) {
var way = context.entity(parent);
context.enter(
modeSelect(context, [way.first()]).follow(true)
);
}
}
function lastVertex() {
d3.event.preventDefault();
var parent = singularParent();
if (parent) {
var way = context.entity(parent);
context.enter(
modeSelect(context, [way.last()]).follow(true)
);
}
}
function previousVertex() {
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 nextVertex() {
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)
);
}
}
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);
});
@@ -211,6 +379,11 @@ export function modeSelect(context, selectedIDs) {
operations.unshift(Operations.operationDelete(selectedIDs, context));
keybinding
.on(['[','pgup'], previousVertex)
.on([']', 'pgdown'], nextVertex)
.on([uiCmd('⌘['), 'home'], firstVertex)
.on([uiCmd('⌘]'), 'end'], lastVertex)
.on(['\\', 'pause'], nextParent)
.on('⎋', esc, true)
.on('space', toggleMenu);
@@ -248,6 +421,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();
@@ -281,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();
};
+4 -8
View File
@@ -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);
+4 -8
View File
@@ -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)
+10
View File
@@ -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));