use-scan-detection
Version:
A react hook for detecting barcode scanner input.
132 lines (120 loc) • 4.16 kB
text/typescript
import {
useCallback,
useEffect,
useRef
} from "react";
interface bufferCharacter {
time: number,
char: String
}
interface buffer {
current: Array<bufferCharacter>
}
interface config {
/** Time to wait from last character to then trigger an evaluation of the buffer. */
timeToEvaluate?: number,
/** Average time between characters in milliseconds. Used to determine if input is from keyboard or a scanner. Defaults to 50ms.*/
averageWaitTime?: number,
/** Character that barcode scanner prefixes input with.*/
startCharacter?: Array<number>,
/** Character that barcode scanner suffixes input with. Defaults to line return.*/
endCharacter?: Array<number>,
/** Callback to use on complete scan input.*/
onComplete: (code: String) => void,
/** Callback to use on error. */
onError?: (error: String) => void,
/** Minimum length a scanned code should be. Defaults to 0.*/
minLength?: number,
/** Ignore scan input if this node is focused.*/
ignoreIfFocusOn?: Node,
/** Stop propagation on keydown event. Defaults to false.*/
stopPropagation?: Boolean,
/** Prevent default on keydown event. Defaults to false.*/
preventDefault?: Boolean,
/** Bind keydown event to this node. Defaults to document.*/
container?: Node
}
/**
* Checks for scan input within a container and sends the output to a callback function.
* @param param0 Config object
*/
const useScanDetection = ({
timeToEvaluate = 100,
averageWaitTime = 50,
startCharacter = [],
endCharacter = [13, 27],
onComplete,
onError,
minLength = 1,
ignoreIfFocusOn,
stopPropagation = false,
preventDefault = false,
container = document
}: config) => {
const buffer: buffer = useRef([])
const timeout: any = useRef(false)
const clearBuffer = () => {
buffer.current = []
}
const evaluateBuffer = () => {
clearTimeout(timeout.current)
const sum = buffer.current
.map(({ time }, k, arr) => k > 0 ? time - arr[k - 1].time : 0)
.slice(1)
.reduce((total, delta) => total + delta, 0)
const avg = sum / (buffer.current.length - 1)
const code = buffer.current
.slice(startCharacter.length > 0 ? 1 : 0)
.map(({ char }) => char)
.join("")
if (
avg <= averageWaitTime
&& buffer.current.slice(startCharacter.length > 0 ? 1 : 0).length >= minLength
) {
onComplete(code)
} else {
avg <= averageWaitTime && !!onError && onError(code)
}
clearBuffer()
}
const onKeyDown: Function = useCallback((event: KeyboardEvent) => {
if (event.currentTarget !== ignoreIfFocusOn) {
if (endCharacter.includes(event.keyCode)) {
evaluateBuffer()
}
if (buffer.current.length > 0 || startCharacter.includes(event.keyCode) || startCharacter.length === 0) {
clearTimeout(timeout.current)
timeout.current = setTimeout(evaluateBuffer, timeToEvaluate)
buffer.current.push({ time: performance.now(), char: event.key })
}
}
if (stopPropagation) {
event.stopPropagation()
}
if (preventDefault) {
event.preventDefault()
}
}, [
startCharacter,
endCharacter,
timeToEvaluate,
onComplete,
onError,
minLength,
ignoreIfFocusOn,
stopPropagation,
preventDefault
])
useEffect(() => {
return () => {
clearTimeout(timeout.current)
}
}, [])
useEffect(() => {
container.addEventListener("keydown", (onKeyDown) as EventListener)
return () => {
container.removeEventListener("keydown", (onKeyDown) as EventListener)
}
}, [onKeyDown])
}
export default useScanDetection