WebUI: switch to lightweight clipboard library

The new library [1] will opt to the modern Clipboard API [2] when it is available. It will
fallback to the old method otherwise.
The new library is also smaller and without any bloat.

Note that the line `module.exports` is required to be removed/commented out. This is the only
patch required.

[1] https://github.com/feross/clipboard-copy
[2] https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API

PR #22792.
This commit is contained in:
Chocobo1 2025-05-31 17:55:10 +08:00 committed by GitHub
parent 4b07597d54
commit 96f0eebc4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 123 additions and 72 deletions

View file

@ -31,7 +31,7 @@
<script defer src="scripts/localpreferences.js?v=${CACHEID}"></script>
<script defer src="scripts/color-scheme.js?v=${CACHEID}"></script>
<script defer src="scripts/mocha-init.js?locale=${LANG}&v=${CACHEID}"></script>
<script defer src="scripts/lib/clipboard.min.js"></script>
<script defer src="scripts/lib/clipboard-copy.js"></script>
<script defer src="scripts/filesystem.js?v=${CACHEID}"></script>
<script defer src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script defer src="scripts/progressbar.js?v=${CACHEID}"></script>

View file

@ -1785,26 +1785,29 @@ window.addEventListener("DOMContentLoaded", (event) => {
}
});
new ClipboardJS(".copyToClipboard", {
text: (trigger) => {
switch (trigger.id) {
case "copyName":
return copyNameFN();
case "copyInfohash1":
return copyInfohashFN(1);
case "copyInfohash2":
return copyInfohashFN(2);
case "copyMagnetLink":
return copyMagnetLinkFN();
case "copyID":
return copyIdFN();
case "copyComment":
return copyCommentFN();
default:
return "";
}
for (const element of document.getElementsByClassName("copyToClipboard")) {
const setupClickEvent = (textFunc) => element.addEventListener("click", async (event) => await clipboardCopy(textFunc()));
switch (element.id) {
case "copyName":
setupClickEvent(copyNameFN);
break;
case "copyInfohash1":
setupClickEvent(() => copyInfohashFN(1));
break;
case "copyInfohash2":
setupClickEvent(() => copyInfohashFN(2));
break;
case "copyMagnetLink":
setupClickEvent(copyMagnetLinkFN);
break;
case "copyID":
setupClickEvent(copyIdFN);
break;
case "copyComment":
setupClickEvent(copyCommentFN);
break;
}
});
}
addEventListener("visibilitychange", (event) => {
if (document.hidden)

View file

@ -0,0 +1,63 @@
/*! clipboard-copy. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/* global DOMException */
//module.exports = clipboardCopy
function makeError () {
return new DOMException('The request is not allowed', 'NotAllowedError')
}
async function copyClipboardApi (text) {
// Use the Async Clipboard API when available. Requires a secure browsing
// context (i.e. HTTPS)
if (!navigator.clipboard) {
throw makeError()
}
return navigator.clipboard.writeText(text)
}
async function copyExecCommand (text) {
// Put the text to copy into a <span>
const span = document.createElement('span')
span.textContent = text
// Preserve consecutive spaces and newlines
span.style.whiteSpace = 'pre'
span.style.webkitUserSelect = 'auto'
span.style.userSelect = 'all'
// Add the <span> to the page
document.body.appendChild(span)
// Make a selection object representing the range of text selected by the user
const selection = window.getSelection()
const range = window.document.createRange()
selection.removeAllRanges()
range.selectNode(span)
selection.addRange(range)
// Copy text to the clipboard
let success = false
try {
success = window.document.execCommand('copy')
} finally {
// Cleanup
selection.removeAllRanges()
window.document.body.removeChild(span)
}
if (!success) throw makeError()
}
async function clipboardCopy (text) {
try {
await copyClipboardApi(text)
} catch (err) {
// ...Otherwise, use document.execCommand() fallback
try {
await copyExecCommand(text)
} catch (err2) {
throw (err2 || err || makeError())
}
}
}

File diff suppressed because one or more lines are too long

View file

@ -183,10 +183,9 @@ window.qBittorrent.PropPeers ??= (() => {
}
});
new ClipboardJS("#CopyPeerInfo", {
text: (trigger) => {
return torrentPeersTable.selectedRowsIds().join("\n");
}
document.getElementById("CopyPeerInfo").addEventListener("click", async (event) => {
const text = torrentPeersTable.selectedRowsIds().join("\n");
await clipboardCopy(text);
});
torrentPeersTable.setup("torrentPeersTableDiv", "torrentPeersTableFixedHeaderDiv", torrentPeersContextMenu, true);

View file

@ -248,10 +248,9 @@ window.qBittorrent.PropTrackers ??= (() => {
torrentTrackersTable.clear();
};
new ClipboardJS("#CopyTrackerUrl", {
text: (trigger) => {
return torrentTrackersTable.selectedRowsIds().join("\n");
}
document.getElementById("CopyTrackerUrl").addEventListener("click", async (event) => {
const text = torrentTrackersTable.selectedRowsIds().join("\n");
await clipboardCopy(text);
});
torrentTrackersTable.setup("torrentTrackersTableDiv", "torrentTrackersTableFixedHeaderDiv", torrentTrackersContextMenu, true);

View file

@ -219,10 +219,9 @@ window.qBittorrent.PropWebseeds ??= (() => {
torrentWebseedsTable.clear();
};
new ClipboardJS("#CopyWebseedUrl", {
text: (trigger) => {
return torrentWebseedsTable.selectedRowsIds().join("\n");
}
document.getElementById("CopyWebseedUrl").addEventListener("click", async (event) => {
const text = torrentWebseedsTable.selectedRowsIds().join("\n");
await clipboardCopy(text);
});
torrentWebseedsTable.setup("torrentWebseedsTableDiv", "torrentWebseedsTableFixedHeaderDiv", torrentWebseedsContextMenu, true);

View file

@ -866,20 +866,20 @@ window.qBittorrent.Search ??= (() => {
state.loadResultsTimer = loadSearchResultsData.delay(500, this, searchId);
};
new ClipboardJS(".copySearchDataToClipboard", {
text: (trigger) => {
switch (trigger.id) {
case "copySearchTorrentName":
return copySearchTorrentName();
case "copySearchTorrentDownloadLink":
return copySearchTorrentDownloadLink();
case "copySearchTorrentDescriptionUrl":
return copySearchTorrentDescriptionUrl();
default:
return "";
}
for (const element of document.getElementsByClassName("copySearchDataToClipboard")) {
const setupClickEvent = (textFunc) => element.addEventListener("click", async (event) => await clipboardCopy(textFunc()));
switch (element.id) {
case "copySearchTorrentName":
setupClickEvent(copySearchTorrentName);
break;
case "copySearchTorrentDownloadLink":
setupClickEvent(copySearchTorrentDownloadLink);
break;
case "copySearchTorrentDescriptionUrl":
setupClickEvent(copySearchTorrentDescriptionUrl);
break;
}
});
}
return exports();
})();

View file

@ -144,7 +144,7 @@
</div>
<ul id="logTableMenu" class="contextMenu">
<li><a href="#" class="copyLogDataToClipboard"><img src="images/edit-copy.svg" alt="QBT_TR(Copy)QBT_TR[CONTEXT=ExecutionLogWidget]">QBT_TR(Copy)QBT_TR[CONTEXT=ExecutionLogWidget]</a></li>
<li><a href="#" id="copyLogDataToClipboard"><img src="images/edit-copy.svg" alt="QBT_TR(Copy)QBT_TR[CONTEXT=ExecutionLogWidget]">QBT_TR(Copy)QBT_TR[CONTEXT=ExecutionLogWidget]</a></li>
<li><a href="#Clear"><img src="images/list-remove.svg" alt="QBT_TR(Clear)QBT_TR[CONTEXT=ExecutionLogWidget]">QBT_TR(Clear)QBT_TR[CONTEXT=ExecutionLogWidget]</a></li>
</ul>
@ -418,15 +418,11 @@
});
};
new ClipboardJS(".copyLogDataToClipboard", {
text: () => {
const msg = [];
tableInfo[currentSelectedTab].instance.selectedRowsIds().forEach((rowId) => {
msg.push(tableInfo[currentSelectedTab].instance.getRow(rowId).full_data[(currentSelectedTab === "main") ? "message" : "ip"]);
});
return msg.join("\n");
}
document.getElementById("copyLogDataToClipboard").addEventListener("click", async (event) => {
const instance = tableInfo[currentSelectedTab].instance;
const type = (currentSelectedTab === "main") ? "message" : "ip";
const msg = instance.selectedRowsIds().map((rowId) => instance.getRow(rowId).full_data[type]);
await clipboardCopy(msg.join("\n"));
});
return exports();

View file

@ -277,17 +277,16 @@
}
});
new ClipboardJS("#CopyFeedURL", {
text: () => {
let joined = "";
for (const rowID of rssFeedTable.selectedRows) {
const row = rssFeedTable.getRow(rowID);
if (row.full_data.dataUid !== "")
joined += `${row.full_data.dataUrl}\n`;
}
return joined.slice(0, -1);
document.getElementById("CopyFeedURL").addEventListener("click", async (event) => {
let joined = "";
for (const rowID of rssFeedTable.selectedRows) {
const row = rssFeedTable.getRow(rowID);
if (row.full_data.dataUid !== "")
joined += `${row.full_data.dataUrl}\n`;
}
await clipboardCopy(joined.slice(0, -1));
});
rssFeedTable.setup("rssFeedTableDiv", "rssFeedFixedHeaderDiv", rssFeedContextMenu);
const rssArticleContextMenu = new window.qBittorrent.ContextMenu.RssArticleContextMenu({

View file

@ -400,7 +400,7 @@
<file>private/scripts/dynamicTable.js</file>
<file>private/scripts/file-tree.js</file>
<file>private/scripts/filesystem.js</file>
<file>private/scripts/lib/clipboard.min.js</file>
<file>private/scripts/lib/clipboard-copy.js</file>
<file>private/scripts/lib/mocha.min.js</file>
<file>private/scripts/lib/MooTools-Core-1.6.0-compat-compressed.js</file>
<file>private/scripts/lib/MooTools-More-1.6.0-compat-compressed.js</file>