[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 <aab@odoo.com>
This commit is contained in:
Julien Mougenot
2019-09-25 06:16:54 +00:00
committed by Aaron Bohy
co-authored by Aaron Bohy
parent d99e80f92a
commit 0ebacff465
3 changed files with 184 additions and 27 deletions
@@ -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
*/
+11 -4
View File
@@ -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;
@@ -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: `
<tree editable="top">
<field name="titi"/>
<field name="grosminet" widget="char"/>
</tree>`,
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: `
<tree editable="top">
<field name="titi"/>
<field name="grosminet1" class="large"/>
<field name="grosminet3" class="large"/>
<field name="grosminet2" class="large"/>
</tree>`,
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);