|
- /*
- * 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 successfull scanning: function(){sScancode, iCount)}()
- onScanError: function(oDebug){}, // Callback after detection of a unsuccessfull 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 successfull scan while the scan button was pressed and held down
- scanButtonKeyCode:false, // Key code of the scanner hardware button (if the scanner button a 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 do difference 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<ignoreSelectors.length; i++){
- if(oFocused.matches(ignoreSelectors[i]) === true){
- return true;
- }
- }
- // if the option consists of an single element, it only checks this one
- } else if (oFocused.matches(ignoreSelectors)){
- return true;
- }
-
- // if the active element is not listed in the ignoreIfFocusOn option, return false
- return false;
- },
-
- /**
- * Validates the scan code accumulated by the given DOM element and fires the respective events.
- *
- * @private
- * @param DomElement oDomElement
- * @return boolean
- */
- _validateScanCode: function(oDomElement, sScanCode){
- var oScannerData = oDomElement.scannerDetectionData;
- var oOptions = oScannerData.options;
- var iSingleScanQty = oScannerData.options.singleScanQty;
- var iFirstCharTime = oScannerData.vars.firstCharTime;
- var iLastCharTime = oScannerData.vars.lastCharTime;
- var oScanError = {};
- var oEvent;
-
- switch(true){
-
- // detect codes that are too short
- case (sScanCode.length < oOptions.minLength):
- oScanError = {
- message: "Receieved code is shorter then minimal length"
- };
- break;
-
- // detect codes that were entered too slow
- case ((iLastCharTime - iFirstCharTime) > (sScanCode.length * oOptions.avgTimeByChar)):
- oScanError = {
- message: "Receieved 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 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;
- })));
|