odoo.define('web.basic_fields', function (require) { "use strict"; /** * This module contains most of the basic (meaning: non relational) field * widgets. Field widgets are supposed to be used in views inheriting from * BasicView, so, they can work with the records obtained from a BasicModel. */ var AbstractField = require('web.AbstractField'); var ajax = require('web.ajax'); var config = require('web.config'); var core = require('web.core'); var crash_manager = require('web.crash_manager'); var datepicker = require('web.datepicker'); var dom = require('web.dom'); var Domain = require('web.Domain'); var DomainSelector = require('web.DomainSelector'); var DomainSelectorDialog = require('web.DomainSelectorDialog'); var field_utils = require('web.field_utils'); var framework = require('web.framework'); var session = require('web.session'); var utils = require('web.utils'); var view_dialogs = require('web.view_dialogs'); var qweb = core.qweb; var _t = core._t; var DebouncedField = AbstractField.extend({ events: _.extend({}, AbstractField.prototype.events, { 'input': '_onInput', 'change': '_onChange', }), /** * for field widgets that may have a large number of field changes quickly, * it could be a good idea to debounce the changes. In that case, this is * the suggested value. */ DEBOUNCE: 1000, /** * Override init to debounce the input changes to make sure they are not * done too quickly. Note that this is done here and not on the prototype, * so each inputfield has its own debounced function to work with. * Also, if the debounce value is set to 0, no debouncing is done, which is * really useful for the unit tests. * * @override */ init: function () { this._super.apply(this, arguments); if (this.DEBOUNCE && this.mode === 'edit') { this._doDebouncedAction = _.debounce(this._doDebouncedAction.bind(this), this.DEBOUNCE); } }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Should return the current value of the field, in the DOM (for example, * the content of the input) * * @abstract * @private * @returns {*} */ _getValue: function () { }, /** * Notifies the outside world of the new value (checked from the DOM). * This method is debounced so that onchanges are not triggered too quickly. * * @private */ _doDebouncedAction: function () { this._setValue(this._getValue()); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * We immediately notify the outside world when this field confirms its * changes. * * @private */ _onChange: function () { this._setValue(this._getValue()); }, /** * 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 * * @private */ _onInput: function () { this._doDebouncedAction(); }, }); var InputField = DebouncedField.extend({ /** * The very purpose of this field is to be an input tag in edit mode. * * @override */ init: function () { this._super.apply(this, arguments); if (this.mode === 'edit') { this.tagName = 'input'; } }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * Automatically selects the content of the field. * * @override */ activate: function () { this.$input.focus(); setTimeout(this.$input.select.bind(this.$input), 0); }, /** * Do not re-render this field if it was the origin of the onchange call. * FIXME: make the onchange work on itself without disturbing the user typing * * @override */ reset: function (record, event) { this._reset(record, event); if (event && event.target === this) { return $.when(); } else { return this._render(); } }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @override * @returns {string} the content of the input */ _getValue: function () { return this.$input.val(); }, /** * 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. * * @private * @param {jQuery|undefined} $input * The element to prepare and save as the $input attribute. * If no element is given, the is created. * @returns {jQuery} the prepared this.$input element */ _prepareInput: function ($input) { this.$input = $input || $(""); this.$input.addClass('o_form_input'); this.$input.attr({ type: 'text', id: this.idForLabel, placeholder: this.attrs.placeholder || "", }); // Save cursor position to restore it after updating value var selectionStart = this.$input[0].selectionStart; var selectionEnd = this.$input[0].selectionEnd; this.$input.val(this._formatValue(this.value)); this.$input[0].selectionStart = selectionStart; this.$input[0].selectionEnd = selectionEnd; return this.$input; }, /** * Formats the HTML input tag for edit mode and stores selection status. * * @override * @private */ _renderEdit: function () { // Keep a reference to the input so $el can become something else // without losing track of the actual input. this._prepareInput(this.$el); }, /** * Resets the content to the formated value in readonly mode. * * @override * @private */ _renderReadonly: function () { this.$el.html(this._formatValue(this.value)); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * Implement keyboard movements. Mostly useful for its environment, such * as a list view. * * @override * @private * @param {any} event */ _onKeydown: function (event) { var input = this.$input[0]; var is_not_selecting; switch (event.which) { case $.ui.keyCode.DOWN: this.trigger_up('move_down'); break; case $.ui.keyCode.UP: this.trigger_up('move_up'); break; case $.ui.keyCode.LEFT: is_not_selecting = input.selectionEnd === input.selectionStart; if (is_not_selecting && input.selectionStart === 0) { this.trigger_up('move_left'); } break; case $.ui.keyCode.RIGHT: is_not_selecting = input.selectionEnd === input.selectionStart; if (is_not_selecting && input.selectionEnd === input.value.length) { this.trigger_up('move_right'); } break; case $.ui.keyCode.ENTER: this.trigger_up('move_next_line'); break; } this._super.apply(this, arguments); }, }); var FieldChar = InputField.extend({ supportedFieldTypes: ['char'], tagName: 'span', }); var FieldDate = InputField.extend({ className: "o_form_field_date", tagName: "span", replace_element: true, supportedFieldTypes: ['date'], /** * In edit mode, instantiates a DateWidget datepicker and listen to changes. * * @override */ start: function () { var self = this; var def; if (this.mode === 'edit') { this.datewidget = this._makeDatePicker(); this.datewidget.on('datetime_changed', this, function () { if (!this.datewidget.get_value().isSame(this.value)) { this._setValue(this.datewidget.get_value()); } }); def = this.datewidget.appendTo('
').done(function () { self.datewidget.$el.addClass(self.$el.attr('class')); self.datewidget.$input.addClass('o_form_input'); self.datewidget.$input.attr('id', self.idForLabel); self.replaceElement(self.datewidget.$el); }); } return $.when(def, this._super.apply(this, arguments)); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Instantiates a new DateWidget datepicker. * * @private */ _makeDatePicker: function () { return new datepicker.DateWidget(this, {defaultDate: this.value}); }, /** * Set the datepicker to the right value rather than the default one. * * @override * @private */ _renderEdit: function () { this.datewidget.set_value(this.value); this.$input = this.datewidget.$input; }, }); var FieldDateTime = FieldDate.extend({ supportedFieldTypes: ['datetime'], //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Instantiates a new DateTimeWidget datepicker rather than DateWidget. * * @override * @private */ _makeDatePicker: function () { return new datepicker.DateTimeWidget(this, {defaultDate: this.value}); }, }); var FieldMonetary = InputField.extend({ className: 'o_form_field_monetary o_list_number', replace_element: true, supportedFieldTypes: ['float', 'monetary'], resetOnAnyFieldChange: true, // Have to listen to currency changes /** * Float fields using a monetary widget have an additional currency_field * parameter which defines the name of the field from which the currency * should be read. * * They are also displayed differently than other inputs in * edit mode. They are a div containing a span with the currency symbol and * the actual input. * * If no currency field is given or the field does not exist, we fallback * to the default input behavior instead. * * @override */ init: function () { this._super.apply(this, arguments); this._setCurrency(); if (this.mode === 'edit' && this.currency) { this.tagName = 'div'; } }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * For monetary fields, 0 is a valid value. * * @override */ isSet: function () { return this.value === 0 || this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * For monetary fields, the input is inside a div, alongside a span * containing the currency symbol. * * @override * @private */ _renderEdit: function () { if (!this.currency) { this._super.apply(this, arguments); return; } this.$el.empty(); // Prepare and add the input this._prepareInput().appendTo(this.$el); // prepare and add the currency symbol var $currencySymbol = $('', {text: this.currency.symbol}); if (this.currency.position === "after") { this.$el.append($currencySymbol); } else { this.$el.prepend($currencySymbol); } }, /** * Re-gets the currency as its value may have changed. * @see FieldMonetary.resetOnAnyFieldChange * * @override * @private */ _reset: function () { this._super.apply(this, arguments); this._setCurrency(); }, /** * Deduces the currency description from the field options and view state. * The description is then available at this.currency. * * @private */ _setCurrency: function () { var currencyField = this.attrs.currency_field || this.field.currency_field || 'currency_id'; var currencyID = this.record.data[currencyField] && this.record.data[currencyField].res_id; this.currency = session.get_currency(currencyID); }, /** * FieldMonetary overrides _formatValue to use the format monetary method * in readonly. * * @override * @private * @param {float} value */ _formatValue: function (value) { var format = field_utils.format[this.mode === 'edit' ? "float" : "monetary"]; return format(value, this.field, { digits: [16, 2], currency: this.currency, }); }, }); var FieldBoolean = AbstractField.extend({ className: 'o_field_boolean', events: _.extend({}, AbstractField.prototype.events, { change: '_onChange', }), replace_element: true, supportedFieldTypes: ['boolean'], //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * Automatically selects the field. * * @override */ activate: function () { this.$input.focus(); setTimeout(this.$input.select.bind(this.$input), 0); }, /** * A boolean field is always set since false is a valid value. * * @override */ isSet: function () { return true; }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * The actual checkbox is designed in css to have full control over its * appearance, as opposed to letting the browser and the os decide how * a checkbox should look. The actual input is disabled and hidden. * * @override * @private */ _renderReadonly: function () { var $checkbox = this._formatValue(this.value); this.$input = $checkbox.find('input'); this.$el.empty().append($checkbox); }, /** * The actual checkbox is designed in css to have full control over its * appearance, as opposed to letting the browser and the os decide how * a checkbox should look. The actual input is hidden. * * @override * @private */ _renderEdit: function () { this._renderReadonly(); this.$input.prop('disabled', false); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * Properly update the value when the checkbox is (un)ticked to trigger * possible onchanges. * * @private */ _onChange: function () { this._setValue(this.$input[0].checked); }, /** * Implement keyboard movements. Mostly useful for its environment, such * as a list view. * * @override * @private * @param {KeyEvent} event */ _onKeydown: function (event) { this._super.apply(this, arguments); switch (event.which) { case $.ui.keyCode.DOWN: this.trigger_up('move_down'); break; case $.ui.keyCode.UP: this.trigger_up('move_up'); break; case $.ui.keyCode.LEFT: this.trigger_up('move_left'); break; case $.ui.keyCode.RIGHT: this.trigger_up('move_right'); break; case $.ui.keyCode.ENTER: this.$input.prop('checked', !this.value); this._setValue(!this.value); break; } }, }); var FieldInteger = InputField.extend({ supportedFieldTypes: ['integer'], /** * For integer fields, 0 is a valid value. * * @override */ isSet: function () { return this.value === 0 || this._super.apply(this, arguments); }, }); var FieldFloat = InputField.extend({ supportedFieldTypes: ['float'], /** * Float fields have an additional precision parameter that is read from * either the field node in the view or the field python definition itself. * * @override */ init: function () { this._super.apply(this, arguments); if (this.attrs.digits) { this.nodeOptions.digits = JSON.parse(this.attrs.digits); } }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * For float fields, 0 is a valid value. * * @override */ isSet: function () { return this.value === 0 || this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Format value according to precision parameter. * * @override * @private */ _renderReadonly: function () { var value = this._formatValue(this.value); var $span = $('').addClass('o_form_field o_form_field_number').text(value); this.$el.html($span); }, }); var FieldFloatTime = FieldFloat.extend({ supportedFieldTypes: ['float'], //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Format the float time value into a human-readable time format. * * @override * @private */ _formatValue: function (value) { return field_utils.format.float_time(value); }, /** * Parse the human-readable formatted time value into a float. * * @override * @private */ _parseValue: function (value) { return field_utils.parse.float_time(value); }, }); var FieldText = DebouncedField.extend({ supportedFieldTypes: ['text'], /** * In edit mode, the text widget contains a textarea. We append it in * start() instead of _renderEdit() to keep the same textarea even * if several _renderEdit are done. This allows to keep the cursor * position and to autoresize only once. * * @override */ start: function () { this.$el.addClass('o_list_text o_form_textarea'); this.$el.attr('id', this.idForLabel); if (this.mode === 'edit') { this.$textarea = $('