Make quote links collapsible with indented hierarchy. Override panelBacklinks behavior.
// ==UserScript==
// @name 8chan Collapsible Thread Chains/Nested Inline Replies
// @version 1.5.9
// @description Make quote links collapsible with indented hierarchy. Override panelBacklinks behavior.
// @match https://8chan.moe/*/res/*
// @match https://8chan.se/*/res/*
// @grant GM_addStyle
// @grant GM.addStyle
// @license MIT
// @namespace https://greasyfork.org/users/1459581
// ==/UserScript==
(function() {
'use strict';
GM_addStyle(`
.collapsible-container {
margin-left: 20px;
padding-left: 5px;
margin-top: 8px;
}
.post-content.collapsed {
display: none;
}
.altBacklinks {
display: none !important;
}
.postCell.post-content {
border: none !important;
}
.innerPost {
border-top: 1px solid #474b53;
border-left: 1px solid #474b53;
width: auto;
max-width: none !important;
}
.preview-tooltip {
position: absolute;
z-index: 9999;
max-width: 500px;
background: #1b1b1b;
border: 1px solid #474b53;
padding: 10px;
pointer-events: none;
box-shadow: 0 2px 5px rgba(0,0,0,0.5);
}
`);
const linkContainers = new WeakMap();
function handlequickreply(event) {
const link = event.target.closest('a');
qr.showQr(link.href.match(/#q(\d+)/)[1]);
}
function handleQuoteClick(event) {
event.preventDefault();
event.stopPropagation();
const link = event.target.closest('a');
if (!link) return;
const rawHash = link.hash.includes('?') ? link.hash.split('?')[0] : link.hash;
const targetId = rawHash.substring(1).replace(/^q/, '');
const targetPost = document.getElementById(targetId);
if (!targetPost) return;
let container = linkContainers.get(link);
if (container) {
const clone = container.querySelector('.post-content');
clone.classList.toggle('collapsed');
return;
}
const level = link.closest('.collapsible-container')?.dataset.level || 0;
container = document.createElement('div');
container.className = 'collapsible-container';
container.dataset.level = parseInt(level) + 1;
const clone = targetPost.cloneNode(true);
clone.removeAttribute('id');
clone.classList.add('post-content');
processClonedElements(clone);
container.appendChild(clone);
const postContainer = link.closest('.innerPost');
if (postContainer) {
postContainer.appendChild(container);
} else {
link.parentNode.insertBefore(container, link.nextSibling);
}
linkContainers.set(link, container);
}
function processClonedElements(clone) {
clone.querySelectorAll('a.linkQuote, .panelBacklinks a').forEach(link => {
const href = link.getAttribute('href');
if (href && href.includes('#')) {
const cleanHash = href.split('#')[1].split('?')[0];
link.href = `#${cleanHash}`;
}
link.addEventListener('click', handleQuoteClick);
});
const firstQuoteLink = clone.querySelector('a.linkQuote');
if (firstQuoteLink) {
firstQuoteLink.addEventListener('click', handlequickreply);
}
}
function showPreview(link, pageX, pageY) {
const href = link.getAttribute('href');
if (!href) return;
const rawHash = href.includes('#') ? href.split('#')[1].split('?')[0] : '';
const targetId = rawHash.replace(/^q/, '');
if (!targetId) return;
const targetPost = document.getElementById(targetId);
if (!targetPost) return;
const innerPost = targetPost.querySelector('.innerPost');
if (!innerPost) return;
hidePreview();
const clone = innerPost.cloneNode(true);
clone.classList.add('preview-tooltip');
clone.style.left = `${pageX + 10}px`;
clone.style.top = `${pageY + 10}px`;
processClonedElements(clone);
document.body.appendChild(clone);
}
function hidePreview() {
const previews = document.querySelectorAll('.preview-tooltip');
previews.forEach(preview => preview.remove());
}
function initializeLinks() {
document.querySelectorAll('a.linkQuote, .panelBacklinks a').forEach(link => {
const href = link.getAttribute('href');
if (href?.includes('#')) {
link.href = `#${href.split('#')[1].split('?')[0]}`;
}
link.removeEventListener('click', handleQuoteClick);
link.addEventListener('click', handleQuoteClick);
});
document.querySelectorAll('span.panelBacklinks a').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
});
link.addEventListener('mouseenter', function(e) {
showPreview(e.currentTarget, e.pageX, e.pageY);
});
link.addEventListener('mouseleave', hidePreview);
});
}
initializeLinks();
new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
initializeLinks();
}
});
}).observe(document.body, { childList: true, subtree: true });
})();