Open In New Tab

Force all links to open in new tab using whitelist mode

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Open In New Tab
// @namespace    https://github.com/xiaowulang-turbo/OpenInNewTab
// @version      1.1.5
// @description  Force all links to open in new tab using whitelist mode
// @author       Xiaowu
// @match        *://*/*
// @updateUrl    https://github.com/xiaowulang-turbo/OpenInNewTab/blob/main/userscript/OpenInNewTab.user.js
// @license      MIT
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==

;(function () {
    "use strict"

    /**
     * Default whitelisted domains
     * These are the initial domains that will be included
     */
    const DEFAULT_DOMAINS = []

    /**
     * Get user whitelist from storage
     * @returns {Array} Array of whitelisted domains
     */
    function getUserWhitelist() {
        const stored = GM_getValue("userWhitelist", [])
        return Array.isArray(stored) ? stored : DEFAULT_DOMAINS
    }

    /**
     * Save user whitelist to storage
     * @param {Array} domains Array of domains to save
     */
    function saveUserWhitelist(domains) {
        GM_setValue("userWhitelist", domains)
    }

    /**
     * Check if current domain is in whitelist
     * @returns {boolean} True if domain is whitelisted
     */
    function isWhitelisted() {
        const currentDomain = window.location.hostname
        const userWhitelist = getUserWhitelist()
        return userWhitelist.some(
            (domain) =>
                currentDomain === domain || currentDomain.endsWith("." + domain)
        )
    }

    /**
     * Add current domain to whitelist
     */
    function addCurrentDomainToWhitelist() {
        const currentDomain = window.location.hostname
        const userWhitelist = getUserWhitelist()
        const lang = detectLanguage()

        if (!userWhitelist.includes(currentDomain)) {
            userWhitelist.push(currentDomain)
            saveUserWhitelist(userWhitelist)
            alert(`${currentDomain} ${getText("addedToWhitelist", lang)}`)
        } else {
            alert(`${currentDomain} ${getText("alreadyInWhitelist", lang)}`)
        }
    }

    /**
     * Detect browser language setting
     * @returns {string} Language code ('en' or 'zh')
     */
    function detectLanguage() {
        const userLang = navigator.language || navigator.userLanguage || "en"
        return userLang.startsWith("zh") ? "zh" : "en"
    }

    /**
     * Language resources for internationalization
     */
    const languageResources = {
        en: {
            modalTitle: "Whitelist Management",
            inputPlaceholder: "Enter domain, e.g., example.com",
            addButton: "Add",
            removeButton: "Remove",
            closeButton: "×",
            addedToWhitelist: "Added to whitelist!",
            alreadyInWhitelist: "Already in whitelist",
            removedFromWhitelist: "Removed from whitelist",
            noDomains: "No domains in whitelist",
            addToWhitelist: "Add to Whitelist",
            manageWhitelist: "Manage Whitelist",
        },
        zh: {
            modalTitle: "白名单管理",
            inputPlaceholder: "输入域名,如:example.com",
            addButton: "添加",
            removeButton: "移除",
            closeButton: "×",
            addedToWhitelist: "已添加到白名单!",
            alreadyInWhitelist: "已在白名单中",
            removedFromWhitelist: "已从白名单移除",
            noDomains: "白名单中没有域名",
            addToWhitelist: "添加白名单",
            manageWhitelist: "管理白名单",
        },
    }

    /**
     * Get text by language
     * @param {string} key Text key
     * @param {string} lang Language code
     * @returns {string} Localized text
     */
    function getText(key, lang = null) {
        const language = lang || detectLanguage()
        return (
            languageResources[language]?.[key] ||
            languageResources.en[key] ||
            key
        )
    }

    /**
     * Detect if the browser is in dark mode
     * @returns {boolean} True if in dark mode
     */
    function isDarkMode() {
        return (
            window.matchMedia &&
            window.matchMedia("(prefers-color-scheme: dark)").matches
        )
    }

    /**
     * Get CSS variables based on theme
     * @returns {Object} CSS color variables
     */
    function getThemeColors() {
        const isDark = isDarkMode()
        return {
            bgPrimary: isDark ? "#1a1a1a" : "#ffffff",
            bgSecondary: isDark ? "#2d2d2d" : "#f8f9fa",
            textPrimary: isDark ? "#ffffff" : "#333333",
            textSecondary: isDark ? "#cccccc" : "#666666",
            borderColor: isDark ? "#404040" : "#dddddd",
            shadowColor: isDark ? "rgba(0,0,0,0.5)" : "rgba(0,0,0,0.3)",
            inputBg: isDark ? "#333333" : "#ffffff",
            inputBorder: isDark ? "#555555" : "#dddddd",
            inputText: isDark ? "#ffffff" : "#333333",
        }
    }

    /**
     * Create whitelist management modal
     */
    function createWhitelistModal() {
        const lang = detectLanguage()
        const modal = document.createElement("div")
        modal.className = "openinnewtabs-modal"
        modal.innerHTML = `
            <div class="openinnewtabs-modal-content">
                <div class="openinnewtabs-modal-header">
                    <h3>${getText("modalTitle", lang)}</h3>
                    <button class="openinnewtabs-close">${getText(
                        "closeButton",
                        lang
                    )}</button>
                </div>
                <div class="openinnewtabs-modal-body">
                    <div class="openinnewtabs-input-group">
                        <input type="text" id="openinnewtabs-new-domain" placeholder="${getText(
                            "inputPlaceholder",
                            lang
                        )}">
                        <button id="openinnewtabs-add-domain">${getText(
                            "addButton",
                            lang
                        )}</button>
                    </div>
                    <div class="openinnewtabs-domains-list" id="openinnewtabs-domains-list">
                        <!-- Domains will be added here -->
                    </div>
                </div>
            </div>
        `

        const colors = getThemeColors()

        modal.style.cssText = `
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.6);
            z-index: 10000;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
                sans-serif;
        `

        const modalContent = modal.querySelector(".openinnewtabs-modal-content")
        modalContent.style.cssText = `
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: ${colors.bgPrimary};
            width: 90%;
            max-width: 500px;
            max-height: 80vh;
            overflow-y: auto;
            border-radius: 12px;
            box-shadow: 0 8px 32px ${colors.shadowColor};
            border: 1px solid ${colors.borderColor};
        `

        const header = modal.querySelector(".openinnewtabs-modal-header")
        header.style.cssText = `
            padding: 20px 24px;
            border-bottom: 1px solid ${colors.borderColor};
            display: flex;
            justify-content: space-between;
            align-items: center;
            background: ${colors.bgSecondary};
            border-radius: 12px 12px 0 0;
        `

        const headerTitle = header.querySelector("h3")
        headerTitle.style.cssText = `
            margin: 0;
            color: ${colors.textPrimary};
            font-size: 18px;
            font-weight: 600;
        `

        const closeBtn = modal.querySelector(".openinnewtabs-close")
        closeBtn.style.cssText = `
            background: none;
            border: none;
            font-size: 28px;
            cursor: pointer;
            color: ${colors.textSecondary};
            width: 32px;
            height: 32px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 50%;
            transition: all 0.2s ease;
        `

        const body = modal.querySelector(".openinnewtabs-modal-body")
        body.style.cssText = `
            padding: 24px;
            color: ${colors.textPrimary};
        `

        const inputGroup = modal.querySelector(".openinnewtabs-input-group")
        inputGroup.style.cssText = `
            display: flex;
            gap: 12px;
            margin-bottom: 24px;
        `

        const input = modal.querySelector("#openinnewtabs-new-domain")
        input.style.cssText = `
            flex: 1;
            padding: 12px 16px;
            border: 2px solid ${colors.inputBorder};
            border-radius: 8px;
            font-size: 14px;
            background: ${colors.inputBg};
            color: ${colors.inputText};
            outline: none;
            transition: border-color 0.2s ease;
        `

        const addBtn = modal.querySelector("#openinnewtabs-add-domain")
        addBtn.style.cssText = `
            padding: 12px 24px;
            background: linear-gradient(135deg, #4caf50, #45a049);
            color: white;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 500;
            transition: all 0.2s ease;
            box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
        `

        // Add hover effects
        closeBtn.addEventListener("mouseover", () => {
            closeBtn.style.background = isDarkMode() ? "#404040" : "#e9ecef"
            closeBtn.style.color = colors.textPrimary
        })

        closeBtn.addEventListener("mouseout", () => {
            closeBtn.style.background = "none"
            closeBtn.style.color = colors.textSecondary
        })

        addBtn.addEventListener("mouseover", () => {
            addBtn.style.transform = "translateY(-1px)"
            addBtn.style.boxShadow = "0 4px 12px rgba(76, 175, 80, 0.4)"
        })

        addBtn.addEventListener("mouseout", () => {
            addBtn.style.transform = "translateY(0)"
            addBtn.style.boxShadow = "0 2px 8px rgba(76, 175, 80, 0.3)"
        })

        input.addEventListener("focus", () => {
            input.style.borderColor = "#4caf50"
        })

        input.addEventListener("blur", () => {
            input.style.borderColor = colors.inputBorder
        })

        document.body.appendChild(modal)

        // Event listeners
        closeBtn.addEventListener("click", () => {
            modal.style.display = "none"
        })

        modal.addEventListener("click", (e) => {
            if (e.target === modal) {
                modal.style.display = "none"
            }
        })

        addBtn.addEventListener("click", () => {
            const input = modal.querySelector("#openinnewtabs-new-domain")
            const domain = input.value.trim()
            if (domain) {
                addDomainToWhitelist(domain)
                input.value = ""
                updateWhitelistDisplay()
            }
        })

        input.addEventListener("keypress", (e) => {
            if (e.key === "Enter") {
                addBtn.click()
            }
        })

        return modal
    }

    /**
     * Add domain to whitelist
     * @param {string} domain Domain to add
     */
    function addDomainToWhitelist(domain) {
        const userWhitelist = getUserWhitelist()
        const lang = detectLanguage()

        if (!userWhitelist.includes(domain)) {
            userWhitelist.push(domain)
            saveUserWhitelist(userWhitelist)
            alert(`${domain} ${getText("addedToWhitelist", lang)}`)
        } else {
            alert(`${domain} ${getText("alreadyInWhitelist", lang)}`)
        }
    }

    /**
     * Remove domain from whitelist
     * @param {string} domain Domain to remove
     */
    function removeDomainFromWhitelist(domain) {
        const userWhitelist = getUserWhitelist()
        const lang = detectLanguage()
        const index = userWhitelist.indexOf(domain)

        if (index > -1) {
            userWhitelist.splice(index, 1)
            saveUserWhitelist(userWhitelist)
            alert(`${domain} ${getText("removedFromWhitelist", lang)}`)
            updateWhitelistDisplay()
        }
    }

    /**
     * Update whitelist display in modal
     */
    function updateWhitelistDisplay() {
        const modal = document.querySelector(".openinnewtabs-modal")
        if (!modal) return

        const domainsList = modal.querySelector("#openinnewtabs-domains-list")
        const userWhitelist = getUserWhitelist()
        const colors = getThemeColors()
        const lang = detectLanguage()

        if (userWhitelist.length === 0) {
            domainsList.innerHTML = `
                <div style="
                    text-align: center;
                    color: ${colors.textSecondary};
                    font-size: 14px;
                    padding: 32px 16px;
                ">
                    ${getText("noDomains", lang)}
                </div>
            `
            return
        }

        domainsList.innerHTML = userWhitelist
            .map(
                (domain) => `
            <div class="openinnewtabs-domain-item" style="
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 16px;
                border: 1px solid ${colors.borderColor};
                margin-bottom: 8px;
                border-radius: 8px;
                background: ${colors.bgSecondary};
                transition: all 0.2s ease;
            ">
                <span style="
                    color: ${colors.textPrimary};
                    font-size: 14px;
                    font-weight: 500;
                    flex: 1;
                ">${domain}</span>
                <button class="openinnewtabs-remove-domain" data-domain="${domain}" style="
                    background: linear-gradient(135deg, #f44336, #d32f2f);
                    color: white;
                    border: none;
                    border-radius: 6px;
                    padding: 8px 16px;
                    cursor: pointer;
                    font-size: 12px;
                    font-weight: 500;
                    transition: all 0.2s ease;
                    box-shadow: 0 2px 8px rgba(244, 67, 54, 0.3);
                ">${getText("removeButton", lang)}</button>
            </div>
        `
            )
            .join("")

        // Add event listeners and hover effects for remove buttons
        domainsList
            .querySelectorAll(".openinnewtabs-remove-domain")
            .forEach((btn) => {
                btn.addEventListener("click", (e) => {
                    const domain = e.target.getAttribute("data-domain")
                    removeDomainFromWhitelist(domain)
                })

                btn.addEventListener("mouseover", () => {
                    btn.style.transform = "translateY(-1px)"
                    btn.style.boxShadow = "0 4px 12px rgba(244, 67, 54, 0.4)"
                })

                btn.addEventListener("mouseout", () => {
                    btn.style.transform = "translateY(0)"
                    btn.style.boxShadow = "0 2px 8px rgba(244, 67, 54, 0.3)"
                })
            })

        // Add hover effects for domain items
        domainsList
            .querySelectorAll(".openinnewtabs-domain-item")
            .forEach((item) => {
                item.addEventListener("mouseover", () => {
                    item.style.transform = "translateY(-1px)"
                    item.style.boxShadow = `0 4px 12px ${colors.shadowColor}`
                })

                item.addEventListener("mouseout", () => {
                    item.style.transform = "translateY(0)"
                    item.style.boxShadow = "none"
                })
            })
    }

    /**
     * Open whitelist management modal
     */
    function openWhitelistManager() {
        let modal = document.querySelector(".openinnewtabs-modal")
        if (!modal) {
            modal = createWhitelistModal()
        }
        modal.style.display = "block"
        updateWhitelistDisplay()
    }

    /**
     * Force all links to open in new tab
     */
    function forceNewTab() {
        if (!isWhitelisted()) {
            return
        }

        // Handle dynamically added elements
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        const links = node.querySelectorAll("a[href]")
                        links.forEach((link) => {
                            if (
                                !link.target &&
                                !link.hasAttribute("download")
                            ) {
                                link.target = "_blank"
                                link.rel = "noopener noreferrer"
                            }
                        })
                    }
                })
            })
        })

        // Observe the entire document for changes
        observer.observe(document.body, {
            childList: true,
            subtree: true,
        })

        // Handle existing links immediately
        document.querySelectorAll("a[href]").forEach((link) => {
            if (!link.target && !link.hasAttribute("download")) {
                link.target = "_blank"
                link.rel = "noopener noreferrer"
            }
        })
    }

    // Initialize when DOM is ready
    function initialize() {
        const lang = detectLanguage()

        // Register menu command for adding current domain to whitelist
        GM_registerMenuCommand(
            getText("addToWhitelist", lang),
            addCurrentDomainToWhitelist
        )

        // Register menu command for whitelist management
        GM_registerMenuCommand(
            getText("manageWhitelist", lang),
            openWhitelistManager
        )

        // Start forcing new tab
        forceNewTab()
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", initialize)
    } else {
        initialize()
    }
})()