RamiEjailat / Open Links in Background Tab

// ==UserScript==
// @name         Open Links in Background Tab
// @description  Opens links in a new background tab next to the current tab
// @author       Rami S Ejailat
// @copyright    RamiEjailat (https://openuserjs.org/users/RamiEjailat)
// @updateURL    https://openuserjs.org/meta/RamiEjailat/Open_Links_in_Background_Tab.meta.js
// @downloadURL  https://openuserjs.org/install/RamiEjailat/Open_Links_in_Background_Tab.user.js
// @license      MIT
// @version      20260429
// @namespace    http://tampermonkey.net/
// @match        *://*/*
// @grant        GM_openInTab
// @icon         https://cdn-icons-png.flaticon.com/64/4906/4906292.png
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    // ─── Helpers ─────────────────────────────────────────────────────────────
    function shouldIntercept(target, e) {
        if (!target || !target.href) return false;
        const href = target.href;
        if (href === '#') return false;
        if (href.startsWith('javascript:')) return false;
        if (href.startsWith(window.location.href.split('#')[0] + '#')) return false;
        if (e && (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey)) return false;
        return true;
    }

    function openInBackground(href) {
        const originalUrl = window.location.href;

        GM_openInTab(href, { active: false, insert: true, setParent: true });

        // Mark link as visited without navigating
        window.history.replaceState({}, '', href);
        window.history.replaceState({}, '', originalUrl);

        // Safety net: if a SPA router reacted to the push, restore after 200ms.
        setTimeout(() => {
            if (window.location.href !== originalUrl) {
                window.history.replaceState({}, '', originalUrl);
            }
        }, 200);
    }

    function intercept(e, href) {
        e.preventDefault();
        e.stopImmediatePropagation();
        openInBackground(href);
    }

    // ─── Touch ───────────────────────────────────────────────────────────────
    const TAP_THRESHOLD_PX = 10;
    let touchAnchor = null;
    let touchOrigin = { x: 0, y: 0 };

    document.addEventListener('touchstart', function (e) {
        touchAnchor = null;
        const target = e.target.closest('a');
        if (!shouldIntercept(target, e)) return;

        const touch = e.touches[0];
        touchOrigin = { x: touch.clientX, y: touch.clientY };
        touchAnchor = target;
        // Passive and no preventDefault so that scroll stays free
    }, { capture: true, passive: true });

    document.addEventListener('touchend', function (e) {
        if (!touchAnchor) return;

        const touch = e.changedTouches[0];
        const moved = Math.abs(touch.clientX - touchOrigin.x) > TAP_THRESHOLD_PX
        || Math.abs(touch.clientY - touchOrigin.y) > TAP_THRESHOLD_PX;

        const anchor = touchAnchor;
        touchAnchor = null;

        if (!moved) { // Confirmed tap
            intercept(e, anchor.href);
        }
    }, { capture: true, passive: false }); // passive: false required for preventDefault

    // ─── Context menu: right-click / long-press ───────────────────────────────
    document.addEventListener('contextmenu', function (e) {
        const target = e.target.closest('a');
        if (!shouldIntercept(target, e)) return;
        intercept(e, target.href);
    }, true);
})();