@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 +99 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +189 -0
- package/package.json +31 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|