129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
/*
|
|
* Vencord, a Discord client mod
|
|
* Copyright (c) 2025 Vendicated and contributors
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
import { definePluginSettings } from "@api/Settings";
|
|
import { EquicordDevs } from "@utils/index";
|
|
import definePlugin, { OptionType } from "@utils/types";
|
|
|
|
const MIDDLE_CLICK = 1;
|
|
|
|
let lastMiddleClickUp = 0;
|
|
|
|
const settings = definePluginSettings({
|
|
scope: {
|
|
type: OptionType.SELECT,
|
|
description: "Situations in which to prevent middle click from pasting.",
|
|
options: [
|
|
{
|
|
label: "Always Prevent Middle Click Pasting",
|
|
value: "always",
|
|
default: true
|
|
},
|
|
{
|
|
label: "Only Prevent When Text Area Not Focused",
|
|
value: "focus"
|
|
},
|
|
]
|
|
},
|
|
threshold: {
|
|
type: OptionType.NUMBER,
|
|
description: "Milliseconds until pasting is enabled again after a middle click.",
|
|
default: 100,
|
|
onChange(newValue) { if (newValue < 1) { settings.store.threshold = 1; } },
|
|
},
|
|
preventLinkOpen: {
|
|
type: OptionType.BOOLEAN,
|
|
description: "Prevent middle-click on links from opening new tabs while preserving autoscroll.",
|
|
default: false,
|
|
onChange(newValue) {
|
|
if (newValue) {
|
|
document.addEventListener("auxclick", handleAuxClick, true);
|
|
} else {
|
|
document.removeEventListener("auxclick", handleAuxClick, true);
|
|
}
|
|
},
|
|
},
|
|
});
|
|
|
|
function shouldBlockLink(e: MouseEvent): boolean {
|
|
if (e.button !== MIDDLE_CLICK) return false;
|
|
|
|
const target = e.target as HTMLElement | null;
|
|
if (!target?.closest) return false;
|
|
|
|
const a = target.closest("a[href]") as HTMLAnchorElement | null;
|
|
if (!a) return false;
|
|
|
|
const href = a.getAttribute("href");
|
|
return !!href && href !== "#";
|
|
}
|
|
|
|
function handleAuxClick(e: MouseEvent) {
|
|
if (!shouldBlockLink(e)) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
export default definePlugin({
|
|
name: "LimitMiddleClickPaste",
|
|
description: "Prevent middle click pasting either always or just when a text area is not focused.",
|
|
authors: [EquicordDevs.Etorix, EquicordDevs.korzi],
|
|
settings,
|
|
|
|
isPastingDisabled(isInput: boolean) {
|
|
const pasteBlocked = Date.now() - lastMiddleClickUp < Math.max(settings.store.threshold, 1);
|
|
const { scope } = settings.store;
|
|
|
|
if (!pasteBlocked) return false;
|
|
if (scope === "always") return true;
|
|
if (scope === "focus" && !isInput) return true;
|
|
|
|
return false;
|
|
},
|
|
|
|
onMouseUp: (e: MouseEvent) => {
|
|
if (e.button === MIDDLE_CLICK) lastMiddleClickUp = Date.now();
|
|
},
|
|
|
|
start() {
|
|
document.addEventListener("mouseup", this.onMouseUp);
|
|
if (settings.store.preventLinkOpen) {
|
|
document.addEventListener("auxclick", handleAuxClick, true);
|
|
}
|
|
},
|
|
|
|
stop() {
|
|
document.removeEventListener("mouseup", this.onMouseUp);
|
|
document.removeEventListener("auxclick", handleAuxClick, true);
|
|
},
|
|
|
|
patches: [
|
|
{
|
|
// Detects paste events triggered by the "browser" outside of input fields.
|
|
find: "document.addEventListener(\"paste\",",
|
|
replacement: {
|
|
match: /(?<=paste",(\i)=>{)/,
|
|
replace: "if($1.target.tagName===\"BUTTON\"||$self.isPastingDisabled(false)){$1.preventDefault?.();$1.stopPropagation?.();return;};"
|
|
}
|
|
},
|
|
{
|
|
// Detects paste events triggered inside of Discord's text input.
|
|
find: ",origin:\"clipboard\"});",
|
|
replacement: {
|
|
match: /(?<=handlePaste=(\i)=>{)(?=let)/,
|
|
replace: "if($self.isPastingDisabled(true)){$1.preventDefault?.();$1.stopPropagation?.();return;}"
|
|
}
|
|
},
|
|
{
|
|
// Detects paste events triggered inside of Discord's search box.
|
|
find: "props.handlePastedText&&",
|
|
replacement: {
|
|
match: /(?<=clipboardData\);)/,
|
|
replace: "if($self.isPastingDisabled(true)){arguments[1].preventDefault?.();arguments[1].stopPropagation?.();return;};"
|
|
}
|
|
},
|
|
],
|
|
});
|