Merge pull request #314 from rkflx/pr/add-and-fix-shortcuts

Add more keyboard shortcuts and fix various shortcut related issues
This commit is contained in:
Norbert Renner 2020-06-19 21:26:13 +02:00 committed by GitHub
commit 77f1b5f0af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 433 additions and 107 deletions

View file

@ -2,6 +2,12 @@ BR.Map = {
initMap: function() {
var map, layersControl;
L.setOptions(this, {
shortcut: {
locate: 76 // char code for 'l'
}
});
BR.keys = BR.keys || {};
var maxZoom = 19;
@ -96,7 +102,7 @@ BR.Map = {
var secureContext = 'isSecureContext' in window ? isSecureContext : location.protocol === 'https:';
if (secureContext) {
L.control
var locationControl = L.control
.locate({
strings: {
title: i18next.t('map.locate-me')
@ -105,6 +111,16 @@ BR.Map = {
iconLoading: 'fa fa-spinner fa-pulse'
})
.addTo(map);
L.DomEvent.addListener(
document,
'keydown',
function(e) {
if (BR.Util.keyboardShortcutsAllowed(e) && e.keyCode === this.options.shortcut.locate) {
locationControl.start();
}
},
this
);
}
L.control.scale().addTo(map);

View file

@ -67,5 +67,35 @@ BR.Util = {
}
$el.remove();
return env;
},
keyboardShortcutsAllowed: function(keyEvent) {
// Skip auto-repeating key events
if (keyEvent.repeat) {
return false;
}
// Suppress shortcut handling when a text or number input field is focussed
if (
document.activeElement.type == 'number' ||
document.activeElement.type == 'text' ||
document.activeElement.type == 'textarea'
) {
return false;
}
// Only allow shortcuts without modifiers for now, to prevent triggering map functions
// when browser shortcuts are triggered (e.g. Ctrl+P for print should not add a POI)
if (keyEvent.ctrlKey || keyEvent.metaKey || keyEvent.altKey) {
return false;
}
// Do not allow shortcuts triggering actions behind Bootstrap's
// modal dialogs or when dropdown menus are opened
if ($('.modal.show').length || $('.dropdown.show').length) {
return false;
}
return true;
}
};

View file

@ -1,6 +1,12 @@
BR.Export = L.Class.extend({
latLngs: [],
options: {
shortcut: {
export: 88 // char code for 'x'
}
},
initialize: function(router, pois) {
this.router = router;
this.pois = pois;
@ -20,6 +26,8 @@ BR.Export = L.Class.extend({
this.exportButton.on('click', L.bind(this._generateTrackname, this));
L.DomUtil.get('submitExport').onclick = L.bind(this._export, this);
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
this.update([]);
},
@ -116,6 +124,17 @@ BR.Export = L.Class.extend({
}
})
);
},
_keydownListener: function(e) {
if (
BR.Util.keyboardShortcutsAllowed(e) &&
e.keyCode === this.options.shortcut.export &&
!this.exportButton.hasClass('disabled')
) {
this._generateTrackname();
$('#export').modal('show');
}
}
});

View file

@ -52,23 +52,13 @@ BR.OpacitySlider = L.Class.extend({
},
_keydownListener: function(e) {
// Suppress shortcut handling when a text input field is focussed
if (document.activeElement.type == 'text' || document.activeElement.type == 'textarea') {
return;
}
if (e.keyCode === this.options.muteKeyCode && !e.repeat) {
if (BR.Util.keyboardShortcutsAllowed(e) && e.keyCode === this.options.muteKeyCode) {
this.options.callback(0);
}
},
_keyupListener: function(e) {
// Suppress shortcut handling when a text input field is focussed
if (document.activeElement.type == 'text' || document.activeElement.type == 'textarea') {
return;
}
if (e.keyCode === this.options.muteKeyCode && !e.repeat) {
if (BR.Util.keyboardShortcutsAllowed(e) && e.keyCode === this.options.muteKeyCode) {
this.options.callback(this.input.val() / 100);
}
},

View file

@ -1,4 +1,10 @@
BR.RoutingOptions = L.Evented.extend({
options: {
shortcut: {
switch: 71 // char code for 'g'
}
},
initialize: function() {
$('#profile-alternative').on('changed.bs.select', this._getChangeHandler());
@ -15,6 +21,8 @@ BR.RoutingOptions = L.Evented.extend({
profiles_list.children[0].value = 'Custom';
// <custom> profile is empty at start, select next one
profiles_list.children[1].selected = true;
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
},
refreshUI: function() {
@ -37,6 +45,10 @@ BR.RoutingOptions = L.Evented.extend({
custom.disabled = true;
}
$('.selectpicker').selectpicker('refresh');
// append shortcut text to tooltip
var button = $('#profile-alternative-form button')[0];
button.title = button.title + i18next.t('navbar.profile-tooltip');
},
getOptions: function() {
@ -105,5 +117,13 @@ BR.RoutingOptions = L.Evented.extend({
return L.bind(function(evt) {
this.fire('update', { options: this.getOptions() });
}, this);
},
_keydownListener: function(e) {
if (BR.Util.keyboardShortcutsAllowed(e) && e.keyCode === this.options.shortcut.switch) {
if (!$('#profile-alternative-form .dropdown').hasClass('show')) {
$('#profile-alternative-form button').click();
}
}
}
});

View file

@ -46,6 +46,7 @@
search = new BR.Search();
map.addControl(search);
$('#map .leaflet-control-geocoder > button')[0].title = i18next.t('map.geocoder');
router = L.bRouter(); //brouterCgi dummyRouter
@ -83,7 +84,7 @@
var deletePointButton = L.easyButton(
'<span><i class="fa fa-caret-left"></i><i class="fa fa-map-marker" style="margin-left: 1px; color: gray;"></i></span>',
function() {
routing.removeWaypoint(routing.getLast(), function(err, data) {});
routing.deleteLastPoint();
},
i18next.t('map.delete-last-point')
);
@ -91,45 +92,66 @@
deleteRouteButton = L.easyButton(
'fa-trash-o',
function() {
bootbox.prompt({
size: 'small',
title: i18next.t('map.clear-route'),
inputType: 'checkbox',
inputOptions: [
{
text: i18next.t('map.delete-route'),
value: 'route'
},
{
text: i18next.t('map.delete-nogo-areas'),
value: 'nogo'
},
{
text: i18next.t('map.delete-pois'),
value: 'pois'
}
],
value: ['route'],
callback: function(result) {
if (result !== null) {
if (result.indexOf('route') !== -1) {
routing.clear();
}
if (result.indexOf('nogo') !== -1) {
nogos.clear();
}
if (result.indexOf('pois') !== -1) {
pois.clear();
}
onUpdate();
urlHash.onMapMove();
}
}
});
clearRoute();
},
i18next.t('map.clear-route')
i18next.t('map.clear-route-tooltip')
);
L.DomEvent.addListener(
document,
'keydown',
function(e) {
if (BR.Util.keyboardShortcutsAllowed(e) && !$('.modal.show').length) {
if (e.keyCode === 8) {
// char code for 'backspace'
clearRoute();
} else if (e.keyCode === 72) {
// char code for 'h'
$('#about').modal('show');
}
}
},
this
);
function clearRoute() {
bootbox.prompt({
size: 'small',
title: i18next.t('map.clear-route'),
inputType: 'checkbox',
inputOptions: [
{
text: i18next.t('map.delete-route'),
value: 'route'
},
{
text: i18next.t('map.delete-nogo-areas'),
value: 'nogo'
},
{
text: i18next.t('map.delete-pois'),
value: 'pois'
}
],
value: ['route'],
callback: function(result) {
if (result !== null) {
if (result.indexOf('route') !== -1) {
routing.clear();
}
if (result.indexOf('nogo') !== -1) {
nogos.clear();
}
if (result.indexOf('pois') !== -1) {
pois.clear();
}
onUpdate();
urlHash.onMapMove();
}
}
});
}
function updateRoute(evt) {
router.setOptions(evt.options);

View file

@ -7,7 +7,10 @@ BR.Elevation = L.Control.Elevation.extend({
bottom: 30,
left: 60
},
theme: 'steelblue-theme'
theme: 'steelblue-theme',
shortcut: {
toggle: 69 // char code for 'e'
}
},
onAdd: function(map) {
@ -30,6 +33,8 @@ BR.Elevation = L.Control.Elevation.extend({
L.DomEvent.on(this._container, 'mouseup', this._dragEndHandler, this);
}
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
return container;
},
@ -64,5 +69,11 @@ BR.Elevation = L.Control.Elevation.extend({
layer.on('mouseout', this._hidePositionMarker.bind(this));
}
},
_keydownListener: function(e) {
if (BR.Util.keyboardShortcutsAllowed(e) && e.keyCode === this.options.shortcut.toggle) {
$('#elevation-btn').click();
}
}
});

View file

@ -1,4 +1,13 @@
BR.NogoAreas = L.Control.extend({
options: {
shortcut: {
draw: {
enable: 78, // char code for 'n'
disable: 27 // char code for 'ESC'
}
}
},
statics: {
MSG_BUTTON: 'Draw no-go area (circle)',
MSG_BUTTON_CANCEL: 'Cancel drawing no-go area',
@ -48,28 +57,32 @@ BR.NogoAreas = L.Control.extend({
featuresLayer: this.drawnItems
}));
this.startDrawing = function(control) {
// initial radius of 0 to detect click, see DeletableCircleEditor.onDrawingMouseUp
var opts = L.extend({ radius: 0 }, self.style);
editTools.startCircle(null, opts);
control.state('cancel-no-go-create');
};
this.stopDrawing = function(control) {
editTools.stopDrawing();
control.state('no-go-create');
};
this.button = L.easyButton({
states: [
{
stateName: BR.NogoAreas.STATE_CREATE,
icon: 'fa-ban',
title: BR.NogoAreas.MSG_BUTTON,
onClick: function(control) {
// initial radius of 0 to detect click, see DeletableCircleEditor.onDrawingMouseUp
var opts = L.extend({ radius: 0 }, self.style);
editTools.startCircle(null, opts);
control.state('cancel-no-go-create');
}
onClick: this.startDrawing
},
{
stateName: BR.NogoAreas.STATE_CANCEL,
icon: 'fa-ban active',
title: BR.NogoAreas.MSG_BUTTON_CANCEL,
onClick: function(control) {
editTools.stopDrawing();
control.state('no-go-create');
}
onClick: this.stopDrawing
}
]
});
@ -78,6 +91,8 @@ BR.NogoAreas = L.Control.extend({
// events firing in Chrome mobile while L.Map.Tap enabled for circle drawing
L.DomEvent.addListener(this.button.button, 'pointerdown', L.DomEvent.stop);
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
this.editTools.on(
'editable:drawing:end',
function(e) {
@ -124,6 +139,20 @@ BR.NogoAreas = L.Control.extend({
return L.DomUtil.create('div');
},
_keydownListener: function(e) {
if (!BR.Util.keyboardShortcutsAllowed(e)) {
return;
}
if (e.keyCode === this.options.shortcut.draw.disable && this.button.state() === BR.NogoAreas.STATE_CANCEL) {
this.stopDrawing(this.button);
} else if (
e.keyCode === this.options.shortcut.draw.enable &&
this.button.state() === BR.NogoAreas.STATE_CREATE
) {
this.startDrawing(this.button);
}
},
displayUploadError: function(message) {
$('#nogoError').text(message ? message : '');
$('#nogoError').css('display', message ? 'block' : 'none');

View file

@ -42,11 +42,9 @@ BR.PoiMarkers = L.Control.extend({
self.draw(false);
});
var container = new L.DomUtil.create('div');
// keys not working when map container does not have focus, use document instead
L.DomEvent.removeListener(container, 'keyup', this._keyupListener);
L.DomEvent.addListener(document, 'keyup', this._keyupListener, this);
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
var container = new L.DomUtil.create('div');
return container;
},
@ -62,9 +60,8 @@ BR.PoiMarkers = L.Control.extend({
}
},
_keyupListener: function(e) {
// Suppress shortcut handling when a text input field is focussed
if (document.activeElement.type == 'text' || document.activeElement.type == 'textarea') {
_keydownListener: function(e) {
if (!BR.Util.keyboardShortcutsAllowed(e)) {
return;
}
if (e.keyCode === this.options.shortcut.draw.disable) {

View file

@ -23,6 +23,14 @@ BR.Routing = L.Routing.extend({
textFunction: function(distance) {
return distance / 1000;
}
},
shortcut: {
draw: {
enable: 68, // char code for 'd'
disable: 27 // char code for 'ESC'
},
reverse: 82, // char code for 'r'
deleteLastPoint: 90 // char code for 'z'
}
},
@ -163,8 +171,7 @@ BR.Routing = L.Routing.extend({
this._draw
);
// keys not working when map container does not have focus, use document instead
L.DomEvent.removeListener(this._container, 'keyup', this._keyupListener);
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
L.DomEvent.addListener(document, 'keyup', this._keyupListener, this);
// enable drawing mode
@ -336,16 +343,26 @@ BR.Routing = L.Routing.extend({
return segments;
},
_keyupListener: function(e) {
// Suppress shortcut handling when a text input field is focussed
if (document.activeElement.type == 'text' || document.activeElement.type == 'textarea') {
_keydownListener: function(e) {
if (!BR.Util.keyboardShortcutsAllowed(e)) {
return;
}
// add 'esc' to disable drawing
if (e.keyCode === 27) {
if (e.keyCode === this.options.shortcut.draw.disable) {
this._draw.disable();
} else {
L.Routing.prototype._keyupListener.call(this, e);
} else if (e.keyCode === this.options.shortcut.draw.enable) {
this._draw.enable();
} else if (e.keyCode === this.options.shortcut.reverse) {
this.reverse();
} else if (e.keyCode === this.options.shortcut.deleteLastPoint) {
this.deleteLastPoint();
}
},
_keyupListener: function(e) {
// Prevent Leaflet from triggering drawing a second time on keyup,
// since this is already done in _keydownListener
if (e.keyCode === this.options.shortcut.draw.enable) {
return;
}
},
@ -360,6 +377,12 @@ BR.Routing = L.Routing.extend({
this.setWaypoints(waypoints);
},
deleteLastPoint: function() {
if ((lastPoint = this.getLast())) {
this.removeWaypoint(lastPoint, function(err, data) {});
}
},
_removeDistanceMarkers: function() {
if (this._map && this._distanceMarkers) {
this._map.removeLayer(this._distanceMarkers);

View file

@ -1,4 +1,11 @@
BR.RoutingPathQuality = L.Control.extend({
options: {
shortcut: {
toggle: 67, // char code for 'c'
muteKeyCode: 77 // char code for 'm'
}
},
initialize: function(map, layersControl, options) {
L.setOptions(this, options);
@ -78,6 +85,7 @@ BR.RoutingPathQuality = L.Control.extend({
this.selectedProvider = this._initialProvider;
this._active = false;
this._muted = false;
},
onAdd: function(map) {
@ -133,6 +141,11 @@ BR.RoutingPathQuality = L.Control.extend({
});
}
if (this.options.shortcut.muteKeyCode || this.options.shortcut.toggle) {
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
L.DomEvent.addListener(document, 'keyup', this._keyupListener, this);
}
this.routingPathButton = new L.easyButton({
states: states
}).addTo(map);
@ -177,6 +190,26 @@ BR.RoutingPathQuality = L.Control.extend({
this._routingSegments.addLayer(layers[i]);
}
}
},
_keydownListener: function(e) {
if (!BR.Util.keyboardShortcutsAllowed(e)) {
return;
}
if (this._active && e.keyCode === this.options.shortcut.muteKeyCode) {
this._muted = true;
this._deactivate(this.routingPathButton);
}
if (!this._muted && e.keyCode === this.options.shortcut.toggle) {
this.routingPathButton.button.click();
}
},
_keyupListener: function(e) {
if (BR.Util.keyboardShortcutsAllowed(e) && this._muted && e.keyCode === this.options.shortcut.muteKeyCode) {
this._muted = false;
this._activate(this.routingPathButton);
}
}
});

View file

@ -6,7 +6,21 @@ BR.Search = L.Control.Geocoder.extend({
}),
sizeInMeters: 800
}),
position: 'topleft'
position: 'topleft',
shortcut: {
search: 70 // char code for 'f'
}
},
initialize: function(options) {
L.Control.Geocoder.prototype.initialize.call(this, options);
L.setOptions(this, {
// i18next.t will only return 'undefined' if it is called in a static context
// (e.g. when added directly to "options:" above), so we have to call it here
placeholder: i18next.t('map.geocoder-placeholder')
});
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
},
markGeocode: function(result) {
@ -29,5 +43,12 @@ BR.Search = L.Control.Geocoder.extend({
if (this._geocodeMarker) {
this._map.removeLayer(this._geocodeMarker);
}
},
_keydownListener: function(e) {
if (BR.Util.keyboardShortcutsAllowed(e) && e.keyCode === this.options.shortcut.search) {
$('#map .leaflet-control-geocoder')[0].dispatchEvent(new MouseEvent('mousedown'));
e.preventDefault();
}
}
});

View file

@ -8,6 +8,10 @@ BR.Sidebar = L.Control.Sidebar.extend({
autopan: false,
defaultTabId: '',
shortcut: {
toggleTabs: 84 // char code for 't'
},
// Tabs to be notified when shown or hidden
// (tab div id -> object implementing show/hide methods)
listeningTabs: {}
@ -17,6 +21,8 @@ BR.Sidebar = L.Control.Sidebar.extend({
L.Control.Sidebar.prototype.initialize.call(this, id, options);
this.oldTab = null;
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
},
addTo: function(map) {
@ -34,6 +40,15 @@ BR.Sidebar = L.Control.Sidebar.extend({
this
);
this.recentTab = this.options.defaultTabId;
this.on(
'content',
function(tab) {
this.recentTab = tab.id;
},
this
);
this._rememberTabState();
if (L.Browser.touch && BR.Browser.touchScreenDetectable && !BR.Browser.touchScreen) {
@ -103,6 +118,30 @@ BR.Sidebar = L.Control.Sidebar.extend({
_storeActiveTab: function(e) {
localStorage.setItem(this.storageId, e.id || '');
},
_keydownListener: function(e) {
if (BR.Util.keyboardShortcutsAllowed(e) && e.keyCode === this.options.shortcut.toggleTabs) {
if ($('#sidebarTabs > ul > li[class=active]').length) {
// sidebar is currently open
if (e.shiftKey) {
// try to find next tab
var nextTab = $('#sidebarTabs > ul > li[class=active] ~ li:not([hidden]) > a');
if (!nextTab.length) {
// wrap around to first tab
nextTab = $('#sidebarTabs > ul > li:not([hidden]) > a');
}
// switch to next or first tab
this.open(nextTab.attr('href').slice(1));
} else {
// close current tab
this.close();
}
} else {
// sidebar is currently closed, open recent or default tab
this.open(this.recentTab);
}
}
}
});

View file

@ -22,7 +22,10 @@ BR.tracksLoader = function(map, layersControl, routing) {
},
addToMap: false,
// File size limit in kb (default: 1024) ?
fileSizeLimit: 1024
fileSizeLimit: 1024,
shortcut: {
open: 79 // char code for 'o'
}
},
_initContainer: function() {
@ -31,6 +34,8 @@ BR.tracksLoader = function(map, layersControl, routing) {
var fileInput;
var container = L.DomUtil.get('navbarLoadTracksContainer');
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
// Create an invisible file input
fileInput = L.DomUtil.create('input', 'hidden', container);
fileInput.type = 'file';
@ -62,6 +67,16 @@ BR.tracksLoader = function(map, layersControl, routing) {
var dummy = L.DomUtil.create('div');
dummy.hidden = true;
return dummy;
},
_keydownListener: function(e) {
if (BR.Util.keyboardShortcutsAllowed(e) && e.keyCode === this.options.shortcut.open) {
if (e.shiftKey) {
$('#loadNogos').modal('show');
} else {
$('#navbarLoadTracks')[0].click();
}
}
}
});
var tracksLoaderControl = new TracksLoader();

View file

@ -16,6 +16,12 @@ BR.stravaSegments = function(map, layersControl) {
);
};
L.setOptions(this, {
shortcut: {
toggleLayer: 83 // char code for 's'
}
});
// hide strava buttons when layer is inactive
var toggleStravaControl = function() {
var stravaBar = stravaControl.runningButton.button.parentElement;
@ -24,5 +30,20 @@ BR.stravaSegments = function(map, layersControl) {
toggleStravaControl();
stravaControl.stravaLayer.on('add remove', toggleStravaControl);
L.DomEvent.addListener(
document,
'keydown',
function(e) {
if (BR.Util.keyboardShortcutsAllowed(e) && e.keyCode === this.options.shortcut.toggleLayer) {
if (map.hasLayer(stravaControl.stravaLayer)) {
map.removeLayer(stravaControl.stravaLayer);
} else {
map.addLayer(stravaControl.stravaLayer);
}
}
},
this
);
return stravaControl;
};