From c9e53c37f551ec9cd584ca65ede22524c0a10f56 Mon Sep 17 00:00:00 2001 From: Filipe Dobreira Date: Thu, 7 Dec 2023 20:34:43 +0000 Subject: [PATCH] Add basic support for hidden barcode input --- frontend/composables/use-quick-search.ts | 119 +++++++++++++++++++++++ frontend/layouts/default.vue | 4 + frontend/package.json | 1 + frontend/pnpm-lock.yaml | 8 ++ 4 files changed, 132 insertions(+) create mode 100644 frontend/composables/use-quick-search.ts diff --git a/frontend/composables/use-quick-search.ts b/frontend/composables/use-quick-search.ts new file mode 100644 index 0000000..6e484e8 --- /dev/null +++ b/frontend/composables/use-quick-search.ts @@ -0,0 +1,119 @@ +import { getRealFormat, isValid } from "gtin"; + +// Convenient representations of gtin's format values. +enum CodeFormat { + EAN13 = "GTIN-13", // 13-digit EAN + UPCA = "GTIN-12", // 12-digit UPC-A +} + +function isSearchShortcut(event: KeyboardEvent): boolean { + return (event.metaKey || event.ctrlKey) && event.key === "f"; +} + +/** + * For our purposes, a valid barcode fragment is any digit, with no + * standard modifiers enabled (shift, ctrl, meta): + */ +function isCodeFragment(event: KeyboardEvent): boolean { + return !Number.isNaN(parseInt(event.key)) && !event.ctrlKey && !event.metaKey && !event.shiftKey; +} + +/** + * Performs a naive check to ensure something is a EAN13 or UPC-A code. + */ +function isValidIshCode(code: string): boolean { + try { + // We check the format in advance, since we're only accepting EAN13 and UPC-A here. + // There's no -real- reason for this limitation, besides protecting against feature + // creep from supporting a larger number of formats. + const realFormat = getRealFormat(code); + + return realFormat === CodeFormat.EAN13 || (realFormat === CodeFormat.UPCA && isValid(code)); + } catch (err) { + // gtin.getRealFormat will throw on codes that are absolutely invalid, + // as opposed to returning false for codes that look correct but fail + // a format-specific check. + return false; + } +} + +/** + * Provides utilities to: + * + * - Quickly open a global search utility, that forwards to the item page. + * - Transparently listens for UPC/EAN-like barcode input, and automatically + * generates an appropriate search query for valid inputs; this allows consumer + * USB barcode scanners to be used as a quick shortcut. + */ +export function useQuickSearch() { + /** + * Tracks if the quick search dialog is active. + * + * We 'steal' cmd/ctrl+F from the user, but allow them to go back to + * the browser-default search by tapping the shortcut twice. + */ + const isActive = ref(false); + + /** + * codeBuffer acts as intermediate buffer state for a partial + * EAN/UPC code. + */ + const codeBuffer = ref(""); + + function onKeyDown(event: KeyboardEvent) { + if (isSearchShortcut(event)) { + // If quick search is already active, and the user taps cmd+F, get it out of the way + // and allow the default browser search to kick in: + if (isActive.value) { + isActive.value = false; + } else { + isActive.value = true; + event.preventDefault(); + } + } else if (isCodeFragment(event)) { + const fragment = event.key; + + // Push this code fragment into our buffer. At this point we also + // ensure we have no more than 13 numbers in the buffer, and if that + // is the case, we clear it ahead of starting what we assume to be + // a new code: + if (codeBuffer.value.length < 13) { + codeBuffer.value = `${codeBuffer.value}${fragment}`; + } else { + // Reset the buffer: + codeBuffer.value = fragment; + } + + event.preventDefault(); + } else if (event.key === "Enter" && isValidIshCode(codeBuffer.value)) { + // If we have an active code buffer that seems valid, and the user presses Enter, + // we want to generate a new search query from this code, as long as it seems valid. + // + // Consumer/most(?) USB barcode scanners will terminate valid codes with an Enter key press, + // which is what we're reacting to here. + const validCode = codeBuffer.value; + + // Regardless of what we do next, we also clear the code buffer here: + codeBuffer.value = ""; + + console.log("VALID", validCode); + } else { + // Every other key press resets the buffer - this applies to non-code values, + // and also to pressing the Enter key without a valid code; + codeBuffer.value = ""; + } + } + + onMounted(() => { + window.addEventListener("keydown", onKeyDown); + }); + + onUnmounted(() => { + window.removeEventListener("keydown", onKeyDown); + }); + + return { + codeBuffer, + isActive, + }; +} diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 6ca37b4..771986e 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -93,12 +93,16 @@