// Desktop HID-wedge scanner capture for the /integrations terminal pages. // // Encapsulates the returns-desktop.vue pattern (global keydown → focus a hidden // off-screen input so a USB scanner works without manual focus) and fixes its // keepalive flaw: app.vue renders which // OVERRIDES page-level `keepalive: false`, so terminal pages stay mounted after // navigating away. Binding only in onMounted/onBeforeUnmount leaves up to 10 // cached pages fighting over every keystroke. This composable binds in // onMounted+onActivated and unbinds in onDeactivated+onBeforeUnmount, and the // handler additionally checks that this page's route is still the active one. // // The page still owns: the hidden element (position it off-screen), // its v-model scan buffer, and all Enter/step dispatching via `onEnter`. import type { Ref } from 'vue' export interface UseDesktopScannerOptions { /** Template ref of the hidden off-screen input the scanner types into. */ input: Ref /** Master switch; when false the handler ignores everything. Default true. */ enabled?: Ref | (() => boolean) /** Return true while any modal/overlay is open or a form-heavy step is active. */ isBlocked?: () => boolean /** Enter pressed outside a real input — dispatch by step; call e.preventDefault() yourself. */ onEnter?: (e: KeyboardEvent) => void } export function useDesktopScanner (opts: UseDesktopScannerOptions) { const router = useRouter() // Captured once at setup: the path this page instance owns. const ownPath = router.currentRoute.value.path const isActive = ref(false) let bound = false const isEnabled = () => { const e = opts.enabled if (e === undefined) return true return typeof e === 'function' ? e() : e.value !== false } const focusInput = () => { if (!isActive.value) return try { opts.input.value?.focus() } catch { /* detached ref */ } } const onWindowKeydown = (e: KeyboardEvent) => { if (!isActive.value) return if (router.currentRoute.value.path !== ownPath) return if (!isEnabled()) return if (e.ctrlKey || e.metaKey || e.altKey) return const target = e.target as HTMLElement | null if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT' || target.isContentEditable)) return if (opts.isBlocked?.()) return if (e.key && e.key.length === 1) { // Printable char: focus the hidden input; the char lands there (no preventDefault). focusInput() } else if (e.key === 'Enter') { opts.onEnter?.(e) } } const bind = () => { if (!import.meta.client || bound) return window.addEventListener('keydown', onWindowKeydown) bound = true isActive.value = true } const unbind = () => { if (!import.meta.client || !bound) return window.removeEventListener('keydown', onWindowKeydown) bound = false isActive.value = false } onMounted(bind) onActivated(bind) onDeactivated(unbind) onBeforeUnmount(unbind) return { focusInput, isActive } }