8chan Collapsible Thread Chains/Nested Inline Replies

Make quote links collapsible with indented hierarchy. Override panelBacklinks behavior.

Fra 19.04.2025. Se den seneste versjonen.

// ==UserScript==
// @name         8chan Collapsible Thread Chains/Nested Inline Replies
// @version      1.6.0
// @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://greatest.deepsurf.us/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;
        }
    `);

    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 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);
        });

        // Add the panelBacklinks prevention
        document.querySelectorAll('span.panelBacklinks a').forEach(link => {
            link.addEventListener('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
            });
        });
    }

    initializeLinks();

    new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.addedNodes.length) {
                initializeLinks();
            }
        });
    }).observe(document.body, { childList: true, subtree: true });
})();