diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 08b6d47f0f5..41885837353 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -334,7 +334,8 @@ var InputField = DebouncedField.extend({
}
if (ev.data.direction ==='next' &&
this.attrs.modifiersValue &&
- this.attrs.modifiersValue.required) {
+ this.attrs.modifiersValue.required &&
+ this.viewType !== 'list') {
if (!this.$input.val()){
this.setInvalidClass();
ev.stopPropagation();
@@ -388,7 +389,7 @@ var NumericField = InputField.extend({
*
* Note: We have to overwrite this method to set the input's type to number if
* option setted into the field.
- *
+ *
* @override
* @private
*/
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index fbb4b0b122c..9dd7c4d1141 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -594,6 +594,7 @@ var BasicModel = AbstractModel.extend({
getDomain: element.getDomain,
getFieldNames: element.getFieldNames,
id: element.id,
+ isDirty: element.isDirty,
limit: element.limit,
model: element.model,
offset: element.offset,
@@ -648,6 +649,7 @@ var BasicModel = AbstractModel.extend({
getFieldNames: element.getFieldNames,
groupedBy: element.groupedBy,
id: element.id,
+ isDirty: element.isDirty,
isOpen: element.isOpen,
limit: element.limit,
model: element.model,
@@ -3490,6 +3492,7 @@ var BasicModel = AbstractModel.extend({
dataPoint.getContext = this._getContext.bind(this, dataPoint);
dataPoint.getDomain = this._getDomain.bind(this, dataPoint);
dataPoint.getFieldNames = this._getFieldNames.bind(this, dataPoint);
+ dataPoint.isDirty = this.isDirty.bind(this, dataPoint.id);
this.localData[dataPoint.id] = dataPoint;
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index 40736b1b508..3691903c079 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -27,6 +27,7 @@ var ListController = BasicController.extend({
selection_changed: '_onSelectionChanged',
toggle_column_order: '_onToggleColumnOrder',
toggle_group: '_onToggleGroup',
+ navigation_move: '_onNavigationMove',
}),
/**
* @constructor
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 9cfd6f0cd61..c423ddd3f02 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
@@ -813,6 +813,10 @@ ListRenderer.include({
_onFooterClick: function () {
this.unselectRow();
},
+ /**
+ * @param {KeyDownEvent} e
+ * @private
+ */
_onKeyDownAddRecord: function(e) {
switch(e.keyCode) {
case $.ui.keyCode.ENTER:
@@ -823,25 +827,25 @@ ListRenderer.include({
}
},
/**
- * It will returns the first visible widget that is editable
+ * It will returns the last visible widget that is editable
*
* @private
- * @returns {Class} Widget returns first widget
+ * @returns {Class} Widget returns last widget
*/
- _getFirstWidget: function () {
+ _getLastWidget: function () {
var record = this.state.data[this.currentRow];
var recordWidgets = this.allFieldWidgets[record.id];
- var firstWidget = _.find(recordWidgets, function (widget) {
- var isFirst =
+ var lastWidget = _.chain(recordWidgets).filter(function (widget) {
+ var isLast =
widget.$el.is(':visible') &&
(
widget.$('input').length > 0 || widget.tagName === 'input' ||
widget.$('textarea').length > 0 || widget.tagName === 'textarea'
) &&
!widget.$el.hasClass('o_readonly_modifier');
- return isFirst;
- });
- return firstWidget;
+ return isLast;
+ }).last().value();
+ return lastWidget;
},
/**
@@ -872,11 +876,22 @@ ListRenderer.include({
break;
case 'next':
// When navigating with the keyboard, we want to get out of the list editable if the
- // first field is left empty.
+ // entire line is left unmodified and we are on the next line.
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');
+ var lastWidget = this._getLastWidget();
+ if (column.attrs.name === lastWidget.name) {
+ if (this.currentRow + 1 < this.state.data.length) {
+ this._selectCell(this.currentRow+1, 0, {wrap:false})
+ .fail(this._moveToNextLine.bind(this));
+ } else {
+ var currentRowData = this.state.data[this.currentRow];
+ if (currentRowData.isDirty(currentRowData.id)) {
+ this._moveToNextLine();
+ }
+ else {
+ this.trigger_up('activate_next_widget');
+ }
+ }
} else {
if (this.currentFieldIndex + 1 < this.columns.length) {
this._selectCell(this.currentRow, this.currentFieldIndex + 1, {wrap: false})
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 8809d219e49..63ac20a0a32 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -13493,7 +13493,8 @@ QUnit.module('relational_fields', {
form.destroy();
});
- QUnit.test('when Navigating to a many to one with tabs, not filling in the first field and hitting tab, we should not add a first line but navigate to the next control', function (assert) {
+ QUnit.test('when Navigating to a many2one with tabs, not filling any field and hitting tab,' +
+ ' we should not add a first line but navigate to the next control', function (assert) {
assert.expect(3);
this.data.partner.records[0].turtles = [];
@@ -13536,6 +13537,10 @@ QUnit.module('relational_fields', {
keyCode: $.ui.keyCode.TAB,
}));
+ // skips the first field of the one2many
+ $(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
+
+ // skips the second (and last) field of the one2many
$(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
assert.strictEqual(assert.strictEqual(form.$el.find('input[name="foo"]')[0],
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index fedb878a669..cb1007c26c8 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -2352,7 +2352,7 @@ QUnit.module('Views', {
});
QUnit.test('pressing tab on last cell of editable list view', function (assert) {
- assert.expect(7);
+ assert.expect(9);
var list = createView({
View: ListView,
@@ -2369,6 +2369,10 @@ QUnit.module('Views', {
assert.strictEqual(document.activeElement.name, "foo",
"focus should be on an input with name = foo");
+ //it will not create a new line unless a modification is made
+ document.activeElement.value = "blip-changed";
+ $(document.activeElement).trigger({type: 'change'});
+
list.$('tr.o_selected_row input[name="foo"]').trigger({type: 'keydown', which: 9}); // tab
assert.strictEqual(document.activeElement.name, "int_field",
"focus should be on an input with name = int_field");
@@ -2380,7 +2384,10 @@ QUnit.module('Views', {
assert.strictEqual(document.activeElement.name, "foo",
"focus should be on an input with name = foo");
- assert.verifySteps(['/web/dataset/search_read', '/web/dataset/call_kw/foo/default_get']);
+ assert.verifySteps(['/web/dataset/search_read',
+ '/web/dataset/call_kw/foo/write',
+ '/web/dataset/call_kw/foo/read',
+ '/web/dataset/call_kw/foo/default_get']);
list.destroy();
});
@@ -2499,7 +2506,7 @@ QUnit.module('Views', {
list.destroy();
});
- QUnit.test('navigation with tab and readonly field', function (assert) {
+ QUnit.test('navigation with tab and readonly field (no modification)', function (assert) {
// This test makes sure that if we have 2 cells in a row, the first in
// edit mode, and the second one readonly, then if we press TAB when the
// focus is on the first, then the focus skip the readonly cells and
@@ -2515,6 +2522,43 @@ QUnit.module('Views', {
// click on first td and press TAB
list.$('td:contains(yop)').last().click();
+
+ list.$('tr.o_selected_row input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.TAB});
+
+ assert.ok(list.$('tr.o_data_row:eq(1)').hasClass('o_selected_row'),
+ "2nd row should be selected");
+
+ // we do it again. This was broken because the this.currentRow variable
+ // was not properly set, and the second TAB could cause a crash.
+ list.$('tr.o_selected_row input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.TAB});
+ assert.ok(list.$('tr.o_data_row:eq(2)').hasClass('o_selected_row'),
+ "3rd row should be selected");
+
+ list.destroy();
+ });
+
+
+ QUnit.test('navigation with tab and readonly field (with modification)', function (assert) {
+ // This test makes sure that if we have 2 cells in a row, the first in
+ // edit mode, and the second one readonly, then if we press TAB when the
+ // focus is on the first, then the focus skips the readonly cells and
+ // directly goes to the next line instead.
+ assert.expect(2);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '',
+ });
+
+ // click on first td and press TAB
+ list.$('td:contains(yop)').last().click();
+
+ //modity the cell content
+ document.activeElement.value = "blip-changed";
+ $(document.activeElement).trigger({type: 'change'});
+
list.$('tr.o_selected_row input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.TAB});
assert.ok(list.$('tr.o_data_row:eq(1)').hasClass('o_selected_row'),
@@ -2578,6 +2622,7 @@ QUnit.module('Views', {
'' +
'' +
'' +
+ '' +
'',
res_id: 1,
viewOptions: {
@@ -2597,10 +2642,10 @@ QUnit.module('Views', {
assert.ok(form.$('.o_field_widget[name=o2m] .o_data_row:nth(1)').hasClass('o_selected_row'),
"second row should be in edition");
- // Press 'Tab' -> should go back to first line as the create action isn't available
+ // Press 'Tab' -> should get out of the one to many and go to the next field of the form
form.$('.o_field_widget[name=o2m] .o_selected_row input').trigger({type: 'keydown', which: 9});
- assert.ok(form.$('.o_field_widget[name=o2m] .o_data_row:first').hasClass('o_selected_row'),
- "first row should be in edition");
+ assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
+ "the next field should be selected");
form.destroy();
});
@@ -2623,12 +2668,12 @@ QUnit.module('Views', {
}
return this._super.apply(this, arguments);
},
- fieldDebounce: 1
+ fieldDebounce: 1,
});
// click on first td and press TAB
list.$('td:contains(yop)').click();
- list.$('tr.o_selected_row input[name="foo"]').val('new value').trigger('input');
+ list.$('tr.o_selected_row input[name="foo"]').val('new value').trigger('change');
list.$('tr.o_selected_row input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.TAB});
assert.strictEqual(list.$('tbody tr:first td:contains(new value)').length, 1,