// ==UserScript==
// @name Open Links in Background Tabs One-Button Toggle (Per-Page, Modifiers, Bottom-Right, menu)
// @namespace phillipfierro.toggle-bg-tabs.page
// @version 1.5
// @description Hover bottom-right to reveal a toggle. ON: links open in background tabs; Alt=current tab; Shift=foreground tab. Per-page state (origin+pathname). Per-host master menu to enable/disable everything for this hostname.
// @author You
// @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
// ==/UserScript==
(function () {
'use strict';
// --- Keys ---
const MASTER_KEY_PREFIX = 'toggleBgTabs:masterEnabled:host:'; // + hostname
const PAGE_KEY_PREFIX = 'toggleBgTabs:page:'; // + origin+pathname(+opts)
// Per-page identity granularity
const INCLUDE_QUERY = false; // set true to include ?query in per-page key
const INCLUDE_HASH = false; // set true to include #hash in per-page key
// --- CSS (loaded once; harmless if master is OFF) ---
const styles = `
.bgTabsToggle-wrap {
position: fixed; bottom: 8px; right: 8px;
z-index: 2147483647; pointer-events: none;
}
.bgTabsToggle-hotspot {
position: fixed; bottom: 0; right: 0; width: 64px; height: 64px;
z-index: 2147483646; pointer-events: auto; background: transparent;
}
.bgTabsToggle-btn {
pointer-events: auto; opacity: 0; 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-hotspot:hover ~ .bgTabsToggle-wrap .bgTabsToggle-btn,
.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; }
`;
if (typeof GM_addStyle === 'function') GM_addStyle(styles);
else {
const st = document.createElement('style');
st.textContent = styles;
document.documentElement.appendChild(st);
}
// --- State ---
const HOSTNAME = location.hostname;
const MASTER_KEY = MASTER_KEY_PREFIX + HOSTNAME;
let masterEnabled = getBool(MASTER_KEY, true); // per-host default: enabled
let pageKey = currentPageKey();
let pageEnabled = getBool(PAGE_KEY_PREFIX + pageKey, false); // per-page default: OFF
// UI nodes
let hotspot = null, wrap = null, btn = null;
// --- Menu (per-host master toggle) ---
let menuId = null;
registerOrUpdateMenu();
function registerOrUpdateMenu() {
const label = masterEnabled
? `Disable “BG Tabs Toggle” on this host (${HOSTNAME})`
: `Enable “BG Tabs Toggle” on this host (${HOSTNAME})`;
try {
if (typeof GM_unregisterMenuCommand === 'function' && menuId !== null) {
GM_unregisterMenuCommand(menuId);
}
} catch (_) {}
try {
menuId = GM_registerMenuCommand(label, () => {
masterEnabled = !masterEnabled;
setBool(MASTER_KEY, masterEnabled); // persist per-host
applyMasterState();
registerOrUpdateMenu(); // refresh label
});
} catch (_) {
// Some managers don't support unregister; fall back to static label
if (menuId === null) {
GM_registerMenuCommand(label, () => {
masterEnabled = !masterEnabled;
setBool(MASTER_KEY, masterEnabled);
applyMasterState();
});
menuId = 'static';
}
}
}
// --- Init UI according to master state ---
applyMasterState();
// --- Click interception (early capture). No-op if disabled. ---
document.addEventListener('click', function (e) {
if (!masterEnabled) return;
if (!pageEnabled) return;
if (e.defaultPrevented) return;
// Ignore clicks on our UI
if (wrap && (wrap.contains(e.target) || (hotspot && hotspot.contains(e.target)))) return;
const b = 'button' in e ? e.button : 0;
if (b !== 0 && b !== 1) return; // left or middle only
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);
// Modifiers (only while ON)
if (e.altKey) {
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
navigateSameTab(url); return;
}
if (e.shiftKey) {
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
openInTab(url, /*active*/ true); return;
}
// Default: background tab
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
openInTab(url, /*active*/ false);
}, true);
// --- SPA awareness: update per-page state on path changes ---
hookLocationChanges(() => {
const newKey = currentPageKey();
if (newKey !== pageKey) {
pageKey = newKey;
pageEnabled = getBool(PAGE_KEY_PREFIX + pageKey, false);
if (masterEnabled) updateButton();
}
});
// ---------- Helpers ----------
function currentPageKey() {
const u = new URL(location.href);
let p = u.pathname.replace(/\/index\.[a-z0-9]+$/i, '/');
p = p.replace(/\/+$/, '/') || '/';
let key = u.origin + p;
if (INCLUDE_QUERY && u.search) key += u.search;
if (INCLUDE_HASH && u.hash) key += u.hash;
return key;
}
function getBool(k, dflt) {
try { return !!GM_getValue(k, dflt); } catch { return dflt; }
}
function setBool(k, v) {
try { GM_setValue(k, !!v); } catch {}
}
function ensureUI() {
if (hotspot && wrap && btn) { updateButton(); return; }
hotspot = document.createElement('div');
hotspot.className = 'bgTabsToggle-hotspot';
wrap = document.createElement('div');
wrap.className = 'bgTabsToggle-wrap';
btn = document.createElement('button');
btn.className = 'bgTabsToggle-btn';
btn.addEventListener('click', () => {
pageEnabled = !pageEnabled;
setBool(PAGE_KEY_PREFIX + pageKey, pageEnabled);
updateButton();
});
// Order matters for the sibling hover selector:
document.documentElement.appendChild(hotspot);
document.documentElement.appendChild(wrap);
wrap.appendChild(btn);
updateButton();
}
function destroyUI() {
try { wrap && wrap.remove(); } catch {}
try { hotspot && hotspot.remove(); } catch {}
wrap = hotspot = btn = null;
}
function applyMasterState() {
if (masterEnabled) {
ensureUI(); // show/restore button
updateButton(); // reflect per-page state
} else {
destroyUI(); // hide button + disable mouseover reveal
}
}
function updateButton() {
if (!btn) return;
if (pageEnabled) {
btn.classList.remove('off'); btn.classList.add('on');
btn.textContent = 'BG Tabs: ON';
btn.title = 'Click to turn OFF (per this page). Alt=current tab, Shift=foreground tab.';
} else {
btn.classList.remove('on'); btn.classList.add('off');
btn.textContent = 'BG Tabs: OFF';
btn.title = 'Click to turn ON (per this page).';
}
}
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); } // legacy boolean: true=background
catch { window.open(url, '_blank', 'noopener,noreferrer'); }
}
}
function hookLocationChanges(onChange) {
const origPush = history.pushState;
const origReplace = history.replaceState;
function fire() { window.dispatchEvent(new Event('locationchange')); }
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);
}
})();