Uniformize folder structure (#693)
* Uniformize folder structure * Fix icons * Fix icons * Fix tests * Fix tests
This commit is contained in:
23
front/src/modules/ui/hotkey/constants/index.ts
Normal file
23
front/src/modules/ui/hotkey/constants/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { AppHotkeyScope } from '../types/AppHotkeyScope';
|
||||
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
|
||||
import { HotkeyScope } from '../types/HotkeyScope';
|
||||
|
||||
export const INITIAL_HOTKEYS_SCOPES: string[] = [AppHotkeyScope.App];
|
||||
|
||||
export const ALWAYS_ON_HOTKEYS_SCOPES: string[] = [
|
||||
AppHotkeyScope.CommandMenu,
|
||||
AppHotkeyScope.App,
|
||||
];
|
||||
|
||||
export const DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES: CustomHotkeyScopes = {
|
||||
commandMenu: true,
|
||||
goto: false,
|
||||
};
|
||||
|
||||
export const INITIAL_HOTKEYS_SCOPE: HotkeyScope = {
|
||||
scope: AppHotkeyScope.App,
|
||||
customScopes: {
|
||||
commandMenu: true,
|
||||
goto: true,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentHotkeyScopeState } from '@/ui/hotkey/states/internal/currentHotkeyScopeState';
|
||||
|
||||
import { AppHotkeyScope } from '../../types/AppHotkeyScope';
|
||||
|
||||
import { useHotkeyScopes } from './useHotkeyScopes';
|
||||
|
||||
export function useHotkeyScopeAutoSync() {
|
||||
const { setHotkeyScopes } = useHotkeyScopes();
|
||||
|
||||
const currentHotkeyScope = useRecoilValue(currentHotkeyScopeState);
|
||||
|
||||
useEffect(() => {
|
||||
const scopesToSet: string[] = [];
|
||||
|
||||
if (currentHotkeyScope.customScopes?.commandMenu) {
|
||||
scopesToSet.push(AppHotkeyScope.CommandMenu);
|
||||
}
|
||||
|
||||
if (currentHotkeyScope?.customScopes?.goto) {
|
||||
scopesToSet.push(AppHotkeyScope.Goto);
|
||||
}
|
||||
|
||||
scopesToSet.push(currentHotkeyScope.scope);
|
||||
|
||||
setHotkeyScopes(scopesToSet);
|
||||
}, [setHotkeyScopes, currentHotkeyScope]);
|
||||
}
|
||||
103
front/src/modules/ui/hotkey/hooks/internal/useHotkeyScopes.ts
Normal file
103
front/src/modules/ui/hotkey/hooks/internal/useHotkeyScopes.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { useHotkeysContext } from 'react-hotkeys-hook';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { internalHotkeysEnabledScopesState } from '../../states/internal/internalHotkeysEnabledScopesState';
|
||||
|
||||
export function useHotkeyScopes() {
|
||||
const { disableScope, enableScope } = useHotkeysContext();
|
||||
|
||||
const disableAllHotkeyScopes = useRecoilCallback(
|
||||
({ set, snapshot }) => {
|
||||
return async () => {
|
||||
const enabledScopes = await snapshot.getPromise(
|
||||
internalHotkeysEnabledScopesState,
|
||||
);
|
||||
|
||||
for (const enabledScope of enabledScopes) {
|
||||
disableScope(enabledScope);
|
||||
}
|
||||
|
||||
set(internalHotkeysEnabledScopesState, []);
|
||||
};
|
||||
},
|
||||
[disableScope],
|
||||
);
|
||||
|
||||
const enableHotkeyScope = useRecoilCallback(
|
||||
({ set, snapshot }) => {
|
||||
return async (scopeToEnable: string) => {
|
||||
const enabledScopes = await snapshot.getPromise(
|
||||
internalHotkeysEnabledScopesState,
|
||||
);
|
||||
|
||||
if (!enabledScopes.includes(scopeToEnable)) {
|
||||
enableScope(scopeToEnable);
|
||||
set(internalHotkeysEnabledScopesState, [
|
||||
...enabledScopes,
|
||||
scopeToEnable,
|
||||
]);
|
||||
}
|
||||
};
|
||||
},
|
||||
[enableScope],
|
||||
);
|
||||
|
||||
const disableHotkeyScope = useRecoilCallback(
|
||||
({ set, snapshot }) => {
|
||||
return async (scopeToDisable: string) => {
|
||||
const enabledScopes = await snapshot.getPromise(
|
||||
internalHotkeysEnabledScopesState,
|
||||
);
|
||||
|
||||
const scopeToRemoveIndex = enabledScopes.findIndex(
|
||||
(scope) => scope === scopeToDisable,
|
||||
);
|
||||
|
||||
if (scopeToRemoveIndex > -1) {
|
||||
disableScope(scopeToDisable);
|
||||
|
||||
enabledScopes.splice(scopeToRemoveIndex);
|
||||
|
||||
set(internalHotkeysEnabledScopesState, enabledScopes);
|
||||
}
|
||||
};
|
||||
},
|
||||
[disableScope],
|
||||
);
|
||||
|
||||
const setHotkeyScopes = useRecoilCallback(
|
||||
({ set, snapshot }) => {
|
||||
return async (scopesToSet: string[]) => {
|
||||
const enabledScopes = await snapshot.getPromise(
|
||||
internalHotkeysEnabledScopesState,
|
||||
);
|
||||
|
||||
const scopesToDisable = enabledScopes.filter(
|
||||
(enabledScope) => !scopesToSet.includes(enabledScope),
|
||||
);
|
||||
|
||||
const scopesToEnable = scopesToSet.filter(
|
||||
(scopeToSet) => !enabledScopes.includes(scopeToSet),
|
||||
);
|
||||
|
||||
for (const scopeToDisable of scopesToDisable) {
|
||||
disableScope(scopeToDisable);
|
||||
}
|
||||
|
||||
for (const scopeToEnable of scopesToEnable) {
|
||||
enableScope(scopeToEnable);
|
||||
}
|
||||
|
||||
set(internalHotkeysEnabledScopesState, scopesToSet);
|
||||
};
|
||||
},
|
||||
[disableScope, enableScope],
|
||||
);
|
||||
|
||||
return {
|
||||
disableAllHotkeyScopes,
|
||||
enableHotkeyScope,
|
||||
disableHotkeyScope,
|
||||
setHotkeyScopes,
|
||||
};
|
||||
}
|
||||
25
front/src/modules/ui/hotkey/hooks/useGoToHotkeys.ts
Normal file
25
front/src/modules/ui/hotkey/hooks/useGoToHotkeys.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Keys } from 'react-hotkeys-hook/dist/types';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { AppHotkeyScope } from '../types/AppHotkeyScope';
|
||||
|
||||
import { useSequenceHotkeys } from './useSequenceScopedHotkeys';
|
||||
|
||||
export function useGoToHotkeys(key: Keys, location: string) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useSequenceHotkeys(
|
||||
'g',
|
||||
key,
|
||||
() => {
|
||||
navigate(location);
|
||||
},
|
||||
AppHotkeyScope.Goto,
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
}
|
||||
39
front/src/modules/ui/hotkey/hooks/usePreviousHotkeyScope.ts
Normal file
39
front/src/modules/ui/hotkey/hooks/usePreviousHotkeyScope.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
|
||||
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
|
||||
import { HotkeyScope } from '../types/HotkeyScope';
|
||||
|
||||
import { useSetHotkeyScope } from './useSetHotkeyScope';
|
||||
|
||||
export function usePreviousHotkeyScope() {
|
||||
const [previousHotkeyScope, setPreviousHotkeyScope] =
|
||||
useState<HotkeyScope | null>();
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const currentHotkeyScope = useRecoilValue(currentHotkeyScopeState);
|
||||
|
||||
function goBackToPreviousHotkeyScope() {
|
||||
if (previousHotkeyScope) {
|
||||
setHotkeyScope(
|
||||
previousHotkeyScope.scope,
|
||||
previousHotkeyScope.customScopes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setHotkeyScopeAndMemorizePreviousScope(
|
||||
scope: string,
|
||||
customScopes?: CustomHotkeyScopes,
|
||||
) {
|
||||
setPreviousHotkeyScope(currentHotkeyScope);
|
||||
setHotkeyScope(scope, customScopes);
|
||||
}
|
||||
|
||||
return {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
};
|
||||
}
|
||||
43
front/src/modules/ui/hotkey/hooks/useScopedHotkeys.ts
Normal file
43
front/src/modules/ui/hotkey/hooks/useScopedHotkeys.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
Hotkey,
|
||||
HotkeyCallback,
|
||||
Keys,
|
||||
Options,
|
||||
OptionsOrDependencyArray,
|
||||
} from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
||||
|
||||
export function useScopedHotkeys(
|
||||
keys: Keys,
|
||||
callback: HotkeyCallback,
|
||||
scope: string,
|
||||
dependencies?: OptionsOrDependencyArray,
|
||||
options: Options = {
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
) {
|
||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||
|
||||
function callbackIfDirectKey(
|
||||
keyboardEvent: KeyboardEvent,
|
||||
hotkeysEvent: Hotkey,
|
||||
) {
|
||||
if (!pendingHotkey) {
|
||||
callback(keyboardEvent, hotkeysEvent);
|
||||
return;
|
||||
}
|
||||
setPendingHotkey(null);
|
||||
}
|
||||
|
||||
return useHotkeys(
|
||||
keys,
|
||||
callbackIfDirectKey,
|
||||
{ ...options, scopes: [scope] },
|
||||
dependencies,
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import { Options, useHotkeys } from 'react-hotkeys-hook';
|
||||
import { Keys } from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
||||
|
||||
export function useSequenceHotkeys(
|
||||
firstKey: Keys,
|
||||
secondKey: Keys,
|
||||
callback: () => void,
|
||||
scope: string,
|
||||
options: Options = {
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
deps: any[] = [],
|
||||
) {
|
||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||
|
||||
useHotkeys(
|
||||
firstKey,
|
||||
() => {
|
||||
setPendingHotkey(firstKey);
|
||||
},
|
||||
{ ...options, scopes: [scope] },
|
||||
[setPendingHotkey],
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
secondKey,
|
||||
() => {
|
||||
if (pendingHotkey !== firstKey) {
|
||||
return;
|
||||
}
|
||||
setPendingHotkey(null);
|
||||
callback();
|
||||
},
|
||||
{ ...options, scopes: [scope] },
|
||||
[pendingHotkey, setPendingHotkey, ...deps],
|
||||
);
|
||||
}
|
||||
59
front/src/modules/ui/hotkey/hooks/useSetHotkeyScope.ts
Normal file
59
front/src/modules/ui/hotkey/hooks/useSetHotkeyScope.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES } from '../constants';
|
||||
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
|
||||
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
|
||||
|
||||
function isCustomScopesEqual(
|
||||
customScopesA: CustomHotkeyScopes | undefined,
|
||||
customScopesB: CustomHotkeyScopes | undefined,
|
||||
) {
|
||||
return (
|
||||
customScopesA?.commandMenu === customScopesB?.commandMenu &&
|
||||
customScopesA?.goto === customScopesB?.goto
|
||||
);
|
||||
}
|
||||
|
||||
export function useSetHotkeyScope() {
|
||||
return useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (HotkeyScopeToSet: string, customScopes?: CustomHotkeyScopes) => {
|
||||
const currentHotkeyScope = await snapshot.getPromise(
|
||||
currentHotkeyScopeState,
|
||||
);
|
||||
|
||||
if (currentHotkeyScope.scope === HotkeyScopeToSet) {
|
||||
if (!isDefined(customScopes)) {
|
||||
if (
|
||||
isCustomScopesEqual(
|
||||
currentHotkeyScope?.customScopes,
|
||||
DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
isCustomScopesEqual(
|
||||
currentHotkeyScope?.customScopes,
|
||||
customScopes,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set(currentHotkeyScopeState, {
|
||||
scope: HotkeyScopeToSet,
|
||||
customScopes: {
|
||||
commandMenu: customScopes?.commandMenu ?? true,
|
||||
goto: customScopes?.goto ?? false,
|
||||
},
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { INITIAL_HOTKEYS_SCOPE } from '../../constants';
|
||||
import { HotkeyScope } from '../../types/HotkeyScope';
|
||||
|
||||
export const currentHotkeyScopeState = atom<HotkeyScope>({
|
||||
key: 'currentHotkeyScopeState',
|
||||
default: INITIAL_HOTKEYS_SCOPE,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const internalHotkeysEnabledScopesState = atom<string[]>({
|
||||
key: 'internalHotkeysEnabledScopesState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { Keys } from 'react-hotkeys-hook/dist/types';
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const pendingHotkeyState = atom<Keys | null>({
|
||||
key: 'pendingHotkeyState',
|
||||
default: null,
|
||||
});
|
||||
5
front/src/modules/ui/hotkey/types/AppHotkeyScope.ts
Normal file
5
front/src/modules/ui/hotkey/types/AppHotkeyScope.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum AppHotkeyScope {
|
||||
App = 'app',
|
||||
Goto = 'goto',
|
||||
CommandMenu = 'command-menu',
|
||||
}
|
||||
4
front/src/modules/ui/hotkey/types/CustomHotkeyScope.ts
Normal file
4
front/src/modules/ui/hotkey/types/CustomHotkeyScope.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type CustomHotkeyScopes = {
|
||||
goto?: boolean;
|
||||
commandMenu?: boolean;
|
||||
};
|
||||
6
front/src/modules/ui/hotkey/types/HotkeyScope.ts
Normal file
6
front/src/modules/ui/hotkey/types/HotkeyScope.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { CustomHotkeyScopes } from './CustomHotkeyScope';
|
||||
|
||||
export type HotkeyScope = {
|
||||
scope: string;
|
||||
customScopes?: CustomHotkeyScopes;
|
||||
};
|
||||
57
front/src/modules/ui/hotkey/utils/isNonTextWritingKey.ts
Normal file
57
front/src/modules/ui/hotkey/utils/isNonTextWritingKey.ts
Normal file
@ -0,0 +1,57 @@
|
||||
export function isNonTextWritingKey(key: string) {
|
||||
const nonTextWritingKeys = [
|
||||
'Enter',
|
||||
'Tab',
|
||||
'Shift',
|
||||
'Escape',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'Delete',
|
||||
'Backspace',
|
||||
'F1',
|
||||
'F2',
|
||||
'F3',
|
||||
'F4',
|
||||
'F5',
|
||||
'F6',
|
||||
'F7',
|
||||
'F8',
|
||||
'F9',
|
||||
'F10',
|
||||
'F11',
|
||||
'F12',
|
||||
'Meta',
|
||||
'Alt',
|
||||
'Control',
|
||||
'CapsLock',
|
||||
'NumLock',
|
||||
'ScrollLock',
|
||||
'Pause',
|
||||
'Insert',
|
||||
'Home',
|
||||
'PageUp',
|
||||
'Delete',
|
||||
'End',
|
||||
'PageDown',
|
||||
'ContextMenu',
|
||||
'PrintScreen',
|
||||
'BrowserBack',
|
||||
'BrowserForward',
|
||||
'BrowserRefresh',
|
||||
'BrowserStop',
|
||||
'BrowserSearch',
|
||||
'BrowserFavorites',
|
||||
'BrowserHome',
|
||||
'VolumeMute',
|
||||
'VolumeDown',
|
||||
'VolumeUp',
|
||||
'MediaTrackNext',
|
||||
'MediaTrackPrevious',
|
||||
'MediaStop',
|
||||
'MediaPlayPause',
|
||||
];
|
||||
|
||||
return nonTextWritingKeys.includes(key);
|
||||
}
|
||||
Reference in New Issue
Block a user