Files
linsa-linsa-io/web/app/hooks/use-key-down.ts
2024-10-07 12:44:17 +03:00

78 lines
1.8 KiB
TypeScript

import * as React from "react"
import { isModKey, isServer, isTextInput } from "@/lib/utils"
export type KeyFilter = ((event: KeyboardEvent) => boolean) | string
export type Options = { allowInInput?: boolean }
type RegisteredCallback = {
callback: (event: KeyboardEvent) => void
options?: Options
}
let callbacks: RegisteredCallback[] = []
let isInitialized = false
const initializeKeyboardListeners = () => {
if (isServer() || isInitialized) return
let imeOpen = false
window.addEventListener("keydown", (event) => {
if (imeOpen) return
for (const registered of [...callbacks].reverse()) {
if (event.defaultPrevented) break
if (
!isTextInput(event.target as HTMLElement) ||
registered.options?.allowInInput ||
isModKey(event)
) {
registered.callback(event)
}
}
})
window.addEventListener("compositionstart", () => {
imeOpen = true
})
window.addEventListener("compositionend", () => {
imeOpen = false
})
isInitialized = true
}
const createKeyPredicate = (keyFilter: KeyFilter) =>
typeof keyFilter === "function"
? keyFilter
: typeof keyFilter === "string"
? (event: KeyboardEvent) => event.key === keyFilter
: keyFilter
? () => true
: () => false
export function useKeyDown(
key: KeyFilter,
fn: (event: KeyboardEvent) => void,
options?: Options,
): void {
const predicate = React.useMemo(() => createKeyPredicate(key), [key])
React.useEffect(() => {
initializeKeyboardListeners()
const handler = (event: KeyboardEvent) => {
if (predicate(event)) {
fn(event)
}
}
callbacks.push({ callback: handler, options })
return () => {
callbacks = callbacks.filter((cb) => cb.callback !== handler)
}
}, [fn, predicate, options])
}