128 lines
3.8 KiB
JavaScript
128 lines
3.8 KiB
JavaScript
import { useCallback, useEffect, useReducer, useState } from "react";
|
|
//Based on https://www.fullstacklabs.co/blog/keyboard-shortcuts-with-react-hooks
|
|
const blacklistedTargets = []; // ["INPUT", "TEXTAREA"];
|
|
export const useKeyboardSaveShortcut = (callback) =>
|
|
useKeyboardShortcut(["Control", "S"], callback, { overrideSystem: true });
|
|
|
|
const keysReducer = (state, action) => {
|
|
switch (action.type) {
|
|
case "set-key-down":
|
|
const keydownState = { ...state, [action.key]: true };
|
|
return keydownState;
|
|
case "set-key-up":
|
|
const keyUpState = { ...state, [action.key]: false };
|
|
return keyUpState;
|
|
case "reset-keys":
|
|
const resetState = { ...action.data };
|
|
return resetState;
|
|
default:
|
|
return state;
|
|
}
|
|
};
|
|
|
|
const useKeyboardShortcut = (shortcutKeys, callback, options) => {
|
|
if (!Array.isArray(shortcutKeys))
|
|
throw new Error(
|
|
"The first parameter to `useKeyboardShortcut` must be an ordered array of `KeyboardEvent.key` strings."
|
|
);
|
|
|
|
if (!shortcutKeys.length)
|
|
throw new Error(
|
|
"The first parameter to `useKeyboardShortcut` must contain atleast one `KeyboardEvent.key` string."
|
|
);
|
|
|
|
if (!callback || typeof callback !== "function")
|
|
throw new Error(
|
|
"The second parameter to `useKeyboardShortcut` must be a function that will be envoked when the keys are pressed."
|
|
);
|
|
|
|
const { overrideSystem } = options || {};
|
|
const initalKeyMapping = shortcutKeys.reduce((currentKeys, key) => {
|
|
currentKeys[key.toLowerCase()] = false;
|
|
return currentKeys;
|
|
}, {});
|
|
|
|
const [keys, setKeys] = useReducer(keysReducer, initalKeyMapping);
|
|
const [listenersAdded, setListenersAdded] = useState(false);
|
|
|
|
const keydownListener = useCallback(
|
|
(assignedKey) => (keydownEvent) => {
|
|
const loweredKey = assignedKey.toLowerCase();
|
|
|
|
if (keydownEvent.repeat) return;
|
|
if (blacklistedTargets.includes(keydownEvent.target.tagName)) return;
|
|
if (loweredKey !== keydownEvent.key.toLowerCase()) return;
|
|
if (keys[loweredKey] === undefined) return;
|
|
|
|
if (overrideSystem) {
|
|
keydownEvent.preventDefault();
|
|
disabledEventPropagation(keydownEvent);
|
|
}
|
|
|
|
setKeys({ type: "set-key-down", key: loweredKey });
|
|
return false;
|
|
},
|
|
[keys, overrideSystem]
|
|
);
|
|
|
|
const keyupListener = useCallback(
|
|
(assignedKey) => (keyupEvent) => {
|
|
const raisedKey = assignedKey.toLowerCase();
|
|
|
|
if (blacklistedTargets.includes(keyupEvent.target.tagName)) return;
|
|
if (keyupEvent.key.toLowerCase() !== raisedKey) return;
|
|
if (keys[raisedKey] === undefined) return;
|
|
|
|
if (overrideSystem) {
|
|
keyupEvent.preventDefault();
|
|
disabledEventPropagation(keyupEvent);
|
|
}
|
|
|
|
setKeys({ type: "set-key-up", key: raisedKey });
|
|
return false;
|
|
},
|
|
[keys, overrideSystem]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!Object.values(keys).filter((value) => !value).length) {
|
|
callback(keys);
|
|
setKeys({ type: "reset-keys", data: initalKeyMapping });
|
|
} else {
|
|
setKeys({ type: null });
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [callback, keys]);
|
|
|
|
useEffect(() => {
|
|
if (!listenersAdded) {
|
|
console.log("Added events for keyup and keydown");
|
|
shortcutKeys.forEach((k) => {
|
|
window.addEventListener("keydown", keydownListener(k));
|
|
window.addEventListener("keyup", keyupListener(k));
|
|
});
|
|
}
|
|
|
|
setListenersAdded(true);
|
|
|
|
return () => {
|
|
shortcutKeys.forEach((k) => {
|
|
window.removeEventListener("keydown", keydownListener(k));
|
|
window.removeEventListener("keyup", keyupListener(k));
|
|
});
|
|
};
|
|
}, [listenersAdded]);
|
|
};
|
|
|
|
export default useKeyboardShortcut;
|
|
|
|
function disabledEventPropagation(e) {
|
|
if (e) {
|
|
if (e.stopPropagation) {
|
|
e.stopPropagation();
|
|
} else if (window.event) {
|
|
window.event.cancelBubble = true;
|
|
}
|
|
}
|
|
}
|