划词助手

选中文字后出现浮窗;浮窗因为一些操作(如复制、搜索)消失后,但选中文字依旧没有取消,鼠标再次移入选中文字的区域浮窗再次出现。

// ==UserScript==
// @name         划词助手
// @namespace    
// @version      1.1
// @description  选中文字后出现浮窗;浮窗因为一些操作(如复制、搜索)消失后,但选中文字依旧没有取消,鼠标再次移入选中文字的区域浮窗再次出现。
// @author       Myzom
// @match        <all_urls>
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- CSS Styles (from style.css) ---
    const styles = `
      #text-selection-toolbar-userscript {
        position: absolute;
        z-index: 2147483647;
        background-color: rgba(30, 30, 30, 0.92);
        border: 1px solid rgba(80, 80, 80, 0.7);
        border-radius: 16px;
        box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
        padding: 4px 6px;
        display: flex;
        align-items: center;
        gap: 6px;
        opacity: 0;
        visibility: hidden;
        transition: opacity 0.1s ease-out, visibility 0.1s ease-out, transform 0.1s ease-out;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
        color: #e0e0e0;
        font-size: 12px;
        line-height: 1;
        transform: translateY(5px);
      }

      #text-selection-toolbar-userscript.visible {
        opacity: 1;
        visibility: visible;
        transform: translateY(0);
      }

      #text-selection-toolbar-userscript button {
        background-color: transparent;
        border: none;
        color: inherit;
        cursor: pointer;
        padding: 3px 5px;
        font-size: 12px;
        border-radius: 8px;
        transition: background-color 0.15s ease;
        display: flex;
        align-items: center;
        gap: 4px;
      }

      #text-selection-toolbar-userscript button:hover {
        background-color: rgba(70, 70, 70, 0.8);
        color: white;
      }

      #text-selection-toolbar-userscript .search-icon-svg,
      #text-selection-toolbar-userscript .copy-icon-svg {
        width: 13px;
        height: 13px;
        fill: currentColor;
        vertical-align: middle;
      }

      #text-selection-toolbar-userscript .separator {
        width: 1px;
        height: 14px;
        background-color: rgba(100, 100, 100, 0.5);
        margin: 0 3px;
      }
    `;
    GM_addStyle(styles);

    // --- JavaScript Logic ---
    let toolbar;
    let currentSelectedText = '';
    let lastSelectionRange = null; // 用于存储最后一个有效选区的Range对象

    const searchEngines = [
      { id: 'google', name: 'Google', url: 'https://www.google.com/search?q=%s' },
      { id: 'baidu', name: '百度', url: 'https://www.baidu.com/s?wd=%s' },
      { id: 'bing', name: '必应', url: 'https://www.bing.com/search?q=%s' }
    ];

    const searchIconSVG = `<svg class="search-icon-svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>`;
    const copyIconSVG = `<svg class="copy-icon-svg" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;

    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    function createToolbar() {
      if (document.getElementById('text-selection-toolbar-userscript')) {
        toolbar = document.getElementById('text-selection-toolbar-userscript');
        return;
      }

      toolbar = document.createElement('div');
      toolbar.id = 'text-selection-toolbar-userscript';
      document.body.appendChild(toolbar);

      document.addEventListener('mousedown', (event) => {
        if (toolbar && toolbar.classList.contains('visible')) {
          if (!toolbar.contains(event.target)) {
            const selection = window.getSelection();
            if (selection.toString().trim() === '' || selection.isCollapsed) {
                 hideToolbar();
                 currentSelectedText = '';
                 lastSelectionRange = null;
            }
          }
        }
      }, true);
    }

    function updateToolbarContent() {
      if (!toolbar) return;
      toolbar.innerHTML = '';

      const searchIconSpan = document.createElement('span');
      searchIconSpan.innerHTML = searchIconSVG;
      toolbar.appendChild(searchIconSpan);

      searchEngines.forEach(engine => {
        const button = document.createElement('button');
        button.textContent = engine.name;
        button.title = `使用 ${engine.name} 搜索`;
        button.addEventListener('click', (e) => {
          e.stopPropagation();
          if (currentSelectedText) {
            const searchUrl = engine.url.replace('%s', encodeURIComponent(currentSelectedText));
            window.open(searchUrl, '_blank');
            hideToolbarAfterAction();
          }
        });
        toolbar.appendChild(button);
      });

      const separator = document.createElement('div');
      separator.className = 'separator';
      toolbar.appendChild(separator);

      const copyButton = document.createElement('button');
      copyButton.innerHTML = copyIconSVG + '复制';
      copyButton.title = '复制选中的文本';
      copyButton.addEventListener('click', (e) => {
        e.stopPropagation();
        if (currentSelectedText) {
          navigator.clipboard.writeText(currentSelectedText)
            .then(() => { /* console.log('文本已复制!'); */ })
            .catch(err => console.error('复制失败:', err));
          hideToolbarAfterAction();
        }
      });
      toolbar.appendChild(copyButton);
    }

    function showToolbar(mouseClientX, mouseClientY) {
      if (!toolbar || !currentSelectedText) return;

      updateToolbarContent(); // 确保内容是最新的

      toolbar.style.left = `0px`; // Reset for size calculation
      toolbar.style.top = `0px`;
      toolbar.classList.add('visible');

      const toolbarRect = toolbar.getBoundingClientRect();
      const toolbarWidth = toolbarRect.width;
      const toolbarHeight = toolbarRect.height;

      let idealX = window.scrollX + mouseClientX + 15;
      let idealY = window.scrollY + mouseClientY + 10;

      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;

      if (idealX + toolbarWidth > window.scrollX + viewportWidth - 5) {
        idealX = window.scrollX + mouseClientX - toolbarWidth - 15;
      }
      if (idealX < window.scrollX + 5) {
        idealX = window.scrollX + 5;
      }
      if (idealY + toolbarHeight > window.scrollY + viewportHeight - 5) {
        idealY = window.scrollY + mouseClientY - toolbarHeight - 10;
      }
      if (idealY < window.scrollY + 5) {
        idealY = window.scrollY + 5;
      }

      toolbar.style.left = `${idealX}px`;
      toolbar.style.top = `${idealY}px`;
    }

    function hideToolbar() {
      if (toolbar) {
        toolbar.classList.remove('visible');
      }
    }

    function hideToolbarAfterAction() {
        setTimeout(() => {
            hideToolbar();
            // currentSelectedText 和 lastSelectionRange 保持不变,以便悬停时可以再次显示
        }, 100);
    }

    document.addEventListener('mouseup', (event) => {
      if (toolbar && toolbar.contains(event.target)) {
        return;
      }
      const targetTagName = event.target.tagName;
      if (targetTagName === 'INPUT' || targetTagName === 'TEXTAREA' || event.target.isContentEditable) {
        currentSelectedText = '';
        lastSelectionRange = null;
        hideToolbar();
        return;
      }

      setTimeout(() => {
        const selection = window.getSelection();
        const selectedText = selection.toString().trim();

        if (selectedText.length > 0 && selectedText.length < 1000) {
          currentSelectedText = selectedText;
          try {
            if (selection.rangeCount > 0) {
                lastSelectionRange = selection.getRangeAt(0).cloneRange(); // 存储选区范围的克隆
            } else {
                lastSelectionRange = null;
            }
          } catch (e) {
            console.warn("Could not get selection range on mouseup:", e);
            lastSelectionRange = null;
          }

          const mouseX = event.clientX;
          const mouseY = event.clientY;
          if (!toolbar) createToolbar();
          showToolbar(mouseX, mouseY);
        } else {
          currentSelectedText = '';
          lastSelectionRange = null;
          hideToolbar();
        }
      }, 10);
    });

    function handleDocumentMouseMove(event) {
        // 条件1: 工具栏已隐藏,并且我们有之前选中文本的记录
        if (!(toolbar && toolbar.classList.contains('visible')) && currentSelectedText && lastSelectionRange) {
            const currentActualSelection = window.getSelection();

            // 条件2: 页面上没有实际的选区,或者当前选区与我们存储的不符
            if (currentActualSelection.isCollapsed || currentActualSelection.rangeCount === 0 || currentActualSelection.toString().trim() !== currentSelectedText) {
                // 如果选区确实消失了(而不是变成了其他内容),则清除我们的跟踪状态
                if (currentActualSelection.isCollapsed || currentActualSelection.rangeCount === 0) {
                    currentSelectedText = '';
                    lastSelectionRange = null;
                }
                return; // 不显示工具栏
            }

            // 条件3: 鼠标在存储的选区边界内
            try {
                const rects = lastSelectionRange.getClientRects(); // 获取选区所有行的矩形区域
                if (rects.length > 0) {
                    for (let i = 0; i < rects.length; i++) {
                        const rect = rects[i];
                        if (event.clientX >= rect.left && event.clientX <= rect.right &&
                            event.clientY >= rect.top && event.clientY <= rect.bottom) {

                            showToolbar(event.clientX, event.clientY);
                            return; // 工具栏已显示,退出
                        }
                    }
                }
            } catch (e) {
                // console.warn("Error checking selection bounds on mousemove:", e);
                // Range 可能因DOM更改而失效,清除它
                lastSelectionRange = null;
                currentSelectedText = ''; // 如果范围无效,文本记录也应清除
            }
        }
    }

    const debouncedMouseMoveHandler = debounce(handleDocumentMouseMove, 150); // 150ms 防抖
    document.addEventListener('mousemove', debouncedMouseMoveHandler);

    // Initial setup
    createToolbar();

})();