WebUI: Filter list improvements

A couple of tweaks to make them a little bit better:
1. Make highlighting functions more consistent (this also fixes minuscule bug when no filter item in tracker list is highlighted due to a type mismatch)
2. Use [event delegation](https://javascript.info/event-delegation) to handle filter toggling & item selection
3. Other minor improvements (everything should work like it was previously)

PR #21191.
This commit is contained in:
skomerko 2024-08-25 08:23:35 +02:00 committed by GitHub
parent 39dd415d43
commit 5b7c9d5725
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 163 additions and 150 deletions

View file

@ -545,7 +545,6 @@ ul.filterList .selectedFilter img {
filter: var(--color-icon-hover);
}
ul.filterList a,
ul.filterList span.link {
align-items: center;
color: inherit;

View file

@ -135,7 +135,7 @@ const CATEGORIES_UNCATEGORIZED = 2;
const category_list = new Map();
let selected_category = Number(LocalPreferences.get("selected_category", CATEGORIES_ALL));
let selectedCategory = Number(LocalPreferences.get("selected_category", CATEGORIES_ALL));
let setCategoryFilter = function() {};
/* Tags filter */
@ -154,12 +154,12 @@ const TRACKERS_TRACKERLESS = 2;
/** @type Map<number, {host: string, trackerTorrentMap: Map<string, string[]>}> **/
const trackerList = new Map();
let selectedTracker = LocalPreferences.get("selected_tracker", TRACKERS_ALL);
let selectedTracker = Number(LocalPreferences.get("selected_tracker", TRACKERS_ALL));
let setTrackerFilter = function() {};
/* All filters */
let selected_filter = LocalPreferences.get("selected_filter", "all");
let setFilter = function() {};
let selectedStatus = LocalPreferences.get("selected_filter", "all");
let setStatusFilter = function() {};
let toggleFilterDisplay = function() {};
window.addEventListener("DOMContentLoaded", () => {
@ -240,62 +240,40 @@ window.addEventListener("DOMContentLoaded", () => {
buildLogTab();
MochaUI.initializeTabs("mainWindowTabsList");
setStatusFilter = function(name) {
LocalPreferences.set("selected_filter", name);
selectedStatus = name;
highlightSelectedStatus();
updateMainData();
};
setCategoryFilter = function(hash) {
selected_category = hash;
LocalPreferences.set("selected_category", selected_category);
LocalPreferences.set("selected_category", hash);
selectedCategory = Number(hash);
highlightSelectedCategory();
if (typeof torrentsTable.tableBody !== "undefined")
updateMainData();
};
setTagFilter = function(hash) {
selectedTag = hash;
LocalPreferences.set("selected_tag", selectedTag);
LocalPreferences.set("selected_tag", hash);
selectedTag = Number(hash);
highlightSelectedTag();
if (torrentsTable.tableBody !== undefined)
updateMainData();
};
setTrackerFilter = function(hash) {
selectedTracker = hash.toString();
LocalPreferences.set("selected_tracker", selectedTracker);
LocalPreferences.set("selected_tracker", hash);
selectedTracker = Number(hash);
highlightSelectedTracker();
if (torrentsTable.tableBody !== undefined)
updateMainData();
};
setFilter = function(f) {
// Visually Select the right filter
$("all_filter").removeClass("selectedFilter");
$("downloading_filter").removeClass("selectedFilter");
$("seeding_filter").removeClass("selectedFilter");
$("completed_filter").removeClass("selectedFilter");
$("stopped_filter").removeClass("selectedFilter");
$("running_filter").removeClass("selectedFilter");
$("active_filter").removeClass("selectedFilter");
$("inactive_filter").removeClass("selectedFilter");
$("stalled_filter").removeClass("selectedFilter");
$("stalled_uploading_filter").removeClass("selectedFilter");
$("stalled_downloading_filter").removeClass("selectedFilter");
$("checking_filter").removeClass("selectedFilter");
$("moving_filter").removeClass("selectedFilter");
$("errored_filter").removeClass("selectedFilter");
$(f + "_filter").addClass("selectedFilter");
selected_filter = f;
LocalPreferences.set("selected_filter", f);
// Reload torrents
if (typeof torrentsTable.tableBody !== "undefined")
updateMainData();
};
toggleFilterDisplay = function(filter) {
const element = filter + "FilterList";
LocalPreferences.set("filter_" + filter + "_collapsed", !$(element).hasClass("invisible"));
$(element).toggleClass("invisible");
const parent = $(element).getParent(".filterWrapper");
const toggleIcon = $(parent).getChildren(".filterTitle img");
if (toggleIcon)
toggleIcon[0].toggleClass("rotate");
toggleFilterDisplay = function(filterListID) {
const filterList = document.getElementById(filterListID);
const filterTitle = filterList.previousElementSibling;
const toggleIcon = filterTitle.firstElementChild;
toggleIcon.classList.toggle("rotate");
LocalPreferences.set(`filter_${filterListID.replace("FilterList", "")}_collapsed`, filterList.classList.toggle("invisible").toString());
};
new MochaUI.Panel({
@ -311,7 +289,7 @@ window.addEventListener("DOMContentLoaded", () => {
loadMethod: "xhr",
contentURL: "views/filters.html",
onContentLoaded: function() {
setFilter(selected_filter);
highlightSelectedStatus();
},
column: "filtersColumn",
height: 300
@ -467,12 +445,21 @@ window.addEventListener("DOMContentLoaded", () => {
updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
};
const highlightSelectedStatus = function() {
const statusFilter = document.getElementById("statusFilterList");
const filterID = `${selectedStatus}_filter`;
for (const status of statusFilter.children)
status.classList.toggle("selectedFilter", (status.id === filterID));
};
const updateCategoryList = function() {
const categoryList = $("categoryFilterList");
if (!categoryList)
return;
categoryList.getChildren().each(c => c.destroy());
const categoryItemTemplate = document.getElementById("categoryFilterItem");
const create_link = function(hash, text, count) {
let display_name = text;
let margin_left = 0;
@ -482,26 +469,15 @@ window.addEventListener("DOMContentLoaded", () => {
margin_left = (category_path.length - 1) * 20;
}
const span = document.createElement("span");
span.classList.add("link");
span.href = "#";
const categoryFilterItem = categoryItemTemplate.content.cloneNode(true).firstElementChild;
categoryFilterItem.id = hash;
categoryFilterItem.classList.toggle("selectedFilter", hash === selectedCategory);
const span = categoryFilterItem.firstElementChild;
span.style.marginLeft = `${margin_left}px`;
span.textContent = `${display_name} (${count})`;
span.addEventListener("click", (event) => {
event.preventDefault();
setCategoryFilter(hash);
});
span.lastChild.textContent = `${display_name} (${count})`;
const img = document.createElement("img");
img.src = "images/view-categories.svg";
span.prepend(img);
const listItem = document.createElement("li");
listItem.id = hash;
listItem.appendChild(span);
window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(listItem);
return listItem;
return categoryFilterItem;
};
const all = torrentsTable.getRowIds().length;
@ -550,20 +526,16 @@ window.addEventListener("DOMContentLoaded", () => {
categoryList.appendChild(create_link(categoryHash, categoryName, categoryCount));
}
highlightSelectedCategory();
window.qBittorrent.Filters.categoriesFilterContextMenu.searchAndAddTargets();
};
const highlightSelectedCategory = function() {
const categoryList = $("categoryFilterList");
const categoryList = document.getElementById("categoryFilterList");
if (!categoryList)
return;
const children = categoryList.childNodes;
for (let i = 0; i < children.length; ++i) {
if (Number(children[i].id) === selected_category)
children[i].className = "selectedFilter";
else
children[i].className = "";
}
for (const category of categoryList.children)
category.classList.toggle("selectedFilter", (Number(category.id) === selectedCategory));
};
const updateTagList = function() {
@ -573,26 +545,17 @@ window.addEventListener("DOMContentLoaded", () => {
tagFilterList.getChildren().each(c => c.destroy());
const tagItemTemplate = document.getElementById("tagFilterItem");
const createLink = function(hash, text, count) {
const span = document.createElement("span");
span.classList.add("link");
span.href = "#";
span.textContent = `${text} (${count})`;
span.addEventListener("click", (event) => {
event.preventDefault();
setTagFilter(hash);
});
const tagFilterItem = tagItemTemplate.content.cloneNode(true).firstElementChild;
tagFilterItem.id = hash;
tagFilterItem.classList.toggle("selectedFilter", hash === selectedTag);
const img = document.createElement("img");
img.src = "images/tags.svg";
span.prepend(img);
const span = tagFilterItem.firstElementChild;
span.lastChild.textContent = `${text} (${count})`;
const listItem = document.createElement("li");
listItem.id = hash;
listItem.appendChild(span);
window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(listItem);
return listItem;
return tagFilterItem;
};
const torrentsCount = torrentsTable.getRowIds().length;
@ -615,17 +578,16 @@ window.addEventListener("DOMContentLoaded", () => {
for (const { tagName, tagHash, tagSize } of sortedTags)
tagFilterList.appendChild(createLink(tagHash, tagName, tagSize));
highlightSelectedTag();
window.qBittorrent.Filters.tagsFilterContextMenu.searchAndAddTargets();
};
const highlightSelectedTag = function() {
const tagFilterList = $("tagFilterList");
const tagFilterList = document.getElementById("tagFilterList");
if (!tagFilterList)
return;
const children = tagFilterList.childNodes;
for (let i = 0; i < children.length; ++i)
children[i].className = (Number(children[i].id) === selectedTag) ? "selectedFilter" : "";
for (const tag of tagFilterList.children)
tag.classList.toggle("selectedFilter", (Number(tag.id) === selectedTag));
};
// getHost emulate the GUI version `QString getHost(const QString &url)`
@ -659,26 +621,17 @@ window.addEventListener("DOMContentLoaded", () => {
trackerFilterList.getChildren().each(c => c.destroy());
const trackerItemTemplate = document.getElementById("trackerFilterItem");
const createLink = function(hash, text, count) {
const span = document.createElement("span");
span.classList.add("link");
span.href = "#";
span.textContent = text.replace("%1", count);
span.addEventListener("click", (event) => {
event.preventDefault();
setTrackerFilter(hash);
});
const trackerFilterItem = trackerItemTemplate.content.cloneNode(true).firstElementChild;
trackerFilterItem.id = hash;
trackerFilterItem.classList.toggle("selectedFilter", hash === selectedTracker);
const img = document.createElement("img");
img.src = "images/trackers.svg";
span.prepend(img);
const span = trackerFilterItem.firstElementChild;
span.lastChild.textContent = text.replace("%1", count);
const listItem = document.createElement("li");
listItem.id = hash;
listItem.appendChild(span);
window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(listItem);
return listItem;
return trackerFilterItem;
};
const torrentsCount = torrentsTable.getRowIds().length;
@ -709,17 +662,16 @@ window.addEventListener("DOMContentLoaded", () => {
for (const { trackerHost, trackerHash, trackerCount } of sortedList)
trackerFilterList.appendChild(createLink(trackerHash, (trackerHost + " (%1)"), trackerCount));
highlightSelectedTracker();
window.qBittorrent.Filters.trackersFilterContextMenu.searchAndAddTargets();
};
const highlightSelectedTracker = function() {
const trackerFilterList = $("trackerFilterList");
const trackerFilterList = document.getElementById("trackerFilterList");
if (!trackerFilterList)
return;
const children = trackerFilterList.childNodes;
for (const child of children)
child.className = (child.id === selectedTracker) ? "selectedFilter" : "";
for (const tracker of trackerFilterList.children)
tracker.classList.toggle("selectedFilter", (Number(tracker.id) === selectedTracker));
};
const setupCopyEventHandler = (function() {

View file

@ -179,6 +179,10 @@ window.qBittorrent.ContextMenu ??= (() => {
this.setupEventListeners(t);
},
searchAndAddTargets: function() {
document.querySelectorAll(this.options.targets).forEach((target) => { this.addTarget(target); });
},
triggerMenu: function(e, el) {
if (this.options.disabled)
return;

View file

@ -1456,8 +1456,7 @@ window.qBittorrent.DynamicTable ??= (() => {
}
}
const trackerHashInt = Number.parseInt(trackerHash, 10);
switch (trackerHashInt) {
switch (trackerHash) {
case TRACKERS_ALL:
break; // do nothing
case TRACKERS_TRACKERLESS:
@ -1465,7 +1464,7 @@ window.qBittorrent.DynamicTable ??= (() => {
return false;
break;
default: {
const tracker = trackerList.get(trackerHashInt);
const tracker = trackerList.get(trackerHash);
if (tracker) {
let found = false;
for (const torrents of tracker.trackerTorrentMap.values()) {
@ -1537,7 +1536,7 @@ window.qBittorrent.DynamicTable ??= (() => {
const rows = this.rows.getValues();
for (let i = 0; i < rows.length; ++i) {
if (this.applyFilter(rows[i], selected_filter, selected_category, selectedTag, selectedTracker, filterTerms)) {
if (this.applyFilter(rows[i], selectedStatus, selectedCategory, selectedTag, selectedTracker, filterTerms)) {
filteredRows.push(rows[i]);
filteredRows[rows[i].rowId] = rows[i];
}

View file

@ -1,46 +1,68 @@
<div class="filterWrapper">
<span class="filterTitle" onclick="toggleFilterDisplay('status');">
<span class="filterTitle">
<img src="images/go-down.svg" alt="QBT_TR(Collapse/expand)QBT_TR[CONTEXT=TransferListFiltersWidget]">QBT_TR(Status)QBT_TR[CONTEXT=TransferListFiltersWidget]
</span>
<ul class="filterList" id="statusFilterList">
<li id="all_filter"><a href="#" onclick="setFilter('all');return false;"><img src="images/filter-all.svg" alt="All">QBT_TR(All (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="downloading_filter"><a href="#" onclick="setFilter('downloading');return false;"><img src="images/downloading.svg" alt="Downloading">QBT_TR(Downloading (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="seeding_filter"><a href="#" onclick="setFilter('seeding');return false;"><img src="images/upload.svg" alt="Seeding">QBT_TR(Seeding (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="completed_filter"><a href="#" onclick="setFilter('completed');return false;"><img src="images/checked-completed.svg" alt="Completed">QBT_TR(Completed (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="running_filter"><a href="#" onclick="setFilter('running');return false;"><img src="images/torrent-start.svg" alt="Running">QBT_TR(Running (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="stopped_filter"><a href="#" onclick="setFilter('stopped');return false;"><img src="images/stopped.svg" alt="Stopped">QBT_TR(Stopped (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="active_filter"><a href="#" onclick="setFilter('active');return false;"><img src="images/filter-active.svg" alt="Active">QBT_TR(Active (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="inactive_filter"><a href="#" onclick="setFilter('inactive');return false;"><img src="images/filter-inactive.svg" alt="Inactive">QBT_TR(Inactive (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="stalled_filter"><a href="#" onclick="setFilter('stalled');return false;"><img src="images/filter-stalled.svg" alt="Stalled">QBT_TR(Stalled (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="stalled_uploading_filter"><a href="#" onclick="setFilter('stalled_uploading');return false;"><img src="images/stalledUP.svg" alt="Stalled Uploading">QBT_TR(Stalled Uploading (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="stalled_downloading_filter"><a href="#" onclick="setFilter('stalled_downloading');return false;"><img src="images/stalledDL.svg" alt="Stalled Downloading">QBT_TR(Stalled Downloading (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="checking_filter"><a href="#" onclick="setFilter('checking'); return false;"><img src="images/force-recheck.svg" alt="Checking">QBT_TR(Checking (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="moving_filter"><a href="#" onclick="setFilter('moving'); return false;"><img src="images/set-location.svg" alt="Moving">QBT_TR(Moving (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="errored_filter"><a href="#" onclick="setFilter('errored');return false;"><img src="images/error.svg" alt="Errored">QBT_TR(Errored (0))QBT_TR[CONTEXT=StatusFilterWidget]</a></li>
<li id="all_filter"><span class="link"><img src="images/filter-all.svg" alt="All">QBT_TR(All (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="downloading_filter"><span class="link"><img src="images/downloading.svg" alt="Downloading">QBT_TR(Downloading (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="seeding_filter"><span class="link"><img src="images/upload.svg" alt="Seeding">QBT_TR(Seeding (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="completed_filter"><span class="link"><img src="images/checked-completed.svg" alt="Completed">QBT_TR(Completed (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="running_filter"><span class="link"><img src="images/torrent-start.svg" alt="Running">QBT_TR(Running (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="stopped_filter"><span class="link"><img src="images/stopped.svg" alt="Stopped">QBT_TR(Stopped (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="active_filter"><span class="link"><img src="images/filter-active.svg" alt="Active">QBT_TR(Active (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="inactive_filter"><span class="link"><img src="images/filter-inactive.svg" alt="Inactive">QBT_TR(Inactive (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="stalled_filter"><span class="link"><img src="images/filter-stalled.svg" alt="Stalled">QBT_TR(Stalled (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="stalled_uploading_filter"><span class="link"><img src="images/stalledUP.svg" alt="Stalled Uploading">QBT_TR(Stalled Uploading (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="stalled_downloading_filter"><span class="link"><img src="images/stalledDL.svg" alt="Stalled Downloading">QBT_TR(Stalled Downloading (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="checking_filter"><span class="link"><img src="images/force-recheck.svg" alt="Checking">QBT_TR(Checking (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="moving_filter"><span class="link"><img src="images/set-location.svg" alt="Moving">QBT_TR(Moving (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
<li id="errored_filter"><span class="link"><img src="images/error.svg" alt="Errored">QBT_TR(Errored (0))QBT_TR[CONTEXT=StatusFilterWidget]</span></li>
</ul>
</div>
<div class="filterWrapper">
<span class="filterTitle" onclick="toggleFilterDisplay('category');">
<span class="filterTitle">
<img src="images/go-down.svg" alt="QBT_TR(Collapse/expand)QBT_TR[CONTEXT=TransferListFiltersWidget]">QBT_TR(Categories)QBT_TR[CONTEXT=TransferListFiltersWidget]
</span>
<ul class="filterList" id="categoryFilterList">
</ul>
</div>
<div class="filterWrapper">
<span class="filterTitle" onclick="toggleFilterDisplay('tag');">
<span class="filterTitle">
<img src="images/go-down.svg" alt="QBT_TR(Collapse/expand)QBT_TR[CONTEXT=TransferListFiltersWidget]">QBT_TR(Tags)QBT_TR[CONTEXT=TransferListFiltersWidget]
</span>
<ul class="filterList" id="tagFilterList">
</ul>
</div>
<div class="filterWrapper">
<span class="filterTitle" onclick="toggleFilterDisplay('tracker');">
<span class="filterTitle">
<img src="images/go-down.svg" alt="QBT_TR(Collapse/expand)QBT_TR[CONTEXT=TransferListFiltersWidget]">QBT_TR(Trackers)QBT_TR[CONTEXT=TransferListFiltersWidget]
</span>
<ul class="filterList" id="trackerFilterList">
</ul>
</div>
<template id="categoryFilterItem">
<li class="categoriesFilterContextMenuTarget">
<span class="link">
<img src="images/view-categories.svg" alt="">
</span>
</li>
</template>
<template id="tagFilterItem">
<li class="tagsFilterContextMenuTarget">
<span class="link">
<img src="images/tags.svg" alt="">
</span>
</li>
</template>
<template id="trackerFilterItem">
<li class="trackersFilterContextMenuTarget">
<span class="link">
<img src="images/trackers.svg" alt="">
</span>
</li>
</template>
<script>
"use strict";
@ -88,7 +110,7 @@
y: 2
},
onShow: function() {
this.options.element.firstChild.click();
this.options.element.click();
}
});
@ -120,7 +142,7 @@
y: 2
},
onShow: function() {
this.options.element.firstChild.click();
this.options.element.click();
}
});
@ -143,21 +165,58 @@
y: 2
},
onShow: function() {
this.options.element.firstChild.click();
this.options.element.click();
}
});
if (LocalPreferences.get("filter_status_collapsed") === "true")
toggleFilterDisplay("status");
toggleFilterDisplay("statusFilterList");
if (LocalPreferences.get("filter_category_collapsed") === "true")
toggleFilterDisplay("category");
toggleFilterDisplay("categoryFilterList");
if (LocalPreferences.get("filter_tag_collapsed") === "true")
toggleFilterDisplay("tag");
toggleFilterDisplay("tagFilterList");
if (LocalPreferences.get("filter_tracker_collapsed") === "true")
toggleFilterDisplay("tracker");
toggleFilterDisplay("trackerFilterList");
document.getElementById("Filters_pad").addEventListener("click", (event) => {
const filterItem = event.target.closest("li");
if (!filterItem)
return;
event.stopImmediatePropagation();
const { id: filterItemID } = filterItem;
const { id: filterListID } = filterItem.closest("ul[id]");
switch (filterListID) {
case "statusFilterList":
setStatusFilter(filterItemID.replace("_filter", ""));
break;
case "categoryFilterList":
setCategoryFilter(filterItemID);
break;
case "tagFilterList":
setTagFilter(filterItemID);
break;
case "trackerFilterList":
setTrackerFilter(filterItemID);
break;
default:
console.error(`Unexpected filterListID: ${filterListID}`);
break;
}
});
document.getElementById("Filters_pad").addEventListener("click", (event) => {
const filterTitle = event.target.closest(".filterTitle");
if (!filterTitle)
return;
const filterList = filterTitle.nextElementSibling.id;
toggleFilterDisplay(filterList);
});
return exports();
})();