summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/basic
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/views/basic')
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js15
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js4
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js520
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js3
4 files changed, 528 insertions, 14 deletions
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index c21d7a43335..5293ee6f4b6 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -88,6 +88,21 @@ return AbstractController.extend(FieldManagerMixin, {
//--------------------------------------------------------------------------
/**
+ * Called by the field manager mixin to confirm that a change just occured
+ * (after that potential onchanges have been applied).
+ *
+ * Basically, this only relays the notification to the renderer with the
+ * new state.
+ *
+ * @param {string} id - the id of one of the view's records
+ * @param {string[]} fields - the changed fields
+ * @param {OdooEvent} e - the event that triggered the change
+ */
+ _confirmChange: function (id, fields, e) {
+ var state = this.model.get(this.handle);
+ this.renderer.confirmChange(state, id, fields, e);
+ },
+ /**
* Delete records (and ask for confirmation if necessary)
*
* @param {string[]} ids list of local record ids
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index cd1dedb2f6c..ba51819b218 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -27,6 +27,8 @@ odoo.define('web.BasicModel', function (require) {
* fieldsInfo: {Object},
* getContext: {function},
* getDomain: {function},
+ * getEvalContext: {function},
+ * getFieldNames: {function},
* groupedBy: {string[]},
* id: {integer},
* isOpen: {boolean},
@@ -2001,8 +2003,8 @@ var BasicModel = AbstractModel.extend({
};
dataPoint.getContext = this._getContext.bind(this, dataPoint);
- dataPoint.getEvalContext = this._getEvalContext.bind(this, dataPoint);
dataPoint.getDomain = this._getDomain.bind(this, dataPoint);
+ dataPoint.getEvalContext = this._getEvalContext.bind(this, dataPoint);
dataPoint.getFieldNames = this._getFieldNames.bind(this, dataPoint);
this.localData[dataPoint.id] = dataPoint;
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index 1db0718545e..a18a82e5745 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -3,27 +3,86 @@ odoo.define('web.BasicRenderer', function (require) {
/**
* The BasicRenderer is an abstract class designed to share code between all
- * views that uses a BasicModel. The main goal is to keep track of all field
- * widgets, and properly destroy them whenever a rerender is done.
- *
- * When a concrete view extends a BasicRenderer, it should add each field
- * widgets in the this.widget list.
+ * views that uses a BasicModel. The main goal is to keep track of all field
+ * widgets, and properly destroy them whenever a rerender is done. The widgets
+ * and modifiers updates mechanism is also shared in the BasicRenderer.
*/
-
var AbstractRenderer = require('web.AbstractRenderer');
var config = require('web.config');
var core = require('web.core');
+var Domain = require('web.Domain');
var qweb = core.qweb;
-return AbstractRenderer.extend({
+var BasicRenderer = AbstractRenderer.extend({
+ custom_events: {
+ /**
+ * All basic renderers should be able to handle widget navigation
+ * through the TAB key.
+ */
+ move_next: '_onMoveNext',
+ move_previous: '_onMovePrevious',
+ },
/**
+ * Basic renderers implements the concept of "mode", they can either be in
+ * readonly mode or editable mode.
+ *
* @override
*/
- init: function (paret, state, params) {
+ init: function (parent, state, params) {
this._super.apply(this, arguments);
- this.widgets = [];
this.activeActions = params.activeActions;
+ this.viewType = params.viewType;
+ this.mode = params.mode || 'readonly';
+ },
+ /**
+ * Updates the internal state of the renderer to the new state. By default,
+ * this also implements the recomputation of the modifiers and their
+ * application to the DOM and the reset of the field widgets if needed.
+ *
+ * In case the given record is not found anymore, a whole re-rendering is
+ * completed (possible if a change in a record caused an onchange which
+ * erased the current record).
+ *
+ * We could always rerender the view from scratch, but then it would not be
+ * as efficient, and we might lose some local state, such as the input focus
+ * cursor, or the scrolling position.
+ *
+ * @param {Object} state
+ * @param {string} id
+ * @param {string[]} fields
+ * @param {OdooEvent} ev
+ * @returns {Deferred<AbstractField[]>} resolved with the list of widgets
+ * that have been reset
+ */
+ confirmChange: function (state, id, fields, ev) {
+ this.state = state;
+
+ var record = state.id === id ? state : _.findWhere(state.data, {id: id});
+ if (!record) {
+ return this._render();
+ }
+
+ var defs = [];
+
+ // Reset all the field widgets that are marked as changed and the ones
+ // which are configured to always be reset on any change
+ var resetWidgets = [];
+ _.each(this.allFieldWidgets[id], function (widget) {
+ if (_.contains(fields, widget.name) || widget.resetOnAnyFieldChange) {
+ defs.push(widget.reset(record, ev));
+ resetWidgets.push(widget);
+ }
+ });
+
+ // The modifiers update is done after widget resets as modifiers
+ // associated callbacks need to have all the widgets with the proper
+ // state before evaluation
+ defs.push(this._updateAllModifiers(record));
+
+ return $.when.apply($, defs).then(function () {
+ return resetWidgets;
+ });
},
//--------------------------------------------------------------------------
@@ -52,15 +111,358 @@ return AbstractRenderer.extend({
});
},
/**
+ * Activates the widget at the given index for the given record if possible
+ * or the "next" possible one.
+ *
+ * @private
+ * @param {Object} record
+ * @param {integer} currentIndex
+ * @param {integer} [inc=1] - the increment to use when searching for the
+ * "next" possible one
+ * @returns {integer} the index of the widget that was activated or -1 if
+ * none was possible to activate
+ */
+ _activateFieldWidget: function (record, currentIndex, inc) {
+ inc = inc === undefined ? 1 : inc;
+
+ var activated;
+ var recordWidgets = this.allFieldWidgets[record.id] || [];
+ for (var i = 0 ; i < recordWidgets.length ; i++) {
+ activated = recordWidgets[currentIndex].activate();
+ if (activated) {
+ return currentIndex;
+ }
+
+ currentIndex += inc;
+ if (currentIndex >= recordWidgets.length) {
+ currentIndex -= recordWidgets.length;
+ } else if (currentIndex < 0) {
+ currentIndex += recordWidgets.length;
+ }
+ }
+ return -1;
+ },
+ /**
+ * This is a wrapper of the {@see _activateFieldWidget} function to select
+ * the next possible widget instead of the given one.
+ *
+ * @private
+ * @param {Object} record
+ * @param {integer} currentIndex
+ * @return {integer}
+ */
+ _activateNextFieldWidget: function (record, currentIndex) {
+ currentIndex = (currentIndex + 1) % (this.allFieldWidgets[record.id] || []).length;
+ return this._activateFieldWidget(record, currentIndex, +1);
+ },
+ /**
+ * This is a wrapper of the {@see _activateFieldWidget} function to select
+ * the previous possible widget instead of the given one.
+ *
+ * @private
+ * @param {Object} record
+ * @param {integer} currentIndex
+ * @return {integer}
+ */
+ _activatePreviousFieldWidget: function (record, currentIndex) {
+ currentIndex = currentIndex ? (currentIndex - 1) : ((this.allFieldWidgets[record.id] || []).length - 1);
+ return this._activateFieldWidget(record, currentIndex, -1);
+ },
+ /**
+ * Does the necessary DOM updates to match the given modifiers data. The
+ * modifiers data is supposed to contain the properly evaluated modifiers
+ * associated to the given records and elements.
+ *
+ * @param {Object} modifiersData
+ * @param {Object} record
+ * @param {Object} [element] - do the update only on this element if given
+ * @returns {Deferred} resolved once finished
+ */
+ _applyModifiers: function (modifiersData, record, element) {
+ var self = this;
+ var defs = [];
+ var modifiers = modifiersData.evaluatedModifiers[record.id] || {};
+
+ this.defs = defs; // Potentially filled by widget rerendering
+ if (element) {
+ _apply(element);
+ } else {
+ // Clone is necessary as the list might change during _.each
+ _.each(_.clone(modifiersData.elementsByRecord[record.id]), _apply);
+ }
+ delete this.defs;
+
+ return $.when.apply($, defs);
+
+ function _apply(element) {
+ // If the view is in edit mode and that a widget have to switch
+ // its "readonly" state, we have to re-render it completely
+ if ('readonly' in modifiers
+ && self.mode === "edit"
+ && element.widget
+ && (element.widget.mode === 'readonly') !== modifiers.readonly)
+ {
+ self._rerenderFieldWidget(element.widget, record);
+ return; // Rerendering already applied the modifiers, no need to go further
+ }
+
+ // Toggle modifiers CSS classes if necessary
+ element.$el.toggleClass("o_form_invisible", !!modifiers.invisible);
+ element.$el.toggleClass("o_readonly", !!modifiers.readonly);
+ element.$el.toggleClass("o_form_required", !!modifiers.required);
+
+ // Call associated callback
+ if (element.callback) {
+ element.callback(element, modifiers, record);
+ }
+ }
+ },
+ /**
+ * Determines if a given field widget value can be saved. For this to be
+ * true, the widget must be valid (properly parsed value) and have a value
+ * if the associated view field is required.
+ *
+ * @private
+ * @param {AbstractField} widget
+ * @returns {boolean|Deferred<boolean>} @see AbstractField.isValid
+ */
+ _canWidgetBeSaved: function (widget) {
+ var modifiers = this._getEvaluatedModifiers(widget.__node, widget.record);
+ var isSetOrNotRequired = (widget.isSet() || !modifiers.required);
+ var isValid = widget.isValid();
+ if (isValid instanceof $.Deferred) {
+ return isValid.then(function (isValid) {
+ return isValid && isSetOrNotRequired;
+ });
+ }
+ return isValid && isSetOrNotRequired;
+ },
+ /**
+ * Updates the modifiers evaluation associated to a given modifiers data and
+ * a given record. This only updates the modifiers values. To see associated
+ * DOM updates: @see _updateAllModifiers @see _applyModifiers.
+ *
+ * @private
+ * @param {Object} modifiersData
+ * @param {Object} record
+ */
+ _computeModifiers: function (modifiersData, record) {
+ var evalContext = record.getEvalContext();
+ modifiersData.evaluatedModifiers[record.id]
+ = _.mapObject(modifiersData.modifiers, function (modifier) {
+ return new Domain(modifier, evalContext).compute(evalContext);
+ });
+ },
+ /**
+ * Destroys a given widget associated to the given record and removes it
+ * from internal referencing.
+ *
+ * @private
+ * @param {Object} record
+ * @param {AbstractField} widget
+ * @returns {integer} the index of the removed widget
+ */
+ _destroyFieldWidget: function (record, widget) {
+ var recordWidgets = this.allFieldWidgets[record.id];
+ var index = recordWidgets.indexOf(widget);
+ if (index >= 0) {
+ recordWidgets.splice(index, 1);
+ }
+ this._unregisterModifiersElement(widget.__node, record, widget);
+ widget.$el.destroy();
+ return index;
+ },
+ /**
+ * Searches for the last evaluation of the modifiers associated to the given
+ * data (modifiers evaluation are supposed to always be up-to-date as soon
+ * as possible).
+ *
+ * @private
+ * @param {Object} node
+ * @param {Object} record
+ * @returns {Object} the evaluated modifiers associated to the given node
+ * and record (not recomputed by the call)
+ */
+ _getEvaluatedModifiers: function (node, record) {
+ var element = this._getModifiersData(node);
+ if (!element) {
+ return {};
+ }
+ return element.evaluatedModifiers[record.id] || {};
+ },
+ /**
+ * Searches through the registered modifiers data for the one which is
+ * related to the given node.
+ *
+ * @private
+ * @param {Object} node
+ * @returns {Object|undefined} related modifiers data if any
+ * undefined otherwise
+ */
+ _getModifiersData: function (node) {
+ return _.findWhere(this.allModifiersData, {node: node});
+ },
+ /**
+ * Registers or updates the modifiers data associated to the given node.
+ * This method is quiet complex as it handles all the needs of the basic
+ * renderers:
+ *
+ * - On first registration, the modifiers are evaluated thanks to the given
+ * record. This allows nodes that will produce an AbstractField instance
+ * to have their modifiers registered before this field creation as we
+ * need the readonly modifier to be able to instantiate the AbstractField.
+ * (@see _computeModifiers).
+ *
+ * - On additional registrations, if the node was already registered but the
+ * record is different, we evaluate the modifiers for this record and
+ * saves them in the same object (without reparsing the modifiers).
+ *
+ * - On additional registrations, the modifiers are not reparsed (or
+ * reevaluated for an already seen record) but the given widget or DOM
+ * element is associated to the node modifiers.
+ *
+ * - The new elements are immediately adapted to match the modifiers and the
+ * given associated callback is called even if there is no modifiers on
+ * the node (@see _applyModifiers). This is indeed necessary as the
+ * callback is a description of what to do when a modifier changes. Even
+ * if there is no modifiers, this action must be performed on first
+ * rendering to avoid code duplication. If there is no modifiers, they
+ * will however not be registered for modifiers updates.
+ *
+ * - When a new element is given, it does not replace the old one, it is
+ * added as an additional element. This is indeed useful for nodes that
+ * will produce multiple DOM (as a list cell and its internal widget or
+ * a form field and its associated label).
+ * (@see _unregisterModifiersElement for removing an associated element.)
+ *
+ * Note: also on view rerendering, all the modifiers are forgotten so that
+ * the renderer only keeps the ones associated to the current DOM state.
+ *
+ * @private
+ * @param {Object} node
+ * @param {Object} record
+ * @param {jQuery|AbstractField} [element]
+ * @param {Object} [options]
+ * @param {Object} [options.callback] - the callback to call on registration
+ * and on modifiers updates
+ * @returns {Object} for code efficiency, returns the last evaluated
+ * modifiers for the given node and record.
+ */
+ _registerModifiers: function (node, record, element, options) {
+ // Check if we already registered the modifiers for the given node
+ // If yes, this is simply an update of the related element
+ // If not, check the modifiers to see if it needs registration
+ var modifiersData = this._getModifiersData(node);
+ if (!modifiersData) {
+ var modifiers = JSON.parse(node.attrs.modifiers || "{}"); // FIXME parsed multiple times (record switching, no modifiers, ...)
+ modifiersData = {
+ node: node,
+ modifiers: modifiers,
+ evaluatedModifiers: {},
+ elementsByRecord: {},
+ };
+ if (!_.isEmpty(modifiers)) { // Register only if modifiers might change (TODO condition might be improved here)
+ this.allModifiersData.push(modifiersData);
+ }
+ }
+
+ // Evaluate if necessary
+ if (!modifiersData.evaluatedModifiers[record.id]) {
+ this._computeModifiers(modifiersData, record);
+ }
+
+ // Element might not be given yet (a second call to the function can
+ // update the registration with the element)
+ if (element) {
+ var newElement = {};
+ if (element instanceof jQuery) {
+ newElement.$el = element;
+ } else {
+ newElement.widget = element;
+ newElement.$el = element.$el;
+ }
+ if (options && options.callback) {
+ newElement.callback = options.callback;
+ }
+
+ if (!modifiersData.elementsByRecord[record.id]) {
+ modifiersData.elementsByRecord[record.id] = [];
+ }
+ modifiersData.elementsByRecord[record.id].push(newElement);
+
+ this._applyModifiers(modifiersData, record, newElement);
+ }
+
+ return modifiersData.evaluatedModifiers[record.id];
+ },
+ /**
* Render the view
*
* @override
* @returns {Deferred}
*/
_render: function () {
- var oldWidgets = this.widgets;
- this.widgets = [];
- return this._renderView().then(_.invoke.bind(_, oldWidgets, 'destroy'));
+ var oldAllFieldWidgets = this.allFieldWidgets;
+ this.allFieldWidgets = {}; // TODO maybe merging allFieldWidgets and allModifiersData into "nodesData" in some way could be great
+ this.allModifiersData = [];
+ return this._renderView().then(function () {
+ _.each(oldAllFieldWidgets, function (recordWidgets) {
+ _.each(recordWidgets, function (widget) {
+ widget.destroy();
+ });
+ });
+ });
+ },
+ /**
+ * Instantiates the appropriate AbstractField specialization for the given
+ * node and prepares its rendering and addition to the DOM. Indeed, the
+ * rendering of the widget will be started and the associated deferred will
+ * be added to the 'defs' attribute. This is supposed to be created and
+ * deleted by the calling code if necessary.
+ * Note: for this implementation to work, AbstractField willStart methods
+ * *must* be synchronous.
+ *
+ * @private
+ * @param {Object} node
+ * @param {Object} record
+ * @param {Object} [options]
+ * @param {Object} [modifiersOptions]
+ * @returns {AbstractField}
+ */
+ _renderFieldWidget: function (node, record, options, modifiersOptions) {
+ var fieldName = node.attrs.name;
+
+ // Register the node-associated modifiers
+ var modifiers = this._registerModifiers(node, record);
+
+ // Initialize and register the widget
+ // Readonly status is known as the modifiers have just been registered
+ var Widget = record.fieldsInfo[this.viewType][fieldName].Widget;
+ var widget = new Widget(this, fieldName, record, _.extend({
+ mode: modifiers.readonly ? 'readonly' : this.mode,
+ viewType: this.viewType,
+ }, options || {}));
+
+ // Register the widget so that it can easily be found again
+ if (this.allFieldWidgets[record.id] === undefined) {
+ this.allFieldWidgets[record.id] = [];
+ }
+ this.allFieldWidgets[record.id].push(widget);
+
+ widget.__node = node; // TODO get rid of this if possible one day
+
+ // Prepare widget rendering and save the related deferred
+ var def = widget.__widgetRenderAndInsert(function () {});
+ if (def.state() === 'pending') {
+ this.defs.push(def);
+ }
+
+ // Update the modifiers registration by associating the widget and by
+ // giving the modifiers options now (as the potential callback is
+ // associated to new widget)
+ this._registerModifiers(node, record, widget, modifiersOptions);
+
+ return widget;
},
/**
* Actual rendering. Supposed to be overridden by concrete renderers.
@@ -69,7 +471,7 @@ return AbstractRenderer.extend({
* - instantiate a widget from the registry for each field in the arch
*
* Note that the 'state' field should contains all necessary information
- * for the rendering. The field widgets should be as synchronous as
+ * for the rendering. The field widgets should be as synchronous as
* possible.
*
* @abstract
@@ -78,6 +480,98 @@ return AbstractRenderer.extend({
_renderView: function () {
return $.when();
},
+ /**
+ * Rerenders a given widget and make sure the associated data which
+ * referenced the old one is updated.
+ *
+ * @private
+ * @param {Widget} widget
+ * @param {Object} record
+ * @returns {AbstractField}
+ */
+ _rerenderFieldWidget: function (widget, record) {
+ // Render the new field widget
+ var newWidget = this._renderFieldWidget(widget.__node, record);
+ widget.$el.replaceWith(newWidget.$el);
+
+ // Destroy the old widget and position the new one at the old one's
+ var oldIndex = this._destroyFieldWidget(record, widget);
+ var recordWidgets = this.allFieldWidgets[record.id];
+ recordWidgets.splice(oldIndex, 0, newWidget);
+ recordWidgets.pop();
+
+ return newWidget;
+ },
+ /**
+ * Unregisters an element of the modifiers data associated to the given
+ * node and record.
+ *
+ * @param {Object} node
+ * @param {Object} record
+ * @param {jQuery|AbstractField} element
+ */
+ _unregisterModifiersElement: function (node, record, element) {
+ var modifiersData = this._getModifiersData(node);
+ if (modifiersData) {
+ var elements = modifiersData.elementsByRecord[record.id];
+ var index = _.findIndex(elements, function (oldElement) {
+ return oldElement.widget === element
+ || oldElement.$el[0] === element[0];
+ });
+ if (index >= 0) {
+ elements.splice(index, 1);
+ }
+ }
+ },
+ /**
+ * Does two actions, for each registered modifiers:
+ * 1) Recomputes the modifiers associated to the given record and saves them
+ * (as boolean values) in the appropriate modifiers data.
+ * 2) Updates the rendering of the view elements associated to the given
+ * record to match the new modifiers.
+ *
+ * @see _computeModifiers
+ * @see _applyModifiers
+ *
+ * @private
+ * @param {Object} record
+ * @returns {Deferred} resolved once finished
+ */
+ _updateAllModifiers: function (record) {
+ var self = this;
+
+ var defs = _.flatten(_.map(this.allModifiersData, function (modifiersData) {
+ self._computeModifiers(modifiersData, record);
+ return self._applyModifiers(modifiersData, record);
+ }));
+
+ return $.when.apply($, defs);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * When someone presses TAB in a widget, it is nice to be able to go to the
+ * next widget, especially since the TAB key event is preventDefaulted
+ *
+ * @abstract
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onMoveNext: function (ev) {},
+ /**
+ * When someone presses SHIFT+TAB in a widget, it is nice to be able to go
+ * back to the previous widget, especially since the TAB key event is
+ * preventDefaulted
+ *
+ * @abstract
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onMovePrevious: function (ev) {},
});
+return BasicRenderer;
});
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 d850b2b7a2f..2604dcf0aa1 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -19,6 +19,7 @@ var BasicView = AbstractView.extend({
Model: BasicModel,
Controller: BasicController,
}),
+ viewType: undefined,
/**
* process the fields_view to find all fields appearing in the views.
* list those fields' name in this.fields_name, which will be the list
@@ -34,6 +35,8 @@ var BasicView = AbstractView.extend({
init: function (viewInfo, params) {
this._super.apply(this, arguments);
+ this.rendererParams.viewType = this.viewType;
+
this.controllerParams.confirmOnDelete = true;
this.controllerParams.archiveEnabled = 'active' in viewInfo.fields;
this.controllerParams.hasButtons =