From 49d4ddd422145beee6d7bf442df9ecd0485d723d Mon Sep 17 00:00:00 2001
From: Joren Van Onder <jov@odoo.com>
Date: Tue, 26 May 2015 15:52:44 +0200
Subject: [PATCH] [IMP] barcode_events: implement a new way of dealing with
 barcode events

Implement a new way of dealing with barcode events. This was necessary
because the old way of dealing with input coming from barcode devices
did not stop keyevents. This lead to all kinds of issues (eg. barcode
reader output being inserted as tender in a paymentline).

This new class inserts itself so that it handles all keyevents before
everything else. This means that it has authority on what keypress
events pass and which ones do not. It differentiates between 'real'
keyboard input and input coming from devices by buffering events. When a
certain amount of events come in fast enough, it is handled as a barcode
and a 'barcode_scanned' event is emitted on core.bus. When buffered
events are decided to not be a barcode, they are recreated and
redispatched.
---
 .../static/src/js/framework/barcode_events.js | 118 ++++++++++++++++++
 addons/web/static/src/js/web_client.js        |   5 +
 addons/web/views/webclient_templates.xml      |   1 +
 3 files changed, 124 insertions(+)
 create mode 100644 addons/web/static/src/js/framework/barcode_events.js

diff --git a/addons/web/static/src/js/framework/barcode_events.js b/addons/web/static/src/js/framework/barcode_events.js
new file mode 100644
index 000000000000..6aa5327a768d
--- /dev/null
+++ b/addons/web/static/src/js/framework/barcode_events.js
@@ -0,0 +1,118 @@
+odoo.define('web.BarcodeEvents', function(require) {
+    "use strict";
+
+    var core = require('web.core');
+    var mixins = core.mixins;
+
+    return core.Class.extend(mixins.PropertiesMixin, {
+        timeout: null,
+        key_released: true,
+        buffered_key_events: [],
+
+        init: function(parent) {
+            mixins.PropertiesMixin.init.call(this);
+        },
+
+        handle_buffered_keys: function() {
+            var code = "";
+
+            if (this.buffered_key_events.length >= 3) {
+                for (var i = 0; i < this.buffered_key_events.length; i++) {
+                    code += String.fromCharCode(this.buffered_key_events[i].which);
+                }
+
+                core.bus.trigger('barcode_scanned', code);
+
+                // Dispatch a barcode_scanned DOM event to elements that have
+                // barcode_events="true" set.
+                if (this.element_is_editable(this.buffered_key_events[0].target)) {
+                    $(this.buffered_key_events[0].target).trigger('barcode_scanned', code);
+                }
+            } else {
+                this.resend_buffered_keys();
+            }
+
+            this.buffered_key_events = [];
+        },
+
+        resend_buffered_keys: function() {
+            for(var i = 0; i < this.buffered_key_events.length; i++) {
+                var old_event = this.buffered_key_events[i];
+
+                if(old_event.which !== 13) { // ignore returns
+                    // We do not create a 'real' keypress event through
+                    // eg. KeyboardEvent because there are several issues
+                    // with them that make them very different from
+                    // genuine keypresses. Chrome per example has had a
+                    // bug for the longest time that causes keyCode and
+                    // charCode to not be set for events created this way:
+                    // https://bugs.webkit.org/show_bug.cgi?id=16735
+                    var new_event = new Event("keypress", {
+                        'bubbles': old_event.bubbles,
+                        'cancelable': old_event.cancelable,
+                    });
+
+                    new_event.viewArg = old_event.viewArg;
+                    new_event.ctrl = old_event.ctrl;
+                    new_event.alt = old_event.alt;
+                    new_event.shift = old_event.shift;
+                    new_event.meta = old_event.meta;
+                    new_event.char = old_event.char;
+                    new_event.key = old_event.key;
+                    new_event.charCode = old_event.charCode;
+                    new_event.keyCode = old_event.keyCode || old_event.which; // Firefox doesn't set keyCode for keypresses, only keyup/down
+                    new_event.which = old_event.which;
+                    new_event.dispatched_by_barcode_reader = true;
+
+                    old_event.target.dispatchEvent(new_event);
+                }
+            }
+        },
+
+        element_is_editable: function(element) {
+            return $(element).is('input,textarea,[contenteditable="true"]');
+        },
+
+        keyup_handler: function(e){
+            this.key_released = true;
+        },
+
+        handler: function(e){
+            if (! e.dispatched_by_barcode_reader) {
+                if (! this.key_released) { // don't allow key repeat
+                    e.preventDefault();
+                    e.stopImmediatePropagation();
+                    return;
+                }
+
+                this.key_released = false;
+
+                // We only stop events targeting body (meaning nothing is
+                // focused). We do not stop events targeting other elements
+                // because we have no way of redispatching 'genuine' key events
+                // that trigger native event handlers of elements. So this means
+                // that our fake events will not appear in eg. an <input>
+                // element. The addition of the contentEditable attribute in HTML5
+                // means that this is not only limited to <input>, <textarea>,...
+                if (! this.element_is_editable(e.target) || e.target.getAttribute("barcode_events") === "true") {
+                    this.buffered_key_events.push(e);
+                    e.preventDefault();
+                    e.stopImmediatePropagation();
+
+                    clearTimeout(this.timeout);
+                    this.timeout = setTimeout(this.handle_buffered_keys.bind(this), 100);
+                }
+            }
+        },
+
+        start: function(){
+            document.body.addEventListener('keypress', this.handler.bind(this), true);
+            document.body.addEventListener('keyup', this.keyup_handler.bind(this), true);
+        },
+
+        stop: function(){
+            document.body.removeEventListener('keypress', this.handler.bind(this), true);
+            document.body.removeEventListener('keyup', this.keyup_handler.bind(this), true);
+        },
+    });
+});
diff --git a/addons/web/static/src/js/web_client.js b/addons/web/static/src/js/web_client.js
index de649d77aa24..dbd1e5628101 100644
--- a/addons/web/static/src/js/web_client.js
+++ b/addons/web/static/src/js/web_client.js
@@ -14,6 +14,7 @@ var SystrayMenu = require('web.SystrayMenu');
 var UserMenu = require('web.UserMenu');
 var utils = require('web.utils');
 var Widget = require('web.Widget');
+var BarcodeEvents = require('web.BarcodeEvents');
 
 var QWeb = core.qweb;
 var _t = core._t;
@@ -36,6 +37,7 @@ var WebClient = Widget.extend({
         this.menu_dm = new utils.DropMisordered();
         this.action_mutex = new utils.Mutex();
         this.set('title_part', {"zopenerp": "Odoo"});
+        this.barcode_events = new BarcodeEvents();
     },
     start: function() {
         var self = this;
@@ -339,6 +341,9 @@ var WebClient = Widget.extend({
         this.$('.oe_webclient').toggleClass(
             'oe_content_full_screen', fullscreen);
     },
+    get_barcode_events: function() {
+        return this.barcode_events;
+    }
 });
 
 return WebClient;
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index b893e41a896a..8e9fd23e563d 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -31,6 +31,7 @@
             <script type="text/javascript" src="/web/static/src/js/framework/session.js"></script>
             <script type="text/javascript" src="/web/static/src/js/framework/model.js"></script>
             <script type="text/javascript" src="/web/static/src/js/framework/utils.js"></script>
+            <script type="text/javascript" src="/web/static/src/js/framework/barcode_events.js"></script>
             <script type="text/javascript" src="/web/static/src/js/framework/core.js"></script>
             <script type="text/javascript" src="/web/static/src/js/tour.js"></script>
             <script type="text/javascript" src="/web/static/test/menu.js"></script>
-- 
GitLab