WebUI: Use event delegation to handle common table events

Event delegation is now used to handle basic table events.
2 minor fixes were added to match GUI behavior:
* Clicking on the table body deselects everything
* Table rows are now scrolled into view when using up/down arrows

PR #21829.
This commit is contained in:
skomerko 2024-11-18 19:12:26 +01:00 committed by GitHub
parent ea35aa45d6
commit c9c85eeb95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 142 additions and 173 deletions

View file

@ -22,10 +22,6 @@
color: var(--color-text-white); color: var(--color-text-white);
} }
#transferList tr:hover {
cursor: pointer;
}
#transferList img.stateIcon { #transferList img.stateIcon {
height: 1.3em; height: 1.3em;
margin-bottom: -1px; margin-bottom: -1px;
@ -37,10 +33,6 @@
display: flex !important; display: flex !important;
} }
tr.dynamicTableHeader {
cursor: pointer;
}
.dynamicTable { .dynamicTable {
border-spacing: 0; border-spacing: 0;
padding: 0; padding: 0;
@ -54,6 +46,10 @@ tr.dynamicTableHeader {
white-space: nowrap; white-space: nowrap;
} }
.dynamicTable tr:hover {
cursor: pointer;
}
.dynamicTable tr:is(:hover, .selected) img:not(.flags) { .dynamicTable tr:is(:hover, .selected) img:not(.flags) {
filter: var(--color-icon-hover); filter: var(--color-icon-hover);
} }

View file

@ -861,10 +861,6 @@ td.statusBarSeparator {
color: var(--color-text-green); color: var(--color-text-green);
} }
.searchPluginsTableRow {
cursor: pointer;
}
#torrentFilesTableDiv .dynamicTable tr.nonAlt:hover { #torrentFilesTableDiv .dynamicTable tr.nonAlt:hover {
background-color: var(--color-background-hover); background-color: var(--color-background-hover);
color: var(--color-text-white); color: var(--color-text-white);

View file

@ -73,6 +73,7 @@ window.qBittorrent.DynamicTable ??= (() => {
setup: function(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu) { setup: function(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu) {
this.dynamicTableDivId = dynamicTableDivId; this.dynamicTableDivId = dynamicTableDivId;
this.dynamicTableFixedHeaderDivId = dynamicTableFixedHeaderDivId; this.dynamicTableFixedHeaderDivId = dynamicTableFixedHeaderDivId;
this.dynamicTableDiv = document.getElementById(dynamicTableDivId);
this.fixedTableHeader = $(dynamicTableFixedHeaderDivId).getElements("tr")[0]; this.fixedTableHeader = $(dynamicTableFixedHeaderDivId).getElements("tr")[0];
this.hiddenTableHeader = $(dynamicTableDivId).getElements("tr")[0]; this.hiddenTableHeader = $(dynamicTableDivId).getElements("tr")[0];
this.tableBody = $(dynamicTableDivId).getElements("tbody")[0]; this.tableBody = $(dynamicTableDivId).getElements("tbody")[0];
@ -93,12 +94,81 @@ window.qBittorrent.DynamicTable ??= (() => {
}, },
setupCommonEvents: function() { setupCommonEvents: function() {
const tableDiv = $(this.dynamicTableDivId);
const tableFixedHeaderDiv = $(this.dynamicTableFixedHeaderDivId); const tableFixedHeaderDiv = $(this.dynamicTableFixedHeaderDivId);
const tableElement = tableFixedHeaderDiv.querySelector("table"); const tableElement = tableFixedHeaderDiv.querySelector("table");
tableDiv.addEventListener("scroll", () => { this.dynamicTableDiv.addEventListener("scroll", function() {
tableElement.style.left = `${-tableDiv.scrollLeft}px`; tableElement.style.left = `${-this.scrollLeft}px`;
});
this.dynamicTableDiv.addEventListener("click", (e) => {
const tr = e.target.closest("tr");
if (!tr) {
// clicking on the table body deselects all rows
this.deselectAll();
this.setRowClass();
return;
}
if (e.ctrlKey || e.metaKey) {
// CTRL/CMD ⌘ key was pressed
if (this.isRowSelected(tr.rowId))
this.deselectRow(tr.rowId);
else
this.selectRow(tr.rowId);
}
else if (e.shiftKey && (this.selectedRows.length === 1)) {
// Shift key was pressed
this.selectRows(this.getSelectedRowId(), tr.rowId);
}
else {
// Simple selection
this.deselectAll();
this.selectRow(tr.rowId);
}
});
this.dynamicTableDiv.addEventListener("contextmenu", (e) => {
const tr = e.target.closest("tr");
if (!tr)
return;
if (!this.isRowSelected(tr.rowId)) {
this.deselectAll();
this.selectRow(tr.rowId);
}
}, true);
this.dynamicTableDiv.addEventListener("touchstart", (e) => {
const tr = e.target.closest("tr");
if (!tr)
return;
if (!this.isRowSelected(tr.rowId)) {
this.deselectAll();
this.selectRow(tr.rowId);
}
}, { passive: true });
this.dynamicTableDiv.addEventListener("keydown", (e) => {
const tr = e.target.closest("tr");
if (!tr)
return;
switch (e.key) {
case "ArrowUp": {
e.preventDefault();
this.selectPreviousRow();
this.dynamicTableDiv.querySelector(".selected").scrollIntoView({ block: "nearest" });
break;
}
case "ArrowDown": {
e.preventDefault();
this.selectNextRow();
this.dynamicTableDiv.querySelector(".selected").scrollIntoView({ block: "nearest" });
break;
}
}
}); });
}, },
@ -801,54 +871,6 @@ window.qBittorrent.DynamicTable ??= (() => {
tr.setAttribute("data-row-id", rowId); tr.setAttribute("data-row-id", rowId);
tr["rowId"] = rowId; tr["rowId"] = rowId;
tr._this = this;
tr.addEventListener("contextmenu", function(e) {
if (!this._this.isRowSelected(this.rowId)) {
this._this.deselectAll();
this._this.selectRow(this.rowId);
}
return true;
});
tr.addEventListener("click", function(e) {
e.preventDefault();
if (e.ctrlKey || e.metaKey) {
// CTRL/CMD ⌘ key was pressed
if (this._this.isRowSelected(this.rowId))
this._this.deselectRow(this.rowId);
else
this._this.selectRow(this.rowId);
}
else if (e.shiftKey && (this._this.selectedRows.length === 1)) {
// Shift key was pressed
this._this.selectRows(this._this.getSelectedRowId(), this.rowId);
}
else {
// Simple selection
this._this.deselectAll();
this._this.selectRow(this.rowId);
}
return false;
});
tr.addEventListener("touchstart", function(e) {
if (!this._this.isRowSelected(this.rowId)) {
this._this.deselectAll();
this._this.selectRow(this.rowId);
}
}, { passive: true });
tr.addEventListener("keydown", function(event) {
switch (event.key) {
case "ArrowUp":
this._this.selectPreviousRow();
return false;
case "ArrowDown":
this._this.selectNextRow();
return false;
}
});
this.setupTr(tr);
for (let k = 0; k < this.columns.length; ++k) { for (let k = 0; k < this.columns.length; ++k) {
const td = new Element("td"); const td = new Element("td");
if ((this.columns[k].visible === "0") || this.columns[k].force_hide) if ((this.columns[k].visible === "0") || this.columns[k].force_hide)
@ -867,8 +889,7 @@ window.qBittorrent.DynamicTable ??= (() => {
} }
// Update context menu // Update context menu
if (this.contextMenu) this.contextMenu?.addTarget(tr);
this.contextMenu.addTarget(tr);
this.updateRow(tr, true); this.updateRow(tr, true);
} }
@ -880,8 +901,6 @@ window.qBittorrent.DynamicTable ??= (() => {
trs.pop().destroy(); trs.pop().destroy();
}, },
setupTr: (tr) => {},
updateRow: function(tr, fullUpdate) { updateRow: function(tr, fullUpdate) {
const row = this.rows.get(tr.rowId); const row = this.rows.get(tr.rowId);
const data = row[fullUpdate ? "full_data" : "data"]; const data = row[fullUpdate ? "full_data" : "data"];
@ -1652,14 +1671,16 @@ window.qBittorrent.DynamicTable ??= (() => {
return filteredRows; return filteredRows;
}, },
setupTr: function(tr) { setupCommonEvents: function() {
tr.addEventListener("dblclick", function(e) { this.parent();
e.preventDefault(); this.dynamicTableDiv.addEventListener("dblclick", (e) => {
e.stopPropagation(); const tr = e.target.closest("tr");
if (!tr)
return;
this._this.deselectAll(); this.deselectAll();
this._this.selectRow(this.rowId); this.selectRow(tr.rowId);
const row = this._this.rows.get(this.rowId); const row = this.getRow(tr.rowId);
const state = row["full_data"].state; const state = row["full_data"].state;
const prefKey = const prefKey =
@ -1679,9 +1700,7 @@ window.qBittorrent.DynamicTable ??= (() => {
startFN(); startFN();
else else
stopFN(); stopFN();
return true;
}); });
tr.addClass("torrentsTableContextMenuTarget");
}, },
getCurrentTorrentID: function() { getCurrentTorrentID: function() {
@ -1920,10 +1939,6 @@ window.qBittorrent.DynamicTable ??= (() => {
return filteredRows; return filteredRows;
}, },
setupTr: (tr) => {
tr.addClass("searchTableRow");
}
}); });
const SearchPluginsTable = new Class({ const SearchPluginsTable = new Class({
@ -1955,10 +1970,6 @@ window.qBittorrent.DynamicTable ??= (() => {
} }
}; };
}, },
setupTr: (tr) => {
tr.addClass("searchPluginsTableRow");
}
}); });
const TorrentTrackersTable = new Class({ const TorrentTrackersTable = new Class({
@ -2416,18 +2427,7 @@ window.qBittorrent.DynamicTable ??= (() => {
row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100))); row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
}, },
setupTr: function(tr) { setupCommonEvents: () => {}
tr.addEventListener("keydown", function(event) {
switch (event.key) {
case "ArrowLeft":
qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
return false;
case "ArrowRight":
qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
return false;
}
});
}
}); });
const TorrentFilesTable = new Class({ const TorrentFilesTable = new Class({
@ -2754,15 +2754,22 @@ window.qBittorrent.DynamicTable ??= (() => {
row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100))); row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
}, },
setupTr: function(tr) { setupCommonEvents: function() {
tr.addEventListener("keydown", function(event) { this.parent();
switch (event.key) { this.dynamicTableDiv.addEventListener("keydown", (e) => {
const tr = e.target.closest("tr");
if (!tr)
return;
switch (e.key) {
case "ArrowLeft": case "ArrowLeft":
qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId()); e.preventDefault();
return false; window.qBittorrent.PropFiles.collapseFolder(this.getSelectedRowId());
break;
case "ArrowRight": case "ArrowRight":
qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId()); e.preventDefault();
return false; window.qBittorrent.PropFiles.expandFolder(this.getSelectedRowId());
break;
} }
}); });
} }
@ -2805,12 +2812,14 @@ window.qBittorrent.DynamicTable ??= (() => {
} }
window.qBittorrent.Rss.showRssFeed(path); window.qBittorrent.Rss.showRssFeed(path);
}, },
setupTr: function(tr) { setupCommonEvents: function() {
tr.addEventListener("dblclick", function(e) { this.parent();
if (this.rowId !== 0) { this.dynamicTableDiv.addEventListener("dblclick", (e) => {
window.qBittorrent.Rss.moveItem(this._this.rows.get(this.rowId).full_data.dataPath); const tr = e.target.closest("tr");
return true; if (!tr || (tr.rowId === "0"))
} return;
window.qBittorrent.Rss.moveItem(this.getRow(tr.rowId).full_data.dataPath);
}); });
}, },
updateRow: function(tr, fullUpdate) { updateRow: function(tr, fullUpdate) {
@ -2938,12 +2947,16 @@ window.qBittorrent.DynamicTable ??= (() => {
} }
window.qBittorrent.Rss.showDetails(feedUid, articleId); window.qBittorrent.Rss.showDetails(feedUid, articleId);
}, },
setupTr: function(tr) {
tr.addEventListener("dblclick", function(e) { setupCommonEvents: function() {
showDownloadPage([this._this.rows.get(this.rowId).full_data.torrentURL]); this.parent();
return true; this.dynamicTableDiv.addEventListener("dblclick", (e) => {
const tr = e.target.closest("tr");
if (!tr)
return;
showDownloadPage([this.getRow(tr.rowId).full_data.torrentURL]);
}); });
tr.addClass("torrentsTableContextMenuTarget");
}, },
updateRow: function(tr, fullUpdate) { updateRow: function(tr, fullUpdate) {
const row = this.rows.get(tr.rowId); const row = this.rows.get(tr.rowId);
@ -3033,10 +3046,15 @@ window.qBittorrent.DynamicTable ??= (() => {
getFilteredAndSortedRows: function() { getFilteredAndSortedRows: function() {
return [...this.getRowValues()]; return [...this.getRowValues()];
}, },
setupTr: function(tr) {
tr.addEventListener("dblclick", function(e) { setupCommonEvents: function() {
window.qBittorrent.RssDownloader.renameRule(this._this.rows.get(this.rowId).full_data.name); this.parent();
return true; this.dynamicTableDiv.addEventListener("dblclick", (e) => {
const tr = e.target.closest("tr");
if (!tr)
return;
window.qBittorrent.RssDownloader.renameRule(this.getRow(tr.rowId).full_data.name);
}); });
}, },
newColumn: function(name, style, caption, defaultWidth, defaultVisible) { newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
@ -3314,12 +3332,6 @@ window.qBittorrent.DynamicTable ??= (() => {
return filteredRows; return filteredRows;
}, },
setupCommonEvents: () => {},
setupTr: (tr) => {
tr.addClass("logTableRow");
}
}); });
const LogPeerTable = new Class({ const LogPeerTable = new Class({

View file

@ -109,7 +109,7 @@ window.qBittorrent.Search ??= (() => {
// load "Search in" preference from local storage // load "Search in" preference from local storage
$("searchInTorrentName").value = (LocalPreferences.get("search_in_filter") === "names") ? "names" : "everywhere"; $("searchInTorrentName").value = (LocalPreferences.get("search_in_filter") === "names") ? "names" : "everywhere";
const searchResultsTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ const searchResultsTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
targets: ".searchTableRow", targets: "#searchResultsTableDiv tr",
menu: "searchResultsTableMenu", menu: "searchResultsTableMenu",
actions: { actions: {
Download: downloadSearchTorrent, Download: downloadSearchTorrent,
@ -124,6 +124,8 @@ window.qBittorrent.Search ??= (() => {
searchResultsTable.setup("searchResultsTableDiv", "searchResultsTableFixedHeaderDiv", searchResultsTableContextMenu); searchResultsTable.setup("searchResultsTableDiv", "searchResultsTableFixedHeaderDiv", searchResultsTableContextMenu);
getPlugins(); getPlugins();
searchResultsTable.dynamicTableDiv.addEventListener("dblclick", (e) => { downloadSearchTorrent(); });
// listen for changes to searchInNameFilter // listen for changes to searchInNameFilter
let searchInNameFilterTimer = -1; let searchInNameFilterTimer = -1;
$("searchInNameFilter").addEventListener("input", () => { $("searchInNameFilter").addEventListener("input", () => {
@ -373,8 +375,6 @@ window.qBittorrent.Search ??= (() => {
$("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length; $("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length;
$("numSearchResultsTotal").textContent = searchResultsTable.getRowSize(); $("numSearchResultsTotal").textContent = searchResultsTable.getRowSize();
setupSearchTableEvents(true);
}; };
const getStatusIconElement = (text, image) => { const getStatusIconElement = (text, image) => {
@ -769,20 +769,6 @@ window.qBittorrent.Search ??= (() => {
$("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length; $("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length;
}; };
const setupSearchTableEvents = (enable) => {
const clickHandler = (e) => { downloadSearchTorrent(); };
if (enable) {
$$(".searchTableRow").each((target) => {
target.addEventListener("dblclick", clickHandler);
});
}
else {
$$(".searchTableRow").each((target) => {
target.removeEventListener("dblclick", clickHandler);
});
}
};
const loadSearchResultsData = function(searchId) { const loadSearchResultsData = function(searchId) {
const state = searchState.get(searchId); const state = searchState.get(searchId);
@ -820,8 +806,6 @@ window.qBittorrent.Search ??= (() => {
} }
if (response) { if (response) {
setupSearchTableEvents(false);
const state = searchState.get(searchId); const state = searchState.get(searchId);
const newRows = []; const newRows = [];
@ -859,8 +843,6 @@ window.qBittorrent.Search ??= (() => {
searchResultsTable.updateTable(); searchResultsTable.updateTable();
} }
setupSearchTableEvents(true);
if ((response.status === "Stopped") && (state.rowId >= response.total)) { if ((response.status === "Stopped") && (state.rowId >= response.total)) {
resetSearchState(searchId); resetSearchState(searchId);
updateStatusIconElement(searchId, "QBT_TR(Search has finished)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-complete.svg"); updateStatusIconElement(searchId, "QBT_TR(Search has finished)QBT_TR[CONTEXT=SearchJobWidget]", "images/task-complete.svg");

View file

@ -206,7 +206,7 @@
}); });
const logTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ const logTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
targets: ".logTableRow", targets: ":is(#logMessageView, #logPeerView) tr",
menu: "logTableMenu", menu: "logTableMenu",
actions: { actions: {
Clear: () => { Clear: () => {

View file

@ -218,7 +218,7 @@
$("rssFetchingDisabled").removeClass("invisible"); $("rssFetchingDisabled").removeClass("invisible");
const rssFeedContextMenu = new window.qBittorrent.ContextMenu.RssFeedContextMenu({ const rssFeedContextMenu = new window.qBittorrent.ContextMenu.RssFeedContextMenu({
targets: ".rssFeedContextMenuTarget", targets: "#rssFeedTableDiv tr",
menu: "rssFeedMenu", menu: "rssFeedMenu",
actions: { actions: {
update: (el) => { update: (el) => {
@ -288,7 +288,7 @@
rssFeedTable.setup("rssFeedTableDiv", "rssFeedFixedHeaderDiv", rssFeedContextMenu); rssFeedTable.setup("rssFeedTableDiv", "rssFeedFixedHeaderDiv", rssFeedContextMenu);
const rssArticleContextMenu = new window.qBittorrent.ContextMenu.RssArticleContextMenu({ const rssArticleContextMenu = new window.qBittorrent.ContextMenu.RssArticleContextMenu({
targets: ".rssArticleElement", targets: "#rssArticleTableDiv tr",
menu: "rssArticleMenu", menu: "rssArticleMenu",
actions: { actions: {
Download: (el) => { Download: (el) => {

View file

@ -95,7 +95,7 @@
const setup = () => { const setup = () => {
searchPluginsTable = new window.qBittorrent.DynamicTable.SearchPluginsTable(); searchPluginsTable = new window.qBittorrent.DynamicTable.SearchPluginsTable();
searchPluginsTableContextMenu = new window.qBittorrent.ContextMenu.SearchPluginsTableContextMenu({ searchPluginsTableContextMenu = new window.qBittorrent.ContextMenu.SearchPluginsTableContextMenu({
targets: ".searchPluginsTableRow", targets: "#searchPluginsTableDiv tr",
menu: "searchPluginsTableMenu", menu: "searchPluginsTableMenu",
actions: { actions: {
Enabled: enablePlugin, Enabled: enablePlugin,
@ -104,6 +104,12 @@
offsets: calculateContextMenuOffsets() offsets: calculateContextMenuOffsets()
}); });
searchPluginsTable.setup("searchPluginsTableDiv", "searchPluginsTableFixedHeaderDiv", searchPluginsTableContextMenu); searchPluginsTable.setup("searchPluginsTableDiv", "searchPluginsTableFixedHeaderDiv", searchPluginsTableContextMenu);
searchPluginsTable.dynamicTableDiv.addEventListener("dblclick", (e) => { enablePlugin(); });
searchPluginsTable.dynamicTableDiv.addEventListener("contextmenu", (e) => {
updateSearchPluginsTableContextMenuOffset();
}, true);
updateTable(); updateTable();
}; };
@ -181,27 +187,7 @@
searchPluginsTableContextMenu.options.offsets = calculateContextMenuOffsets(); searchPluginsTableContextMenu.options.offsets = calculateContextMenuOffsets();
}; };
const setupSearchPluginTableEvents = (enable) => {
const clickHandler = (e) => { enablePlugin(); };
const menuHandler = (e) => { updateSearchPluginsTableContextMenuOffset(); };
if (enable) {
$$(".searchPluginsTableRow").each((target) => {
target.addEventListener("dblclick", clickHandler);
target.addEventListener("contextmenu", menuHandler, true);
});
}
else {
$$(".searchPluginsTableRow").each((target) => {
target.removeEventListener("dblclick", clickHandler);
target.removeEventListener("contextmenu", menuHandler, true);
});
}
};
const updateTable = () => { const updateTable = () => {
// clear event listeners
setupSearchPluginTableEvents(false);
const oldPlugins = [...searchPluginsTable.getRowIds()]; const oldPlugins = [...searchPluginsTable.getRowIds()];
// remove old rows from the table // remove old rows from the table
for (let i = 0; i < oldPlugins.length; ++i) { for (let i = 0; i < oldPlugins.length; ++i) {
@ -222,9 +208,6 @@
} }
searchPluginsTable.updateTable(); searchPluginsTable.updateTable();
// add event listeners
setupSearchPluginTableEvents(true);
}; };
return exports(); return exports();

View file

@ -29,7 +29,7 @@
// create a context menu // create a context menu
const contextMenu = new window.qBittorrent.ContextMenu.TorrentsTableContextMenu({ const contextMenu = new window.qBittorrent.ContextMenu.TorrentsTableContextMenu({
targets: ".torrentsTableContextMenuTarget", targets: "#torrentsTableDiv tr",
menu: "torrentsTableMenu", menu: "torrentsTableMenu",
actions: { actions: {
start: (element, ref) => { start: (element, ref) => {