@4efficiency/react-barcode-reader 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ ## Introduction
2
+ A [React](https://facebook.github.io/react/) component for reading barcode and QR codes from devices that are represent as keyboard to the system.
3
+
4
+ This is a conversion to typescript fork of https://kybarg.github.io/react-barcode-reader/
5
+
6
+
7
+ ## Demo
8
+ to come maybe
9
+
10
+ ## Install
11
+ `npm install --save @4efficiency/react-barcode-reader`
12
+
13
+ ## Example
14
+
15
+ ```js
16
+ import React, {useState} from 'react'
17
+ import BarcodeReader from '@4efficiency/react-barcode-reader'
18
+
19
+ const Test = ()=> {
20
+ const [result,setResult] = useState<string>("No result")
21
+
22
+ const handleScan = (data)=>{
23
+ setResult(data)
24
+ }
25
+ const handleError = (err) =>{
26
+ console.error(err)
27
+ }
28
+
29
+
30
+ return(
31
+ <div>
32
+ <BarcodeReader
33
+ onError={handleError}
34
+ onScan={handleScan}
35
+ />
36
+ <p>{this.state.result}</p>
37
+ </div>
38
+ )
39
+
40
+ }
41
+ ```
42
+
43
+ ## Props
44
+ | Prop | Type | Default Value | Description |
45
+ |---|---|---|---|
46
+ | onScan | func | | Callback after detection of a successfull scanning (scanned string in parameter) |
47
+ | onError | func | | Callback after detection of a unsuccessfull scanning (scanned string in parameter) |
48
+ | onReceive | func | | Callback after receiving and processing a char (scanned char in parameter)
49
+ | onKeyDetect | func | | Callback after detecting a keyDown (key char in parameter) - in contrast to onReceive, this fires for non-character keys like tab, arrows, etc. too!
50
+ | timeBeforeScanTest | number | 100 | Wait duration (ms) after keypress event to check if scanning is finished
51
+ | avgTimeByChar | number | 30 | Average time (ms) between 2 chars. Used to do difference between keyboard typing and scanning
52
+ | minLength | number | 6 | Minimum length for a scanning
53
+ | endChar | [number] | [9, 13] | Chars to remove and means end of scanning
54
+ | startChar | [number] | [] | Chars to remove and means start of scanning
55
+ | scanButtonKeyCode | number | | Key code of the scanner hardware button (if the scanner button a acts as a key itself)
56
+ | scanButtonLongPressThreshold | number | 3 | How many times the hardware button should issue a pressed event before a barcode is read to detect a longpress
57
+ | onScanButtonLongPressed | func | | Callback after detection of a successfull scan while the scan button was pressed and held down
58
+ | stopPropagation | bool | false | Stop immediate propagation on keypress event
59
+ | preventDefault | bool | false | Prevent default action on keypress event
60
+ | testCode | string | | Test string for simulating
61
+ | splitPattern | RegExp | | Optional regexp to split scans into multiple events
62
+
63
+ ## Dev
64
+
65
+ ### Install dependencies
66
+ `npm install`
67
+
68
+ ### Build
69
+ `npm run build`
70
+
71
+
72
+ ### Test
73
+ `npm test`
74
+
75
+ ### Linting
76
+ `npm run lint`
77
+
78
+ ## License
79
+ The MIT License (MIT)
80
+
81
+ Copyright (c) 2025 4Efficiency Services B.V.
82
+
83
+ Permission is hereby granted, free of charge, to any person obtaining a copy
84
+ of this software and associated documentation files (the "Software"), to deal
85
+ in the Software without restriction, including without limitation the rights
86
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
87
+ copies of the Software, and to permit persons to whom the Software is
88
+ furnished to do so, subject to the following conditions:
89
+
90
+ The above copyright notice and this permission notice shall be included in all
91
+ copies or substantial portions of the Software.
92
+
93
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
94
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
95
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
96
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
97
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
98
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
99
+ SOFTWARE.
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Props interface for the BarcodeScanner component.
3
+ * Configures barcode/QR code scanning behavior from keyboard input devices.
4
+ */
5
+ export interface BarcodeScannerProps {
6
+ /**
7
+ * Callback invoked after successful barcode detection.
8
+ * @param code - The scanned barcode string
9
+ * @param scanButtonCounter - Number of times the scan button was pressed
10
+ */
11
+ onScan?: (code: string, scanButtonCounter: number) => void;
12
+ /**
13
+ * Callback invoked after unsuccessful barcode detection.
14
+ * @param code - The attempted scan string
15
+ * @param error - Error message describing why the scan failed
16
+ */
17
+ onError?: (code: string, error: string) => void;
18
+ /**
19
+ * Callback invoked after receiving and processing each character.
20
+ * @param e - The keyboard event for the processed character
21
+ */
22
+ onReceive?: (e: KeyboardEvent) => void;
23
+ /**
24
+ * Callback invoked after detecting any keydown event.
25
+ * Unlike onReceive, this fires for non-character keys like Tab, Enter, arrows, etc.
26
+ * @param e - The keyboard event that was detected
27
+ */
28
+ onKeyDetect?: (e: KeyboardEvent) => void;
29
+ /**
30
+ * Wait duration in milliseconds after the last keypress before testing if scanning is complete.
31
+ * @default 100
32
+ */
33
+ timeBeforeScanTest?: number;
34
+ /**
35
+ * Average time in milliseconds between two characters during a scan.
36
+ * Used to differentiate between manual keyboard typing and barcode scanner input.
37
+ * @default 30
38
+ */
39
+ avgTimeByChar?: number;
40
+ /**
41
+ * Minimum number of characters required for a valid scan.
42
+ * @default 6
43
+ */
44
+ minLength?: number;
45
+ /**
46
+ * Array of character codes that indicate the end of a scan and should be removed from the result.
47
+ * @default [9, 13] (Tab and Enter)
48
+ */
49
+ endChar?: number[];
50
+ /**
51
+ * Array of character codes that indicate the start of a scan and should be removed from the result.
52
+ * @default []
53
+ */
54
+ startChar?: number[];
55
+ /**
56
+ * Key code of the scanner hardware button if the scanner button acts as a key itself.
57
+ */
58
+ scanButtonKeyCode?: number;
59
+ /**
60
+ * Number of times the hardware scan button must be pressed before considering it a long press.
61
+ * @default 3
62
+ */
63
+ scanButtonLongPressThreshold?: number;
64
+ /**
65
+ * Callback invoked after successful scan detection when the scan button was held down (long press).
66
+ * @param code - The scanned barcode string
67
+ * @param scanButtonCounter - Number of times the scan button was pressed
68
+ */
69
+ onScanButtonLongPressed?: (code: string, scanButtonCounter: number) => void;
70
+ /**
71
+ * If true, stops immediate propagation of keypress events.
72
+ * @default false
73
+ */
74
+ stopPropagation?: boolean;
75
+ /**
76
+ * If true, prevents default action on keypress events.
77
+ * @default false
78
+ */
79
+ preventDefault?: boolean;
80
+ /**
81
+ * Test string for simulating a barcode scan without physical scanner input.
82
+ */
83
+ testCode?: string;
84
+ /**
85
+ * Optional regular expression pattern to split concatenated scans into multiple scan events.
86
+ * When provided, matched groups will trigger separate onScan callbacks.
87
+ */
88
+ splitPattern?: RegExp;
89
+ }
90
+ declare function BarcodeScanner(props: BarcodeScannerProps): null;
91
+ export default BarcodeScanner;
package/dist/index.js ADDED
@@ -0,0 +1,189 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ function isContentEditable(element) {
3
+ const anyEl = element;
4
+ if (typeof anyEl.getAttribute !== "function") {
5
+ return false;
6
+ }
7
+ return !!anyEl.getAttribute("contenteditable");
8
+ }
9
+ function isInput(element) {
10
+ if (!element)
11
+ return false;
12
+ const el = element;
13
+ const { tagName } = el;
14
+ const editable = isContentEditable(el);
15
+ return tagName === "INPUT" || tagName === "TEXTAREA" || editable;
16
+ }
17
+ function inIframe() {
18
+ try {
19
+ return window.self !== window.top;
20
+ }
21
+ catch {
22
+ return true;
23
+ }
24
+ }
25
+ function BarcodeScanner(props) {
26
+ // Mutable refs for internal state previously kept as fields
27
+ const firstCharTimeRef = useRef(0);
28
+ const lastCharTimeRef = useRef(0);
29
+ const stringWritingRef = useRef("");
30
+ const callIsScannerRef = useRef(false);
31
+ const testTimerRef = useRef(false);
32
+ const scanButtonCounterRef = useRef(0);
33
+ // Defaults equivalent to previous defaultProps
34
+ const { onScan, onError, onReceive, onKeyDetect, timeBeforeScanTest = 100, avgTimeByChar = 30, minLength = 6, endChar = [9, 13], startChar = [], scanButtonKeyCode, scanButtonLongPressThreshold = 3, onScanButtonLongPressed, stopPropagation = false, preventDefault = false, testCode, splitPattern, } = props;
35
+ const initScannerDetection = () => {
36
+ // Reset all state between scans to avoid cross-scan interference
37
+ firstCharTimeRef.current = 0;
38
+ lastCharTimeRef.current = 0;
39
+ stringWritingRef.current = "";
40
+ scanButtonCounterRef.current = 0;
41
+ callIsScannerRef.current = false;
42
+ if (testTimerRef.current) {
43
+ clearTimeout(testTimerRef.current);
44
+ testTimerRef.current = false;
45
+ }
46
+ };
47
+ const emitScans = useCallback((payload) => {
48
+ if (!onScan)
49
+ return;
50
+ if (splitPattern) {
51
+ const matches = payload.match(splitPattern);
52
+ if (matches && matches.length > 0) {
53
+ for (const m of matches) {
54
+ onScan(m, scanButtonCounterRef.current || 1);
55
+ }
56
+ return;
57
+ }
58
+ }
59
+ onScan(payload, scanButtonCounterRef.current || 1);
60
+ }, [onScan, splitPattern]);
61
+ const scannerDetectionTest = useCallback((s) => {
62
+ // If string is given, test it
63
+ if (s) {
64
+ firstCharTimeRef.current = 0;
65
+ lastCharTimeRef.current = 0;
66
+ stringWritingRef.current = s;
67
+ }
68
+ if (!scanButtonCounterRef.current) {
69
+ scanButtonCounterRef.current = 1;
70
+ }
71
+ if (stringWritingRef.current.length >= minLength &&
72
+ lastCharTimeRef.current - firstCharTimeRef.current < stringWritingRef.current.length * avgTimeByChar) {
73
+ if (onScanButtonLongPressed && scanButtonCounterRef.current > scanButtonLongPressThreshold) {
74
+ onScanButtonLongPressed(stringWritingRef.current, scanButtonCounterRef.current);
75
+ }
76
+ else {
77
+ emitScans(stringWritingRef.current);
78
+ }
79
+ initScannerDetection();
80
+ return true;
81
+ }
82
+ let errorMsg = "";
83
+ if (stringWritingRef.current.length < minLength) {
84
+ errorMsg = `String length should be greater or equal ${minLength}`;
85
+ }
86
+ else if (lastCharTimeRef.current - firstCharTimeRef.current > stringWritingRef.current.length * avgTimeByChar) {
87
+ errorMsg = `Average key character time should be less or equal ${avgTimeByChar}ms`;
88
+ }
89
+ if (onError)
90
+ onError(stringWritingRef.current, errorMsg);
91
+ initScannerDetection();
92
+ return false;
93
+ }, [avgTimeByChar, emitScans, minLength, onError, onScanButtonLongPressed, scanButtonLongPressThreshold]);
94
+ const handleKey = useCallback((e) => {
95
+ const target = e.target;
96
+ if (target instanceof window.HTMLElement && isInput(target)) {
97
+ return;
98
+ }
99
+ // Prefer modern KeyboardEvent.key but keep compatibility with keyCode/which
100
+ const key = (e.key || "");
101
+ const code = typeof e.which !== "undefined" ? e.which : typeof e.keyCode !== "undefined" ? e.keyCode : undefined;
102
+ const isEnd = (code !== undefined && endChar.indexOf(code) !== -1) || key === "Enter" || key === "Tab";
103
+ // If it's just the button of the scanner, ignore it and wait for the real input
104
+ if (scanButtonKeyCode && code === scanButtonKeyCode) {
105
+ scanButtonCounterRef.current += 1;
106
+ if (preventDefault)
107
+ e.preventDefault();
108
+ if (stopPropagation)
109
+ e.stopImmediatePropagation?.();
110
+ }
111
+ // Fire keyDetect event in any case!
112
+ if (onKeyDetect)
113
+ onKeyDetect(e);
114
+ // For end characters, prevent default and stop propagation to avoid focus shifts
115
+ if (isEnd && firstCharTimeRef.current) {
116
+ e.preventDefault();
117
+ e.stopImmediatePropagation?.();
118
+ }
119
+ else {
120
+ // Only apply stopPropagation/preventDefault for non-end characters if configured
121
+ if (stopPropagation)
122
+ e.stopImmediatePropagation?.();
123
+ if (preventDefault)
124
+ e.preventDefault();
125
+ }
126
+ const isStart = code !== undefined && startChar.indexOf(code) !== -1;
127
+ if (firstCharTimeRef.current && isEnd) {
128
+ callIsScannerRef.current = true;
129
+ }
130
+ else if (!firstCharTimeRef.current && isStart) {
131
+ callIsScannerRef.current = false;
132
+ }
133
+ else {
134
+ // Append printable chars only
135
+ if (typeof code !== "undefined" && key.length === 1) {
136
+ stringWritingRef.current += key;
137
+ }
138
+ callIsScannerRef.current = false;
139
+ }
140
+ if (!firstCharTimeRef.current) {
141
+ firstCharTimeRef.current = Date.now();
142
+ }
143
+ lastCharTimeRef.current = Date.now();
144
+ if (testTimerRef.current)
145
+ clearTimeout(testTimerRef.current);
146
+ if (callIsScannerRef.current) {
147
+ scannerDetectionTest();
148
+ testTimerRef.current = false;
149
+ }
150
+ else {
151
+ testTimerRef.current = setTimeout(scannerDetectionTest, timeBeforeScanTest);
152
+ }
153
+ if (onReceive)
154
+ onReceive(e);
155
+ }, [
156
+ endChar,
157
+ onKeyDetect,
158
+ onReceive,
159
+ preventDefault,
160
+ scanButtonKeyCode,
161
+ scannerDetectionTest,
162
+ startChar,
163
+ stopPropagation,
164
+ timeBeforeScanTest,
165
+ ]);
166
+ // Register and clean up listeners
167
+ useEffect(() => {
168
+ const listener = handleKey;
169
+ // Use keydown (reliable for Enter/Tab)
170
+ if (inIframe())
171
+ window.parent.document.addEventListener("keydown", listener);
172
+ window.document.addEventListener("keydown", listener);
173
+ return () => {
174
+ if (inIframe())
175
+ window.parent.document.removeEventListener("keydown", listener);
176
+ window.document.removeEventListener("keydown", listener);
177
+ if (testTimerRef.current) {
178
+ clearTimeout(testTimerRef.current);
179
+ }
180
+ };
181
+ }, [handleKey]);
182
+ // Mimic the previous render side effect for testCode
183
+ useEffect(() => {
184
+ if (testCode)
185
+ scannerDetectionTest(testCode);
186
+ }, [testCode, scannerDetectionTest]);
187
+ return null;
188
+ }
189
+ export default BarcodeScanner;
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@4efficiency/react-barcode-reader",
3
+ "version": "1.0.0",
4
+ "description": "A react component for reading barcodes and Qr codes from keybord input devices.",
5
+ "main": "dist/index.js",
6
+ "module":"dist/index.js",
7
+ "types":"dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "peerDependencies": {
16
+ "react": ">=19.2.0"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://4ef.visualstudio.com/4EF%20Shared%20Libraries/_git/4ef-react-barcode-reader"
21
+ },
22
+ "keywords": [],
23
+ "author": "",
24
+ "license": "ISC",
25
+ "type": "module",
26
+ "devDependencies": {
27
+ "@types/react": "^19.2.7",
28
+ "@types/react-dom": "^19.2.3",
29
+ "typescript": "^5.9.3"
30
+ }
31
+ }