Files
iD/modules/util/keybinding.js
Martin Raifer e25af7c05e enable keybindings for unavailable operations -> show info message
this should make it easier to discover what a given keypress was supposed to do (e.g. which operation it triggered)

closes #9896
2025-05-15 17:03:31 +02:00

426 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
select as d3_select
} from 'd3-selection';
import { utilArrayUniq } from './array';
export function utilKeybinding(namespace) {
var _keybindings = {};
function testBindings(d3_event, isCapturing) {
var didMatch = false;
var bindings = Object.keys(_keybindings).map(function(id) { return _keybindings[id]; });
// Most key shortcuts will accept either lower or uppercase ('h' or 'H'),
// so we don't strictly match on the shift key, but we prioritize
// shifted keybindings first, and fallback to unshifted only if no match.
// (This lets us differentiate between '←'/'⇧←' or '⌘Z'/'⌘⇧Z')
// priority match shifted keybindings first
for (const binding of bindings) {
if (!binding.event.modifiers.shiftKey) continue; // no shift
if (!!binding.capture !== isCapturing) continue;
if (matches(d3_event, binding, true)) {
binding.callback(d3_event);
didMatch = true;
// match a max of one binding per event
break;
}
}
if (didMatch) return;
// then unshifted keybindings
for (const binding of bindings) {
if (binding.event.modifiers.shiftKey) continue; // shift
if (!!binding.capture !== isCapturing) continue;
if (matches(d3_event, binding, false)) {
binding.callback(d3_event);
break;
}
}
function matches(d3_event, binding, testShift) {
var event = d3_event;
var isMatch = false;
var tryKeyCode = true;
// Prefer a match on `KeyboardEvent.key`
if (event.key !== undefined) {
tryKeyCode = (event.key.charCodeAt(0) > 127); // outside ISO-Latin-1
isMatch = true;
if (binding.event.key === undefined) {
isMatch = false;
} else if (Array.isArray(binding.event.key)) {
if (binding.event.key.map(function(s) {
return s.toLowerCase();
}).indexOf(event.key.toLowerCase()) === -1) {
isMatch = false;
}
} else {
if (event.key.toLowerCase() !== binding.event.key.toLowerCase()) {
isMatch = false;
}
}
}
// Fallback match on `KeyboardEvent.keyCode`, can happen if:
// - `KeyboardEvent.key` is outside ASCII range (e.g. cyrillic - # )
// - alt/option/⌥ key is also requested (e.g. Spanish keyboard on MacOS - #8905)
if (!isMatch && (tryKeyCode || binding.event.modifiers.altKey)) {
isMatch = (event.keyCode === binding.event.keyCode);
}
if (!isMatch) return false;
// test modifier keys
if (!(event.ctrlKey && event.altKey)) { // if both are set, assume AltGr and skip it - #4096
if (event.ctrlKey !== binding.event.modifiers.ctrlKey) return false;
if (event.altKey !== binding.event.modifiers.altKey) return false;
}
if (event.metaKey !== binding.event.modifiers.metaKey) return false;
if (testShift && event.shiftKey !== binding.event.modifiers.shiftKey) return false;
return true;
}
}
function capture(d3_event) {
testBindings(d3_event, true);
}
function bubble(d3_event) {
var tagName = d3_select(d3_event.target).node().tagName;
if (tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA') {
return;
}
testBindings(d3_event, false);
}
function keybinding(selection) {
selection = selection || d3_select(document);
selection.on('keydown.capture.' + namespace, capture, true);
selection.on('keydown.bubble.' + namespace, bubble, false);
return keybinding;
}
// was: keybinding.off()
keybinding.unbind = function(selection) {
_keybindings = [];
selection = selection || d3_select(document);
selection.on('keydown.capture.' + namespace, null);
selection.on('keydown.bubble.' + namespace, null);
return keybinding;
};
keybinding.clear = function() {
_keybindings = {};
return keybinding;
};
// Remove one or more keycode bindings.
keybinding.off = function(codes, capture) {
var arr = utilArrayUniq([].concat(codes));
for (var i = 0; i < arr.length; i++) {
var id = arr[i] + (capture ? '-capture' : '-bubble');
delete _keybindings[id];
}
return keybinding;
};
// Add one or more keycode bindings.
keybinding.on = function(codes, callback, capture) {
if (typeof callback !== 'function') {
return keybinding.off(codes, capture);
}
var arr = utilArrayUniq([].concat(codes));
for (var i = 0; i < arr.length; i++) {
var id = arr[i] + (capture ? '-capture' : '-bubble');
var binding = {
id: id,
capture: capture,
callback: callback,
event: {
key: undefined, // preferred
keyCode: 0, // fallback
modifiers: {
shiftKey: false,
ctrlKey: false,
altKey: false,
metaKey: false
}
}
};
if (_keybindings[id]) {
console.warn('warning: duplicate keybinding for "' + id + '"'); // eslint-disable-line no-console
}
_keybindings[id] = binding;
var matches = arr[i].toLowerCase().match(/(?:(?:[^+⇧⌃⌥⌘])+|[⇧⌃⌥⌘]|\+\+|^\+$)/g);
for (var j = 0; j < matches.length; j++) {
// Normalise matching errors
if (matches[j] === '++') matches[j] = '+';
if (matches[j] in utilKeybinding.modifierCodes) {
var prop = utilKeybinding.modifierProperties[utilKeybinding.modifierCodes[matches[j]]];
binding.event.modifiers[prop] = true;
} else {
binding.event.key = utilKeybinding.keys[matches[j]] || matches[j];
if (matches[j] in utilKeybinding.keyCodes) {
binding.event.keyCode = utilKeybinding.keyCodes[matches[j]];
}
}
}
}
return keybinding;
};
return keybinding;
}
/*
* See https://github.com/keithamus/jwerty
*/
utilKeybinding.modifierCodes = {
// Shift key, ⇧
'⇧': 16, shift: 16,
// CTRL key, on Mac: ⌃
'⌃': 17, ctrl: 17,
// ALT key, on Mac: ⌥ (Alt)
'⌥': 18, alt: 18, option: 18,
// META, on Mac: ⌘ (CMD), on Windows (Win), on Linux (Super)
'⌘': 91, meta: 91, cmd: 91, 'super': 91, win: 91
};
utilKeybinding.modifierProperties = {
16: 'shiftKey',
17: 'ctrlKey',
18: 'altKey',
91: 'metaKey'
};
utilKeybinding.plusKeys = ['plus', 'ffplus', '=', 'ffequals', '≠', '±'];
utilKeybinding.minusKeys = ['_', '-', 'ffminus', 'dash', '', '—'];
utilKeybinding.keys = {
// Backspace key, on Mac: ⌫ (Backspace)
'⌫': 'Backspace', backspace: 'Backspace',
// Tab Key, on Mac: ⇥ (Tab), on Windows ⇥⇥
'⇥': 'Tab', '⇆': 'Tab', tab: 'Tab',
// Return key, ↩
'↩': 'Enter', '↵': 'Enter', '⏎': 'Enter', 'return': 'Enter', enter: 'Enter', '⌅': 'Enter',
// Pause/Break key
'pause': 'Pause', 'pause-break': 'Pause',
// Caps Lock key, ⇪
'⇪': 'CapsLock', caps: 'CapsLock', 'caps-lock': 'CapsLock',
// Escape key, on Mac: ⎋, on Windows: Esc
'⎋': ['Escape', 'Esc'], escape: ['Escape', 'Esc'], esc: ['Escape', 'Esc'],
// Space key
space: [' ', 'Spacebar'],
// Page-Up key, or pgup, on Mac: ↖
'↖': 'PageUp', pgup: 'PageUp', 'page-up': 'PageUp',
// Page-Down key, or pgdown, on Mac: ↘
'↘': 'PageDown', pgdown: 'PageDown', 'page-down': 'PageDown',
// END key, on Mac: ⇟
'⇟': 'End', end: 'End',
// HOME key, on Mac: ⇞
'⇞': 'Home', home: 'Home',
// Insert key, or ins
ins: 'Insert', insert: 'Insert',
// Delete key, on Mac: ⌦ (Delete)
'⌦': ['Delete', 'Del'], del: ['Delete', 'Del'], 'delete': ['Delete', 'Del'],
// Left Arrow Key, or ←
'←': ['ArrowLeft', 'Left'], left: ['ArrowLeft', 'Left'], 'arrow-left': ['ArrowLeft', 'Left'],
// Up Arrow Key, or ↑
'↑': ['ArrowUp', 'Up'], up: ['ArrowUp', 'Up'], 'arrow-up': ['ArrowUp', 'Up'],
// Right Arrow Key, or →
'→': ['ArrowRight', 'Right'], right: ['ArrowRight', 'Right'], 'arrow-right': ['ArrowRight', 'Right'],
// Up Arrow Key, or ↓
'↓': ['ArrowDown', 'Down'], down: ['ArrowDown', 'Down'], 'arrow-down': ['ArrowDown', 'Down'],
// odities, stuff for backward compatibility (browsers and code):
// Num-Multiply, or *
'*': ['*', 'Multiply'], star: ['*', 'Multiply'], asterisk: ['*', 'Multiply'], multiply: ['*', 'Multiply'],
// Num-Plus or +
'+': ['+', 'Add'], 'plus': ['+', 'Add'],
// Num-Subtract, or -
'-': ['-', 'Subtract'], subtract: ['-', 'Subtract'], 'dash': ['-', 'Subtract'],
// Semicolon
semicolon: ';',
// = or equals
equals: '=',
// Comma, or ,
comma: ',',
// Period, or ., or full-stop
period: '.', 'full-stop': '.',
// Slash, or /, or forward-slash
slash: '/', 'forward-slash': '/',
// Tick, or `, or back-quote
tick: '`', 'back-quote': '`',
// Open bracket, or [
'open-bracket': '[',
// Back slash, or \
'back-slash': '\\',
// Close bracket, or ]
'close-bracket': ']',
// Apostrophe, or Quote, or '
quote: '\'', apostrophe: '\'',
// NUMPAD 0-9
'num-0': '0',
'num-1': '1',
'num-2': '2',
'num-3': '3',
'num-4': '4',
'num-5': '5',
'num-6': '6',
'num-7': '7',
'num-8': '8',
'num-9': '9',
// F1-F25
f1: 'F1',
f2: 'F2',
f3: 'F3',
f4: 'F4',
f5: 'F5',
f6: 'F6',
f7: 'F7',
f8: 'F8',
f9: 'F9',
f10: 'F10',
f11: 'F11',
f12: 'F12',
f13: 'F13',
f14: 'F14',
f15: 'F15',
f16: 'F16',
f17: 'F17',
f18: 'F18',
f19: 'F19',
f20: 'F20',
f21: 'F21',
f22: 'F22',
f23: 'F23',
f24: 'F24',
f25: 'F25'
};
utilKeybinding.keyCodes = {
// Backspace key, on Mac: ⌫ (Backspace)
'⌫': 8, backspace: 8,
// Tab Key, on Mac: ⇥ (Tab), on Windows ⇥⇥
'⇥': 9, '⇆': 9, tab: 9,
// Return key, ↩
'↩': 13, '↵': 13, '⏎': 13, 'return': 13, enter: 13, '⌅': 13,
// Pause/Break key
'pause': 19, 'pause-break': 19,
// Caps Lock key, ⇪
'⇪': 20, caps: 20, 'caps-lock': 20,
// Escape key, on Mac: ⎋, on Windows: Esc
'⎋': 27, escape: 27, esc: 27,
// Space key
space: 32,
// Page-Up key, or pgup, on Mac: ↖
'↖': 33, pgup: 33, 'page-up': 33,
// Page-Down key, or pgdown, on Mac: ↘
'↘': 34, pgdown: 34, 'page-down': 34,
// END key, on Mac: ⇟
'⇟': 35, end: 35,
// HOME key, on Mac: ⇞
'⇞': 36, home: 36,
// Insert key, or ins
ins: 45, insert: 45,
// Delete key, on Mac: ⌦ (Delete)
'⌦': 46, del: 46, 'delete': 46,
// Left Arrow Key, or ←
'←': 37, left: 37, 'arrow-left': 37,
// Up Arrow Key, or ↑
'↑': 38, up: 38, 'arrow-up': 38,
// Right Arrow Key, or →
'→': 39, right: 39, 'arrow-right': 39,
// Up Arrow Key, or ↓
'↓': 40, down: 40, 'arrow-down': 40,
// odities, printing characters that come out wrong:
// Firefox Equals
'ffequals': 61,
// Num-Multiply, or *
'*': 106, star: 106, asterisk: 106, multiply: 106,
// Num-Plus or +
'+': 107, 'plus': 107,
// Num-Subtract, or -
'-': 109, subtract: 109,
// Vertical Bar / Pipe
'|': 124,
// Firefox Plus
'ffplus': 171,
// Firefox Minus
'ffminus': 173,
// Semicolon
';': 186, semicolon: 186,
// = or equals
'=': 187, 'equals': 187,
// Comma, or ,
',': 188, comma: 188,
// Dash / Underscore key
'dash': 189,
// Period, or ., or full-stop
'.': 190, period: 190, 'full-stop': 190,
// Slash, or /, or forward-slash
'/': 191, slash: 191, 'forward-slash': 191,
// Tick, or `, or back-quote
'`': 192, tick: 192, 'back-quote': 192,
// Open bracket, or [
'[': 219, 'open-bracket': 219,
// Back slash, or \
'\\': 220, 'back-slash': 220,
// Close bracket, or ]
']': 221, 'close-bracket': 221,
// Apostrophe, or Quote, or '
'\'': 222, quote: 222, apostrophe: 222
};
// NUMPAD 0-9
var i = 95, n = 0;
while (++i < 106) {
utilKeybinding.keyCodes['num-' + n] = i;
++n;
}
// 0-9
i = 47; n = 0;
while (++i < 58) {
utilKeybinding.keyCodes[n] = i;
++n;
}
// F1-F25
i = 111; n = 1;
while (++i < 136) {
utilKeybinding.keyCodes['f' + n] = i;
++n;
}
// a-z
i = 64;
while (++i < 91) {
utilKeybinding.keyCodes[String.fromCharCode(i).toLowerCase()] = i;
}