summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js
diff options
context:
space:
mode:
authorOdoo's Mergebot <xmo+roboto@odoo.com>2020-03-31 09:49:08 +0200
committerGitHub <noreply@github.com>2020-03-31 09:49:08 +0200
commit0e9f4e6cdea02573702577c4754b129f9d84cdd8 (patch)
treec0c83535f6c0f91a9a227e3e7959c8da6e902b3c /addons/web/static/src/js
parent30f4f610bf146638b71a0b2a936f8ae9acef502e (diff)
parent72feab43b16d9d49703eacb7c886aafad9faa6c5 (diff)
[IMP] web: convert FieldBoolean to Owl
This commit aims to convert the FieldBoolean into Owl. For this, we also add missing features to AbstractFieldOwl (and to the renderers that will use them), like the keyboard navigation. Enterprise PR: https://github.com/odoo/enterprise/pull/8041 Task 2193996 closes odoo/odoo#44267 Related: odoo/enterprise#8041 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
Diffstat (limited to 'addons/web/static/src/js')
-rw-r--r--addons/web/static/src/js/_deprecated/basic_fields.js154
-rw-r--r--addons/web/static/src/js/components/custom_checkbox.js58
-rw-r--r--addons/web/static/src/js/core/utils.js8
-rw-r--r--addons/web/static/src/js/fields/abstract_field_owl.js468
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js131
-rw-r--r--addons/web/static/src/js/fields/basic_fields_owl.js111
-rw-r--r--addons/web/static/src/js/fields/field_registry.js11
-rw-r--r--addons/web/static/src/js/fields/field_registry_owl.js31
-rw-r--r--addons/web/static/src/js/fields/field_wrapper.js46
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js8
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_popover.js48
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js7
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js8
13 files changed, 755 insertions, 334 deletions
diff --git a/addons/web/static/src/js/_deprecated/basic_fields.js b/addons/web/static/src/js/_deprecated/basic_fields.js
new file mode 100644
index 00000000000..2a70c3793c0
--- /dev/null
+++ b/addons/web/static/src/js/_deprecated/basic_fields.js
@@ -0,0 +1,154 @@
+////////////////////////////////////////////////////////////////////////////////
+// /!\ DEPRECATED
+//
+// Legacy Field Widgets are added in this file when they are converted into
+// Owl Component.
+////////////////////////////////////////////////////////////////////////////////
+
+odoo.define('web.basic_fields.deprecated', 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 core = require('web.core');
+
+var _lt = core._lt;
+
+var FieldBoolean = AbstractField.extend({
+ className: 'o_field_boolean',
+ description: _lt("Checkbox"),
+ events: _.extend({}, AbstractField.prototype.events, {
+ change: '_onChange',
+ }),
+ supportedFieldTypes: ['boolean'],
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Toggle the checkbox if it is activated due to a click on itself.
+ *
+ * @override
+ */
+ activate: function (options) {
+ var activated = this._super.apply(this, arguments);
+ // The formatValue of boolean fields renders HTML elements similar to
+ // the one rendered by the widget itself. Even though the event might
+ // have been fired on the non-widget version of this field, we can still
+ // test the presence of its custom class.
+ if (activated && options && options.event && $(options.event.target).closest('.custom-control.custom-checkbox').length) {
+ this._setValue(!this.value); // Toggle the checkbox
+ }
+ return activated;
+ },
+
+ /**
+ * @override
+ * @returns {jQuery} the focusable checkbox input
+ */
+ getFocusableElement: function () {
+ return this.mode === 'readonly' ? $() : this.$input;
+ },
+ /**
+ * A boolean field is always set since false is a valid value.
+ *
+ * @override
+ */
+ isSet: function () {
+ return true;
+ },
+ /**
+ * When the checkbox is rerendered, we need to check if it was the actual
+ * origin of the reset. If it is, we need to activate it back so it looks
+ * like it was not rerendered but is still the same input.
+ *
+ * @override
+ */
+ reset: function (record, event) {
+ var rendered = this._super.apply(this, arguments);
+ if (event && event.target.name === this.name) {
+ this.activate();
+ }
+ return rendered;
+ },
+ /**
+ * Associates the 'for' attribute of the internal label.
+ *
+ * @override
+ */
+ setIDForLabel: function (id) {
+ this._super.apply(this, arguments);
+ this.$('.custom-control-label').attr('for', id);
+ },
+
+ //--------------------------------------------------------------------------
+ // 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. In
+ * readonly mode, the checkbox is disabled.
+ *
+ * @override
+ * @private
+ */
+ _render: function () {
+ var $checkbox = this._formatValue(this.value);
+ this.$input = $checkbox.find('input');
+ this.$input.prop('disabled', this.mode === 'readonly');
+ this.$el.addClass($checkbox.attr('class'));
+ this.$el.empty().append($checkbox.contents());
+ },
+
+ //--------------------------------------------------------------------------
+ // 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} ev
+ */
+ _onKeydown: function (ev) {
+ switch (ev.which) {
+ case $.ui.keyCode.ENTER:
+ // prevent subsequent 'click' event (see _onKeydown of AbstractField)
+ ev.preventDefault();
+ this.$input.prop('checked', !this.value);
+ this._setValue(!this.value);
+ return;
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.RIGHT:
+ case $.ui.keyCode.DOWN:
+ case $.ui.keyCode.LEFT:
+ ev.preventDefault();
+ }
+ this._super.apply(this, arguments);
+ },
+});
+
+return {
+ FieldBoolean: FieldBoolean,
+};
+
+});
diff --git a/addons/web/static/src/js/components/custom_checkbox.js b/addons/web/static/src/js/components/custom_checkbox.js
new file mode 100644
index 00000000000..bc98dd7bab2
--- /dev/null
+++ b/addons/web/static/src/js/components/custom_checkbox.js
@@ -0,0 +1,58 @@
+odoo.define('web.CustomCheckbox', function (require) {
+ "use strict";
+
+ const utils = require('web.utils');
+
+ const { Component } = owl;
+
+ /**
+ * Custom checkbox
+ *
+ * Component that can be used in templates to render the custom checkbox of Odoo.
+ *
+ * <CustomCheckbox
+ * value="boolean"
+ * disabled="boolean"
+ * text="'Change the label text'"
+ * t-on-change="_onValueChange"
+ * />
+ *
+ * @extends Component
+ */
+ class CustomCheckbox extends Component {
+ /**
+ * @param {Object} [props]
+ * @param {string | number | null} [props.id]
+ * @param {boolean} [props.value=false]
+ * @param {boolean} [props.disabled=false]
+ * @param {string} [props.text]
+ */
+ constructor() {
+ super(...arguments);
+ this._id = `checkbox-comp-${utils.generateID()}`;
+ }
+ }
+
+ CustomCheckbox.props = {
+ id: {
+ type: [String, Number],
+ optional: 1,
+ },
+ disabled: {
+ type: Boolean,
+ optional: 1,
+ },
+ value: {
+ type: Boolean,
+ optional: 1,
+ },
+ text: {
+ type: String,
+ optional: 1,
+ },
+ };
+
+ CustomCheckbox.template = 'web.CustomCheckbox';
+
+ return CustomCheckbox;
+});
diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js
index 93e9bda5593..f1d4b00c6a9 100644
--- a/addons/web/static/src/js/core/utils.js
+++ b/addons/web/static/src/js/core/utils.js
@@ -446,6 +446,14 @@ var utils = {
return (/^\d+(\.\d*)? [^0-9]+$/).test(v);
},
/**
+ * Checks if a class is an extension of owl.Component.
+ *
+ * @param {any} value A class reference
+ */
+ isComponent: function (value) {
+ return value.prototype instanceof owl.Component;
+ },
+ /**
* Returns whether the given anchor is valid.
*
* This test is useful to prevent a crash that would happen if using an invalid
diff --git a/addons/web/static/src/js/fields/abstract_field_owl.js b/addons/web/static/src/js/fields/abstract_field_owl.js
index c9beafc9203..937fc2f88ee 100644
--- a/addons/web/static/src/js/fields/abstract_field_owl.js
+++ b/addons/web/static/src/js/fields/abstract_field_owl.js
@@ -9,34 +9,39 @@ odoo.define('web.AbstractFieldOwl', function (require) {
/**
* This file defines the Owl version of the AbstractField. Specific fields
* written in Owl should override this component.
+ *
+ * =========================================================================
+ *
+ * /!\ This api works almost exactly like the legacy one but
+ * /!\ it still could change! There are already a few methods that will be
+ * /!\ removed like setIdForLabel, setInvalidClass, etc..
+ *
+ * =========================================================================
*
- * Note that the API is not complete yet. Some features may not work properly
- * yet (e.g. part of keyboard navigation, invalid fields notification).
- *
- * This is the basic field widget used by all the views to render a field in a view.
- * These field widgets are mostly common to all views, in particular form and list
+ * This is the basic field component used by all the views to render a field in a view.
+ * These field components are mostly common to all views, in particular form and list
* views.
*
- * The responsabilities of a field widget are mainly:
+ * The responsabilities of a field component are mainly:
* - render a visual representation of the current value of a field
* - that representation is either in 'readonly' or in 'edit' mode
* - notify the rest of the system when the field has been changed by
* the user (in edit mode)
*
* Notes
- * - the widget is not supposed to be able to switch between modes. If another
- * mode is required, the view will take care of instantiating another widget.
+ * - the component is not supposed to be able to switch between modes. If another
+ * mode is required, the view will take care of instantiating another component.
* - notify the system when its value has changed and its mode is changed to 'readonly'
* - notify the system when some action has to be taken, such as opening a record
- * - the Field widget should not, ever, under any circumstance, be aware of
- * its parent. The way it communicates changes with the rest of the system is by
- * triggering events (with trigger_up). These events bubble up and are interpreted
+ * - the Field component should not, ever, under any circumstance, be aware of
+ * its parent. The way it communicates changes with the rest of the system is by
+ * triggering events. These events bubble up and are interpreted
* by the most appropriate parent.
*
- * Also, in some cases, it may not be practical to have the same widget for all
- * views. In that situation, you can have a 'view specific widget'. Just register
- * the widget in the registry prefixed by the view type and a dot. So, for example,
- * a form specific many2one widget should be registered as 'form.many2one'.
+ * Also, in some cases, it may not be practical to have the same component for all
+ * views. In that situation, you can have a 'view specific component'. Just register
+ * the component in the registry prefixed by the view type and a dot. So, for example,
+ * a form specific many2one component should be registered as 'form.many2one'.
*
* @module web.AbstractFieldOwl
*/
@@ -45,118 +50,28 @@ odoo.define('web.AbstractFieldOwl', function (require) {
* Abstract field class
*
* @constructor
- * @param {Widget} parent
- * @param {string} name The field name defined in the model
- * @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 {Component} parent
+ * @param {Object} props
+ * @param {string} props.fieldName The field name defined in the model
+ * @param {Object} props.record A record object (result of the get method
+ * of a basic model)
+ * @param {Object} [props.options]
+ * @param {string} [props.options.mode=readonly] should be 'readonly' or 'edit'
+ * @param {string} [props.options.viewType=default]
*/
- constructor(parent, props) {
- super(parent, props);
-
- const options = Object.assign({}, props.options);
- const record = props.record;
- // 'name' is the field name displayed by this widget
- this.name = props.fieldName;
-
- // the datapoint fetched from the model
- this.record = record;
-
- // the 'field' property is a description of all the various field properties,
- // such as the type, the comodel (relation), ...
- this.field = record.fields[this.name];
-
- // the 'viewType' is the type of the view in which the field widget is
- // instantiated. For standalone widgets, a 'default' viewType is set.
- this.viewType = options.viewType || 'default';
-
- // the 'attrs' property contains the attributes of the xml 'field' tag,
- // the inner views...
- const fieldsInfo = record.fieldsInfo[this.viewType];
- this.attrs = options.attrs || (fieldsInfo && fieldsInfo[this.name]) || {};
-
- // the 'additionalContext' property contains the attributes to pass through the context.
- this.additionalContext = options.additionalContext || {};
-
- // 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.
- this.value = record.data[this.name];
-
- // recordData tracks the values for the other fields for the same record.
- // note that it is expected to be mostly a readonly property, you cannot
- // use this to try to change other fields value, this is not how it is
- // supposed to work. Also, do not use this.recordData[this.name] to get
- // the current value, this could be out of sync after a _setValue.
- this.recordData = record.data;
-
- // the 'string' property is a human readable (and translated) description
- // of the field. Mostly useful to be displayed in various places in the
- // UI, such as tooltips or create dialogs.
- this.string = this.attrs.string || this.field.string || this.name;
-
- // Widget can often be configured in the 'options' attribute in the
- // xml 'field' tag. These options are saved (and evaled) in nodeOptions
- this.nodeOptions = this.attrs.options || {};
-
- // dataPointID is the id corresponding to the current record in the model.
- // Its intended use is to be able to tag any messages going upstream,
- // so the view knows which records was changed for example.
- this.dataPointID = record.id;
+ constructor() {
+ super(...arguments);
- // this is the res_id for the record in database. Obviously, it is
- // readonly. Also, when the user is creating a new record, there is
- // no res_id. When the record will be created, the field widget will
- // be destroyed (when the form view switches to readonly mode) and a new
- // widget with a res_id in mode readonly will be created.
- this.res_id = record.res_id;
-
- // useful mostly to trigger rpcs on the correct model
- this.model = record.model;
-
- // a widget can be in two modes: 'edit' or 'readonly'. This mode should
- // never be changed, if a view changes its mode, it will destroy and
- // recreate a new field widget.
- this.mode = options.mode || "readonly";
-
- // this flag tracks if the widget is in a valid state, meaning that the
- // current value represented in the DOM is a value that can be parsed
- // and saved. For example, a float field can only use a number and not
- // a string.
this._isValid = true;
-
- // this is the last value that was set by the user, unparsed. This is
+ // 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;
-
- // formatType is used to determine which format (and parse) functions
- // to call to format the field's value to insert into the DOM (typically
- // put into a span or an input), and to parse the value from the input
- // to send it to the server. These functions are chosen according to
- // the 'widget' attrs if is is given, and if it is a valid key, with a
- // fallback on the field type, ensuring that the value is formatted and
- // displayed according to the chosen widget, if any.
- this.formatType = this.attrs.widget in field_utils.format ?
- this.attrs.widget :
- this.field.type;
- // formatOptions (resp. parseOptions) is a dict of options passed to
- // calls to the format (resp. parse) function.
- this.formatOptions = {};
- this.parseOptions = {};
-
- // if we add decorations, we need to reevaluate the field whenever any
- // value from the record is changed
- if (this.attrs.decorations) {
- this.resetOnAnyFieldChange = true;
- }
+ this._lastSetValue = undefined;
useListener('keydown', this._onKeydown);
useListener('navigation-move', this._onNavigationMove);
onMounted(() => this._applyDecorations());
onPatched(() => this._applyDecorations());
}
-
/**
* Hack: studio tries to find the field with a selector base on its
* name, before it is mounted into the DOM. Ideally, this should be
@@ -174,38 +89,91 @@ odoo.define('web.AbstractFieldOwl', function (require) {
this.el.classList.add('o_field_widget');
return res;
}
-
/**
* @async
* @param {Object} [nextProps]
- * @param {Object} [nextProps.record]
- * @param {Object} [nextProps.event]
* @returns {Promise}
*/
async willUpdateProps(nextProps) {
- this.record = nextProps.record;
- this.recordData = this.record.data;
- this.value = this.recordData[this.name];
- this.lastSetValue = undefined;
+ this._lastSetValue = undefined;
return super.willUpdateProps(nextProps);
}
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
+ //----------------------------------------------------------------------
+ // Getters
+ //----------------------------------------------------------------------
/**
- * Returns the main field's DOM element (jQuery form) which can be focused
- * by the browser.
+ * This contains the attributes to pass through the context.
+ *
+ * @returns {Object}
+ */
+ get additionalContext() {
+ return this.options.additionalContext || {};
+ }
+ /**
+ * This contains the attributes of the xml 'field' tag, the inner views...
+ *
+ * @returns {Object}
+ */
+ get attrs() {
+ const fieldsInfo = this.record.fieldsInfo[this.viewType];
+ return this.options.attrs || (fieldsInfo && fieldsInfo[this.name]) || {};
+ }
+ /**
+ * Id corresponding to the current record in the model.
+ * Its intended use is to be able to tag any messages going upstream,
+ * so the view knows which records was changed for example.
+ *
+ * @returns {string}
+ */
+ get dataPointId() {
+ return this.record.id;
+ }
+ /**
+ * This is a description of all the various field properties,
+ * such as the type, the comodel (relation), ...
+ *
+ * @returns {string}
+ */
+ get field() {
+ return this.record.fields[this.name];
+ }
+ /**
+ * Returns the main field's DOM element which can be focused by the browser.
*
- * @returns {HTMLElement|null} main focusable element inside the widget
+ * @returns {HTMLElement|null} main focusable element inside the component
*/
get focusableElement() {
return null;
}
/**
+ * Returns the additional options pass to the format function.
+ * Override this getter to add options.
+ *
+ * @returns {Object}
+ */
+ get formatOptions() {
+ return {};
+ }
+ /**
+ * Used to determine which format (and parse) functions
+ * to call to format the field's value to insert into the DOM (typically
+ * put into a span or an input), and to parse the value from the input
+ * to send it to the server. These functions are chosen according to
+ * the 'widget' attrs if is is given, and if it is a valid key, with a
+ * fallback on the field type, ensuring that the value is formatted and
+ * displayed according to the chosen widget, if any.
+ *
+ * @returns {string}
+ */
+ get formatType() {
+ return this.attrs.widget in field_utils.format ?
+ this.attrs.widget : this.field.type;
+ }
+ /**
* Returns whether or not the field is empty and can thus be hidden. This
- * method is typically called when the widget is in readonly, to hide it
+ * method is typically called when the component is in readonly, to hide it
* (and its label) if it is empty.
*
* @returns {boolean}
@@ -214,39 +182,171 @@ odoo.define('web.AbstractFieldOwl', function (require) {
return !this.isSet;
}
/**
- * Returns true if the widget has a visible element that can take the focus
+ * Returns true if the component has a visible element that can take the focus
*
* @returns {boolean}
*/
get isFocusable() {
const focusable = this.focusableElement;
// check if element is visible
- return focusable && !!(focusable.offsetWidth || focusable.offsetHeight
- || focusable.getClientRects().length);
+ return focusable && !!(focusable.offsetWidth ||
+ focusable.offsetHeight || focusable.getClientRects().length);
}
/**
+ * Determines if the field value is set to a meaningful
+ * value. This is useful to determine if a field should be displayed as empty
+ *
* @returns {boolean}
*/
get isSet() {
return !!this.value;
}
/**
+ * Tracks if the component is in a valid state, meaning that the current
+ * value represented in the DOM is a value that can be parsed and saved.
+ * For example, a float field can only use a number and not a string.
+ *
* @returns {boolean}
*/
get isValid() {
return this._isValid;
}
/**
- * Activates the field widget. By default, activation means focusing and
+ * Fields can be in two modes: 'edit' or 'readonly'.
+ *
+ * @returns {string}
+ */
+ get mode() {
+ return this.options.mode || "readonly";
+ }
+ /**
+ * Useful mostly to trigger rpcs on the correct model.
+ *
+ * @returns {string}
+ */
+ get model() {
+ return this.record.model;
+ }
+ /**
+ * The field name displayed by this component.
+ *
+ * @returns {string}
+ */
+ get name() {
+ return this.props.fieldName;
+ }
+ /**
+ * Component can often be configured in the 'options' attribute in the
+ * xml 'field' tag. These options are saved (and evaled) in nodeOptions.
+ *
+ * @returns {Object}
+ */
+ get nodeOptions() {
+ return this.attrs.options || {};
+ }
+ /**
+ * @returns {Object}
+ */
+ get options() {
+ return this.props.options || {};
+ }
+ /**
+ * Returns the additional options passed to the parse function.
+ * Override this getter to add options.
+ *
+ * @returns {Object}
+ */
+ get parseOptions() {
+ return {};
+ }
+ /**
+ * The datapoint fetched from the model.
+ *
+ * @returns {Object}
+ */
+ get record() {
+ return this.props.record;
+ }
+ /**
+ * Tracks the values for the other fields for the same record.
+ * note that it is expected to be mostly a readonly property, you cannot
+ * use this to try to change other fields value, this is not how it is
+ * supposed to work. Also, do not use this.recordData[this.name] to get
+ * the current value, this could be out of sync after a _setValue.
+ *
+ * @returns {Object}
+ */
+ get recordData() {
+ return this.record.data;
+ }
+ /**
+ * If this flag is set to true, the field component will be reset on
+ * every change which is made in the view (if the view supports it).
+ * This is currently a form view feature.
+ *
+ * /!\ This getter could be removed when basic views (form, list, kanban)
+ * are converted.
+ *
+ * @returns {boolean}
+ */
+ get resetOnAnyFieldChange() {
+ return !!this.attrs.decorations;
+ }
+ /**
+ * The res_id of the record in database.
+ * When the user is creating a new record, there is no res_id.
+ * When the record will be created, the field component will
+ * be destroyed (when the form view switches to readonly mode) and a
+ * new component with a res_id in mode readonly will be created.
+ *
+ * @returns {Number}
+ */
+ get resId() {
+ return this.record.res_id;
+ }
+ /**
+ * Human readable (and translated) description of the field.
+ * Mostly useful to be displayed in various places in the
+ * UI, such as tooltips or create dialogs.
+ *
+ * @returns {string}
+ */
+ get string() {
+ return this.attrs.string || this.field.string || this.name;
+ }
+ /**
+ * Tracks the current (parsed if needed) value of the field.
+ *
+ * @returns {any}
+ */
+ get value() {
+ return this.record.data[this.name];
+ }
+ /**
+ * The type of the view in which the field component is instantiated.
+ * For standalone components, a 'default' viewType is set.
+ *
+ * @returns {string}
+ */
+ get viewType() {
+ return this.options.viewType || 'default';
+ }
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * Activates the field component. By default, activation means focusing and
* selecting (if possible) the associated focusable element. The selecting
- * part can be disabled. In that case, note that the focused input/textarea
+ * part can be disabled. In that case, note that the focused input/textarea
* will have the cursor at the very end.
*
* @param {Object} [options]
* @param {boolean} [options.noselect=false] if false and the input
* is of type text or textarea, the content will also be selected
* @param {Event} [options.event] the event which fired this activation
- * @returns {boolean} true if the widget was activated, false if the
+ * @returns {boolean} true if the component was activated, false if the
* focusable element was not found or invisible
*/
activate(options) {
@@ -264,12 +364,12 @@ odoo.define('web.AbstractFieldOwl', function (require) {
return false;
}
/**
- * This function should be implemented by widgets that are not able to
+ * This function should be implemented by components that are not able to
* notify their environment when their value changes (maybe because their
* are not aware of the changes) or that may have a value in a temporary
* state (maybe because some action should be performed to validate it
* before notifying it). This is typically called before trying to save the
- * widget's value, so it should call _setValue() to notify the environment
+ * component's value, so it should call _setValue() to notify the environment
* if the value changed but was not notified.
*
* @abstract
@@ -277,37 +377,47 @@ odoo.define('web.AbstractFieldOwl', function (require) {
*/
commitChanges() {}
/**
+ * Remove the invalid class on a field
+ *
+ * This function should be removed when BasicRenderer will be rewritten in owl
+ */
+ removeInvalidClass() {
+ this.el.classList.remove('o_field_invalid');
+ this.el.removeAttribute('aria-invalid');
+ }
+ /**
* Sets the given id on the focusable element of the field and as 'for'
* attribute of potential internal labels.
+ *
+ * This function should be removed when BasicRenderer will be rewritten in owl
*
* @param {string} id
*/
- setIDForLabel(id) {
+ setIdForLabel(id) {
if (this.focusableElement) {
this.focusableElement.setAttribute('id', id);
}
}
/**
- * Update the modifiers with the newest value.
- * Now this.attrs.modifiersValue can be used consistantly even with
- * conditional modifiers inside field widgets, and without needing new
- * events or synchronization between the widgets, renderer and controller
- *
- * @param {Object | null} modifiers the updated modifiers
- * @override
+ * add the invalid class on a field
+ *
+ * This function should be removed when BasicRenderer will be rewritten in owl
*/
- updateModifiersValue(modifiers) {
- this.attrs.modifiersValue = modifiers || {};
+ setInvalidClass() {
+ this.el.classList.add('o_field_invalid');
+ this.el.setAttribute('aria-invalid', 'true');
}
- //--------------------------------------------------------------------------
+ //----------------------------------------------------------------------
// Private
- //--------------------------------------------------------------------------
+ //----------------------------------------------------------------------
/**
* Apply field decorations (only if field-specific decorations have been
* defined in an attribute).
*
+ * This function should be removed when BasicRenderer will be rewritten in owl
+ *
* @private
*/
_applyDecorations() {
@@ -326,13 +436,13 @@ odoo.define('web.AbstractFieldOwl', function (require) {
* @returns {string}
*/
_formatValue(value) {
- const options = _.extend({}, this.nodeOptions,
+ const options = Object.assign({}, this.nodeOptions,
{ data: this.recordData }, this.formatOptions);
return field_utils.format[this.formatType](value, this.field, options);
}
/**
* This method check if a value is the same as the current value of the
- * field. For example, a fieldDate widget might want to use the moment
+ * field. For example, a fieldDate component might want to use the moment
* specific value isSame instead of ===.
*
* This method is used by the _setValue method.
@@ -355,18 +465,18 @@ odoo.define('web.AbstractFieldOwl', function (require) {
return field_utils.parse[this.formatType](value, this.field, this.parseOptions);
}
/**
- * This method is called by the widget, to change its value and to notify
- * the outside world of its new state. This method also validates the new
- * value. Note that this method does not rerender the widget, it should be
- * handled by the widget itself, if necessary.
+ * This method is called by the component, to change its value and to notify
+ * the outside world of its new state. This method also validates the new
+ * value. Note that this method does not rerender the component, it should be
+ * handled by the component itself, if necessary.
*
* @private
* @param {any} value
* @param {Object} [options]
* @param {boolean} [options.doNotSetDirty=false] if true, the basic model
* will not consider that this field is dirty, even though it was changed.
- * Please do not use this flag unless you really need it. Our only use
- * case is currently the pad widget, which does a _setValue in the
+ * Please do not use this flag unless you really need it. Our only use
+ * case is currently the pad component, which does a _setValue in the
* renderEdit method.
* @param {boolean} [options.notifyChange=true] if false, the basic model
* will not notify and not trigger the onchange, even though it was changed.
@@ -377,16 +487,16 @@ odoo.define('web.AbstractFieldOwl', function (require) {
_setValue(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 === '')) {
+ if (this._lastSetValue === value || (this.value === false && value === '')) {
return Promise.resolve();
}
- this.lastSetValue = value;
+ this._lastSetValue = value;
try {
value = this._parseValue(value);
this._isValid = true;
} catch (e) {
this._isValid = false;
- this.trigger('set-dirty', {dataPointID: this.dataPointID});
+ this.trigger('set-dirty', {dataPointID: this.dataPointId});
return Promise.reject({message: "Value set is not valid"});
}
if (!(options && options.forceChange) && this._isSameValue(value)) {
@@ -396,7 +506,7 @@ odoo.define('web.AbstractFieldOwl', function (require) {
const changes = {};
changes[this.name] = value;
this.trigger('field-changed', {
- dataPointID: this.dataPointID,
+ dataPointID: this.dataPointId,
changes: changes,
viewType: this.viewType,
doNotSetDirty: options && options.doNotSetDirty,
@@ -408,20 +518,14 @@ odoo.define('web.AbstractFieldOwl', function (require) {
});
}
- //--------------------------------------------------------------------------
+ //----------------------------------------------------------------------
// Handlers
- //--------------------------------------------------------------------------
+ //----------------------------------------------------------------------
/**
* Intercepts navigation keyboard events to prevent their default behavior
* and notifies the view so that it can handle it its own way.
*
- * Note: the navigation keyboard events are stopped so that potential parent
- * abstract field does not trigger the navigation_move event a second time.
- * However, this might be controversial, we might wanna let the event
- * continue its propagation and flag it to say that navigation has already
- * been handled (TODO ?).
- *
* @private
* @param {KeyEvent} ev
*/
@@ -430,9 +534,8 @@ odoo.define('web.AbstractFieldOwl', function (require) {
case $.ui.keyCode.TAB:
this.trigger('navigation-move', {
direction: ev.shiftKey ? 'previous' : 'next',
+ originalEvent: ev,
});
- // TODO: stop/prevent original event if navigation-move event
- // has been handled
break;
case $.ui.keyCode.ENTER:
// We preventDefault the ENTER key because of two coexisting behaviours:
@@ -479,19 +582,14 @@ odoo.define('web.AbstractFieldOwl', function (require) {
}
/**
- * An object representing fields to be fetched by the model eventhough not present in the view
+ * An object representing fields to be fetched by the model even though not
+ * present in the view.
* This object contains "field name" as key and an object as value.
* That value object must contain the key "type"
- * see FieldBinaryImage for an example.
+ * @see FieldBinaryImage for an example.
*/
AbstractField.fieldDependencies = {};
/**
- * 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.
- */
- AbstractField.resetOnAnyFieldChange = 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].
@@ -500,15 +598,15 @@ odoo.define('web.AbstractFieldOwl', function (require) {
*/
AbstractField.specialData = false;
/**
- * to override to indicate which field types are supported by the widget
+ * to override to indicate which field types are supported by the component
*
- * @type Array<String>
+ * @type Array<string>
*/
AbstractField.supportedFieldTypes = [];
/**
- * To override to give a user friendly name to the widget.
+ * To override to give a user friendly name to the component.
*
- * @type <string>
+ * @type string
*/
AbstractField.description = "";
/**
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 3a3a037a0f0..ce7a7fac07d 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -11,6 +11,7 @@ var AbstractField = require('web.AbstractField');
var config = require('web.config');
var core = require('web.core');
var datepicker = require('web.datepicker');
+var deprecatedFields = require('web.basic_fields.deprecated');
var dom = require('web.dom');
var Domain = require('web.Domain');
var DomainSelector = require('web.DomainSelector');
@@ -24,6 +25,8 @@ var field_utils = require('web.field_utils');
var time = require('web.time');
var ColorpickerDialog = require('web.ColorpickerDialog');
+let FieldBoolean = deprecatedFields.FieldBoolean;
+
require("web.zoomodoo");
var qweb = core.qweb;
@@ -986,134 +989,6 @@ var FieldMonetary = NumericField.extend({
},
});
-var FieldBoolean = AbstractField.extend({
- className: 'o_field_boolean',
- description: _lt("Checkbox"),
- events: _.extend({}, AbstractField.prototype.events, {
- change: '_onChange',
- }),
- supportedFieldTypes: ['boolean'],
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Toggle the checkbox if it is activated due to a click on itself.
- *
- * @override
- */
- activate: function (options) {
- var activated = this._super.apply(this, arguments);
- // The formatValue of boolean fields renders HTML elements similar to
- // the one rendered by the widget itself. Even though the event might
- // have been fired on the non-widget version of this field, we can still
- // test the presence of its custom class.
- if (activated && options && options.event && $(options.event.target).closest('.custom-control.custom-checkbox').length) {
- this._setValue(!this.value); // Toggle the checkbox
- }
- return activated;
- },
-
- /**
- * @override
- * @returns {jQuery} the focusable checkbox input
- */
- getFocusableElement: function () {
- return this.mode === 'readonly' ? $() : this.$input;
- },
- /**
- * A boolean field is always set since false is a valid value.
- *
- * @override
- */
- isSet: function () {
- return true;
- },
- /**
- * When the checkbox is rerendered, we need to check if it was the actual
- * origin of the reset. If it is, we need to activate it back so it looks
- * like it was not rerendered but is still the same input.
- *
- * @override
- */
- reset: function (record, event) {
- var rendered = this._super.apply(this, arguments);
- if (event && event.target.name === this.name) {
- this.activate();
- }
- return rendered;
- },
- /**
- * Associates the 'for' attribute of the internal label.
- *
- * @override
- */
- setIDForLabel: function (id) {
- this._super.apply(this, arguments);
- this.$('.custom-control-label').attr('for', id);
- },
-
- //--------------------------------------------------------------------------
- // 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. In
- * readonly mode, the checkbox is disabled.
- *
- * @override
- * @private
- */
- _render: function () {
- var $checkbox = this._formatValue(this.value);
- this.$input = $checkbox.find('input');
- this.$input.prop('disabled', this.mode === 'readonly');
- this.$el.addClass($checkbox.attr('class'));
- this.$el.empty().append($checkbox.contents());
- },
-
- //--------------------------------------------------------------------------
- // 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} ev
- */
- _onKeydown: function (ev) {
- switch (ev.which) {
- case $.ui.keyCode.ENTER:
- // prevent subsequent 'click' event (see _onKeydown of AbstractField)
- ev.preventDefault();
- this.$input.prop('checked', !this.value);
- this._setValue(!this.value);
- return;
- case $.ui.keyCode.UP:
- case $.ui.keyCode.RIGHT:
- case $.ui.keyCode.DOWN:
- case $.ui.keyCode.LEFT:
- ev.preventDefault();
- }
- this._super.apply(this, arguments);
- },
-});
-
var FieldInteger = NumericField.extend({
description: _lt("Integer"),
className: 'o_field_integer o_field_number',
diff --git a/addons/web/static/src/js/fields/basic_fields_owl.js b/addons/web/static/src/js/fields/basic_fields_owl.js
new file mode 100644
index 00000000000..a00b14fb9ee
--- /dev/null
+++ b/addons/web/static/src/js/fields/basic_fields_owl.js
@@ -0,0 +1,111 @@
+odoo.define('web.basic_fields_owl', function (require) {
+ "use strict";
+
+ const AbstractField = require('web.AbstractFieldOwl');
+ const CustomCheckbox = require('web.CustomCheckbox');
+ const core = require('web.core');
+
+ const _lt = core._lt;
+
+ class FieldBoolean extends AbstractField {
+ patched() {
+ super.patched();
+ if (this.props.event && this.props.event.target === this) {
+ this.activate();
+ }
+ }
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * @override
+ * @returns {HTMLElement|null} the focusable checkbox input
+ */
+ get focusableElement() {
+ return this.mode === 'readonly' ? null : this.el.querySelector('input');
+ }
+ /**
+ * A boolean field is always set since false is a valid value.
+ *
+ * @override
+ */
+ get isSet() {
+ return true;
+ }
+ /**
+ * Toggle the checkbox if it is activated due to a click on itself.
+ *
+ * @override
+ * @param {Object} [options]
+ * @param {Event} [options.event] the event which fired this activation
+ * @returns {boolean} true if the component was activated, false if the
+ * focusable element was not found or invisible
+ */
+ activate(options) {
+ const activated = super.activate(options);
+ // The event might have been fired on the non field version of
+ // this field, we can still test the presence of its custom class.
+ if (activated && options && options.event && options.event.target
+ .closest('.custom-control.custom-checkbox')) {
+ this._setValue(!this.value); // Toggle the checkbox
+ }
+ return activated;
+ }
+ /**
+ * Associates the 'for' attribute of the internal label.
+ *
+ * @override
+ */
+ setIdForLabel(id) {
+ super.setIdForLabel(id);
+ this.el.querySelector('label').setAttribute('for', id);
+ }
+
+ //----------------------------------------------------------------------
+ // Handlers
+ //----------------------------------------------------------------------
+
+ /**
+ * Properly update the value when the checkbox is (un)ticked to trigger
+ * possible onchanges.
+ *
+ * @private
+ */
+ _onChange(ev) {
+ this._setValue(ev.target.checked);
+ }
+ /**
+ * Implement keyboard movements. Mostly useful for its environment, such
+ * as a list view.
+ *
+ * @override
+ * @private
+ * @param {KeyEvent} ev
+ */
+ _onKeydown(ev) {
+ switch (ev.which) {
+ case $.ui.keyCode.ENTER:
+ // prevent subsequent 'click' event (see _onKeydown of AbstractField)
+ ev.preventDefault();
+ this._setValue(!this.value);
+ return;
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.RIGHT:
+ case $.ui.keyCode.DOWN:
+ case $.ui.keyCode.LEFT:
+ ev.preventDefault();
+ }
+ super._onKeydown(ev);
+ }
+ }
+ FieldBoolean.components = { CustomCheckbox };
+ FieldBoolean.description = _lt("Checkbox");
+ FieldBoolean.supportedFieldTypes = ['boolean'];
+ FieldBoolean.template = 'web.FieldBoolean';
+
+ return {
+ FieldBoolean,
+ };
+});
diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js
index 49f1f678304..b8b3fe61452 100644
--- a/addons/web/static/src/js/fields/field_registry.js
+++ b/addons/web/static/src/js/fields/field_registry.js
@@ -3,7 +3,16 @@ odoo.define('web.field_registry', function (require) {
var Registry = require('web.Registry');
-return new Registry();
+var FieldRegistry = Registry.extend({
+ add: function (key, value, score) {
+ if (value.prototype instanceof owl.Component) {
+ throw new Error("This registry should not contain any Component. Use 'web.field_registry_owl' instead.");
+ }
+ return this._super(...arguments);
+ },
+});
+
+return new FieldRegistry();
});
odoo.define('web._field_registry', function (require) {
diff --git a/addons/web/static/src/js/fields/field_registry_owl.js b/addons/web/static/src/js/fields/field_registry_owl.js
new file mode 100644
index 00000000000..f5f7d900ef8
--- /dev/null
+++ b/addons/web/static/src/js/fields/field_registry_owl.js
@@ -0,0 +1,31 @@
+odoo.define('web.field_registry_owl', function (require) {
+ "use strict";
+
+ const Registry = require('web.Registry');
+
+ const FieldRegistry = Registry.extend({
+ add: function (key, value, score) {
+ if (!(value.prototype instanceof owl.Component)) {
+ throw new Error("This registry should only contain subclasses of Component");
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ return new FieldRegistry();
+});
+
+odoo.define('web._field_registry_owl', function (require) {
+ "use strict";
+
+ /**
+ * This module registers field components (specifications of the AbstractField Component)
+ */
+
+ const basicFields = require('web.basic_fields_owl');
+ const registry = require('web.field_registry_owl');
+
+ // Basic fields
+ registry
+ .add('boolean', basicFields.FieldBoolean);
+});
diff --git a/addons/web/static/src/js/fields/field_wrapper.js b/addons/web/static/src/js/fields/field_wrapper.js
index 0b808d0c732..8cf747d84e9 100644
--- a/addons/web/static/src/js/fields/field_wrapper.js
+++ b/addons/web/static/src/js/fields/field_wrapper.js
@@ -33,8 +33,6 @@ odoo.define('web.FieldWrapper', function (require) {
this._data.res_id = record.res_id;
this._data.model = record.model;
this._data.mode = options.mode || "readonly";
- this._data._isValid = true;
- this._data.lastSetValue = undefined;
this._data.formatType = this._data.attrs.widget in field_utils.format ?
this._data.attrs.widget :
this._data.field.type;
@@ -43,9 +41,19 @@ odoo.define('web.FieldWrapper', function (require) {
if (this._data.attrs.decorations) {
this._data.resetOnAnyFieldChange = true;
}
- for (const key in this._data) {
+
+ for (let key in this._data) {
Object.defineProperty(this, key, {
- get: () => (this.el ? this.componentRef.comp : this._data)[key],
+ get: () => {
+ if (this.el) {
+ if (key === 'dataPointID') {
+ return this.componentRef.comp.dataPointId;
+ } else if (key === 'res_id') {
+ return this.componentRef.comp.resId;
+ }
+ }
+ return (this.el ? this.componentRef.comp : this._data)[key];
+ },
});
}
}
@@ -60,9 +68,6 @@ odoo.define('web.FieldWrapper', function (require) {
get fieldDependencies() {
return this.Component.fieldDependencies;
}
- get resetOnAnyFieldChange() {
- return this.Component.resetOnAnyFieldChange;
- }
get specialData() {
return this.Component.specialData;
}
@@ -80,11 +85,11 @@ odoo.define('web.FieldWrapper', function (require) {
// Public
//----------------------------------------------------------------------
- activate(options) {
- return this.componentRef.comp.activate(options);
+ activate() {
+ return this.componentRef.comp.activate(...arguments);
}
commitChanges() {
- return this.componentRef.comp.commitChanges();
+ return this.componentRef.comp.commitChanges(...arguments);
}
getFocusableElement() {
return $(this.componentRef.comp.focusableElement);
@@ -99,7 +104,7 @@ odoo.define('web.FieldWrapper', function (require) {
if (this.componentRef.comp) {
return this.componentRef.comp.isSet;
}
- // because of the willStart, the real field widget may not be
+ // because of the willStart, the real field component may not be
// instantiated yet when the renderer first asks if it is set
// (only the wrapper is instantiated), so we instantiate one
// with the same props, get its 'isSet' status, and destroy it.
@@ -111,14 +116,27 @@ odoo.define('web.FieldWrapper', function (require) {
isValid() {
return this.componentRef.comp.isValid;
}
+ removeInvalidClass() {
+ return this.componentRef.comp.removeInvalidClass(...arguments);
+ }
reset(record, event) {
return this.update({record, event});
}
- setIDForLabel(id) {
- return this.componentRef.comp.setIDForLabel(id);
+ setIDForLabel() {
+ return this.componentRef.comp.setIdForLabel(...arguments);
+ }
+ setInvalidClass() {
+ return this.componentRef.comp.setInvalidClass(...arguments);
}
updateModifiersValue(modifiers) {
- return this.componentRef.comp.updateModifiersValue(modifiers);
+ if (this.props.options.attrs) {
+ this.props.options.attrs.modifiersValue = modifiers || {};
+ } else {
+ const viewType = this.props.options.viewType || 'default';
+ const fieldsInfo = this.props.record.fieldsInfo[viewType];
+ fieldsInfo[this.props.fieldName].modifiersValue = modifiers || {};
+ }
+ this.componentRef.comp.props = this.props;
}
}
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index 140c338d6f6..4d266657c71 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -15,6 +15,7 @@ var BasicController = require('web.BasicController');
var BasicModel = require('web.BasicModel');
var config = require('web.config');
var fieldRegistry = require('web.field_registry');
+var fieldRegistryOwl = require('web.field_registry_owl');
var pyUtils = require('web.py_utils');
var utils = require('web.utils');
@@ -78,7 +79,8 @@ var BasicView = AbstractView.extend({
_getFieldWidgetClass: function (viewType, field, attrs) {
var FieldWidget;
if (attrs.widget) {
- FieldWidget = fieldRegistry.getAny([viewType + "." + attrs.widget, attrs.widget]);
+ FieldWidget = fieldRegistryOwl.getAny([viewType + "." + attrs.widget, attrs.widget]) ||
+ fieldRegistry.getAny([viewType + "." + attrs.widget, attrs.widget]);
if (!FieldWidget) {
console.warn("Missing widget: ", attrs.widget, " for field", attrs.name, "of type", field.type);
}
@@ -87,7 +89,9 @@ var BasicView = AbstractView.extend({
// is not specified in the view
FieldWidget = fieldRegistry.get('kanban.many2many_tags');
}
- return FieldWidget || fieldRegistry.getAny([viewType + "." + field.type, field.type, "abstract"]);
+ return FieldWidget ||
+ fieldRegistryOwl.getAny([viewType + "." + field.type, field.type, "abstract"]) ||
+ fieldRegistry.getAny([viewType + "." + field.type, field.type, "abstract"]);
},
/**
* In some cases, we already have a preloaded record
diff --git a/addons/web/static/src/js/views/calendar/calendar_popover.js b/addons/web/static/src/js/views/calendar/calendar_popover.js
index d804f74e53e..9b4af4bb065 100644
--- a/addons/web/static/src/js/views/calendar/calendar_popover.js
+++ b/addons/web/static/src/js/views/calendar/calendar_popover.js
@@ -2,10 +2,13 @@ odoo.define('web.CalendarPopover', function (require) {
"use strict";
var fieldRegistry = require('web.field_registry');
+const fieldRegistryOwl = require('web.field_registry_owl');
+const FieldWrapper = require('web.FieldWrapper');
var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
var Widget = require('web.Widget');
+const { WidgetAdapterMixin } = require('web.OwlCompatibility');
-var CalendarPopover = Widget.extend(StandaloneFieldManagerMixin, {
+var CalendarPopover = Widget.extend(WidgetAdapterMixin, StandaloneFieldManagerMixin, {
template: 'CalendarView.event.popover',
events: {
'click .o_cw_popover_edit': '_onClickPopoverEdit',
@@ -45,6 +48,25 @@ var CalendarPopover = Widget.extend(StandaloneFieldManagerMixin, {
});
return this._super.apply(this, arguments);
},
+ /**
+ * @override
+ */
+ destroy: function () {
+ this._super.apply(this, arguments);
+ WidgetAdapterMixin.destroy.call(this);
+ },
+ /**
+ * Called each time the widget is attached into the DOM.
+ */
+ on_attach_callback: function () {
+ WidgetAdapterMixin.on_attach_callback.call(this);
+ },
+ /**
+ * Called each time the widget is detached from the DOM.
+ */
+ on_detach_callback: function () {
+ WidgetAdapterMixin.on_detach_callback.call(this);
+ },
//--------------------------------------------------------------------------
// Public
@@ -129,8 +151,20 @@ var CalendarPopover = Widget.extend(StandaloneFieldManagerMixin, {
var record = self.model.get(recordID);
_.each(fieldsToGenerate, function (field) {
if (field.invisible) return;
- var FieldClass = fieldRegistry.getAny([field.widget, field.type]);
- var fieldWidget = new FieldClass(self, field.name, record, self.displayFields[field.name]);
+ let isLegacy = true;
+ let fieldWidget;
+ let FieldClass = fieldRegistryOwl.getAny([field.widget, field.type]);
+ if (FieldClass) {
+ isLegacy = false;
+ fieldWidget = new FieldWrapper(this, FieldClass, {
+ fieldName: field.name,
+ record,
+ options: self.displayFields[field.name],
+ });
+ } else {
+ FieldClass = fieldRegistry.getAny([field.widget, field.type]);
+ fieldWidget = new FieldClass(self, field.name, record, self.displayFields[field.name]);
+ }
self._registerWidget(recordID, field.name, fieldWidget);
var $field = $('<li>', {class: 'list-group-item flex-shrink-0 d-flex flex-wrap'});
@@ -139,7 +173,13 @@ var CalendarPopover = Widget.extend(StandaloneFieldManagerMixin, {
var $fieldContainer = $('<div>', {class: 'flex-grow-1'});
$fieldContainer.appendTo($field);
- defs.push(fieldWidget.appendTo($fieldContainer).then(function () {
+ let def;
+ if (isLegacy) {
+ def = fieldWidget.appendTo($fieldContainer);
+ } else {
+ def = fieldWidget.mount($fieldContainer[0]);
+ }
+ defs.push(def.then(function () {
self.$fieldsList.push($field);
}));
});
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index bcab9808773..a258d16a917 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -1065,6 +1065,13 @@ var FormRenderer = BasicRenderer.extend({
*/
_onNavigationMove: function (ev) {
ev.stopPropagation();
+ // We prevent the default behaviour and stop the propagation of the
+ // originalEvent when the originalEvent is a tab keydown to not let
+ // the browser do it. The action is done by this renderer.
+ if (ev.data.originalEvent && ['next', 'previous'].includes(ev.data.direction)) {
+ ev.data.originalEvent.preventDefault();
+ ev.data.originalEvent.stopPropagation();
+ }
var index;
let target = ev.data.target || ev.target;
if (target.__owl__) {
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 8b4db487ac4..8cd7f5a370d 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -15,6 +15,7 @@ var core = require('web.core');
var dom = require('web.dom');
var ListRenderer = require('web.ListRenderer');
var utils = require('web.utils');
+const { WidgetAdapterMixin } = require('web.OwlCompatibility');
var _t = core._t;
@@ -462,6 +463,9 @@ ListRenderer.include({
}
return Promise.all(defs).then(function () {
+ // mark Owl sub components as mounted
+ WidgetAdapterMixin.on_attach_callback.call(self);
+
// necessary to trigger resize on fieldtexts
core.bus.trigger('DOM_updated');
});
@@ -1553,6 +1557,10 @@ ListRenderer.include({
return;
}
ev.stopPropagation(); // stop the event, the action is done by this renderer
+ if (ev.data.originalEvent && ['next', 'previous'].includes(ev.data.direction)) {
+ ev.data.originalEvent.preventDefault();
+ ev.data.originalEvent.stopPropagation();
+ }
switch (ev.data.direction) {
case 'previous':
if (this.currentFieldIndex > 0) {