Clean and re-organize post table refactoring (#1000)
* Clean and re-organize post table refactoring * Fix tests
This commit is contained in:
23
front/src/modules/ui/utilities/hotkey/constants/index.ts
Normal file
23
front/src/modules/ui/utilities/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,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,
|
||||
};
|
||||
}
|
||||
@ -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],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
import { useState } from 'react';
|
||||
import { useRecoilCallback } 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();
|
||||
|
||||
function goBackToPreviousHotkeyScope() {
|
||||
if (previousHotkeyScope) {
|
||||
setHotkeyScope(
|
||||
previousHotkeyScope.scope,
|
||||
previousHotkeyScope.customScopes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const setHotkeyScopeAndMemorizePreviousScope = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(scope: string, customScopes?: CustomHotkeyScopes) => {
|
||||
const currentHotkeyScope = snapshot
|
||||
.getLoadable(currentHotkeyScopeState)
|
||||
.valueOrThrow();
|
||||
|
||||
setHotkeyScope(scope, customScopes);
|
||||
|
||||
setPreviousHotkeyScope(currentHotkeyScope);
|
||||
},
|
||||
[setPreviousHotkeyScope],
|
||||
);
|
||||
|
||||
return {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import { Hotkey } from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
|
||||
|
||||
const DEBUG_HOTKEY_SCOPE = true;
|
||||
|
||||
export function useScopedHotkeyCallback() {
|
||||
return useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
({
|
||||
callback,
|
||||
hotkeysEvent,
|
||||
keyboardEvent,
|
||||
scope,
|
||||
preventDefault = true,
|
||||
}: {
|
||||
keyboardEvent: KeyboardEvent;
|
||||
hotkeysEvent: Hotkey;
|
||||
callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void;
|
||||
scope: string;
|
||||
preventDefault?: boolean;
|
||||
}) => {
|
||||
const currentHotkeyScopes = snapshot
|
||||
.getLoadable(internalHotkeysEnabledScopesState)
|
||||
.valueOrThrow();
|
||||
|
||||
if (!currentHotkeyScopes.includes(scope)) {
|
||||
if (DEBUG_HOTKEY_SCOPE) {
|
||||
console.debug(
|
||||
`%cI can't call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
|
||||
', ',
|
||||
)}]`,
|
||||
'color: gray; ',
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG_HOTKEY_SCOPE) {
|
||||
console.debug(
|
||||
`%cI can call hotkey (${
|
||||
hotkeysEvent.keys
|
||||
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
|
||||
', ',
|
||||
)}]`,
|
||||
'color: green;',
|
||||
);
|
||||
}
|
||||
|
||||
if (preventDefault) {
|
||||
keyboardEvent.stopPropagation();
|
||||
keyboardEvent.preventDefault();
|
||||
keyboardEvent.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
return callback(keyboardEvent, hotkeysEvent);
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
HotkeyCallback,
|
||||
Keys,
|
||||
Options,
|
||||
OptionsOrDependencyArray,
|
||||
} from 'react-hotkeys-hook/dist/types';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
||||
|
||||
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
|
||||
|
||||
export function useScopedHotkeys(
|
||||
keys: Keys,
|
||||
callback: HotkeyCallback,
|
||||
scope: string,
|
||||
dependencies?: OptionsOrDependencyArray,
|
||||
options: Options = {
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
) {
|
||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||
|
||||
const callScopedHotkeyCallback = useScopedHotkeyCallback();
|
||||
|
||||
return useHotkeys(
|
||||
keys,
|
||||
(keyboardEvent, hotkeysEvent) => {
|
||||
callScopedHotkeyCallback({
|
||||
keyboardEvent,
|
||||
hotkeysEvent,
|
||||
callback: () => {
|
||||
if (!pendingHotkey) {
|
||||
callback(keyboardEvent, hotkeysEvent);
|
||||
return;
|
||||
}
|
||||
setPendingHotkey(null);
|
||||
},
|
||||
scope,
|
||||
preventDefault: !!options.preventDefault,
|
||||
});
|
||||
},
|
||||
{
|
||||
enableOnContentEditable: options.enableOnContentEditable,
|
||||
enableOnFormTags: options.enableOnFormTags,
|
||||
},
|
||||
dependencies,
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
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';
|
||||
|
||||
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
|
||||
|
||||
export function useSequenceHotkeys(
|
||||
firstKey: Keys,
|
||||
secondKey: Keys,
|
||||
sequenceCallback: () => void,
|
||||
scope: string,
|
||||
options: Options = {
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
deps: any[] = [],
|
||||
) {
|
||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
||||
|
||||
const callScopedHotkeyCallback = useScopedHotkeyCallback();
|
||||
|
||||
useHotkeys(
|
||||
firstKey,
|
||||
(keyboardEvent, hotkeysEvent) => {
|
||||
callScopedHotkeyCallback({
|
||||
keyboardEvent,
|
||||
hotkeysEvent,
|
||||
callback: () => {
|
||||
setPendingHotkey(firstKey);
|
||||
},
|
||||
scope,
|
||||
preventDefault: !!options.preventDefault,
|
||||
});
|
||||
},
|
||||
{
|
||||
enableOnContentEditable: options.enableOnContentEditable,
|
||||
enableOnFormTags: options.enableOnFormTags,
|
||||
},
|
||||
[setPendingHotkey, scope],
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
secondKey,
|
||||
(keyboardEvent, hotkeysEvent) => {
|
||||
callScopedHotkeyCallback({
|
||||
keyboardEvent,
|
||||
hotkeysEvent,
|
||||
callback: () => {
|
||||
if (pendingHotkey !== firstKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingHotkey(null);
|
||||
|
||||
if (!!options.preventDefault) {
|
||||
keyboardEvent.stopImmediatePropagation();
|
||||
keyboardEvent.stopPropagation();
|
||||
keyboardEvent.preventDefault();
|
||||
}
|
||||
|
||||
sequenceCallback();
|
||||
},
|
||||
scope,
|
||||
preventDefault: false,
|
||||
});
|
||||
},
|
||||
{
|
||||
enableOnContentEditable: options.enableOnContentEditable,
|
||||
enableOnFormTags: options.enableOnFormTags,
|
||||
},
|
||||
[pendingHotkey, setPendingHotkey, scope, ...deps],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES } from '../constants';
|
||||
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
|
||||
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
|
||||
import { AppHotkeyScope } from '../types/AppHotkeyScope';
|
||||
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
|
||||
import { HotkeyScope } from '../types/HotkeyScope';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newHotkeyScope: HotkeyScope = {
|
||||
scope: hotkeyScopeToSet,
|
||||
customScopes: {
|
||||
commandMenu: customScopes?.commandMenu ?? true,
|
||||
goto: customScopes?.goto ?? false,
|
||||
},
|
||||
};
|
||||
|
||||
const scopesToSet: string[] = [];
|
||||
|
||||
if (newHotkeyScope.customScopes?.commandMenu) {
|
||||
scopesToSet.push(AppHotkeyScope.CommandMenu);
|
||||
}
|
||||
|
||||
if (newHotkeyScope?.customScopes?.goto) {
|
||||
scopesToSet.push(AppHotkeyScope.Goto);
|
||||
}
|
||||
|
||||
scopesToSet.push(newHotkeyScope.scope);
|
||||
|
||||
set(internalHotkeysEnabledScopesState, scopesToSet);
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
export enum AppHotkeyScope {
|
||||
App = 'app',
|
||||
Goto = 'goto',
|
||||
CommandMenu = 'command-menu',
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export type CustomHotkeyScopes = {
|
||||
goto?: boolean;
|
||||
commandMenu?: boolean;
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
import { CustomHotkeyScopes } from './CustomHotkeyScope';
|
||||
|
||||
export type HotkeyScope = {
|
||||
scope: string;
|
||||
customScopes?: CustomHotkeyScopes;
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import { HotkeyScope } from '../types/HotkeyScope';
|
||||
|
||||
export function isSameHotkeyScope(
|
||||
hotkeyScope1: HotkeyScope | undefined | null,
|
||||
hotkeyScope2: HotkeyScope | undefined | null,
|
||||
): boolean {
|
||||
return JSON.stringify(hotkeyScope1) === JSON.stringify(hotkeyScope2);
|
||||
}
|
||||
Reference in New Issue
Block a user