diff options
Diffstat (limited to 'addons/web/static/src/js/fields')
| -rw-r--r-- | addons/web/static/src/js/fields/abstract_field.js | 74 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/basic_fields.js | 125 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/field_utils.js | 14 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/relational_fields.js | 82 |
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 |
