From 0ebacff465e35dc8465560bddb8ac7ad911bf2cc Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Fri, 20 Sep 2019 14:29:17 +0000 Subject: [PATCH] [FIX] web: list: prevent tables from overflowing In an editable list, select a very long value in a many2one... Before this commit, the table stretched as much as it needed to to display all of the columns content. Now, the largest columns are trimmed in order to keep the table as wide as the list container (meaning it won't overflow anymore unless there is a huge load of columns). Also ensured that values trimmed by this feature are followed by an ellipsis. Task 2068261 Co-authored-by: Aaron Bohy --- .../js/views/list/list_editable_renderer.js | 101 ++++++++++++++---- addons/web/static/src/scss/list_view.scss | 15 ++- addons/web/static/tests/views/list_tests.js | 95 ++++++++++++++++ 3 files changed, 184 insertions(+), 27 deletions(-) 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);