From e0e61ffd02d19abfebaa236a0a792a1fb13714f6 Mon Sep 17 00:00:00 2001 From: Thomas Piccirello <8296030+Piccirello@users.noreply.github.com> Date: Sun, 27 Oct 2024 22:59:13 -0700 Subject: [PATCH] WebUI: Support auto resizing table columns Auto resize can be triggered by: 1. Double clicking the column's resize handle (its rightmost edge) 2. The table header's context menu Closes #21627. PR #21655. --- src/webui/www/private/scripts/dynamicTable.js | 124 +++++++++++++++++- 1 file changed, 119 insertions(+), 5 deletions(-) diff --git a/src/webui/www/private/scripts/dynamicTable.js b/src/webui/www/private/scripts/dynamicTable.js index 1764c874c..9d3869890 100644 --- a/src/webui/www/private/scripts/dynamicTable.js +++ b/src/webui/www/private/scripts/dynamicTable.js @@ -233,7 +233,7 @@ window.qBittorrent.DynamicTable ??= (() => { resetElementBorderStyle(this.lastHoverTh); el.style.backgroundColor = ""; if (this.currentHeaderAction === "resize") - LocalPreferences.set("column_" + this.resizeTh.columnName + "_width_" + this.dynamicTableDivId, this.columns[this.resizeTh.columnName].width); + this.saveColumnWidth(this.resizeTh.columnName); if ((this.currentHeaderAction === "drag") && (el !== this.lastHoverTh)) { this.saveColumnsOrder(); const val = LocalPreferences.get("columns_order_" + this.dynamicTableDivId).split(","); @@ -260,7 +260,10 @@ window.qBittorrent.DynamicTable ??= (() => { const onCancel = function(el) { this.currentHeaderAction = ""; - this.setSortedColumn(el.columnName); + + // ignore click/touch events performed when on the column's resize area + if (!this.canResize) + this.setSortedColumn(el.columnName); }.bind(this); const onTouch = function(e) { @@ -269,6 +272,18 @@ window.qBittorrent.DynamicTable ??= (() => { this.setSortedColumn(column); }.bind(this); + const onDoubleClick = function(e) { + e.preventDefault(); + this.currentHeaderAction = ""; + + // only resize when hovering on the column's resize area + if (this.canResize) { + this.currentHeaderAction = "resize"; + this.autoResizeColumn(e.target.columnName); + onComplete(e.target); + } + }.bind(this); + const ths = this.fixedTableHeader.getElements("th"); for (let i = 0; i < ths.length; ++i) { @@ -276,6 +291,7 @@ window.qBittorrent.DynamicTable ??= (() => { th.addEventListener("mousemove", mouseMoveFn); th.addEventListener("mouseout", mouseOutFn); th.addEventListener("touchend", onTouch, { passive: true }); + th.addEventListener("dblclick", onDoubleClick); th.makeResizable({ modifiers: { x: "", @@ -311,6 +327,60 @@ window.qBittorrent.DynamicTable ??= (() => { this.updateColumn(columnName); }, + _calculateColumnBodyWidth: function(column) { + const columnIndex = this.getColumnPos(column.name); + const bodyColumn = document.getElementById(this.dynamicTableDivId).querySelectorAll("tr>th")[columnIndex]; + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + context.font = window.getComputedStyle(bodyColumn, null).getPropertyValue("font"); + + const longestTd = { value: "", width: 0 }; + for (const tr of this.tableBody.querySelectorAll("tr")) { + const tds = tr.querySelectorAll("td"); + const td = tds[columnIndex]; + + const buffer = column.calculateBuffer(tr.rowId); + const valueWidth = context.measureText(td.textContent).width; + if ((valueWidth + buffer) > (longestTd.width)) { + longestTd.value = td.textContent; + longestTd.width = valueWidth + buffer; + } + } + + // slight buffer to prevent clipping + return longestTd.width + 10; + }, + + autoResizeColumn: function(columnName) { + const column = this.columns[columnName]; + + let width = column.staticWidth ?? 0; + if (column.staticWidth === null) { + // check required min body width + const bodyTextWidth = this._calculateColumnBodyWidth(column); + + // check required min header width + const columnIndex = this.getColumnPos(column.name); + const headColumn = document.getElementById(this.dynamicTableFixedHeaderDivId).querySelectorAll("tr>th")[columnIndex]; + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + context.font = window.getComputedStyle(headColumn, null).getPropertyValue("font"); + const columnTitle = column.caption; + const sortedIconWidth = 20; + const headTextWidth = context.measureText(columnTitle).width + sortedIconWidth; + + width = Math.max(headTextWidth, bodyTextWidth); + } + + column.width = width; + this.updateColumn(column.name); + this.saveColumnWidth(column.name); + }, + + saveColumnWidth: function(columnName) { + LocalPreferences.set(`column_${columnName}_width_${this.dynamicTableDivId}`, this.columns[columnName].width); + }, + setupHeaderMenu: function() { this.setupDynamicTableHeaderContextMenuClass(); @@ -337,7 +407,16 @@ window.qBittorrent.DynamicTable ??= (() => { return listItem; }; - const actions = {}; + const actions = { + autoResizeAction: function(element, ref, action) { + this.autoResizeColumn(element.columnName); + }.bind(this), + + autoResizeAllAction: function(element, ref, action) { + for (const { name } of this.columns) + this.autoResizeColumn(name); + }.bind(this), + }; const onMenuItemClicked = function(element, ref, action) { this.showColumn(action, this.columns[action].visible === "0"); @@ -357,10 +436,30 @@ window.qBittorrent.DynamicTable ??= (() => { actions[this.columns[i].name] = onMenuItemClicked; } + const createResizeElement = function(text, href) { + const anchor = document.createElement("a"); + anchor.href = href; + anchor.textContent = text; + + const spacer = document.createElement("span"); + spacer.style = "display: inline-block; width: calc(.5em + 16px);"; + anchor.prepend(spacer); + + const li = document.createElement("li"); + li.appendChild(anchor); + return li; + }; + + const autoResizeAllElement = createResizeElement("Resize All", "#autoResizeAllAction"); + const autoResizeElement = createResizeElement("Resize", "#autoResizeAction"); + + ul.firstChild.classList.add("separator"); + ul.insertBefore(autoResizeAllElement, ul.firstChild); + ul.insertBefore(autoResizeElement, ul.firstChild); ul.inject(document.body); this.headerContextMenu = new DynamicTableHeaderContextMenuClass({ - targets: "#" + this.dynamicTableFixedHeaderDivId + " tr", + targets: "#" + this.dynamicTableFixedHeaderDivId + " tr th", actions: actions, menu: menuId, offsets: { @@ -402,6 +501,8 @@ window.qBittorrent.DynamicTable ??= (() => { td.title = value; }; column["onResize"] = null; + column["staticWidth"] = null; + column["calculateBuffer"] = () => 0; this.columns.push(column); this.columns[name] = column; @@ -1163,7 +1264,7 @@ window.qBittorrent.DynamicTable ??= (() => { td.resized = false; } }; - + this.columns["progress"].staticWidth = 100; this.columns["progress"].onResize = function(columnName) { const pos = this.getColumnPos(columnName); const trs = this.tableBody.getElements("tr"); @@ -1721,6 +1822,7 @@ window.qBittorrent.DynamicTable ??= (() => { // relevance this.columns["relevance"].updateTd = this.columns["progress"].updateTd; + this.columns["relevance"].staticWidth = 100; // files this.columns["files"].updateTd = function(td, row) { @@ -2138,6 +2240,7 @@ window.qBittorrent.DynamicTable ??= (() => { checkbox.indeterminate = false; td.adopt(treeImg, checkbox); }; + this.columns["checked"].staticWidth = 50; // original this.columns["original"].updateTd = function(td, row) { @@ -2460,6 +2563,7 @@ window.qBittorrent.DynamicTable ??= (() => { td.adopt(treeImg, window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value)); } }; + this.columns["checked"].staticWidth = 50; // name this.columns["name"].updateTd = function(td, row) { @@ -2514,6 +2618,12 @@ window.qBittorrent.DynamicTable ??= (() => { td.replaceChildren(span); } }; + this.columns["name"].calculateBuffer = function(rowId) { + const node = that.getNode(rowId); + // folders add 20px for folder icon and 15px for collapse icon + const folderBuffer = node.isFolder ? 35 : 0; + return (node.depth * 20) + folderBuffer; + }; // size this.columns["size"].updateTd = displaySize; @@ -2534,6 +2644,7 @@ window.qBittorrent.DynamicTable ??= (() => { progressBar.setValue(value.toFloat()); } }; + this.columns["progress"].staticWidth = 100; // priority this.columns["priority"].updateTd = function(td, row) { @@ -2545,6 +2656,7 @@ window.qBittorrent.DynamicTable ??= (() => { else td.adopt(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value)); }; + this.columns["priority"].staticWidth = 140; // remaining, availability this.columns["remaining"].updateTd = displaySize; @@ -2942,6 +3054,7 @@ window.qBittorrent.DynamicTable ??= (() => { $("cbRssDlRule" + row.rowId).checked = row.full_data.checked; } }; + this.columns["checked"].staticWidth = 50; }, setupHeaderMenu: function() {}, setupHeaderEvents: function() {}, @@ -3033,6 +3146,7 @@ window.qBittorrent.DynamicTable ??= (() => { $("cbRssDlFeed" + row.rowId).checked = row.full_data.checked; } }; + this.columns["checked"].staticWidth = 50; }, setupHeaderMenu: function() {}, setupHeaderEvents: function() {},