// ==UserScript==
// @name Open all links in selection
// @namespace https://github.com/chirag127/userscripts
// @version 0.1.3
// @description Tampermonkey menu command — opens every link found in the current text selection in new tabs. Catches both elements AND plain-text URLs (http://, https://, www.).
// @author chirag127
// @match *://*/*
// @run-at document-end
// @grant GM_registerMenuCommand
// @license MIT
// @homepageURL https://github.com/chirag127/userscripts/tree/main/open-links-in-selection
// @supportURL https://github.com/chirag127/userscripts/issues
// @updateURL https://raw.githubusercontent.com/chirag127/userscripts/main/open-links-in-selection/open-links-in-selection.user.js
// @downloadURL https://raw.githubusercontent.com/chirag127/userscripts/main/open-links-in-selection/open-links-in-selection.user.js
// ==/UserScript==
(() => {
'use strict'
const CONFIRM_THRESHOLD = 5
// Matches http://, https://, and bare www.* URLs. Allows the common URL char set
// and stops at whitespace, common surrounding punctuation, and quotes/brackets.
const URL_RE = /\b((?:https?:\/\/|www\.)[^\s<>"'`()\[\]{},]+[^\s<>"'`()\[\]{},.;:!?])/gi
function collectLinksFromSelection() {
const sel = window.getSelection()
if (!sel || sel.isCollapsed || sel.rangeCount === 0) return []
const found = new Set()
// 1. Plain-text URLs inside the selection's stringified content.
const text = sel.toString()
if (text) {
for (const m of text.matchAll(URL_RE)) {
let u = m[1]
if (u.startsWith('www.')) u = 'https://' + u
found.add(u)
}
}
// 2. elements that intersect the selection range.
// Walk every range (selections can have multiple under some browsers / textarea cases)
// and collect any anchor whose own range intersects.
const activeEl = document.activeElement
const inTextarea =
activeEl && (activeEl.tagName === 'TEXTAREA' || (activeEl.tagName === 'INPUT' && activeEl.type === 'text'))
if (!inTextarea) {
for (let i = 0; i < sel.rangeCount; i++) {
const range = sel.getRangeAt(i)
// Find anchors whose own range overlaps this selection range
const root = range.commonAncestorContainer
const rootEl = root.nodeType === Node.ELEMENT_NODE ? root : root.parentElement
if (!rootEl) continue
const anchors = rootEl.querySelectorAll('a[href]')
for (const a of anchors) {
if (!range.intersectsNode(a)) continue
const href = a.href
if (!href) continue
// Skip javascript:, mailto:, tel:, hash-only anchors
if (/^(javascript:|mailto:|tel:|#)/i.test(href)) continue
found.add(href)
}
// Also: if the selection is INSIDE a single anchor (selectstart inside ),
// its anchor may not be picked up by querySelectorAll on commonAncestorContainer.
// Check ancestors of both endpoints.
for (const node of [range.startContainer, range.endContainer]) {
let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement
while (el && el !== document.body) {
if (el.tagName === 'A' && el.href && !/^(javascript:|mailto:|tel:|#)/i.test(el.href)) {
found.add(el.href)
break
}
el = el.parentElement
}
}
}
}
return [...found]
}
function openAll(urls) {
if (urls.length === 0) {
alert('Open all links: no links found in selection.')
return
}
if (urls.length >= CONFIRM_THRESHOLD) {
if (!confirm(`Open ${urls.length} tabs?`)) return
}
// Open with a tiny stagger to dodge naive popup blockers.
let i = 0
const tick = () => {
if (i >= urls.length) return
const url = urls[i++]
try {
window.open(url, '_blank', 'noopener,noreferrer')
} catch (e) {
console.error('[open-links-in-selection] failed to open', url, e)
}
setTimeout(tick, 0)
}
tick()
}
function run() {
const urls = collectLinksFromSelection()
openAll(urls)
}
// Register a Tampermonkey menu command.
if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand('Open all links in selection', run, { accessKey: 'o' })
}
// Fallback for engines without the GM API: expose a global so a user-defined
// keybind can still call it from devtools.
// @ts-ignore — userscript global
window.__openLinksInSelection = run
})()