Greasy Fork is available in English.
悬停链接打开小窗并优化资源管理。重用打开的小窗以节省资源,同时保留其位置和大小。包含悬停时间和窗口大小设置,并改进了链接预取功能和资源管理效率。修复了鼠标移动到小窗时意外关闭的问题。支持 URL 路径和链接属性的排除。
当前为
// ==UserScript==
// @name 悬停预览
// @version 3.8
// @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',
'/static', '/assets', '/images', '/videos', '/css', '/js',
'/terms-of-service', '/cookie-policy', '/legal', '/cookies', '/privacy-policy',
'/resources', '/docs', '/guides', '/manual', '/tutorial',
'/event', '/calendar', '/schedule', '/announcement', '/webinar',
'/login', '/auth', '/oauth', '/signin', '/signup', '/social',
'/search-results', '/search/?q=', '/search?query=', '/search/?query=',
'/file', '/files', '/upload', '/downloads', '/saved',
'/docs/', '/downloads/', '/web/', '/api/', '/service/',
'/wp-admin', '/wp-login', '/wp-content', '/wp-includes',
'/wp-json', '/index.php', '/cgi-bin', '/phpmyadmin',
'/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`);
// 立即注册事件监听器,确保即使页面尚未加载完成也不会意外关闭
popupWindow.addEventListener('mouseover', () => {
isMouseOverPopup = true;
});
popupWindow.addEventListener('mouseout', () => {
isMouseOverPopup = false;
closePopupWindow();
});
}
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();
});
});
}
};
// 关闭小窗
const closePopupWindow = () => {
if (popupWindow && !popupWindow.closed && !isMouseOverPopup) {
setTimeout(() => {
if (!isMouseOverPopup) {
popupWindow.close();
popupWindow = null;
popupWindowRect = null;
}
}, 200);
}
};
// 处理鼠标移出事件
const handleMouseOut = (event) => {
clearTimeout(hoverTimeoutId);
clearTimeout(prefetchTimeoutId);
document.querySelectorAll('.tm-prefetch').forEach(link => link.remove());
if (popupWindow && !popupWindow.closed && popupWindowRect && !isMouseOverPopup) {
const { clientX: x, clientY: y } = event;
const outsidePopupWindow = (
x < popupWindowRect.left ||
x > popupWindowRect.right ||
y < popupWindowRect.top ||
y > popupWindowRect.bottom
);
if (outsidePopupWindow) {
closePopupWindow();
}
}
};
// 处理鼠标悬停事件
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);
prefetchTimeoutId = setTimeout(() => prefetchLink(linkElement.href), 200);
hoverTimeoutId = setTimeout(async () => {
const { clientX: x, clientY: y } = event;
await createOrUpdatePopupWindow(linkElement.href, x + 10, y + 10);
}, hoverDelay);
}
};
// 防抖动函数
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 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);
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 });
document.addEventListener('scroll', handleDocumentScrollOrClick, { capture: true, passive: true });
document.addEventListener('click', handleDocumentScrollOrClick, true);
};
window.addEventListener('beforeunload', cleanup);
addEventListeners();
})();