summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/fields
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/fields')
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js11
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js41
-rw-r--r--addons/web/static/src/js/fields/field_utils.js70
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js124
4 files changed, 195 insertions, 51 deletions
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index 725a237750d..d09174b2b20 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -348,8 +348,11 @@ var AbstractField = Widget.extend({
*
* @private
* @param {any} value
+ * @param {Object} [options]
+ * @param {boolean} [options.forceChange=false] if true, the change event will be
+ * triggered even if the new value is the same as the old one
*/
- _setValue: function (value) {
+ _setValue: function (value, options) {
// we try to avoid doing useless work, if the value given has not
// changed. Note that we compare the unparsed values.
if (this.lastSetValue === value || (this.value === false && value === '')) {
@@ -363,7 +366,7 @@ var AbstractField = Widget.extend({
this._isValid = false;
return;
}
- if (this._isSameValue(value)) {
+ if (!(options && options.forceChange) && this._isSameValue(value)) {
return;
}
var changes = {};
@@ -371,6 +374,7 @@ var AbstractField = Widget.extend({
this.trigger_up('field_changed', {
dataPointID: this.dataPointID,
changes: changes,
+ viewType: this.viewType,
});
},
@@ -404,6 +408,9 @@ var AbstractField = Widget.extend({
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'next_line'});
break;
+ case $.ui.keyCode.ESCAPE:
+ this.trigger_up('navigation_move', {direction: 'cancel'});
+ break;
case $.ui.keyCode.UP:
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'up'});
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 49bcccd2640..a5ceba50d3f 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -83,10 +83,10 @@ var DebouncedField = AbstractField.extend({
init: function () {
this._super.apply(this, arguments);
- // _debouncedStarted is used to detect that the user interacted at least
+ // _isDirty is used to detect that the user interacted at least
// once with the widget, so that we can prevent it from triggering a
// field_changed in commitChanges if the user didn't change anything
- this._debouncedStarted = false;
+ this._isDirty = false;
if (this.mode === 'edit') {
if (this.DEBOUNCE) {
this._doDebouncedAction = _.debounce(this._doAction, this.DEBOUNCE);
@@ -97,7 +97,7 @@ var DebouncedField = AbstractField.extend({
var self = this;
var debouncedFunction = this._doDebouncedAction;
this._doDebouncedAction = function () {
- self._debouncedStarted = true;
+ self._isDirty = true;
debouncedFunction.apply(self, arguments);
};
}
@@ -115,7 +115,7 @@ var DebouncedField = AbstractField.extend({
* @override
*/
commitChanges: function () {
- if (this._debouncedStarted && this.mode === 'edit') {
+ if (this._isDirty && this.mode === 'edit') {
this._doAction();
}
},
@@ -132,7 +132,13 @@ var DebouncedField = AbstractField.extend({
* @private
*/
_doAction: function () {
- this._setValue(this._getValue());
+ // as _doAction may be debounced, it may happen that it is called after
+ // the widget has been destroyed, and in this case, we don't want it to
+ // do anything (commitChanges ensures that if it has local changes, they
+ // are triggered up before the widget is destroyed, if necessary).
+ if (!this.isDestroyed()) {
+ this._setValue(this._getValue());
+ }
},
/**
* Should return the current value of the field, in the DOM (for example,
@@ -159,6 +165,7 @@ var InputField = DebouncedField.extend({
*/
init: function () {
this._super.apply(this, arguments);
+ this.nodeOptions.isPassword = 'password' in this.attrs;
if (this.mode === 'edit') {
this.tagName = 'input';
}
@@ -216,8 +223,11 @@ var InputField = DebouncedField.extend({
this.$input = $input || $("<input/>");
this.$input.addClass('o_input');
this.$input.attr({
- type: 'text',
+ type: this.nodeOptions.isPassword ? 'password' : 'text',
placeholder: this.attrs.placeholder || "",
+ autocomplete: this.nodeOptions.isPassword ?
+ 'new-password' :
+ this.attrs.autocomplete,
});
this.$input.val(this._formatValue(this.value));
return this.$input;
@@ -240,7 +250,7 @@ var InputField = DebouncedField.extend({
* @private
*/
_renderReadonly: function () {
- this.$el.html(this._formatValue(this.value));
+ this.$el.text(this._formatValue(this.value));
},
//--------------------------------------------------------------------------
@@ -524,6 +534,13 @@ var FieldMonetary = InputField.extend({
}
},
/**
+ * @override
+ * @private
+ */
+ _renderReadonly: function () {
+ this.$el.html(this._formatValue(this.value));
+ },
+ /**
* Re-gets the currency as its value may have changed.
* @see FieldMonetary.resetOnAnyFieldChange
*
@@ -541,7 +558,7 @@ var FieldMonetary = InputField.extend({
* @private
*/
_setCurrency: function () {
- var currencyField = this.attrs.currency_field || this.field.currency_field || 'currency_id';
+ var currencyField = this.nodeOptions.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);
},
@@ -885,8 +902,8 @@ var FieldPhone = FieldEmail.extend({
_renderReadonly: function () {
this._super();
if (this._canCall()) {
- var text = this.$el.text();
// Split phone number into two to prevent Skype app from finding it
+ var text = this.$el.text();
var part1 = _.escape(text.substr(0, text.length/2));
var part2 = _.escape(text.substr(text.length/2));
this.$el.html(part1 + "&shy;" + part2);
@@ -1543,7 +1560,7 @@ var FieldPercentPie = AbstractField.extend({
this.$leftMask.css({transform: leftDeg, msTransform: leftDeg, mozTransform: leftDeg, webkitTransform: leftDeg});
this.$rightMask.css({transform: rightDeg, msTransform: rightDeg, mozTransform: rightDeg, webkitTransform: rightDeg});
- this.$pieValue.html(Math.round(value) + '%');
+ this.$pieValue.text(Math.round(value) + '%');
},
});
@@ -1684,9 +1701,9 @@ var FieldProgressBar = AbstractField.extend({
if (!this.write_mode) {
if (max_value !== 100) {
- this.$('.o_progressbar_value').html(utils.human_number(value) + " / " + utils.human_number(max_value));
+ this.$('.o_progressbar_value').text(utils.human_number(value) + " / " + utils.human_number(max_value));
} else {
- this.$('.o_progressbar_value').html(utils.human_number(value) + "%");
+ this.$('.o_progressbar_value').text(utils.human_number(value) + "%");
}
} else if (isNaN(v)) {
this.$('.o_progressbar_value').val(this.edit_max_value ? max_value : value);
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index ed3014d9bb6..9a6734b7baf 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -52,10 +52,23 @@ function formatBoolean(value) {
* an empty string.
*
* @param {string|false} value
+ * @param {Object} [field]
+ * a description of the field (note: this parameter is ignored)
+ * @param {Object} [options] additional options
+ * @param {boolean} [options.escape=false] if true, escapes the formatted value
+ * @param {boolean} [options.isPassword=false] if true, returns '********'
+ * instead of the formatted value
* @returns {string}
*/
-function formatChar(value) {
- return typeof value === 'string' ? value : '';
+function formatChar(value, field, options) {
+ value = typeof value === 'string' ? value : '';
+ if (options && options.isPassword) {
+ return _.str.repeat('*', value ? value.length : 0);
+ }
+ if (options && options.escape) {
+ value = _.escape(value);
+ }
+ return value;
}
/**
@@ -169,9 +182,16 @@ function formatFloatTime(value) {
* return an empty string.
*
* @param {integer|false} value
+ * @param {Object} [field]
+ * a description of the field (note: this parameter is ignored)
+ * @param {Object} [options] additional options
+ * @param {boolean} [options.isPassword=false] if true, returns '********'
* @returns {string}
*/
-function formatInteger(value) {
+function formatInteger(value, field, options) {
+ if (options && options.isPassword) {
+ return _.str.repeat('*', String(value).length);
+ }
if (!value && value !== 0) {
// previously, it returned 'false'. I don't know why. But for the Pivot
// view, I want to display the concept of 'no value' with an empty
@@ -189,10 +209,18 @@ function formatInteger(value) {
* case, we assume that it is a record from a BasicModel.
*
* @param {Array|Object|false} value
+ * @param {Object} [field]
+ * a description of the field (note: this parameter is ignored)
+ * @param {Object} [options] additional options
+ * @param {boolean} [options.escape=false] if true, escapes the formatted value
* @returns {string}
*/
-function formatMany2one(value) {
- return value && (_.isArray(value) ? value[1] : value.data.display_name) || '';
+function formatMany2one(value, field, options) {
+ value = value && (_.isArray(value) ? value[1] : value.data.display_name) || '';
+ if (options && options.escape) {
+ value = _.escape(value);
+ }
+ return value;
}
/**
@@ -270,14 +298,27 @@ function formatMonetary(value, field, options) {
}
}
-function formatSelection(value, field) {
- if (!value) {
- return '';
- }
+/**
+ * Returns a string representing the value of the selection.
+ *
+ * @param {string|false} value
+ * @param {Object} [field]
+ * a description of the field (note: this parameter is ignored)
+ * @param {Object} [options] additional options
+ * @param {boolean} [options.escape=false] if true, escapes the formatted value
+ */
+function formatSelection(value, field, options) {
var val = _.find(field.selection, function (option) {
return option[0] === value;
});
- return val[1];
+ if (!val) {
+ return '';
+ }
+ value = val[1];
+ if (options && options.escape) {
+ value = _.escape(value);
+ }
+ return value;
}
////////////////////////////////////////////////////////////////////////////////
@@ -318,7 +359,7 @@ function parseDate(value, field, options) {
}
if (date.year() >= 1900) {
date.toJSON = function () {
- return this.format('YYYY-MM-DD');
+ return this.clone().locale('en').format('YYYY-MM-DD');
};
return date;
}
@@ -365,7 +406,7 @@ function parseDateTime(value, field, options) {
}
if (datetime.year() >= 1900) {
datetime.toJSON = function () {
- return this.format('YYYY-MM-DD HH:mm:ss');
+ return this.clone().locale('en').format('YYYY-MM-DD HH:mm:ss');
};
return datetime;
}
@@ -374,7 +415,10 @@ function parseDateTime(value, field, options) {
}
function parseFloat(value) {
- value = value.replace(new RegExp(core._t.database.parameters.thousands_sep, "g"), '');
+ if (core._t.database.parameters.thousands_sep) {
+ var escapedSep = _.str.escapeRegExp(core._t.database.parameters.thousands_sep);
+ value = value.replace(new RegExp(escapedSep, 'g'), '');
+ }
value = value.replace(core._t.database.parameters.decimal_point, '.');
var parsed = Number(value);
if (isNaN(parsed)) {
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 10d72a1398d..a5f9b443ae0 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -88,6 +88,7 @@ var FieldMany2One = AbstractField.extend({
'click .o_external_button': '_onExternalButtonClick',
'click': '_onClick',
}),
+ AUTOCOMPLETE_DELAY: 200,
init: function () {
this._super.apply(this, arguments);
@@ -147,6 +148,11 @@ var FieldMany2One = AbstractField.extend({
});
},
select: function (event, ui) {
+ // we do not want the select event to trigger any additional
+ // effect, such as navigating to another field.
+ event.stopImmediatePropagation();
+ event.preventDefault();
+
var item = ui.item;
self.floating = false;
if (item.id) {
@@ -159,10 +165,17 @@ var FieldMany2One = AbstractField.extend({
focus: function (event) {
event.preventDefault(); // don't automatically select values on focus
},
+ close: function (event) {
+ // it is necessary to prevent ESC key from propagating to field
+ // root, to prevent unwanted discard operations.
+ if (event.which === $.ui.keyCode.ESCAPE) {
+ event.stopPropagation();
+ }
+ },
autoFocus: true,
html: true,
minLength: 0,
- delay: 200,
+ delay: this.AUTOCOMPLETE_DELAY,
});
this.$input.autocomplete("option", "position", { my : "left top", at: "left bottom" });
this.autocomplete_bound = true;
@@ -250,6 +263,7 @@ var FieldMany2One = AbstractField.extend({
*/
_reset: function () {
this._super.apply(this, arguments);
+ this.floating = false;
this.m2o_value = field_utils.format.many2one(this.value);
},
/**
@@ -427,6 +441,7 @@ var FieldMany2One = AbstractField.extend({
view_id: view_id,
readonly: !self.can_write,
on_saved: function () {
+ self._setValue(self.value.data, {forceChange: true});
self.trigger_up('reload', {db_id: self.value.id});
},
}).open();
@@ -462,6 +477,14 @@ var FieldMany2One = AbstractField.extend({
}
},
/**
+ * @override
+ * @private
+ */
+ _onKeydown: function () {
+ this.floating = false;
+ this._super.apply(this, arguments);
+ },
+ /**
* Stops the left/right navigation move event if the cursor is not at the
* start/end of the input element. Stops any navigation move event if the
* user is selecting text.
@@ -531,6 +554,7 @@ var FieldX2Many = AbstractField.extend({
tagName: 'div',
custom_events: _.extend({}, AbstractField.prototype.custom_events, {
add_record: '_onAddRecord',
+ discard_changes: '_onDiscardChanges',
edit_line: '_onEditLine',
field_changed: '_onFieldChanged',
kanban_record_delete: '_onDeleteRecord',
@@ -740,7 +764,7 @@ var FieldX2Many = AbstractField.extend({
this.renderer.commitChanges(recordID).then(function () { // TODO wrong as no mutex protection
var fieldNames = self.renderer.canBeSaved(recordID);
if (fieldNames.length) {
- self.trigger_up('discard_x2m_changes', {
+ self.trigger_up('discard_changes', {
recordID: recordID,
onSuccess: def.resolve.bind(def),
onFailure: def.reject.bind(def),
@@ -783,6 +807,18 @@ var FieldX2Many = AbstractField.extend({
});
},
/**
+ * When the discard_change event go through this field, we can just decorate
+ * the data with the name of the field. The origin field ignore this
+ * information (it is a subfield in a o2m), and the controller will need to
+ * know which field needs to be handled.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onDiscardChanges: function (ev) {
+ ev.data.fieldName = this.name;
+ },
+ /**
* Called when the renderer asks to edit a line, in that case simply tells
* him back to toggle the mode of this row.
*
@@ -837,15 +873,30 @@ var FieldX2Many = AbstractField.extend({
* changes; if the row could be saved, we make the row readonly. Otherwise,
* we trigger a new event for the view to tell it to discard the changes
* made to that row.
+ * Note that we do that in the controller mutex to ensure that the check on
+ * the row (whether or not it can be saved) is done once all potential
+ * onchange RPCs are done (those RPCs being executed in the same mutex).
+ * This particular handling is done in this handler, instead of in the
+ * _saveLine function directly, because _saveLine is also called from
+ * the controller (via commitChanges), and in this case, it is already
+ * executed in the mutex.
*
* @private
* @param {OdooEvent} ev
+ * @param {string} ev.recordID
+ * @param {function} ev.onSuccess success callback (see '_saveLine')
+ * @param {function} ev.onFailure fail callback (see '_saveLine')
*/
_onSaveLine: function (ev) {
+ var self = this;
ev.stopPropagation();
- this._saveLine(ev.data.recordID)
- .done(ev.data.onSuccess)
- .fail(ev.data.onFailure);
+ this.trigger_up('mutexify', {
+ action: function () {
+ return self._saveLine(ev.data.recordID)
+ .done(ev.data.onSuccess)
+ .fail(ev.data.onFailure);
+ },
+ });
},
/**
* Adds field name information to the event, so that the view upstream is
@@ -863,6 +914,16 @@ var FieldOne2Many = FieldX2Many.extend({
className: 'o_field_one2many',
supportedFieldTypes: ['one2many'],
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+
+ // boolean used to prevent concurrent record creation
+ this.creatingRecord = false;
+ },
+
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -880,6 +941,7 @@ var FieldOne2Many = FieldX2Many.extend({
var index = self.editable === 'top' ? 0 : self.value.data.length - 1;
var newID = self.value.data[index].id;
self.renderer.editRecord(newID);
+ self.creatingRecord = false;
}
}
});
@@ -933,10 +995,13 @@ var FieldOne2Many = FieldX2Many.extend({
ev.stopPropagation();
if (this.editable) {
- this._setValue({
- operation: 'CREATE',
- position: this.editable,
- });
+ if (!this.creatingRecord) {
+ this.creatingRecord = true;
+ this._setValue({
+ operation: 'CREATE',
+ position: this.editable,
+ });
+ }
} else {
var self = this;
this._openFormDialog({
@@ -962,7 +1027,7 @@ var FieldOne2Many = FieldX2Many.extend({
this._openFormDialog({
id: ev.data.id,
- on_saved: this._setValue.bind(this, { operation: 'NOOP' }),
+ on_saved: this._setValue.bind(this, { operation: 'NOOP' }, {}),
readonly: this.mode === 'readonly',
});
},
@@ -1026,11 +1091,15 @@ var FieldMany2Many = FieldX2Many.extend({
* @param {OdooEvent} ev
*/
_onOpenRecord: function (ev) {
+ var self = this;
_.extend(ev.data, {
context: this.record.getContext(this.recordParams),
domain: this.record.getDomain(this.recordParams),
fields_view: this.attrs.views && this.attrs.views.form,
- on_saved: this.trigger_up.bind(this, 'reload', {db_id: ev.data.id}),
+ on_saved: function () {
+ self._setValue({operation: 'TRIGGER_ONCHANGE'}, {forceChange: true});
+ self.trigger_up('reload', {db_id: ev.data.id});
+ },
readonly: this.mode === 'readonly',
string: this.string,
});
@@ -1671,7 +1740,7 @@ var FieldSelection = AbstractField.extend({
},
//--------------------------------------------------------------------------
- // Private
+ // Public
//--------------------------------------------------------------------------
/**
@@ -1681,6 +1750,12 @@ var FieldSelection = AbstractField.extend({
getFocusableElement: function () {
return this.$el.is('select') ? this.$el : $();
},
+ /**
+ * @override
+ */
+ isSet: function () {
+ return this.value !== false;
+ },
//--------------------------------------------------------------------------
// Private
@@ -1710,7 +1785,7 @@ var FieldSelection = AbstractField.extend({
* @private
*/
_renderReadonly: function () {
- this.$el.empty().html(this._formatValue(this.value));
+ this.$el.empty().text(this._formatValue(this.value));
},
//--------------------------------------------------------------------------
@@ -1779,14 +1854,17 @@ var FieldRadio = FieldSelection.extend({
* @override
*/
_renderEdit: function () {
- this.$("input").prop("checked", false);
- var key;
+ var value;
if (this.field.type === 'many2one') {
- key = this.value && this.value.data.id;
+ value = this.value && this.value.data.id;
} else {
- key = this.value;
+ value = this.value;
}
- this.$('input[data-value="' + key + '"]').prop('checked', true);
+ var index = _.findIndex(this.values, function (option) {
+ return option[0] === value;
+ });
+ this.$("input").prop("checked", false);
+ this.$('input[data-index="' + index + '"]').prop('checked', true);
},
//--------------------------------------------------------------------------
@@ -1798,14 +1876,12 @@ var FieldRadio = FieldSelection.extend({
* @param {MouseEvent} event
*/
_onInputClick: function (event) {
- var res_id = $(event.target).data('value');
+ var index = $(event.target).data('index');
+ var value = this.values[index];
if (this.field.type === 'many2one') {
- var value = _.find(this.values, function (val) {
- return val[0] === res_id;
- });
- this._setValue({id: res_id, display_name: value[1]});
+ this._setValue({id: value[0], display_name: value[1]});
} else {
- this._setValue(res_id);
+ this._setValue(value[0]);
}
},
});