// ==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()
}
})()