您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Ctrl+F 呼出自定义搜索面板,支持上下键导航,Esc 关闭,搜索框固定顶部,自动监听新内容并保持当前位置
// ==UserScript== // @name 网页内容搜索增强 // @namespace http://tampermonkey.net/ // @version 3.1 // @description Ctrl+F 呼出自定义搜索面板,支持上下键导航,Esc 关闭,搜索框固定顶部,自动监听新内容并保持当前位置 // @match *://*/* // @grant none // ==/UserScript== (function() { 'use strict'; // 创建侧边栏 const panel = document.createElement("div"); panel.style.cssText = ` position: fixed; top: 0; right: 0; width: 340px; height: 100%; background: #f9f9f9; border-left: 1px solid #ccc; overflow-y: auto; z-index: 2147483647; font-size: 14px; padding: 10px; display: none; box-sizing: border-box; `; panel.innerHTML = ` <input id="searchBox" type="text" placeholder="输入关键词..." style="width:100%;padding:6px 8px;box-sizing:border-box; border:1px solid #ccc;border-radius:4px; position:sticky; top:0; background:#f9f9f9; z-index:1;"> <div id="count" style="margin:6px 0;color:#333;font-weight:bold;"></div> <div id="results"></div> <style> .__wl-item { cursor:pointer; margin:6px 0; padding:4px 6px; border-radius:4px; } .__wl-item.__active { background:#e6f0ff; outline:1px solid #99c2ff; } .__wl-item b { color:red; } </style> `; document.body.appendChild(panel); const searchBox = panel.querySelector("#searchBox"); const resultsDiv = panel.querySelector("#results"); const countDiv = panel.querySelector("#count"); let matches = []; let items = []; let currentIndex = -1; // 搜索并渲染结果 function search(keyword) { resultsDiv.innerHTML = ""; countDiv.textContent = ""; matches = []; items = []; if (!keyword) { currentIndex = -1; return; } const regex = new RegExp(`(.{0,20})(${keyword})(.{0,20})`, "gi"); const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false); let node, index = 0; while (node = walker.nextNode()) { if (panel.contains(node.parentNode)) continue; // 跳过侧边栏自身 const text = node.textContent; let match; while ((match = regex.exec(text)) !== null) { index++; const matchObj = { node, start: match.index + match[1].length, end: match.index + match[1].length + match[2].length, context: match }; matches.push(matchObj); const item = document.createElement("div"); item.className = "__wl-item"; item.dataset.index = String(index - 1); const beforeSpan = document.createElement("span"); beforeSpan.textContent = match[1]; const keywordSpan = document.createElement("b"); keywordSpan.textContent = match[2]; const afterSpan = document.createElement("span"); afterSpan.textContent = match[3]; item.append(`${index}. ...`); item.appendChild(beforeSpan); item.appendChild(keywordSpan); item.appendChild(afterSpan); item.append("..."); item.onclick = (() => { const m = matchObj; return () => locateMatch(m, item); })(); resultsDiv.appendChild(item); items.push(item); } } countDiv.textContent = `共找到 ${index} 个结果`; // 保持当前位置 if (items.length > 0) { let restoreIndex = currentIndex; if (restoreIndex < 0) restoreIndex = 0; if (restoreIndex >= items.length) restoreIndex = items.length - 1; setActiveIndex(restoreIndex, false); // false 表示只高亮,不强制滚动定位 } else { currentIndex = -1; } } // 用 Selection 高亮并滚动定位 function locateMatch(m, itemEl) { const range = document.createRange(); range.setStart(m.node, m.start); range.setEnd(m.node, m.end); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); const rect = range.getBoundingClientRect(); window.scrollTo({ top: window.scrollY + rect.top - window.innerHeight / 2, behavior: "smooth" }); const idx = Number(itemEl.dataset.index || -1); if (idx >= 0) setActiveIndex(idx, false); } function setActiveIndex(idx, optionallyLocate = true) { if (items.length === 0) return; if (idx < 0) idx = 0; if (idx >= items.length) idx = items.length - 1; if (currentIndex >= 0 && items[currentIndex]) { items[currentIndex].classList.remove("__active"); } currentIndex = idx; const itemEl = items[currentIndex]; itemEl.classList.add("__active"); itemEl.scrollIntoView({ behavior: "smooth", block: "nearest" }); if (optionallyLocate) { const m = matches[currentIndex]; locateMatch(m, itemEl); } } function debounce(fn, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } searchBox.addEventListener("input", debounce(e => { search(e.target.value.trim()); }, 250)); function togglePanel(show) { if (show === undefined) { panel.style.display = (panel.style.display === "none") ? "block" : "none"; } else { panel.style.display = show ? "block" : "none"; } if (panel.style.display === "block") { setTimeout(() => searchBox.focus(), 50); } else { window.getSelection().removeAllRanges(); } } // 全局快捷键 document.addEventListener("keydown", e => { const active = document.activeElement; const isInput = active && ( active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.isContentEditable ); // 焦点在搜索框时,Esc 仍然能关闭;上下键也能切换 if (isInput && panel.contains(active)) { if (e.key === "Escape" && panel.style.display === "block") { togglePanel(false); e.preventDefault(); } if (e.key === "ArrowDown") { e.preventDefault(); if (items.length > 0) setActiveIndex(currentIndex + 1, true); } else if (e.key === "ArrowUp") { e.preventDefault(); if (items.length > 0) setActiveIndex(currentIndex - 1, true); } return; } // Ctrl+F 打开/关闭(覆盖浏览器默认) if (e.ctrlKey && e.key.toLowerCase() === "f") { e.preventDefault(); togglePanel(); } // Esc 关闭 if (e.key === "Escape" && panel.style.display === "block") { togglePanel(false); } // 上下键全局拦截(点击结果后也能用) if (panel.style.display === "block") { if (e.key === "ArrowDown") { e.preventDefault(); if (items.length > 0) setActiveIndex(currentIndex + 1, true); } else if (e.key === "ArrowUp") { e.preventDefault(); if (items.length > 0) setActiveIndex(currentIndex - 1, true); } } }, true); // 监听网页内容变化,自动更新搜索结果(忽略侧边栏自身,防止死循环) const observer = new MutationObserver(mutations => { if (mutations.some(m => panel.contains(m.target))) return; // 忽略侧边栏 const keyword = searchBox.value.trim(); if (keyword) { clearTimeout(observer.timer); observer.timer = setTimeout(() => search(keyword), 300); // 防抖 } }); observer.observe(document.body, { childList: true, subtree: true }); })();