8chan Nested Inline Reply

Make Nested Inline Reply like 4chanX

Устаревшая версия за 22.04.2025. Перейдите к последней версии.

// ==UserScript==
// @name         8chan Nested Inline Reply
// @version      2.0.4
// @description  Make Nested Inline Reply like 4chanX
// @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;
            border-left: 1px solid #474b53;
            border-top: 1px solid #474b53;
        }
        .post-content.collapsed {
            display: none;
        }
        .altBacklinks {
            display: none !important;
        }
        .postCell.post-content {
            border: none !important;
        }
        .innerPost {
            width: auto;
            max-width: none !important;
        }
        .moved-post {
            position: relative;
            opacity: 0.9;
        }
        .linkQuote.toggled, .panelBacklinks a.toggled {
            color: #9a5;
        }
        .post-placeholder {
            display: none;
            padding: 5px;
            background: rgba(50, 50, 50, 0.3);
            border: 1px dashed #474b53;
            font-style: italic;
            color: #8c8c8c;
            text-align: center;
            margin: 5px 0;
        }
        .placeholder-visible {
            display: block;
        }
    `);

    const movedPosts = new Map();
    const linkContainers = new Map();
    const originalPosts = new Map();

    document.body.addEventListener('click', function(event) {
        const target = event.target;

        if (target.classList.contains('linkQuote') && event.ctrlKey) {
            const postId = target.href.match(/#q?(\d+)/)?.[1];
            if (postId && typeof qr !== 'undefined' && qr.showQr) {
                qr.showQr(postId);
                event.preventDefault();
                return;
            }
        }

        // Handle restore post link clicks
        if (target.classList.contains('restore-post-link')) {
            event.preventDefault();
            const postId = target.dataset.postId;
            if (postId && movedPosts.has(postId)) {
                restorePost(postId);
            }
            return;
        }

        // Handle backlink panel direct restoration
        if ((target.parentNode && target.parentNode.classList.contains('panelBacklinks')) && !event.ctrlKey) {
            const link = target.closest('a');
            if (!link) return;

            const rawHash = link.hash.includes('?') ? link.hash.split('?')[0] : link.hash;
            const targetId = rawHash.substring(1).replace(/^q/, '');

            if (movedPosts.has(targetId)) {
                event.preventDefault();
                restorePost(targetId);
                return;
            }
        }

        // Modified condition: Only process .panelBacklinks
        if ((target.parentNode && target.parentNode.classList.contains('panelBacklinks'))) {
            event.preventDefault();

            const link = target.closest('a');
            if (!link) return;

            const rawHash = link.hash.includes('?') ? link.hash.split('?')[0] : link.hash;
            const targetId = rawHash.substring(1).replace(/^q/, '');

            if (linkContainers.has(link)) {
                const container = linkContainers.get(link);
                const content = container.querySelector('.post-content');

                if (content) {
                    const wasCollapsed = content.classList.contains('collapsed');
                    content.classList.toggle('collapsed');
                    link.classList.toggle('toggled');

                    if (!wasCollapsed && movedPosts.has(targetId)) {
                        const postData = movedPosts.get(targetId);
                        postData.links.delete(link);
                        if (postData.links.size === 0) {
                            restorePost(targetId);
                        }
                    }
                }
                return;
            }

            if (!originalPosts.has(targetId)) {
                let targetPost = document.getElementById(targetId);
                if (!targetPost) return;
                originalPosts.set(targetId, targetPost);
            }

            let postToUse;

            if (movedPosts.has(targetId)) {
                postToUse = movedPosts.get(targetId).element;
            } else {
                postToUse = document.getElementById(targetId) || originalPosts.get(targetId);
                if (!postToUse) return;
            }

            const level = link.closest('.collapsible-container')?.dataset.level || 0;
            const container = document.createElement('div');
            container.className = 'collapsible-container';
            container.dataset.level = parseInt(level) + 1;

            movePostToContainer(targetId, postToUse, container, link);

            const postContainer = link.closest('.innerPost');
            if (postContainer) {
                postContainer.appendChild(container);
            } else {
                link.parentNode.insertBefore(container, link.nextSibling);
            }

            linkContainers.set(link, container);
            link.classList.add('toggled');
        }
    });

    function movePostToContainer(postId, postToUse, container, link) {
        if (movedPosts.has(postId)) {
            const postData = movedPosts.get(postId);
            const lightClone = postData.element.cloneNode(true);
            lightClone.classList.add('post-content', 'moved-post');
            lightClone.setAttribute('data-original-id', postId);
            container.appendChild(lightClone);
            postData.links.add(link);
            return;
        }

        if (!document.getElementById(postId) && originalPosts.has(postId)) {
            const originalPost = originalPosts.get(postId);
            const clone = originalPost.cloneNode(true);
            clone.classList.add('post-content', 'moved-post');
            clone.setAttribute('data-original-id', postId);
            container.appendChild(clone);
            const placeholder = document.createElement('div');
            placeholder.className = 'post-placeholder';
            movedPosts.set(postId, {
                element: clone,
                placeholder: placeholder,
                links: new Set([link])
            });
            return;
        }

        const placeholder = document.createElement('div');
        placeholder.className = 'post-placeholder placeholder-visible';
        placeholder.innerHTML = `Post moved <a href="#" class="restore-post-link" data-post-id="${postId}">Restore</a>`;
        postToUse.parentNode.insertBefore(placeholder, postToUse);
        postToUse.classList.add('post-content', 'moved-post');
        postToUse.setAttribute('data-original-id', postId);
        container.appendChild(postToUse);
        movedPosts.set(postId, {
            element: postToUse,
            placeholder: placeholder,
            links: new Set([link])
        });
    }

    function restorePost(postId) {
        if (!movedPosts.has(postId)) return;

        const {element, placeholder, links} = movedPosts.get(postId);

        links.forEach(link => {
            if (linkContainers.has(link)) {
                const container = linkContainers.get(link);
                container.remove();
                linkContainers.delete(link);
                link.classList.remove('toggled');
            }
        });

        document.querySelectorAll(`.moved-post[data-original-id="${postId}"]`).forEach(instance => {
            if (instance !== element) {
                instance.remove();
            }
        });

        if (placeholder.parentNode) {
            placeholder.parentNode.insertBefore(element, placeholder);
            placeholder.remove();
        }

        element.classList.remove('post-content', 'moved-post');
        element.removeAttribute('data-original-id'); // Remove the data-original-id attribute

        movedPosts.delete(postId);

        if (!originalPosts.has(postId)) {
            originalPosts.set(postId, element);
        }
    }

    function cleanupBacklinks() {
        document.querySelectorAll('span.panelBacklinks a').forEach(link => {
            const href = link.getAttribute('href');
            if (href?.includes('#')) {
                link.href = `#${href.split('#')[1].split('?')[0]}`;
            }
        });
    }

    const observer = new MutationObserver((mutations) => {
        let shouldProcess = false;

        for (const mutation of mutations) {
            if (mutation.addedNodes.length) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === 1 &&
                       (node.classList?.contains('post') ||
                        node.querySelector?.('.post, .linkQuote, .panelBacklinks'))) {
                        shouldProcess = true;
                        break;
                    }
                }
                if (shouldProcess) break;
            }
        }

        if (shouldProcess) {
            cleanupBacklinks();
        }
    });

    const threadContainer = document.querySelector('.thread');
    if (threadContainer) {
        observer.observe(threadContainer, { childList: true, subtree: true });
    } else {
        observer.observe(document.body, { childList: true, subtree: false });
    }

    cleanupBacklinks();
})();