// ==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 https://update.greasyfork.icu/scripts/566135/Multi-Keyword%20In-Page%20Finder.user.js
// @updateURL https://update.greasyfork.icu/scripts/566135/Multi-Keyword%20In-Page%20Finder.meta.js
// ==/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);
};
})();