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',
}),
init: function () {
this._super.apply(this, arguments);
if (this.mode === 'edit') {
this.tagName = 'input';
}
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
activate: function () {
this.$el.focus();
setTimeout(this.$el.select.bind(this.$el), 0);
},
//--------------------------------------------------------------------------
// 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;
},
_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
* @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.
*/
_onInput: function () {
this._setValue(this.$el.val());
},
});
var FieldChar = InputField.extend({
supportedFieldTypes: ['char', 'text'],
tagName: 'span',
});
var FieldDate = InputField.extend({
className: "o_form_field_date",
tagName: "span",
replace_element: true,
supportedFieldTypes: ['date', 'datetime'],
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
//--------------------------------------------------------------------------
activate: function () {
var $input = this.datewidget.$input;
$input.focus();
setTimeout($input.select.bind($input), 0);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_makeDatePicker: function () {
return new datepicker.DateWidget(this);
},
_renderEdit: function () {
this.datewidget.set_value(this.value);
},
});
var FieldDateTime = FieldDate.extend({
supportedFieldTypes: ['datetime'],
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_makeDatePicker: function () {
return new datepicker.DateTimeWidget(this);
},
});
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
//--------------------------------------------------------------------------
activate: function () {
this.$input.focus();
setTimeout(this.$input.select.bind(this.$input), 0);
},
isSet: function () {
return true;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_renderReadonly: function () {
var $checkbox = this._formatValue(this.value);
this.$input = $checkbox.find('input');
this.$el.empty().append($checkbox);
},
_renderEdit: function () {
this._renderReadonly();
this.$input.prop('disabled', false);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_onChange: function () {
this._setValue(this.$input[0].checked);
},
/**
* @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'],
isSet: function () {
return this.value !== false;
},
});
var FieldFloat = InputField.extend({
supportedFieldTypes: ['float'],
_renderReadonly: function () {
var precision;
var value = this.value;
if (this.attrs.digits) {
precision = this.node.attrs.digits;
} else {
precision = this.field.digits;
}
if (precision && precision.length === 2) {
value = utils.round_decimals(value, precision[1]);
}
var $span = $('').addClass('o_form_field o_form_field_number').text(this._formatValue(value));
this.$el.html($span);
},
isSet: function () {
return this.value !== '';
},
});
var FieldFloatTime = FieldFloat.extend({
supportedFieldTypes: ['float'],
_formatValue: function (value) {
var pattern = '%02d:%02d';
if (value < 0) {
value = Math.abs(value);
pattern = '-' + pattern;
}
var hour = Math.floor(value);
var min = Math.round((value % 1) * 60);
if (min === 60){
min = 0;
hour = hour + 1;
}
return _.str.sprintf(pattern, hour, min);
},
_parseValue: function (value) {
var factor = 1;
if (value[0] === '-') {
value = value.slice(1);
factor = -1;
}
var float_time_pair = value.split(":");
if (float_time_pair.length !== 2) {
return factor * field_utils.parse_float(value, this.field);
}
var hours = field_utils.parse_integer(float_time_pair[0]);
var minutes = field_utils.parse_integer(float_time_pair[1]);
return factor * (hours + (minutes / 60));
},
});
var FieldText = AbstractField.extend({
events: _.extend({}, AbstractField.prototype.events, {
'input': function () {
this._setValue(this.$textarea.val());
},
}),
supportedFieldTypes: ['text'],
start: function () {
this.$el.addClass('o_list_text o_form_textarea');
this.$el.attr('id', this.idForLabel);
if (this.mode === 'edit') {
// in edit mode, the 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
this.$textarea = $('