/* * onScan.js - scan-events for hardware barcodes scanners in javascript */ ;(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory()) : global.onScan = factory() }(this, (function () { var onScan = { /** * * @param DomElement oDomElement * @param Object oOptions * @return self */ attachTo: function(oDomElement, oOptions) { if(oDomElement.scannerDetectionData !== undefined){ throw new Error("onScan.js is already initialized for DOM element " + oDomElement); } var oDefaults = { onScan: function(sScanned, iQty){}, // Callback after detection of a successful scanning: function(){sScancode, iCount)}() onScanError: function(oDebug){}, // Callback after detection of an unsuccessful scanning (scanned string in parameter) onKeyProcess: function(sChar, oEvent){}, // Callback after receiving and processing a char (scanned char in parameter) onKeyDetect: function(iKeyCode, oEvent){}, // Callback after detecting a keyDown (key char in parameter) - in contrast to onKeyProcess, this fires for non-character keys like tab, arrows, etc. too! onPaste: function(sPasted, oEvent){}, // Callback after receiving a value on paste, no matter if it is a valid code or not keyCodeMapper: function(oEvent) {return onScan.decodeKeyEvent(oEvent)}, // Custom function to decode a keydown event into a character. Must return decoded character or NULL if the given event should not be processed. onScanButtonLongPress: function(){}, // Callback after detection of a successful scan while the scan button was pressed and held down scanButtonKeyCode:false, // Key code of the scanner hardware button (if the scanner button acts as a key itself) scanButtonLongPressTime:500, // How long (ms) the hardware button should be pressed, until a callback gets executed timeBeforeScanTest:100, // Wait duration (ms) after keypress event to check if scanning is finished avgTimeByChar:30, // Average time (ms) between 2 chars. Used to differentiate between keyboard typing and scanning minLength:6, // Minimum length for a scanning suffixKeyCodes:[9,13], // Chars to remove and means end of scanning prefixKeyCodes:[], // Chars to remove and means start of scanning ignoreIfFocusOn:false, // do not handle scans if the currently focused element matches this selector or object stopPropagation:false, // Stop immediate propagation on keypress event preventDefault:false, // Prevent default action on keypress event captureEvents:false, // Get the events before any listeners deeper in the DOM reactToKeydown:true, // look for scan input in keyboard events reactToPaste:false, // look for scan input in paste events singleScanQty: 1, // Quantity of Items put out to onScan in a single scan } oOptions = this._mergeOptions(oDefaults, oOptions); // initializing options and variables on DomElement oDomElement.scannerDetectionData = { options: oOptions, vars:{ firstCharTime: 0, lastCharTime: 0, accumulatedString: '', testTimer: false, longPressTimeStart: 0, longPressed: false } }; // initializing handlers (based on settings) if (oOptions.reactToPaste === true){ oDomElement.addEventListener("paste", this._handlePaste, oOptions.captureEvents); } if (oOptions.scanButtonKeyCode !== false){ oDomElement.addEventListener("keyup", this._handleKeyUp, oOptions.captureEvents); } if (oOptions.reactToKeydown === true || oOptions.scanButtonKeyCode !== false){ oDomElement.addEventListener("keydown", this._handleKeyDown, oOptions.captureEvents); } return this; }, /** * * @param DomElement oDomElement * @return void */ detachFrom: function(oDomElement) { // detaching all used events if (oDomElement.scannerDetectionData.options.reactToPaste){ oDomElement.removeEventListener("paste", this._handlePaste); } if (oDomElement.scannerDetectionData.options.scanButtonKeyCode !== false){ oDomElement.removeEventListener("keyup", this._handleKeyUp); } oDomElement.removeEventListener("keydown", this._handleKeyDown); // clearing data off DomElement oDomElement.scannerDetectionData = undefined; return; }, /** * * @param DomElement oDomElement * @return Object */ getOptions: function(oDomElement){ return oDomElement.scannerDetectionData.options; }, /** * * @param DomElement oDomElement * @param Object oOptions * @return self */ setOptions: function(oDomElement, oOptions){ // check if some handlers need to be changed based on possible option changes switch (oDomElement.scannerDetectionData.options.reactToPaste){ case true: if (oOptions.reactToPaste === false){ oDomElement.removeEventListener("paste", this._handlePaste); } break; case false: if (oOptions.reactToPaste === true){ oDomElement.addEventListener("paste", this._handlePaste); } break; } switch (oDomElement.scannerDetectionData.options.scanButtonKeyCode){ case false: if (oOptions.scanButtonKeyCode !== false){ oDomElement.addEventListener("keyup", this._handleKeyUp); } break; default: if (oOptions.scanButtonKeyCode === false){ oDomElement.removeEventListener("keyup", this._handleKeyUp); } break; } // merge old and new options oDomElement.scannerDetectionData.options = this._mergeOptions(oDomElement.scannerDetectionData.options, oOptions); // reinitiallize this._reinitialize(oDomElement); return this; }, /** * Transforms key codes into characters. * * By default, only the follwing key codes are taken into account * - 48-90 (letters and regular numbers) * - 96-105 (numeric keypad numbers) * - 106-111 (numeric keypad operations) * * All other keys will yield empty strings! * * The above keycodes will be decoded using the KeyboardEvent.key property on modern * browsers. On older browsers the method will fall back to String.fromCharCode() * putting the result to upper/lower case depending on KeyboardEvent.shiftKey if * it is set. * * @param KeyboardEvent oEvent * @return string */ decodeKeyEvent : function (oEvent) { var iCode = this._getNormalizedKeyNum(oEvent); switch (true) { case iCode >= 48 && iCode <= 90: // numbers and letters case iCode >= 106 && iCode <= 111: // operations on numeric keypad (+, -, etc.) if (oEvent.key !== undefined && oEvent.key !== '') { return oEvent.key; } var sDecoded = String.fromCharCode(iCode); switch (oEvent.shiftKey) { case false: sDecoded = sDecoded.toLowerCase(); break; case true: sDecoded = sDecoded.toUpperCase(); break; } return sDecoded; case iCode >= 96 && iCode <= 105: // numbers on numeric keypad return 0+(iCode-96); } return ''; }, /** * Simulates a scan of the provided code. * * The scan code can be defined as * - a string - in this case no keyCode decoding is done and the code is merely validated * against constraints like minLenght, etc. * - an array of keyCodes (e.g. `[70,71,80]`) - will produce `keydown` events with corresponding * `keyCode` properties. NOTE: these events will have empty `key` properties, so decoding may * yield different results than with native events. * - an array of objects (e.g. `[{keyCode: 70, key: "F", shiftKey: true}, {keyCode: 71, key: "g"}]`) - * this way almost any event can be simulated, but it's a lot of work to do. * * @param DomElement oDomElement * @param string|array mStringOrArray * @return self */ simulate: function(oDomElement, mStringOrArray){ this._reinitialize(oDomElement); if (Array.isArray(mStringOrArray)){ mStringOrArray.forEach(function(mKey){ var oEventProps = {}; if( (typeof mKey === "object" || typeof mKey === 'function') && (mKey !== null) ) { oEventProps = mKey; } else { oEventProps.keyCode = parseInt(mKey); } var oEvent = new KeyboardEvent('keydown', oEventProps); document.dispatchEvent(oEvent); }) } else { this._validateScanCode(oDomElement, mStringOrArray); } return this; }, /** * @private * @param DomElement oDomElement * @return void */ _reinitialize: function(oDomElement){ var oVars = oDomElement.scannerDetectionData.vars; oVars.firstCharTime = 0; oVars.lastCharTime = 0; oVars.accumulatedString = ''; return; }, /** * @private * @param DomElement oDomElement * @return boolean */ _isFocusOnIgnoredElement: function(oDomElement){ var ignoreSelectors = oDomElement.scannerDetectionData.options.ignoreIfFocusOn; if(!ignoreSelectors){ return false; } var oFocused = document.activeElement; // checks if ignored element is an array, and if so it checks if one of the elements of it is an active one if (Array.isArray(ignoreSelectors)){ for(var i=0; i (sScanCode.length * oOptions.avgTimeByChar)): oScanError = { message: "Received code was not entered in time" }; break; // if a code was not filtered out earlier it is valid default: oOptions.onScan.call(oDomElement, sScanCode, iSingleScanQty); oEvent = new CustomEvent( 'scan', { detail: { scanCode: sScanCode, qty: iSingleScanQty } } ); oDomElement.dispatchEvent(oEvent); onScan._reinitialize(oDomElement); return true; } // If an error occurred (otherwise the method would return earlier) create an object for errordetection oScanError.scanCode = sScanCode; oScanError.scanDuration = iLastCharTime - iFirstCharTime; oScanError.avgTimeByChar = oOptions.avgTimeByChar; oScanError.minLength = oOptions.minLength; oOptions.onScanError.call(oDomElement, oScanError); oEvent = new CustomEvent( 'scanError', {detail: oScanError} ); oDomElement.dispatchEvent(oEvent); onScan._reinitialize(oDomElement); return false; }, /** * @private * @param Object oDefaults * @param Object oOptions * @return Object */ _mergeOptions: function(oDefaults, oOptions){ var oExtended = {}; var prop; for (prop in oDefaults){ if (Object.prototype.hasOwnProperty.call(oDefaults, prop)){ oExtended[prop] = oDefaults[prop]; } } for (prop in oOptions){ if (Object.prototype.hasOwnProperty.call(oOptions, prop)){ oExtended[prop] = oOptions[prop]; } } return oExtended; }, /** * @private * @param KeyboardEvent e * @return int * @see https://www.w3schools.com/jsref/event_key_keycode.asp */ _getNormalizedKeyNum: function(e){ return e.which || e.keyCode; }, /** * @private * @param KeyboardEvent e * @return void */ _handleKeyDown: function(e){ var iKeyCode = onScan._getNormalizedKeyNum(e); var oOptions = this.scannerDetectionData.options; var oVars = this.scannerDetectionData.vars; var bScanFinished = false; if (oOptions.onKeyDetect.call(this, iKeyCode, e) === false) { return; } if (onScan._isFocusOnIgnoredElement(this)){ return; } // If it's just the button of the scanner, ignore it and wait for the real input if(oOptions.scanButtonKeyCode !== false && iKeyCode==oOptions.scanButtonKeyCode) { // if the button was first pressed, start a timeout for the callback, which gets interrupted if the scanbutton gets released if (!oVars.longPressed){ oVars.longPressTimer = setTimeout( oOptions.onScanButtonLongPress, oOptions.scanButtonLongPressTime, this); oVars.longPressed = true; } return; } switch(true){ // If it's not the first character and we encounter a terminating character, trigger scan process case (oVars.firstCharTime && oOptions.suffixKeyCodes.indexOf(iKeyCode)!==-1): e.preventDefault(); e.stopImmediatePropagation(); bScanFinished=true; break; // If it's the first character and we encountered one of the starting characters, don't process the scan case (!oVars.firstCharTime && oOptions.prefixKeyCodes.indexOf(iKeyCode)!==-1): e.preventDefault(); e.stopImmediatePropagation(); bScanFinished=false; break; // Otherwise, just add the character to the scan string we're building default: var character = oOptions.keyCodeMapper.call(this, e); if (character === null){ return; } oVars.accumulatedString += character; if (oOptions.preventDefault) { e.preventDefault(); } if (oOptions.stopPropagation) { e.stopImmediatePropagation(); } bScanFinished=false; break; } if(!oVars.firstCharTime){ oVars.firstCharTime=Date.now(); } oVars.lastCharTime=Date.now(); if(oVars.testTimer){ clearTimeout(oVars.testTimer); } if(bScanFinished){ onScan._validateScanCode(this, oVars.accumulatedString); oVars.testTimer=false; } else { oVars.testTimer=setTimeout(onScan._validateScanCode, oOptions.timeBeforeScanTest, this, oVars.accumulatedString); } oOptions.onKeyProcess.call(this, character, e); return; }, /** * @private * @param Event e * @return void */ _handlePaste: function(e){ var oOptions = this.scannerDetectionData.options; var oVars = this.scannerDetectionData.vars; var sPasteString = (event.clipboardData || window.clipboardData).getData('text'); // if the focus is on an ignored element, abort if (onScan._isFocusOnIgnoredElement(this)){ return; } e.preventDefault(); if (oOptions.stopPropagation) { e.stopImmediatePropagation(); } oOptions.onPaste.call(this, sPasteString, event); oVars.firstCharTime = 0; oVars.lastCharTime = 0; // validate the string onScan._validateScanCode(this, sPasteString); return; }, /** * @private * @param KeyboardEvent e * @return void */ _handleKeyUp: function(e){ // if the focus is on an ignored element, abort if (onScan._isFocusOnIgnoredElement(this)){ return; } var iKeyCode = onScan._getNormalizedKeyNum(e); // if hardware key is not being pressed anymore stop the timeout and reset if (iKeyCode == this.scannerDetectionData.options.scanButtonKeyCode){ clearTimeout(this.scannerDetectionData.vars.longPressTimer); this.scannerDetectionData.vars.longPressed = false; } return; }, /** * Returns TRUE if the scanner is currently in the middle of a scan sequence. * * @param DomElement * @return boolean */ isScanInProgressFor: function(oDomElement) { return oDomElement.scannerDetectionData.vars.firstCharTime > 0; }, /** * Returns TRUE if onScan is attached to the given DOM element and FALSE otherwise. * * @param DomElement * @return boolean */ isAttachedTo: function(oDomElement) { return (oDomElement.scannerDetectionData !== undefined); } }; return onScan; })));