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) {