718 lines
28 KiB
JavaScript
718 lines
28 KiB
JavaScript
flectra.define('web.BasicRenderer', function (require) {
|
|
"use strict";
|
|
|
|
/**
|
|
* 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. 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 dom = require('web.dom');
|
|
var widgetRegistry = require('web.widget_registry');
|
|
|
|
var qweb = core.qweb;
|
|
|
|
var BasicRenderer = AbstractRenderer.extend({
|
|
custom_events: {
|
|
navigation_move: '_onNavigationMove',
|
|
},
|
|
/**
|
|
* Basic renderers implements the concept of "mode", they can either be in
|
|
* readonly mode or editable mode.
|
|
*
|
|
* @override
|
|
*/
|
|
init: function (parent, state, params) {
|
|
this._super.apply(this, arguments);
|
|
this.activeActions = params.activeActions;
|
|
this.viewType = params.viewType;
|
|
this.mode = params.mode || 'readonly';
|
|
this.widgets = [];
|
|
},
|
|
/**
|
|
* This method has two responsabilities: find every invalid fields in the
|
|
* current view, and making sure that they are displayed as invalid, by
|
|
* toggling the o_form_invalid css class. It has to be done both on the
|
|
* widget, and on the label, if any.
|
|
*
|
|
* @param {string} recordID
|
|
* @returns {string[]} the list of invalid field names
|
|
*/
|
|
canBeSaved: function (recordID) {
|
|
var self = this;
|
|
var invalidFields = [];
|
|
_.each(this.allFieldWidgets[recordID], function (widget) {
|
|
var canBeSaved = self._canWidgetBeSaved(widget);
|
|
if (!canBeSaved) {
|
|
invalidFields.push(widget.name);
|
|
}
|
|
widget.$el.toggleClass('o_field_invalid', !canBeSaved);
|
|
});
|
|
return invalidFields;
|
|
},
|
|
/**
|
|
* Calls 'commitChanges' on all field widgets, so that they can notify the
|
|
* environment with their current value (useful for widgets that can't
|
|
* detect when their value changes or that have to validate their changes
|
|
* before notifying them).
|
|
*
|
|
* @param {string} recordID
|
|
* @return {Deferred}
|
|
*/
|
|
commitChanges: function (recordID) {
|
|
var defs = _.map(this.allFieldWidgets[recordID], function (widget) {
|
|
return widget.commitChanges();
|
|
});
|
|
return $.when.apply($, defs);
|
|
},
|
|
/**
|
|
* 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 {FlectraEvent} 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().then(_.constant([]));
|
|
}
|
|
|
|
// reset all widgets (from the <widget> tag) if any:
|
|
_.invoke(this.widgets, 'updateState', state);
|
|
|
|
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) {
|
|
var fieldChanged = _.contains(fields, widget.name);
|
|
if (fieldChanged || widget.resetOnAnyFieldChange) {
|
|
defs.push(widget.reset(record, ev, fieldChanged));
|
|
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;
|
|
});
|
|
},
|
|
/**
|
|
* Activates the widget and move the cursor to the given offset
|
|
*
|
|
* @param {string} id
|
|
* @param {string} fieldName
|
|
* @param {integer} offset
|
|
*/
|
|
focusField: function (id, fieldName, offset) {
|
|
this.editRecord(id);
|
|
if (typeof offset === "number") {
|
|
var field = _.findWhere(this.allFieldWidgets[id], {name: fieldName});
|
|
dom.setSelectionRange(field.getFocusableElement().get(0), {start: offset, end: offset});
|
|
}
|
|
},
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Private
|
|
//--------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Add a tooltip on a $node, depending on a field description
|
|
*
|
|
* @param {FieldWidget} widget
|
|
* @param {$node} $node
|
|
*/
|
|
_addFieldTooltip: function (widget, $node) {
|
|
// optional argument $node, the jQuery element on which the tooltip
|
|
// should be attached if not given, the tooltip is attached on the
|
|
// widget's $el
|
|
$node = $node.length ? $node : widget.$el;
|
|
$node.tooltip({
|
|
delay: { show: 1000, hide: 0 },
|
|
title: function () {
|
|
return qweb.render('WidgetLabel.tooltip', {
|
|
debug: config.debug,
|
|
widget: widget,
|
|
});
|
|
}
|
|
});
|
|
},
|
|
/**
|
|
* Activates the widget at the given index for the given record if possible
|
|
* or the "next" possible one. Usually, a widget can be activated if it is
|
|
* in edit mode, and if it is visible.
|
|
*
|
|
* @private
|
|
* @param {Object} record
|
|
* @param {integer} currentIndex
|
|
* @param {Object} [options]
|
|
* @param {integer} [options.inc=1] - the increment to use when searching for the
|
|
* "next" possible one
|
|
* @param {boolean} [options.wrap=true] if true, when we arrive at the end of the
|
|
* list of widget, we wrap around and try to activate widgets starting at
|
|
* the beginning. Otherwise, we just stop trying and return -1
|
|
* @returns {integer} the index of the widget that was activated or -1 if
|
|
* none was possible to activate
|
|
*/
|
|
_activateFieldWidget: function (record, currentIndex, options) {
|
|
options = options || {};
|
|
_.defaults(options, {inc: 1, wrap: true});
|
|
|
|
var recordWidgets = this.allFieldWidgets[record.id] || [];
|
|
for (var i = 0 ; i < recordWidgets.length ; i++) {
|
|
var activated = recordWidgets[currentIndex].activate({event: options.event});
|
|
if (activated) {
|
|
return currentIndex;
|
|
}
|
|
|
|
currentIndex += options.inc;
|
|
if (currentIndex >= recordWidgets.length) {
|
|
if (options.wrap) {
|
|
currentIndex -= recordWidgets.length;
|
|
} else {
|
|
return -1;
|
|
}
|
|
} else if (currentIndex < 0) {
|
|
if (options.wrap) {
|
|
currentIndex += recordWidgets.length;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
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, {inc: 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, {inc:-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
|
|
*/
|
|
_applyModifiers: function (modifiersData, record, element) {
|
|
var self = this;
|
|
var modifiers = modifiersData.evaluatedModifiers[record.id] || {};
|
|
|
|
if (element) {
|
|
_apply(element);
|
|
} else {
|
|
// Clone is necessary as the list might change during _.each
|
|
_.each(_.clone(modifiersData.elementsByRecord[record.id]), _apply);
|
|
}
|
|
|
|
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 && element.widget) {
|
|
var mode = modifiers.readonly ? 'readonly' : modifiersData.baseModeByRecord[record.id];
|
|
if (mode !== element.widget.mode) {
|
|
self._rerenderFieldWidget(element.widget, record, {
|
|
keepBaseMode: true,
|
|
mode: mode,
|
|
});
|
|
return; // Rerendering already applied the modifiers, no need to go further
|
|
}
|
|
}
|
|
|
|
// Toggle modifiers CSS classes if necessary
|
|
element.$el.toggleClass("o_invisible_modifier", !!modifiers.invisible);
|
|
element.$el.toggleClass("o_readonly_modifier", !!modifiers.readonly);
|
|
element.$el.toggleClass("o_required_modifier", !!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);
|
|
return widget.isValid() && (widget.isSet() || !modifiers.required);
|
|
},
|
|
/**
|
|
* Destroys a given widget associated to the given record and removes it
|
|
* from internal referencing.
|
|
*
|
|
* @private
|
|
* @param {string} recordID id of the local resource
|
|
* @param {AbstractField} widget
|
|
* @returns {integer} the index of the removed widget
|
|
*/
|
|
_destroyFieldWidget: function (recordID, widget) {
|
|
var recordWidgets = this.allFieldWidgets[recordID];
|
|
var index = recordWidgets.indexOf(widget);
|
|
if (index >= 0) {
|
|
recordWidgets.splice(index, 1);
|
|
}
|
|
this._unregisterModifiersElement(widget.__node, recordID, widget);
|
|
widget.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});
|
|
},
|
|
/**
|
|
* @private
|
|
* @param {jQueryElement} $el
|
|
* @param {Object} node
|
|
*/
|
|
_handleAttributes: function ($el, node) {
|
|
if (node.attrs.class) {
|
|
$el.addClass(node.attrs.class);
|
|
}
|
|
if (node.attrs.style) {
|
|
$el.attr('style', node.attrs.style);
|
|
}
|
|
},
|
|
/**
|
|
* Used by list and kanban renderers to determine whether or not to display
|
|
* the no content helper (if there is no data in the state to display)
|
|
*
|
|
* @private
|
|
* @returns {boolean}
|
|
*/
|
|
_hasContent: function () {
|
|
return this.state.count !== 0;
|
|
},
|
|
/**
|
|
* This function is called each time a field widget is created, when it is
|
|
* ready (after its willStart and Start methods are complete). This is the
|
|
* place where work having to do with $el should be done.
|
|
*
|
|
* @private
|
|
* @param {Widget} widget the field widget instance
|
|
* @param {Object} node the attrs coming from the arch
|
|
*/
|
|
_postProcessField: function (widget, 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.
|
|
*
|
|
* - 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
|
|
* @param {boolean} [options.keepBaseMode=false] this function registers the
|
|
* 'baseMode' of the node on a per record basis;
|
|
* this is a field widget specific settings which
|
|
* represents the generic mode of the widget, regardless of its modifiers
|
|
* (the interesting case is the list view: all widgets are supposed to be
|
|
* in the baseMode 'readonly', except the ones that are in the line that
|
|
* is currently being edited).
|
|
* With option 'keepBaseMode' set to true, the baseMode of the record's
|
|
* node isn't overridden (this is particularily useful when a field widget
|
|
* is re-rendered because its readonly modifier changed, as in this case,
|
|
* we don't want to change its base mode).
|
|
* @param {string} [options.mode] the 'baseMode' of the record's node is set to this
|
|
* value (if not given, it is set to this.mode, the mode of the renderer)
|
|
* @returns {Object} for code efficiency, returns the last evaluated
|
|
* modifiers for the given node and record.
|
|
*/
|
|
_registerModifiers: function (node, record, element, options) {
|
|
options = 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 = node.attrs.modifiers || {};
|
|
modifiersData = {
|
|
node: node,
|
|
modifiers: modifiers,
|
|
evaluatedModifiers: {},
|
|
elementsByRecord: {},
|
|
baseModeByRecord : {},
|
|
};
|
|
if (!_.isEmpty(modifiers)) { // Register only if modifiers might change (TODO condition might be improved here)
|
|
this.allModifiersData.push(modifiersData);
|
|
}
|
|
}
|
|
|
|
// Compute the record's base mode
|
|
if (!modifiersData.baseModeByRecord[record.id] || !options.keepBaseMode) {
|
|
modifiersData.baseModeByRecord[record.id] = options.mode || this.mode;
|
|
}
|
|
|
|
// Evaluate if necessary
|
|
if (!modifiersData.evaluatedModifiers[record.id]) {
|
|
modifiersData.evaluatedModifiers[record.id] = record.evalModifiers(modifiersData.modifiers);
|
|
}
|
|
|
|
// 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, options);
|
|
}
|
|
|
|
return modifiersData.evaluatedModifiers[record.id];
|
|
},
|
|
/**
|
|
* Render the view
|
|
*
|
|
* @override
|
|
* @returns {Deferred}
|
|
*/
|
|
_render: function () {
|
|
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] passed to @_registerModifiers
|
|
* @param {string} [options.mode] either 'edit' or 'readonly' (defaults to
|
|
* this.mode, the mode of the renderer)
|
|
* @returns {AbstractField}
|
|
*/
|
|
_renderFieldWidget: function (node, record, options) {
|
|
options = options || {};
|
|
var fieldName = node.attrs.name;
|
|
// Register the node-associated modifiers
|
|
var mode = options.mode || this.mode;
|
|
var modifiers = this._registerModifiers(node, record, null, options);
|
|
// 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, {
|
|
mode: modifiers.readonly ? 'readonly' : mode,
|
|
viewType: this.viewType,
|
|
});
|
|
|
|
// 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)
|
|
var self = this;
|
|
def.then(function () {
|
|
self._registerModifiers(node, record, widget, {
|
|
callback: function (element, modifiers, record) {
|
|
element.$el.toggleClass('o_field_empty', !!(
|
|
record.data.id
|
|
&& (modifiers.readonly || mode === 'readonly')
|
|
&& !element.widget.isSet()
|
|
));
|
|
},
|
|
keepBaseMode: !!options.keepBaseMode,
|
|
mode: mode,
|
|
});
|
|
self._postProcessField(widget, node);
|
|
});
|
|
|
|
return widget;
|
|
},
|
|
/**
|
|
* Renders the nocontent helper.
|
|
*
|
|
* This method is a helper for renderers that want to display a help
|
|
* message when no content is available.
|
|
*
|
|
* @private
|
|
*/
|
|
_renderNoContentHelper: function () {
|
|
var $msg = $('<div>')
|
|
.addClass('oe_view_nocontent')
|
|
.html(this.noContentHelp);
|
|
this.$el.html($msg);
|
|
},
|
|
/**
|
|
* Actual rendering. Supposed to be overridden by concrete renderers.
|
|
* The basic responsabilities of _renderView are:
|
|
* - use the xml arch of the view to render a jQuery representation
|
|
* - 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
|
|
* possible.
|
|
*
|
|
* @abstract
|
|
* @returns {Deferred}
|
|
*/
|
|
_renderView: function () {
|
|
return $.when();
|
|
},
|
|
/**
|
|
* Instantiate custom widgets
|
|
*
|
|
* @private
|
|
* @param {Object} record
|
|
* @param {Object} node
|
|
* @returns {jQueryElement}
|
|
*/
|
|
_renderWidget: function (record, node) {
|
|
var Widget = widgetRegistry.get(node.attrs.name);
|
|
var widget = new Widget(this, record, node);
|
|
|
|
this.widgets.push(widget);
|
|
|
|
// Prepare widget rendering and save the related deferred
|
|
var def = widget.__widgetRenderAndInsert(function () {});
|
|
if (def.state() === 'pending') {
|
|
this.defs.push(def);
|
|
}
|
|
|
|
// handle other attributes/modifiers
|
|
this._handleAttributes(widget.$el, node);
|
|
this._registerModifiers(node, record, widget);
|
|
widget.$el.addClass('o_widget');
|
|
return widget.$el;
|
|
},
|
|
|
|
/**
|
|
* Rerenders a given widget and make sure the associated data which
|
|
* referenced the old one is updated.
|
|
*
|
|
* @private
|
|
* @param {Widget} widget
|
|
* @param {Object} record
|
|
* @param {Object} [options] options passed to @_renderFieldWidget
|
|
* @returns {AbstractField}
|
|
*/
|
|
_rerenderFieldWidget: function (widget, record, options) {
|
|
// Render the new field widget
|
|
var newWidget = this._renderFieldWidget(widget.__node, record, options);
|
|
widget.$el.replaceWith(newWidget.$el);
|
|
|
|
// Destroy the old widget and position the new one at the old one's
|
|
var oldIndex = this._destroyFieldWidget(record.id, 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 {string} recordID id of the local resource
|
|
* @param {jQuery|AbstractField} element
|
|
*/
|
|
_unregisterModifiersElement: function (node, recordID, element) {
|
|
var modifiersData = this._getModifiersData(node);
|
|
if (modifiersData) {
|
|
var elements = modifiersData.elementsByRecord[recordID];
|
|
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 _applyModifiers
|
|
*
|
|
* @private
|
|
* @param {Object} record
|
|
* @returns {Deferred} resolved once finished
|
|
*/
|
|
_updateAllModifiers: function (record) {
|
|
var self = this;
|
|
|
|
var defs = [];
|
|
this.defs = defs; // Potentially filled by widget rerendering
|
|
_.each(this.allModifiersData, function (modifiersData) {
|
|
modifiersData.evaluatedModifiers[record.id] = record.evalModifiers(modifiersData.modifiers);
|
|
self._applyModifiers(modifiersData, record);
|
|
});
|
|
delete this.defs;
|
|
|
|
return $.when.apply($, defs);
|
|
},
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Handlers
|
|
//--------------------------------------------------------------------------
|
|
|
|
/**
|
|
* When someone presses the TAB/UP/DOWN/... key in a widget, it is nice to
|
|
* be able to navigate in the view (default browser behaviors are disabled
|
|
* by Flectra).
|
|
*
|
|
* @abstract
|
|
* @private
|
|
* @param {FlectraEvent} ev
|
|
*/
|
|
_onNavigationMove: function (ev) {},
|
|
});
|
|
|
|
return BasicRenderer;
|
|
});
|