Greasy Fork

Greasy Fork is available in English.

悬停预览

悬停链接打开小窗并优化资源管理。重用打开的小窗以节省资源,同时保留其位置和大小。包含悬停时间和窗口大小设置,并改进了链接预取功能和资源管理效率。修复了鼠标移动到小窗时意外关闭的问题。支持 URL 路径和链接属性的排除。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         悬停预览
// @version      3.9
// @description  悬停链接打开小窗并优化资源管理。重用打开的小窗以节省资源,同时保留其位置和大小。包含悬停时间和窗口大小设置,并改进了链接预取功能和资源管理效率。修复了鼠标移动到小窗时意外关闭的问题。支持 URL 路径和链接属性的排除。
// @author       hiisme
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @namespace http://greasyfork.icu/users/217852
// ==/UserScript==

(function () {
    'use strict';

    let hoverTimeoutId = null;
    let prefetchTimeoutId = null;
    let popupWindow = null;
    let popupWindowRect = null;
    let isMouseOverPopup = false;

    // 读取设置或设置默认值
    const hoverDelay = GM_getValue('hoverDelay', 1200);
    const windowWidth = GM_getValue('windowWidth', 690);
    const windowHeight = GM_getValue('windowHeight', 400);

    // 注册菜单命令以更改设置
    GM_registerMenuCommand('设置悬停延迟时间 (毫秒)', () => {
        const delay = prompt('请输入悬停延迟时间(毫秒):', hoverDelay);
        if (delay !== null) {
            const parsedDelay = parseInt(delay, 10) || 1200;
            GM_setValue('hoverDelay', parsedDelay);
            alert(`悬停延迟时间设置为 ${parsedDelay} 毫秒。`);
        }
    });

    GM_registerMenuCommand('设置小窗大小', () => {
        const width = prompt('请输入窗口宽度:', windowWidth);
        const height = prompt('请输入窗口高度:', windowHeight);
        if (width !== null && height !== null) {
            const parsedWidth = parseInt(width, 10) || 690;
            const parsedHeight = parseInt(height, 10) || 400;
            GM_setValue('windowWidth', parsedWidth);
            GM_setValue('windowHeight', parsedHeight);
            alert(`窗口大小设置为 ${parsedWidth}x${parsedHeight}。`);
        }
    });

    // 排除链接类型配置
    const excludedLinkPatterns = GM_getValue('excludedLinkPatterns', [
        '/logout', '/download', '/pdf', '/doc', '/xls', '/zip', '/rar', // 文件路径
        '.pdf', '.doc', '.xls', '.zip', '.rar', '.7z', // 文件扩展名
        'mailto:', 'tel:', '#', 'javascript:', 'data:', 'blob:', // 链接协议
        '/login', '/register', '/search', '/settings', '/update', '/change-password', // 登录、注册、搜索、设置、更新、修改密码
        '/terms', '/privacy', '/about', '/contact', '/sitemap', '/faq', // 网站条款、隐私政策、关于我们、联系、站点地图、常见问题
        '/checkout', '/cart', '/order', '/confirmation', // 购物车、结账、订单、确认
        '/profile', '/dashboard', '/user', '/admin', '/management', // 用户、管理
        '/help', '/support', '/feedback', '/report', '/complaint', // 帮助、支持、反馈、报告、投诉
        '/affiliate', '/sponsored', '/promo', '/ad', '/campaign', // 关联、赞助、促销、广告、活动
        '/newsletter', '/subscription', '/unsub', '/unsubscribe', // 新闻订阅、订阅、取消订阅
        '/api', '/ajax', '/webhook', '/endpoint', '/graphql', // API、AJAX、Webhooks、端点、GraphQL
        '/static', '/assets', '/images', '/videos', '/css', '/js', // 静态资源、图片、视频、CSS、JS
        '/terms-of-service', '/cookie-policy', '/legal', '/cookies', '/privacy-policy', // 服务条款、Cookie 政策、法律声明、隐私政策
        '/resources', '/docs', '/guides', '/manual', '/tutorial', // 资源、文档、指南、手册、教程
        '/event', '/calendar', '/schedule', '/announcement', '/webinar', // 事件、日历、计划、公告、网络研讨会
        '/login', '/auth', '/oauth', '/signin', '/signup', '/social', // 登录、认证、OAuth、登录、注册、社交登录
        '/search-results', '/search/?q=', '/search?query=', '/search/?query=', // 搜索结果
        '/file', '/files', '/upload', '/downloads', '/saved', // 文件上传、下载、保存
        '/docs/', '/downloads/', '/web/', '/api/', '/service/', // 文件、服务、API 目录
        '/wp-admin', '/wp-login', '/wp-content', '/wp-includes', // WordPress 特有路径
        '/wp-json', '/index.php', '/cgi-bin', '/phpmyadmin', // PHP 和管理路径
        '/admin/', '/admin.php', '/admin_panel', '/admin_area' // 管理面板
    ]);

    const isExcludedLink = (href) => {
        return excludedLinkPatterns.some(pattern => href.includes(pattern));
    };

    // 预取链接
    const prefetchLink = async (url) => {
        // 清除之前的预取链接
        clearTimeout(prefetchTimeoutId);

        // 删除之前同样链接的预取
        document.querySelectorAll(`.tm-prefetch[href="${url}"]`).forEach(link => link.remove());

        return new Promise((resolve) => {
            const linkElement = document.createElement('link');
            linkElement.rel = 'prefetch';
            linkElement.href = url;
            linkElement.className = 'tm-prefetch';
            linkElement.onload = () => resolve(true);
            linkElement.onerror = () => resolve(false);
            document.head.appendChild(linkElement);
        });
    };

    // 创建或更新小窗
    const createOrUpdatePopupWindow = async (url, x, y) => {
        if (popupWindow && !popupWindow.closed) {
            if (popupWindow.location.href !== url) {
                popupWindow.location.href = url;
            }
            popupWindow.moveTo(x, y);
        } else {
            popupWindow = window.open(url, 'popupWindow', `width=${windowWidth},height=${windowHeight},top=${y},left=${x},scrollbars=yes,resizable=yes`);
        }

        if (popupWindow) {
            await new Promise((resolve) => {
                popupWindow.addEventListener('load', () => {
                    // 计算小窗的位置和大小
                    popupWindowRect = {
                        left: popupWindow.screenX,
                        top: popupWindow.screenY,
                        right: popupWindow.screenX + popupWindow.innerWidth,
                        bottom: popupWindow.screenY + popupWindow.innerHeight
                    };
                    resolve();
                });
            });

            // 确保当鼠标进入小窗时不关闭它
            popupWindow.addEventListener('focus', () => {
                isMouseOverPopup = true;
            });

            popupWindow.addEventListener('mousemove', () => {
                isMouseOverPopup = true;
            });
        }
    };

    // 关闭小窗
    const closePopupWindow = () => {
        if (popupWindow && !popupWindow.closed) {
            // 延迟关闭以确认鼠标真的在外面
            setTimeout(() => {
                if (!isMouseOverPopup) {
                    popupWindow.close();
                    popupWindow = null;
                    popupWindowRect = null;
                }
            }, 200); // 延迟时间,确保鼠标不会快速返回
        }
    };

    // 防抖动函数
    const debounce = (fn, delay) => {
        let timeoutId;
        return (...args) => {
            clearTimeout(timeoutId);
            timeoutId = setTimeout(() => fn.apply(this, args), delay);
        };
    };

    // 节流函数
    const throttle = (fn, limit) => {
        let lastFn, lastRan;
        return (...args) => {
            if (!lastRan) {
                fn.apply(this, args);
                lastRan = Date.now();
            } else {
                clearTimeout(lastFn);
                lastFn = setTimeout(() => {
                    if (Date.now() - lastRan >= limit) {
                        fn.apply(this, args);
                        lastRan = Date.now();
                    }
                }, limit - (Date.now() - lastRan));
            }
        };
    };

    // 处理鼠标悬停事件
    const handleMouseOver = async (event) => {
        // 检查事件是否发生在主窗口中
        if (window.name === 'popupWindow') return; // 防止在小窗中触发悬停行为

        const linkElement = event.target.closest('a');
        if (linkElement && linkElement.href && !isExcludedLink(linkElement.href)) {
            clearTimeout(hoverTimeoutId);
            clearTimeout(prefetchTimeoutId);

            // 1000ms 后预取链接
            prefetchTimeoutId = setTimeout(() => prefetchLink(linkElement.href), 1000);

            hoverTimeoutId = setTimeout(async () => {
                const { clientX: x, clientY: y } = event;
                await createOrUpdatePopupWindow(linkElement.href, x + 10, y + 10);
            }, hoverDelay);
        }
    };

    // 处理鼠标移出事件
    const handleMouseOut = (event) => {
        clearTimeout(hoverTimeoutId);
        clearTimeout(prefetchTimeoutId);

        // 移除预取链接
        document.querySelectorAll('.tm-prefetch').forEach(link => link.remove());

        if (popupWindow && !popupWindow.closed && popupWindowRect) {
            const { clientX: x, clientY: y } = event;
            const outsidePopupWindow = (
                x < popupWindowRect.left ||
                x > popupWindowRect.right ||
                y < popupWindowRect.top ||
                y > popupWindowRect.bottom
            );

            if (outsidePopupWindow) {
                isMouseOverPopup = false; // 更新状态为 false
                closePopupWindow();
            }
        }
    };

    // 处理窗口聚焦事件
    const handleWindowFocus = () => {
        closePopupWindow();
    };

    // 处理滚动和点击事件
    const handleDocumentScrollOrClick = throttle(closePopupWindow, 100);

    // 清理资源和事件监听器
    const cleanup = () => {
        clearTimeout(hoverTimeoutId);
        clearTimeout(prefetchTimeoutId);

        document.querySelectorAll('.tm-prefetch').forEach(link => link.remove());

        document.removeEventListener('mouseover', handleMouseOver, true);
        document.removeEventListener('mouseout', handleMouseOut, true);
        window.removeEventListener('focus', handleWindowFocus);
        document.removeEventListener('scroll', handleDocumentScrollOrClick, true);
        document.removeEventListener('click', handleDocumentScrollOrClick, true);

        closePopupWindow();
    };

    // 注册事件监听器
    const addEventListeners = () => {
        document.addEventListener('mouseover', handleMouseOver, { capture: true, passive: true });
        document.addEventListener('mouseout', handleMouseOut, { capture: true, passive: true });
        window.addEventListener('focus', handleWindowFocus);
        document.addEventListener('scroll', handleDocumentScrollOrClick, { capture: true, passive: true });
        document.addEventListener('click', handleDocumentScrollOrClick, true);
    };

    // 页面卸载时清理
    window.addEventListener('beforeunload', cleanup);

    // 初始事件监听器
    addEventListeners();

})();