barcode-scan-js
Version:
JavaScript browser-based scanner keyboard input library for seamless integration of barcode and QR code scanning into web applications
141 lines (125 loc) • 4.34 kB
text/typescript
type BarcodeScanConfiguration = {
codeRegex?: RegExp | null;
minLength?: number;
maxLength?: number;
scanEndsWithKey?: string;
scanTimeoutMs?: number;
ignoreOverElements?: string[];
};
type ScanDetected = {
code: string;
cleanedCode: string;
length: number;
isValid: boolean;
timeTakenMs: number;
config?: BarcodeScanConfiguration
errorMessage?: string
};
class BarcodeScanElement extends HTMLElement {
private config: BarcodeScanConfiguration = {
codeRegex: null,
minLength: 1,
maxLength: 14,
scanEndsWithKey: "",
scanTimeoutMs: 3000,
ignoreOverElements: ["INPUT"]
};
private inputKeys:string[] = [];
private startTime: number | null = null;
private timeout: any | null = null;
constructor() {
super();
}
connectedCallback() {
this.setupConfig();
document.addEventListener("keydown", this.onKeyDown.bind(this));
}
disconnectedCallback() {
document.removeEventListener("keydown", this.onKeyDown.bind(this));
}
private setupConfig() {
const inputConfig = this.getAttribute("config");
if (inputConfig) {
try {
this.config = Object.assign(this.config, JSON.parse(inputConfig));
this.config.codeRegex = this.config.codeRegex ?? new RegExp(
`^${'(.'}${this.config.minLength!},${this.config.maxLength! - this.config.scanEndsWithKey!.length}${')'}
${this.config.scanEndsWithKey}$`,
'g'
);
} catch (error) {
console.error("Invalid configuration:", error);
}
}
}
private escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
private onKeyDown(ev: KeyboardEvent) {
if (
this.config.ignoreOverElements?.includes(
(ev.target as HTMLElement).tagName
)
) {
return;
}
if (this.startTime === null) {
this.startTime = performance.now();
}
this.inputKeys.push(ev.key);
if (ev.key === this.config.scanEndsWithKey) {
this.validateInput();
}
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => {
this.validateInput();
}, this.config.scanTimeoutMs || 1000);
}
private validateInput() {
if (!this.startTime) return;
const endTime = performance.now();
const timeTakenMs = Math.round(endTime - this.startTime);
const cleanedCode = this.inputKeys.filter(Boolean).filter(f => f != 'Shift' && f != 'Enter' && f != 'NumLock' && f != 'ArrowDown').join('');
let errorMessage = '';
switch (true) {
case cleanedCode.length < this.config.minLength!:
errorMessage = `${cleanedCode} length is < ${this.config.minLength}`
case cleanedCode.length > this.config.maxLength!:
errorMessage = `${cleanedCode} length is > ${this.config.maxLength}`
case this.config.codeRegex!.test(cleanedCode):
errorMessage = `${cleanedCode} doesn't pass regex ${this.config.codeRegex}`
}
if (!errorMessage) {
this.dispatchEvent(
new CustomEvent<ScanDetected>("scan", {
detail: {
code: cleanedCode,
length,
isValid: true,
timeTakenMs,
cleanedCode,
},
})
);
} else {
this.dispatchEvent(
new CustomEvent<ScanDetected>("scan", {
detail: {
code: cleanedCode,
length,
isValid: false,
config: this.config,
timeTakenMs,
cleanedCode,
errorMessage: errorMessage
},
})
);
}
this.inputKeys = [];
this.startTime = null;
}
}
customElements.define("barcode-scan", BarcodeScanElement);