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