summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/fields
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/fields')
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js74
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js125
-rw-r--r--addons/web/static/src/js/fields/field_utils.js14
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js82
4 files changed, 174 insertions, 121 deletions
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index b78e0b3e28b..725a237750d 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -37,6 +37,9 @@ var AbstractField = Widget.extend({
events: {
'keydown': '_onKeydown',
},
+ custom_events: {
+ navigation_move: '_onNavigationMove',
+ },
/**
* 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
@@ -87,7 +90,7 @@ var AbstractField = Widget.extend({
// the 'attrs' property contains the attributes of the xml 'field' tag,
// the inner views...
var fieldsInfo = record.fieldsInfo[this.viewType];
- this.attrs = fieldsInfo && fieldsInfo[name] || {};
+ this.attrs = options.attrs || (fieldsInfo && fieldsInfo[name]) || {};
// 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')
@@ -165,18 +168,20 @@ var AbstractField = Widget.extend({
* part can be disabled. In that case, note that the focused input/textarea
* will have the cursor at the very end.
*
- * @param {boolean} [noselect=false] if false and the input is of type text
- * or textarea, the content will also be selected
+ * @param {Object} [options]
+ * @param {boolean} [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
* focusable element was not found or invisible
*/
- activate: function (noselect) {
+ activate: function (options) {
var $focusable = this.getFocusableElement();
if ($focusable.length && $focusable.is(':visible')) {
$focusable.focus();
if ($focusable.is('input[type="text"], textarea')) {
$focusable[0].selectionStart = $focusable[0].selectionEnd = $focusable[0].value.length;
- if (!noselect) {
+ if (options && !options.noselect) {
$focusable.select();
}
}
@@ -374,21 +379,60 @@ var AbstractField = Widget.extend({
//--------------------------------------------------------------------------
/**
- * might be controversial: intercept the tab key, to allow the editable list
- * view to control where the focus is.
+ * 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} event
+ * @param {KeyEvent} ev
*/
- _onKeydown: function (event) {
- if (event.which === $.ui.keyCode.TAB) {
- // the event needs to be stopped, to prevent other field widgets
- // to retrigger a move event
- event.stopPropagation();
- this.trigger_up(event.shiftKey ? 'move_previous' : 'move_next');
- event.preventDefault();
+ _onKeydown: function (ev) {
+ switch (ev.which) {
+ case $.ui.keyCode.TAB:
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.trigger_up('navigation_move', {
+ direction: ev.shiftKey ? 'previous' : 'next',
+ });
+ break;
+ case $.ui.keyCode.ENTER:
+ ev.stopPropagation();
+ this.trigger_up('navigation_move', {direction: 'next_line'});
+ break;
+ case $.ui.keyCode.UP:
+ ev.stopPropagation();
+ this.trigger_up('navigation_move', {direction: 'up'});
+ break;
+ case $.ui.keyCode.RIGHT:
+ ev.stopPropagation();
+ this.trigger_up('navigation_move', {direction: 'right'});
+ break;
+ case $.ui.keyCode.DOWN:
+ ev.stopPropagation();
+ this.trigger_up('navigation_move', {direction: 'down'});
+ break;
+ case $.ui.keyCode.LEFT:
+ ev.stopPropagation();
+ this.trigger_up('navigation_move', {direction: 'left'});
+ break;
}
},
+ /**
+ * Updates the target data value with the current AbstractField instance.
+ * This allows to consider the parent field in case of nested fields. The
+ * field which triggered the event is still accessible through ev.target.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onNavigationMove: function (ev) {
+ ev.data.target = this;
+ },
});
return AbstractField;
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index b3defa97911..cafa59bbd00 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -56,14 +56,7 @@ var TranslatableFieldMixin = {
* @private
*/
_onTranslate: function () {
- this._rpc({
- route: '/web/dataset/call_button',
- params: {
- model: 'ir.translation',
- method: 'translate_fields',
- args: [this.model, this.res_id, this.name, this.record.getContext()],
- }
- }).then(this.do_action.bind(this));
+ this.trigger_up('translate', {fieldName: this.name, id: this.dataPointID});
},
};
@@ -93,9 +86,6 @@ var DebouncedField = AbstractField.extend({
// _debouncedStarted is used to detect that the user interacted at least
// once with the widget, so that we can prevent it from triggering a
// field_changed in commitChanges if the user didn't change anything
- // (this is required as sometimes it is hard to detect that an unset
- // value is still unset, e.g. if a numerical field contains the value 0,
- // is it because it is still unset or because the user set it to 0?
this._debouncedStarted = false;
if (this.mode === 'edit') {
if (this.DEBOUNCE) {
@@ -229,12 +219,7 @@ var InputField = DebouncedField.extend({
type: 'text',
placeholder: this.attrs.placeholder || "",
});
- // Save cursor position to restore it after updating value
- var selectionStart = this.$input[0].selectionStart;
- var selectionEnd = this.$input[0].selectionEnd;
this.$input.val(this._formatValue(this.value));
- this.$input[0].selectionStart = selectionStart;
- this.$input[0].selectionEnd = selectionEnd;
return this.$input;
},
/**
@@ -282,40 +267,20 @@ var InputField = DebouncedField.extend({
this._doDebouncedAction();
},
/**
- * Implement keyboard movements. Mostly useful for its environment, such
- * as a list view.
+ * Stops the left/right navigation move event if the cursor is not at the
+ * start/end of the input element.
*
- * @override
* @private
- * @param {any} event
+ * @param {OdooEvent} ev
*/
- _onKeydown: function (event) {
+ _onNavigationMove: function (ev) {
+ this._super.apply(this, arguments);
var input = this.$input[0];
- var is_not_selecting;
- switch (event.which) {
- case $.ui.keyCode.DOWN:
- this.trigger_up('move_down');
- break;
- case $.ui.keyCode.UP:
- this.trigger_up('move_up');
- break;
- case $.ui.keyCode.LEFT:
- is_not_selecting = input.selectionEnd === input.selectionStart;
- if (is_not_selecting && input.selectionStart === 0) {
- this.trigger_up('move_left');
- }
- break;
- case $.ui.keyCode.RIGHT:
- is_not_selecting = input.selectionEnd === input.selectionStart;
- if (is_not_selecting && input.selectionEnd === input.value.length) {
- this.trigger_up('move_right');
- }
- break;
- case $.ui.keyCode.ENTER:
- this.trigger_up('move_next_line');
- break;
+ var selecting = (input.selectionEnd !== input.selectionStart);
+ if ((ev.data.direction === "left" && (selecting || input.selectionStart !== 0))
+ || (ev.data.direction === "right" && (selecting || input.selectionStart !== input.value.length))) {
+ ev.stopPropagation();
}
- this._super.apply(this, arguments);
},
});
@@ -380,7 +345,7 @@ var FieldDate = InputField.extend({
* @private
*/
_doDebouncedAction: function () {
- this.datewidget.change_datetime();
+ this.datewidget.changeDatetime();
},
/**
@@ -389,7 +354,7 @@ var FieldDate = InputField.extend({
* @private
*/
_getValue: function () {
- return this.datewidget.get_value();
+ return this.datewidget.getValue();
},
/**
* @override
@@ -418,7 +383,7 @@ var FieldDate = InputField.extend({
* @private
*/
_renderEdit: function () {
- this.datewidget.set_value(this.value);
+ this.datewidget.setValue(this.value);
this.$input = this.datewidget.$input;
},
@@ -447,7 +412,7 @@ var FieldDateTime = FieldDate.extend({
* @private
*/
_getValue: function () {
- var value = this.datewidget.get_value();
+ var value = this.datewidget.getValue();
return value && value.add(-this.getSession().tzOffset, 'minutes');
},
@@ -470,7 +435,7 @@ var FieldDateTime = FieldDate.extend({
*/
_renderEdit: function () {
var value = this.value && this.value.clone().add(this.getSession().tzOffset, 'minutes');
- this.datewidget.set_value(value);
+ this.datewidget.setValue(value);
this.$input = this.datewidget.$input;
},
@@ -610,6 +575,23 @@ var FieldBoolean = AbstractField.extend({
//--------------------------------------------------------------------------
/**
+ * 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 o_checkbox class.
+ if (activated && options && options.event && $(options.event.target).parents('.o_checkbox').length) {
+ this._setValue(!this.value); // Toggle the checkbox
+ }
+ return activated;
+ },
+
+ /**
* @override
* @returns {jQuery} the focusable checkbox input
*/
@@ -624,6 +606,20 @@ var FieldBoolean = AbstractField.extend({
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;
+ },
//--------------------------------------------------------------------------
// Private
@@ -665,28 +661,16 @@ var FieldBoolean = AbstractField.extend({
*
* @override
* @private
- * @param {KeyEvent} event
+ * @param {KeyEvent} ev
*/
- _onKeydown: function (event) {
- this._super.apply(this, arguments);
- switch (event.which) {
- case $.ui.keyCode.DOWN:
- this.trigger_up('move_down');
- break;
- case $.ui.keyCode.UP:
- this.trigger_up('move_up');
- break;
- case $.ui.keyCode.LEFT:
- this.trigger_up('move_left');
- break;
- case $.ui.keyCode.RIGHT:
- this.trigger_up('move_right');
- break;
+ _onKeydown: function (ev) {
+ switch (ev.which) {
case $.ui.keyCode.ENTER:
this.$input.prop('checked', !this.value);
this._setValue(!this.value);
- break;
+ return;
}
+ this._super.apply(this, arguments);
},
});
@@ -1774,7 +1758,8 @@ var JournalDashboardGraph = AbstractField.extend({
this.data = JSON.parse(this.value);
},
start: function () {
- nv.utils.windowResize(this._onResize.bind(this));
+ this._onResize = this._onResize.bind(this);
+ nv.utils.windowResize(this._onResize);
return this._super.apply(this, arguments);
},
destroy: function () {
@@ -1909,10 +1894,10 @@ var FieldDomain = AbstractField.extend({
"click .o_domain_show_selection_button": "_onShowSelectionButtonClick",
"click .o_field_domain_dialog_button": "_onDialogEditButtonClick",
}),
- custom_events: {
+ custom_events: _.extend({}, AbstractField.prototype.custom_events, {
"domain_changed": "_onDomainSelectorValueChange",
"domain_selected": "_onDomainSelectorDialogValueChange",
- },
+ }),
/**
* @constructor
* @override init from AbstractField
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index 9a683954e7e..ed3014d9bb6 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -71,7 +71,7 @@ function formatChar(value) {
* @returns {string}
*/
function formatDate(value, field, options) {
- if (!value) {
+ if (value === false) {
return "";
}
if (!options || !('timezone' in options) || options.timezone) {
@@ -96,7 +96,7 @@ function formatDate(value, field, options) {
* @returns {string}
*/
function formatDateTime(value, field, options) {
- if (!value) {
+ if (value === false) {
return "";
}
if (!options || !('timezone' in options) || options.timezone) {
@@ -113,7 +113,7 @@ function formatDateTime(value, field, options) {
* Returns a string representing a float. The result takes into account the
* user settings (to display the correct decimal separator).
*
- * @param {float} value the value that should be formatted
+ * @param {float|false} value the value that should be formatted
* @param {Object} [field] a description of the field (returned by fields_get
* for example). It may contain a description of the number of digits that
* should be used.
@@ -124,6 +124,9 @@ function formatDateTime(value, field, options) {
* @returns {string}
*/
function formatFloat(value, field, options) {
+ if (value === false) {
+ return "";
+ }
var l10n = core._t.database.parameters;
var precision;
if (options && options.digits) {
@@ -213,7 +216,7 @@ function formatX2Many(value) {
* Returns a string representing a monetary value. The result takes into account
* the user settings (to display the correct decimal separator, currency, ...).
*
- * @param {float} value the value that should be formatted
+ * @param {float|false} value the value that should be formatted
* @param {Object} [field]
* a description of the field (returned by fields_get for example). It
* may contain a description of the number of digits that should be used.
@@ -238,6 +241,9 @@ function formatX2Many(value) {
* @returns {string}
*/
function formatMonetary(value, field, options) {
+ if (value === false) {
+ return "";
+ }
options = options || {};
var currency = options.currency;
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index e9fa7dd09d1..10d72a1398d 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -14,6 +14,7 @@ odoo.define('web.relational_fields', function (require) {
*/
var AbstractField = require('web.AbstractField');
+var basicFields = require('web.basic_fields');
var concurrency = require('web.concurrency');
var ControlPanel = require('web.ControlPanel');
var dialogs = require('web.view_dialogs');
@@ -76,10 +77,10 @@ var M2ODialog = Dialog.extend({
var FieldMany2One = AbstractField.extend({
supportedFieldTypes: ['many2one'],
template: 'FieldMany2One',
- custom_events: {
+ custom_events: _.extend({}, AbstractField.prototype.custom_events, {
'quick_create': '_onQuickCreate',
'search_create_popup': '_onSearchCreatePopup',
- },
+ }),
events: _.extend({}, AbstractField.prototype.events, {
'click input': '_onInputClick',
'focusout input': '_onInputFocusout',
@@ -461,6 +462,19 @@ var FieldMany2One = AbstractField.extend({
}
},
/**
+ * Stops the left/right navigation move event if the cursor is not at the
+ * start/end of the input element. Stops any navigation move event if the
+ * user is selecting text.
+ *
+ * TODO this is a duplicate of InputField._onNavigationMove, maybe this
+ * should be done in a mixin or, better, the m2o field should be an
+ * InputField (but this requires some refactoring).
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onNavigationMove: basicFields.InputField.prototype._onNavigationMove,
+ /**
* @private
* @param {OdooEvent} event
*/
@@ -600,7 +614,7 @@ var FieldX2Many = AbstractField.extend({
if (command.operation === 'UPDATE') {
var state = record.data[this.name];
var fieldNames = state.getFieldNames();
- this.renderer.confirmChange(state, command.id, fieldNames);
+ this.renderer.confirmChange(state, command.id, fieldNames, ev.initialEvent);
return $.when();
}
}
@@ -624,7 +638,7 @@ var FieldX2Many = AbstractField.extend({
return this._super();
}
if (this.renderer) {
- this.renderer.updateState(this.value);
+ this.renderer.updateState(this.value, {});
this.pager.updateState({ size: this.value.count });
return $.when();
}
@@ -732,8 +746,8 @@ var FieldX2Many = AbstractField.extend({
onFailure: def.reject.bind(def),
});
} else {
- self.renderer.setRowMode(recordID, 'readonly');
- def.resolve();
+ self.renderer.setRowMode(recordID, 'readonly')
+ .done(def.resolve.bind(def));
}
});
return def;
@@ -777,7 +791,8 @@ var FieldX2Many = AbstractField.extend({
*/
_onEditLine: function (ev) {
ev.stopPropagation();
- this.renderer.setRowMode(ev.data.recordID, 'edit');
+ this.renderer.setRowMode(ev.data.recordID, 'edit')
+ .done(ev.data.onSuccess);
},
/**
* Updates the given record with the changes.
@@ -787,12 +802,19 @@ var FieldX2Many = AbstractField.extend({
*/
_onFieldChanged: function (ev) {
if (ev.target === this) {
+ ev.initialEvent = this.lastInitialEvent;
return;
}
ev.stopPropagation();
// changes occured in an editable list
var changes = ev.data.changes;
+ // save the initial event triggering the field_changed, as it will be
+ // necessary when the field triggering this event will be reset (to
+ // prevent it from re-rendering itself, formatting its value, loosing
+ // the focus... while still being edited)
+ this.lastInitialEvent = undefined;
if (Object.keys(changes).length) {
+ this.lastInitialEvent = ev;
this._setValue({
operation: 'UPDATE',
id: ev.data.dataPointID,
@@ -1050,6 +1072,11 @@ var FieldMany2ManyBinaryMultiFiles = AbstractField.extend({
this._generatedMetadata();
},
+ destroy: function () {
+ this._super();
+ $(window).off(this.fileupload_id);
+ },
+
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -1208,13 +1235,11 @@ var FieldMany2ManyTags = AbstractField.extend({
tag_template: "FieldMany2ManyTag",
className: "o_field_many2manytags",
supportedFieldTypes: ['many2many'],
- custom_events: {
+ custom_events: _.extend({}, AbstractField.prototype.custom_events, {
field_changed: '_onFieldChanged',
- move_next: '_onMoveNext',
- },
+ }),
events: _.extend({}, AbstractField.prototype.events, {
'click .o_delete': '_onDeleteTag',
- 'keydown .o_field_many2one input': '_onKeyDown',
}),
fieldsToFetch: {
color: {type: 'integer'},
@@ -1358,42 +1383,35 @@ var FieldMany2ManyTags = AbstractField.extend({
this._removeTag($(event.target).parent().data('id'));
},
/**
+ * Controls the changes made in the internal m2o field.
+ *
* @private
- * @param {OdooEvent} event
+ * @param {OdooEvent} ev
*/
- _onFieldChanged: function (event) {
- if (event.target === this) {
+ _onFieldChanged: function (ev) {
+ if (ev.target !== this.many2one) {
return;
}
- event.stopPropagation();
- var new_value = event.data.changes[this.name];
- if (new_value) {
- this._addTag(new_value);
+ ev.stopPropagation();
+ var newValue = ev.data.changes[this.name];
+ if (newValue) {
+ this._addTag(newValue);
this.many2one.reinitialize(false);
}
},
/**
* @private
- * @param {KeyboardEvent} event
+ * @param {KeyboardEvent} ev
*/
- _onKeyDown: function (event) {
- if ($(event.target).val() === "" && event.which === $.ui.keyCode.BACKSPACE) {
+ _onKeydown: function (ev) {
+ if (ev.which === $.ui.keyCode.BACKSPACE && this.$('input').val() === "") {
var $badges = this.$('.badge');
if ($badges.length) {
this._removeTag($badges.last().data('id'));
+ return;
}
}
- },
- /**
- * @private
- * @param {OdooEvent} event
- */
- _onMoveNext: function (event) {
- if (event.target === this) {
- return;
- }
- // Intercept event triggered up by the many2one, to prevent triggering it twice
- event.stopPropagation();
+ this._super.apply(this, arguments);
},
/**
* @private