Refactor: add possibility to use different keys or combinations as a key code (#1554)
* feat(keys): support for combinations and multiple possibilites for one action * refactor(globalKeyHandler): pass deps to useEffect * refactor(examples): define key codes outside of the component
This commit is contained in:
@@ -23,6 +23,9 @@ const onLoad = (reactFlowInstance: OnLoadParams) => {
|
||||
|
||||
const initialElements = getElements();
|
||||
|
||||
const multiSelectionKeyCode = ['ShiftLeft', 'ShiftRight'];
|
||||
const deleteKeyCode = ['AltLeft+KeyD', 'Backspace'];
|
||||
|
||||
const EdgeTypesFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
@@ -35,6 +38,11 @@ const EdgeTypesFlow = () => {
|
||||
onElementsRemove={onElementsRemove}
|
||||
onConnect={onConnect}
|
||||
minZoom={0.2}
|
||||
zoomOnScroll={false}
|
||||
selectionKeyCode="a+s"
|
||||
multiSelectionKeyCode={multiSelectionKeyCode}
|
||||
deleteKeyCode={deleteKeyCode}
|
||||
zoomActivationKeyCode="z"
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
@@ -36,7 +36,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: Hook
|
||||
unsetNodesSelection();
|
||||
resetSelectedElements();
|
||||
}
|
||||
}, [deleteKeyPressed]);
|
||||
}, [deleteKeyPressed, onElementsRemove]);
|
||||
|
||||
useEffect(() => {
|
||||
setMultiSelectionActive(multiSelectionKeyPressed);
|
||||
|
||||
+87
-16
@@ -1,40 +1,111 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
|
||||
import { isInputDOMNode } from '../utils';
|
||||
import { KeyCode } from '../types';
|
||||
|
||||
export default (keyCode?: KeyCode): boolean => {
|
||||
type Keys = Array<string>;
|
||||
type PressedKeys = Set<string>;
|
||||
type KeyOrCode = 'key' | 'code';
|
||||
export interface UseKeyPressOptions {
|
||||
target: Document | HTMLElement | ShadowRoot;
|
||||
}
|
||||
|
||||
// the keycode can be a string 'a' or an array of strings ['a', 'a+d']
|
||||
// a string means a single key 'a' or a combination when '+' is used 'a+d'
|
||||
// an array means different possibilites. Explainer: ['a', 'd+s'] here the
|
||||
// user can use the single key 'a' or the combination 'd' + 's'
|
||||
export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = { target: document }): boolean => {
|
||||
const [keyPressed, setKeyPressed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof keyCode !== 'undefined') {
|
||||
const downHandler = (event: KeyboardEvent) => {
|
||||
if (!isInputDOMNode(event) && (event.key === keyCode || event.keyCode === keyCode)) {
|
||||
event.preventDefault();
|
||||
// we need to remember the pressed keys in order to support combinations
|
||||
const pressedKeys = useRef<PressedKeys>(new Set([]));
|
||||
|
||||
// keyCodes = array with single keys [['a']] or key combinations [['a', 's']]
|
||||
// keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft']
|
||||
// used to check if we store event.code or event.key. When the code is in the list of keysToWatch
|
||||
// we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft"
|
||||
// and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when
|
||||
// we can't find it in the list of keysToWatch.
|
||||
const [keyCodes, keysToWatch] = useMemo<[Array<Keys>, Keys]>(() => {
|
||||
if (keyCode !== null) {
|
||||
const keyCodeArr = Array.isArray(keyCode) ? keyCode : [keyCode];
|
||||
const keys = keyCodeArr.map((kc) => kc.split('+'));
|
||||
const keysFlat = keys.reduce((res: Keys, item) => res.concat(...item), []);
|
||||
|
||||
return [keys, keysFlat];
|
||||
}
|
||||
|
||||
return [[], []];
|
||||
}, [keyCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (keyCode !== null) {
|
||||
const downHandler = (event: KeyboardEvent) => {
|
||||
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
|
||||
pressedKeys.current.add(event[keyOrCode]);
|
||||
|
||||
if (isMatchingKey(event, keyCodes, pressedKeys.current)) {
|
||||
event.preventDefault();
|
||||
setKeyPressed(true);
|
||||
}
|
||||
};
|
||||
|
||||
const upHandler = (event: KeyboardEvent) => {
|
||||
if (!isInputDOMNode(event) && (event.key === keyCode || event.keyCode === keyCode)) {
|
||||
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
|
||||
|
||||
if (isMatchingKey(event, keyCodes, pressedKeys.current)) {
|
||||
setKeyPressed(false);
|
||||
}
|
||||
|
||||
pressedKeys.current.delete(event[keyOrCode]);
|
||||
};
|
||||
|
||||
const resetHandler = () => setKeyPressed(false);
|
||||
const resetHandler = () => {
|
||||
pressedKeys.current.clear();
|
||||
setKeyPressed(false);
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', downHandler);
|
||||
window.addEventListener('keyup', upHandler);
|
||||
window.addEventListener('blur', resetHandler);
|
||||
options.target.addEventListener('keydown', downHandler as EventListenerOrEventListenerObject);
|
||||
options.target.addEventListener('keyup', upHandler as EventListenerOrEventListenerObject);
|
||||
options.target.addEventListener('blur', resetHandler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', downHandler);
|
||||
window.removeEventListener('keyup', upHandler);
|
||||
window.removeEventListener('blur', resetHandler);
|
||||
pressedKeys.current.clear();
|
||||
|
||||
options.target.removeEventListener('keydown', downHandler as EventListenerOrEventListenerObject);
|
||||
options.target.removeEventListener('keyup', upHandler as EventListenerOrEventListenerObject);
|
||||
options.target.removeEventListener('blur', resetHandler);
|
||||
};
|
||||
}
|
||||
}, [keyCode, setKeyPressed]);
|
||||
|
||||
return keyPressed;
|
||||
};
|
||||
|
||||
// utils
|
||||
|
||||
function isMatchingKey(event: KeyboardEvent, keyCodes: Array<Keys>, pressedKeys: PressedKeys): boolean {
|
||||
if (isInputDOMNode(event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
keyCodes
|
||||
// we only want to compare same sizes of keyCode definitions
|
||||
// and pressed keys. When the user specified 'Meta' as a key somewhere
|
||||
// this would also be truthy without this filter when user presses 'Meta' + 'r'
|
||||
.filter((keys) => keys.length === pressedKeys.size)
|
||||
// since we want to support multiple possibilities only one of the
|
||||
// combinations need to be part of the pressed keys
|
||||
.some((keys) => keys.every((k) => pressedKeys.has(k)))
|
||||
);
|
||||
}
|
||||
|
||||
function useKeyOrCode(eventCode: string, keysToWatch: KeyCode): KeyOrCode {
|
||||
return keysToWatch.includes(eventCode) ? 'code' : 'key';
|
||||
}
|
||||
|
||||
function isInputDOMNode(e: KeyboardEvent): boolean {
|
||||
const target = e?.target as HTMLElement;
|
||||
|
||||
return ['INPUT', 'SELECT', 'TEXTAREA'].includes(target?.nodeName) || target?.hasAttribute('contenteditable');
|
||||
}
|
||||
|
||||
+1
-1
@@ -363,7 +363,7 @@ export type FlowTransform = {
|
||||
export type TranslateExtent = [[number, number], [number, number]];
|
||||
export type NodeExtent = TranslateExtent;
|
||||
|
||||
export type KeyCode = number | string;
|
||||
export type KeyCode = string | Array<string>;
|
||||
|
||||
export enum PanOnScrollMode {
|
||||
Free = 'free',
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
import { DraggableEvent } from 'react-draggable';
|
||||
import { MouseEvent as ReactMouseEvent } from 'react';
|
||||
|
||||
import { Dimensions, XYPosition, NodeExtent } from '../types';
|
||||
|
||||
export const isInputDOMNode = (e: ReactMouseEvent | DraggableEvent | KeyboardEvent) => {
|
||||
const target = e?.target as HTMLElement;
|
||||
|
||||
return (
|
||||
['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target?.nodeName) || target?.hasAttribute('contenteditable')
|
||||
);
|
||||
};
|
||||
|
||||
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
|
||||
width: node.offsetWidth,
|
||||
height: node.offsetHeight,
|
||||
|
||||
Reference in New Issue
Block a user