Last active
April 26, 2026 19:12
-
-
Save scarlac/a1d00eece75c7f199bbc906a6269ac52 to your computer and use it in GitHub Desktop.
UserScript for forcing links to open in the same tab
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Force links to open in same window | |
| // @namespace https://github.com/scarlac | |
| // @version 1.1.0 | |
| // @description Rewrite target="_blank" (and similar) on links, areas, forms, and base tags so navigation stays in the current window/tab. Does not touch window.open or other JS APIs. | |
| // @author scarlac | |
| // @match *://*/* | |
| // @run-at document-start | |
| // @grant none | |
| // @noframes | |
| // ==/UserScript== | |
| /** | |
| * This script was written to address the user-hostile practice | |
| * where many websites decide that you MUST open a new tab, as if you had no capacity to manage it yourself. | |
| * The modern equivalent of pop-ups from the 90'ies. | |
| * And since Chrome does not seem to have a native feature to disable this, you end up with a million mental threads. | |
| * The Observer is necessary for sites like React. It does not fix all links as some sites use window.open and other dynamic tricks. | |
| * | |
| * Tested with TamperMonkey for Chrome. | |
| **/ | |
| (function () { | |
| 'use strict'; | |
| const BAD_TARGETS = new Set(['_blank', '_new', 'blank', 'new']); | |
| const SELECTOR = 'a[target], area[target], form[target], base[target]'; | |
| function fixTarget(el) { | |
| if (!el || !el.getAttribute) return; | |
| const t = el.getAttribute('target'); | |
| if (t && BAD_TARGETS.has(t.toLowerCase())) { | |
| el.setAttribute('target', '_self'); | |
| } | |
| } | |
| function scan(root) { | |
| if (!root || !root.querySelectorAll) return; | |
| root.querySelectorAll(SELECTOR).forEach(fixTarget); | |
| } | |
| // Initial sweep as soon as the DOM is parseable. | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => scan(document), { once: true }); | |
| } else { | |
| scan(document); | |
| } | |
| // Watch for dynamically added/changed elements (SPAs, infinite scroll, etc.). | |
| const mo = new MutationObserver((mutations) => { | |
| for (const m of mutations) { | |
| if (m.type === 'attributes') { | |
| fixTarget(m.target); | |
| } else { | |
| m.addedNodes.forEach((node) => { | |
| if (node.nodeType !== 1) return; // Element nodes only | |
| fixTarget(node); | |
| scan(node); | |
| }); | |
| } | |
| } | |
| }); | |
| const startObserver = () => { | |
| mo.observe(document.documentElement || document, { | |
| subtree: true, | |
| childList: true, | |
| attributes: true, | |
| attributeFilter: ['target'], | |
| }); | |
| }; | |
| if (document.documentElement) { | |
| startObserver(); | |
| } else { | |
| document.addEventListener('readystatechange', startObserver, { once: true }); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment