One-Button Toggle: Open Links in Background Tabs (Per-Page + Modifiers)

Hover top-right to reveal a toggle. When ON: links open in background tabs; Alt = current tab; Shift = foreground tab. When OFF: keyboard shortcuts act as normal. State persists per page (origin+pathname).

// ==UserScript==
// @name         One-Button Toggle: Open Links in Background Tabs (Per-Page + Modifiers)
// @namespace    phillipfierro.toggle-bg-tabs.page
// @version      1.2
// @description  Hover top-right to reveal a toggle. When ON: links open in background tabs; Alt = current tab; Shift = foreground tab. When OFF: keyboard shortcuts act as normal. State persists per page (origin+pathname).
// @author       You
// @match        http*://*/*
// @grant        GM_openInTab
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-start
// @noframes
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // --- Key granularity for per-page persistence ---
  const INCLUDE_QUERY = false;
  const INCLUDE_HASH  = false;

  const STORAGE_PREFIX = 'toggleBgTabs:page:';

  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;
  }

  // --- State (per-page) ---
  let pageKey = currentPageKey();
  let enabled = !!GM_getValue(STORAGE_PREFIX + pageKey, false);

  // --- UI: hover-to-reveal corner button ---
  const styles = `
    .bgTabsToggle-wrap {
      position: fixed; top: 8px; right: 8px;
      z-index: 2147483647; pointer-events: none;
    }
    .bgTabsToggle-hotspot {
      position: fixed; top: 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);
  }

  const hotspot = document.createElement('div');
  hotspot.className = 'bgTabsToggle-hotspot';

  const wrap = document.createElement('div');
  wrap.className = 'bgTabsToggle-wrap';

  const btn = document.createElement('button');
  btn.className = 'bgTabsToggle-btn';
  updateButton();

  btn.addEventListener('click', () => {
    enabled = !enabled;
    GM_setValue(STORAGE_PREFIX + pageKey, enabled);
    updateButton();
  });

  document.documentElement.appendChild(hotspot);
  document.documentElement.appendChild(wrap);
  wrap.appendChild(btn);

  function updateButton() {
    if (enabled) {
      btn.classList.remove('off'); btn.classList.add('on');
      btn.textContent = 'BG Tabs: ON';
      btn.title = 'Click to turn OFF (per this page). Modifiers: 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)';
    }
  }

  // --- Core behavior: intercept clicks when enabled ---
  document.addEventListener('click', function (e) {
    if (!enabled) return;
    if (e.defaultPrevented) return;

    // Ignore clicks on our UI
    if (wrap.contains(e.target) || 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 apply when toggle is ON
    if (e.altKey) {
      // Open in current tab even if link has target or site handlers
      e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
      navigateSameTab(url);
      return;
    }
    if (e.shiftKey) {
      // Open in a new foreground tab
      e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
      openInTab(url, /*active*/ true);
      return;
    }

    // Default ON behavior: open in background tab
    e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
    openInTab(url, /*active*/ false);
  }, true); // capture

  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) {
    // Prefer same-document navigation without referrer spoofing changes
    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'); } // browser decides foreground/background
    }
  }

  // --- SPA awareness: update per-page state on URL changes ---
  hookLocationChanges(() => {
    const newKey = currentPageKey();
    if (newKey !== pageKey) {
      pageKey = newKey;
      enabled = !!GM_getValue(STORAGE_PREFIX + pageKey, false);
      updateButton();
    }
  });

  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);
  }
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。