From e654b32bc1ffa1e3593e88f4e380e863683ec685 Mon Sep 17 00:00:00 2001 From: Bruno Boi Date: Wed, 2 Sep 2020 13:49:34 +0000 Subject: [PATCH] [IMP] web: use user's decimal separator for numeric fields Until now, the numpad decimal key was not properly handled for some keyboard layouts. This commit will ensure that when this key will get pressed inside a numeric field, the user's display language decimal separator will get inserted. closes odoo/odoo#56962 Taskid: 1913999 Signed-off-by: Lucas Perais (lpe) --- addons/web/static/src/js/core/utils.js | 14 +++ .../web/static/src/js/fields/basic_fields.js | 56 ++++++++++-- .../static/tests/fields/basic_fields_tests.js | 85 +++++++++++++++++++ 3 files changed, 149 insertions(+), 6 deletions(-) diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js index a5a5f4eb465..80270f59a1f 100644 --- a/addons/web/static/src/js/core/utils.js +++ b/addons/web/static/src/js/core/utils.js @@ -500,6 +500,20 @@ var utils = { isComponent: function (value) { return value.prototype instanceof owl.Component; }, + /** + * Checks if a keyboard event concerns + * the numpad decimal separator key. + * + * Some countries may emit a comma instead + * of a period when this key get pressed. + * More info: https://www.iso.org/schema/isosts/v1.0/doc/n-cdf0.html + * + * @param {KeyboardEvent} ev + * @returns {boolean} + */ + isNumpadDecimalSeparatorKey(ev) { + return ['.', ','].includes(ev.key) && ev.code === 'NumpadDecimal'; + }, /** * Returns whether the given anchor is valid. * diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index d7dbed4db38..5d18a22c328 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -245,6 +245,15 @@ var InputField = DebouncedField.extend({ _getValue: function () { return this.$input.val(); }, + /** + * By default this only calls a debounced method to notify the outside world + * of the changes if the actual value is not the same than the previous one. + * @see _doDebouncedAction + */ + _notifyChanges() { + this.isDirty = !this._isLastSetValue(this.$input.val()); + this._doDebouncedAction(); + }, /** * Formats an input element for edit mode. This is in a separate function so * extending widgets can use it on their input without having input as tagName. @@ -319,15 +328,13 @@ var InputField = DebouncedField.extend({ this.lastChangeEvent = event; }, /** - * Called when the user is typing text -> By default this only calls a - * debounced method to notify the outside world of the changes. - * @see _doDebouncedAction + * Called when the user is typing text + * @see _notifyChanges * * @private */ - _onInput: function () { - this.isDirty = !this._isLastSetValue(this.$input.val()); - this._doDebouncedAction(); + _onInput() { + this._notifyChanges(); }, /** * Stops the left/right navigation move event if the cursor is not at the @@ -508,6 +515,43 @@ var NumericField = InputField.extend({ } return this._super(value, options); }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Replace the decimal separator of the numpad decimal key + * by the decimal separator from the user's language setting. + * + * @private + * @param {OdooEvent} ev + */ + _onKeydown(ev) { + const kbdEvt = ev.originalEvent; + if (kbdEvt && utils.isNumpadDecimalSeparatorKey(kbdEvt)) { + const inputField = this.$input[0]; + const curVal = inputField.value; + const from = inputField.selectionStart; + const to = inputField.selectionEnd; + const point = _t.database.parameters.decimal_point; + + // Make sure the correct decimal separator + // from the user's settings is inserted + inputField.value = curVal.slice(0, from) + point + curVal.slice(to); + + // Put the user caret at the right place + inputField.selectionStart = inputField.selectionEnd = from + point.length; + + // Tell the world we made some changes and + // return preventing event default behaviour. + this._notifyChanges(); + kbdEvt.preventDefault(); + return; + } + + return this._super(...arguments); + }, }); var FieldChar = InputField.extend(TranslatableFieldMixin, { diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 65df3a5af19..62e30fe285f 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -479,6 +479,91 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.module('FieldNumeric'); + + QUnit.test('numeric field: fields with keydown on numpad decimal key', async function (assert) { + assert.expect(6); + + this.data.partner.fields.float_factor_field = { string: "Float Factor", type: 'float_factor' }; + this.data.partner.records[0].float_factor_field = 9.99; + + this.data.partner.fields.float_time_field = { string: "Float Time", type: 'float_time' }; + this.data.partner.records[0].float_time_field = 9.99; + + this.data.partner.fields.monetary = { string: "Monetary", type: 'monetary' }; + this.data.partner.records[0].monetary = 9.99; + this.data.partner.records[0].currency_id = 1; + + this.data.partner.fields.percentage = { string: "Percentage", type: 'percentage' }; + this.data.partner.records[0].percentage = .99; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + + + + `, + res_id: 1, + translateParameters: { + decimal_point: "🇧🇪", + }, + }); + + // Record edit mode + await testUtilsDom.click(form.el.querySelector('.o_form_button_edit')); + + // Get all inputs + const floatFactorField = form.el.querySelector('.o_input[name="float_factor_field"]'); + const floatInput = form.el.querySelector('.o_input[name="qux"]'); + const floatTimeInput = form.el.querySelector('.o_input[name="float_time_field"]'); + const integerInput = form.el.querySelector('.o_input[name="int_field"]'); + const monetaryInput = form.el.querySelector('.o_input[name="monetary"]'); + const percentageInput = form.el.querySelector('.o_input[name="percentage"]'); + + // Dispatch numpad "dot" and numpad "comma" keydown events to all inputs and check + // Numpad "comma" is specific to some countries (Brazil...) + floatFactorField.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: '.' })); + floatFactorField.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: ',' })); + await testUtils.nextTick(); + assert.ok(floatFactorField.value.endsWith('🇧🇪🇧🇪')); + + floatInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: '.' })); + floatInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: ',' })); + await testUtils.nextTick(); + assert.ok(floatInput.value.endsWith('🇧🇪🇧🇪')); + + floatTimeInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: '.' })); + floatTimeInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: ',' })); + await testUtils.nextTick(); + assert.ok(floatTimeInput.value.endsWith('🇧🇪🇧🇪')); + + integerInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: '.' })); + integerInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: ',' })); + await testUtils.nextTick(); + assert.ok(integerInput.value.endsWith('🇧🇪🇧🇪')); + + monetaryInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: '.' })); + monetaryInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: ',' })); + await testUtils.nextTick(); + assert.ok(monetaryInput.querySelector('input.o_input') .value.endsWith('🇧🇪🇧🇪')); + + percentageInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: '.' })); + percentageInput.dispatchEvent(new KeyboardEvent('keydown', { code: 'NumpadDecimal', key: ',' })); + await testUtils.nextTick(); + assert.ok(percentageInput.querySelector('input.o_input').value.endsWith('🇧🇪🇧🇪')); + + form.destroy(); + }); + QUnit.module('FieldFloat'); QUnit.test('float field when unset', async function (assert) {