UNPKG

16.7 kBJavaScriptView Raw
1/*
2 * onScan.js - scan-events for hardware barcodes scanners in javascript
3 */
4;(function (global, factory) {
5 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
6 typeof define === 'function' && define.amd ? define(factory()) :
7 global.onScan = factory()
8}(this, (function () {
9 var onScan = {
10
11 /**
12 *
13 * @param DomElement oDomElement
14 * @param Object oOptions
15 * @return self
16 */
17 attachTo: function(oDomElement, oOptions) {
18
19 if(oDomElement.scannerDetectionData !== undefined){
20 throw new Error("onScan.js is already initialized for DOM element " + oDomElement);
21 }
22
23 var oDefaults = {
24 onScan: function(sScanned, iQty){}, // Callback after detection of a successfull scanning: function(){sScancode, iCount)}()
25 onScanError: function(oDebug){}, // Callback after detection of a unsuccessfull scanning (scanned string in parameter)
26 onKeyProcess: function(sChar, oEvent){}, // Callback after receiving and processing a char (scanned char in parameter)
27 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!
28 onPaste: function(sPasted, oEvent){}, // Callback after receiving a value on paste, no matter if it is a valid code or not
29 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.
30 onScanButtonLongPress: function(){}, // Callback after detection of a successfull scan while the scan button was pressed and held down
31 scanButtonKeyCode:false, // Key code of the scanner hardware button (if the scanner button a acts as a key itself)
32 scanButtonLongPressTime:500, // How long (ms) the hardware button should be pressed, until a callback gets executed
33 timeBeforeScanTest:100, // Wait duration (ms) after keypress event to check if scanning is finished
34 avgTimeByChar:30, // Average time (ms) between 2 chars. Used to do difference between keyboard typing and scanning
35 minLength:6, // Minimum length for a scanning
36 suffixKeyCodes:[9,13], // Chars to remove and means end of scanning
37 prefixKeyCodes:[], // Chars to remove and means start of scanning
38 ignoreIfFocusOn:false, // do not handle scans if the currently focused element matches this selector or object
39 stopPropagation:false, // Stop immediate propagation on keypress event
40 preventDefault:false, // Prevent default action on keypress event
41 captureEvents:false, // Get the events before any listeners deeper in the DOM
42 reactToKeydown:true, // look for scan input in keyboard events
43 reactToPaste:false, // look for scan input in paste events
44 singleScanQty: 1, // Quantity of Items put out to onScan in a single scan
45 }
46
47 oOptions = this._mergeOptions(oDefaults, oOptions);
48
49 // initializing options and variables on DomElement
50 oDomElement.scannerDetectionData = {
51 options: oOptions,
52 vars:{
53 firstCharTime: 0,
54 lastCharTime: 0,
55 accumulatedString: '',
56 testTimer: false,
57 longPressTimeStart: 0,
58 longPressed: false
59 }
60
61 };
62
63 // initializing handlers (based on settings)
64 if (oOptions.reactToPaste === true){
65 oDomElement.addEventListener("paste", this._handlePaste, oOptions.captureEvents);
66 }
67 if (oOptions.scanButtonKeyCode !== false){
68 oDomElement.addEventListener("keyup", this._handleKeyUp, oOptions.captureEvents);
69 }
70 if (oOptions.reactToKeydown === true || oOptions.scanButtonKeyCode !== false){
71 oDomElement.addEventListener("keydown", this._handleKeyDown, oOptions.captureEvents);
72 }
73 return this;
74 },
75
76 /**
77 *
78 * @param DomElement oDomElement
79 * @return void
80 */
81 detachFrom: function(oDomElement) {
82 // detaching all used events
83 if (oDomElement.scannerDetectionData.options.reactToPaste){
84 oDomElement.removeEventListener("paste", this._handlePaste);
85 }
86 if (oDomElement.scannerDetectionData.options.scanButtonKeyCode !== false){
87 oDomElement.removeEventListener("keyup", this._handleKeyUp);
88 }
89 oDomElement.removeEventListener("keydown", this._handleKeyDown);
90
91 // clearing data off DomElement
92 oDomElement.scannerDetectionData = undefined;
93 return;
94 },
95
96 /**
97 *
98 * @param DomElement oDomElement
99 * @return Object
100 */
101 getOptions: function(oDomElement){
102 return oDomElement.scannerDetectionData.options;
103 },
104
105 /**
106 *
107 * @param DomElement oDomElement
108 * @param Object oOptions
109 * @return self
110 */
111 setOptions: function(oDomElement, oOptions){
112 // check if some handlers need to be changed based on possible option changes
113 switch (oDomElement.scannerDetectionData.options.reactToPaste){
114 case true:
115 if (oOptions.reactToPaste === false){
116 oDomElement.removeEventListener("paste", this._handlePaste);
117 }
118 break;
119 case false:
120 if (oOptions.reactToPaste === true){
121 oDomElement.addEventListener("paste", this._handlePaste);
122 }
123 break;
124 }
125
126 switch (oDomElement.scannerDetectionData.options.scanButtonKeyCode){
127 case false:
128 if (oOptions.scanButtonKeyCode !== false){
129 oDomElement.addEventListener("keyup", this._handleKeyUp);
130 }
131 break;
132 default:
133 if (oOptions.scanButtonKeyCode === false){
134 oDomElement.removeEventListener("keyup", this._handleKeyUp);
135 }
136 break;
137 }
138
139 // merge old and new options
140 oDomElement.scannerDetectionData.options = this._mergeOptions(oDomElement.scannerDetectionData.options, oOptions);
141
142 // reinitiallize
143 this._reinitialize(oDomElement);
144 return this;
145 },
146
147 /**
148 * Transforms key codes into characters.
149 *
150 * By default, only the follwing key codes are taken into account
151 * - 48-90 (letters and regular numbers)
152 * - 96-105 (numeric keypad numbers)
153 * - 106-111 (numeric keypad operations)
154 *
155 * All other keys will yield empty strings!
156 *
157 * The above keycodes will be decoded using the KeyboardEvent.key property on modern
158 * browsers. On older browsers the method will fall back to String.fromCharCode()
159 * putting the result to upper/lower case depending on KeyboardEvent.shiftKey if
160 * it is set.
161 *
162 * @param KeyboardEvent oEvent
163 * @return string
164 */
165 decodeKeyEvent : function (oEvent) {
166 var iCode = this._getNormalizedKeyNum(oEvent);
167 switch (true) {
168 case iCode >= 48 && iCode <= 90: // numbers and letters
169 case iCode >= 106 && iCode <= 111: // operations on numeric keypad (+, -, etc.)
170 if (oEvent.key !== undefined && oEvent.key !== '') {
171 return oEvent.key;
172 }
173
174 var sDecoded = String.fromCharCode(iCode);
175 switch (oEvent.shiftKey) {
176 case false: sDecoded = sDecoded.toLowerCase(); break;
177 case true: sDecoded = sDecoded.toUpperCase(); break;
178 }
179 return sDecoded;
180 case iCode >= 96 && iCode <= 105: // numbers on numeric keypad
181 return 0+(iCode-96);
182 }
183 return '';
184 },
185
186 /**
187 * Simulates a scan of the provided code.
188 *
189 * The scan code can be defined as
190 * - a string - in this case no keyCode decoding is done and the code is merely validated
191 * against constraints like minLenght, etc.
192 * - an array of keyCodes (e.g. `[70,71,80]`) - will produce `keydown` events with corresponding
193 * `keyCode` properties. NOTE: these events will have empty `key` properties, so decoding may
194 * yield different results than with native events.
195 * - an array of objects (e.g. `[{keyCode: 70, key: "F", shiftKey: true}, {keyCode: 71, key: "g"}]`) -
196 * this way almost any event can be simulated, but it's a lot of work to do.
197 *
198 * @param DomElement oDomElement
199 * @param string|array mStringOrArray
200 * @return self
201 */
202 simulate: function(oDomElement, mStringOrArray){
203 this._reinitialize(oDomElement);
204 if (Array.isArray(mStringOrArray)){
205 mStringOrArray.forEach(function(mKey){
206 var oEventProps = {};
207 if( (typeof mKey === "object" || typeof mKey === 'function') && (mKey !== null) ) {
208 oEventProps = mKey;
209 } else {
210 oEventProps.keyCode = parseInt(mKey);
211 }
212 var oEvent = new KeyboardEvent('keydown', oEventProps);
213 document.dispatchEvent(oEvent);
214 })
215 } else {
216 this._validateScanCode(oDomElement, mStringOrArray);
217 }
218 return this;
219 },
220
221 /**
222 * @private
223 * @param DomElement oDomElement
224 * @return void
225 */
226 _reinitialize: function(oDomElement){
227 var oVars = oDomElement.scannerDetectionData.vars;
228 oVars.firstCharTime = 0;
229 oVars.lastCharTime = 0;
230 oVars.accumulatedString = '';
231 return;
232 },
233
234 /**
235 * @private
236 * @param DomElement oDomElement
237 * @return boolean
238 */
239 _isFocusOnIgnoredElement: function(oDomElement){
240
241 var ignoreSelectors = oDomElement.scannerDetectionData.options.ignoreIfFocusOn;
242
243 if(!ignoreSelectors){
244 return false;
245 }
246
247 var oFocused = document.activeElement;
248
249 // checks if ignored element is an array, and if so it checks if one of the elements of it is an active one
250 if (Array.isArray(ignoreSelectors)){
251 for(var i=0; i<ignoreSelectors.length; i++){
252 if(oFocused.matches(ignoreSelectors[i]) === true){
253 return true;
254 }
255 }
256 // if the option consists of an single element, it only checks this one
257 } else if (oFocused.matches(ignoreSelectors)){
258 return true;
259 }
260
261 // if the active element is not listed in the ignoreIfFocusOn option, return false
262 return false;
263 },
264
265 /**
266 * Validates the scan code accumulated by the given DOM element and fires the respective events.
267 *
268 * @private
269 * @param DomElement oDomElement
270 * @return boolean
271 */
272 _validateScanCode: function(oDomElement, sScanCode){
273 var oScannerData = oDomElement.scannerDetectionData;
274 var oOptions = oScannerData.options;
275 var iSingleScanQty = oScannerData.options.singleScanQty;
276 var iFirstCharTime = oScannerData.vars.firstCharTime;
277 var iLastCharTime = oScannerData.vars.lastCharTime;
278 var oScanError = {};
279 var oEvent;
280
281 switch(true){
282
283 // detect codes that are too short
284 case (sScanCode.length < oOptions.minLength):
285 oScanError = {
286 message: "Receieved code is shorter then minimal length"
287 };
288 break;
289
290 // detect codes that were entered too slow
291 case ((iLastCharTime - iFirstCharTime) > (sScanCode.length * oOptions.avgTimeByChar)):
292 oScanError = {
293 message: "Receieved code was not entered in time"
294 };
295 break;
296
297 // if a code was not filtered out earlier it is valid
298 default:
299 oOptions.onScan.call(oDomElement, sScanCode, iSingleScanQty);
300 oEvent = new CustomEvent(
301 'scan',
302 {
303 detail: {
304 scanCode: sScanCode,
305 qty: iSingleScanQty
306 }
307 }
308 );
309 oDomElement.dispatchEvent(oEvent);
310 onScan._reinitialize(oDomElement);
311 return true;
312 }
313
314 // If an error occurred (otherwise the method would return earlier) create an object for errordetection
315 oScanError.scanCode = sScanCode;
316 oScanError.scanDuration = iLastCharTime - iFirstCharTime;
317 oScanError.avgTimeByChar = oOptions.avgTimeByChar;
318 oScanError.minLength = oOptions.minLength;
319
320 oOptions.onScanError.call(oDomElement, oScanError);
321
322 oEvent = new CustomEvent(
323 'scanError',
324 {detail: oScanError}
325 );
326 oDomElement.dispatchEvent(oEvent);
327
328 onScan._reinitialize(oDomElement);
329 return false;
330 },
331
332 /**
333 * @private
334 * @param Object oDefaults
335 * @param Object oOptions
336 * @return Object
337 */
338 _mergeOptions: function(oDefaults, oOptions){
339 var oExtended = {};
340 var prop;
341 for (prop in oDefaults){
342 if (Object.prototype.hasOwnProperty.call(oDefaults, prop)){
343 oExtended[prop] = oDefaults[prop];
344 }
345 }
346 for (prop in oOptions){
347 if (Object.prototype.hasOwnProperty.call(oOptions, prop)){
348 oExtended[prop] = oOptions[prop];
349 }
350 }
351 return oExtended;
352 },
353
354 /**
355 * @private
356 * @param KeyboardEvent e
357 * @return int
358 * @see https://www.w3schools.com/jsref/event_key_keycode.asp
359 */
360 _getNormalizedKeyNum: function(e){
361 return e.which || e.keyCode;
362 },
363
364
365 /**
366 * @private
367 * @param KeyboardEvent e
368 * @return void
369 */
370 _handleKeyDown: function(e){
371 var iKeyCode = onScan._getNormalizedKeyNum(e);
372 var oOptions = this.scannerDetectionData.options;
373 var oVars = this.scannerDetectionData.vars;
374 var bScanFinished = false;
375
376 if (oOptions.onKeyDetect.call(this, iKeyCode, e) === false) {
377 return;
378 }
379
380 if (onScan._isFocusOnIgnoredElement(this)){
381 return;
382 }
383
384 // If it's just the button of the scanner, ignore it and wait for the real input
385 if(oOptions.scanButtonKeyCode !== false && iKeyCode==oOptions.scanButtonKeyCode) {
386
387 // if the button was first pressed, start a timeout for the callback, which gets interrupted if the scanbutton gets released
388 if (!oVars.longPressed){
389 oVars.longPressTimer = setTimeout( oOptions.onScanButtonLongPress, oOptions.scanButtonLongPressTime, this);
390 oVars.longPressed = true;
391 }
392
393 return;
394 }
395
396 switch(true){
397 // If it's not the first character and we encounter a terminating character, trigger scan process
398 case (oVars.firstCharTime && oOptions.suffixKeyCodes.indexOf(iKeyCode)!==-1):
399 e.preventDefault();
400 e.stopImmediatePropagation();
401 bScanFinished=true;
402 break;
403
404 // If it's the first character and we encountered one of the starting characters, don't process the scan
405 case (!oVars.firstCharTime && oOptions.prefixKeyCodes.indexOf(iKeyCode)!==-1):
406 e.preventDefault();
407 e.stopImmediatePropagation();
408 bScanFinished=false;
409 break;
410
411 // Otherwise, just add the character to the scan string we're building
412 default:
413 var character = oOptions.keyCodeMapper.call(this, e);
414 if (character === null){
415 return;
416 }
417 oVars.accumulatedString += character;
418
419 if (oOptions.preventDefault) {
420 e.preventDefault();
421 }
422 if (oOptions.stopPropagation) {
423 e.stopImmediatePropagation();
424 }
425
426 bScanFinished=false;
427 break;
428 }
429
430 if(!oVars.firstCharTime){
431 oVars.firstCharTime=Date.now();
432 }
433
434 oVars.lastCharTime=Date.now();
435
436 if(oVars.testTimer){
437 clearTimeout(oVars.testTimer);
438 }
439
440 if(bScanFinished){
441 onScan._validateScanCode(this, oVars.accumulatedString);
442 oVars.testTimer=false;
443 } else {
444 oVars.testTimer=setTimeout(onScan._validateScanCode, oOptions.timeBeforeScanTest, this, oVars.accumulatedString);
445 }
446
447 oOptions.onKeyProcess.call(this, character, e);
448 return;
449 },
450
451 /**
452 * @private
453 * @param Event e
454 * @return void
455 */
456 _handlePaste: function(e){
457
458 var oOptions = this.scannerDetectionData.options;
459 var oVars = this.scannerDetectionData.vars;
460 var sPasteString = (event.clipboardData || window.clipboardData).getData('text');
461
462 // if the focus is on an ignored element, abort
463 if (onScan._isFocusOnIgnoredElement(this)){
464 return;
465 }
466
467 e.preventDefault();
468
469 if (oOptions.stopPropagation) {
470 e.stopImmediatePropagation();
471 }
472
473 oOptions.onPaste.call(this, sPasteString, event);
474
475 oVars.firstCharTime = 0;
476 oVars.lastCharTime = 0;
477
478 // validate the string
479 onScan._validateScanCode(this, sPasteString);
480 return;
481 },
482
483 /**
484 * @private
485 * @param KeyboardEvent e
486 * @return void
487 */
488 _handleKeyUp: function(e){
489 // if the focus is on an ignored element, abort
490 if (onScan._isFocusOnIgnoredElement(this)){
491 return;
492 }
493
494 var iKeyCode = onScan._getNormalizedKeyNum(e);
495
496 // if hardware key is not being pressed anymore stop the timeout and reset
497 if (iKeyCode == this.scannerDetectionData.options.scanButtonKeyCode){
498 clearTimeout(this.scannerDetectionData.vars.longPressTimer);
499 this.scannerDetectionData.vars.longPressed = false;
500 }
501 return;
502 },
503
504 /**
505 * Returns TRUE the scanner is currently in the middle of a scan sequence.
506 *
507 * @param DomElement
508 * @return boolean
509 */
510 isScanInProgressFor: function(oDomElement) {
511 return oDomElement.scannerDetectionData.vars.firstCharTime > 0;
512 },
513
514 /**
515 * Returns TRUE if onScan is attached to the given DOM element and FALSE otherwise.
516 *
517 * @param DomElement
518 * @return boolean
519 */
520 isAttachedTo: function(oDomElement) {
521 return (oDomElement.scannerDetectionData !== undefined);
522 }
523 };
524
525 return onScan;
526})));
\No newline at end of file