mirror of
https://github.com/hay-kot/homebox.git
synced 2024-12-18 21:16:31 +00:00
Add basic support for hidden barcode input
This commit is contained in:
parent
f085a58242
commit
c9e53c37f5
4 changed files with 132 additions and 0 deletions
119
frontend/composables/use-quick-search.ts
Normal file
119
frontend/composables/use-quick-search.ts
Normal file
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -93,12 +93,16 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useLabelStore } from "~~/stores/labels";
|
import { useLabelStore } from "~~/stores/labels";
|
||||||
import { useLocationStore } from "~~/stores/locations";
|
import { useLocationStore } from "~~/stores/locations";
|
||||||
|
import { useQuickSearch } from "~~/composables/use-quick-search";
|
||||||
|
|
||||||
const username = computed(() => authCtx.user?.name || "User");
|
const username = computed(() => authCtx.user?.name || "User");
|
||||||
|
|
||||||
// Preload currency format
|
// Preload currency format
|
||||||
useFormatCurrency();
|
useFormatCurrency();
|
||||||
|
|
||||||
|
// Enable global quick search:
|
||||||
|
useQuickSearch();
|
||||||
|
|
||||||
const modals = reactive({
|
const modals = reactive({
|
||||||
item: false,
|
item: false,
|
||||||
location: false,
|
location: false,
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
"autoprefixer": "^10.4.8",
|
"autoprefixer": "^10.4.8",
|
||||||
"daisyui": "^2.24.0",
|
"daisyui": "^2.24.0",
|
||||||
"dompurify": "^3.0.0",
|
"dompurify": "^3.0.0",
|
||||||
|
"gtin": "^1.0.2",
|
||||||
"h3": "^1.7.1",
|
"h3": "^1.7.1",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
|
|
|
@ -41,6 +41,9 @@ dependencies:
|
||||||
dompurify:
|
dompurify:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
|
gtin:
|
||||||
|
specifier: ^1.0.2
|
||||||
|
version: 1.0.2
|
||||||
h3:
|
h3:
|
||||||
specifier: ^1.7.1
|
specifier: ^1.7.1
|
||||||
version: 1.7.1
|
version: 1.7.1
|
||||||
|
@ -5419,6 +5422,11 @@ packages:
|
||||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/gtin@1.0.2:
|
||||||
|
resolution: {integrity: sha512-jEsHMz16c3yz0rlM4TvUUU0022FTniIAcBfCDoch+38RJC32yGkdKFC9ixpBqPskYpCRrb614AjF8O0QQP0gPg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/gzip-size@7.0.0:
|
/gzip-size@7.0.0:
|
||||||
resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==}
|
resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
Loading…
Reference in a new issue