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 InputField = AbstractField.extend({ events: _.extend({}, AbstractField.prototype.events, { 'input': '_onInput', }), /** * 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.$el.focus(); setTimeout(this.$el.select.bind(this.$el), 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 //-------------------------------------------------------------------------- /** * Formats the HTML input tag for edit mode and stores selection status. * * @override * @private */ _renderEdit: function () { this.$el.addClass('o_form_input'); this.$el.attr('type', 'text'); if (this.attrs.placeholder) { this.$el.attr('placeholder', this.attrs.placeholder); } this.$el.attr('id', this.idForLabel); // save cursor position to restore it after updating value var selectionStart = this.el.selectionStart; var selectionEnd = this.el.selectionEnd; this.$el.val(this._formatValue(this.value)); this.el.selectionStart = selectionStart; this.el.selectionEnd = selectionEnd; }, /** * Resets the content to the formated value in readonly mode. * * @override * @private */ _renderReadonly: function () { this.$el.empty().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.$el[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); }, /** * We notify the outside world for each change in this input value. It does * not necessarily mean that onchanges will be triggered instantly. * * @private */ _onInput: function () { this._setValue(this.$el.val()); }, }); 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)); }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * Automatically selects the input associated with the datepicker widget. * When entering this field via a tab mechanism in list view for example. * * @override */ activate: function () { var $input = this.datewidget.$input; $input.focus(); setTimeout($input.select.bind($input), 0); }, //-------------------------------------------------------------------------- // 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); }, }); 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_list_number', supportedFieldTypes: ['float', 'monetary'], // FieldMonetary overrides _formatValue to ensure that the format_monetary // method is used. This allows this widget to be used with other field // types, such as float. _formatValue: function (value) { var options = _.extend({}, this.nodeOptions, { data: this.recordData }); return field_utils.format.monetary(value, this.field, options); }, isSet: function () { return this.value !== false; } }); var FieldBoolean = AbstractField.extend({ className: 'o_field_boolean', events: _.extend({}, AbstractField.prototype.events, { change: '_onChange', keydown: '_onKeyDown', }), 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) { 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.TAB: this.trigger_up('move_next'); event.preventDefault(); 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); } else { this.nodeOptions.digits = this.field.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.value; if (this.nodeOptions.digits && this.nodeOptions.digits.length === 2) { value = utils.round_decimals(value, this.nodeOptions.digits[1]); } var $span = $('').addClass('o_form_field o_form_field_number').text(this._formatValue(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 = AbstractField.extend({ events: _.extend({}, AbstractField.prototype.events, { 'input': function () { this._setValue(this.$textarea.val()); }, }), 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 = $('