Allow column resizing in Sao

Closes #13115
This commit is contained in:
Nicolas Évrard
2025-04-14 23:11:48 +02:00
parent 15b3e1f5cf
commit 93ae0df5c3
4 changed files with 200 additions and 5 deletions
+1
View File
@@ -1,3 +1,4 @@
* Allow column resizing
* Add simple chat widget on sidebar
* Support color as type of icon and image
* Support color widget on form view
+67 -4
View File
@@ -708,6 +708,15 @@ img.icon {
display: flex;
flex-direction: column;
min-height: 0;
.tree-dragged-image {
display: block;
height: 0;
left: 0;
position: absolute;
top: 0;
width: 0;
}
}
.wizard-form {
@@ -724,6 +733,11 @@ img.icon {
> table.tree {
width: 100%;
table-layout: fixed;
border: 1px solid transparent;
&.table-bordered {
border-color: @table-border-color;
}
col.selection-state, col.favorite, col.tree-menu {
width: 30px;
@@ -749,15 +763,37 @@ img.icon {
}
}
> thead > tr > th {
> label.sortable {
cursor: pointer;
position: relative;
padding: 5px;
> label {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
&.sortable {
cursor: pointer;
}
}
> div.resizer {
z-index: 1;
position: absolute;
top: 0;
inset-inline-end: -5px;
width: 2px;
height: 100%;
cursor: ew-resize;
padding: 0 4px;
background-color: transparent;
}
&.tree-menu {
cursor: pointer;
overflow: visible;
}
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
&.char, &.text, &.many2one, &.one2one, &.date, &.time, &.selection,
&.reference, &.url, &.email, &.callto, &.sip {
@@ -775,8 +811,15 @@ img.icon {
z-index: 100;
th {
border: 1px solid transparent;
border-bottom: 2px solid @table-border-color;
background-color: @body-bg;
}
&:hover th {
border: 1px solid @table-border-color;
border-bottom-width: 2px;
}
}
> tfoot > tr {
bottom: 0;
@@ -920,6 +963,26 @@ img.icon {
padding-top: 0;
padding-bottom: 0;
}
> table.table-bordered {
border: 1px solid @table-border-color;
> thead,
> tbody,
> tfoot {
> tr:first-child {
> th,
> td {
border: 1px solid @table-border-color;
}
}
}
> thead > tr:first-child {
> th,
> td {
border-bottom-width: 2px;
}
}
}
}
.form, .board {
+2
View File
@@ -2219,6 +2219,7 @@
} else if (~['tree', 'list-form'].indexOf(view.view_type)) {
var paths;
if (view.view_type == 'tree') {
view.save_width();
paths = view.get_expanded_paths();
} else {
paths = [];
@@ -2345,5 +2346,6 @@
});
}
});
Sao.Screen.tree_column_width = {};
Sao.Screen.tree_column_optional = {};
}());
+130 -1
View File
@@ -15,6 +15,35 @@
});
}
// circumvent https://bugzilla.mozilla.org/show_bug.cgi?id=505521
let dragoverFixAdded = false;
let lastWindowDragEvent = null;
const fillEventScreenPosition = (e) => {
e.screenPosition = {
x: e.screenX,
y: e.screenY,
};
if (!navigator.userAgent.search("Firefox")) {
return;
}
if (!dragoverFixAdded) {
window.addEventListener('dragover', (e) => {
lastWindowDragEvent = e;
});
dragoverFixAdded = true;
}
if (lastWindowDragEvent &&
(e.timeStamp - lastWindowDragEvent.timeStamp) < 100) {
e.screenPosition = {
x: lastWindowDragEvent.screenX,
y: lastWindowDragEvent.screenY,
};
}
}
Sao.View.TreeXMLViewParser = Sao.class_(Sao.View.XMLViewParser, {
_parse_tree: function(node, attributes) {
for (const child of node.childNodes) {
@@ -118,6 +147,9 @@
this.treeview = jQuery('<div/>', {
'class': 'treeview responsive'
}).appendTo(this.el);
this.dragged_image= jQuery('<span/>', {
'class': 'tree-dragged-image',
}).html('&nbsp;').appendTo(this.el);
// Synchronize both scrollbars
this.treeview.scroll(() => {
@@ -189,6 +221,7 @@
});
}
let idx = 2;
for (const column of this.columns) {
col = jQuery('<col/>', {
'class': column.attributes.widget,
@@ -197,6 +230,7 @@
'class': column.attributes.widget,
});
th.uniqueId();
th.get(0).dataset.column = idx;
var label = jQuery('<label/>')
.text(column.attributes.string)
.attr('title', column.attributes.string);
@@ -221,6 +255,52 @@
label.addClass('sortable');
}
tr.append(th.append(label));
let resizer = jQuery('<div/>', {
'class': 'resizer',
'draggable': true,
}).appendTo(th);
resizer.on('dragstart', (event) => {
let th = event.target.parentNode;
let headers = th.parentNode.childNodes;
let cols = this.colgroup[0].childNodes;
for (let i = 0; i < headers.length; i++) {
let header = headers[i];
let col = cols[i];
if (header == th) {
break;
}
if (col.style.display != 'none') {
col.style.width = `${header.offsetWidth}px`;
}
}
th.dataset.startPosition = event.screenX;
th.dataset.originalWidth = th.offsetWidth;
this.table.addClass('table-bordered');
event.originalEvent.dataTransfer.setDragImage(
this.dragged_image.get(0), 0, 0);
});
// column_widths is used as a global for the callback of the
// drag event so that when the setTimeout callback is triggered
// the latest value of the width is used
let column_widths = {};
resizer.on('drag', (event) => {
fillEventScreenPosition(event);
let resized_th = event.target.parentNode;
let col = this.colgroup[0].childNodes[resized_th.dataset.column];
let width_offset = (event.screenPosition.x -
resized_th.dataset.startPosition);
if (Sao.i18n.rtl) {
width_offset *= -1;
}
let width = Number(resized_th.dataset.originalWidth) + width_offset;
column_widths[resized_th] = width;
setTimeout(() => {
let width = column_widths[resized_th];
col.style.width = `${width}px`;
});
});
column.header = th;
column.col = col;
@@ -238,7 +318,20 @@
sum_row.append(total_cell);
column.footers.push(total_cell);
}
idx += 1;
}
this.table.on('dragover', (event) => {
// This is necessary so that the 'drop' event is processed
event.preventDefault();
});
this.table.on('drop', (event) => {
event.preventDefault();
if (!this.editable) {
this.table.removeClass('table-bordered');
}
});
this.tbody = jQuery('<tbody/>');
this.table.append(this.tbody);
@@ -352,6 +445,36 @@
}
Sao.Screen.tree_column_optional[this.view_id] = fields;
},
save_width: function() {
var widths = {};
for (let column of this.columns) {
if (!column.get_visible() || !column.attributes.name ||
column instanceof Sao.View.Tree.ButtonColumn) {
continue;
}
// Use the DOM element to retrieve the exact style set
var width = column.col[0].style.width;
if (width.endsWith('px')) {
widths[column.attributes.name] = Number(width.slice(0, -2));
}
}
var model_name = this.screen.model_name;
var TreeWidth = new Sao.Model('ir.ui.view_tree_width');
TreeWidth.execute(
'set_width',
[model_name, widths],
{});
if (Object.prototype.hasOwnProperty.call(
Sao.Screen.tree_column_width, model_name)) {
Object.assign(
Sao.Screen.tree_column_width[model_name],
widths);
} else {
Sao.Screen.tree_column_width[model_name] = widths;
}
},
reset: function() {
this.display_size = null;
},
@@ -826,6 +949,8 @@
domain = inversion.simplify(domain);
var decoder = new Sao.PYSON.Decoder(this.screen.context);
var min_width = [];
let tree_column_width = (
Sao.Screen.tree_column_width[this.screen.model_name] || {});
var tree_column_optional = (
Sao.Screen.tree_column_optional[this.view_id] || {});
for (const column of this.columns) {
@@ -875,7 +1000,11 @@
!column.col.hasClass('selection-state') &&
!column.col.hasClass('favorite')) {
var width, c_width;
if (column.attributes.width) {
width = tree_column_width[name];
if (width) {
c_width = width;
min_width.push(`${width}px`);
} else if (column.attributes.width) {
width = c_width = column.attributes.width;
min_width.push(width + 'px');
} else {