// ==UserScript== // @name Email Copy Button for All Sites // @name:zh-CN 全站邮箱一键复制 // @namespace http://tampermonkey.net/ // @version 1.7 // @description Automatically detects email addresses on any webpage and adds a one-click copy button. Supports plain text emails and mailto links, including React/Next.js dynamically rendered pages and AI chat interfaces (e.g. Gumloop). // @description:zh-CN 在任意网页自动识别邮箱地址并添加一键复制按钮,支持纯文本邮箱和 mailto 链接,兼容 React/Next.js 动态渲染页面及 AI 聊天界面(如 Gumloop)。 // @author Nosy Swab // @match *://*/* // @exclude https://apps.sfc.hk/* // @run-at document-end // @grant none // @license MIT // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0Ij48cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHJ4PSIxMiIgZmlsbD0iIzI1NjNFQiIvPjxyZWN0IHg9IjEwIiB5PSIxOCIgd2lkdGg9IjM0IiBoZWlnaHQ9IjI0IiByeD0iMyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz48cG9seWxpbmUgcG9pbnRzPSIxMCwxOCAyNywzMiA0NCwxOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz48cmVjdCB4PSIzNiIgeT0iMzYiIHdpZHRoPSIxNiIgaGVpZ2h0PSIxOCIgcng9IjMiIGZpbGw9IiMxRDREQjgiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIvPjxyZWN0IHg9IjMxIiB5PSIzMSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE4IiByeD0iMyIgZmlsbD0iIzM3ODRGOCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+ // @downloadURL https://update.greasyfork.icu/scripts/569979/Email%20Copy%20Button%20for%20All%20Sites.user.js // @updateURL https://update.greasyfork.icu/scripts/569979/Email%20Copy%20Button%20for%20All%20Sites.meta.js // ==/UserScript== (function () { 'use strict'; if (location.hostname === 'apps.sfc.hk') return; var EMAIL_RE = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/; var EMAIL_RE_G = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g; // v1.7: Removed CODE and PRE from skip list to support AI chat output (e.g. Gumloop) var SKIP_TAGS = new Set([ 'SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'HEAD', 'IFRAME', 'BUTTON' ]); var pending = []; var scheduled = false; var MAX_WALK_PER_CALL = 2000; var MAX_PROCESS_PER_BATCH = 50; function makeBtn(email) { var btn = document.createElement('button'); btn.textContent = '\uD83D\uDCCB'; btn.title = email; btn.setAttribute('data-copy-btn', '1'); btn.style.cssText = 'display:inline-block;margin-left:5px;padding:0 5px;' + 'background:#00695c;color:#fff;border:none;border-radius:3px;' + 'cursor:pointer;font-size:11px;height:17px;line-height:17px;' + 'vertical-align:middle;font-family:sans-serif'; btn.addEventListener('click', function (e) { e.stopPropagation(); e.preventDefault(); function showSuccess() { btn.textContent = '\u2705'; setTimeout(function () { btn.textContent = '\uD83D\uDCCB'; }, 2000); } if (!navigator.clipboard || !navigator.clipboard.writeText) { var tmp = document.createElement('textarea'); tmp.value = email; tmp.style.cssText = 'position:fixed;top:0;left:-9999px;opacity:0'; document.body.appendChild(tmp); tmp.select(); try { document.execCommand('copy'); } catch (err) {} document.body.removeChild(tmp); showSuccess(); return; } navigator.clipboard.writeText(email).then(showSuccess).catch(function () { var tmp = document.createElement('textarea'); tmp.value = email; tmp.style.cssText = 'position:fixed;top:0;left:-9999px;opacity:0'; document.body.appendChild(tmp); tmp.select(); try { document.execCommand('copy'); } catch (err) {} document.body.removeChild(tmp); showSuccess(); }); }); return btn; } function isInDocument(node) { try { return document.contains ? document.contains(node) : document.body.contains(node); } catch (e) { return false; } } function processMailtoLink(a) { if (!a || !a.parentNode) return; if (!isInDocument(a)) return; if (a.hasAttribute('data-email-tagged')) return; var href = a.getAttribute('href') || ''; if (href.toLowerCase().indexOf('mailto:') !== 0) return; var email = href.replace(/^mailto:/i, '').split('?')[0].trim(); if (!email) email = (a.textContent || '').trim(); if (!email || !EMAIL_RE.test(email)) return; var next = a.nextSibling; while (next && next.nodeType === Node.TEXT_NODE && next.textContent.trim() === '') { next = next.nextSibling; } if (next && next.nodeType === Node.ELEMENT_NODE && next.getAttribute && next.getAttribute('data-copy-btn')) return; a.setAttribute('data-email-tagged', '1'); try { a.parentNode.insertBefore(makeBtn(email), a.nextSibling); } catch (e) {} } function processTextNode(node) { if (!node || !node.parentNode) return; if (!isInDocument(node)) return; var text = node.textContent; if (!text || text.indexOf('@') === -1) return; var parent = node.parentNode; if (SKIP_TAGS.has(parent.nodeName)) return; if (parent.nodeName === 'A') return; if (parent.hasAttribute && (parent.hasAttribute('data-email-tagged') || parent.hasAttribute('data-copy-btn'))) return; EMAIL_RE_G.lastIndex = 0; var matches = []; var m; while ((m = EMAIL_RE_G.exec(text)) !== null) { matches.push({ email: m[0], index: m.index, length: m[0].length }); } if (!matches.length) return; var frag = document.createDocumentFragment(); var last = 0; for (var i = 0; i < matches.length; i++) { var match = matches[i]; if (match.index > last) { frag.appendChild(document.createTextNode(text.slice(last, match.index))); } var span = document.createElement('span'); span.textContent = match.email; span.setAttribute('data-email-tagged', '1'); frag.appendChild(span); frag.appendChild(makeBtn(match.email)); last = match.index + match.length; } if (last < text.length) { frag.appendChild(document.createTextNode(text.slice(last))); } try { parent.replaceChild(frag, node); } catch (e) {} } function queueFromRoot(root) { if (!root) return; if (root.nodeType === Node.ELEMENT_NODE) { try { var style = window.getComputedStyle(root); if (style && (style.display === 'none' || style.visibility === 'hidden')) return; } catch (e) { return; } try { var allLinks = (root.nodeName === 'A') ? [root] : root.querySelectorAll('a[href]'); for (var i = 0; i < allLinks.length; i++) { var h = allLinks[i].getAttribute('href') || ''; if (h.toLowerCase().indexOf('mailto:') === 0) { pending.push({ type: 'mailto', el: allLinks[i] }); } } } catch (e) {} } try { var walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { var p = node.parentNode; if (!p) return NodeFilter.FILTER_REJECT; if (SKIP_TAGS.has(p.nodeName)) return NodeFilter.FILTER_REJECT; if (p.nodeName === 'A') return NodeFilter.FILTER_REJECT; if (p.hasAttribute && (p.hasAttribute('data-email-tagged') || p.hasAttribute('data-copy-btn'))) return NodeFilter.FILTER_REJECT; if (!node.textContent || node.textContent.indexOf('@') === -1) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } } ); var n; var limit = MAX_WALK_PER_CALL; while ((n = walker.nextNode()) && limit-- > 0) { pending.push({ type: 'text', el: n }); } } catch (e) {} if (pending.length) schedule(); } function flush(deadline) { scheduled = false; var count = 0; while (pending.length > 0 && count < MAX_PROCESS_PER_BATCH) { if (deadline && typeof deadline.timeRemaining === 'function' && deadline.timeRemaining() < 2) { schedule(); return; } var item = pending.shift(); if (item.type === 'mailto') { processMailtoLink(item.el); } else { processTextNode(item.el); } count++; } if (pending.length) schedule(); } function schedule() { if (scheduled) return; scheduled = true; if ('requestIdleCallback' in window) { window.requestIdleCallback(flush, { timeout: 1500 }); } else { setTimeout(function () { flush({}); }, 100); } } queueFromRoot(document.body); // v1.7: Extended retry delays to cover AI streaming output (up to 15s) [500, 1000, 2000, 3000, 5000, 8000, 12000, 15000].forEach(function (delay) { setTimeout(function () { queueFromRoot(document.body); }, delay); }); var obs = new MutationObserver(function (mutations) { for (var i = 0; i < mutations.length; i++) { var m = mutations[i]; // v1.7: Also handle characterData mutations for streaming text output if (m.type === 'characterData') { var tn = m.target; if (tn && tn.textContent && tn.textContent.indexOf('@') !== -1) { pending.push({ type: 'text', el: tn }); } } for (var j = 0; j < m.addedNodes.length; j++) { var n = m.addedNodes[j]; if (n.nodeType === Node.TEXT_NODE) { if (n.textContent && n.textContent.indexOf('@') !== -1) { pending.push({ type: 'text', el: n }); } } else if (n.nodeType === Node.ELEMENT_NODE) { queueFromRoot(n); } } } if (pending.length) schedule(); }); try { // v1.7: Added characterData:true to capture streaming text mutations obs.observe(document.body, { childList: true, subtree: true, characterData: true }); } catch (e) {} })();