WebUI: Improve table overflow handling

This PR relies on flexbox to ensure all WebUI tables are the correct height without overflowing. Table headers are now always visible and JS-based dynamic resizing is no longer needed.

PR #21652.
This commit is contained in:
Thomas Piccirello 2024-11-03 00:11:30 -07:00 committed by GitHub
parent b083029841
commit dc30b9c2ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 65 additions and 73 deletions

View file

@ -246,6 +246,7 @@ li.divider {
} }
.pad { .pad {
height: 100%;
padding: 8px; padding: 8px;
} }

View file

@ -32,6 +32,11 @@
vertical-align: middle; vertical-align: middle;
} }
#transferList #transferList_pad {
/* override for default mocha inline style */
display: flex !important;
}
tr.dynamicTableHeader { tr.dynamicTableHeader {
cursor: pointer; cursor: pointer;
} }
@ -82,7 +87,14 @@ tr.dynamicTableHeader {
padding-left: 25px; padding-left: 25px;
} }
div:has(> div.dynamicTableFixedHeaderDiv):not(.invisible) {
display: flex;
flex-direction: column;
height: 100%;
}
.dynamicTableFixedHeaderDiv { .dynamicTableFixedHeaderDiv {
flex-shrink: 0;
overflow: hidden; overflow: hidden;
} }
@ -92,6 +104,7 @@ tr.dynamicTableHeader {
} }
.dynamicTableDiv { .dynamicTableDiv {
flex-grow: 1;
overflow: auto; overflow: auto;
} }

View file

@ -270,6 +270,14 @@ a.propButton img {
overflow: hidden auto; overflow: hidden auto;
} }
.propertiesTabContent {
height: 100%;
> div {
height: 100%;
}
}
/* context menu specific */ /* context menu specific */
.contextMenu { .contextMenu {

View file

@ -452,7 +452,7 @@
</div> </div>
<input id="closeButton" type="button" value="Close" style="float: right; width: 30%;"> <input id="closeButton" type="button" value="Close" style="float: right; width: 30%;">
</div> </div>
<div id="torrentFiles" class="panel" style="position: absolute; top: 0; right: 0; bottom: 0; left: 228px; margin: 35px 10px 45px 20px; border-bottom: 0"> <div id="torrentFiles" class="panel" style="position: absolute; top: 0; right: 0; bottom: 0; left: 228px; margin: 35px 10px 45px 20px; border-bottom: 0; height: initial;">
<div id="bulkRenameFilesTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv"> <div id="bulkRenameFilesTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable"> <table class="dynamicTable">
<thead> <thead>

View file

@ -100,33 +100,6 @@ window.qBittorrent.DynamicTable ??= (() => {
tableDiv.addEventListener("scroll", () => { tableDiv.addEventListener("scroll", () => {
tableElement.style.left = `${-tableDiv.scrollLeft}px`; tableElement.style.left = `${-tableDiv.scrollLeft}px`;
}); });
// if the table exists within a panel
const parentPanel = tableDiv.getParent(".panel");
if (parentPanel) {
const resizeFn = (entries) => {
const panel = entries[0].target;
let h = panel.getBoundingClientRect().height - tableFixedHeaderDiv.getBoundingClientRect().height;
tableDiv.style.height = `${h}px`;
// Workaround due to inaccurate calculation of elements heights by browser
let n = 2;
// is panel vertical scrollbar visible or does panel content not fit?
while (((panel.clientWidth !== panel.offsetWidth) || (panel.clientHeight !== panel.scrollHeight)) && (n > 0)) {
--n;
h -= 0.5;
tableDiv.style.height = `${h}px`;
}
};
const resizeDebouncer = window.qBittorrent.Misc.createDebounceHandler(100, (entries) => {
resizeFn(entries);
});
const resizeObserver = new ResizeObserver(resizeDebouncer);
resizeObserver.observe(parentPanel, { box: "border-box" });
}
}, },
setupHeaderEvents: function() { setupHeaderEvents: function() {

View file

@ -205,10 +205,10 @@ window.qBittorrent.Search ??= (() => {
// unhide the results elements // unhide the results elements
if (numSearchTabs() >= 1) { if (numSearchTabs() >= 1) {
$("searchResultsNoSearches").style.display = "none"; $("searchResultsNoSearches").classList.add("invisible");
$("searchResultsFilters").style.display = "block"; $("searchResultsFilters").classList.remove("invisible");
$("searchResultsTableContainer").style.display = "block"; $("searchResultsTableContainer").classList.remove("invisible");
$("searchTabsToolbar").style.display = "block"; $("searchTabsToolbar").classList.remove("invisible");
} }
// select new tab // select new tab
@ -271,10 +271,10 @@ window.qBittorrent.Search ??= (() => {
$("numSearchResultsVisible").textContent = 0; $("numSearchResultsVisible").textContent = 0;
$("numSearchResultsTotal").textContent = 0; $("numSearchResultsTotal").textContent = 0;
$("searchResultsNoSearches").style.display = "block"; $("searchResultsNoSearches").classList.remove("invisible");
$("searchResultsFilters").style.display = "none"; $("searchResultsFilters").classList.add("invisible");
$("searchResultsTableContainer").style.display = "none"; $("searchResultsTableContainer").classList.add("invisible");
$("searchTabsToolbar").style.display = "none"; $("searchTabsToolbar").classList.add("invisible");
} }
else if (isTabSelected && newTabToSelect) { else if (isTabSelected && newTabToSelect) {
setActiveTab(newTabToSelect); setActiveTab(newTabToSelect);
@ -670,9 +670,9 @@ window.qBittorrent.Search ??= (() => {
const searchPluginsEmpty = (searchPlugins.length === 0); const searchPluginsEmpty = (searchPlugins.length === 0);
if (!searchPluginsEmpty) { if (!searchPluginsEmpty) {
$("searchResultsNoPlugins").style.display = "none"; $("searchResultsNoPlugins").classList.add("invisible");
if (numSearchTabs() === 0) if (numSearchTabs() === 0)
$("searchResultsNoSearches").style.display = "block"; $("searchResultsNoSearches").classList.remove("invisible");
// sort plugins alphabetically // sort plugins alphabetically
const allPlugins = searchPlugins.sort((left, right) => { const allPlugins = searchPlugins.sort((left, right) => {

View file

@ -1,5 +1,6 @@
<style type="text/css"> <style type="text/css">
#logTopBar { #logTopBar {
flex-shrink: 0;
margin-top: 1em; margin-top: 1em;
} }
@ -22,13 +23,16 @@
} }
#logView { #logView {
display: flex;
flex-direction: column;
height: 100%;
padding: 0 20px; padding: 0 20px;
overflow: auto; overflow: auto;
} }
#logContentView { #logContentView {
display: block; flex-grow: 1;
vertical-align: top; overflow: auto;
} }
#logMessageTableFixedHeaderDiv .dynamicTableHeader, #logMessageTableFixedHeaderDiv .dynamicTableHeader,
@ -227,7 +231,6 @@
tableInfo["peer"].instance.setup("logPeerTableDiv", "logPeerTableFixedHeaderDiv", logTableContextMenu); tableInfo["peer"].instance.setup("logPeerTableDiv", "logPeerTableFixedHeaderDiv", logTableContextMenu);
MUI.Panels.instances.LogPanel.contentEl.style.height = "100%"; MUI.Panels.instances.LogPanel.contentEl.style.height = "100%";
$("logView").style.height = "inherit";
load(); load();
}; };

View file

@ -2,13 +2,18 @@
#rssView { #rssView {
padding: 20px 20px 0 20px; padding: 20px 20px 0 20px;
height: calc(100% - 20px); height: calc(100% - 20px);
display: flex;
flex-direction: column;
}
#rssTopBar {
flex-shrink: 0;
} }
#rssContentView { #rssContentView {
display: table;
width: 100%; width: 100%;
height: calc(100% - 30px); flex-grow: 1;
vertical-align: top; overflow: auto;
} }
#rssFeedFixedHeaderDiv .dynamicTableHeader, #rssFeedFixedHeaderDiv .dynamicTableHeader,
@ -45,6 +50,7 @@
#rightRssColumn { #rightRssColumn {
overflow: auto; overflow: auto;
height: 100%;
} }
#rssFeedTableDiv, #rssFeedTableDiv,
@ -53,17 +59,21 @@
} }
#rssTorrentDetailsName { #rssTorrentDetailsName {
flex-shrink: 0;
background-color: var(--color-background-blue); background-color: var(--color-background-blue);
padding: 0; padding: 0;
color: var(--color-text-white); color: var(--color-text-white);
} }
#rssTorrentDetailsDate { #rssTorrentDetailsDate {
flex-shrink: 1;
background-color: var(--color-background-default); background-color: var(--color-background-default);
} }
#rssDetailsView { #rssDetailsView {
height: calc(100vh - 135px); display: flex;
flex-direction: column;
height: 100%;
overflow: auto; overflow: auto;
} }
@ -85,6 +95,7 @@
} }
#rssDescription { #rssDescription {
flex-grow: 2;
width: 100%; width: 100%;
border: none; border: none;
} }
@ -206,18 +217,6 @@
if (!pref.rss_processing_enabled) if (!pref.rss_processing_enabled)
$("rssFetchingDisabled").removeClass("invisible"); $("rssFetchingDisabled").removeClass("invisible");
// recalculate heights
const nonPageHeight = $("rssTopBar").getBoundingClientRect().height
+ $("desktopHeader").getBoundingClientRect().height
+ $("desktopFooterWrapper").getBoundingClientRect().height + 20;
$("rssDetailsView").style.height = "calc(100vh - " + nonPageHeight + "px)";
const nonTableHeight = nonPageHeight + $("rssFeedFixedHeaderDiv").getBoundingClientRect().height;
$("rssFeedTableDiv").style.height = "calc(100vh - " + nonTableHeight + "px)";
$("rssArticleTableDiv").style.height = "calc(100vh - " + nonTableHeight + "px)";
$("rssContentView").style.height = "calc(100% - " + $("rssTopBar").getBoundingClientRect().height + "px)";
const rssFeedContextMenu = new window.qBittorrent.ContextMenu.RssFeedContextMenu({ const rssFeedContextMenu = new window.qBittorrent.ContextMenu.RssFeedContextMenu({
targets: ".rssFeedContextMenuTarget", targets: ".rssFeedContextMenuTarget",
menu: "rssFeedMenu", menu: "rssFeedMenu",
@ -451,11 +450,6 @@
const torrentDescription = document.createRange().createContextualFragment('<iframe sandbox id="rssDescription"></iframe>'); const torrentDescription = document.createRange().createContextualFragment('<iframe sandbox id="rssDescription"></iframe>');
$("rssDetailsView").append(torrentDescription); $("rssDetailsView").append(torrentDescription);
document.getElementById("rssDescription").srcdoc = '<html><head><link rel="stylesheet" type="text/css" href="css/style.css"></head><body>' + article.description + "</body></html>"; document.getElementById("rssDescription").srcdoc = '<html><head><link rel="stylesheet" type="text/css" href="css/style.css"></head><body>' + article.description + "</body></html>";
// calculate height to fill screen
document.getElementById("rssDescription").style.height =
"calc(100% - " + document.getElementById("rssTorrentDetailsName").offsetHeight + "px - "
+ document.getElementById("rssTorrentDetailsDate").offsetHeight + "px - 5px)";
} }
}; };

View file

@ -113,7 +113,7 @@
<span></span> <span></span>
</div> </div>
<div id="searchResultsNoSearches" style="display: none"> <div id="searchResultsNoSearches" class="invisible">
<table> <table>
<tbody> <tbody>
<tr> <tr>
@ -126,13 +126,13 @@
<span></span> <span></span>
</div> </div>
<div id="searchTabsToolbar" class="toolbarTabs" style="border-bottom: 1px solid var(--color-border-default); display: none"> <div id="searchTabsToolbar" class="toolbarTabs invisible" style="border-bottom: 1px solid var(--color-border-default);">
<ul id="searchTabs" class="tab-menu"></ul> <ul id="searchTabs" class="tab-menu"></ul>
<div class="clear"></div> <div class="clear"></div>
</div> </div>
<div id="searchResultsFilters" style="display: none;"> <div id="searchResultsFilters" class="invisible">
<input type="text" id="searchInNameFilter" placeholder="QBT_TR(Filter)QBT_TR[CONTEXT=SearchEngineWidget]" autocorrect="off" autocapitalize="none"> <input type="text" id="searchInNameFilter" placeholder="QBT_TR(Filter)QBT_TR[CONTEXT=SearchEngineWidget]" aria-label="QBT_TR(Filter)QBT_TR[CONTEXT=SearchEngineWidget]" autocorrect="off" autocapitalize="none">
<span>QBT_TR(Results)QBT_TR[CONTEXT=SearchEngineWidget] (QBT_TR(showing)QBT_TR[CONTEXT=SearchEngineWidget] <span id="numSearchResultsVisible" class="numSearchResults">0</span> QBT_TR(out of)QBT_TR[CONTEXT=SearchEngineWidget] <span id="numSearchResultsTotal" class="numSearchResults">0</span>):</span> <span>QBT_TR(Results)QBT_TR[CONTEXT=SearchEngineWidget] (QBT_TR(showing)QBT_TR[CONTEXT=SearchEngineWidget] <span id="numSearchResultsVisible" class="numSearchResults">0</span> QBT_TR(out of)QBT_TR[CONTEXT=SearchEngineWidget] <span id="numSearchResultsTotal" class="numSearchResults">0</span>):</span>
@ -146,14 +146,14 @@
<img id="searchResultsGranularFiltersWarning" src="images/dialog-warning.svg" title="QBT_TR(Increase window width to display additional filters)QBT_TR[CONTEXT=SearchEngineWidget]" alt="QBT_TR(Warning)QBT_TR[CONTEXT=SearchEngineWidget]" width="24" height="24"> <img id="searchResultsGranularFiltersWarning" src="images/dialog-warning.svg" title="QBT_TR(Increase window width to display additional filters)QBT_TR[CONTEXT=SearchEngineWidget]" alt="QBT_TR(Warning)QBT_TR[CONTEXT=SearchEngineWidget]" width="24" height="24">
<div id="searchResultsGranularFilters"> <div id="searchResultsGranularFilters">
<span style="margin-left: 15px;">QBT_TR(Seeds:)QBT_TR[CONTEXT=SearchEngineWidget]</span> <label for="searchMinSeedsFilter" style="margin-left: 15px;">QBT_TR(Seeds:)QBT_TR[CONTEXT=SearchEngineWidget]</label>
<input type="number" min="0" max="1000" id="searchMinSeedsFilter" value="0" onchange="qBittorrent.Search.searchSeedsFilterChanged()"> <input type="number" min="0" max="1000" id="searchMinSeedsFilter" value="0" onchange="qBittorrent.Search.searchSeedsFilterChanged()">
<span>QBT_TR(to)QBT_TR[CONTEXT=SearchEngineWidget]</span> <label for="searchMaxSeedsFilter">QBT_TR(to)QBT_TR[CONTEXT=SearchEngineWidget]</label>
<input type="number" min="0" max="1000" id="searchMaxSeedsFilter" value="0" onchange="qBittorrent.Search.searchSeedsFilterChanged()"> <input type="number" min="0" max="1000" id="searchMaxSeedsFilter" value="0" onchange="qBittorrent.Search.searchSeedsFilterChanged()">
<span style="margin-left: 15px;">QBT_TR(Size:)QBT_TR[CONTEXT=SearchEngineWidget]</span> <label for="searchMinSizeFilter" style="margin-left: 15px;">QBT_TR(Size:)QBT_TR[CONTEXT=SearchEngineWidget]</label>
<input type="number" min="0" max="1000" step=".01" value="0.00" id="searchMinSizeFilter" onchange="qBittorrent.Search.searchSizeFilterChanged()"> <input type="number" min="0" max="1000" step=".01" value="0.00" id="searchMinSizeFilter" onchange="qBittorrent.Search.searchSizeFilterChanged()">
<select id="searchMinSizePrefix" onchange="qBittorrent.Search.searchSizeFilterPrefixChanged()"> <select id="searchMinSizePrefix" onchange="qBittorrent.Search.searchSizeFilterPrefixChanged()" aria-label="QBT_TR(Min size prefix)QBT_TR[CONTEXT=SearchEngineWidget]">
<option value="0">QBT_TR(B)QBT_TR[CONTEXT=misc]</option> <option value="0">QBT_TR(B)QBT_TR[CONTEXT=misc]</option>
<option value="1">QBT_TR(KiB)QBT_TR[CONTEXT=misc]</option> <option value="1">QBT_TR(KiB)QBT_TR[CONTEXT=misc]</option>
<option value="2" selected>QBT_TR(MiB)QBT_TR[CONTEXT=misc]</option> <option value="2" selected>QBT_TR(MiB)QBT_TR[CONTEXT=misc]</option>
@ -162,9 +162,9 @@
<option value="5">QBT_TR(PiB)QBT_TR[CONTEXT=misc]</option> <option value="5">QBT_TR(PiB)QBT_TR[CONTEXT=misc]</option>
<option value="6">QBT_TR(EiB)QBT_TR[CONTEXT=misc]</option> <option value="6">QBT_TR(EiB)QBT_TR[CONTEXT=misc]</option>
</select> </select>
<span>QBT_TR(to)QBT_TR[CONTEXT=SearchEngineWidget]</span> <label for="searchMaxSizeFilter">QBT_TR(to)QBT_TR[CONTEXT=SearchEngineWidget]</label>
<input type="number" min="0" max="1000" step=".01" value="0.00" id="searchMaxSizeFilter" onchange="qBittorrent.Search.searchSizeFilterChanged()"> <input type="number" min="0" max="1000" step=".01" value="0.00" id="searchMaxSizeFilter" onchange="qBittorrent.Search.searchSizeFilterChanged()">
<select id="searchMaxSizePrefix" onchange="qBittorrent.Search.searchSizeFilterPrefixChanged()"> <select id="searchMaxSizePrefix" onchange="qBittorrent.Search.searchSizeFilterPrefixChanged()" aria-label="QBT_TR(Max size prefix)QBT_TR[CONTEXT=SearchEngineWidget]">
<option value="0">QBT_TR(B)QBT_TR[CONTEXT=misc]</option> <option value="0">QBT_TR(B)QBT_TR[CONTEXT=misc]</option>
<option value="1">QBT_TR(KiB)QBT_TR[CONTEXT=misc]</option> <option value="1">QBT_TR(KiB)QBT_TR[CONTEXT=misc]</option>
<option value="2">QBT_TR(MiB)QBT_TR[CONTEXT=misc]</option> <option value="2">QBT_TR(MiB)QBT_TR[CONTEXT=misc]</option>
@ -177,7 +177,7 @@
</div> </div>
</div> </div>
<div id="searchResultsTableContainer" style="display: none"> <div id="searchResultsTableContainer" class="invisible">
<div id="searchResultsTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv"> <div id="searchResultsTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
<table class="dynamicTable unselectable" style="position:relative;"> <table class="dynamicTable unselectable" style="position:relative;">
<thead> <thead>