// ==UserScript== // @name Multi-Keyword In-Page Finder // @name:zh-CN 多关键词页内查找 // @namespace https://github.com/ShualX // @version 1.3 // @description Paste keywords , highlight matches, and navigate. Hidden by default; proximity-reveal dot. // @description:zh-CN 输入关键词,在当前网页高亮并逐个定位;默认隐藏,鼠标靠近右下角才出现小圆点(不闪烁)。 // @author ShualX // @license MIT // @match *://*/* // @grant none // @downloadURL none // ==/UserScript== (function () { 'use strict'; // ====== UI config ====== const REVEAL_ZONE_PX = 88; // 右下角感应区:距离右边/下边 <= 88px 时显示圆点 const DOT_SIZE = 24; // 圆点尺寸 24x24 const DOT_OFFSET = 14; // 圆点距离右/下边距 const SHOW_ICON = true; // ✅ 想要“放大镜图标”就 true;只要纯圆点就 false const AUTO_HIDE_DELAY = 350; // 鼠标离开感应区后延迟隐藏(ms),避免抖动 const PANEL_ID = 'mk_panel_dot_v3'; const STYLE_ID = 'mk_style_dot_v3'; const DOT_ID = 'mk_dot_v3'; // ----- CSS ----- if (!document.getElementById(STYLE_ID)) { const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = ` #${PANEL_ID}, #${PANEL_ID} * { box-sizing: border-box !important; } /* Dot */ #${DOT_ID}{ position: fixed; right: ${DOT_OFFSET}px; bottom: ${DOT_OFFSET}px; width: ${DOT_SIZE}px; height: ${DOT_SIZE}px; border-radius: 999px; background: rgba(20,20,20,0.65); border: 1px solid rgba(255,255,255,0.22); box-shadow: 0 8px 18px rgba(0,0,0,0.25); cursor: pointer; z-index: 2147483647; opacity: 0; pointer-events: none; transform: translateY(6px); transition: opacity 140ms ease, transform 140ms ease, background 140ms ease; display: flex; align-items: center; justify-content: center; } #${DOT_ID}.mk_show{ opacity: 1; pointer-events: auto; transform: translateY(0); } #${DOT_ID}:hover{ background: rgba(20,20,20,0.9); } #${DOT_ID} svg{ width: 12px; height: 12px; opacity: .85; } /* Panel */ #${PANEL_ID} { position: fixed; right: ${DOT_OFFSET}px; bottom: ${DOT_OFFSET + DOT_SIZE + 8}px; z-index: 2147483647; width: 320px; max-width: min(320px, calc(100vw - 24px)); background: rgba(20,20,20,0.92); color: #fff; border-radius: 12px; padding: 10px; font: 12px/1.4 system-ui, -apple-system, Segoe UI, Arial; box-shadow: 0 10px 30px rgba(0,0,0,0.35); overflow: hidden; } #${PANEL_ID}.mk_hidden { display: none !important; } #${PANEL_ID} .mk_header { display:flex; align-items:center; justify-content:space-between; gap:8px; cursor: move; user-select: none; } #${PANEL_ID} .mk_title { font-weight: 700; } #${PANEL_ID} .mk_btn { background:#444; border:0; color:#fff; border-radius:8px; padding:4px 8px; cursor:pointer; line-height: 1; } #${PANEL_ID} textarea { width:100% !important; max-width:100% !important; height: 96px; max-height: 30vh; border-radius:10px; border:1px solid #555; padding:8px; background:#111; color:#fff; resize: vertical; outline: none; } #${PANEL_ID} .mk_row { display:flex; gap:8px; margin-top:8px; } #${PANEL_ID} .mk_primary { flex:1; background:#2f6fed; } #${PANEL_ID} .mk_secondary { flex:1; background:#555; } #${PANEL_ID} .mk_nav { flex:1; background:#333; } #${PANEL_ID} .mk_status { margin-top:8px; opacity:.85; word-break: break-word; } #${PANEL_ID} .mk_hint { margin-top:6px; opacity:.65; } `; document.documentElement.appendChild(style); } function ensureDot() { let dot = document.getElementById(DOT_ID); if (dot) return dot; dot = document.createElement('div'); dot.id = DOT_ID; dot.title = '多关键词页内查找(点击展开/收起,Alt+K)'; if (SHOW_ICON) { // tiny magnifier icon (inline SVG) dot.innerHTML = ` `; } else { dot.innerHTML = ''; // pure dot } document.documentElement.appendChild(dot); return dot; } function createPanel() { const panel = document.createElement('div'); panel.id = PANEL_ID; panel.classList.add('mk_hidden'); // 默认关闭 panel.innerHTML = `
多关键词页内查找
粘贴Excel关键词(每行一个):
快捷键:Alt+K 面板;Alt+N/Alt+P 跳转
`; document.documentElement.appendChild(panel); return panel; } function getPanel() { return document.getElementById(PANEL_ID) || createPanel(); } function togglePanel(force) { const panel = getPanel(); if (typeof force === 'boolean') panel.classList.toggle('mk_hidden', !force); else panel.classList.toggle('mk_hidden'); // 面板打开时,强制让圆点可见(方便关闭) const dot = ensureDot(); const isOpen = !panel.classList.contains('mk_hidden'); dot.classList.toggle('mk_show', isOpen || dot.classList.contains('mk_show')); } // ===== Proximity reveal logic (no flicker) ===== const dot = ensureDot(); let hideTimer = null; function showDot() { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } dot.classList.add('mk_show'); } function hideDotLater() { if (hideTimer) clearTimeout(hideTimer); hideTimer = setTimeout(() => { const panelOpen = !getPanel().classList.contains('mk_hidden'); if (!panelOpen) dot.classList.remove('mk_show'); }, AUTO_HIDE_DELAY); } window.addEventListener('mousemove', (e) => { const vw = window.innerWidth; const vh = window.innerHeight; const nearRight = (vw - e.clientX) <= REVEAL_ZONE_PX; const nearBottom = (vh - e.clientY) <= REVEAL_ZONE_PX; const panelOpen = !getPanel().classList.contains('mk_hidden'); if (panelOpen) { showDot(); return; } if (nearRight && nearBottom) showDot(); else hideDotLater(); }, { passive: true }); dot.addEventListener('click', () => togglePanel()); // Alt+K toggle panel (works even when dot hidden) window.addEventListener('keydown', (e) => { if (e.altKey && e.key.toLowerCase() === 'k') togglePanel(); }); // ===== Drag panel ===== function enableDrag(panel) { const header = panel.querySelector('.mk_header'); let dragging = false; let startX = 0, startY = 0; let startRight = 0, startBottom = 0; header.addEventListener('mousedown', (e) => { if (e.target && e.target.tagName === 'BUTTON') return; dragging = true; startX = e.clientX; startY = e.clientY; const cs = getComputedStyle(panel); startRight = parseInt(cs.right, 10) || DOT_OFFSET; startBottom = parseInt(cs.bottom, 10) || (DOT_OFFSET + DOT_SIZE + 8); e.preventDefault(); }); window.addEventListener('mousemove', (e) => { if (!dragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; panel.style.right = `${Math.max(8, startRight - dx)}px`; panel.style.bottom = `${Math.max(8, startBottom - dy)}px`; }, { passive: true }); window.addEventListener('mouseup', () => dragging = false); } // ===== Highlight engine ===== const MARK_CLASS = 'mk_mark'; let marks = []; let activeIndex = -1; function $(sel) { return getPanel().querySelector(sel); } function escapeRegExp(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function getKeywords() { const raw = ($('#mk_input')?.value || ''); const parts = raw .split(/\r?\n/) .flatMap(line => line.split('\t')) .map(s => s.trim()) .filter(Boolean); const seen = new Set(); const unique = []; for (const p of parts) if (!seen.has(p)) { seen.add(p); unique.push(p); } return unique; } function setStatus(msg) { const el = $('#mk_status'); if (el) el.textContent = msg; } function clearHighlights() { const nodes = Array.from(document.querySelectorAll(`span.${MARK_CLASS}`)); for (const n of nodes) { const parent = n.parentNode; if (!parent) continue; parent.replaceChild(document.createTextNode(n.textContent), n); parent.normalize(); } marks = []; activeIndex = -1; setStatus('已清除高亮。'); } function shouldSkipNode(node, panel) { if (!node || !node.parentElement) return true; const tag = node.parentElement.tagName; if (['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'SELECT', 'OPTION'].includes(tag)) return true; if (panel.contains(node.parentElement)) return true; return false; } function highlightAll() { const panel = getPanel(); clearHighlights(); const keywords = getKeywords(); if (keywords.length === 0) { setStatus('没有检测到关键词(请粘贴一列词)。'); return; } const sorted = [...keywords].sort((a, b) => b.length - a.length); const joined = sorted.map(escapeRegExp).join('|'); const MAX_TOTAL_CHARS = 20000; if (joined.length > MAX_TOTAL_CHARS) { setStatus(`关键词过多/过长(规则约 ${joined.length} 字符)。建议分批(每次 200 个左右)。`); return; } const regex = new RegExp(joined, 'gi'); const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { if (shouldSkipNode(node, panel)) return NodeFilter.FILTER_REJECT; if (!node.nodeValue) return NodeFilter.FILTER_SKIP; regex.lastIndex = 0; if (!regex.test(node.nodeValue)) return NodeFilter.FILTER_SKIP; regex.lastIndex = 0; return NodeFilter.FILTER_ACCEPT; } }); const toProcess = []; while (walker.nextNode()) toProcess.push(walker.currentNode); for (const textNode of toProcess) { const text = textNode.nodeValue; regex.lastIndex = 0; let match, lastIdx = 0; const frag = document.createDocumentFragment(); while ((match = regex.exec(text)) !== null) { const start = match.index; const end = start + match[0].length; if (start > lastIdx) frag.appendChild(document.createTextNode(text.slice(lastIdx, start))); const span = document.createElement('span'); span.className = MARK_CLASS; span.textContent = text.slice(start, end); span.style.cssText = 'background:#ffeb3b;color:#000;padding:0 2px;border-radius:4px;'; frag.appendChild(span); lastIdx = end; } if (lastIdx < text.length) frag.appendChild(document.createTextNode(text.slice(lastIdx))); const parent = textNode.parentNode; if (parent) parent.replaceChild(frag, textNode); } marks = Array.from(document.querySelectorAll(`span.${MARK_CLASS}`)); if (marks.length === 0) { setStatus(`未命中:关键词 ${keywords.length} 个。`); return; } activeIndex = 0; focusActive(); setStatus(`命中 ${marks.length} 处(关键词 ${keywords.length} 个)。`); } function focusActive() { const cur = marks[activeIndex]; if (!cur) return; cur.style.outline = '2px solid #ff5722'; cur.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => { cur.style.outline = 'none'; }, 600); setStatus(`定位到第 ${activeIndex + 1}/${marks.length} 处`); } function next() { if (!marks.length) return; activeIndex = (activeIndex + 1) % marks.length; focusActive(); } function prev() { if (!marks.length) return; activeIndex = (activeIndex - 1 + marks.length) % marks.length; focusActive(); } // ----- Bind panel events once (lazy) ----- let bound = false; function bindOnce() { if (bound) return; bound = true; const panel = getPanel(); enableDrag(panel); panel.querySelector('#mk_hide').onclick = () => togglePanel(false); panel.querySelector('#mk_highlight').onclick = highlightAll; panel.querySelector('#mk_clear').onclick = clearHighlights; panel.querySelector('#mk_next').onclick = next; panel.querySelector('#mk_prev').onclick = prev; window.addEventListener('keydown', (e) => { if (e.altKey && e.key.toLowerCase() === 'n') next(); if (e.altKey && e.key.toLowerCase() === 'p') prev(); }); } const _togglePanel = togglePanel; togglePanel = function (force) { bindOnce(); return _togglePanel(force); }; })();