Greasy Fork is available in English.
Preview NodeSeek topics in a floating right drawer. Original page remains interactive and no blur.
// ==UserScript==
// @name NodeSeek SidePeek
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Preview NodeSeek topics in a floating right drawer. Original page remains interactive and no blur.
// @author NodeSeek SidePeek
// @match https://www.nodeseek.com/*
// @run-at document-idle
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const CONFIG = {
drawerWidth: "clamp(360px, 42vw, 920px)",
drawerMode: "overlay", // 浮层模式,不挤压原页面
iframeMode: true,
};
let drawerRoot = null;
let iframe = null;
let resizeHandle = null;
let activeLink = null;
let isResizing = false;
let isDrawerOpen = false;
// 获取所有帖子链接
function getPostLinks() {
const links = [];
const items = document.querySelectorAll('.post-list-item .post-title a');
items.forEach(link => {
if (link instanceof HTMLAnchorElement && link.href && !link.closest('#ns-drawer-root')) {
links.push(link);
}
});
return links;
}
function isPostLink(link) {
if (!(link instanceof HTMLAnchorElement)) return false;
return /\/post-\d+-\d+/.test(link.pathname);
}
// 打开或更新侧边栏内容
function openOrUpdateDrawer(url, linkElement) {
if (!drawerRoot) createDrawer();
iframe.src = url;
if (activeLink) activeLink.classList.remove('ns-drawer-active-link');
activeLink = linkElement;
activeLink.classList.add('ns-drawer-active-link');
if (!isDrawerOpen) {
document.body.classList.add('ns-drawer-open');
drawerRoot.style.transform = 'translateX(0)';
isDrawerOpen = true;
}
document.body.style.paddingRight = '';
}
function closeDrawer() {
if (!drawerRoot) return;
document.body.classList.remove('ns-drawer-open');
drawerRoot.style.transform = 'translateX(100%)';
isDrawerOpen = false;
if (activeLink) {
activeLink.classList.remove('ns-drawer-active-link');
activeLink = null;
}
}
function createDrawer() {
if (drawerRoot) return;
drawerRoot = document.createElement('aside');
drawerRoot.id = 'ns-drawer-root';
drawerRoot.setAttribute('aria-hidden', 'true');
drawerRoot.innerHTML = `
<div class="ns-drawer-resize-handle" title="拖动调整宽度"></div>
<div class="ns-drawer-shell">
<div class="ns-drawer-header">
<div class="ns-drawer-title-group">
<div class="ns-drawer-eyebrow">NodeSeek 预览</div>
<h2 class="ns-drawer-title">点击帖子标题开始预览</h2>
</div>
<div class="ns-drawer-actions">
<button class="ns-drawer-close" title="关闭抽屉 (Esc)">✕</button>
</div>
</div>
<div class="ns-drawer-body">
<iframe class="ns-drawer-iframe" src="about:blank" title="帖子预览"></iframe>
</div>
</div>
`;
document.body.appendChild(drawerRoot);
iframe = drawerRoot.querySelector('.ns-drawer-iframe');
const closeBtn = drawerRoot.querySelector('.ns-drawer-close');
resizeHandle = drawerRoot.querySelector('.ns-drawer-resize-handle');
closeBtn.addEventListener('click', closeDrawer);
if (resizeHandle) {
resizeHandle.addEventListener('mousedown', startResize);
}
document.documentElement.style.setProperty('--ns-drawer-width', CONFIG.drawerWidth);
document.body.classList.add('ns-drawer-mode-overlay');
}
function startResize(e) {
if (e.button !== 0) return;
e.preventDefault();
isResizing = true;
document.body.classList.add('ns-drawer-resizing');
document.addEventListener('mousemove', onResizeMove);
document.addEventListener('mouseup', stopResize);
}
function onResizeMove(e) {
if (!isResizing) return;
let newWidth = window.innerWidth - e.clientX;
const minWidth = 320;
const maxWidth = Math.min(1400, window.innerWidth - 40);
newWidth = Math.min(maxWidth, Math.max(minWidth, newWidth));
document.documentElement.style.setProperty('--ns-drawer-width', `${newWidth}px`);
}
function stopResize() {
isResizing = false;
document.body.classList.remove('ns-drawer-resizing');
document.removeEventListener('mousemove', onResizeMove);
document.removeEventListener('mouseup', stopResize);
}
// 监听动态加载的链接
function observeLinks() {
const observer = new MutationObserver(() => {
attachLinkListeners();
});
observer.observe(document.body, { childList: true, subtree: true });
attachLinkListeners();
}
function attachLinkListeners() {
const links = getPostLinks();
for (const link of links) {
if (link.dataset.nsPreviewAttached) continue;
link.dataset.nsPreviewAttached = 'true';
link.addEventListener('click', (e) => {
if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey) return;
const url = link.href;
if (isPostLink(link) && url) {
e.preventDefault();
e.stopPropagation();
openOrUpdateDrawer(url, link);
}
});
}
}
// 点击外部区域关闭侧边栏(但不关闭更新链接时触发的点击)
function setupOutsideClick() {
document.addEventListener('click', (e) => {
if (!isDrawerOpen) return;
// 如果点击目标在抽屉内部,不关闭
if (drawerRoot && drawerRoot.contains(e.target)) return;
// 如果点击目标是帖子链接(且该链接已绑定预览行为),不关闭,因为 openOrUpdateDrawer 会处理更新
let target = e.target;
while (target && target !== document) {
if (target.tagName === 'A' && target.href && target.closest('.post-list-item .post-title a')) {
// 是帖子链接,交给链接自己的处理器,不关闭
return;
}
target = target.parentElement;
}
closeDrawer();
});
}
// 注入样式:无遮罩、无模糊
function injectStyles() {
GM_addStyle(`
:root {
--ns-drawer-width: clamp(360px, 42vw, 920px);
}
body.ns-drawer-resizing,
body.ns-drawer-resizing * {
cursor: ew-resize !important;
user-select: none !important;
}
#ns-drawer-root {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: var(--ns-drawer-width);
z-index: 2147483647;
transform: translateX(100%);
transition: transform 0.2s ease;
pointer-events: none;
}
body.ns-drawer-open #ns-drawer-root {
transform: translateX(0);
}
#ns-drawer-root .ns-drawer-resize-handle {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 8px;
transform: translateX(-50%);
cursor: ew-resize;
pointer-events: auto;
z-index: 10;
}
#ns-drawer-root .ns-drawer-resize-handle:hover {
background: rgba(0, 0, 0, 0.1);
}
#ns-drawer-root .ns-drawer-shell {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
background: var(--bg-main-color, #ffffff);
border-left: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15);
pointer-events: auto;
}
#ns-drawer-root .ns-drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
background: inherit;
flex-shrink: 0;
}
#ns-drawer-root .ns-drawer-title-group {
flex: 1;
min-width: 0;
}
#ns-drawer-root .ns-drawer-eyebrow {
font-size: 12px;
color: #888;
margin-bottom: 4px;
}
#ns-drawer-root .ns-drawer-title {
margin: 0;
font-size: 18px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#ns-drawer-root .ns-drawer-actions {
margin-left: 12px;
}
#ns-drawer-root .ns-drawer-close {
background: transparent;
border: none;
font-size: 24px;
line-height: 1;
cursor: pointer;
color: #666;
padding: 0 8px;
border-radius: 6px;
}
#ns-drawer-root .ns-drawer-close:hover {
background: rgba(0, 0, 0, 0.05);
color: #000;
}
#ns-drawer-root .ns-drawer-body {
flex: 1;
min-height: 0;
overflow: auto;
}
#ns-drawer-root .ns-drawer-iframe {
width: 100%;
height: 100%;
border: none;
display: block;
}
.ns-drawer-active-link {
color: #f90 !important;
font-weight: bold;
text-decoration: underline;
}
@media (max-width: 720px) {
#ns-drawer-root {
width: 100vw;
}
.ns-drawer-resize-handle {
display: none;
}
}
`);
}
function init() {
injectStyles();
createDrawer();
observeLinks();
setupOutsideClick();
}
init();
})();