Greasy Fork

Greasy Fork is available in English.

左键点击链接在新标签页打开 (可配置站点+UI)

强制所有鼠标左键点击的普通链接都在新的浏览器标签页中打开。可自定义排除或包含特定网站,并通过侧边栏UI管理规则。

当前为 2025-05-27 提交的版本,查看 最新版本

// ==UserScript==
// @name         左键点击链接在新标签页打开 (可配置站点+UI)
// @name:en      Open Links in New Tab with Left Click (Configurable Sites + UI)
// @namespace    http://greasyfork.icu/users/your-username // 建议替换为你的唯一命名空间
// @version      1.3
// @description  强制所有鼠标左键点击的普通链接都在新的浏览器标签页中打开。可自定义排除或包含特定网站,并通过侧边栏UI管理规则。
// @description:en Forces all regular links clicked with the left mouse button to open in a new browser tab. Allows whitelisting/blacklisting sites with a UI panel for management.
// @author       AI Assistant
// @match        *://*/*
// @grant        GM_openInTab
// @grant        window.open
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置键名 ---
    const CONFIG_KEY_WHITELIST = 'openInNewTab_custom_whitelist_v1';
    const CONFIG_KEY_BLACKLIST = 'openInNewTab_custom_blacklist_v1';
    const CONFIG_KEY_PANEL_VISIBLE = 'openInNewTab_panelVisibleState_v1';

    // --- 默认配置 ---
    const defaultWhitelistPatterns = [];
    const defaultBlacklistPatterns = [];

    let userWhitelist = [];
    let userBlacklist = [];
    let panelVisible = false;
    let uiPanel = null;

    // --- CSS 样式 ---
    GM_addStyle(`
        #openInNewTab-config-panel {
            position: fixed;
            left: 10px;
            top: 70px;
            width: 280px;
            max-height: calc(100vh - 90px);
            background-color: #f8f9fa;
            border: 1px solid #ced4da;
            border-radius: 6px;
            padding: 12px;
            z-index: 999999; /* 确保在顶层 */
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            font-size: 14px;
            color: #212529;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
            display: none; /* 默认隐藏 */
            overflow-y: auto;
        }
        #openInNewTab-config-panel h3 {
            margin-top: 0;
            margin-bottom: 10px;
            font-size: 16px;
            color: #343a40;
            border-bottom: 1px solid #dee2e6;
            padding-bottom: 5px;
        }
        #openInNewTab-config-panel h4 {
            margin-top: 10px;
            margin-bottom: 5px;
            font-size: 14px;
            color: #495057;
        }
        #openInNewTab-config-panel ul {
            list-style-type: none;
            padding-left: 0;
            margin: 0;
            max-height: 200px; /* 给列表一个最大高度 */
            overflow-y: auto; /* 列表内部也允许滚动 */
            border: 1px solid #e9ecef;
            border-radius: 4px;
            background-color: #fff;
        }
        #openInNewTab-config-panel li {
            padding: 6px 8px;
            border-bottom: 1px solid #f1f3f5;
            display: flex;
            align-items: center;
            justify-content: space-between;
            font-size: 13px;
        }
        #openInNewTab-config-panel li:last-child {
            border-bottom: none;
        }
        #openInNewTab-config-panel li span {
            word-break: break-all;
            margin-right: 8px;
            flex-grow: 1;
        }
        #openInNewTab-config-panel .delete-btn {
            color: #dc3545;
            background-color: transparent;
            border: none;
            cursor: pointer;
            font-weight: bold;
            font-size: 16px;
            padding: 0 5px;
            line-height: 1;
            opacity: 0.7;
        }
        #openInNewTab-config-panel .delete-btn:hover {
            color: #c82333;
            opacity: 1;
        }
        #openInNewTab-config-panel .panel-empty-msg {
            font-size: 12px;
            color: #6c757d;
            padding: 8px;
            text-align: center;
        }
        #openInNewTab-config-panel .close-panel-btn {
            display: block;
            width: 100%;
            margin-top: 15px;
            padding: 6px 10px;
            font-size: 13px;
            color: #fff;
            background-color: #6c757d;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        #openInNewTab-config-panel .close-panel-btn:hover {
            background-color: #5a6268;
        }
    `);

    function createUIPanel() {
        if (!document.body) {
            // console.warn("左键新标签页UI: document.body 尚未准备好,稍后尝试创建面板。");
            setTimeout(createUIPanel, 100); // 稍后重试
            return;
        }
        if (document.getElementById('openInNewTab-config-panel')) return; // 防止重复创建

        uiPanel = document.createElement('div');
        uiPanel.id = 'openInNewTab-config-panel';
        // 样式已通过 GM_addStyle 添加

        const title = document.createElement('h3');
        title.textContent = '新标签页打开 - 站点规则';
        uiPanel.appendChild(title);

        const whitelistDiv = document.createElement('div');
        whitelistDiv.id = 'openInNewTab-whitelist-section';
        uiPanel.appendChild(whitelistDiv);

        const blacklistDiv = document.createElement('div');
        blacklistDiv.id = 'openInNewTab-blacklist-section';
        uiPanel.appendChild(blacklistDiv);

        const closeButton = document.createElement('button');
        closeButton.textContent = '关闭面板';
        closeButton.className = 'close-panel-btn';
        closeButton.onclick = togglePanelVisibility;
        uiPanel.appendChild(closeButton);

        document.body.appendChild(uiPanel);
        renderListsUI(); // 创建后立即渲染内容
    }

    function renderListsUI() {
        if (!uiPanel || !panelVisible || !document.body.contains(uiPanel)) return;

        const renderList = (listArray, parentDivId, listTitleText, listType) => {
            const parentElement = uiPanel.querySelector('#' + parentDivId);
            if (!parentElement) return;
            parentElement.innerHTML = ''; // 清空旧内容

            const titleElem = document.createElement('h4');
            titleElem.textContent = listTitleText;
            parentElement.appendChild(titleElem);

            if (listArray.length === 0) {
                const emptyMsg = document.createElement('p');
                emptyMsg.textContent = '列表为空';
                emptyMsg.className = 'panel-empty-msg';
                parentElement.appendChild(emptyMsg);
                return;
            }

            const ul = document.createElement('ul');
            listArray.forEach((pattern, index) => {
                const li = document.createElement('li');
                const patternText = document.createElement('span');
                patternText.textContent = pattern;
                const deleteBtn = document.createElement('button');
                deleteBtn.innerHTML = '×'; // 使用 HTML实体 "×"
                deleteBtn.className = 'delete-btn';
                deleteBtn.title = '删除此条规则';

                deleteBtn.onclick = function() {
                    if (confirm(`确定要从${listType === 'white' ? '白' : '黑'}名单中删除 "${pattern}" 吗?`)) {
                        if (listType === 'white') {
                            userWhitelist.splice(index, 1);
                            GM_setValue(CONFIG_KEY_WHITELIST, userWhitelist.join(','));
                        } else {
                            userBlacklist.splice(index, 1);
                            GM_setValue(CONFIG_KEY_BLACKLIST, userBlacklist.join(','));
                        }
                        // loadConfig(); // loadConfig 会从存储重载,这里直接重渲染即可
                        renderListsUI();
                        // console.log(`${listType === 'white' ? '白' : '黑'}名单已更新,删除了: ${pattern}`);
                    }
                };
                li.appendChild(patternText);
                li.appendChild(deleteBtn);
                ul.appendChild(li);
            });
            parentElement.appendChild(ul);
        };

        renderList(userWhitelist, 'openInNewTab-whitelist-section', '白名单 (强制启用):', 'white');
        renderList(userBlacklist, 'openInNewTab-blacklist-section', '黑名单 (禁用功能):', 'black');
    }

    function togglePanelVisibility() {
        panelVisible = !panelVisible;
        GM_setValue(CONFIG_KEY_PANEL_VISIBLE, panelVisible);
        if (panelVisible) {
            if (!uiPanel || !document.body.contains(uiPanel)) {
                createUIPanel();
            } else {
                renderListsUI(); // 确保内容是最新的
                uiPanel.style.display = 'block';
            }
        } else {
            if (uiPanel) {
                uiPanel.style.display = 'none';
            }
        }
    }

    function loadConfig() {
        const storedWhitelistStr = GM_getValue(CONFIG_KEY_WHITELIST);
        const storedBlacklistStr = GM_getValue(CONFIG_KEY_BLACKLIST);

        if (typeof storedWhitelistStr === 'string' && storedWhitelistStr.trim() !== '') {
            userWhitelist = storedWhitelistStr.split(',').map(s => s.trim()).filter(s => s);
        } else if (typeof storedWhitelistStr === 'undefined') {
            userWhitelist = [...defaultWhitelistPatterns]; // 使用副本
            GM_setValue(CONFIG_KEY_WHITELIST, userWhitelist.join(','));
        } else {
            userWhitelist = [];
        }

        if (typeof storedBlacklistStr === 'string' && storedBlacklistStr.trim() !== '') {
            userBlacklist = storedBlacklistStr.split(',').map(s => s.trim()).filter(s => s);
        } else if (typeof storedBlacklistStr === 'undefined') {
            userBlacklist = [...defaultBlacklistPatterns]; // 使用副本
            GM_setValue(CONFIG_KEY_BLACKLIST, userBlacklist.join(','));
        } else {
            userBlacklist = [];
        }

        // 如果面板已创建且可见,则在加载配置后刷新它
        if (uiPanel && panelVisible && document.body.contains(uiPanel)) {
            renderListsUI();
        }
        // console.log("左键新标签页配置已加载 - 白名单:", userWhitelist, "黑名单:", userBlacklist);
    }

    function urlMatchesPattern(url, pattern) {
        if (!pattern || !url) return false;
        let currentUrl = url;
        let p = pattern.trim();
        if (!p.includes("://")) {
            currentUrl = currentUrl.replace(/^https?:\/\//, "");
        }
        if (!p.startsWith("www.") && !p.includes("://") && currentUrl.startsWith("www.")) {
            currentUrl = currentUrl.replace(/^www\./, "");
        }
        const escapedPattern = p.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
        const regexString = '^' + escapedPattern.replace(/\\\*/g, '.*') + '$';
        try {
            return new RegExp(regexString, 'i').test(currentUrl);
        } catch (e) { return false; }
    }

    if (typeof GM_registerMenuCommand === 'function' && typeof GM_getValue === 'function' && typeof GM_setValue === 'function') {
        GM_registerMenuCommand('配置“新标签页打开”白名单 (强制启用)', function() {
            const currentWhitelistStr = GM_getValue(CONFIG_KEY_WHITELIST, userWhitelist.join(','));
            const newWhitelist = prompt(
                '请输入网站白名单模式 (强制在此列表网站启用“新标签页打开”功能),用英文逗号 "," 分隔:\n例如: my-allowed-site.com/*, another.com/specific-path/*',
                currentWhitelistStr
            );
            if (newWhitelist !== null) {
                userWhitelist = newWhitelist.split(',').map(s => s.trim()).filter(s => s);
                GM_setValue(CONFIG_KEY_WHITELIST, userWhitelist.join(','));
                alert('白名单已更新!');
                loadConfig(); // 重新加载并刷新UI(如果可见)
            }
        }, 'W');

        GM_registerMenuCommand('配置“新标签页打开”黑名单 (禁用功能)', function() {
            const currentBlacklistStr = GM_getValue(CONFIG_KEY_BLACKLIST, userBlacklist.join(','));
            const newBlacklist = prompt(
                '请输入网站黑名单模式 (在这些网站禁用“新标签页打开”功能),用英文逗号 "," 分隔:\n例如: excluded-site.com/*, another.com/general-area/*',
                currentBlacklistStr
            );
            if (newBlacklist !== null) {
                userBlacklist = newBlacklist.split(',').map(s => s.trim()).filter(s => s);
                GM_setValue(CONFIG_KEY_BLACKLIST, userBlacklist.join(','));
                alert('黑名单已更新!');
                loadConfig();
            }
        }, 'B');

        GM_registerMenuCommand('显示/隐藏“新标签页打开”规则面板', togglePanelVisibility, 'P');
    }

    // --- 主逻辑开始 ---
    loadConfig(); // 首先加载配置

    // 根据保存的状态决定是否初始显示面板
    // 这需要在DOM基本可用后执行,以确保document.body存在
    const initPanel = () => {
        if (GM_getValue(CONFIG_KEY_PANEL_VISIBLE, false) === true) {
            // panelVisible 初始为 false, togglePanelVisibility 会将其设为 true 并显示
            togglePanelVisibility();
        }
    };

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", initPanel);
    } else {
        initPanel(); // DOM 已加载
    }


    document.addEventListener('click', function(event) {
        const currentPageUrl = window.location.href;
        let scriptShouldRunOnThisPage = true;

        let whitelisted = false;
        for (const pattern of userWhitelist) {
            if (urlMatchesPattern(currentPageUrl, pattern)) {
                whitelisted = true; break;
            }
        }

        if (whitelisted) {
            scriptShouldRunOnThisPage = true;
        } else {
            let blacklisted = false;
            for (const pattern of userBlacklist) {
                if (urlMatchesPattern(currentPageUrl, pattern)) {
                    blacklisted = true; break;
                }
            }
            scriptShouldRunOnThisPage = !blacklisted;
        }

        if (!scriptShouldRunOnThisPage) return;

        if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;

        let targetElement = event.target;
        let anchorElement = null;
        for (let i = 0; i < 5 && targetElement && targetElement !== document.body; i++) {
            if (targetElement.tagName === 'A') {
                anchorElement = targetElement; break;
            }
            targetElement = targetElement.parentElement;
        }

        if (anchorElement) {
            const href = anchorElement.href;
            const rawHref = anchorElement.getAttribute('href');
            if (!href || (rawHref && rawHref.startsWith('#')) || href.startsWith('javascript:')) return;
            if (anchorElement.hasAttribute('download')) return;

            event.preventDefault();
            event.stopPropagation();

            if (typeof GM_openInTab === 'function') {
                GM_openInTab(href, { active: true, insert: true });
            } else if (typeof window.open === 'function') {
                window.open(href, '_blank');
            } else {
                console.warn('无法在新标签页中打开链接:GM_openInTab 和 window.open 均不可用。');
            }
        }
    }, true);

})();