// ==UserScript== // @name Snap Links Mod-修复版 // @description 从网页中批量复制、打开链接,选择复选框 // @name:en Snap Links Mod // @description:en snap Links(open, copy), radios, chenkboxs, images from website // @author Griever, ywzhaiqi, lastdream2013, Hanchy Hill // @namespace http://minhill.com/ // @homepageURL https://greasyfork.org/en/scripts/25051/ // @include http* // @include https* // @version 2026.03.28 // @license The MIT License (MIT); http://opensource.org/licenses/MIT // @grant GM_getValue // @grant GM_setValue // @grant GM_openInTab // @grant GM_deleteValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant GM_log // @compatible firefox // @compatible chrome // @compatible edge // @icon http://minhill.com/blog/wp-content/uploads/2012/03/favicon.ico // @note 2025/03/28 添加开关控制,修复菜单显示和重复使用问题 // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/571521/Snap%20Links%20Mod-%E4%BF%AE%E5%A4%8D%E7%89%88.user.js // @updateURL https://update.greasyfork.icu/scripts/571521/Snap%20Links%20Mod-%E4%BF%AE%E5%A4%8D%E7%89%88.meta.js // ==/UserScript== // 开关状态 var scriptEnabled = false; // 从存储中读取开关状态 scriptEnabled = GM_getValue("enabled", false); var snapLinks = { timer: null, button: 0, active: false, // 是否已激活 inited: false, // 是否已初始化菜单DOM init: function() { // 如果开关未开启,不激活 if (!scriptEnabled) return; // 防止重复初始化 if (snapLinks.active) return; this.win = window; this.doc = this.win.document; this.body = this.doc.body; if (!this.body) { console.log("Can not snap links."); return false; } this.root = snapLinks.doc.documentElement; this.popup = document.getElementById("snapLinksMenupopup"); // 保存原始光标样式 this.bodyCursor = this.body.style.cursor; this.rootCursor = this.root.style.cursor; this.body.style.setProperty("cursor", "crosshair", "important"); this.root.style.setProperty("cursor", "crosshair", "important"); this.highlights = []; this.elements = []; // 添加事件监听 this.doc.addEventListener("mousedown", snapLinks.handleEvent, true); this.doc.addEventListener("pagehide", snapLinks.handleEvent, true); snapLinks.active = true; }, uninit: function() { if (!snapLinks.active) return; snapLinks.doc.removeEventListener("mousedown", snapLinks.handleEvent, true); snapLinks.doc.removeEventListener("mousemove", snapLinks.handleEvent, true); snapLinks.doc.removeEventListener("pagehide", snapLinks.handleEvent, true); snapLinks.doc.removeEventListener("mouseup", snapLinks.handleEvent, true); setTimeout(function(self){ if (self.doc) self.doc.removeEventListener("click", snapLinks.handleEvent, true); }, 10, snapLinks); // 移除选框 if (snapLinks.box && snapLinks.box.parentNode) snapLinks.box.parentNode.removeChild(snapLinks.box); snapLinks.box = null; // 恢复光标 if (this.body) this.body.style.cursor = this.bodyCursor; if (this.root) this.root.style.cursor = this.rootCursor; snapLinks.active = false; }, deactivate: function() { // 完全停用,清理所有,不再自动激活 snapLinks.uninit(); snapLinks.lowlightAll(); // 确保菜单隐藏 var sslpop = document.getElementById("snapLinksMenupopup"); if (sslpop) { sslpop.classList.remove("trigger_popup"); sslpop.classList.add("hidden_popup"); } document.removeEventListener("click", snapLinks.destroy, false); snapLinks.active = false; }, destroy: function() { snapLinks.uninit(); snapLinks.lowlightAll(); var sslpop = document.getElementById("snapLinksMenupopup"); if (sslpop) { sslpop.classList.remove("trigger_popup"); sslpop.classList.add("hidden_popup"); } document.removeEventListener("click", snapLinks.destroy, false); // 如果开关开启,重新激活 if (scriptEnabled) { snapLinks.init(); } }, handleEvent: function(event) { switch(event.type){ case "mousedown": // 仅响应左键,且无修饰键 if (event.button != 0 || event.ctrlKey || event.shiftKey || event.altKey) return; event.preventDefault(); event.stopPropagation(); // 重置状态 snapLinks.elements = []; snapLinks.draw(event); break; case "mousemove": event.preventDefault(); event.stopPropagation(); var moveX = event.pageX; var moveY = event.pageY; if (snapLinks.downX > moveX) snapLinks.box.style.left = moveX + "px"; if (snapLinks.downY > moveY) snapLinks.box.style.top = moveY + "px"; snapLinks.box.style.width = Math.abs(moveX - snapLinks.downX) + "px"; snapLinks.box.style.height = Math.abs(moveY - snapLinks.downY) + "px"; if (snapLinks.timer) { clearTimeout(snapLinks.timer); snapLinks.timer = null; } var timeStamp = new Date().getTime(); if (timeStamp - snapLinks.lastHiglightedTime > 150) { snapLinks.boxRect = snapLinks.box.getBoundingClientRect(); snapLinks.highlightAll(); } else { var self = snapLinks; snapLinks.timer = setTimeout(function() { if (self.box) { self.boxRect = self.box.getBoundingClientRect(); self.highlightAll(); } }, 200); } break; case "mouseup": if (event.button != snapLinks.button || event.ctrlKey || event.shiftKey) return; event.preventDefault(); event.stopPropagation(); if (snapLinks.timer) { clearTimeout(snapLinks.timer); snapLinks.timer = null; } if (snapLinks.box) { snapLinks.boxRect = snapLinks.box.getBoundingClientRect(); snapLinks.highlightAll(); } // 收集高亮元素 for (let e of snapLinks.highlights) { if (e instanceof HTMLImageElement) { let link = snapLinks.doc.evaluate( 'ancestor::*[@href]', e, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (link && snapLinks.highlights.indexOf(link) === -1) { snapLinks.elements.push(link); } continue; } snapLinks.elements.push(e); } snapLinks.uninit(); // 如果有选中元素则显示菜单,否则直接重新激活 if (snapLinks.elements.length > 0) { snapLinks.showPopup(event); } else { snapLinks.lowlightAll(); if (scriptEnabled) snapLinks.init(); // 无元素时且开关开启则重新激活 } break; case "click": event.preventDefault(); event.stopPropagation(); break; case "pagehide": snapLinks.destroy(); break; } }, draw: function(aEvent) { this.lastHiglightedTime = new Date().getTime(); this.downX = aEvent.pageX; this.downY = aEvent.pageY; this.box = this.doc.createElement("div"); this.box.id = "snap-links-box"; this.box.style.cssText = [ 'background-color: rgba(0,128,255,.1) !important;', 'border: 1px solid rgb(255,255,0) !important;', 'box-sizing: border-box !important;', '-moz-box-sizing: border-box !important;', 'position: absolute !important;', 'z-index: 2147483647 !important;', 'top:' + this.downY + 'px;', 'left:' + this.downX + 'px;', 'cursor: crosshair !important;', 'margin: 0px !important;', 'padding: 0px !important;', 'outline: none !important;', ].join(" "); this.body.appendChild(this.box); this.doc.removeEventListener("mousedown", this.handleEvent, true); this.doc.addEventListener("mousemove", this.handleEvent, true); this.doc.addEventListener("mouseup", this.handleEvent, true); this.doc.addEventListener("click", this.handleEvent, true); }, highlightAll: function() { var a = '[href]:not([href^="javascript:"]):not([href^="mailto:"]):not([href^="#"])'; var selector = a + ', ' + a + ' img, input[type="checkbox"], input[type="radio"]'; selector += ', a.b-in-blk.input-cbx[href^="javascript:"]'; var contains = this.getContainsElements(); contains.reverse(); var matches = []; for (let e of contains) { if (e.nodeType !== 1 || !e.matches(selector)) continue; if (e.hasAttribute('href')) { let imgs = Array.prototype.slice.call(e.getElementsByTagName('img')); if (imgs[0]) { [].push.apply(contains, imgs); continue; } } if (!("defStyle" in e)) this.highlight(e); matches.push(e); } this.highlights.forEach(function(e, i, a){ if (matches.indexOf(e) === -1) this.lowlight(e); }, this); this.highlights = matches; this.lastHiglightedTime = new Date().getTime(); }, lowlightAll: function() { this.highlights.forEach(function(e){ this.lowlight(e); }, this); this.highlights = []; }, highlight: function(elem) { if (!('defStyle' in elem)) elem.defStyle = elem.getAttribute('style'); elem.style.setProperty('outline', '2px solid #ff0000', null); elem.style.setProperty('outline-offset', '-1px', null); }, lowlight: function(elem) { if ("defStyle" in elem) { elem.defStyle ? elem.style.cssText = elem.defStyle : elem.removeAttribute("style"); delete elem.defStyle; } }, getContainsElements: function() { if (!this.boxRect) return []; var a = '[href]:not([href^="javascript:"]):not([href^="mailto:"]):not([href^="#"])'; var selector = a + ', ' + a + ' img, input[type="checkbox"], input[type="radio"]'; selector += ', a.b-in-blk.input-cbx[href^="javascript:"]'; var nodes = document.querySelectorAll(selector); var arraynode = []; for (let i = 0; i < nodes.length; i++) { if(this.inSelect(nodes[i])) arraynode.push(nodes[i]); } return arraynode; }, inSelect: function(node) { var boxPos = snapLinks.boxRect; var xmin = boxPos.left, xmax = boxPos.right, ymin = boxPos.top, ymax = boxPos.bottom; var pos = this.getOffset(node); var left = pos.x, right = pos.x + pos.width; var top = pos.y, bottom = pos.y + pos.height; var xOverlap = (left <= xmax && right >= xmin); var yOverlap = (top <= ymax && bottom >= ymin); return xOverlap && yOverlap; }, getOffset: function(node) { var rect = node.getBoundingClientRect(); return { x: rect.left, y: rect.top, width: rect.width, height: rect.height }; }, showPopup: function(aEvent) { var cls = []; var linkcount = 0; var specialLinkCount = 0; var imagecount = 0; var checkboxcount = 0; var radiocount = 0; for (let elem of this.elements) { if (elem instanceof HTMLAnchorElement) { if (elem.href.indexOf('javascript:') == 0) specialLinkCount++; else linkcount++; } } for (let elem of this.elements) { if (elem instanceof HTMLAnchorElement && /\.(jpe?g|png|gif|bmp)$/i.test(elem.href)) imagecount++; } for (let elem of this.elements) { if (elem instanceof HTMLInputElement && elem.type === 'checkbox') checkboxcount++; } for (let elem of this.elements) { if (elem instanceof HTMLInputElement && elem.type === 'radio') radiocount++; } if (linkcount > 0) cls.push("hasLink"); if (imagecount > 0) cls.push("hasImageLink"); if (checkboxcount > 0) cls.push("hasCheckbox"); if (radiocount > 0) cls.push("hasRadio"); if (specialLinkCount > 0) cls.push("hasSpecialLink"); var setCount = function(id, label){ let currentEntry = document.getElementById(id); if(currentEntry) currentEntry.innerHTML = label; }; var data = { "SnapLinksOpenLinks": "在新标签打开所有链接 (" + linkcount + ")", "SnapLinksCopyLinks": "复制所有链接URL (" + linkcount + ")", "SnapLinksCopyLinksReverse": "复制所有链接URL (" + linkcount + ") (反向)", "SnapLinksCopyLinksAndTitles": "复制所有链接标题 + URL (" + linkcount + ")", "SnapLinksCopyLinksAndTitlesMD": "复制所有链接标题 + URL (" + linkcount + ") (MD)", "SnapLinksCopyLinksAndTitlesBBS": "复制所有链接标题 + URL (" + linkcount + ") (BBS)", "SnapLinksCopyLinksRegExp": "复制所有链接标题 + URL (" + linkcount + ") (筛选)", "SnapLinksCopyLinksSetFormat": "复制所有链接标题 + URL (" + linkcount + ") (设置复制格式)", "SnapLinksOpenImageLinks": "在新标签页打开所有图片链接 (" + imagecount + ")", "SnapLinksImageLinksOnePage": "在一个标签页显示所有图片链接 (" + imagecount + ")", "SnapLinksCheckBoxSelect": "复选框 - 选中 (" + checkboxcount + ")", "SnapLinksCheckBoxCancel": "复选框 - 取消 (" + checkboxcount + ")", "SnapLinksCheckBoxTaggle": "复选框 - 反选 (" + checkboxcount + ")", "SnapLinksRadioSelect": "单选框 - 选中 (" + radiocount + ")", "SnapLinksRadioCancel": "单选框 - 取消 (" + radiocount + ")", "SnapLinksClickLinks": "特殊单选框 - 选中 (" + specialLinkCount + ")", }; for(let id in data){ setCount(id, data[id]); } var setStyleNode = function(showList){ var setList = ["hasLink","hasImageLink","hasCheckbox","hasRadio","hasSpecialLink"]; setList.forEach(function(elist){ let elements = document.getElementsByClassName(elist); if(elements){ for(var i=0; i 0) { setStyleNode(cls); this.openPopupAtScreen(aEvent.clientX, aEvent.clientY); } else { this.lowlightAll(); if (scriptEnabled) this.init(); // 无有效菜单项时且开关开启则重新激活 } }, openPopupAtScreen: function(clientX, clientY) { var popMenu = document.getElementById("snapLinksMenupopup"); if (!popMenu) return; // 先临时显示菜单以获取真实尺寸 popMenu.classList.remove("hidden_popup"); popMenu.classList.add("temp_show"); popMenu.style.visibility = "hidden"; popMenu.style.display = "block"; var menuWidth = popMenu.offsetWidth; var menuHeight = popMenu.offsetHeight; // 计算最佳位置(优先右下角,超出则调整) var left = clientX + 5; var top = clientY + 5; var viewportWidth = window.innerWidth; var viewportHeight = window.innerHeight; if (left + menuWidth > viewportWidth) { left = clientX - menuWidth - 5; } if (top + menuHeight > viewportHeight) { top = clientY - menuHeight - 5; } // 确保不超出左/上边界 left = Math.max(5, left); top = Math.max(5, top); // 设置最终样式 popMenu.style.left = left + "px"; popMenu.style.top = top + "px"; popMenu.style.visibility = "visible"; popMenu.classList.remove("temp_show"); popMenu.classList.add("trigger_popup"); // 添加销毁监听 document.addEventListener("click", snapLinks.destroy, false); }, openLinks: function(regexp) { var obj = {}; for (let elem of this.elements) { if (!elem.href || /^(?:javascript:|mailto:|#)/i.test(elem.href)) continue; if (!regexp || regexp.test(elem.href)) obj[elem.href] = true; } for (let key in obj) { GM_openInTab(key); } }, clickLinks: function() { for (let elem of this.elements) { if (!elem.href || /^(?:javascript:|mailto:|#)/i.test(elem.href)) { elem.click(); } } }, copyLinks: function(regexp, reverse, format) { var links = this.elements.filter(function(elem){ return elem instanceof HTMLAnchorElement && (!regexp || regexp.test(elem.href)) }); var num = 1, numReverse = links.length; links = links.map(function(e) { if (format) { return format.replace(/%t/g, e.textContent) .replace(/%u/g, e.href) .replace(/%r/g, numReverse--) .replace(/%n/g, num++); } return e.href; }); links = snapLinks.unique(links); if(reverse) links = links.reverse(); if (links.length){ GM_setClipboard(links.join('\n')); } }, imageOnePage: function() { var htmlsrc = [ '' ].join(''); for (let elem of this.elements) { if (elem instanceof HTMLAnchorElement && /\.(jpe?g|png|gif|bmp)$/i.test(elem.href)) htmlsrc += '\n' } GM_openInTab("data:text/html;charset=utf-8," + '' + snapLinks.doc.domain + ' 图象列表' + encodeURIComponent(htmlsrc)); }, checkbox: function(bool) { for (let elem of this.elements) { if (elem instanceof HTMLInputElement && elem.type === 'checkbox') { elem.checked = arguments.length == 0 ? !elem.checked : bool; } } }, radio: function(bool) { for (let elem of this.elements) { if (elem instanceof HTMLInputElement && elem.type === 'radio') { elem.checked = arguments.length == 0 ? !elem.checked : bool; } } }, unique: function(a) { var o = {}, r = [], t; for (var i = 0, l = a.length; i < l; i++) { t = a[i]; if(!o[t]){ o[t] = true; r.push(t); } } return r; } }; // 切换开关状态 function toggleScript() { scriptEnabled = !scriptEnabled; GM_setValue("enabled", scriptEnabled); if (scriptEnabled) { // 开启:激活脚本 snapLinks.init(); } else { // 关闭:停用脚本,清理所有状态 snapLinks.deactivate(); } // 更新菜单项名称(可选) // 由于GM_registerMenuCommand不支持动态修改,这里不实现 } // 创建菜单项 GM_registerMenuCommand(scriptEnabled ? "禁用 Snap Links" : "启用 Snap Links", toggleScript); function begin() { // 避免重复创建菜单 if (document.getElementById("snapLinksMenupopup")) return; var ibody = document.getElementsByTagName("body")[0]; if (!ibody) return; var popup = document.createElement("div"); popup.setAttribute("id", "snapLinksMenupopup"); popup.setAttribute("class", "hidden_popup"); popup.innerHTML = '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
复选框 - 选中
' + '
复选框 - 取消
' + '
复选框 - 反选
' + '
单选框 - 选中
' + '
单选框 - 取消
' + '' + '
'; ibody.appendChild(popup); // 绑定菜单项事件 document.getElementById("SnapLinksOpenLinks").addEventListener("click", function(){ snapLinks.openLinks(); snapLinks.destroy(); }, false); document.getElementById("SnapLinksCopyLinks").addEventListener("click", function(){ snapLinks.copyLinks(); snapLinks.destroy(); }, false); document.getElementById("SnapLinksCopyLinksReverse").addEventListener("click", function(){ snapLinks.copyLinks(null, true); snapLinks.destroy(); }, false); document.getElementById("SnapLinksCopyLinksAndTitles").addEventListener("click", function(){ snapLinks.copyLinks(null, false, '%t\n%u'); snapLinks.destroy(); }, false); document.getElementById("SnapLinksCopyLinksAndTitlesMD").addEventListener("click", function(){ snapLinks.copyLinks(null, false, '[%t](%u)'); snapLinks.destroy(); }, false); document.getElementById("SnapLinksCopyLinksAndTitlesBBS").addEventListener("click", function(){ snapLinks.copyLinks(null, false, '[url=%u]%t[/url]'); snapLinks.destroy(); }, false); document.getElementById("SnapLinksCopyLinksRegExp").addEventListener("click", function(){ var reg=prompt('请输入需要筛选的 RegExp', ''); if(reg) snapLinks.copyLinks(new RegExp(reg)); snapLinks.destroy(); }, false); document.getElementById("SnapLinksOpenImageLinks").addEventListener("click", function(){ snapLinks.openLinks(/\.(jpe?g|png|gif|bmp)$/i); snapLinks.destroy(); }, false); document.getElementById("SnapLinksImageLinksOnePage").addEventListener("click", function(){ snapLinks.imageOnePage(); snapLinks.destroy(); }, false); document.getElementById("SnapLinksCheckBoxSelect").addEventListener("click", function(){ snapLinks.checkbox(true); snapLinks.destroy(); }, false); document.getElementById("SnapLinksCheckBoxCancel").addEventListener("click", function(){ snapLinks.checkbox(false); snapLinks.destroy(); }, false); document.getElementById("SnapLinksCheckBoxTaggle").addEventListener("click", function(){ snapLinks.checkbox(); snapLinks.destroy(); }, false); document.getElementById("SnapLinksRadioSelect").addEventListener("click", function(){ snapLinks.radio(true); snapLinks.destroy(); }, false); document.getElementById("SnapLinksRadioCancel").addEventListener("click", function(){ snapLinks.radio(false); snapLinks.destroy(); }, false); document.getElementById("SnapLinksClickLinks").addEventListener("click", function(){ snapLinks.clickLinks(); snapLinks.destroy(); }, false); GM_addStyle(` .hidden_popup { display: none !important; } .trigger_popup { display: block !important; position: fixed !important; z-index: 99999 !important; background-color: rgb(45,53,63) !important; border: 1px solid rgb(22,25,28) !important; border-radius: 4px !important; padding: 5px !important; cursor: pointer !important; box-shadow: 0 1px 0 rgba(162,184,204,0.25) inset, 0 0 4px hsla(0,0%,0%,0.95) !important; } .trigger_popup div { color: white !important; padding: 2px 8px !important; white-space: nowrap !important; } .trigger_popup > div > div:hover { color: rgb(51,159,255) !important; background-color: transparent !important; background-image: linear-gradient(to bottom, rgb(37,46,54), rgb(36,40,45)) !important; } .temp_show { display: block !important; position: fixed !important; visibility: hidden !important; } `); } begin(); // 根据开关状态决定是否初始化 if (scriptEnabled) { snapLinks.init(); }