diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js index 154df08051a..d312746578e 100644 --- a/addons/web/static/src/js/chrome/action_manager.js +++ b/addons/web/static/src/js/chrome/action_manager.js @@ -417,6 +417,7 @@ var ActionManager = Widget.extend({ callbacks: [{widget: dialog}, {widget: controller.widget}], }); widget.renderButtons(dialog.$footer); + dialog.rebindButtonBehavior(); return action; }); diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js index d5be248b16e..5ba24f57cad 100644 --- a/addons/web/static/src/js/core/dialog.js +++ b/addons/web/static/src/js/core/dialog.js @@ -18,7 +18,12 @@ var _t = core._t; **/ var Dialog = Widget.extend({ xmlDependencies: ['/web/static/src/xml/dialog.xml'], - + custom_events: _.extend({}, Widget.prototype.custom_events, { + focus_control_button: '_onFocusControlButton', + }), + events: _.extend({} , Widget.prototype.events, { + 'keydown .modal-footer button':'_onFooterButtonKeyDown', + }), /** * @param {Widget} parent * @param {Object} [options] @@ -102,6 +107,9 @@ var Dialog = Widget.extend({ } this.$el.addClass('modal-body ' + this.dialogClass); }, + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- /** * @param {Object[]} buttons - @see init */ @@ -148,7 +156,14 @@ var Dialog = Widget.extend({ return (handler)? this._opened.then(handler) : this._opened; }, - open: function () { + /** + * Show a dialog + * + * @param {Object} options + * @param {boolean} options.shouldFocusButtons if true, put the focus on + * the first button primary when the dialog opens + */ + open: function (options) { $('.tooltip').remove(); // remove open tooltip if any to prevent them staying when modal is opened var self = this; @@ -157,6 +172,9 @@ var Dialog = Widget.extend({ self.$modal.modal('show'); self._opened.resolve(); }); + if (options && options.shouldFocusButtons) { + self._onFocusControlButton(); + } return self; }, @@ -176,6 +194,8 @@ var Dialog = Widget.extend({ if (this.isDestroyed()) { return; } + var isFocusSet = this._focusOnClose(); + this._super(); $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal has disappeared @@ -184,11 +204,75 @@ var Dialog = Widget.extend({ this.$modal.remove(); } - var modals = $('body > .modal').filter(':visible'); - if (modals.length) { - modals.last().focus(); - // Keep class modal-open (deleted by bootstrap hide fnct) on body to allow scrolling inside the modal - $('body').addClass('modal-open'); + if (!isFocusSet) { + var modals = $('body > .modal').filter(':visible'); + if (modals.length) { + modals.last().focus(); + // Keep class modal-open (deleted by bootstrap hide fnct) on body to allow scrolling inside the modal + $('body').addClass('modal-open'); + } + } + }, + /** + * adds the keydown behavior to the dialogs after external files modifies + * its DOM. + */ + rebindButtonBehavior: function () { + this.$footer.on('keydown', this._onFooterButtonKeyDown); + }, + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * Manages the focus when the dialog closes. The default behavior is to set the focus on the top-most opened popup. + * The goal of this function is to be overridden by all children of the dialog class. + * + * @returns: boolean should return true if the focus has already been set else false. + */ + _focusOnClose: function() { + return false; + }, + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + /** + * Moves the focus to the first button primary in the footer of the dialog + * + * @private + * @param {odooEvent} e + */ + _onFocusControlButton: function (e) { + if (this.$footer) { + if (e) { + e.stopPropagation(); + } + this.$footer.find('.btn-primary:visible:first()').focus(); + } + }, + /** + * Manages the TAB key on the buttons. If you the focus is on a primary + * button and the users tries to tab to go to the next button, display + * a tooltip + * + * @param {jQueryEvent} e + * @private + */ + _onFooterButtonKeyDown: function (e) { + switch(e.which) { + case $.ui.keyCode.TAB: + if (!e.shiftKey && (e.target.classList.contains("btn-primary") || e.target.classList.contains("oe_highlight"))) { + e.preventDefault(); + var $primaryButton = $(e.target); + $primaryButton.tooltip({ + delay: {show: 200, hide:0}, + title: function(){ + return QWeb.render('FormButton.tooltip',{title:$primaryButton.text().toUpperCase()}); + }, + trigger: 'manual', + }); + $primaryButton.tooltip('show'); + } + break; } } }); @@ -207,7 +291,7 @@ Dialog.alert = function (owner, message, options) { text: message, }), title: _t("Alert"), - }, options)).open(); + }, options)).open({shouldFocusButtons:true}); }; // static method to open simple confirm dialog @@ -232,7 +316,7 @@ Dialog.confirm = function (owner, message, options) { text: message, }), title: _t("Confirmation"), - }, options)).open(); + }, options)).open({shouldFocusButtons:true}); }; /** diff --git a/addons/web/static/src/js/core/mixins.js b/addons/web/static/src/js/core/mixins.js index e4738fc2f43..fd8e8f93dbe 100644 --- a/addons/web/static/src/js/core/mixins.js +++ b/addons/web/static/src/js/core/mixins.js @@ -331,6 +331,7 @@ var EventDispatcherMixin = _.extend({}, ParentedMixin, { }, trigger_up: function (name, info) { var event = new OdooEvent(this, name, info); + //console.info('event: ', name, info); this._trigger_up(event); }, _trigger_up: function (event) { diff --git a/addons/web/static/src/js/core/widget.js b/addons/web/static/src/js/core/widget.js index 5e23c3f2c47..bd868596546 100644 --- a/addons/web/static/src/js/core/widget.js +++ b/addons/web/static/src/js/core/widget.js @@ -284,6 +284,16 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { return this; }, + + /** + * Update the modifiers with the newest value. + * Now this.attrs.modifiersValue can be used consistantly even with + * conditional modifiers + * + * @param {Object} modifiers the updated modifiers + */ + updateModifiersValue: function(modifiers) { + }, //-------------------------------------------------------------------------- // Private diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index 5cc3f1d17d2..36a9f0cb258 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -299,6 +299,34 @@ var AbstractField = Widget.extend({ return this._render() || $.when(); }, + /** + * Remove the invalid class on a field + */ + removeInvalidClass: function () { + this.$el.removeClass('o_field_invalid'); + }, + + /** + * add the invalid class on a field + */ + setInvalidClass: function () { + this.$el.addClass('o_field_invalid'); + }, + + /** + * 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 + */ + updateModifiersValue: function(modifiers) { + this.attrs.modifiersValue = modifiers || {}; + }, + + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index ee9958406c5..ae0da5427ad 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -312,13 +312,21 @@ var InputField = DebouncedField.extend({ this._super.apply(this, arguments); // the following code only makes sense in edit mode, with an input - if (this.mode === 'edit') { + if (this.mode === 'edit' && ev.data.direction !== 'cancel') { var input = this.$input[0]; 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.data.direction === "right" && (selecting || input.selectionStart !== input.value.length))) { ev.stopPropagation(); } + if (ev.data.direction ==='next' && this.attrs.modifiersValue.required) { + if (!this.$input.val()){ + this.setInvalidClass(); + ev.stopPropagation(); + } else { + this.removeInvalidClass(); + } + } } }, }); diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index fedaf5c07f6..93c924fad4c 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -682,6 +682,8 @@ var FieldX2Many = AbstractField.extend({ resequence: '_onResequence', save_line: '_onSaveLine', toggle_column_order: '_onToggleColumnOrder', + activate_next_widget: '_onActiveNextWidget', + navigation_move: '_onNavigationMove', }), // We need to trigger the reset on every changes to be aware of the parent changes @@ -797,6 +799,30 @@ var FieldX2Many = AbstractField.extend({ return this._super.apply(this, arguments); }, + /** + * @override + * @returns {jQuery} + */ + getFocusableElement: function () { + return (this.mode === 'edit' && this.$input) || this.$el; + }, + + /** + * @override + * @param {Object} [options] + */ + activate: function (options) { + if (!this.activeActions.create || this.isReadonly || !this.$el.is(":visible")) { + return false; + } + if (this.view.type === 'kanban') { + this.$buttons.find(".o-kanban-button-new").focus(); + } + if (this.view.arch.tag === 'tree') { + this.renderer.$('.o_field_x2many_list_row_add a').focus(); + } + return true; + }, //-------------------------------------------------------------------------- // Private @@ -1159,6 +1185,16 @@ var FieldX2Many = AbstractField.extend({ _onToggleColumnOrder: function (ev) { ev.data.field = this.name; }, + /* + * Move to next widget. + * + * @private + */ + _onActiveNextWidget: function (e) { + e.stopPropagation(); + this.renderer.unselectRow(); + this.trigger_up('navigation_move',{direction:'next'}); + }, }); var FieldOne2Many = FieldX2Many.extend({ @@ -1257,7 +1293,6 @@ var FieldOne2Many = FieldX2Many.extend({ var self = this; // we don't want interference with the components upstream. ev.stopPropagation(); - if (this.editable) { if (!this.activeActions.create) { if (ev.data.onFail) { @@ -1329,7 +1364,6 @@ var FieldMany2Many = FieldX2Many.extend({ _onAddRecord: function (ev) { var self = this; ev.stopPropagation(); - var domain = this.record.getDomain({fieldName: this.name}); new dialogs.SelectCreateDialog(this, { diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js index ccc7e089e65..8d15efda686 100644 --- a/addons/web/static/src/js/services/crash_manager.js +++ b/addons/web/static/src/js/services/crash_manager.js @@ -108,7 +108,7 @@ var CrashManager = core.Class.extend({ title: _.str.capitalize(error.type || error.message) || _t("Odoo Warning"), subtitle: error.data.title, $content: $(QWeb.render('CrashManager.warning', {error: error})) - }).open(); + }).open({shouldFocusButtons:true}); }, show_error: function(error) { if (!this.active) { 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 e8367e79c26..4fb7983d00f 100644 --- a/addons/web/static/src/js/views/basic/basic_renderer.js +++ b/addons/web/static/src/js/views/basic/basic_renderer.js @@ -172,7 +172,7 @@ var BasicRenderer = AbstractRenderer.extend({ * @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 + * @param {boolean} [options.wrap=false] 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 @@ -180,7 +180,8 @@ var BasicRenderer = AbstractRenderer.extend({ */ _activateFieldWidget: function (record, currentIndex, options) { options = options || {}; - _.defaults(options, {inc: 1, wrap: true}); + _.defaults(options, {inc: 1, wrap: false}); + currentIndex = Math.max(0,currentIndex); // do not allow negative currentIndex var recordWidgets = this.allFieldWidgets[record.id] || []; for (var i = 0 ; i < recordWidgets.length ; i++) { @@ -271,6 +272,10 @@ var BasicRenderer = AbstractRenderer.extend({ element.$el.toggleClass("o_readonly_modifier", !!modifiers.readonly); element.$el.toggleClass("o_required_modifier", !!modifiers.required); + if (element.widget) { + element.widget.updateModifiersValue(modifiers); + } + // Call associated callback if (element.callback) { element.callback(element, modifiers, record); diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index 00a4631d3f7..084c9ae4579 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -19,6 +19,8 @@ var FormController = BasicController.extend({ open_one2many_record: '_onOpenOne2ManyRecord', open_record: '_onOpenRecord', toggle_column_order: '_onToggleColumnOrder', + focus_control_button: '_onFocusControlButton', + form_dialog_discarded: '_onFormDialogDiscarded', }), /** * @override @@ -41,12 +43,19 @@ var FormController = BasicController.extend({ // Public //-------------------------------------------------------------------------- + /** * Calls autofocus on the renderer */ autofocus: function () { if (!this.disableAutofocus) { - this.renderer.autofocus(); + var isControlActivted = this.renderer.autofocus(); + if (!isControlActivted) { + // this can happen in read mode if there are no button with class oe_highlight + if (this.$buttons && this.mode === 'readonly') { + return this.$buttons.find('.o_form_button_edit').focus(); + } + } } }, /** @@ -119,13 +128,21 @@ var FormController = BasicController.extend({ this.$buttons = $('
'); if (mustRenderFooterButtons) { this.$buttons.append($footer); + } else { this.$buttons.append(qweb.render("FormView.buttons", {widget: this})); this.$buttons.on('click', '.o_form_button_edit', this._onEdit.bind(this)); this.$buttons.on('click', '.o_form_button_create', this._onCreate.bind(this)); this.$buttons.on('click', '.o_form_button_save', this._onSave.bind(this)); this.$buttons.on('click', '.o_form_button_cancel', this._onDiscard.bind(this)); - + this._assignSaveCancelKeyboardBehavior(this.$buttons.find('.o_form_buttons_edit')); + this.$buttons.find('.o_form_buttons_edit').tooltip({ + delay: {show: 200, hide:0}, + title: function(){ + return qweb.render('SaveCancelButton.tooltip'); + }, + trigger: 'manual', + }); this._updateButtons(); } this.$buttons.appendTo($node); @@ -218,7 +235,37 @@ var FormController = BasicController.extend({ //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- - + /** + * Assign on the buttons save and discard additionnal behavior to facilitate + * the work of the users doing input only using the keyboard + * + * @param {jQueryElement} $saveCancelButtonContainer The div containing the + * save and cancel buttons + * @private + */ + _assignSaveCancelKeyboardBehavior: function ($saveCancelButtonContainer) { + var self = this; + $saveCancelButtonContainer.children().on('keydown', function(e) { + switch(e.which) { + case $.ui.keyCode.ENTER: + e.preventDefault(); + self.saveRecord.apply(self); + break; + case $.ui.keyCode.ESCAPE: + e.preventDefault(); + self._discardChanges.apply(self); + break; + case $.ui.keyCode.TAB: + if (!e.shiftKey && + (e.target.classList.contains("btn-primary") || + e.target.classList.contains("oe_highlight"))) { + $saveCancelButtonContainer.tooltip('show'); + e.preventDefault(); + } + break; + } + }); + }, /** * When a save operation has been confirmed from the model, this method is * called. @@ -488,6 +535,28 @@ var FormController = BasicController.extend({ event.stopPropagation(); this.model.freezeOrder(event.data.id); }, + /** + * Set the focus on the first primary button of the controller (likely Edit) + * + * @private + * @param {OdooEvent} event + */ + _onFocusControlButton:function(e) { + if (this.$buttons) { + e.stopPropagation(); + this.$buttons.find('.btn-primary:visible:first()').focus(); + } + }, + /** + * Reset the focus on the control that openned a Dialog after it was closed + * + * @private + * @param {OdooEvent} event + */ + _onFormDialogDiscarded: function(e) { + e.stopPropagation(); + this.renderer.focusLastActivatedWidget(); + }, /** * Opens a one2many record (potentially new) in a dialog. This handler is * o2m specific as in this case, the changes done on the related record 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 d221f93a4e8..94e870b78d6 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -15,6 +15,10 @@ var FormRenderer = BasicRenderer.extend({ 'click .o_notification_box .oe_field_translate': '_onTranslate', 'click .oe_title, .o_inner_group': '_onClick', }), + custom_events: _.extend({}, BasicRenderer.prototype.custom_events, { + 'navigation_move':'_onNavigationMove', + 'activate_next_widget' : '_onActivateNextWidget', + }), // default col attributes for the rendering of groups INNER_GROUP_COL: 2, OUTER_GROUP_COL: 2, @@ -25,6 +29,7 @@ var FormRenderer = BasicRenderer.extend({ init: function () { this._super.apply(this, arguments); this.idsForLabels = {}; + this.lastActivatedFieldIndex = -1; }, /** * @override @@ -43,10 +48,19 @@ var FormRenderer = BasicRenderer.extend({ /** * Focuses the field having attribute 'default_focus' set, if any, or the * first focusable field otherwise. + * In read mode, delegate which button to give the focus to, to the form_renderer + * + * @returns {int || undefined} the index of the widget activated else + * undefined */ autofocus: function () { if (this.mode === 'readonly') { - return; + var firstPrimaryFormButton = this.$el.find('button.oe_highlight:enabled:visible:first()'); + if (firstPrimaryFormButton.length > 0) { + return firstPrimaryFormButton.focus(); + } else { + return; + } } var focusWidget = this.defaultFocusField; if (!focusWidget || !focusWidget.isFocusable()) { @@ -60,7 +74,7 @@ var FormRenderer = BasicRenderer.extend({ } } if (focusWidget) { - focusWidget.activate({noselect: true}); + return focusWidget.activate({noselect: true}); } }, /** @@ -144,6 +158,15 @@ var FormRenderer = BasicRenderer.extend({ this.$('.o_statusbar_buttons button, .oe_button_box button') .removeAttr('disabled'); }, + /** + * Put the focus on the last activated widget. + * This function is used when closing a dialog to give the focus back to the + * form that has opened it and ensures that the focus is in the correct + * field. + */ + focusLastActivatedWidget: function () { + this._activateNextFieldWidget(this.state, this.lastActivatedFieldIndex - 1); + }, /** * returns the active tab pages for each notebook * @@ -211,7 +234,27 @@ var FormRenderer = BasicRenderer.extend({ //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- - + /** + * @override + */ + _activateNextFieldWidget: function (record, currentIndex) { + //if we are the last widget, we should give the focus to the first Primary Button in the form + //else do the default behavior + if ( (currentIndex + 1) >= (this.allFieldWidgets[record.id] || []).length) { + this.trigger_up('focus_control_button'); + this.lastActivatedFieldIndex = -1; + } else { + var activatedIndex = this._super.apply(this, arguments); + if (activatedIndex === -1 ) { // no widget have been activated, we should go to the edit/save buttons + this.trigger_up('focus_control_button'); + this.lastActivatedFieldIndex = -1; + } + else { + this.lastActivatedFieldIndex = activatedIndex; + } + } + return this.lastActivatedFieldIndex; + }, /** * Add a tooltip on a button * @@ -819,11 +862,15 @@ var FormRenderer = BasicRenderer.extend({ this.defs = defs; var $form = this._renderNode(this.arch).addClass(this.className); delete this.defs; - + return $.when.apply($, defs).then(function () { self._updateView($form.contents()); }, function () { $form.remove(); + }).then(function(){ + if (self.lastActivatedFieldIndex >= 0) { + self._activateNextFieldWidget(self.state, self.lastActivatedFieldIndex); + } }); }, /** @@ -882,7 +929,11 @@ var FormRenderer = BasicRenderer.extend({ //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- - + _onActivateNextWidget: function (e) { + e.stopPropagation(); + var index = this.allFieldWidgets[this.state.id].indexOf(e.data.target); + this._activateNextFieldWidget(this.state, index); + }, /** * Makes the Edit button bounce in readonly * @@ -899,11 +950,13 @@ var FormRenderer = BasicRenderer.extend({ * @param {OdooEvent} ev */ _onNavigationMove: function (ev) { - ev.stopPropagation(); + if (ev.data.direction !== "cancel") { + ev.stopPropagation(); + } var index; if (ev.data.direction === "next") { - index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target); + index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target || ev.target); this._activateNextFieldWidget(this.state, index); } else if (ev.data.direction === "previous") { index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target); 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 11735fa94f8..f3dc79cd4bf 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 @@ -24,6 +24,7 @@ ListRenderer.include({ }), events: _.extend({}, ListRenderer.prototype.events, { 'click .o_field_x2many_list_row_add a': '_onAddRecord', + 'keydown .o_field_x2many_list_row_add a': '_onKeyDownAddRecord', 'click tbody td.o_data_cell': '_onCellClick', 'click tbody tr:not(.o_data_row)': '_onEmptyRowClick', 'click tfoot': '_onFooterClick', @@ -738,6 +739,34 @@ ListRenderer.include({ _onFooterClick: function () { this.unselectRow(); }, + _onKeyDownAddRecord: function(e) { + switch(e.keyCode) { + case $.ui.keyCode.ENTER: + e.stopPropagation(); + e.preventDefault(); + this._onAddRecord(e); + break; + } + }, + /** + * It will returns the first visible widget that is editable + * + * @private + * @returns {Class} Widget returns first widget + */ + _getFirstWidget: function () { + var record = this.state.data[this.currentRow]; + var recordWidgets = this.allFieldWidgets[record.id]; + var firstWidget = _.find(recordWidgets, function (widget) { + var isFirst = widget.$el.is(':visible') && + (widget.$el.has('input').length > 0 || + widget.tagName== 'input') && + !widget.$el.hasClass('o_readonly_modifier'); + return isFirst; + }); + return firstWidget; + }, + /** * Handles the keyboard navigation according to events triggered by field * widgets. @@ -791,12 +820,20 @@ ListRenderer.include({ } break; case 'next': - if (this.currentFieldIndex + 1 < this.columns.length) { - this._selectCell(this.currentRow, this.currentFieldIndex + 1, {wrap: false}) - .fail(this._moveToNextLine.bind(this)); + // When navigating with the keyboard, we want to get out of the list editable if the + // first field is left empty. + var column = this.columns[this.currentFieldIndex]; + var firstWidget = this._getFirstWidget(); + if (column.attrs.name === firstWidget.name && !firstWidget.$input.val()) { + this.trigger_up('activate_next_widget'); } else { - this._moveToNextLine(); - } + if (this.currentFieldIndex + 1 < this.columns.length) { + this._selectCell(this.currentRow, this.currentFieldIndex + 1, {wrap: false}) + .fail(this._moveToNextLine.bind(this)); + } else { + this._moveToNextLine(); + } + } break; case 'next_line': this._moveToNextLine(); diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index d4ae709008c..55f9a2e85d7 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -209,7 +209,13 @@ var FormViewDialog = ViewDialog.extend({ //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- - + /** + * @override + */ + _focusOnClose: function() { + this.trigger_up('form_dialog_discarded'); + return true; + }, _save: function () { var self = this; return this.form_view.saveRecord(this.form_view.handle, { @@ -418,7 +424,13 @@ var SelectCreateDialog = ViewDialog.extend({ dialog.on('closed', this, this.close); return dialog; }, - + /** + * @override + */ + _focusOnClose: function() { + this.trigger_up('form_dialog_discarded'); + return true; + }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index be992c8f38a..94536d30dd2 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -209,7 +209,7 @@
  • -
  • + +
    +
    + Hit ENTER to SAVE
    + Hit ESCAPE to DISCARD +
    +
    +
    + +
    +
    + Hit ENTER to CREATE
    + Hit DOWN to navigate to the list bellow +
    +
    +
    diff --git a/addons/web/static/src/xml/dialog.xml b/addons/web/static/src/xml/dialog.xml index 8f5779e0eb8..68c3ab3f2e3 100644 --- a/addons/web/static/src/xml/dialog.xml +++ b/addons/web/static/src/xml/dialog.xml @@ -7,7 +7,7 @@