| 1 | export interface RecordingInputOptions { |
| 2 | readonly showKeyPresses: boolean; |
| 3 | readonly showMousePresses: boolean; |
| 4 | } |
| 5 | |
| 6 | export const DEFAULT_RECORDING_INPUT_OPTIONS: RecordingInputOptions = { |
| 7 | showKeyPresses: false, |
| 8 | showMousePresses: false, |
| 9 | }; |
| 10 | |
| 11 | export interface RecordingKeyPress { |
| 12 | readonly key: string; |
| 13 | readonly metaKey: boolean; |
| 14 | readonly ctrlKey: boolean; |
| 15 | readonly altKey: boolean; |
| 16 | readonly shiftKey: boolean; |
| 17 | } |
| 18 | |
| 19 | /** Formats a single chord without duplicating a modifier pressed on its own. */ |
| 20 | export function recordingKeyLabel(input: RecordingKeyPress, isMac: boolean): string | null { |
| 21 | if (["Dead", "Process", "Unidentified", ""].includes(input.key)) return null; |
| 22 | const modifiers = [ |
| 23 | input.ctrlKey || input.key === "Control" ? (isMac ? "⌃" : "Ctrl") : null, |
| 24 | input.altKey || input.key === "Alt" ? (isMac ? "⌥" : "Alt") : null, |
| 25 | input.shiftKey || input.key === "Shift" ? (isMac ? "⇧" : "Shift") : null, |
| 26 | input.metaKey || input.key === "Meta" ? (isMac ? "⌘" : "Win") : null, |
| 27 | ].filter((value) => value !== null); |
| 28 | const labels: Record<string, string> = { |
| 29 | Enter: "↵", |
| 30 | Tab: "⇥", |
| 31 | Backspace: "⌫", |
| 32 | Delete: "⌦", |
| 33 | Escape: "Esc", |
| 34 | ArrowUp: "↑", |
| 35 | ArrowDown: "↓", |
| 36 | ArrowLeft: "←", |
| 37 | ArrowRight: "→", |
| 38 | " ": "Space", |
| 39 | Space: "Space", |
| 40 | }; |
| 41 | if (!["Control", "Alt", "Shift", "Meta"].includes(input.key)) { |
| 42 | modifiers.push( |
| 43 | labels[input.key] ?? (input.key.length === 1 ? input.key.toUpperCase() : input.key), |
| 44 | ); |
| 45 | } |
| 46 | return modifiers.join(isMac ? "" : " + "); |
| 47 | } |
| 48 | |
| 49 | /** Unknown iframe or closed-shadow focus is excluded because its field type cannot be checked. */ |
| 50 | export function recordingKeysAreSensitive(document: Document): boolean { |
| 51 | let element = document.activeElement; |
| 52 | while (element?.shadowRoot?.activeElement) element = element.shadowRoot.activeElement; |
| 53 | return ( |
| 54 | element?.tagName === "IFRAME" || |
| 55 | element?.tagName.includes("-") === true || |
| 56 | element?.getAttribute("type")?.toLowerCase() === "password" |
| 57 | ); |
| 58 | } |