Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 

526 wiersze
16 KiB

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