diff options
Diffstat (limited to 'addons/web/static/src/js/fields')
| -rw-r--r-- | addons/web/static/src/js/fields/abstract_field.js | 11 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/basic_fields.js | 41 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/field_utils.js | 70 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/relational_fields.js | 124 |
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 + "­" + 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]); } }, }); |
