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/barcode_service.js b/addons/barcodes/static/src/barcode_service.js new file mode 100644 index 00000000000..354f295b7a9 --- /dev/null +++ b/addons/barcodes/static/src/barcode_service.js @@ -0,0 +1,120 @@ +/** @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 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(); + let timeout = null; + + 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; + if (str.length >= 3) { + handleBarcode(str, currentTarget); + } + if (barcodeInput) { + barcodeInput.value = ""; + } + bufferedBarcode = ""; + currentTarget = null; + } + + function keydownHandler(ev) { + // 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; + } + + 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; + } + + clearTimeout(timeout); + if (isEndCharacter) { + checkBarcode(); + } else { + bufferedBarcode += ev.key; + 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); + } + + 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 6f2f678506c..ee18c54aab7 100644 --- a/addons/barcodes/static/src/js/barcode_events.js +++ b/addons/barcodes/static/src/js/barcode_events.js @@ -1,300 +1,24 @@ -odoo.define('barcodes.BarcodeEvents', function(require) { -"use strict"; +/** @odoo-module **/ -var config = require('web.config'); -var core = require('web.core'); -var mixins = require('web.mixins'); -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 + */ +import { registry } from "@web/core/registry"; +import core from "web.core"; -// 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, { - 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, - - 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)); - - // 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', - }, +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); }); - // Avoid to show autocomplete for a non appearing input - this.$barcodeInput.attr('autocomplete', 'off'); - - this.__blurBarcodeInput = _.debounce(this._blurBarcodeInput, this.inputTimeOut); }, - - handle_buffered_keys: function() { - var str = this.buffered_key_events.reduce(function(memo, e) { return memo + String.fromCharCode(e.which) }, ''); - 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); - } 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"]'); - }, - - // 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 === "Unidentified" || /F\d\d?/.test(e.key)) { - return true; - } else { - return false; - } - }, - - // 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)) - 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) - 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 - this.buffered_key_events.push(e); - e.preventDefault(); - e.stopImmediatePropagation(); - - // 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) { - $('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); - } - // 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 - * 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(prevent_key_repeat){ - // 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) { - $('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); - } - }, - - stop: function(){ - $('body').off("keypress", this.__handler); - $('body').off("keydown", this.__keydown_handler); - $('body').off('keyup', this.__keyup_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 b5786f8a161..a0105e4a21a 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 //-------------------------------------------------------------------------- @@ -27,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; }); }, @@ -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/src/js/barcode_form_view.js b/addons/barcodes/static/src/js/barcode_form_view.js index 7e3c197ed77..f2a2c022f54 100644 --- a/addons/barcodes/static/src/js/barcode_form_view.js +++ b/addons/barcodes/static/src/js/barcode_form_view.js @@ -1,15 +1,14 @@ 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'); var FormController = require('web.FormController'); -var FormRenderer = require('web.FormRenderer'); var _t = core._t; +const reservedBarcodePrefixes = ['O-CMD']; + FormController.include({ custom_events: _.extend({}, FormController.prototype.custom_events, { @@ -192,14 +191,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 @@ -308,7 +305,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 = []; @@ -344,153 +341,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(); - }, }); - -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; - } -}); - -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..b2a27ceb94c 100644 --- a/addons/barcodes/static/tests/barcode_mobile_tests.js +++ b/addons/barcodes/static/tests/barcode_mobile_tests.js @@ -1,21 +1,51 @@ -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, + }); + } + + 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 +64,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 +80,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 +93,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 8ba4b92c3a7..de86cf10e70 100644 --- a/addons/barcodes/static/tests/barcode_tests.js +++ b/addons/barcodes/static/tests/barcode_tests.js @@ -1,7 +1,11 @@ odoo.define('barcodes.tests', function (require) { "use strict"; -var barcodeEvents = require('barcodes.BarcodeEvents'); +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"); var AbstractField = require('web.AbstractField'); var fieldRegistry = require('web.field_registry'); @@ -10,9 +14,40 @@ 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'); + +const maxTimeBetweenKeysInMs = barcodeService.maxTimeBetweenKeysInMs; + +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, + }); + } +} 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(); + }, + after() { + barcodeService.maxTimeBetweenKeysInMs = maxTimeBetweenKeysInMs; + }, beforeEach: function () { this.data = { order: { @@ -39,6 +74,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 +116,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 +143,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 +188,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'); @@ -174,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'); @@ -207,34 +241,29 @@ 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) 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 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 +272,7 @@ QUnit.test('widget field_float_scannable', async function (assert) { arch: '' + '' + '' + + '' + '', mockRPC: function (route, args) { if (args.method === 'onchange') { @@ -266,42 +296,29 @@ 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(); + 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) { @@ -335,7 +352,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"); @@ -343,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 () { @@ -404,17 +417,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"); @@ -422,146 +435,7 @@ 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; }); -QUnit.test('specification of widget barcode_handler with keypress and notifyChange', async function (assert) { - 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 () {}, - }; - - // 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', - }, - }); - _.each(['1','2','3','4','5','6','7','8','9','0','Enter'], triggerKeypressEvent); - 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(); - barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; - delete fieldRegistry.map.test_barcode_handler; - done(); - }); -}); -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. - 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); - await testUtils.nextTick(); - assert.verifySteps(['danger'], "only one event should be triggered"); - form.destroy(); -}); }); 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) {