| 1 |
import { __, _x } from '@wordpress/i18n' |
| 2 |
import { getKeyMap } from 'codemirror/src/input/keymap' |
| 3 |
import React, { Fragment, useMemo } from 'react' |
| 4 |
import { useSnippetForm } from '../WithSnippetFormContext' |
| 5 |
import { isMacOS } from '../../../../utils/screen' |
| 6 |
import type { KeyMap } from 'codemirror' |
| 7 |
|
| 8 |
const KEYBOARD_KEYS = { |
| 9 |
'Fn': _x('Fn', 'keyboard key', 'code-snippets'), |
| 10 |
'Cmd': _x('Ctrl', 'keyboard key', 'code-snippets'), |
| 11 |
'Ctrl': _x('Ctrl', 'keyboard key', 'code-snippets'), |
| 12 |
'Shift': _x('Shift', 'keyboard key', 'code-snippets'), |
| 13 |
'Alt': _x('Alt', 'keyboard key', 'code-snippets'), |
| 14 |
'Tab': _x('Tab', 'keyboard key', 'code-snippets'), |
| 15 |
'Up': _x('Up', 'keyboard key', 'code-snippets'), |
| 16 |
'Down': _x('Down', 'keyboard key', 'code-snippets'), |
| 17 |
'Left': _x('Left', 'keyboard key', 'code-snippets'), |
| 18 |
'Right': _x('Right', 'keyboard key', 'code-snippets'), |
| 19 |
'A': _x('A', 'keyboard key', 'code-snippets'), |
| 20 |
'B': _x('B', 'keyboard key', 'code-snippets'), |
| 21 |
'C': _x('C', 'keyboard key', 'code-snippets'), |
| 22 |
'D': _x('D', 'keyboard key', 'code-snippets'), |
| 23 |
'E': _x('E', 'keyboard key', 'code-snippets'), |
| 24 |
'F': _x('F', 'keyboard key', 'code-snippets'), |
| 25 |
'G': _x('G', 'keyboard key', 'code-snippets'), |
| 26 |
'H': _x('H', 'keyboard key', 'code-snippets'), |
| 27 |
'I': _x('I', 'keyboard key', 'code-snippets'), |
| 28 |
'J': _x('J', 'keyboard key', 'code-snippets'), |
| 29 |
'K': _x('K', 'keyboard key', 'code-snippets'), |
| 30 |
'L': _x('L', 'keyboard key', 'code-snippets'), |
| 31 |
'M': _x('M', 'keyboard key', 'code-snippets'), |
| 32 |
'N': _x('N', 'keyboard key', 'code-snippets'), |
| 33 |
'O': _x('O', 'keyboard key', 'code-snippets'), |
| 34 |
'P': _x('P', 'keyboard key', 'code-snippets'), |
| 35 |
'Q': _x('Q', 'keyboard key', 'code-snippets'), |
| 36 |
'R': _x('R', 'keyboard key', 'code-snippets'), |
| 37 |
'S': _x('S', 'keyboard key', 'code-snippets'), |
| 38 |
'T': _x('T', 'keyboard key', 'code-snippets'), |
| 39 |
'U': _x('U', 'keyboard key', 'code-snippets'), |
| 40 |
'V': _x('V', 'keyboard key', 'code-snippets'), |
| 41 |
'W': _x('W', 'keyboard key', 'code-snippets'), |
| 42 |
'X': _x('X', 'keyboard key', 'code-snippets'), |
| 43 |
'Y': _x('Y', 'keyboard key', 'code-snippets'), |
| 44 |
'Z': _x('Z', 'keyboard key', 'code-snippets'), |
| 45 |
'/': _x('/', 'keyboard key', 'code-snippets'), |
| 46 |
'[': _x(']', 'keyboard key', 'code-snippets'), |
| 47 |
']': _x(']', 'keyboard key', 'code-snippets') |
| 48 |
} |
| 49 |
|
| 50 |
export const KEYBOARD_SYMBOLS: Partial<typeof KEYBOARD_KEYS> = { |
| 51 |
Cmd: '⌘', |
| 52 |
Ctrl: '⌃', |
| 53 |
Alt: '⌥', |
| 54 |
Shift: '⇧', |
| 55 |
Tab: '⇥', |
| 56 |
Up: '↑', |
| 57 |
Down: '↓', |
| 58 |
Left: '←', |
| 59 |
Right: '→' |
| 60 |
} |
| 61 |
|
| 62 |
const keyMapLabels = { |
| 63 |
saveChanges: __('Save changes', 'code-snippets'), |
| 64 |
selectAll: __('Select all', 'code-snippets'), |
| 65 |
find: __('Begin searching', 'code-snippets'), |
| 66 |
findNext: __('Find next', 'code-snippets'), |
| 67 |
findPrev: __('Find previous', 'code-snippets'), |
| 68 |
replace: __('Replace', 'code-snippets'), |
| 69 |
replaceAll: __('Replace all', 'code-snippets'), |
| 70 |
findPersistent: __('Persistent search', 'code-snippets'), |
| 71 |
toggleComment: __('Toggle comment', 'code-snippets'), |
| 72 |
swapLineUp: __('Swap line up', 'code-snippets'), |
| 73 |
swapLineDown: __('Swap line down', 'code-snippets'), |
| 74 |
autoIndent: __('Auto-indent current line or selection', 'code-snippets') |
| 75 |
} |
| 76 |
|
| 77 |
const KEY_ORDER: readonly (keyof typeof KEYBOARD_KEYS)[] = isMacOS() |
| 78 |
? ['Fn', 'Ctrl', 'Alt', 'Shift', 'Cmd'] |
| 79 |
: ['Cmd', 'Ctrl', 'Shift', 'Alt', 'Fn'] |
| 80 |
|
| 81 |
const getKeyComparisonValue = (key: string): string => |
| 82 |
KEY_ORDER.includes(key as keyof typeof KEYBOARD_KEYS) |
| 83 |
? String(KEY_ORDER.indexOf(key as keyof typeof KEYBOARD_KEYS)) |
| 84 |
: key |
| 85 |
|
| 86 |
const unpackKeyMap = (keyMap: KeyMap): Map<string, string[]> => { |
| 87 |
const result = new Map<string, string[]>() |
| 88 |
|
| 89 |
for (const [shortcut, action] of Object.entries(keyMap)) { |
| 90 |
if ('string' === typeof action && keyMapLabels[action as keyof typeof keyMapLabels]) { |
| 91 |
const keys = shortcut.split('-') |
| 92 |
|
| 93 |
keys.sort((a, b) => |
| 94 |
getKeyComparisonValue(a).localeCompare(getKeyComparisonValue(b))) |
| 95 |
|
| 96 |
result.set(action, keys) |
| 97 |
} |
| 98 |
} |
| 99 |
|
| 100 |
return result |
| 101 |
} |
| 102 |
|
| 103 |
interface KeyboardShortcutMacProps { |
| 104 |
keys: string[] |
| 105 |
keyLabels: Partial<Record<string, string>> |
| 106 |
keySymbols: Partial<Record<string, string>> |
| 107 |
} |
| 108 |
|
| 109 |
const KeyboardShortcutMac: React.FC<KeyboardShortcutMacProps> = ({ keys, keyLabels, keySymbols }) => |
| 110 |
<span className="keyboard-shortcut mac-keyboard-shortcut"> |
| 111 |
{keys.map(key => |
| 112 |
<kbd key={key}>{keySymbols[key] ?? keyLabels[key] ?? key}</kbd>)} |
| 113 |
</span> |
| 114 |
|
| 115 |
const SEP = _x('+', 'keyboard shortcut separator', 'code-snippets') |
| 116 |
|
| 117 |
interface KeyboardShortcutPCProps { |
| 118 |
keys: string[] |
| 119 |
keyLabels: Partial<Record<string, string>> |
| 120 |
} |
| 121 |
|
| 122 |
const KeyboardShortcutPC: React.FC<KeyboardShortcutPCProps> = ({ keys, keyLabels }) => |
| 123 |
<span className="keyboard-shortcut pc-keyboard-shortcot"> |
| 124 |
{keys.map((key, index) => |
| 125 |
<Fragment key={key}> |
| 126 |
<kbd>{keyLabels[key] ?? key}</kbd> |
| 127 |
{index < keys.length - 1 && <span className="shortcut-separator">{SEP}</span>} |
| 128 |
</Fragment>)} |
| 129 |
</span> |
| 130 |
|
| 131 |
const fallbackKeyMap: Partial<Record<`${string}-${string}`, keyof typeof keyMapLabels>> = { |
| 132 |
'Ctrl-S': 'saveChanges', |
| 133 |
'Shift-Tab': 'autoIndent' |
| 134 |
} |
| 135 |
|
| 136 |
const fallbackKeyMapMac: typeof fallbackKeyMap = { |
| 137 |
'Cmd-S': 'saveChanges', |
| 138 |
'Shift-Tab': 'autoIndent' |
| 139 |
} |
| 140 |
|
| 141 |
export interface CodeEditorShortcutsProps { |
| 142 |
editorTheme: string |
| 143 |
} |
| 144 |
|
| 145 |
export const CodeEditorShortcuts: React.FC<CodeEditorShortcutsProps> = ({ editorTheme }) => { |
| 146 |
const { codeEditorInstance } = useSnippetForm() |
| 147 |
|
| 148 |
const shortcutKeys: Map<string, string[]> | undefined = useMemo(() => { |
| 149 |
if (codeEditorInstance) { |
| 150 |
const extraKeys = codeEditorInstance.codemirror.getOption('extraKeys') |
| 151 |
const keyMapName = codeEditorInstance.codemirror.getOption('keyMap') |
| 152 |
|
| 153 |
const combinedKeyMap: KeyMap = { |
| 154 |
...isMacOS() ? fallbackKeyMapMac : fallbackKeyMap, |
| 155 |
...keyMapName && getKeyMap(keyMapName), |
| 156 |
...'object' === typeof extraKeys ? extraKeys : undefined |
| 157 |
} |
| 158 |
|
| 159 |
return unpackKeyMap(combinedKeyMap) |
| 160 |
} |
| 161 |
|
| 162 |
return undefined |
| 163 |
}, |
| 164 |
[codeEditorInstance] |
| 165 |
) |
| 166 |
|
| 167 |
return shortcutKeys |
| 168 |
? <div className="snippet-editor-help tooltip tooltip-inline tooltip-start"> |
| 169 |
<span className={`dashicons dashicons-editor-help cm-s-${editorTheme}`} aria-hidden="true"></span> |
| 170 |
|
| 171 |
<div className="tooltip-content"> |
| 172 |
<table> |
| 173 |
<tbody> |
| 174 |
{Object.entries(keyMapLabels).map(([action, label]) => { |
| 175 |
const keys = shortcutKeys.get(action) |
| 176 |
return keys |
| 177 |
? <tr key={action}> |
| 178 |
<td>{label}</td> |
| 179 |
<td> |
| 180 |
{isMacOS() |
| 181 |
? <KeyboardShortcutMac |
| 182 |
keys={keys} |
| 183 |
keyLabels={KEYBOARD_KEYS} |
| 184 |
keySymbols={KEYBOARD_SYMBOLS} |
| 185 |
/> |
| 186 |
: <KeyboardShortcutPC |
| 187 |
keys={keys} |
| 188 |
keyLabels={KEYBOARD_KEYS} |
| 189 |
/>} |
| 190 |
</td> |
| 191 |
</tr> |
| 192 |
: null |
| 193 |
})} |
| 194 |
</tbody> |
| 195 |
</table> |
| 196 |
</div> |
| 197 |
</div> |
| 198 |
: null |
| 199 |
} |
| 200 |
|