Greasy Fork

Greasy Fork is available in English.

高亮特定文字开发版

突出显示网页上的特定单词

当前为 2024-08-27 提交的版本,查看 最新版本

// ==UserScript==
// @name         高亮特定文字开发版
// @namespace    http://tampermonkey.net/
// @version      0.12
// @description  突出显示网页上的特定单词
// @author       niweizhuan
// @match        *://*/*
// @license      MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    var isSettingsOpen = false
    const defaultConfig = {
        searchWords: ['example1', 'example2', 'example3'],
        highlightColor: '#ffff00'
    };


    const getConfig = () => {
        const config = localStorage.getItem('highlightConfig');
        return config ? JSON.parse(config) : defaultConfig;
    };


    const saveConfig = (config) => {
        localStorage.setItem('highlightConfig', JSON.stringify(config));
    };

    const 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, `<mark style="background-color: ${highlightColor};">$1</mark>`);
            });
            if (newHtml !== nodeText) {
                const newNode = document.createElement('span');
                newNode.innerHTML = newHtml;
                node.parentNode.replaceChild(newNode, node);
            }
        });

    };
    const 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;
    };
    const highlightOnPage = () => {
        const body = document.body;
        const config = getConfig();
        const textNodes = getTextNodes(body);
        highlightText(textNodes, config.searchWords, config.highlightColor);
    };

    const 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: 0px 0px 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 = `
            <h2>高亮设置</h2>
            <div id="wordBlocks" style="display: flex; flex-wrap: wrap; max-height: 15vh; overflow-y: auto; margin-bottom: 1vw; border: 0.1vw solid #ccc;">
                ${config.searchWords.map(word => `<div class="wordBlock" style="padding: 0.5vw; border: 0.1vw solid #ccc; margin: 0.5vw; cursor: pointer;">${word}</div>`).join('')}
            </div>
            <div>
                <label for="highlightColor">高亮颜色: </label>
                <input type="color" id="highlightColor" value="${config.highlightColor}">
            </div>
            <button id="newWord" style="margin-right: 1vw;">新建</button>
            <button id="editWord" style="margin-right: 1vw;">编辑</button>
            <button id="deleteWord" style="margin-right: 1vw;">删除</button>
            <button id="saveSettings" style="margin-right: 1vw;">保存</button>
        `;

        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');
        //新建
        const handleNewWord = () => {
            const newWord = prompt('请输入新的高亮文本:');
            if (newWord) {
                config.searchWords.push(newWord.trim());
                saveConfig(config);
                document.body.removeChild(overlay);
                isSettingsOpen = false;
                openSettings();
            }
        };
        //编辑
        const 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('请先选择一个单词块进行编辑');
            }
        };
        //删除
        const 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('请先选择一个单词块进行删除');
            }
        };
        //保存
        const handleSaveSettings = () => {
            config.highlightColor = document.getElementById('highlightColor').value;
            saveConfig(config);
            // 重新应用高亮
            highlightOnPage();
            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 = '20px'; // 弹出时的位置
        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' });

        // 创建悬浮球按钮
        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 = `
            <div style="
                width: 100%;
                height: 100%;
                display: flex;
                align-items: center;
                justify-content: center;
                user-select: none;  /* 禁止文字被选中 */
            ">
                <div class="container" style="display: flex; align-items: center; justify-content: center; height: 100%; width: 100%;">
                    <div class="left" style="padding-right: 5px; font-size: 12px; color: white;">设置高亮文本</div>
                    <div class="line" style="width: 1px; height: 20px; background-color: white;"></div>
                    <div class="right" style="padding-left: 5px; font-size: 12px; color: white;">
                        <div class="rotate-text" style="transform: rotate(90deg);">Aa</div>
                    </div>
                </div>
            </div>
        `;
    } else {
        floatButton.innerHTML = `
            <div style="
                width: 100%;
                height: 100%;
                display: flex;
                align-items: center;
                justify-content: center;
                user-select: none;  /* 禁止文字被选中 */
            ">
                <div class="container" style="display: flex; align-items: center; justify-content: center; height: 100%; width: 100%;">
                    <div class="left" style="padding-right: 5px; font-size: 12px; color: black;">设置高亮文本</div>
                    <div class="line" style="width: 1px; height: 20px; background-color: black;"></div>
                    <div class="right" style="padding-left: 5px; font-size: 12px; color: black;">
                        <div class="rotate-text" style="transform: rotate(90deg);">Aa</div>
                    </div>
                </div>
            </div>
        `;
    }

    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();
}
    createFloatButton();

})();