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',
}),
/**
* 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._onInput = _.debounce(this._onInput.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 () {
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* We immediately notify the outside world when this field confirms its
* changes
*
* @private
*/
_onChange: function () {
this._setValue(this._getValue());
},
/**
* We notify the outside world for each change in this field. It does not
* necessarily mean that onchanges will be triggered instantly.
*
* @private
*/
_onInput: function () {
this._setValue(this._getValue());
},
});
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);
} 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 = 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 = $('