/* Copyright 2019 GRAP - Quentin DUPONT * Copyright 2020 Tecnativa - Alexandre Díaz * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */ odoo.define("web_widget_numeric_step.field", function (require) { "use strict"; const field_utils = require("web.field_utils"); const Registry = require("web.field_registry"); const FieldFloat = require("web.basic_fields").FieldFloat; const NumericStep = FieldFloat.extend({ template: "web_widget_numeric_step", className: "o_field_numeric_step o_field_number", events: _.extend({}, _.omit(FieldFloat.prototype.events, ["change", "input"]), { "mousedown .btn_numeric_step": "_onStepMouseDown", "touchstart .btn_numeric_step": "_onStepMouseDown", "click .btn_numeric_step": "_onStepClick", "wheel .input_numeric_step": "_onWheel", "keydown .input_numeric_step": "_onKeyDown", "change .input_numeric_step": "_onChange", "input .input_numeric_step": "_onInput", "focusin .input_numeric_step": "_onFocusIn", "focusout .widget_numeric_step": "_onFocusOut", }), supportedFieldTypes: ["float", "integer"], // Values in milliseconds used for mouse down smooth speed feature DEF_CLICK_DELAY: 400, MIN_DELAY: 50, SUBSTRACT_DELAY_STEP: 25, DELAY_THROTTLE_CHANGE: 200, /** * @override */ init: function () { this._super.apply(this, arguments); // Widget config let max_val = this.nodeOptions.max; let min_val = this.nodeOptions.min; if ( !_.isUndefined(min_val) && !_.isUndefined(max_val) && min_val > max_val ) { min_val = this.nodeOptions.max; max_val = this.nodeOptions.min; } this._config = { step: Number(this.nodeOptions.step) || 1, min: Number(min_val), max: Number(max_val), autoSelect: this.nodeOptions.auto_select, addClasses: this.nodeOptions.add_class, removeClasses: this.nodeOptions.remove_class, }; this._lazyOnChangeTrigger = _.debounce( () => this.$input.trigger("change"), this.DELAY_THROTTLE_CHANGE ); this._auto_step_interval = false; }, /** * Add global events listeners * * @override */ start: function () { this._click_delay = this.DEF_CLICK_DELAY; this._autoStep = false; return this._super.apply(this, arguments).then(() => { document.addEventListener("mouseup", this._onMouseUp.bind(this), false); document.addEventListener( "touchend", this._onMouseUp.bind(this), false ); }); }, /** * Transform database value to usable widget value * * @override */ _formatValue: function (value) { if (this.mode === "edit") { return this._sanitizeNumberValue(value); } return this._super.apply(this, arguments); }, /** * Transform widget value to usable database value * * @override */ _parseValue: function () { const parsedVal = this._super.apply(this, arguments); if (this.mode === "edit") { return Number(parsedVal) || 0; } return parsedVal; }, /** * Adds HTML attributes to the input * * @override */ _prepareInput: function () { const result = this._super.apply(this, arguments); this.$input.attr(_.pick(this.nodeOptions, ["placeholder"])); // InputField hard set the input type to 'text' or 'password', // we force it again to be 'tel'. // The widget uses 'tel' type because offers a good layout on // mobiles and can accept alphanumeric characters. // The bad news is that require implement all good 'number' type // features like the minus and plus buttons, steps, min and max... // Perhaps in a near future this can be improved to have the best of // two types without hacky developments. this.$input.attr("type", "tel"); return result; }, /** * Select the proper widget input * * @override */ _renderEdit: function () { _.defer(() => this.$el .parents("td.o_numeric_step_cell") .addClass("numeric_step_editing_cell") ); this._prepareInput(this.$el.find("input.input_numeric_step")); if (this._config.addClasses) { this.$input.addClass(this._config.addClasses); } if (this._config.removeClasses) { this.$input.removeClass(this._config.removeClasses); } }, /** * Resets the content to the formated value in readonly mode. * * @override */ _renderReadonly: function () { this.$el .parents("td.numeric_step_editing_cell") .removeClass("numeric_step_editing_cell"); this._super.apply(this, arguments); }, /** * Increase/Decrease widget input value * * @param {String} mode can be "plus" or "minus" */ _doStep: function (mode) { let cval = 0; try { const field = this.record.fields[this.name]; cval = field_utils.parse[field.type](this.$input.val()); } catch (e) { cval = this.value; mode = false; // Only set the value in this case } if (mode === "plus") { cval += this._config.step; } else if (mode === "minus") { cval -= this._config.step; } const nval = this._sanitizeNumberValue(cval); if (nval !== this.lastSetValue || !mode) { this.$input.val(nval); // Every time that user update the value we must trigger an // onchange method. this._lazyOnChangeTrigger(); } }, /** * @private */ _clearStepInterval: function () { clearTimeout(this._auto_step_interval); this._auto_step_interval = false; this._click_delay = this.DEF_CLICK_DELAY; }, // Handle Events /** * @private * @param {MouseClickEvent} ev */ _onStepClick: function (ev) { if (!this._autoStep) { const mode = ev.target.dataset.mode; this._doStep(mode); } this._autoStep = false; }, /** * @private * @param {MouseClickEvent} ev */ _onStepMouseDown: function (ev) { if (ev.button === 0 && !this._auto_step_interval) { this._auto_step_interval = setTimeout( this._whileMouseDown.bind(this, ev), this._click_delay ); } }, /** * Auto select all content when user enters into fields with this * widget. * * @private */ _onFocusIn: function () { if (this._config.autoSelect) { this.$input.select(); } }, /** * @private * @param {FocusoutEvent} ev */ _onFocusOut: function () { if (this._auto_step_interval) { this._clearStepInterval(); } }, /** * @private */ _onMouseUp: function () { this._clearStepInterval(); }, /** * @private * @param {MouseClickEvent} ev */ _whileMouseDown: function (ev) { this._autoStep = true; const mode = ev.target.dataset.mode; this._doStep(mode); if (this._click_delay > this.MIN_DELAY) { this._click_delay -= this.SUBSTRACT_DELAY_STEP; } this._auto_step_interval = false; this._onStepMouseDown(ev); }, /** * Enable mouse wheel support * * @param {WheelEvent} ev */ _onWheel: function (ev) { ev.preventDefault(); if (ev.originalEvent.deltaY > 0) { this._doStep("minus"); } else { this._doStep("plus"); } }, /** * Enable keyboard arrows support * * @param {KeyEvent} ev */ _onKeyDown: function (ev) { if (ev.keyCode === $.ui.keyCode.UP) { this._doStep("plus"); } else if (ev.keyCode === $.ui.keyCode.DOWN) { this._doStep("minus"); } }, /** * Sanitize user input value * * @override */ _onChange: function (ev) { ev.target.value = this._sanitizeNumberValue(ev.target.value); return this._super.apply(this, arguments); }, // Helper Functions /** * Check limits and precision of the value. * If the value 'is not a number', the function does nothing to * be good with other possible modules. * * @param {Number} value * @returns {Number} */ _sanitizeNumberValue: function (value) { let cval = Number(value); if (_.isNaN(cval)) { return value; } if (!_.isNaN(this._config.min) && cval < this._config.min) { cval = this._config.min; } else if (!_.isNaN(this._config.max) && cval > this._config.max) { cval = this._config.max; } const field = this.record.fields[this.name]; // Formatted value return field_utils.format[field.type](cval, field, { data: this.record.data, escape: true, isPassword: false, digits: field.digits, }); }, }); Registry.add("numeric_step", NumericStep); return NumericStep; });