// ==UserScript==
// @name EZLinkZ
// @namespace https://github.com/codeMonkeyHopeful/EZLinkZ
// @homepage https://github.com/codeMonkeyHopeful/EZLinkZ
// @version 1.4
// @description Copy hrefs from selected links with toast at top; toast clickable to show copied links; user can set keyboard shortcut via menu command
// @author CodeMonkeyHopeful
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @supportURL https://github.com/codeMonkeyHopeful/EZLinkZ
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// Default shortcut keys (Ctrl+Shift+C)
let shortcut = {
ctrlKey: true,
shiftKey: true,
altKey: false,
metaKey: false,
key: "c",
};
// Load saved shortcut from storage
async function loadShortcut() {
const saved = await GM_getValue("shortcut");
if (saved) {
try {
const parsed = JSON.parse(saved);
if (parsed.key) shortcut = parsed;
} catch {}
}
}
// Save shortcut to storage
async function saveShortcut(newShortcut) {
await GM_setValue("shortcut", JSON.stringify(newShortcut));
shortcut = newShortcut;
showToast(`Shortcut updated to ${formatShortcut(newShortcut)}`);
}
function formatShortcut(sc) {
let parts = [];
if (sc.ctrlKey) parts.push("Ctrl");
if (sc.shiftKey) parts.push("Shift");
if (sc.altKey) parts.push("Alt");
if (sc.metaKey) parts.push("Meta");
parts.push(sc.key.toUpperCase());
return parts.join("+");
}
// Prompt user to enter new shortcut (simple example, expects something like "Ctrl+Shift+X")
async function promptShortcut() {
const input = prompt(
`Enter new shortcut keys separated by +\nExample: Ctrl+Shift+X\nCurrent: ${formatShortcut(
shortcut
)}`
);
if (!input) return;
// Parse input
const parts = input
.toLowerCase()
.split("+")
.map((s) => s.trim());
const newSc = {
ctrlKey: parts.includes("ctrl"),
shiftKey: parts.includes("shift"),
altKey: parts.includes("alt"),
metaKey: parts.includes("meta"),
key:
parts.find((k) => !["ctrl", "shift", "alt", "meta"].includes(k)) || "c",
};
await saveShortcut(newSc);
}
// Toast and popup elements
const toast = document.createElement("div");
const popup = document.createElement("div");
const closeBtn = document.createElement("button");
let popupVisible = false;
// Style toast (top center)
Object.assign(toast.style, {
position: "fixed",
top: "20px",
left: "50%",
transform: "translateX(-50%)",
backgroundColor: "rgba(60,60,60,0.9)",
color: "#fff",
padding: "10px 20px",
borderRadius: "5px",
fontSize: "14px",
fontFamily: "sans-serif",
zIndex: 9999,
opacity: "0",
transition: "opacity 0.3s ease",
pointerEvents: "auto",
cursor: "pointer",
userSelect: "none",
maxWidth: "80vw",
boxSizing: "border-box",
});
document.body.appendChild(toast);
// Style popup (hidden by default)
Object.assign(popup.style, {
position: "fixed",
top: "60px",
left: "50%",
transform: "translateX(-50%)",
backgroundColor: "rgba(30,30,30,0.95)",
color: "#fff",
padding: "15px 20px 20px 20px",
borderRadius: "8px",
fontSize: "13px",
fontFamily: "monospace",
zIndex: 10000,
maxHeight: "300px",
maxWidth: "90vw",
overflowY: "auto",
boxSizing: "border-box",
display: "none",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
});
document.body.appendChild(popup);
// Close button inside popup
closeBtn.textContent = "×";
Object.assign(closeBtn.style, {
position: "absolute",
top: "5px",
right: "10px",
background: "transparent",
border: "none",
color: "#fff",
fontSize: "20px",
cursor: "pointer",
userSelect: "none",
});
popup.appendChild(closeBtn);
// Show toast message for duration (ms)
let toastTimeout;
function showToast(message, duration = 2500) {
if (popupVisible) return; // don't show toast if popup open
toast.textContent = message;
toast.style.opacity = "1";
clearTimeout(toastTimeout);
toastTimeout = setTimeout(() => {
toast.style.opacity = "0";
}, duration);
}
// Show popup with links text
function showPopup(text) {
popup.textContent = text;
popup.appendChild(closeBtn);
popup.style.display = "block";
popupVisible = true;
toast.style.opacity = "0";
}
// Hide popup
function hidePopup() {
popup.style.display = "none";
popupVisible = false;
}
// Get links from selection
function getSelectedLinks() {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
return null;
}
const range = selection.getRangeAt(0);
const container = document.createElement("div");
container.appendChild(range.cloneContents());
const links = container.querySelectorAll("a");
const hrefs = Array.from(links)
.map((a) => a.href)
.filter((href) => href);
return hrefs.length > 0 ? hrefs : null;
}
// Copy selected links and handle UI
function copySelectedLinks() {
const hrefs = getSelectedLinks();
if (!hrefs) {
showToast("No links found in selection");
return;
}
const textToCopy = hrefs.join("\n");
navigator.clipboard
.writeText(textToCopy)
.then(() => {
showToast(`Copied ${hrefs.length} link(s) to clipboard`);
// When toast clicked, show popup with links
toast.onclick = () => showPopup(textToCopy);
})
.catch(() => showToast("Failed to copy to clipboard"));
}
// Hide popup when clicking outside it
document.addEventListener("click", (e) => {
if (!popupVisible) return;
if (popup.contains(e.target) || toast.contains(e.target)) return;
hidePopup();
});
// Close button click
closeBtn.addEventListener("click", () => {
hidePopup();
});
// Keyboard shortcut listener
document.addEventListener("keydown", (e) => {
const keyMatches =
e.key.toLowerCase() === shortcut.key.toLowerCase() &&
e.ctrlKey === !!shortcut.ctrlKey &&
e.shiftKey === !!shortcut.shiftKey &&
e.altKey === !!shortcut.altKey &&
e.metaKey === !!shortcut.metaKey;
if (keyMatches) {
e.preventDefault();
copySelectedLinks();
}
});
// Register Tampermonkey menu command to set shortcut
GM_registerMenuCommand("Set Keyboard Shortcut", promptShortcut);
// Load shortcut from storage on start
loadShortcut();
})();