mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-15 21:48:20 +02:00
Merge pull request #3539 from openstreetmap/vertex-keyboard-nav
Add vertex keyboard navigation
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user