From 4aa1643cc40260079d41132a398b8ea4bf86f276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 15 Jun 2022 09:03:45 +0000 Subject: [PATCH 1/7] [REF] barcodes: simplify and improve barcode implementation - use keydown events all the time - remove dead code - simplify implementation Part-of: odoo/odoo#93387 --- .../barcodes/static/src/js/barcode_events.js | 178 +++++------------- .../barcodes/static/src/js/barcode_field.js | 34 +--- addons/barcodes/static/tests/barcode_tests.js | 117 ++++++------ 3 files changed, 110 insertions(+), 219 deletions(-) diff --git a/addons/barcodes/static/src/js/barcode_events.js b/addons/barcodes/static/src/js/barcode_events.js index 6f2f678506c..7481cd43dd4 100644 --- a/addons/barcodes/static/src/js/barcode_events.js +++ b/addons/barcodes/static/src/js/barcode_events.js @@ -3,20 +3,9 @@ odoo.define('barcodes.BarcodeEvents', function(require) { var config = require('web.config'); var core = require('web.core'); -var mixins = require('web.mixins'); var session = require('web.session'); - -// For IE >= 9, use this, new CustomEvent(), instead of new Event() -function CustomEvent ( event, params ) { - params = params || { bubbles: false, cancelable: false, detail: undefined }; - var evt = document.createEvent( 'CustomEvent' ); - evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); - return evt; - } -CustomEvent.prototype = window.Event.prototype; - -var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { +var BarcodeEvents = core.Class.extend({ timeout: null, key_pressed: {}, buffered_key_events: [], @@ -37,38 +26,24 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { inputTimeOut: 800, init: function() { - mixins.PropertiesMixin.init.call(this); - // Keep a reference of the handler functions to use when adding and removing event listeners - this.__keydown_handler = _.bind(this.keydown_handler, this); - this.__keyup_handler = _.bind(this.keyup_handler, this); this.__handler = _.bind(this.handler, this); // Bind event handler once the DOM is loaded // TODO: find a way to be active only when there are listeners on the bus - $(_.bind(this.start, this, false)); + $(() => this.start()); // Mobile device detection this.isChromeMobile = config.device.isMobileDevice && navigator.userAgent.match(/Chrome/i); + }, - // Creates an input who will receive the barcode scanner value. - this.$barcodeInput = $('', { - name: 'barcode', - type: 'text', - css: { - 'position': 'fixed', - 'top': '50%', - 'transform': 'translateY(-50%)', - 'z-index': '-1', - 'opacity': '0', - }, - }); - // Avoid to show autocomplete for a non appearing input - this.$barcodeInput.attr('autocomplete', 'off'); - - this.__blurBarcodeInput = _.debounce(this._blurBarcodeInput, this.inputTimeOut); + getCurrentString() { + if (this.$barcodeInput) { + return this.$barcodeInput.val(); + } + return this.buffered_key_events.reduce(function(memo, e) { return memo + e.key }, ''); }, handle_buffered_keys: function() { - var str = this.buffered_key_events.reduce(function(memo, e) { return memo + String.fromCharCode(e.which) }, ''); + let str = this.getCurrentString(); var match = str.match(this.regexp); if (match) { @@ -81,48 +56,11 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { // Dispatch a barcode_scanned DOM event to elements that have barcode_events="true" set. if (this.buffered_key_events[0].target.getAttribute("barcode_events") === "true") $(this.buffered_key_events[0].target).trigger('barcode_scanned', barcode); - } else { - this.resend_buffered_keys(); } this.buffered_key_events = []; }, - resend_buffered_keys: function() { - var old_event, new_event; - for(var i = 0; i < this.buffered_key_events.length; i++) { - 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 params = { - 'bubbles': old_event.bubbles, - 'cancelable': old_event.cancelable, - }; - new_event = $.Event('keypress', params); - 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).trigger(new_event); - } - } - }, - element_is_editable: function(element) { return $(element).is('input,textarea,[contenteditable="true"]'); }, @@ -138,6 +76,7 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { e.key === "Backspace" || e.key === "Delete" || e.key === "Home" || e.key === "End" || e.key === "PageUp" || e.key === "PageDown" || + e.key === "Shift" || e.key === "Unidentified" || /F\d\d?/.test(e.key)) { return true; } else { @@ -145,46 +84,27 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { } }, - // The keydown and keyup handlers are here to disallow key - // repeat. When preventDefault() is called on a keydown event - // the keypress that normally follows is cancelled. - keydown_handler: function(e){ - if (this.key_pressed[e.which]) { - e.preventDefault(); - } else { - this.key_pressed[e.which] = true; - } - }, - - keyup_handler: function(e){ - this.key_pressed[e.which] = false; - }, - handler: function(e){ - // Don't catch events we resent - if (e.dispatched_by_barcode_reader) - return; // Don't catch non-printable keys for which Firefox triggers a keypress - if (this.is_special_key(e)) + if (this.is_special_key(e)) { return; + } // Don't catch keypresses which could have a UX purpose (like shortcuts) - if (e.ctrlKey || e.metaKey || e.altKey) - return; - // Don't catch Return when nothing is buffered. This way users - // can still use Return to 'click' on focused buttons or links. - if (e.which === 13 && this.buffered_key_events.length === 0) + if (e.ctrlKey || e.metaKey || e.altKey) { return; + } // Don't catch events targeting elements that are editable because we // have no way of redispatching 'genuine' key events. Resent events // don't trigger native event handlers of elements. So this means that // our fake events will not appear in eg. an element. - if ((this.element_is_editable(e.target) && !$(e.target).data('enableBarcode')) && e.target.getAttribute("barcode_events") !== "true") + if ((this.element_is_editable(e.target) && !$(e.target).data('enableBarcode')) && e.target.getAttribute("barcode_events") !== "true") { return; + } // Catch and buffer the event - this.buffered_key_events.push(e); - e.preventDefault(); - e.stopImmediatePropagation(); + if (e.key !== "Enter") { + this.buffered_key_events.push(e); + } // Handle buffered keys immediately if the keypress marks the end // of a barcode or after x milliseconds without a new keypress @@ -215,40 +135,15 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { _listenBarcodeScanner: function (e) { if ($(document.activeElement).not('input:text, textarea, [contenteditable], ' + '[type="email"], [type="number"], [type="password"], [type="tel"], [type="search"]').length) { - $('body').append(this.$barcodeInput); this.$barcodeInput.focus(); } if (this.$barcodeInput.is(":focus")) { - // Handle buffered keys immediately if the keypress marks the end - // of a barcode or after x milliseconds without a new keypress. - clearTimeout(this.timeout); - // On chrome mobile, e.which only works for some special characters like ENTER or TAB. - if (String.fromCharCode(e.which).match(this.suffix)) { - this._handleBarcodeValue(e); - } else { - this.timeout = setTimeout(this._handleBarcodeValue.bind(this, e), - this.max_time_between_keys_in_ms); - } + this.handler(e); // if the barcode input doesn't receive keydown for a while, remove it. this.__blurBarcodeInput(); } }, - /** - * Retrieves the barcode value from the temporary input element. - * This checks this value and trigger it on the bus. - * - * @private - * @param {jQuery.Event} keydown event - */ - _handleBarcodeValue: function (e) { - var barcodeValue = this.$barcodeInput.val(); - if (barcodeValue.match(this.regexp)) { - core.bus.trigger('barcode_scanned', barcodeValue, $(e.target).parent()[0]); - this._blurBarcodeInput(); - } - }, - /** * Removes the value and focus from the barcode input. * If nothing happens, the focus will be lost and @@ -262,7 +157,7 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { this.$barcodeInput.val('').blur(); }, - start: function(prevent_key_repeat){ + start: function(){ // Chrome Mobile isn't triggering keypress event. // This is marked as Legacy in the DOM-Level-3 Standard. // See: https://www.w3.org/TR/uievents/#legacy-keyboardevent-event-types @@ -270,20 +165,35 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { // all other cases. // In master, we could remove the behavior with keypress and only use keydown. if (this.isChromeMobile) { + // Creates an input who will receive the barcode scanner value. + this.$barcodeInput = $('', { + name: 'barcode', + type: 'text', + css: { + 'position': 'fixed', + 'top': '50%', + 'transform': 'translateY(-50%)', + 'z-index': '-1', + 'opacity': '0', + }, + }); + // Avoid to show autocomplete for a non appearing input + this.$barcodeInput.attr('autocomplete', 'off'); + + this.__blurBarcodeInput = _.debounce(this._blurBarcodeInput, this.inputTimeOut); + + + $('body').append(this.$barcodeInput); + core.bus.on('barcode_scanned', null, () => this._blurBarcodeInput()); + $('body').on("keydown", this._listenBarcodeScanner.bind(this)); } else { - $('body').bind("keypress", this.__handler); - } - if (prevent_key_repeat === true) { - $('body').bind("keydown", this.__keydown_handler); - $('body').bind('keyup', this.__keyup_handler); + $('body').bind("keydown", this.__handler); } }, stop: function(){ - $('body').off("keypress", this.__handler); - $('body').off("keydown", this.__keydown_handler); - $('body').off('keyup', this.__keyup_handler); + $('body').off("keydown", this.__handler); }, }); diff --git a/addons/barcodes/static/src/js/barcode_field.js b/addons/barcodes/static/src/js/barcode_field.js index b5786f8a161..dee98412975 100644 --- a/addons/barcodes/static/src/js/barcode_field.js +++ b/addons/barcodes/static/src/js/barcode_field.js @@ -4,18 +4,13 @@ odoo.define('barcodes.field', function(require) { var AbstractField = require('web.AbstractField'); var basicFields = require('web.basic_fields'); var fieldRegistry = require('web.field_registry'); -var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents; // Field in which the user can both type normally and scan barcodes var FieldFloatScannable = basicFields.FieldFloat.extend({ events: _.extend({}, basicFields.FieldFloat.prototype.events, { - // The barcode_events component intercepts keypresses and releases them when it - // appears they are not part of a barcode. But since released keypresses don't - // trigger native behaviour (like characters input), we must simulate it. - keypress: '_onKeypress', + barcode_scanned: '_onBarcodeScan', }), - //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -35,28 +30,11 @@ var FieldFloatScannable = basicFields.FieldFloat.extend({ // Handlers //-------------------------------------------------------------------------- - /** - * @private - * @param {KeyboardEvent} e - */ - _onKeypress: function (e) { - /* only simulate a keypress if it has been previously prevented */ - if (e.dispatched_by_barcode_reader !== true) { - if (!BarcodeEvents.is_special_key(e)) { - e.preventDefault(); - } - return; - } - var character = String.fromCharCode(e.which); - var current_str = e.target.value; - var str_before_carret = current_str.substring(0, e.target.selectionStart); - var str_after_carret = current_str.substring(e.target.selectionEnd); - e.target.value = str_before_carret + character + str_after_carret; - var new_carret_index = str_before_carret.length + character.length; - e.target.setSelectionRange(new_carret_index, new_carret_index); - // trigger an 'input' event to notify the widget that it's value changed - $(e.target).trigger('input'); - }, + _onBarcodeScan() { + // trigger an 'input' event to make sure that the widget is call + // notifyChanges + this.$input.trigger('input'); + } }); // Field to use scan barcodes diff --git a/addons/barcodes/static/tests/barcode_tests.js b/addons/barcodes/static/tests/barcode_tests.js index 8ba4b92c3a7..1740a6321bc 100644 --- a/addons/barcodes/static/tests/barcode_tests.js +++ b/addons/barcodes/static/tests/barcode_tests.js @@ -11,6 +11,26 @@ var testUtils = require('web.test_utils'); var createView = testUtils.createView; var triggerKeypressEvent = testUtils.dom.triggerKeypressEvent; +var triggerEvent = testUtils.dom.triggerEvent; +var core = require('web.core'); + +function simulateBarCode(chars, target = document.body) { + for (let char of chars) { + let keycode; + if (char === 'Enter') { + keycode = $.ui.keyCode.ENTER; + } else if (char === "Tab") { + keycode = $.ui.keyCode.TAB; + } else { + keycode = char.charCodeAt(0); + } + triggerEvent(target, 'keydown', { + key: char, + keyCode: keycode, + which: keycode, + }); + } +} QUnit.module('Barcodes', { beforeEach: function () { @@ -39,6 +59,7 @@ QUnit.module('Barcodes', { fields: { name: {string : "Product name", type: "char"}, int_field: {string : "Integer", type: "integer"}, + int_field_2: {string : "Integer", type: "integer"}, barcode: {string: "Barcode", type: "char"}, }, records: [ @@ -80,9 +101,9 @@ QUnit.test('Button with barcode_trigger', async function (assert) { }); // O-BTN.doit - _.each(['O','-','B','T','N','.','d','o','i','t','Enter'], triggerKeypressEvent); + simulateBarCode(['O','-','B','T','N','.','d','o','i','t','Enter']); // O-BTN.dothat (should not call execute_action as the button isn't visible) - _.each(['O','-','B','T','N','.','d','o','t','h','a','t','Enter'], triggerKeypressEvent); + simulateBarCode(['O','-','B','T','N','.','d','o','t','h','a','t','Enter']); await testUtils.nextTick(); assert.verifySteps([], "no warning should be displayed"); @@ -107,26 +128,26 @@ QUnit.test('edit, save and cancel buttons', async function (assert) { }); // O-CMD.EDIT - _.each(["O","-","C","M","D",".","E","D","I","T","Enter"], triggerKeypressEvent); + simulateBarCode(["O","-","C","M","D",".","E","D","I","T","Enter"], document.body); await testUtils.nextTick(); assert.containsOnce(form, ".o_form_editable", "should have switched to 'edit' mode"); // dummy change to check that it actually saves await testUtils.fields.editInput(form.$('.o_field_widget'), 'test'); // O-CMD.SAVE - _.each(["O","-","C","M","D",".","S","A","V","E","Enter"], triggerKeypressEvent); + simulateBarCode(["O","-","C","M","D",".","S","A","V","E","Enter"], document.body); await testUtils.nextTick(); assert.containsOnce(form, ".o_form_readonly", "should have switched to 'readonly' mode"); assert.verifySteps(['save'], 'should have saved'); // O-CMD.EDIT - _.each(["O","-","C","M","D",".","E","D","I","T","Enter"], triggerKeypressEvent); + simulateBarCode(["O","-","C","M","D",".","E","D","I","T","Enter"], document.body); await testUtils.nextTick(); // dummy change to check that it correctly discards await testUtils.fields.editInput(form.$('.o_field_widget'), 'test'); // O-CMD.CANCEL - _.each(["O","-","C","M","D",".","D","I","S","C","A","R","D","Enter"], triggerKeypressEvent); + simulateBarCode(["O","-","C","M","D",".","D","I","S","C","A","R","D","Enter"], document.body); await testUtils.nextTick(); assert.containsOnce(form, ".o_form_readonly", "should have switched to 'readonly' mode"); @@ -152,19 +173,19 @@ QUnit.test('pager buttons', async function (assert) { assert.strictEqual(form.$('.o_field_widget').text(), 'Large Cabinet'); // O-CMD.PAGER-NEXT - _.each(["O","-","C","M","D",".","N","E","X","T","Enter"], triggerKeypressEvent); + simulateBarCode(["O","-","C","M","D",".","N","E","X","T","Enter"]); await testUtils.nextTick(); assert.strictEqual(form.$('.o_field_widget').text(), 'Cabinet with Doors'); // O-CMD.PAGER-PREV - _.each(["O","-","C","M","D",".","P","R","E","V","Enter"], triggerKeypressEvent); + simulateBarCode(["O","-","C","M","D",".","P","R","E","V","Enter"]); await testUtils.nextTick(); assert.strictEqual(form.$('.o_field_widget').text(), 'Large Cabinet'); // O-CMD.PAGER-LAST - _.each(["O","-","C","M","D",".","P","A","G","E","R","-","L","A","S","T","Enter"], triggerKeypressEvent); + simulateBarCode(["O","-","C","M","D",".","P","A","G","E","R","-","L","A","S","T","Enter"]); await testUtils.nextTick(); assert.strictEqual(form.$('.o_field_widget').text(), 'Cabinet with Doors'); // O-CMD.PAGER-FIRST - _.each(["O","-","C","M","D",".","P","A","G","E","R","-","F","I","R","S","T","Enter"], triggerKeypressEvent); + simulateBarCode(["O","-","C","M","D",".","P","A","G","E","R","-","F","I","R","S","T","Enter"]); await testUtils.nextTick(); assert.strictEqual(form.$('.o_field_widget').text(), 'Large Cabinet'); @@ -207,13 +228,13 @@ QUnit.test('do no update form twice after a command barcode scanned', async func assert.verifySteps(['read'], "update should not have been called yet"); // switch to next record - _.each(["O","-","C","M","D",".","N","E","X","T","Enter"], triggerKeypressEvent); + simulateBarCode(["O","-","C","M","D",".","N","E","X","T","Enter"]); await testUtils.nextTick(); // a first update is done to reload the data (thus followed by a read), but // update shouldn't be called afterwards assert.verifySteps(['update', 'read']); - _.each(['5','4','3','9','8','2','6','7','1','2','5','2','Enter'], triggerKeypressEvent); + simulateBarCode(['5','4','3','9','8','2','6','7','1','2','5','2','Enter']); await testUtils.nextTick(); // a real barcode has been scanned -> an update should be requested (with // option reload='false', so it isn't followed by a read) @@ -225,16 +246,15 @@ QUnit.test('do no update form twice after a command barcode scanned', async func }); QUnit.test('widget field_float_scannable', async function (assert) { - var done = assert.async(); - assert.expect(11); - var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms; barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = 0; this.data.product.records[0].int_field = 4; - this.data.product.onchanges = { - int_field: function () {}, - }; + + function _onBarcodeScanned (code) { + assert.step(`barcode scanned ${code}`) + } + core.bus.on('barcode_scanned', null, _onBarcodeScanned); var form = await createView({ View: FormView, @@ -243,6 +263,7 @@ QUnit.test('widget field_float_scannable', async function (assert) { arch: '
' + '' + '' + + '' + '', mockRPC: function (route, args) { if (args.method === 'onchange') { @@ -266,34 +287,25 @@ QUnit.test('widget field_float_scannable', async function (assert) { // simulates keypress events in the input to replace 0.00 by 26 (should not trigger onchanges) form.$('.o_field_widget[name=int_field]').focus(); + + // we check here that a scan on the fieldflotscannable widget triggers a + // barcode event + simulateBarCode(["6", "0", "1", "6", "4", "7", "8", "5"], document.activeElement) + await testUtils.nextTick(); + assert.verifySteps(['barcode scanned 60164785']); assert.strictEqual(form.$('.o_field_widget[name=int_field]').get(0), document.activeElement, "int field should be focused"); - form.$('.o_field_widget[name=int_field]').trigger({type: 'keypress', which: 50, keyCode: 50}); // 2 + + // we check here that a scan on the field without widget does not trigger a + // barcode event + form.$('.o_field_widget[name=int_field_2]').focus(); + simulateBarCode(["6", "0", "1", "6", "4", "7", "8", "5"], document.activeElement) await testUtils.nextTick(); - assert.strictEqual(form.$('.o_field_widget[name=int_field]').get(0), document.activeElement, - "int field should still be focused"); - form.$('.o_field_widget[name=int_field]').trigger({type: 'keypress', which: 54, keyCode: 54}); // 6 - await testUtils.nextTick(); - assert.strictEqual(form.$('.o_field_widget[name=int_field]').get(0), document.activeElement, - "int field should still be focused"); + assert.verifySteps([]); - setTimeout(async function () { - assert.strictEqual(form.$('.o_field_widget[name=int_field]').val(), '426', - "should display the correct value in edit"); - assert.strictEqual(form.$('.o_field_widget[name=int_field]').get(0), document.activeElement, - "int field should still be focused"); - - assert.verifySteps([], 'should not have done any onchange RPC'); - - form.$('.o_field_widget[name=int_field]').trigger('change'); // should trigger the onchange - await testUtils.nextTick(); - - assert.verifySteps(['onchange'], 'should have done the onchange RPC'); - - form.destroy(); - barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; - done(); - }); + form.destroy(); + barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; + core.bus.off('barcode_scanned', null, _onBarcodeScanned) }); QUnit.test('widget barcode_handler', async function (assert) { @@ -335,7 +347,7 @@ QUnit.test('widget barcode_handler', async function (assert) { assert.strictEqual(form.$('.o_field_widget[name=int_field]').val(), '0', "initial value should be correct"); - _.each(['5','4','3','9','8','2','6','7','1','2','5','2','Enter'], triggerKeypressEvent); + simulateBarCode(['5','4','3','9','8','2','6','7','1','2','5','2','Enter']); await testUtils.nextTick(); assert.strictEqual(form.$('.o_field_widget[name=int_field]').val(), '1', "value should have been incremented"); @@ -404,17 +416,17 @@ QUnit.test('specification of widget barcode_handler', async function (assert) { "one2many should contain 2 rows"); // scan twice product 1 - _.each(['1','2','3','4','5','6','7','8','9','0','Enter'], triggerKeypressEvent); + simulateBarCode(['1','2','3','4','5','6','7','8','9','0','Enter']); await testUtils.nextTick(); assert.strictEqual(form.$('.o_data_row:first .o_data_cell:nth(1)').text(), '1', "quantity of line one should have been incremented"); - _.each(['1','2','3','4','5','6','7','8','9','0','Enter'], triggerKeypressEvent); + simulateBarCode(['1','2','3','4','5','6','7','8','9','0','Enter']); await testUtils.nextTick(); assert.strictEqual(form.$('.o_data_row:first .o_data_cell:nth(1)').text(), '2', "quantity of line one should have been incremented"); // scan once product 2 - _.each(['0','9','8','7','6','5','4','3','2','1','Enter'], triggerKeypressEvent); + simulateBarCode(['0','9','8','7','6','5','4','3','2','1','Enter']); await testUtils.nextTick(); assert.strictEqual(form.$('.o_data_row:nth(1) .o_data_cell:nth(1)').text(), '1', "quantity of line one should have been incremented"); @@ -479,7 +491,7 @@ QUnit.test('specification of widget barcode_handler with keypress and notifyChan mode: 'edit', }, }); - _.each(['1','2','3','4','5','6','7','8','9','0','Enter'], triggerKeypressEvent); + simulateBarCode(['1','2','3','4','5','6','7','8','9','0','Enter']); await testUtils.nextTick(); // Quantity listener should open a dialog. triggerKeypressEvent('5'); @@ -550,16 +562,7 @@ QUnit.test('barcode_scanned only trigger error for active view', async function // We do not trigger on the body since modal and // form view are both inside it. - function modalTriggerKeypressEvent(char) { - var keycode; - if (char === "Enter") { - keycode = $.ui.keyCode.ENTER; - } else { - keycode = char.charCodeAt(0); - } - return $('.modal').trigger($.Event('keypress', {which: keycode, keyCode: keycode})); - } - _.each(['O','-','B','T','N','.','c','a','n','c','e','l','Enter'], modalTriggerKeypressEvent); + simulateBarCode(['O','-','B','T','N','.','c','a','n','c','e','l','Enter'], $('.modal')); await testUtils.nextTick(); assert.verifySteps(['danger'], "only one event should be triggered"); form.destroy(); From 2dd2f97de3dc3c9a3c179b10ac1ad56c0b4a5e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 15 Jun 2022 13:40:38 +0000 Subject: [PATCH 2/7] [REF] barcodes: introduce barcode_service And remove most of barcode_events. The goal is to have a service that can be used by components to reliably detect a barcode action. For now, we keep barcode_events around, but the goal is to completely remove it as soon as all the code using it is updated Part-of: odoo/odoo#93387 --- addons/barcodes/static/src/barcode_service.js | 153 ++++++++++++ .../barcodes/static/src/js/barcode_events.js | 224 ++---------------- .../barcodes/static/src/js/barcode_field.js | 2 +- .../static/src/js/barcode_form_view.js | 7 +- .../static/tests/barcode_mobile_tests.js | 68 ++++-- addons/barcodes/static/tests/barcode_tests.js | 36 ++- addons/point_of_sale/static/src/js/Chrome.js | 8 - .../static/src/js/Misc/NumberBuffer.js | 4 +- 8 files changed, 243 insertions(+), 259 deletions(-) create mode 100644 addons/barcodes/static/src/barcode_service.js diff --git a/addons/barcodes/static/src/barcode_service.js b/addons/barcodes/static/src/barcode_service.js new file mode 100644 index 00000000000..ccb1e2c224f --- /dev/null +++ b/addons/barcodes/static/src/barcode_service.js @@ -0,0 +1,153 @@ +/** @odoo-module **/ + +import { isBrowserChrome, isMobileOS } from "@web/core/browser/feature_detection"; +import { registry } from "@web/core/registry"; +import { session } from "@web/session"; + +const { EventBus, whenReady } = owl; + +function isSpecialKey(key) { + return (key === "ArrowLeft" || key === "ArrowRight" || + key === "ArrowUp" || key === "ArrowDown" || + key === "Escape" || key === "Tab" || + key === "Backspace" || key === "Delete" || + key === "Home" || key === "End" || + key === "PageUp" || key === "PageDown" || + key === "Shift" || /F\d\d?/.test(key)); +} + +function isEditable(element) { + return element.matches('input,textarea,[contenteditable="true"]'); +} + +function makeBarcodeInput() { + const inputEl = document.createElement('input'); + inputEl.setAttribute("style", "position:fixed;top:50%;transform:translateY(-50%);z-index:-1;opacity:0"); + inputEl.setAttribute("autocomplete", "off"); + inputEl.setAttribute("inputmode", "none"); // magic! prevent native keyboard from popping + inputEl.classList.add("o-barcode-input"); + inputEl.setAttribute('name', 'barcode'); + return inputEl; +} + +export const barcodeService = { + // Keys from a barcode scanner are usually processed as quick as possible, + // but some scanners can use an intercharacter delay (we support <= 50 ms) + maxTimeBetweenKeysInMs: session.max_time_between_keys_in_ms || 55, + + // this is done here to make it easily mockable in mobile tests + isMobileChrome: isMobileOS() && isBrowserChrome(), + + start() { + const bus = new EventBus(); + const endRegexp = /[\n\r\t]+/; + const barcodeRegexp = /(.{3,})[\n\r\t]*/; + let timeout = null; + let blurTimeout = null; + + // To be able to receive the barcode value, an input must be focused. + // On mobile devices, this causes the virtual keyboard to open. + // Unfortunately it is not possible to avoid this behavior... + // To avoid keyboard flickering at each detection of a barcode value, + // we want to keep it open for a while (800 ms). + const inputTimeOut = 8000; + + let bufferedBarcode = ""; + let currentTarget = null; + let barcodeInput = null; + + function handleBarcode(barcode, target) { + bus.trigger('barcode_scanned', {barcode,target}); + if (target.getAttribute('barcode_events') === "true") { + $(target).trigger('barcode_scanned', barcode); + } + } + + /** + * check if we have a barcode, and trigger appropriate events + */ + function checkBarcode() { + const str = barcodeInput ? barcodeInput.value : bufferedBarcode; + const match = str.match(barcodeRegexp); + if (match) { + const barcode = match[1]; + handleBarcode(barcode, currentTarget); + } + if (barcodeInput) { + blurBarcodeInput(); + } + bufferedBarcode = ""; + currentTarget = null; + } + + function keydownHandler(ev) { + // Don't catch non-printable keys for which Firefox triggers a keypress + if (isSpecialKey(ev.key)) { + return; + } + // Don't catch keypresses which could have a UX purpose (like shortcuts) + if (ev.ctrlKey || ev.metaKey || ev.altKey) { + return; + } + + currentTarget = ev.target; + // Don't catch events targeting elements that are editable because we + // have no way of redispatching 'genuine' key events. Resent events + // don't trigger native event handlers of elements. So this means that + // our fake events will not appear in eg. an element. + if (currentTarget !== barcodeInput && isEditable(currentTarget) && + !currentTarget.dataset.enableBarcode && + currentTarget.getAttribute("barcode_events") !== "true") { + return; + } + + if (ev.key !== "Enter" && ev.key !== "Unidentified") { + bufferedBarcode += ev.key; + } + clearTimeout(timeout); + if (String.fromCharCode(ev.which).match(endRegexp)) { + checkBarcode(); + } else { + timeout = setTimeout(checkBarcode, barcodeService.maxTimeBetweenKeysInMs); + } + } + + function mobileChromeHandler(ev) { + if (ev.key === "Unidentified") { + return; + } + if ($(document.activeElement).not('input:text, textarea, [contenteditable], ' + + '[type="email"], [type="number"], [type="password"], [type="tel"], [type="search"]').length) { + barcodeInput.focus(); + } + keydownHandler(ev); + clearTimeout(blurTimeout); + // if the barcode input doesn't receive keydown for a while, remove it. + blurTimeout = setTimeout(() => blurBarcodeInput(), inputTimeOut); + } + + function blurBarcodeInput() { + barcodeInput.value = ""; + // fixme: blurring the input should not be necessary, but for some + // reason, doing it fix some weird scrolling issue in a mobile (non + // native) device. + barcodeInput.blur(); + } + + whenReady(() => { + const isMobileChrome = barcodeService.isMobileChrome; + if (isMobileChrome) { + barcodeInput = makeBarcodeInput(); + document.body.appendChild(barcodeInput); + } + const handler = isMobileChrome ? mobileChromeHandler : keydownHandler; + document.body.addEventListener('keydown', handler); + }); + + return { + bus, + }; + }, +}; + +registry.category("services").add("barcode", barcodeService); diff --git a/addons/barcodes/static/src/js/barcode_events.js b/addons/barcodes/static/src/js/barcode_events.js index 7481cd43dd4..ee18c54aab7 100644 --- a/addons/barcodes/static/src/js/barcode_events.js +++ b/addons/barcodes/static/src/js/barcode_events.js @@ -1,210 +1,24 @@ -odoo.define('barcodes.BarcodeEvents', function(require) { -"use strict"; +/** @odoo-module **/ -var config = require('web.config'); -var core = require('web.core'); -var session = require('web.session'); +/** + * BarcodeEvents has been removed and replaced by the barcode service. + * + * This file is a temporary service to remap barcode events from new barcode + * service to core.bus (which was the purpose of BarcodeEvents). + * + * @TODO: remove this as soon as all barcode code is using new barcode service + */ -var BarcodeEvents = core.Class.extend({ - timeout: null, - key_pressed: {}, - buffered_key_events: [], - // Regexp to match a barcode input and extract its payload - // Note: to build in init() if prefix/suffix can be configured - regexp: /(.{3,})[\n\r\t]*/, - // By knowing the terminal character we can interpret buffered keys - // as a barcode as soon as it's encountered (instead of waiting x ms) - suffix: /[\n\r\t]+/, - // Keys from a barcode scanner are usually processed as quick as possible, - // but some scanners can use an intercharacter delay (we support <= 50 ms) - max_time_between_keys_in_ms: session.max_time_between_keys_in_ms || 55, - // To be able to receive the barcode value, an input must be focused. - // On mobile devices, this causes the virtual keyboard to open. - // Unfortunately it is not possible to avoid this behavior... - // To avoid keyboard flickering at each detection of a barcode value, - // we want to keep it open for a while (800 ms). - inputTimeOut: 800, +import { registry } from "@web/core/registry"; +import core from "web.core"; - init: function() { - this.__handler = _.bind(this.handler, this); - // Bind event handler once the DOM is loaded - // TODO: find a way to be active only when there are listeners on the bus - $(() => this.start()); - - // Mobile device detection - this.isChromeMobile = config.device.isMobileDevice && navigator.userAgent.match(/Chrome/i); +export const barcodeRemapperService = { + dependencies: ["barcode"], + start(env, { barcode }) { + barcode.bus.addEventListener("barcode_scanned", ev => { + const { barcode, target } = ev.detail; + core.bus.trigger('barcode_scanned', barcode, target); + }); }, - - getCurrentString() { - if (this.$barcodeInput) { - return this.$barcodeInput.val(); - } - return this.buffered_key_events.reduce(function(memo, e) { return memo + e.key }, ''); - }, - - handle_buffered_keys: function() { - let str = this.getCurrentString(); - var match = str.match(this.regexp); - - if (match) { - var barcode = match[1]; - - // Send the target in case there are several barcode widgets on the same page (e.g. - // registering the lot numbers in a stock picking) - core.bus.trigger('barcode_scanned', barcode, this.buffered_key_events[0].target); - - // Dispatch a barcode_scanned DOM event to elements that have barcode_events="true" set. - if (this.buffered_key_events[0].target.getAttribute("barcode_events") === "true") - $(this.buffered_key_events[0].target).trigger('barcode_scanned', barcode); - } - - this.buffered_key_events = []; - }, - - element_is_editable: function(element) { - return $(element).is('input,textarea,[contenteditable="true"]'); - }, - - // This checks that a keypress event is either ESC, TAB, an arrow - // key or a function key. This is Firefox specific, in Chrom{e,ium} - // keypress events are not fired for these types of keys, only - // keyup/keydown. - is_special_key: function(e) { - if (e.key === "ArrowLeft" || e.key === "ArrowRight" || - e.key === "ArrowUp" || e.key === "ArrowDown" || - e.key === "Escape" || e.key === "Tab" || - e.key === "Backspace" || e.key === "Delete" || - e.key === "Home" || e.key === "End" || - e.key === "PageUp" || e.key === "PageDown" || - e.key === "Shift" || - e.key === "Unidentified" || /F\d\d?/.test(e.key)) { - return true; - } else { - return false; - } - }, - - handler: function(e){ - // Don't catch non-printable keys for which Firefox triggers a keypress - if (this.is_special_key(e)) { - return; - } - // Don't catch keypresses which could have a UX purpose (like shortcuts) - if (e.ctrlKey || e.metaKey || e.altKey) { - return; - } - // Don't catch events targeting elements that are editable because we - // have no way of redispatching 'genuine' key events. Resent events - // don't trigger native event handlers of elements. So this means that - // our fake events will not appear in eg. an element. - if ((this.element_is_editable(e.target) && !$(e.target).data('enableBarcode')) && e.target.getAttribute("barcode_events") !== "true") { - return; - } - - // Catch and buffer the event - if (e.key !== "Enter") { - this.buffered_key_events.push(e); - } - - // Handle buffered keys immediately if the keypress marks the end - // of a barcode or after x milliseconds without a new keypress - clearTimeout(this.timeout); - if (String.fromCharCode(e.which).match(this.suffix)) { - this.handle_buffered_keys(); - } else { - this.timeout = setTimeout(_.bind(this.handle_buffered_keys, this), this.max_time_between_keys_in_ms); - } - }, - - /** - * Try to detect the barcode value by listening all keydown events: - * Checks if a dom element who may contains text value has the focus. - * If not, it's probably because these events are triggered by a barcode scanner. - * To be able to handle this value, a focused input will be created. - * - * This function also has the responsibility to detect the end of the barcode value. - * (1) In most of cases, an optional key (tab or enter) is sent to mark the end of the value. - * So, we direclty handle the value. - * (2) If no end key is configured, we have to calculate the delay between each keydowns. - * 'max_time_between_keys_in_ms' depends of the device and may be configured. - * Exceeded this timeout, we consider that the barcode value is entirely sent. - * - * @private - * @param {jQuery.Event} e keydown event - */ - _listenBarcodeScanner: function (e) { - if ($(document.activeElement).not('input:text, textarea, [contenteditable], ' + - '[type="email"], [type="number"], [type="password"], [type="tel"], [type="search"]').length) { - this.$barcodeInput.focus(); - } - if (this.$barcodeInput.is(":focus")) { - this.handler(e); - // if the barcode input doesn't receive keydown for a while, remove it. - this.__blurBarcodeInput(); - } - }, - - /** - * Removes the value and focus from the barcode input. - * If nothing happens, the focus will be lost and - * the virtual keyboard on mobile devices will be closed. - * - * @private - */ - _blurBarcodeInput: function () { - // Close the virtual keyboard on mobile browsers - // FIXME: actually we can't prevent keyboard from opening - this.$barcodeInput.val('').blur(); - }, - - start: function(){ - // Chrome Mobile isn't triggering keypress event. - // This is marked as Legacy in the DOM-Level-3 Standard. - // See: https://www.w3.org/TR/uievents/#legacy-keyboardevent-event-types - // This fix is only applied for Google Chrome Mobile but it should work for - // all other cases. - // In master, we could remove the behavior with keypress and only use keydown. - if (this.isChromeMobile) { - // Creates an input who will receive the barcode scanner value. - this.$barcodeInput = $('', { - name: 'barcode', - type: 'text', - css: { - 'position': 'fixed', - 'top': '50%', - 'transform': 'translateY(-50%)', - 'z-index': '-1', - 'opacity': '0', - }, - }); - // Avoid to show autocomplete for a non appearing input - this.$barcodeInput.attr('autocomplete', 'off'); - - this.__blurBarcodeInput = _.debounce(this._blurBarcodeInput, this.inputTimeOut); - - - $('body').append(this.$barcodeInput); - core.bus.on('barcode_scanned', null, () => this._blurBarcodeInput()); - - $('body').on("keydown", this._listenBarcodeScanner.bind(this)); - } else { - $('body').bind("keydown", this.__handler); - } - }, - - stop: function(){ - $('body').off("keydown", this.__handler); - }, -}); - -return { - /** Singleton that emits barcode_scanned events on core.bus */ - BarcodeEvents: new BarcodeEvents(), - /** - * List of barcode prefixes that are reserved for internal purposes - * @type Array - */ - ReservedBarcodePrefixes: ['O-CMD'], }; - -}); +registry.category("services").add("barcode_remapper", barcodeRemapperService); diff --git a/addons/barcodes/static/src/js/barcode_field.js b/addons/barcodes/static/src/js/barcode_field.js index dee98412975..a0105e4a21a 100644 --- a/addons/barcodes/static/src/js/barcode_field.js +++ b/addons/barcodes/static/src/js/barcode_field.js @@ -22,7 +22,7 @@ var FieldFloatScannable = basicFields.FieldFloat.extend({ _renderEdit: function () { var self = this; return Promise.resolve(this._super()).then(function () { - self.$input.data('enableBarcode', true); + self.$input[0].dataset.enableBarcode = true; }); }, diff --git a/addons/barcodes/static/src/js/barcode_form_view.js b/addons/barcodes/static/src/js/barcode_form_view.js index 7e3c197ed77..f050ec15cfe 100644 --- a/addons/barcodes/static/src/js/barcode_form_view.js +++ b/addons/barcodes/static/src/js/barcode_form_view.js @@ -1,7 +1,6 @@ odoo.define('barcodes.FormView', function (require) { "use strict"; -var BarcodeEvents = require('barcodes.BarcodeEvents'); // handle to trigger barcode on bus var concurrency = require('web.concurrency'); var core = require('web.core'); var Dialog = require('web.Dialog'); @@ -10,6 +9,8 @@ var FormRenderer = require('web.FormRenderer'); var _t = core._t; +const reservedBarcodePrefixes = ['O-CMD', 'O-BTN']; + FormController.include({ custom_events: _.extend({}, FormController.prototype.custom_events, { @@ -308,7 +309,7 @@ FormController.include({ _barcodeScanned: function (barcode, target) { var self = this; return this.barcodeMutex.exec(function () { - var prefixed = _.any(BarcodeEvents.ReservedBarcodePrefixes, + var prefixed = _.any(reservedBarcodePrefixes, function (reserved) {return barcode.indexOf(reserved) === 0;}); var hasCommand = false; var defs = []; @@ -491,6 +492,4 @@ FormRenderer.include({ } }); -BarcodeEvents.ReservedBarcodePrefixes.push('O-BTN'); - }); diff --git a/addons/barcodes/static/tests/barcode_mobile_tests.js b/addons/barcodes/static/tests/barcode_mobile_tests.js index c030af9ce64..ee59df19b60 100644 --- a/addons/barcodes/static/tests/barcode_mobile_tests.js +++ b/addons/barcodes/static/tests/barcode_mobile_tests.js @@ -1,21 +1,52 @@ -odoo.define('barcodes.barcode_mobile_tests', function () { +odoo.define('barcodes.barcode_mobile_tests', function (require) { "use strict"; - QUnit.module('Barcodes', {}, function () { + const {barcodeService} = require("@barcodes/barcode_service"); + const {barcodeRemapperService} = require("@barcodes/js/barcode_events"); + const { makeTestEnv } = require("@web/../tests/helpers/mock_env"); + const { registry } = require("@web/core/registry"); + var testUtils = require('web.test_utils'); + + const maxTimeBetweenKeysInMs = barcodeService.maxTimeBetweenKeysInMs; + const isMobileChrome = barcodeService.isMobileChrome; + var triggerEvent = testUtils.dom.triggerEvent; + + function triggerKeyDown(char, target = document.body) { + let keycode; + if (char === 'Enter') { + keycode = $.ui.keyCode.ENTER; + } else if (char === "Tab") { + keycode = $.ui.keyCode.TAB; + } else { + keycode = char.charCodeAt(0); + } + triggerEvent(target, 'keydown', { + key: char, + keyCode: keycode, + which: keycode, + }); + } + + QUnit.module('Barcodes', { + before() { + barcodeService.maxTimeBetweenKeysInMs = 0; + barcodeService.isMobileChrome = true; + registry.category("services").add("barcode", barcodeService, { force: true}); + // remove this one later + registry.category("services").add("barcode_remapper", barcodeRemapperService); + this.env = makeTestEnv(); + }, + after() { + barcodeService.maxTimeBetweenKeysInMs = maxTimeBetweenKeysInMs; + barcodeService.isMobileChrome = isMobileChrome; + }, + }, function () { QUnit.module('Barcodes Mobile'); QUnit.test('barcode field automatically focus behavior', function (assert) { assert.expect(10); - // Mock Chrome mobile environment - var barcodeEvents = odoo.__DEBUG__.services["barcodes.BarcodeEvents"].BarcodeEvents; - var __isChromeMobile = barcodeEvents.isChromeMobile; - barcodeEvents.isChromeMobile = true; - // Rebind keyboard events - barcodeEvents.stop(); - barcodeEvents.start(); - var $form = $( '
' + '' + @@ -34,12 +65,13 @@ odoo.define('barcodes.barcode_mobile_tests', function () { $('#qunit-fixture').append($form); // Some elements doesn't need to keep the focus - $('body').keydown(); + triggerKeyDown('a', document.body) assert.strictEqual(document.activeElement.name, 'barcode', "hidden barcode input should have the focus"); var $element = $form.find('select'); - $element.focus().keydown(); + $element.focus(); + triggerKeyDown('b', $element[0]); assert.strictEqual(document.activeElement.name, 'barcode', "hidden barcode input should have the focus"); @@ -49,7 +81,9 @@ odoo.define('barcodes.barcode_mobile_tests', function () { 'text', 'explicit_text']; for (var i = 0; i < keepFocusedElements.length; ++i) { $element = $form.find('input[name=' + keepFocusedElements[i] + ']'); - $element.focus().keydown(); + $element.focus(); + triggerKeyDown('c', $element[0]); + assert.strictEqual(document.activeElement, $element[0], "input " + keepFocusedElements[i] + " should keep focus"); } @@ -60,16 +94,12 @@ odoo.define('barcodes.barcode_mobile_tests', function () { "textarea should keep focus"); // contenteditable elements $element = $form.find('[contenteditable=true]'); - $element.focus().keydown(); + $element.focus(); + triggerKeyDown('d', $element[0]); assert.strictEqual(document.activeElement, $element[0], "contenteditable should keep focus"); $('#qunit-fixture').empty(); - barcodeEvents.isChromeMobile = __isChromeMobile; - // Rebind keyboard events - barcodeEvents.stop(); - barcodeEvents.start(); - document.querySelector('input[name=barcode]').remove(); }); }); diff --git a/addons/barcodes/static/tests/barcode_tests.js b/addons/barcodes/static/tests/barcode_tests.js index 1740a6321bc..19b7f3a9402 100644 --- a/addons/barcodes/static/tests/barcode_tests.js +++ b/addons/barcodes/static/tests/barcode_tests.js @@ -1,7 +1,10 @@ odoo.define('barcodes.tests', function (require) { "use strict"; -var barcodeEvents = require('barcodes.BarcodeEvents'); +const {barcodeService} = require("@barcodes/barcode_service"); +const {barcodeRemapperService} = require("@barcodes/js/barcode_events"); +const { makeTestEnv } = require("@web/../tests/helpers/mock_env"); +const { registry } = require("@web/core/registry"); var AbstractField = require('web.AbstractField'); var fieldRegistry = require('web.field_registry'); @@ -14,6 +17,8 @@ var triggerKeypressEvent = testUtils.dom.triggerKeypressEvent; var triggerEvent = testUtils.dom.triggerEvent; var core = require('web.core'); +const maxTimeBetweenKeysInMs = barcodeService.maxTimeBetweenKeysInMs; + function simulateBarCode(chars, target = document.body) { for (let char of chars) { let keycode; @@ -33,6 +38,16 @@ function simulateBarCode(chars, target = document.body) { } QUnit.module('Barcodes', { + before() { + barcodeService.maxTimeBetweenKeysInMs = 0; + registry.category("services").add("barcode", barcodeService, { force: true}); + // remove this one later + registry.category("services").add("barcode_remapper", barcodeRemapperService); + this.env = makeTestEnv(); + }, + after() { + barcodeService.maxTimeBetweenKeysInMs = maxTimeBetweenKeysInMs; + }, beforeEach: function () { this.data = { order: { @@ -195,8 +210,6 @@ QUnit.test('pager buttons', async function (assert) { QUnit.test('do no update form twice after a command barcode scanned', async function (assert) { assert.expect(7); - var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms; - barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = 0; testUtils.mock.patch(FormController, { update: function () { assert.step('update'); @@ -241,14 +254,10 @@ QUnit.test('do no update form twice after a command barcode scanned', async func assert.verifySteps(['update']); form.destroy(); - barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; testUtils.mock.unpatch(FormController); }); QUnit.test('widget field_float_scannable', async function (assert) { - var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms; - barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = 0; - this.data.product.records[0].int_field = 4; function _onBarcodeScanned (code) { @@ -304,16 +313,12 @@ QUnit.test('widget field_float_scannable', async function (assert) { assert.verifySteps([]); form.destroy(); - barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; core.bus.off('barcode_scanned', null, _onBarcodeScanned) }); QUnit.test('widget barcode_handler', async function (assert) { assert.expect(4); - var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms; - barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = 0; - this.data.product.fields.barcode_scanned = {string : "Scanned barcode", type: "char"}; this.data.product.onchanges = { barcode_scanned: function (obj) { @@ -355,15 +360,11 @@ QUnit.test('widget barcode_handler', async function (assert) { assert.verifySteps(['onchange'], "an onchange should have been done"); form.destroy(); - barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; }); QUnit.test('specification of widget barcode_handler', async function (assert) { assert.expect(5); - var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms; - barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = 0; - // Define a specific barcode_handler widget for this test case var TestBarcodeHandler = AbstractField.extend({ init: function () { @@ -434,7 +435,6 @@ QUnit.test('specification of widget barcode_handler', async function (assert) { await testUtils.form.clickSave(form); form.destroy(); - barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; delete fieldRegistry.map.test_barcode_handler; }); @@ -442,9 +442,6 @@ QUnit.test('specification of widget barcode_handler with keypress and notifyChan assert.expect(6); var done = assert.async(); - var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms; - barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = 0; - this.data.order.onchanges = { _barcode_scanned: function () {}, }; @@ -514,7 +511,6 @@ QUnit.test('specification of widget barcode_handler with keypress and notifyChan assert.verifySteps(['read', 'read']); form.destroy(); - barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; delete fieldRegistry.map.test_barcode_handler; done(); }); diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js index 57721805f41..008ebe38ba3 100644 --- a/addons/point_of_sale/static/src/js/Chrome.js +++ b/addons/point_of_sale/static/src/js/Chrome.js @@ -3,7 +3,6 @@ odoo.define('point_of_sale.Chrome', function(require) { const { loadCSS } = require('web.ajax'); const { useListener } = require("@web/core/utils/hooks"); - const { BarcodeEvents } = require('barcodes.BarcodeEvents'); const BarcodeParser = require('barcodes.BarcodeParser'); const PosComponent = require('point_of_sale.PosComponent'); const NumberBuffer = require('point_of_sale.NumberBuffer'); @@ -20,7 +19,6 @@ odoo.define('point_of_sale.Chrome', function(require) { onError, onMounted, onWillDestroy, - onWillUnmount, useExternalListener, useRef, useState, @@ -83,12 +81,6 @@ odoo.define('point_of_sale.Chrome', function(require) { $(window).off(); $('html').off(); $('body').off(); - // The above lines removed the bindings, but we really need them for the barcode - BarcodeEvents.start(); - }); - - onWillUnmount(() => { - BarcodeEvents.stop(); }); onWillDestroy(() => { diff --git a/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js b/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js index 3e868be320d..c82abfa2e79 100644 --- a/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js +++ b/addons/point_of_sale/static/src/js/Misc/NumberBuffer.js @@ -3,7 +3,7 @@ odoo.define('point_of_sale.NumberBuffer', function(require) { const { useListener } = require("@web/core/utils/hooks"); const { parse } = require('web.field_utils'); - const { BarcodeEvents } = require('barcodes.BarcodeEvents'); + const { barcodeService } = require('@barcodes/barcode_service'); const { _t } = require('web.core'); const { Gui } = require('point_of_sale.Gui'); @@ -162,7 +162,7 @@ odoo.define('point_of_sale.NumberBuffer', function(require) { this.config = config; this.decimalPoint = config.decimalPoint || this.defaultDecimalPoint; this.maxTimeBetweenKeys = this.config.useWithBarcode - ? BarcodeEvents.max_time_between_keys_in_ms + ? barcodeService.maxTimeBetweenKeysInMs : 0; } _onKeyboardInput(event) { From 5f15909c8a6e9f197f45681a0afd2d7286c5a107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 22 Jun 2022 10:48:08 +0000 Subject: [PATCH 3/7] [REF] barcodes: remove dead code Part-of: odoo/odoo#93387 --- .../static/src/js/barcode_form_view.js | 69 ---------------- addons/barcodes/static/tests/barcode_tests.js | 78 ------------------- 2 files changed, 147 deletions(-) diff --git a/addons/barcodes/static/src/js/barcode_form_view.js b/addons/barcodes/static/src/js/barcode_form_view.js index f050ec15cfe..9fa45c55d33 100644 --- a/addons/barcodes/static/src/js/barcode_form_view.js +++ b/addons/barcodes/static/src/js/barcode_form_view.js @@ -3,7 +3,6 @@ odoo.define('barcodes.FormView', function (require) { var concurrency = require('web.concurrency'); var core = require('web.core'); -var Dialog = require('web.Dialog'); var FormController = require('web.FormController'); var FormRenderer = require('web.FormRenderer'); @@ -193,14 +192,12 @@ FormController.include({ */ _barcodeStartListening: function () { core.bus.on('barcode_scanned', this, this._barcodeScanned); - core.bus.on('keypress', this, this._quantityListener); }, /** * @private */ _barcodeStopListening: function () { core.bus.off('barcode_scanned', this, this._barcodeScanned); - core.bus.off('keypress', this, this._quantityListener); }, /** * @private @@ -345,72 +342,6 @@ FormController.include({ }); }); }, - /** - * @private - * @param {KeyEvent} event - */ - _quantityListener: function (event) { - var character = String.fromCharCode(event.which); - - if (! $.contains(event.target, this.el)) { - return; - } - // only catch the event if we're not focused in - // another field and it's a number - if (!$(event.target).is('body, .modal') || !/[0-9]/.test(character)) { - return; - } - - var barcodeInfos = _.filter(this.activeBarcode, 'setQuantityWithKeypress'); - if (!barcodeInfos.length) { - return; - } - - if (!_.compact(_.pluck(barcodeInfos, 'candidate')).length) { - return this.displayNotification({ message: _t('Scan a barcode to set the quantity'), type: 'danger' }); - } - - for (var k in this.activeBarcode) { - if (this.activeBarcode[k].candidate) { - this._quantityOpenDialog(character, this.activeBarcode[k]); - } - } - }, - /** - * @private - * @param {string} character - * @param {Object} activeBarcode: options sent by the field who use barcode features - */ - _quantityOpenDialog: function (character, activeBarcode) { - var self = this; - var $content = $('
').append($('', {type: 'text', class: 'o_set_qty_input'})); - this.dialog = new Dialog(this, { - title: _t('Set quantity'), - buttons: [{text: _t('Select'), classes: 'btn-primary', close: true, click: function () { - var new_qty = this.$content.find('.o_set_qty_input').val(); - var record = self.model.get(self.handle); - return self._barcodeSelectedCandidate(activeBarcode.candidate, record, - self.current_barcode, activeBarcode, parseFloat(new_qty)) - .then(function () { - self.update({}, {reload: false}); - }); - }}, {text: _t('Discard'), close: true}], - $content: $content, - }); - this.dialog.opened().then(function () { - // This line set the value of the key which triggered the _set_quantity in the input - var $input = self.dialog.$('.o_set_qty_input').focus().val(character); - var $selectBtn = self.dialog.$footer.find('.btn-primary'); - $input.on('keypress', function (event){ - if (event.which === 13) { - event.preventDefault(); - $input.off(); - $selectBtn.click(); - } - }); - }); - this.dialog.open(); - }, }); diff --git a/addons/barcodes/static/tests/barcode_tests.js b/addons/barcodes/static/tests/barcode_tests.js index 19b7f3a9402..2609cdeb2dd 100644 --- a/addons/barcodes/static/tests/barcode_tests.js +++ b/addons/barcodes/static/tests/barcode_tests.js @@ -13,7 +13,6 @@ var FormView = require('web.FormView'); var testUtils = require('web.test_utils'); var createView = testUtils.createView; -var triggerKeypressEvent = testUtils.dom.triggerKeypressEvent; var triggerEvent = testUtils.dom.triggerEvent; var core = require('web.core'); @@ -438,83 +437,6 @@ QUnit.test('specification of widget barcode_handler', async function (assert) { delete fieldRegistry.map.test_barcode_handler; }); -QUnit.test('specification of widget barcode_handler with keypress and notifyChange', async function (assert) { - assert.expect(6); - var done = assert.async(); - - this.data.order.onchanges = { - _barcode_scanned: function () {}, - }; - - // Define a specific barcode_handler widget for this test case - var TestBarcodeHandler = AbstractField.extend({ - init: function () { - this._super.apply(this, arguments); - - this.trigger_up('activeBarcode', { - name: 'test', - fieldName: 'line_ids', - notifyChange: false, - setQuantityWithKeypress: true, - quantity: 'quantity', - commands: { - barcode: '_barcodeAddX2MQuantity', - } - }); - }, - }); - fieldRegistry.add('test_barcode_handler', TestBarcodeHandler); - - var form = await createView({ - View: FormView, - model: 'order', - data: this.data, - arch: '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '', - mockRPC: function (route, args) { - assert.step(args.method); - return this._super.apply(this, arguments); - }, - res_id: 1, - viewOptions: { - mode: 'edit', - }, - }); - simulateBarCode(['1','2','3','4','5','6','7','8','9','0','Enter']); - await testUtils.nextTick(); - // Quantity listener should open a dialog. - triggerKeypressEvent('5'); - await testUtils.nextTick(); - - setTimeout(async function () { - var keycode = $.ui.keyCode.ENTER; - - assert.strictEqual($('.modal .modal-body').length, 1, 'should open a modal with a quantity as input'); - assert.strictEqual($('.modal .modal-body .o_set_qty_input').val(), '5', 'the quantity by default in the modal shoud be 5'); - - $('.modal .modal-body .o_set_qty_input').val('7'); - await testUtils.nextTick(); - - $('.modal .modal-body .o_set_qty_input').trigger($.Event('keypress', {which: keycode, keyCode: keycode})); - await testUtils.nextTick(); - assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(1)').text(), '7', - "quantity checked should be 7"); - - assert.verifySteps(['read', 'read']); - - form.destroy(); - delete fieldRegistry.map.test_barcode_handler; - done(); - }); -}); QUnit.test('barcode_scanned only trigger error for active view', async function (assert) { assert.expect(2); From 325afc5bc66c67d50e5713b9fc9cab40f0ed8622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 20 Jun 2022 14:32:24 +0000 Subject: [PATCH 4/7] [IMP] barcode: introduce barcode handler service This provides a simple way to react to some specific scans. This commit implements one handler for 'O-BTN' scans Part-of: odoo/odoo#93387 --- .../barcodes/static/src/barcode_handlers.js | 30 +++++++ .../static/src/js/barcode_form_view.js | 82 +------------------ addons/barcodes/static/tests/barcode_tests.js | 50 +---------- 3 files changed, 33 insertions(+), 129 deletions(-) create mode 100644 addons/barcodes/static/src/barcode_handlers.js diff --git a/addons/barcodes/static/src/barcode_handlers.js b/addons/barcodes/static/src/barcode_handlers.js new file mode 100644 index 00000000000..84b6ffd77d6 --- /dev/null +++ b/addons/barcodes/static/src/barcode_handlers.js @@ -0,0 +1,30 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { getVisibleElements } from "@web/core/utils/ui"; + +export const barcodeAutoClick = { + dependencies: ["ui", "barcode"], + start(env, { ui, barcode }) { + + barcode.bus.addEventListener("barcode_scanned", (ev) => { + const barcode = ev.detail.barcode; + if (!barcode.startsWith("O-BTN.")) { + return; + } + let targets = []; + try { + // the scanned barcode could be anything, and could crash the queryselectorall + // function + targets = getVisibleElements(ui.activeElement, `[barcode_trigger=${barcode.slice(6)}]`); + } catch (_e) { + console.warn(`Barcode '${barcode}' is not valid`) + } + for (let elem of targets) { + elem.click(); + } + }); + } +}; + +registry.category("services").add("barcode_autoclick", barcodeAutoClick); diff --git a/addons/barcodes/static/src/js/barcode_form_view.js b/addons/barcodes/static/src/js/barcode_form_view.js index 9fa45c55d33..f2a2c022f54 100644 --- a/addons/barcodes/static/src/js/barcode_form_view.js +++ b/addons/barcodes/static/src/js/barcode_form_view.js @@ -4,11 +4,10 @@ odoo.define('barcodes.FormView', function (require) { var concurrency = require('web.concurrency'); var core = require('web.core'); var FormController = require('web.FormController'); -var FormRenderer = require('web.FormRenderer'); var _t = core._t; -const reservedBarcodePrefixes = ['O-CMD', 'O-BTN']; +const reservedBarcodePrefixes = ['O-CMD']; FormController.include({ @@ -344,83 +343,4 @@ FormController.include({ }, }); - -FormRenderer.include({ - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - /** - * trigger_up 'activeBarcode' to Add barcode event handler - * - * @private - * @param {jQueryElement} $button - * @param {Object} node - */ - _barcodeButtonHandler: function ($button, node) { - var commands = {}; - commands.barcode = function () {return Promise.resolve();}; - commands['O-BTN.' + node.attrs.barcode_trigger] = function () { - if (!$button.hasClass('o_invisible_modifier')) { - $button.click(); - } - return Promise.resolve(); - }; - var name = node.attrs.name; - if (node.attrs.string) { - name = name + '_' + node.attrs.string; - } - - this.trigger_up('activeBarcode', { - name: name, - commands: commands - }); - }, - /** - * Add barcode event handler - * - * @override - * @private - * @param {Object} node - * @returns {jQueryElement} - */ - _renderHeaderButton: function (node) { - var $button = this._super.apply(this, arguments); - if (node.attrs.barcode_trigger) { - this._barcodeButtonHandler($button, node); - } - return $button; - }, - /** - * Add barcode event handler - * - * @override - * @private - * @param {Object} node - * @returns {jQueryElement} - */ - _renderStatButton: function (node) { - var $button = this._super.apply(this, arguments); - if (node.attrs.barcode_trigger) { - this._barcodeButtonHandler($button, node); - } - return $button; - }, - /** - * Add barcode event handler - * - * @override - * @private - * @param {Object} node - * @returns {jQueryElement} - */ - _renderTagButton: function (node) { - var $button = this._super.apply(this, arguments); - if (node.attrs.barcode_trigger) { - this._barcodeButtonHandler($button, node); - } - return $button; - } -}); - }); diff --git a/addons/barcodes/static/tests/barcode_tests.js b/addons/barcodes/static/tests/barcode_tests.js index 2609cdeb2dd..0cc2eb174b1 100644 --- a/addons/barcodes/static/tests/barcode_tests.js +++ b/addons/barcodes/static/tests/barcode_tests.js @@ -2,6 +2,7 @@ odoo.define('barcodes.tests', function (require) { "use strict"; const {barcodeService} = require("@barcodes/barcode_service"); +const {barcodeAutoClick} = require("@barcodes/barcode_handlers"); const {barcodeRemapperService} = require("@barcodes/js/barcode_events"); const { makeTestEnv } = require("@web/../tests/helpers/mock_env"); const { registry } = require("@web/core/registry"); @@ -40,6 +41,7 @@ QUnit.module('Barcodes', { before() { barcodeService.maxTimeBetweenKeysInMs = 0; registry.category("services").add("barcode", barcodeService, { force: true}); + registry.category("services").add("barcode_autoclick", barcodeAutoClick, { force: true}); // remove this one later registry.category("services").add("barcode_remapper", barcodeRemapperService); this.env = makeTestEnv(); @@ -437,52 +439,4 @@ QUnit.test('specification of widget barcode_handler', async function (assert) { delete fieldRegistry.map.test_barcode_handler; }); -QUnit.test('barcode_scanned only trigger error for active view', async function (assert) { - assert.expect(2); - - this.data.order_line.fields._barcode_scanned = {string: 'Barcode scanned', type: 'char'}; - - var form = await createView({ - View: FormView, - model: 'order', - data: this.data, - arch: '
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '', - archs: { - "order_line,false,form": - '
' + - '' + - '' + - '', - }, - res_id: 1, - services: { - notification: { - notify: function (params) { - assert.step(params.type); - } - }, - }, - viewOptions: { - mode: 'edit', - }, - }); - - await testUtils.dom.click(form.$('.o_data_row:first')); - - // We do not trigger on the body since modal and - // form view are both inside it. - simulateBarCode(['O','-','B','T','N','.','c','a','n','c','e','l','Enter'], $('.modal')); - await testUtils.nextTick(); - assert.verifySteps(['danger'], "only one event should be triggered"); - form.destroy(); -}); }); From d621d15d22a24271103d9009be9ef4c7fed2c149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Dieudonn=C3=A9?= Date: Thu, 23 Jun 2022 07:48:15 +0000 Subject: [PATCH 5/7] [REF] barcodes: removes blur functions As we now use 'inputmode=none' on the barcode field, the mobile virtual keyboard doesn't open anymore. So, there is no need to have to blur barcode input after receiving a value. There is no issue to keep the focus on this field. We only did it to close the virtual keyboard as soon as they pop up after each scan. Part-of: odoo/odoo#93387 --- addons/barcodes/static/src/barcode_service.js | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/addons/barcodes/static/src/barcode_service.js b/addons/barcodes/static/src/barcode_service.js index ccb1e2c224f..d3aec1c096c 100644 --- a/addons/barcodes/static/src/barcode_service.js +++ b/addons/barcodes/static/src/barcode_service.js @@ -43,14 +43,6 @@ export const barcodeService = { const endRegexp = /[\n\r\t]+/; const barcodeRegexp = /(.{3,})[\n\r\t]*/; let timeout = null; - let blurTimeout = null; - - // To be able to receive the barcode value, an input must be focused. - // On mobile devices, this causes the virtual keyboard to open. - // Unfortunately it is not possible to avoid this behavior... - // To avoid keyboard flickering at each detection of a barcode value, - // we want to keep it open for a while (800 ms). - const inputTimeOut = 8000; let bufferedBarcode = ""; let currentTarget = null; @@ -74,7 +66,7 @@ export const barcodeService = { handleBarcode(barcode, currentTarget); } if (barcodeInput) { - blurBarcodeInput(); + barcodeInput.value = ""; } bufferedBarcode = ""; currentTarget = null; @@ -121,17 +113,6 @@ export const barcodeService = { barcodeInput.focus(); } keydownHandler(ev); - clearTimeout(blurTimeout); - // if the barcode input doesn't receive keydown for a while, remove it. - blurTimeout = setTimeout(() => blurBarcodeInput(), inputTimeOut); - } - - function blurBarcodeInput() { - barcodeInput.value = ""; - // fixme: blurring the input should not be necessary, but for some - // reason, doing it fix some weird scrolling issue in a mobile (non - // native) device. - barcodeInput.blur(); } whenReady(() => { From f7ff35dad06ba0bfabf752797816d159a0ed133c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Dieudonn=C3=A9?= Date: Mon, 27 Jun 2022 07:52:22 +0000 Subject: [PATCH 6/7] [FIX] barcodes: Allows 'tab' as end character Before this commit 'tab' was escaped because isSpecialKey was true. This was strange because this key was defined in 'endRegexp'. So, there were only 'enter' key who worked as a barcode end key. Note that it wasn't a big deal if 'tab' was used because checkBarcode was executed anyway after a timeout. Now the barcode value is directly checked if the strind ends with 'enter' or 'tab' (by-passing this timeout). Part-of: odoo/odoo#93387 --- addons/barcodes/static/src/barcode_service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/barcodes/static/src/barcode_service.js b/addons/barcodes/static/src/barcode_service.js index d3aec1c096c..821a33e590c 100644 --- a/addons/barcodes/static/src/barcode_service.js +++ b/addons/barcodes/static/src/barcode_service.js @@ -9,7 +9,7 @@ const { EventBus, whenReady } = owl; function isSpecialKey(key) { return (key === "ArrowLeft" || key === "ArrowRight" || key === "ArrowUp" || key === "ArrowDown" || - key === "Escape" || key === "Tab" || + key === "Escape" || key === "Backspace" || key === "Delete" || key === "Home" || key === "End" || key === "PageUp" || key === "PageDown" || From a8312703f633cbdb6127e89ee803a50bb4c6a481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Dieudonn=C3=A9?= Date: Mon, 27 Jun 2022 09:17:00 +0000 Subject: [PATCH 7/7] [REF] barcodes: simplify isSpecialKey method Note that more 'non printable keys' are escaped now, like 'Home'. This seems like a bit naive solution but it avoids having to list every key. The only issue we might have is if a device sends the whole value in the 'key' property directly without sending sequentially several keydown events. As a reminder, 'ctrlKey', 'metaKey', 'altKey' properties are used because we also want to ignore keys pressed while this modifier is active. For example, if you press CTRL+e, we want to ignore 'CTRL' AND 'e'. Useful references: https://w3c.github.io/uievents/tools/key-event-viewer.html Part-of: odoo/odoo#93387 --- addons/barcodes/static/src/barcode_service.js | 38 ++++++------------- .../static/tests/barcode_mobile_tests.js | 1 - addons/barcodes/static/tests/barcode_tests.js | 1 - 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/addons/barcodes/static/src/barcode_service.js b/addons/barcodes/static/src/barcode_service.js index 821a33e590c..354f295b7a9 100644 --- a/addons/barcodes/static/src/barcode_service.js +++ b/addons/barcodes/static/src/barcode_service.js @@ -6,16 +6,6 @@ import { session } from "@web/session"; const { EventBus, whenReady } = owl; -function isSpecialKey(key) { - return (key === "ArrowLeft" || key === "ArrowRight" || - key === "ArrowUp" || key === "ArrowDown" || - key === "Escape" || - key === "Backspace" || key === "Delete" || - key === "Home" || key === "End" || - key === "PageUp" || key === "PageDown" || - key === "Shift" || /F\d\d?/.test(key)); -} - function isEditable(element) { return element.matches('input,textarea,[contenteditable="true"]'); } @@ -40,8 +30,6 @@ export const barcodeService = { start() { const bus = new EventBus(); - const endRegexp = /[\n\r\t]+/; - const barcodeRegexp = /(.{3,})[\n\r\t]*/; let timeout = null; let bufferedBarcode = ""; @@ -60,10 +48,8 @@ export const barcodeService = { */ function checkBarcode() { const str = barcodeInput ? barcodeInput.value : bufferedBarcode; - const match = str.match(barcodeRegexp); - if (match) { - const barcode = match[1]; - handleBarcode(barcode, currentTarget); + if (str.length >= 3) { + handleBarcode(str, currentTarget); } if (barcodeInput) { barcodeInput.value = ""; @@ -73,12 +59,14 @@ export const barcodeService = { } function keydownHandler(ev) { - // Don't catch non-printable keys for which Firefox triggers a keypress - if (isSpecialKey(ev.key)) { - return; - } - // Don't catch keypresses which could have a UX purpose (like shortcuts) - if (ev.ctrlKey || ev.metaKey || ev.altKey) { + // Ignore 'Shift', 'Escape', 'Backspace', 'Insert', 'Delete', 'Home', 'End', Arrow*, F*, Page*, ... + // ctrl, meta and alt are often used for UX purpose (like shortcuts) + // Note: shiftKey is not ignored because it can be used by some barcode scanner for digits. + const isSpecialKey = ev.key.length > 1 || ev.ctrlKey || ev.metaKey || ev.altKey; + const isEndCharacter = ev.key.match(/(Enter|Tab)/); + + // Don't catch non-printable keys except 'enter' and 'tab' + if (isSpecialKey && !isEndCharacter) { return; } @@ -93,13 +81,11 @@ export const barcodeService = { return; } - if (ev.key !== "Enter" && ev.key !== "Unidentified") { - bufferedBarcode += ev.key; - } clearTimeout(timeout); - if (String.fromCharCode(ev.which).match(endRegexp)) { + if (isEndCharacter) { checkBarcode(); } else { + bufferedBarcode += ev.key; timeout = setTimeout(checkBarcode, barcodeService.maxTimeBetweenKeysInMs); } } diff --git a/addons/barcodes/static/tests/barcode_mobile_tests.js b/addons/barcodes/static/tests/barcode_mobile_tests.js index ee59df19b60..b2a27ceb94c 100644 --- a/addons/barcodes/static/tests/barcode_mobile_tests.js +++ b/addons/barcodes/static/tests/barcode_mobile_tests.js @@ -23,7 +23,6 @@ odoo.define('barcodes.barcode_mobile_tests', function (require) { triggerEvent(target, 'keydown', { key: char, keyCode: keycode, - which: keycode, }); } diff --git a/addons/barcodes/static/tests/barcode_tests.js b/addons/barcodes/static/tests/barcode_tests.js index 0cc2eb174b1..de86cf10e70 100644 --- a/addons/barcodes/static/tests/barcode_tests.js +++ b/addons/barcodes/static/tests/barcode_tests.js @@ -32,7 +32,6 @@ function simulateBarCode(chars, target = document.body) { triggerEvent(target, 'keydown', { key: char, keyCode: keycode, - which: keycode, }); } }