duoyun-ui
Version:
A lightweight desktop UI component library, implemented using Gem
248 lines • 7.22 kB
JavaScript
import { isNotBoolean } from './types';
import { proxyObject } from './utils';
/**
* @see
* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values
*/
export function normalizeKey(code) {
const s = code.toLowerCase();
if (s.startsWith('control'))
return 'ctrl';
if (s.startsWith('meta') || s === 'command' || s.startsWith('os'))
return 'meta';
if (s.startsWith('shift'))
return 'shift';
if (s.startsWith('alt') || s === 'option')
return 'alt';
return s.replace(/^(digit|key|numpad)/, '');
}
function appendToMap(keys) {
Object.entries(keys).forEach(([named, key]) => {
Object.entries(key).forEach(([_, k]) => {
map[k] = named;
});
});
}
const keysInfo = {
ctrl: {
alias: 'control',
macSymbol: '⌃',
},
meta: {
win: 'win',
mac: 'command',
macSymbol: '⌘',
},
shift: {
symbol: '⇧',
},
alt: {
mac: 'option',
macSymbol: '⌥',
},
escape: {
alias: 'esc',
},
backspace: {
symbol: '⌫',
},
enter: {
alias: 'return',
symbol: '↵',
},
space: {
symbol: '␣',
},
capsLock: {
symbol: '⇪',
},
arrowdown: {
alias: 'down',
symbol: '↓',
},
arrowup: {
alias: 'up',
symbol: '↑',
},
arrowleft: {
alias: 'left',
symbol: '←',
},
arrowright: {
alias: 'right',
symbol: '→',
},
minus: {
symbol: '-',
},
equal: {
symbol: '=',
},
period: {
symbol: '.',
},
comma: {
symbol: ',',
},
slash: {
symbol: '/',
},
backslash: {
symbol: '|',
},
bracketleft: {
symbol: '[',
},
bracketright: {
symbol: ']',
},
semicolon: {
symbol: ';',
},
quote: {
symbol: "'",
},
backquote: {
symbol: '`',
},
};
const map = proxyObject({});
appendToMap(keysInfo);
export const isMac = navigator.platform.includes('Mac');
/**Get the platform button */
export function getDisplayKey(code, type) {
const key = normalizeKey(code);
const keyObj = keysInfo[key];
let result = undefined;
if (!keyObj) {
result = key;
}
else if (type) {
result = keyObj[type];
}
if (!result) {
result = (isMac ? keyObj.macSymbol || keyObj.mac : keyObj.win) || keyObj.symbol || key;
}
return result.replace(/^(.)/, (_substr, $1) => $1.toUpperCase());
}
/**Custom key map */
export function setKeys(keysRecord) {
Object.assign(keysInfo, keysRecord);
appendToMap(keysRecord);
}
const hotkeySplitter = /,(?!,)/;
// https://bugs.webkit.org/show_bug.cgi?id=174931
// const keySplitter = /(?<!\+)\+/;
const keySplitter = /\+/;
/**Detect whether the current keyboard event matches the specified button */
export function matchHotKey(evt, hotkey) {
const keys = hotkey.split(keySplitter).map((k) => map[k]);
const targetKeyEvent = { ctrl: false, meta: false, shift: false, alt: false, namedKey: '' };
keys.forEach((named) => {
switch (named) {
case 'ctrl':
return (targetKeyEvent.ctrl = true);
case 'meta':
return (targetKeyEvent.meta = true);
case 'shift':
return (targetKeyEvent.shift = true);
case 'alt':
return (targetKeyEvent.alt = true);
default:
return (targetKeyEvent.namedKey = named);
}
});
let nextKey = '';
if (targetKeyEvent.namedKey.length > 2 && targetKeyEvent.namedKey.includes('-')) {
// not support `a--`, `--a`, `a--b`, only allow `a-b`
[targetKeyEvent.namedKey, nextKey] = [...targetKeyEvent.namedKey.split('-')];
}
const match = evt.ctrlKey === targetKeyEvent.ctrl &&
evt.metaKey === targetKeyEvent.meta &&
evt.shiftKey === targetKeyEvent.shift &&
evt.altKey === targetKeyEvent.alt &&
(!targetKeyEvent.namedKey ||
normalizeKey(evt.code) === targetKeyEvent.namedKey ||
/**
* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
*/
evt.key.toLowerCase() === targetKeyEvent.namedKey);
return nextKey ? match && nextKey : match;
}
let locked = false;
const unlockCallback = new Set();
/**Release the lock state of the continuous button, see `hotkeys` */
export function unlock() {
locked = false;
unlockCallback.forEach((callback) => callback());
unlockCallback.clear();
}
/**
* Must have non-control character;
* Not case sensitive;
* Support `a-b`, press `a`, hotkeys be locked, wait next `keydown` event, allow call `unlock`
*/
export function hotkeys(handles, options = {}) {
const { stopPropagation, preventDefault = true } = options;
return (event) => {
if (event.isComposing)
return;
if (locked)
return;
let captured = false;
const nextKeyHandleSet = new Map();
for (const str in handles) {
const handle = handles[str];
if (!handle)
break;
const shortcuts = str.split(hotkeySplitter).map((e) => e.trim());
const matchResult = shortcuts.map((hotkey) => matchHotKey(event, hotkey));
if (matchResult.some((r) => r === true)) {
captured = true;
if (preventDefault)
event.preventDefault();
if (stopPropagation)
event.stopPropagation();
handle(event);
}
matchResult.filter(isNotBoolean).forEach((key) => {
const set = nextKeyHandleSet.get(key) || new Set();
set.add(handle);
nextKeyHandleSet.set(key, set);
});
}
if (nextKeyHandleSet.size) {
captured = true;
unlockCallback.clear();
handles.onLock?.(event);
locked = true;
const nextKeyHandle = (evt) => {
handles.onUnlock?.(evt);
locked = false;
evt.stopPropagation();
evt.preventDefault();
let nextKeyCaptured = false;
nextKeyHandleSet.forEach((handleSet, k) => {
if (matchHotKey(evt, k)) {
nextKeyCaptured = true;
handleSet.forEach((h) => h(evt));
}
});
if (!nextKeyCaptured)
handles.onUncapture?.(evt);
};
unlockCallback.add(() => removeEventListener('keydown', nextKeyHandle, { capture: true }));
addEventListener('keydown', nextKeyHandle, { once: true, capture: true });
}
if (!captured)
handles.onUncapture?.(event);
};
}
/**
* Support space,enter
*/
export const commonHandle = hotkeys({
'space,enter': (evt) => evt.target.click(),
esc: (evt) => evt.target.blur(),
});
//# sourceMappingURL=hotkeys.js.map