// ==UserScript==
// @name 高亮特定文字
// @namespace http://tampermonkey.net/
// @version 0.17
// @description 突出显示网页上的特定单词
// @author niweizhuan
// @match *://*/*
// @license MIT
// @grant none
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
// 正则表达式缓存
let regexCache = {};
// 是否打开设置窗口
var isSettingsOpen = false;
// 默认配置
const defaultConfig = {
searchWords: ['example1', 'example2', 'example3'],
highlightColor: '#ffff00'
};
// 使用缓存获取正则表达式
function getCombinedRegex(searchWords) {
const key = searchWords.join('|'); // 用关键词数组生成缓存key
if (!regexCache[key]) {
regexCache[key] = new RegExp(`(${searchWords.map(escapeRegExp).join('|')})`, 'gi');
}
return regexCache[key];
}
// 高亮文本批量处理
function highlightTextInBatches(textNodes, searchWords, highlightColor, batchSize = 100) {
const combinedRegex = getCombinedRegex(searchWords);
let index = 0;
function processBatch() {
const batch = textNodes.slice(index, index + batchSize);
batch.forEach(node => {
let nodeText = node.nodeValue;
let newHtml = nodeText.replace(combinedRegex, `$1`);
if (newHtml !== nodeText && node.parentNode) { // 仅在内容变更时操作 DOM
const newNode = document.createElement('span');
newNode.innerHTML = newHtml;
node.parentNode.replaceChild(newNode, node);
}
});
index += batchSize;
if (index < textNodes.length) {
setTimeout(processBatch, 0); // 使用 setTimeout 分批次处理,防止卡顿
}
}
processBatch(); // 启动批量处理
}
// 获取配置
function getConfig() {
const config = localStorage.getItem('highlightConfig');
return config ? JSON.parse(config) : defaultConfig;
}
// 保存配置
function saveConfig(config) {
localStorage.setItem('highlightConfig', JSON.stringify(config));
}
// 高亮文本
function highlightText(textNodes, searchWords, highlightColor) {
textNodes.forEach(node => {
let nodeText = node.nodeValue;
let newHtml = nodeText;
searchWords.forEach(word => {
const regex = new RegExp(`(${word})`, 'gi');
newHtml = newHtml.replace(regex, `$1`);
});
if (newHtml !== nodeText) {
const newNode = document.createElement('span');
newNode.innerHTML = newHtml;
node.parentNode.replaceChild(newNode, node);
}
});
}
// 获取文本节点
function getTextNodes(node) {
const textNodes = [];
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
let currentNode;
while ((currentNode = walker.nextNode())) {
textNodes.push(currentNode);
}
return textNodes;
}
// 页面上高亮
function highlightOnPage() {
clearAllHighlights(); // 先清除已有高亮
const body = document.body;
const config = getConfig();
const textNodes = getTextNodes(body);
highlightTextInBatches(textNodes, config.searchWords, config.highlightColor);
}
// 打开设置界面
function openSettings() {
if (isSettingsOpen) return;
isSettingsOpen = true;
const config = getConfig();
const overlay = document.createElement('div');
overlay.id = 'highlightOverlay';
overlay.style = `
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
`;
const settingsDiv = document.createElement('div');
settingsDiv.id = 'highlightSettings';
settingsDiv.style = `
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 2vw;
background-color: white;
border: 0.1vw solid #ccc;
border-radius: 0.5vw;
z-index: 1001;
box-shadow: 0vw 0vh 1vw rgba(0, 0, 0, 0.5);
max-width: 80%;
max-height: 80%;
overflow-y: auto;
transition: opacity 0.3s;
font-size: 1vw;
`;
settingsDiv.innerHTML = `
高亮设置
${config.searchWords.map(word => `
${word}
`).join('')}
`;
overlay.appendChild(settingsDiv);
document.body.appendChild(overlay);
let selectedWordBlock = null;
const wordBlocks = document.getElementsByClassName('wordBlock');
Array.from(wordBlocks).forEach(block => {
block.onclick = function() {
if (selectedWordBlock) {
selectedWordBlock.style.backgroundColor = '';
}
selectedWordBlock = this;
selectedWordBlock.style.backgroundColor = '#d3d3d3';
};
block.ontouchstart = function() {
if (selectedWordBlock) {
selectedWordBlock.style.backgroundColor = '';
}
selectedWordBlock = this;
selectedWordBlock.style.backgroundColor = '#d3d3d3';
};
});
const newWordButton = document.getElementById('newWord');
const editWordButton = document.getElementById('editWord');
const deleteWordButton = document.getElementById('deleteWord');
const saveSettingsButton = document.getElementById('saveSettings');
// 新建单词块处理
function handleNewWord() {
const newWord = prompt('请输入新的高亮文本:');
if (newWord) {
config.searchWords.push(newWord.trim());
saveConfig(config);
document.body.removeChild(overlay);
isSettingsOpen = false;
openSettings();
}
}
// 编辑单词块处理
function handleEditWord() {
if (selectedWordBlock) {
const editedWord = prompt('编辑高亮文本:', selectedWordBlock.textContent);
if (editedWord) {
const index = config.searchWords.indexOf(selectedWordBlock.textContent);
if (index !== -1) {
config.searchWords[index] = editedWord.trim();
saveConfig(config);
document.body.removeChild(overlay);
isSettingsOpen = false;
openSettings();
}
}
} else {
alert('请先选择一个单词块进行编辑');
}
}
// 删除单词块处理
function handleDeleteWord() {
if (selectedWordBlock) {
const index = config.searchWords.indexOf(selectedWordBlock.textContent);
if (index !== -1) {
config.searchWords.splice(index, 1);
saveConfig(config);
document.body.removeChild(overlay);
isSettingsOpen = false;
openSettings(); // 重新打开设置界面以反映删除后的状态
}
} else {
alert('请先选择一个单词块进行删除');
}
}
// 保存设置处理
function handleSaveSettings() {
config.highlightColor = document.getElementById('highlightColor').value;
saveConfig(config);
//重新高亮
//clearAllHighlights();
highlightOnPage();
// 删除原先的浮动按钮和 Shadow DOM
const oldButtonContainer = document.getElementById('floatButtonContainer');
if (oldButtonContainer) {
oldButtonContainer.remove(); // 移除旧的按钮和其附带的 Shadow DOM
}
// 重新创建浮动按钮
createFloatButton();
// 关闭设置窗口
document.body.removeChild(overlay);
isSettingsOpen = false;
}
// 绑定按钮事件
newWordButton.onclick = handleNewWord;
newWordButton.ontouchstart = handleNewWord;
editWordButton.onclick = handleEditWord;
editWordButton.ontouchstart = handleEditWord;
deleteWordButton.onclick = handleDeleteWord;
deleteWordButton.ontouchstart = handleDeleteWord;
saveSettingsButton.onclick = handleSaveSettings;
saveSettingsButton.ontouchstart = handleSaveSettings;
overlay.onclick = (event) => {
if (event.target === overlay) {
document.body.removeChild(overlay);
isSettingsOpen = false;
}
};
}
// 判断颜色是否过暗
function isColorTooDark(color) {
const c = color.substring(1); // 去掉 '#'
const rgb = parseInt(c, 16); // 转换为整数
const r = (rgb >> 16) & 0xff; // 提取红色部分
const g = (rgb >> 8) & 0xff; // 提取绿色部分
const b = (rgb >> 0) & 0xff; // 提取蓝色部分
const luma = 0.299 * r + 0.587 * g + 0.114 * b; // 计算亮度
return luma < 50; // 亮度阈值,可以根据需要调整
}
// 创建悬浮按钮
function createFloatButton() {
let isDragging = false;
let startY, initialTop;
let moved = false;
let hideTimeout;
const hideDelay = 1000; // 1秒未操作则隐藏
const visibleLeft = '1vw'; // 弹出时的位置
let isHidden = false; // 标记按钮是否处于隐藏状态
let isAnimating = false; // 标记按钮是否正在动画中
const config = getConfig();
// 创建悬浮球按钮容器
const floatButtonContainer = document.createElement('div');
floatButtonContainer.style.position = 'fixed';
floatButtonContainer.style.left = '0';
floatButtonContainer.style.top = '1vw';
floatButtonContainer.style.zIndex = '9999';
floatButtonContainer.style.width = 'auto';
floatButtonContainer.style.height = 'auto';
// 创建Shadow DOM
const shadow = floatButtonContainer.attachShadow({ mode: 'open' });
floatButtonContainer.id = 'floatButtonContainer';
// 创建悬浮球按钮
const floatButton = document.createElement('div');
floatButton.style.cssText = `
min-width: 8vw; /* 确保悬浮球的最小宽度 */
min-height: 2vw; /* 确保悬浮球的最小高度 */
background-color: ${config.highlightColor};
background-size: cover;
background-position: center;
cursor: pointer;
border-radius: 0.5vw;
box-shadow: 0px 0px 1vw rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 0;
transition: top 0.3s ease, left 0.3s ease;
white-space: nowrap; /* 防止文本换行 */
text-overflow: ellipsis; /* 添加省略号以防文本过长 */
`;
// 根据颜色亮度调整文字颜色
if (isColorTooDark(config.highlightColor)) {
floatButton.innerHTML = `
`;
} else {
floatButton.innerHTML = `
`;
}
shadow.appendChild(floatButton);
document.body.appendChild(floatButtonContainer);
// 显示设置界面
function showSettings() {
openSettings();
}
// 点击处理
function handleClick() {
if (!isDragging && !moved && !isHidden && !isAnimating) {
showSettings();
}
}
// 计算隐藏后的left值
function calculateHiddenLeft() {
const buttonWidth = floatButtonContainer.offsetWidth;
return `-${(buttonWidth * 0.78)}px`; // 隐藏78%宽度
}
// 鼠标事件处理
floatButton.addEventListener('mousedown', (e) => {
if (isHidden || isAnimating) return; // 隐藏或动画状态下不允许拖动
clearTimeout(hideTimeout); // 停止隐藏计时器
isDragging = true;
moved = false;
startY = e.clientY;
initialTop = floatButtonContainer.offsetTop;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
const deltaY = e.clientY - startY;
let newTop = Math.max(0, Math.min(initialTop + deltaY, window.innerHeight - floatButtonContainer.offsetHeight));
floatButtonContainer.style.top = `${newTop}px`;
if (Math.abs(deltaY) > 5) {
moved = true;
}
e.preventDefault();
}
resetHideTimer(); // 任何鼠标移动都重置隐藏计时器
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
if (!moved) {
handleClick();
}
resetHideTimer(); // 拖动结束后重置隐藏计时器
}
});
// 触摸事件处理
floatButton.addEventListener('touchstart', (e) => {
if (isHidden || isAnimating) {
if (isHidden) {
beginAnimation(); // 开始恢复动画
floatButtonContainer.style.left = visibleLeft;
isHidden = false;
clearTimeout(hideTimeout); // 停止隐藏计时器
}
e.preventDefault();
return;
}
clearTimeout(hideTimeout); // 停止隐藏计时器
isDragging = true;
moved = false;
const touch = e.touches[0];
startY = touch.clientY;
initialTop = floatButtonContainer.offsetTop;
e.preventDefault();
});
document.addEventListener('touchmove', (e) => {
if (isDragging && !isHidden && !isAnimating) {
const touch = e.touches[0];
const deltaY = touch.clientY - startY;
let newTop = Math.max(0, Math.min(initialTop + deltaY, window.innerHeight - floatButtonContainer.offsetHeight));
floatButtonContainer.style.top = `${newTop}px`;
if (Math.abs(deltaY) > 5) {
moved = true;
}
e.preventDefault();
}
resetHideTimer(); // 任何触摸移动都重置隐藏计时器
});
document.addEventListener('touchend', (e) => {
if (isDragging && !isHidden && !isAnimating) {
isDragging = false;
if (!moved) {
handleClick();
}
if (moved) {
e.preventDefault();
}
resetHideTimer(); // 触摸结束后重置隐藏计时器
}
});
floatButton.addEventListener('mouseenter', () => {
if (isHidden && !isAnimating) {
beginAnimation(); // 开始恢复动画
floatButtonContainer.style.left = visibleLeft; // 鼠标悬停时弹出按钮
isHidden = false;
}
clearTimeout(hideTimeout); // 停止隐藏计时器
});
floatButton.addEventListener('mouseleave', () => {
resetHideTimer();
});
floatButton.addEventListener('click', (e) => {
handleClick();
e.preventDefault();
});
// 重置隐藏计时器
function resetHideTimer() {
clearTimeout(hideTimeout);
hideTimeout = setTimeout(() => {
beginAnimation(); // 开始隐藏动画
floatButtonContainer.style.left = calculateHiddenLeft(); // 隐藏按钮
isHidden = true;
}, hideDelay);
}
// 开始动画
function beginAnimation() {
isAnimating = true;
floatButton.style.pointerEvents = 'none'; // 禁用按钮点击事件
floatButtonContainer.style.transition = 'left 0.3s ease, transform 0.3s ease'; // 确保应用了左转换和变换转换
setTimeout(endAnimation, 300); // 300ms后恢复交互
}
// 结束动画
function endAnimation() {
isAnimating = false;
floatButton.style.pointerEvents = 'auto'; // 恢复按钮点击事件
}
resetHideTimer();
}
// 清除所有高亮文字
function clearAllHighlights() {
// 获取所有的 元素
const highlightedElements = document.querySelectorAll('mark');
// 遍历所有的 元素
highlightedElements.forEach(element => {
// 创建一个文本节点,内容为 元素的文本内容
const textNode = document.createTextNode(element.textContent);
// 替换 元素为文本节点
element.parentNode.replaceChild(textNode, element);
});
}
// 转义正则表达式中的特殊字符
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // 转义正则表达式特殊字符
}
// 创建悬浮按钮
createFloatButton();
highlightOnPage();
})();