|
|
@@ -1,4 +1,11 @@
|
|
|
-import { createContext, useCallback, useContext, useState } from 'react'
|
|
|
+import { isMac } from '@/shared/utils/os'
|
|
|
+import {
|
|
|
+ createContext,
|
|
|
+ useCallback,
|
|
|
+ useContext,
|
|
|
+ useMemo,
|
|
|
+ useState,
|
|
|
+} from 'react'
|
|
|
|
|
|
type CommandInvocationContext = {
|
|
|
location?: string
|
|
|
@@ -10,17 +17,21 @@ export type Command = {
|
|
|
handler?: (context: CommandInvocationContext) => void
|
|
|
href?: string
|
|
|
disabled?: boolean
|
|
|
- // TODO: Keybinding?
|
|
|
}
|
|
|
|
|
|
const CommandRegistryContext = createContext<CommandRegistry | undefined>(
|
|
|
undefined
|
|
|
)
|
|
|
|
|
|
+export type Shortcut = { key: string }
|
|
|
+
|
|
|
+export type Shortcuts = Record<string, Shortcut[]>
|
|
|
+
|
|
|
type CommandRegistry = {
|
|
|
registry: Map<string, Command>
|
|
|
register: (...elements: Command[]) => void
|
|
|
unregister: (...id: string[]) => void
|
|
|
+ shortcuts: Shortcuts
|
|
|
}
|
|
|
|
|
|
export const CommandRegistryProvider: React.FC<React.PropsWithChildren> = ({
|
|
|
@@ -43,8 +54,35 @@ export const CommandRegistryProvider: React.FC<React.PropsWithChildren> = ({
|
|
|
)
|
|
|
}, [])
|
|
|
|
|
|
+ // NOTE: This is where we'd add functionality for customising shortcuts.
|
|
|
+ const shortcuts: Record<string, Shortcut[]> = useMemo(
|
|
|
+ () => ({
|
|
|
+ undo: [
|
|
|
+ {
|
|
|
+ key: 'Mod-z',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ redo: [
|
|
|
+ {
|
|
|
+ key: 'Mod-y',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'Mod-Shift-Z',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ find: [{ key: 'Mod-f' }],
|
|
|
+ 'select-all': [{ key: 'Mod-a' }],
|
|
|
+ 'insert-comment': [{ key: 'Mod-Shift-C' }],
|
|
|
+ 'format-bold': [{ key: 'Mod-b' }],
|
|
|
+ 'format-italics': [{ key: 'Mod-i' }],
|
|
|
+ }),
|
|
|
+ []
|
|
|
+ )
|
|
|
+
|
|
|
return (
|
|
|
- <CommandRegistryContext.Provider value={{ registry, register, unregister }}>
|
|
|
+ <CommandRegistryContext.Provider
|
|
|
+ value={{ registry, register, unregister, shortcuts }}
|
|
|
+ >
|
|
|
{children}
|
|
|
</CommandRegistryContext.Provider>
|
|
|
)
|
|
|
@@ -59,3 +97,92 @@ export const useCommandRegistry = (): CommandRegistry => {
|
|
|
}
|
|
|
return context
|
|
|
}
|
|
|
+
|
|
|
+function parseShortcut(shortcut: Shortcut) {
|
|
|
+ // Based on KeyBinding type of CodeMirror 6
|
|
|
+ let alt = false
|
|
|
+ let ctrl = false
|
|
|
+ let shift = false
|
|
|
+ let meta = false
|
|
|
+
|
|
|
+ let character = null
|
|
|
+ // isMac ? shortcut.mac : shortcut.key etc.
|
|
|
+ const shortcutString = shortcut.key ?? ''
|
|
|
+ const keys = shortcutString.split(/-(?!$)/) ?? []
|
|
|
+
|
|
|
+ for (let i = 0; i < keys.length; i++) {
|
|
|
+ const isLast = i === keys.length - 1
|
|
|
+ const key = keys[i]
|
|
|
+ if (!key) {
|
|
|
+ throw new Error('Empty key in shortcut: ' + shortcutString)
|
|
|
+ }
|
|
|
+ if (key === 'Alt' || (!isLast && key === 'a')) {
|
|
|
+ alt = true
|
|
|
+ } else if (
|
|
|
+ key === 'Ctrl' ||
|
|
|
+ key === 'Control' ||
|
|
|
+ (!isLast && key === 'c')
|
|
|
+ ) {
|
|
|
+ ctrl = true
|
|
|
+ } else if (key === 'Shift' || (!isLast && key === 's')) {
|
|
|
+ shift = true
|
|
|
+ } else if (key === 'Meta' || key === 'Cmd' || (!isLast && key === 'm')) {
|
|
|
+ meta = true
|
|
|
+ } else if (key === 'Mod') {
|
|
|
+ if (isMac) {
|
|
|
+ meta = true
|
|
|
+ } else {
|
|
|
+ ctrl = true
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (key === 'Space') {
|
|
|
+ character = ' '
|
|
|
+ }
|
|
|
+ if (!isLast) {
|
|
|
+ throw new Error(
|
|
|
+ 'Character key must be last in shortcut: ' + shortcutString
|
|
|
+ )
|
|
|
+ }
|
|
|
+ if (key.length !== 1) {
|
|
|
+ throw new Error(`Invalid key '${key}' in shortcut: ${shortcutString}`)
|
|
|
+ }
|
|
|
+ if (character) {
|
|
|
+ throw new Error('Multiple characters in shortcut: ' + shortcutString)
|
|
|
+ }
|
|
|
+ character = key
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!character) {
|
|
|
+ throw new Error('No character in shortcut: ' + shortcutString)
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ alt,
|
|
|
+ ctrl,
|
|
|
+ shift,
|
|
|
+ meta,
|
|
|
+ character,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export const formatShortcut = (shortcut: Shortcut): string => {
|
|
|
+ const { alt, ctrl, shift, meta, character } = parseShortcut(shortcut)
|
|
|
+
|
|
|
+ if (isMac) {
|
|
|
+ return [
|
|
|
+ ctrl ? '⌃' : '',
|
|
|
+ alt ? '⌥' : '',
|
|
|
+ shift ? '⇧' : '',
|
|
|
+ meta ? '⌘' : '',
|
|
|
+ character.toUpperCase(),
|
|
|
+ ].join('')
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ ctrl ? 'Ctrl' : '',
|
|
|
+ shift ? 'Shift' : '',
|
|
|
+ meta ? 'Meta' : '',
|
|
|
+ alt ? 'Alt' : '',
|
|
|
+ character.toUpperCase(),
|
|
|
+ ].join(' ')
|
|
|
+}
|