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 a134faccc81..c9fd1cea92b 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 @@ -411,18 +411,6 @@ ListRenderer.include({ var $tds = $row.children('.o_data_cell'); var oldWidgets = _.clone(this.allFieldWidgets[record.id]); - // When switching to edit mode, force the dimensions of all cells to - // their current value so that they won't change if their content - // changes, to prevent the view from flickering. - // We need to use getBoundingClientRect instead of outerWidth to - // prevent a rounding issue on Firefox. - if (editMode) { - $tds.each(function () { - var $td = $(this); - $td.css({width: $td[0].getBoundingClientRect().width}); - }); - } - // Prepare options for cell rendering (this depends on the mode) var options = { renderInvisible: editMode, @@ -608,19 +596,25 @@ ListRenderer.include({ } const table = this.el.getElementsByTagName('table')[0]; const thead = table.getElementsByTagName('thead')[0]; - - // Set table layout auto and remove inline style to make sure that css - // rules apply (e.g. fixed width of record selector) - table.style.tableLayout = 'auto'; - [...table.getElementsByTagName('th')].forEach((th) => { - th.style.width = null; - }); - - // Freeze each th width according to their size in auto layout const thElements = [...thead.getElementsByTagName('th')]; - const thWidths = thElements.map((th) => th.offsetWidth); + + let columnWidths = this.columnWidths; + if (!columnWidths) { // no column widths to restore + // Set table layout auto and remove inline style to make sure that css + // rules apply (e.g. fixed width of record selector) + table.style.tableLayout = 'auto'; + thElements.forEach((th) => { + th.style.width = null; + th.style.maxWidth = null; + }); + + // Squeeze the table by applying a max-width on largest columns to + // ensure that it doesn't overflow + columnWidths = this._squeezeTable(); + } + thElements.forEach((th, index) => { - th.style.width = `${this.columnWidths ? this.columnWidths[index] : thWidths[index]}px`; + th.style.width = `${columnWidths[index]}px`; }); // Set the table layout to fixed @@ -1256,6 +1250,67 @@ ListRenderer.include({ }); }); }, + /** + * Set a maximum width on the largest columns in the list in case the table + * is overflowing. The idea is to shrink largest columns first, but to + * ensure that they are still the largest at the end (maybe in equal measure + * with other columns). + * + * @private + * @returns {integer[]} width (in px) of each column s.t. the table doesn't + * overflow + */ + _squeezeTable: function () { + const table = this.el.getElementsByTagName('table')[0]; + const thead = table.getElementsByTagName('thead')[0]; + const thElements = [...thead.getElementsByTagName('th')]; + const columnWidths = thElements.map(th => th.offsetWidth); + const getWidth = th => columnWidths[thElements.indexOf(th)] || 0; + const getTotalWidth = () => thElements.reduce((tot, th, i) => tot + columnWidths[i], 0); + const shrinkColumns = (columns, width) => { + let thresholdReached = false; + columns.forEach(th => { + const index = thElements.indexOf(th); + let maxWidth = columnWidths[index] - Math.ceil(width / columns.length); + if (maxWidth < 92) { // prevent the columns from shrinking under 92px (~ date field) + maxWidth = 92; + thresholdReached = true; + } + th.style.maxWidth = `${maxWidth}px`; + columnWidths[index] = maxWidth; + }); + return thresholdReached; + }; + // Sort columns, largest first + const sortedThs = [...thead.getElementsByTagName('th')] + .sort((a, b) => getWidth(b) - getWidth(a)); + const allowedWidth = table.parentNode.offsetWidth; + + let totalWidth = getTotalWidth(); + let stop = false; + let index = 0; + while (totalWidth > allowedWidth && !stop) { + // Find the largest columns + index++; + const largests = sortedThs.slice(0, index); + while (getWidth(largests[0]) === getWidth(sortedThs[index])) { + largests.push(sortedThs[index]); + index++; + } + + // Compute the number of px to remove from the largest columns + const nextLargest = sortedThs[index]; // largest column when omitting those in largests + const totalToRemove = totalWidth - allowedWidth; + const canRemove = (getWidth(largests[0]) - getWidth(nextLargest)) * largests.length; + + // Shrink the largests columns + stop = shrinkColumns(largests, Math.min(totalToRemove, canRemove)); + + totalWidth = getTotalWidth(); + } + + return columnWidths; + }, /** * @private */ diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss index 6c3d452fb47..4ee916f9a1f 100644 --- a/addons/web/static/src/scss/list_view.scss +++ b/addons/web/static/src/scss/list_view.scss @@ -211,10 +211,6 @@ .o_list_record_remove_header { width: 32px; } - &>tbody>tr>td.o_list_char, - &>tbody>tr>td.o_list_many2one { - white-space: nowrap; - } // checkboxes in editable lists should be clickable and activate the row .o_data_row:not(.o_selected_row) .o_data_cell { &.o_toggle_button_cell:not(.o_readonly_modifier) button, // registry: toggle_button @@ -224,6 +220,17 @@ } } + .o_data_row:not(.o_selected_row) { + .o_list_many2one, + .o_list_char, + .o_list_number { + white-space: nowrap; + .o_field_widget { + display: inline; + } + } + } + .o_data_row.o_selected_row > .o_data_cell:not(.o_readonly_modifier):not(.o_invisible_modifier) { position: relative; // for o_field_translate background-color: white; diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 277a980bb4d..540606d9770 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -1636,6 +1636,101 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('editable list: overflowing table', async function (assert) { + assert.expect(1); + + this.data.bar = { + fields: { + titi: { string: "Small char", type: "char", sortable: true }, + grosminet: { string: "Beeg char", type: "char", sortable: true }, + }, + records: [ + { + id: 1, + titi: "Tiny text", + grosminet: + // Just want to make sure that the table is overflowed + `Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Donec est massa, gravida eget dapibus ac, eleifend eget libero. + Suspendisse feugiat sed massa eleifend vestibulum. Sed tincidunt + velit sed lacinia lacinia. Nunc in fermentum nunc. Vestibulum ante + ipsum primis in faucibus orci luctus et ultrices posuere cubilia + Curae; Nullam ut nisi a est ornare molestie non vulputate orci. + Nunc pharetra porta semper. Mauris dictum eu nulla a pulvinar. Duis + eleifend odio id ligula congue sollicitudin. Curabitur quis aliquet + nunc, ut aliquet enim. Suspendisse malesuada felis non metus + efficitur aliquet.`, + }, + ], + }; + const list = await createView({ + arch: ` + + + + `, + data: this.data, + model: 'bar', + View: ListView, + }); + + assert.strictEqual(list.$('table').width(), list.$('.o_list_view').width(), + "Table should not be stretched by its content"); + + list.destroy(); + }); + + QUnit.test('editable list: overflowing table (3 columns)', async function (assert) { + assert.expect(4); + + const longText = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Donec est massa, gravida eget dapibus ac, eleifend eget libero. + Suspendisse feugiat sed massa eleifend vestibulum. Sed tincidunt + velit sed lacinia lacinia. Nunc in fermentum nunc. Vestibulum ante + ipsum primis in faucibus orci luctus et ultrices posuere cubilia + Curae; Nullam ut nisi a est ornare molestie non vulputate orci. + Nunc pharetra porta semper. Mauris dictum eu nulla a pulvinar. Duis + eleifend odio id ligula congue sollicitudin. Curabitur quis aliquet + nunc, ut aliquet enim. Suspendisse malesuada felis non metus + efficitur aliquet.`; + + this.data.bar = { + fields: { + titi: { string: "Small char", type: "char", sortable: true }, + grosminet1: { string: "Beeg char 1", type: "char", sortable: true }, + grosminet2: { string: "Beeg char 2", type: "char", sortable: true }, + grosminet3: { string: "Beeg char 3", type: "char", sortable: true }, + }, + records: [{ + id: 1, + titi: "Tiny text", + grosminet1: longText, + grosminet2: longText + longText, + grosminet3: longText + longText + longText, + }], + }; + const list = await createView({ + arch: ` + + + + + + `, + data: this.data, + model: 'bar', + View: ListView, + }); + + assert.strictEqual(list.$('table').width(), list.$('.o_list_view').width()); + const largeCells = list.$('.o_data_cell.large'); + assert.strictEqual(largeCells[0].offsetWidth, largeCells[1].offsetWidth); + assert.strictEqual(largeCells[1].offsetWidth, largeCells[2].offsetWidth); + assert.ok(list.$('.o_data_cell:not(.large)')[0].offsetWidth < largeCells[0].offsetWidth); + + list.destroy(); + }); + QUnit.test('editable list: list view in an initially unselected notebook page', async function (assert) { assert.expect(8);