UNPKG

toukey

Version:

Toukey is a Javascript library for keyboard shortcuts

246 lines (241 loc) 6.29 kB
'use strict'; function transModifierKey(key) { switch (key) { case "ctrl": return "Control"; case "space": return " "; case "left": return "ArrowLeft"; case "right": return "ArrowRight"; case "up": return "ArrowUp"; case "bottom": return "ArrowBottom"; default: return key; } } function filterBlank(str) { return str.replace(/\s+/g, ""); } function lowerCase(value) { return value.toLowerCase(); } function isString(value) { return typeof value === "string"; } function isFunction(value) { return typeof value === "function"; } function isObject(value) { return typeof value === "object"; } function isUndefined(value) { return typeof value === "undefined"; } function remove(arr, value) { const index = arr.indexOf(value); if (index !== -1) { arr.splice(index, 1); } } const KEY_DOWN = "keydown"; const KEY_UP = "keyup"; let _curScope = "default"; let _shouldBindToDocument = true; let _isEnabled = true; const _pressedKeys = []; const _handlerMap = /* @__PURE__ */ new Map(); _handlerMap.set("*", []); function _splitKeys(keys) { return filterBlank(keys).split(","); } function _composeKeys(keys, subStr) { return filterBlank(keys).split(subStr); } function _isComposeKey(key, subStr) { return key.split(subStr).length > 1; } function _isKeyMatch(key) { if (Array.isArray(key)) { return lowerCase( key.map((value) => transModifierKey(value)).sort().join("") ) === lowerCase(_pressedKeys.join("")); } else { return _pressedKeys.length === 1 && lowerCase(_pressedKeys[0]) === lowerCase(transModifierKey(key)); } } function _isKeyEventMatch(event, keydown, keyup) { const { type } = event; return type === "keydown" && keydown || type === "keyup" && keyup; } function _clearPressedKeys() { _pressedKeys.length = 0; } function _handleEvent(event) { _updatePressedKeys(event); if (!isEnabled()) { return; } const listForAll = _handlerMap.get("*") || []; const listForScope = _handlerMap.get(_curScope) || []; [...listForAll, ...listForScope].forEach((item) => { const { handler, keydown, keyup, key, splitValue, once } = item; const triggerIfMatch = (matchKey) => { if (_isKeyMatch(matchKey) && _isKeyEventMatch(event, keydown, keyup)) { handler.call(this, event); once && queueMicrotask(() => unsubscribe(item, _curScope)); } }; const chunks = _splitKeys(key); chunks.forEach((chunk) => { if (_isComposeKey(chunk, splitValue)) { const composes = _composeKeys(chunk, splitValue); triggerIfMatch(composes); } else { triggerIfMatch(chunk); } }); }); } function _updatePressedKeys(event) { const { type, key, repeat } = event; if (type === "keydown" && !repeat) { _pressedKeys.push(key); } if (type === "keyup") { queueMicrotask(() => { remove(_pressedKeys, key); }); } } function subscribe(key, handler, options) { if (!isString(key)) { throw new Error("key must be string"); } if (!isFunction(handler)) { throw new Error("handler must be function"); } const _key = key; let _scope = "default"; let _splitValue = "+"; let _shouldHandleInKeydown = true; let _shouldHandleInKeyup = false; let _once = false; if (isString(options)) { _scope = options; } else if (isObject(options)) { Object.keys(options).forEach((key2) => { if (key2 === "scope") { _scope = options[key2]; } if (key2 === "splitValue") { _splitValue = options[key2]; } if (key2 === KEY_DOWN) { _shouldHandleInKeydown = true; } if (key2 === KEY_UP) { _shouldHandleInKeyup = true; } if (key2 === "once") { _once = true; } }); } if (!_shouldHandleInKeydown && !_shouldHandleInKeyup) { _shouldHandleInKeydown = true; } if (_shouldBindToDocument) { _shouldBindToDocument = false; document.addEventListener("keydown", _handleEvent); document.addEventListener("keyup", _handleEvent); window.addEventListener("focus", _clearPressedKeys); window.addEventListener("blur", _clearPressedKeys); } if (!_handlerMap.has(_scope)) { _handlerMap.set(_scope, []); } const _list = _handlerMap.get(_scope); const _item = { handler, key: _key, splitValue: _splitValue, keydown: _shouldHandleInKeydown, keyup: _shouldHandleInKeyup, once: _once }; _list.push(_item); return () => { unsubscribe(_item, _scope); }; } function unsubscribe(item, scope) { const _list = _handlerMap.get(scope); _list && remove(_list, item); } function getScope() { return _curScope; } function setScope(scope) { if (!isString(scope)) { throw new Error("scope must be string"); } _curScope = scope; } function deleteScope(scope) { if (scope !== "*") { _handlerMap.delete(scope); _curScope = "default"; } } function clearAll() { _handlerMap.clear(); _handlerMap.set("*", []); _curScope = "default"; _clearPressedKeys(); } function enable() { _isEnabled = true; } function disable() { _isEnabled = false; } function isEnabled() { return _isEnabled; } function on(key, handler, options) { subscribe(key, handler, options); } function off(key, handler, options) { if (isUndefined(handler) && isUndefined(options)) { _handlerMap.forEach((list, mapKey) => { _handlerMap.set( mapKey, list.filter((item) => item.key !== key) ); }); return; } const { scope = "default", keydown = true, keyup = false, splitValue = "+" } = options || {}; const listForAll = _handlerMap.get("*") || []; const listForScope = _handlerMap.get(scope) || []; const removeList = [...listForAll, ...listForScope].filter( (item) => item.handler === handler && item.key === key && item.keydown === keydown && item.keyup === keyup && item.splitValue === splitValue ); removeList.forEach((item) => { unsubscribe(item, scope); }); } exports.clearAll = clearAll; exports.deleteScope = deleteScope; exports.disable = disable; exports.enable = enable; exports.getScope = getScope; exports.isEnabled = isEnabled; exports.off = off; exports.on = on; exports.setScope = setScope; exports.subscribe = subscribe;