-
-
Save 14paxton/09adce350289bdcc1df92ed425c1d548 to your computer and use it in GitHub Desktop.
React Typescript Keyboard input reader
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ReactNode, useEffect } from 'react'; | |
import useBarcodeScanner from './useBarcodeScanner'; | |
import { NavigateFunction, useNavigate } from 'react-router-dom'; | |
import { ScanCacDetails } from './Scan'; | |
import { GuestAction } from '../../state/GuestContext'; | |
const isEmpty = (obj: object): boolean => Object.keys(obj).length === 0; | |
const BarCode = ({ children }: { children: ReactNode }) => { | |
const navigate: NavigateFunction = useNavigate(); | |
const [cacDetails, guestContextValues]: [ScanCacDetails, GuestAction] = useBarcodeScanner(); | |
const handleScannedDetails = function () { | |
if (!isEmpty(guestContextValues) && !cacDetails.scanError) { | |
console.debug('CAC Barcode Parsed, Navigate to stay page, GuestContextValues: ', guestContextValues); | |
navigate('stay'); | |
} | |
const handleUnmounting = function () { | |
console.debug('Barcode Component Mount/Un-Mount, guestContextValues: ', guestContextValues); | |
console.debug('Barcode Component Mount/Un-Mount, cacDetails: ', cacDetails); | |
}; | |
return handleUnmounting(); | |
}; | |
useEffect(handleScannedDetails, [cacDetails, guestContextValues, navigate]); | |
return children; | |
}; | |
export default BarCode; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ | |
import { buildNewScanEvent, ScanCacDetails, ScanEvent } from './Scan'; | |
import parse from '../commonAccessCard/Parse'; | |
export const defaultOptions: BarcodeScannerOptions = { | |
timeOut: 137, characterCount: 18, | |
}; | |
export function isShiftKey({ shiftKey, key }: KeyboardEvent): string { | |
const shift: boolean = shiftKey || 'Shift' === key; | |
if (shift) { | |
return key.replace('Shift', ''); | |
} else {return key;} | |
} | |
export type BarcodeScannerOptions = { | |
timeOut: number; characterCount: number; | |
}; | |
interface BarcodeEventMap { | |
scan: ScanEvent; | |
} | |
export interface BarcodeScanner extends EventTarget { | |
addEventListener<K extends keyof BarcodeEventMap>(type: K, listener: (this: BarcodeScanner, ev: BarcodeEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void; | |
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; | |
removeEventListener<K extends keyof BarcodeEventMap>(type: K, listener: (this: BarcodeScanner, ev: BarcodeEventMap[K]) => void, options?: boolean | EventListenerOptions): void; | |
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; | |
} | |
export class BarcodeScanner extends EventTarget { | |
capture: string; | |
target: EventTarget; | |
private timer: number; | |
private readonly timeOut: number; | |
private readonly characterCount: number; | |
private scanCacDetails: ScanCacDetails; | |
private scanReadComplete: boolean = false; | |
private _handleCompletedScan: (scanCacDetails: (ScanCacDetails | object)) => void = (scanCacDetails: ScanCacDetails | object): void => { | |
this.scanCacDetails = { ...this.scanCacDetails, ...scanCacDetails }; | |
this.dispatchScanEvent(); | |
}; | |
protected get handleCompletedScan(): (scanCacDetails: (ScanCacDetails | object)) => void { | |
return this._handleCompletedScan; | |
} | |
private _reset: () => void = (): void => { | |
console.debug('BarcodeScanner Reset Values. Ending Values: ', { | |
timer: this.timer, capture: this.capture, scanCacDetails: this.scanCacDetails, | |
}); | |
this.timer = performance.now(); | |
this.capture = ''; | |
this.scanCacDetails = <ScanCacDetails>{ scanError: false }; | |
this.scanReadComplete = false; | |
}; | |
protected get reset(): () => void { | |
return this._reset; | |
} | |
constructor(userDefinedOptions: BarcodeScannerOptions) { | |
super(); | |
const options: BarcodeScannerOptions = <BarcodeScannerOptions>{ ...defaultOptions, ...userDefinedOptions }; | |
this.timeOut = options.timeOut; | |
this.characterCount = options.characterCount; | |
this.scanCacDetails = <ScanCacDetails>{ scanError: false }; | |
this.timer = performance.now(); | |
this.capture = ''; | |
this.target = new EventTarget(); | |
window.addEventListener('keyup', this.keyup.bind(this)); | |
} | |
keyup(event: KeyboardEvent): void { | |
// Set current time | |
const now: number = performance.now(); | |
// If out timer is out, we need to reset because it was not a barcode | |
if (now - this.timer > this.timeOut) { | |
this._reset(); | |
} | |
// It seems we are still fast enough to be a barcode, so add to capture | |
const sinceFirst: number = now - this.timer; | |
if (sinceFirst < this.timeOut) { | |
this.capture += isShiftKey(event).toUpperCase(); | |
// It seems we managed to get enough characters within the time-out, send scan! | |
if (this.capture.length === this.characterCount && !this.scanReadComplete) { | |
console.debug('BarcodeScanner CAC Scanner Scanned Code: ', this.capture); | |
this.scanReadComplete = true; | |
parse(this.capture).then(this._handleCompletedScan); | |
} | |
} | |
} | |
dispatchScanEvent(): void { | |
console.debug('BarcodeScanner Dispatch Scan Event CAC Details: ', this.scanCacDetails); | |
const event: ScanEvent = buildNewScanEvent(this.scanCacDetails); | |
this.dispatchEvent(event); | |
this._reset(); | |
} | |
} | |
export default function buildNewBarcodeScanner(options?: BarcodeScannerOptions): BarcodeScanner { | |
return new BarcodeScanner(options || defaultOptions); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export enum CACMatchParts { | |
BarcodeVersion = 'barcodeVersion', | |
PersonalDesignatorIdentifier = 'personalDesignatorIdentifier', | |
PersonalDesignatorType = 'personalDesignatorType', | |
EDIPI = 'EDIPI', | |
FirstName = 'firstName', | |
LastName = 'lastName', | |
DateofBirth = 'dateofBirth', | |
PersonnelCategoryCode = 'personnelCategoryCode', | |
BranchCode = 'branchCode', | |
PersonnelEntitlementConditionType = 'personnelEntitlementConditionType', | |
Rank = 'rank', | |
PayPlanCode = 'payPlanCode', | |
PayPlanGradeCode = 'payPlanGradeCode', | |
CardIssueDate = 'cardIssueDate', | |
CardExpirationDate = 'cardExpirationDate', | |
CardInstanceIdentifier = 'cardInstanceIdentifier', | |
MiddleInitial = 'middleInitial', | |
Nationality = 'nationality', | |
} | |
type CACMatchNumberParts = | |
| CACMatchParts.PersonalDesignatorIdentifier | |
| CACMatchParts.EDIPI; | |
type CACMatchDateParts = | |
| CACMatchParts.DateofBirth | |
| CACMatchParts.CardIssueDate | |
| CACMatchParts.CardExpirationDate; | |
export type CACDetails = { | |
[key in CACMatchParts]: string; | |
} & { | |
[key in CACMatchNumberParts]: number; | |
} & { | |
[key in CACMatchDateParts]: Date; | |
} & { | |
personnelCategory: string; | |
branch: string; | |
branchID: string; | |
rankID: string; | |
}; | |
export interface CACResult { | |
cac: CACDetails; | |
match: RegExpMatchArray; | |
} | |
export enum PersonnelCategories { | |
'Active Duty', | |
'Retired', | |
'Foreign National', | |
'NAF', | |
'Reserve', | |
'Contractor', | |
'Guard', | |
'Other', | |
} | |
export enum PersonnelCategoryCodes { | |
A = PersonnelCategories['Active Duty'], | |
R = PersonnelCategories.Retired, | |
Y = PersonnelCategories.Retired, | |
U = PersonnelCategories['Foreign National'], | |
K = PersonnelCategories.NAF, | |
V = PersonnelCategories.Reserve, | |
S = PersonnelCategories.Reserve, | |
E = PersonnelCategories.Contractor, | |
O = PersonnelCategories.Contractor, | |
N = PersonnelCategories.Guard, | |
} | |
export type Branch = { | |
A: string; | |
C: string; | |
D: string; | |
F: string; | |
H: string; | |
M: string; | |
N: string; | |
0: string; | |
1: string; | |
2: string; | |
3: string; | |
4: string; | |
X: string; | |
}; | |
export const Branches: Branch = { | |
A: 'Army', | |
C: 'Coast Guard', | |
D: 'Civilian', | |
F: 'Air Force', | |
H: 'Public Health Services', | |
M: 'USMC', | |
N: 'Navy', | |
0: 'National Oceanic and Atmospheric Administration', | |
1: 'Foreign Army', | |
2: 'Foreign Navy', | |
3: 'Foreign Marine Corps', | |
4: 'Foreign Air Force', | |
X: 'Other', | |
}; | |
export enum ScanType { | |
cac = 'cac', | |
cac39 = 'cac39', | |
loa = 'loa', | |
} | |
export const CACEpoch = new Date(Date.parse('1/1/1000')); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enum GuestActionType { | |
CLEAR = 'CLEAR', | |
MANUAL_ENTRY = 'MANUAL_ENTRY', | |
BARCODE_SCAN = 'BARCODE_SCAN', | |
} | |
export default GuestActionType; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Dispatch, ReactNode, useReducer } from 'react'; | |
export type GuestState = { | |
dodId?: string; lastName?: string; | |
}; | |
export type GuestContextValues = { | |
state: GuestState; dispatch: Dispatch<GuestAction>; | |
}; | |
export const GuestContext = React.createContext<GuestContextValues | undefined>(undefined); | |
export type GuestAction = { | |
type: 'CLEAR' | 'MANUAL_ENTRY' | 'BARCODE_SCAN'; payload?: { | |
dodId: string; lastName?: string; | |
}; | |
}; | |
const reducer = (state: GuestState, action: GuestAction) => { | |
switch (action.type) { | |
case 'CLEAR': | |
return {}; | |
case 'MANUAL_ENTRY': | |
return { | |
dodId: action.payload?.dodId, lastName: action.payload?.lastName, | |
}; | |
case 'BARCODE_SCAN': | |
return { | |
dodId: action.payload?.dodId, lastName: undefined, | |
}; | |
default: | |
return state; | |
} | |
}; | |
export const GuestContextProvider = ({ children }: { children: ReactNode }) => { | |
const [state, dispatch] = useReducer(reducer, {}); | |
return (<GuestContext.Provider value={{ state, dispatch }}> | |
{children} | |
</GuestContext.Provider>); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Dispatch } from 'react'; | |
export type GuestState = { | |
dodId?: string; | |
lastName?: string; | |
}; | |
export type GuestAction = { | |
type: 'CLEAR' | 'MANUAL_ENTRY' | 'BARCODE_SCAN'; | |
payload?: { | |
dodId: string; | |
lastName?: string; | |
}; | |
}; | |
export type GuestContextValues = { | |
state: GuestState; | |
dispatch: Dispatch<GuestAction>; | |
}; | |
export const reducer = (state: GuestState, action: GuestAction) => { | |
switch (action.type) { | |
case 'CLEAR': | |
return {}; | |
case 'MANUAL_ENTRY': | |
return { | |
dodId: action.payload?.dodId, | |
lastName: action.payload?.lastName, | |
}; | |
case 'BARCODE_SCAN': | |
return { | |
dodId: action.payload?.dodId, | |
lastName: undefined, | |
}; | |
default: | |
return state; | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Branch, Branches, CACDetails, PersonnelCategories, PersonnelCategoryCodes, ScanType } from './CacCommon'; | |
import { ScanCacDetails } from '../barcodeScanner/Scan'; | |
import GuestActionType from '../enum/GuestActionType'; | |
enum CACMatchParts { | |
BarcodeVersion = 'barcodeVersion', | |
PersonalDesignatorIdentifier = 'personalDesignatorIdentifier', | |
PersonalDesignatorType = 'personalDesignatorType', | |
EDIPI = 'EDIPI', | |
PersonnelCategoryCode = 'personnelCategoryCode', | |
BranchCode = 'branchCode', | |
CardInstanceIdentifier = 'cardInstanceIdentifier', | |
} | |
const CAC39Match: RegExp = /([1N])([A-Z0-9]{6})([A-Z0-9])([A-Z0-9]{7})(.)(.)([A-Z0-9])/; | |
const CAC39MatchPartIndexes: CACMatchParts[] = Object.values<CACMatchParts>(CACMatchParts); | |
function getTypedKeys<T extends object>(o: T): (keyof T)[] { | |
return Object.keys(o) as (keyof T)[]; | |
} | |
function getBranch(key: PropertyKey): string | undefined { | |
function getBranchByKey(k: keyof Branch): boolean { return k === key; } | |
const typedKey: keyof Branch | undefined = getTypedKeys(Branches).find(getBranchByKey); | |
if (typedKey) { | |
return Branches[typedKey]; | |
} else {return undefined;} | |
} | |
interface CACPart { | |
part: string; | |
index: number; | |
indexes: CACMatchParts[]; | |
} | |
export const parseCACPart = ({ | |
part, index, indexes, | |
}: CACPart): Partial<CACDetails> => { | |
const key: CACMatchParts = indexes[index]; | |
switch (key) { | |
case CACMatchParts.PersonalDesignatorIdentifier: | |
case CACMatchParts.EDIPI: | |
return { | |
[key]: parseInt(part, 32), | |
}; | |
case CACMatchParts.PersonnelCategoryCode: | |
return { | |
personnelCategory: PersonnelCategories[PersonnelCategoryCodes[part as keyof typeof PersonnelCategoryCodes]], [key]: part, | |
}; | |
case CACMatchParts.BranchCode: | |
return { | |
branch: getBranch(part), [key]: part, | |
}; | |
default: | |
return { | |
[key]: part.trimEnd(), | |
}; | |
} | |
}; | |
function buildCacDetails(parsed: ScanCacDetails, part: string, index: number) { | |
return { | |
...parsed, ...parseCACPart({ | |
part: part, index: index, indexes: CAC39MatchPartIndexes, | |
}), | |
}; | |
} | |
export default async function parse(input: string): Promise<ScanCacDetails | object> { | |
const match: RegExpExecArray | null = CAC39Match.exec(input); | |
const cacDetailsObject: ScanCacDetails = <ScanCacDetails>{ | |
scanType: GuestActionType.BARCODE_SCAN, cacScanType: ScanType.cac39, | |
}; | |
let returnPromise: Promise<ScanCacDetails | object> = Promise.resolve({ | |
scanError: true, | |
}); | |
if (match) { | |
returnPromise = Promise.resolve(match | |
.slice(1) | |
.reduce(buildCacDetails, cacDetailsObject)); | |
} | |
return returnPromise; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { CACDetails, ScanType } from '../commonAccessCard/CacCommon'; | |
import GuestActionType from '../enum/GuestActionType'; | |
export interface ScanCacDetails extends CACDetails { | |
//Property EDIPI from CACDetails is DOD ID | |
scanType: GuestActionType.BARCODE_SCAN; | |
cacScanType: ScanType.cac39; | |
scanError: boolean; | |
} | |
export class ScanEvent extends CustomEvent<ScanCacDetails> { | |
constructor(detail: ScanCacDetails) { | |
super('scan', { detail }); | |
} | |
} | |
export function buildNewScanEvent(detail: ScanCacDetails): ScanEvent { | |
return new ScanEvent(detail); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; | |
import { ScanCacDetails, ScanEvent } from './Scan'; | |
import buildNewBarcodeScanner, { BarcodeScanner } from './BarcodeScanner'; | |
import { GuestAction, GuestContext, GuestContextValues } from '../../state/GuestContext'; | |
import GuestActionType from '../enum/GuestActionType'; | |
export default function useBarcodeScanner(): [ScanCacDetails, GuestAction] { | |
const barcodeScanner: BarcodeScanner = useMemo(buildNewBarcodeScanner, []); | |
const { state: guestAction, dispatch }: GuestContextValues = useContext(GuestContext) as GuestContextValues; | |
const [cacDetails, setCacDetails]: [ScanCacDetails, (value: (((prevState: ScanCacDetails) => ScanCacDetails) | ScanCacDetails)) => void] = useState<ScanCacDetails>(<ScanCacDetails>{ scanError: false }); | |
const setDetailsAndGuestContext = (event: ScanEvent): void => { | |
const { detail: details }: ScanEvent = event; | |
setCacDetails(details); | |
dispatch({ | |
type: GuestActionType.BARCODE_SCAN, payload: { | |
dodId: details.EDIPI, lastName: details?.lastName, | |
}, | |
} as GuestAction); | |
}; | |
const handleEvent = useCallback(setDetailsAndGuestContext, [dispatch]); | |
const setAndRemoveEventListener = (): () => void => { | |
let createScanEventListener: boolean = true; | |
const removeEventListener = (): void => { | |
barcodeScanner.removeEventListener('scan', handleEvent); | |
createScanEventListener = false; | |
}; | |
if (barcodeScanner && createScanEventListener) { | |
barcodeScanner.addEventListener('scan', handleEvent); | |
} | |
return removeEventListener; | |
}; | |
useEffect(setAndRemoveEventListener, [barcodeScanner, handleEvent]); | |
return [<ScanCacDetails>cacDetails, <GuestAction>guestAction]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment