diff --git a/package-lock.json b/package-lock.json index 8f292459..63486fc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "react": "^18.3.1", "react-code-blocks": "^0.1.6", "react-dom": "^18.3.1", + "react-hotkeys-hook": "^4.5.0", "react-resizable-panels": "^2.0.22", "react-responsive": "^10.0.0", "react-router-dom": "^6.26.0", @@ -8019,6 +8020,16 @@ "react": "^18.3.1" } }, + "node_modules/react-hotkeys-hook": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz", + "integrity": "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.1", + "react-dom": ">=16.8.1" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 9585bc2f..87c9787a 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "react": "^18.3.1", "react-code-blocks": "^0.1.6", "react-dom": "^18.3.1", + "react-hotkeys-hook": "^4.5.0", "react-resizable-panels": "^2.0.22", "react-responsive": "^10.0.0", "react-router-dom": "^6.26.0", diff --git a/src/context/keyboard-shortcuts-context/keyboard-shortcuts-context.tsx b/src/context/keyboard-shortcuts-context/keyboard-shortcuts-context.tsx new file mode 100644 index 00000000..4926a950 --- /dev/null +++ b/src/context/keyboard-shortcuts-context/keyboard-shortcuts-context.tsx @@ -0,0 +1,7 @@ +import { createContext } from 'react'; + +export interface KeyboardShortcutsContext {} + +export const keyboardShortcutsContext = createContext( + {} +); diff --git a/src/context/keyboard-shortcuts-context/keyboard-shortcuts-provider.tsx b/src/context/keyboard-shortcuts-context/keyboard-shortcuts-provider.tsx new file mode 100644 index 00000000..6e6c3081 --- /dev/null +++ b/src/context/keyboard-shortcuts-context/keyboard-shortcuts-provider.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { keyboardShortcutsContext } from './keyboard-shortcuts-context'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { + KeyboardShortcutAction, + keyboardShortcutsForOS, +} from './keyboard-shortcuts'; +import { useHistory } from '@/hooks/use-history'; + +export const KeyboardShortcutsProvider: React.FC = ({ + children, +}) => { + const { redo, undo } = useHistory(); + useHotkeys( + keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination, + redo, + [redo] + ); + useHotkeys( + keyboardShortcutsForOS[KeyboardShortcutAction.UNDO].keyCombination, + undo, + [undo] + ); + return ( + + {children} + + ); +}; diff --git a/src/context/keyboard-shortcuts-context/keyboard-shortcuts.ts b/src/context/keyboard-shortcuts-context/keyboard-shortcuts.ts new file mode 100644 index 00000000..7c54563d --- /dev/null +++ b/src/context/keyboard-shortcuts-context/keyboard-shortcuts.ts @@ -0,0 +1,70 @@ +import { getOperatingSystem } from '@/lib/utils'; + +export enum KeyboardShortcutAction { + REDO = 'redo', + UNDO = 'undo', +} + +export interface KeyboardShortcut { + action: KeyboardShortcutAction; + keyCombinationLabelMac: string; + keyCombinationLabelWin: string; + keyCombinationMac: string; + keyCombinationWin: string; +} + +export const keyboardShortcuts: Record< + KeyboardShortcutAction, + KeyboardShortcut +> = { + [KeyboardShortcutAction.REDO]: { + action: KeyboardShortcutAction.REDO, + keyCombinationLabelMac: '⇧⌘Z', + keyCombinationLabelWin: 'Ctrl+Shift+Z', + keyCombinationMac: 'meta+shift+z', + keyCombinationWin: 'ctrl+shift+z', + }, + [KeyboardShortcutAction.UNDO]: { + action: KeyboardShortcutAction.UNDO, + keyCombinationLabelMac: '⌘Z', + keyCombinationLabelWin: 'Ctrl+Z', + keyCombinationMac: 'meta+z', + keyCombinationWin: 'ctrl+z', + }, +}; + +export interface KeyboardShortcutForOS { + action: KeyboardShortcutAction; + keyCombinationLabel: string; + keyCombination: string; +} + +const operatingSystem = getOperatingSystem(); + +export const keyboardShortcutsForOS: Record< + KeyboardShortcutAction, + KeyboardShortcutForOS +> = Object.keys(keyboardShortcuts).reduce( + (acc, action) => { + const keyboardShortcut = + keyboardShortcuts[action as KeyboardShortcutAction]; + const keyCombinationLabel = + operatingSystem === 'mac' + ? keyboardShortcut.keyCombinationLabelMac + : keyboardShortcut.keyCombinationLabelWin; + const keyCombination = + operatingSystem === 'mac' + ? keyboardShortcut.keyCombinationMac + : keyboardShortcut.keyCombinationWin; + + return { + ...acc, + [action]: { + action: keyboardShortcut.action, + keyCombinationLabel, + keyCombination, + }, + }; + }, + {} as Record +); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 5c9a660e..1574d858 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -13,3 +13,14 @@ export function cn(...inputs: ClassValue[]) { export const emptyFn = (): any => undefined; export const generateId = () => randomId(); + +export const getOperatingSystem = (): 'mac' | 'windows' | 'unknown' => { + const userAgent = window.navigator.userAgent; + if (userAgent.includes('Mac OS X')) { + return 'mac'; + } + if (userAgent.includes('Windows')) { + return 'windows'; + } + return 'unknown'; +}; diff --git a/src/pages/editor-page/top-navbar/top-navbar.tsx b/src/pages/editor-page/top-navbar/top-navbar.tsx index 75a4fecb..acff73bc 100644 --- a/src/pages/editor-page/top-navbar/top-navbar.tsx +++ b/src/pages/editor-page/top-navbar/top-navbar.tsx @@ -33,6 +33,11 @@ import { useConfig } from '@/hooks/use-config'; import { IS_CHARTDB_IO } from '@/lib/env'; import { useBreakpoint } from '@/hooks/use-breakpoint'; import { DiagramIcon } from '@/components/diagram-icon/diagram-icon'; +import { + KeyboardShortcutAction, + keyboardShortcutsForOS, +} from '@/context/keyboard-shortcuts-context/keyboard-shortcuts'; +import { useHistory } from '@/hooks/use-history'; export interface TopNavbarProps {} @@ -50,6 +55,7 @@ export const TopNavbar: React.FC = () => { openExportSQLDialog, showAlert, } = useDialog(); + const { redo, undo, hasRedo, hasUndo } = useHistory(); const { isMd: isDesktop } = useBreakpoint('md'); const { config, updateConfig } = useConfig(); const [editMode, setEditMode] = useState(false); @@ -398,8 +404,26 @@ export const TopNavbar: React.FC = () => { Edit - Undo - Redo + + Undo + + { + keyboardShortcutsForOS[ + KeyboardShortcutAction.UNDO + ].keyCombinationLabel + } + + + + Redo + + { + keyboardShortcutsForOS[ + KeyboardShortcutAction.REDO + ].keyCombinationLabel + } + + diff --git a/src/router.tsx b/src/router.tsx index 1973c0ac..399def5f 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -13,6 +13,7 @@ import { DialogProvider } from './context/dialog-context/dialog-provider'; import { ExportImageProvider } from './context/export-image-context/export-image-provider'; import { FullScreenLoaderProvider } from './context/full-screen-spinner-context/full-screen-spinner-provider'; import { ExamplesPage } from './pages/examples-page/examples-page'; +import { KeyboardShortcutsProvider } from './context/keyboard-shortcuts-context/keyboard-shortcuts-provider'; const routes: RouteObject[] = [ ...['', 'diagrams/:diagramId'].map((path) => ({ @@ -28,9 +29,11 @@ const routes: RouteObject[] = [ - {/* */} - - {/* */} + + {/* */} + + {/* */} +