summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/fields
diff options
context:
space:
mode:
authorqsm-odoo <qsm@odoo.com>2017-04-18 10:43:10 +0200
committerqsm-odoo <qsm@odoo.com>2017-04-19 11:42:01 +0200
commit7ff8aa73e17b281b3e2bc4c7275d020eab0204e7 (patch)
treed45b9ea0299c66bc1bb448634163d8aa686050b5 /addons/web/static/src/js/fields
parentff86e5b9862deab9a9772774e941140966a9ab7c (diff)
[REF] web, *: implement modifiers behaviors
* account, mail, sale Since the views refactoring, modifiers were not properly handled anymore (readonly/required not recomputed, use of readonly/required of "python field" instead of "view field", ...). This commit tries to implement a system which allows to handle the modifiers (re)computation the same way for all the basic views. For this to work, many specialized renderers behaviors have been moved to the basic renderer so that more mechanisms are shared (especially by the form renderer and the list editable renderer). The list editable renderer is hugely impacted by this commit. Here is a list of key changes: - The basic renderer now has a `_renderFieldWidget` function which is used without modification by the form and the list renderers. - Modifiers have to be registered thanks to the basic renderer `_registerModifiersData` function (this is done automatically for fields by the `_renderFieldWidget` function for example). - All instantiated widgets are accessible, organized by record, and ordered in a special basic renderer variable. - The code which resets the widgets of the form view and the code which updates the row of a list editable view is now shared in the basic renderer and now also automatically updates the DOM according to the reevaluated modifiers. - The last point has an huge impact on list editable renderer: all the widgets have to be instantiated when editing a row (even readonly and invisible ones) as they can be switched to editable/visible during edition (after a modifiers update). - `replace_element` and `readonly` options of `AbstractField` are useless: widgets are now always replacing the list editable cells in edition and the `readonly` widget mechanism is replaced by the notion of 'focusable' widgets. (see `AbstractField.getFocusableElement`) - The 'tab' navigation mechanism is impacted by the previous point, some code sharing have been done in basic renderer for this too. - `AbstractField` does not care anymore of the 'required' status, this is the view responsability to check that a required field has a set value on save. - Some list editable bug fixing (e.g. it was possible to edit multiple rows at the same time, it was not possible to navigate towards the previous field, crash on o2m add an item click, ...) - The mockserver now properly simulates server modifiers computation. - ... Note: list editable style may be worse than before this commit but a CSS update is coming in a few days.
Diffstat (limited to 'addons/web/static/src/js/fields')
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js83
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js38
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js58
3 files changed, 86 insertions, 93 deletions
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index f36418e3b8b..0d7ed2f4fe2 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -32,7 +32,6 @@ odoo.define('web.AbstractField', function (require) {
var field_utils = require('web.field_utils');
var Widget = require('web.Widget');
-var Domain = require('web.Domain');
var AbstractField = Widget.extend({
className: 'o_field_widget',
@@ -40,22 +39,12 @@ var AbstractField = Widget.extend({
'keydown': '_onKeydown',
},
/**
- * if this flag is set to true, the rest of the web client will assume that
- * it is not editable. For example, the list view in editable mode will
- * skip the widget when pressing tab
- */
- readonly: false,
- /**
* If this flag is set to true, the field widget will be reset on every
* change which is made in the view (if the view supports it). This is
* currently a form view feature.
*/
resetOnAnyFieldChange: false,
/**
- * if true, the widget will replace a cell in an editable list view
- */
- replace_element: false,
- /**
* If this flag is given a string, the related BasicModel will be used to
* initialize specialData the field might need. This data will be available
* through this.record.specialData[this.name].
@@ -77,8 +66,6 @@ var AbstractField = Widget.extend({
* @param {Object} record A record object (result of the get method of a basic model)
* @param {Object} [options]
* @param {string} [options.mode=readonly] should be 'readonly' or 'edit'
- * @param {string} [options.required=false]
- * @param {string} [options.idForLabel]
*/
init: function (parent, name, record, options) {
this._super(parent);
@@ -103,9 +90,6 @@ var AbstractField = Widget.extend({
var fieldsInfo = record.fieldsInfo[this.viewType];
this.attrs = fieldsInfo && fieldsInfo[name] || {};
- // modifiers
- this.modifiers = JSON.parse(this.attrs.modifiers || "{}");
-
// this property tracks the current (parsed if needed) value of the field.
// Note that we don't use an event system anymore, using this.get('value')
// is no longer valid.
@@ -153,15 +137,6 @@ var AbstractField = Widget.extend({
// a string.
this._isValid = true;
- // the 'required' flag is basically only needed to determine if the widget
- // is in a valid state (not valid if empty and required)
- this.required = options.required || this.field.required;
-
- // the 'idForLabel' is the (html) id that should be set to the relevent
- // dom entity. If done correctly, clicking on the corresponding label
- // (in form view) will focus and select the value.
- this.idForLabel = options.idForLabel;
-
// this is the last value that was set by the user, unparsed. This is
// used to avoid setting the value twice in a row with the exact value.
this.lastSetValue = undefined;
@@ -175,8 +150,8 @@ var AbstractField = Widget.extend({
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
- self._render();
self.$el.attr('name', self.name);
+ return self._render();
});
},
@@ -185,11 +160,22 @@ var AbstractField = Widget.extend({
//--------------------------------------------------------------------------
/**
- * Right now, this function is only used in editable list view when the user
- * select a cell. The corresponding widget will be activated, which in general
- * means that the input text will be focused and selected
+ * Activates the field widget. By default, activation means focusing and
+ * selecting the associated focusable element.
+ *
+ * @returns {boolean} true if the widget was activated, false if the
+ * focusable element was not found or invisible
*/
activate: function () {
+ var $focusable = this.getFocusableElement();
+ if ($focusable.length && $focusable.is(':visible')) {
+ $focusable.focus();
+ if ($focusable.is('input[type="text"], textarea')) {
+ setTimeout($focusable.select.bind($focusable), 0);
+ }
+ return true;
+ }
+ return false;
},
/**
* This function should be implemented by widgets that are not able to
@@ -202,6 +188,15 @@ var AbstractField = Widget.extend({
commitChanges: function () {
},
/**
+ * Returns the main field's DOM element (jQuery form) which can be focused
+ * by the browser.
+ *
+ * @returns {jQuery} main focusable element inside the widget
+ */
+ getFocusableElement: function () {
+ return $();
+ },
+ /**
* this method is used to determine if the field value is set to a meaningful
* value. This is useful to determine if a field should be displayed as empty
*
@@ -211,23 +206,23 @@ var AbstractField = Widget.extend({
return !!this.value;
},
/**
- * a field widget is valid if its value is valid and if there is a value when
- * it is required. This is checked before saving a record, by the form view.
+ * A field widget is valid if it was checked as valid the last time its
+ * value was changed by the user. This is checked before saving a record, by
+ * the view.
*
- * Note: the return value may be a boolean. It is necessary for some fields
- * (x2many) to sometimes ask confirmation to the user if the current dirty
- * invalid line can be discarded. This is an asynchronous operation, so we
- * need the expressivity of something like a deferred.
+ * Note: the return value may not be a boolean. It is necessary for some
+ * fields (x2many) to sometimes ask confirmation to the user if the current
+ * dirty invalid line can be discarded. This is an asynchronous operation,
+ * so we need the expressivity of something like a deferred.
+ *
+ * Note2: this is the responsability of the view to check that required
+ * fields have a set value.
*
* @returns {boolean|Deferred} true/false if the widget is valid, or a
* deferred which resolves to true/false if the widget is valid or not
*/
isValid: function () {
- var is_required = this.required;
- if ('required' in this.modifiers) {
- is_required = new Domain(this.modifiers.required).compute(this.recordData);
- }
- return this._isValid && !(is_required && !this.isSet());
+ return this._isValid;
},
/**
* this method is supposed to be called from the outside of field widgets.
@@ -285,12 +280,6 @@ var AbstractField = Widget.extend({
* @returns {Deferred}
*/
_render: function () {
- var is_required = this.required;
- if ('required' in this.modifiers) {
- is_required = new Domain(this.modifiers.required).compute(this.recordData);
- }
- this.$el.toggleClass('o_form_required', is_required);
-
if (this.mode === 'edit') {
return this._renderEdit();
} else if (this.mode === 'readonly') {
@@ -347,7 +336,7 @@ var AbstractField = Widget.extend({
try {
value = this._parseValue(value);
this._isValid = true;
- } catch(e) {
+ } catch (e) {
this._isValid = false;
}
var changes = {};
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index d2649195111..9f37240e0cc 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -122,13 +122,12 @@ var InputField = DebouncedField.extend({
//--------------------------------------------------------------------------
/**
- * Automatically selects the content of the field.
+ * Returns the associated <input/> element.
*
* @override
*/
- activate: function () {
- this.$input.focus();
- setTimeout(this.$input.select.bind(this.$input), 0);
+ getFocusableElement: function () {
+ return this.$input || $();
},
/**
* Do not re-render this field if it was the origin of the onchange call.
@@ -171,7 +170,6 @@ var InputField = DebouncedField.extend({
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
@@ -253,7 +251,6 @@ var FieldChar = InputField.extend({
var FieldDate = InputField.extend({
className: "o_form_field_date",
tagName: "span",
- replace_element: true,
supportedFieldTypes: ['date'],
/**
@@ -274,8 +271,7 @@ var FieldDate = InputField.extend({
});
def = this.datewidget.appendTo('<div>').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._prepareInput(self.datewidget.$input);
self.replaceElement(self.datewidget.$el);
});
}
@@ -371,7 +367,6 @@ var FieldDateTime = FieldDate.extend({
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
@@ -484,7 +479,6 @@ var FieldBoolean = AbstractField.extend({
events: _.extend({}, AbstractField.prototype.events, {
change: '_onChange',
}),
- replace_element: true,
supportedFieldTypes: ['boolean'],
//--------------------------------------------------------------------------
@@ -492,15 +486,12 @@ var FieldBoolean = AbstractField.extend({
//--------------------------------------------------------------------------
/**
- * Automatically selects the field.
- *
* @override
+ * @returns {jQuery} the focusable checkbox input
*/
- activate: function () {
- this.$input.focus();
- setTimeout(this.$input.select.bind(this.$input), 0);
+ getFocusableElement: function () {
+ return this.$input || $();
},
-
/**
* A boolean field is always set since false is a valid value.
*
@@ -686,7 +677,6 @@ var FieldText = DebouncedField.extend({
*/
start: function () {
this.$el.addClass('o_list_text o_form_textarea');
- this.$el.attr('id', this.idForLabel);
if (this.mode === 'edit') {
this.$textarea = $('<textarea>').appendTo(this.$el);
@@ -704,14 +694,12 @@ var FieldText = DebouncedField.extend({
//--------------------------------------------------------------------------
/**
- * Automatically selects the content of the field.
+ * Returns the associated <textarea/> element.
*
* @override
*/
- activate: function () {
- var $textarea = this.$('textarea');
- $textarea.focus();
- setTimeout($textarea.select.bind($textarea), 0);
+ getFocusableElement: function () {
+ return this.$textarea || $();
},
//--------------------------------------------------------------------------
@@ -753,8 +741,6 @@ var HandleWidget = AbstractField.extend({
tagName: 'span',
className: 'o_row_handle fa fa-arrows ui-sortable-handle',
description: "",
- readonly: true,
- replace_element: true,
supportedFieldTypes: ['integer'],
});
@@ -1093,7 +1079,6 @@ var PriorityWidget = AbstractField.extend({
'mouseout > a': '_onMouseOut',
'click > a': '_onClick',
},
- readonly: true,
supportedFieldTypes: ['selection'],
//--------------------------------------------------------------------------
@@ -1195,7 +1180,6 @@ var PriorityWidget = AbstractField.extend({
this.$('.o_priority_star').removeClass('fa-star-o').addClass('fa-star');
$(event.currentTarget).nextAll().removeClass('fa-star').addClass('fa-star-o');
},
-
});
var AttachmentImage = AbstractField.extend({
@@ -1210,7 +1194,6 @@ var StateSelectionWidget = AbstractField.extend({
},
'click li': '_setSelection'
},
- readonly: true,
supportedFieldTypes: ['selection'],
//--------------------------------------------------------------------------
@@ -1300,7 +1283,6 @@ var StateSelectionWidget = AbstractField.extend({
});
var LabelSelection = AbstractField.extend({
- readonly: true,
supportedFieldTypes: ['selection'],
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 06f38abffbb..abe3de97779 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -116,18 +116,16 @@ var FieldMany2One = AbstractField.extend({
// Public
//--------------------------------------------------------------------------
- activate: function () {
- this.$input.focus();
- setTimeout(this.$input.select.bind(this.$input), 0);
- },
/**
- * Focuses the input.
+ * @override
+ * @returns {jQuery}
*/
- focus: function () {
- if (this.mode === "edit") {
- this.$input.focus();
- }
+ getFocusableElement: function () {
+ return this.$input || $();
},
+ /**
+ * TODO
+ */
reinitialize: function (value) {
this.floating = false;
this._setValue(value);
@@ -366,7 +364,7 @@ var FieldMany2One = AbstractField.extend({
disable_multiple_selection: true,
on_selected: function (element_ids) {
self.reinitialize({id: element_ids[0]});
- self.focus();
+ self.activate();
}
})).open();
},
@@ -409,7 +407,7 @@ var FieldMany2One = AbstractField.extend({
*/
_onExternalButtonClick: function () {
if (!this.value) {
- this.focus();
+ this.activate();
return;
}
var self = this;
@@ -578,8 +576,7 @@ var FieldX2Many = AbstractField.extend({
}
var def = $.Deferred();
this.renderer.canBeSaved()
- .then(def.resolve.bind(def, true))
- .fail(def.resolve.bind(def, false));
+ .then(def.resolve.bind(def, true), def.resolve.bind(def, false));
return def;
},
@@ -610,6 +607,7 @@ var FieldX2Many = AbstractField.extend({
mode: this.mode,
addCreateLine: !this.isReadonly && this.activeActions.create,
addTrashIcon: !this.isReadonly,
+ viewType: 'list',
});
}
if (arch.tag === 'kanban') {
@@ -620,7 +618,8 @@ var FieldX2Many = AbstractField.extend({
};
this.renderer = new KanbanRenderer(this, this.value, {
arch: arch,
- record_options: record_options
+ record_options: record_options,
+ viewType: 'kanban',
});
}
return this.renderer ? this.renderer.appendTo(this.$el) : this._super();
@@ -940,7 +939,6 @@ var FieldMany2Many = FieldX2Many.extend({
var FieldMany2ManyTags = AbstractField.extend({
tag_template: "FieldMany2ManyTag",
className: "o_form_field o_form_field_many2manytags",
- replace_element: true,
supportedFieldTypes: ['many2many'],
custom_events: {
field_changed: '_onFieldChanged',
@@ -957,9 +955,23 @@ var FieldMany2ManyTags = AbstractField.extend({
// Public
//--------------------------------------------------------------------------
+ /**
+ * @override
+ */
activate: function () {
- this.many2one.focus();
+ return this.many2one ? this.many2one.activate() : false;
+ },
+ /**
+ * @override
+ * @returns {jQuery}
+ */
+ getFocusableElement: function () {
+ return this.many2one ? this.many2one.getFocusableElement() : $();
},
+ /**
+ * @override
+ * @returns {boolean}
+ */
isSet: function () {
return !!this.value && this.value.count;
},
@@ -1012,7 +1024,6 @@ var FieldMany2ManyTags = AbstractField.extend({
this.many2one.destroy();
}
this.many2one = new FieldMany2One(this, this.name, this.record, {
- idForLabel: this.idForLabel,
mode: 'edit',
viewType: this.viewType,
});
@@ -1343,7 +1354,6 @@ var FieldSelection = AbstractField.extend({
events: _.extend({}, AbstractField.prototype.events, {
'change': '_onChange',
}),
- replace_element: true,
/**
* @override
*/
@@ -1365,6 +1375,18 @@ var FieldSelection = AbstractField.extend({
/**
* @override
+ * @returns {jQuery}
+ */
+ getFocusableElement: function () {
+ return this.$el.is('select') ? this.$el : $();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
* @private
*/
_renderEdit: function () {