// ==UserScript==
// @name Open Links in Background Tabs One-Button Toggle (Per-Page, Modifiers, Draggable, master-toggle menu)
// @version 2.0
// @description A semi-transparent toggle appears in the corner. ON: links open in background tabs. Includes a master enable/disable menu command.
// @author Te55eract
// @match http*://*/*
// @grant GM_openInTab
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @run-at document-start
// @noframes
// @license MIT
// @namespace te55eract.toggle-bg-tabs.page
// ==/UserScript==
(function () {
'use strict';
// --- Key granularity for per-page persistence ---
const INCLUDE_QUERY = false;
const INCLUDE_HASH = false;
const STORAGE_PREFIX = 'toggleBgTabs:page:';
const POS_STORAGE_PREFIX = 'toggleBgTabs:pos:';
const MASTER_DISABLED_PREFIX = 'toggleBgTabs:masterDisabled:';
// --- State Variables ---
let wrap = null;
let btn = null;
let enabled = false;
let pageKey = currentPageKey();
let isMasterDisabled = !!GM_getValue(MASTER_DISABLED_PREFIX + pageKey, false);
let masterMenuCommand = null;
function normalizePathname(pathname) {
let p = pathname.replace(/\/index\.[a-z0-9]+$/i, '/');
p = p.replace(/\/+$/, '/') || '/';
return p;
}
function currentPageKey() {
const u = new URL(location.href);
let key = u.origin + normalizePathname(u.pathname);
if (INCLUDE_QUERY && u.search) key += u.search;
if (INCLUDE_HASH && u.hash) key += u.hash;
return key;
}
// --- Master Toggle via Menu Command (with Dynamic Text Update) ---
function updateMasterMenuCommand() {
if (masterMenuCommand) {
GM_unregisterMenuCommand(masterMenuCommand);
}
const menuText = isMasterDisabled ? '✅ Enable Script on this Page' : '❌ Disable Script on this Page';
masterMenuCommand = GM_registerMenuCommand(menuText, toggleMasterState);
}
function toggleMasterState() {
isMasterDisabled = !isMasterDisabled;
GM_setValue(MASTER_DISABLED_PREFIX + pageKey, isMasterDisabled);
if (isMasterDisabled) {
if (wrap) wrap.style.display = 'none';
console.log('[BG Tabs Toggle] Script disabled.');
} else {
if (wrap) {
wrap.style.display = '';
} else {
initializeUI();
}
console.log('[BG Tabs Toggle] Script enabled.');
}
// After changing state, update the menu item text to reflect the new state
updateMasterMenuCommand();
}
// Initialize the menu command when the script first runs
updateMasterMenuCommand();
// --- Main Initialization Function ---
function initializeUI() {
// Prevent re-initialization
if (document.querySelector('.bgTabsToggle-wrap')) return;
enabled = !!GM_getValue(STORAGE_PREFIX + pageKey, false);
// --- UI: hover-to-reveal corner button ---
const styles = `
.bgTabsToggle-wrap {
position: fixed;
z-index: 2147483647;
pointer-events: none;
}
.bgTabsToggle-btn {
pointer-events: auto; opacity: 0.4; transform: translateY(6px);
transition: opacity 120ms ease, transform 120ms ease, background 120ms ease, color 120ms ease, border-color 120ms ease;
font: 12px/1.2 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
padding: 6px 10px; border-radius: 999px; border: 1px solid; cursor: pointer; user-select: none;
box-shadow: 0 2px 10px rgba(0,0,0,0.25);
}
.bgTabsToggle-wrap:hover .bgTabsToggle-btn { opacity: 1; transform: translateY(0); }
.bgTabsToggle-btn.off { background: #ffefef; color: #a40000; border-color: #ff8a8a; }
.bgTabsToggle-btn.off:hover { background: #ffd9d9; }
.bgTabsToggle-btn.on { background: #eefbee; color: #0a6b00; border-color: #85e485; }
.bgTabsToggle-btn.on:hover { background: #d9f8d9; }
.bgTabsToggle-btn:active { cursor: grabbing; }
`;
if (typeof GM_addStyle === 'function') GM_addStyle(styles);
else {
const st = document.createElement('style');
st.textContent = styles;
document.documentElement.appendChild(st);
}
wrap = document.createElement('div');
wrap.className = 'bgTabsToggle-wrap';
btn = document.createElement('button');
btn.className = 'bgTabsToggle-btn';
updateButton();
document.documentElement.appendChild(wrap);
wrap.appendChild(btn);
// --- Draggable & Persistence Logic ---
const DEFAULT_POS = { x: null, y: null };
const getStoredPos = () => {
try {
return JSON.parse(GM_getValue(POS_STORAGE_PREFIX + pageKey, null)) || DEFAULT_POS;
} catch {
return DEFAULT_POS;
}
};
const savePos = (x, y) => {
GM_setValue(POS_STORAGE_PREFIX + pageKey, JSON.stringify({ x, y }));
};
const resetPos = () => {
GM_setValue(POS_STORAGE_PREFIX + pageKey, null);
setDefaultPosition();
};
function setDefaultPosition() {
wrap.style.top = 'auto';
wrap.style.left = 'auto';
wrap.style.bottom = '8px';
wrap.style.right = '8px';
}
let dragging = false;
let hasMoved = false;
let offsetX = 0;
let offsetY = 0;
btn.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
e.preventDefault();
dragging = true;
hasMoved = false;
const rect = wrap.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
wrap.style.transition = 'none';
wrap.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
hasMoved = true;
const rawX = e.clientX - offsetX;
const rawY = e.clientY - offsetY;
const { x: newX, y: newY } = clampPosition(rawX, rawY);
wrap.style.left = `${newX}px`;
wrap.style.top = `${newY}px`;
wrap.style.bottom = 'auto';
wrap.style.right = 'auto';
});
document.addEventListener('mouseup', (e) => {
if (!dragging) return;
dragging = false;
wrap.style.transition = '';
wrap.style.cursor = '';
const rect = wrap.getBoundingClientRect();
savePos(rect.left, rect.top);
});
btn.addEventListener('dblclick', (e) => {
e.preventDefault();
resetPos();
});
// --- NEW: Position Clamping & Window Resize ---
function clampPosition(x, y) {
if (!wrap || !btn) return { x, y };
const rect = btn.getBoundingClientRect();
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
const clampedX = Math.max(0, Math.min(x, winWidth - rect.width));
const clampedY = Math.max(0, Math.min(y, winHeight - rect.height));
return { x: clampedX, y: clampedY };
}
function keepButtonInBounds() {
if (!wrap) return;
const rect = wrap.getBoundingClientRect();
const { x, y } = clampPosition(rect.left, rect.top);
if (x !== rect.left || y !== rect.top) {
wrap.style.left = `${x}px`;
wrap.style.top = `${y}px`;
savePos(x, y);
}
}
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(keepButtonInBounds, 100);
});
// --- Set Initial Position ---
const pos = getStoredPos();
if (pos.x !== null && pos.y !== null) {
wrap.style.top = `${pos.y}px`;
wrap.style.left = `${pos.x}px`;
wrap.style.bottom = 'auto';
wrap.style.right = 'auto';
} else {
setDefaultPosition();
}
// Verify initial position is in bounds after a brief delay for rendering
setTimeout(keepButtonInBounds, 100);
btn.addEventListener('click', () => {
if (hasMoved) return;
enabled = !enabled;
GM_setValue(STORAGE_PREFIX + pageKey, enabled);
updateButton();
});
// --- Core behavior: intercept clicks when enabled ---
document.addEventListener('click', function (e) {
if (isMasterDisabled || !enabled) return;
if (e.defaultPrevented) return;
if (wrap.contains(e.target)) return;
const b = 'button' in e ? e.button : 0;
if (b !== 0 && b !== 1) return;
const anchor = findAnchor(e.target);
if (!anchor) return;
const href = anchor.getAttribute('href');
if (!href || href.startsWith('#') || href.startsWith('javascript:')) return;
const url = resolveUrl(anchor, href);
if (e.altKey) {
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
navigateSameTab(url);
return;
}
if (e.shiftKey) {
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
openInTab(url, true);
return;
}
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
openInTab(url, false);
}, true);
// --- SPA awareness: update per-page state on URL changes ---
hookLocationChanges(() => {
const newKey = currentPageKey();
if (newKey === pageKey) return;
pageKey = newKey;
isMasterDisabled = !!GM_getValue(MASTER_DISABLED_PREFIX + pageKey, false);
updateMasterMenuCommand();
if (isMasterDisabled) {
wrap.style.display = 'none';
} else {
wrap.style.display = '';
enabled = !!GM_getValue(STORAGE_PREFIX + pageKey, false);
const pos = getStoredPos();
if (pos.x !== null && pos.y !== null) {
wrap.style.top = `${pos.y}px`;
wrap.style.left = `${pos.x}px`;
wrap.style.bottom = 'auto';
wrap.style.right = 'auto';
} else {
setDefaultPosition();
}
updateButton();
setTimeout(keepButtonInBounds, 100);
}
});
} // --- End of initializeUI ---
function updateButton() {
if (!btn) return;
if (enabled) {
btn.classList.remove('off'); btn.classList.add('on');
btn.textContent = 'BG Tabs: ON';
btn.title = 'Click to turn OFF. Drag to move. Dbl-click to reset position. Modifiers: Alt=current, Shift=foreground.';
} else {
btn.classList.remove('on'); btn.classList.add('off');
btn.textContent = 'BG Tabs: OFF';
btn.title = 'Click to turn ON. Drag to move. Dbl-click to reset position.';
}
}
function findAnchor(node) {
let el = node;
while (el && el !== document && el !== document.documentElement) {
if (el.tagName === 'A' && el.href) return el;
el = el.parentNode;
}
return null;
}
function resolveUrl(anchor, href) {
try { return new URL(href, anchor.baseURI || document.baseURI).toString(); }
catch { return href; }
}
function navigateSameTab(url) {
location.assign(url);
}
function openInTab(url, active) {
try {
GM_openInTab(url, { active, insert: true, setParent: true });
} catch (_) {
try { GM_openInTab(url, !active ? true : false); }
catch { window.open(url, '_blank', 'noopener,noreferrer'); }
}
}
function hookLocationChanges(onChange) {
const origPush = history.pushState;
const origReplace = history.replaceState;
function fire() { setTimeout(() => window.dispatchEvent(new Event('locationchange')), 0); }
history.pushState = function () { const r = origPush.apply(this, arguments); fire(); return r; };
history.replaceState = function () { const r = origReplace.apply(this, arguments); fire(); return r; };
window.addEventListener('popstate', fire);
window.addEventListener('hashchange', fire);
window.addEventListener('locationchange', onChange);
}
// --- Initial Script Execution ---
if (!isMasterDisabled) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeUI);
} else {
initializeUI();
}
} else {
console.log('[BG Tabs Toggle] Script is disabled for this page via menu command.');
}
})();