8chan.moe Mod Shortcuts

Adds a larger checkbox and ban button to each post's title bar on 8chan.moe mod.js and thread pages, hidden by default with a toggle button in the OP title bar and clickable toggle text in non-OP posts.

// ==UserScript==
// @name        8chan.moe Mod Shortcuts
// @namespace   Violentmonkey Scripts
// @match       https://8chan.moe/mod.js?boardUri=*&threadId=*
// @match       https://8chan.moe/*/res/*
// @grant       none
// @version     1.8
// @author      Anonymous
// @license     MIT
// @description Adds a larger checkbox and ban button to each post's title bar on 8chan.moe mod.js and thread pages, hidden by default with a toggle button in the OP title bar and clickable toggle text in non-OP posts.
// ==/UserScript==

/* eslint-env browser, es6 */

(function() {
    'use strict';

    // Track toggle state (hidden by default)
    let areModShortcutsVisible = false;

    // Function to add a larger checkbox and ban button to a post
    function addModShortcuts(post) {
        const deletionCheckbox = post.querySelector('input.deletionCheckBox');
        if (!deletionCheckbox) return; // Skip if no deletion checkbox found

        // Find the postInfo or opHead div to append the new checkbox
        const postInfo = post.querySelector('.postInfo.title, .opHead.title');
        if (!postInfo) return;

        // Create a container for the large checkbox and ban button
        const modShortcutsContainer = document.createElement('span');
        modShortcutsContainer.className = 'mod-shortcuts-container';
        modShortcutsContainer.style.marginLeft = '12px';
        modShortcutsContainer.style.display = areModShortcutsVisible ? 'inline-flex' : 'none';
        modShortcutsContainer.style.verticalAlign = 'top';
        modShortcutsContainer.style.gap = '5px';

        // Create the large checkbox
        const largeCheckbox = document.createElement('input');
        largeCheckbox.type = 'checkbox';
        largeCheckbox.style.width = '48px'; // 2x larger (24px * 2 = 48px)
        largeCheckbox.style.height = '48px';
        largeCheckbox.style.border = '4px solid black'; // Thicker border
        largeCheckbox.style.cursor = 'pointer';

        // Sync the large checkbox with the original
        largeCheckbox.checked = deletionCheckbox.checked;
        largeCheckbox.addEventListener('change', () => {
            deletionCheckbox.checked = largeCheckbox.checked;
            // Trigger change event on original checkbox to ensure form functionality
            const event = new Event('change', { bubbles: true });
            deletionCheckbox.dispatchEvent(event);
        });

        // Sync the original checkbox with the large one
        deletionCheckbox.addEventListener('change', () => {
            largeCheckbox.checked = deletionCheckbox.checked;
        });

        // Create the ban button
        const banButton = document.createElement('button');
        banButton.type = 'button'; // Prevent form submission
        banButton.textContent = 'Ban';
        banButton.style.width = '48px';
        banButton.style.height = '48px';
        banButton.style.border = '4px solid black';
        banButton.style.cursor = 'pointer';
        banButton.style.backgroundColor = '#f0f0f0';
        banButton.style.borderRadius = '3px';
        banButton.style.fontSize = '14px';
        banButton.style.marginTop = '0.2em'; // Align with checkbox

        // Ban button functionality
        banButton.addEventListener('click', (event) => {
            event.preventDefault();
            event.stopPropagation();

            // Find the extraMenuButton for this post
            const extraMenuButton = post.querySelector('.extraMenuButton');
            if (!extraMenuButton) {
                console.log('No .extraMenuButton found for post');
                return;
            }

            // Simulate click to open the floating menu
            extraMenuButton.click();

            // Wait briefly for the menu to appear
            setTimeout(() => {
                const floatingMenu = post.querySelector('.floatingList.extraMenu');
                if (!floatingMenu) {
                    console.log('No .floatingList.extraMenu found after clicking extraMenuButton');
                    return;
                }

                // Find the "Ban" menu item
                const banItem = Array.from(floatingMenu.querySelectorAll('li')).find(li => li.textContent === 'Ban');
                if (!banItem) {
                    console.log('No "Ban" item found in extraMenu');
                    return;
                }

                // Simulate click on the Ban item
                banItem.click();
                console.log('Ban modal triggered for post');
            }, 100); // Delay to ensure menu appears
        });

        // Append checkbox and ban button to the container
        modShortcutsContainer.appendChild(largeCheckbox);
        modShortcutsContainer.appendChild(banButton);

        // Append the container to the postInfo div
        postInfo.appendChild(modShortcutsContainer);
    }

    // Function to toggle mod shortcuts and update all buttons and text
    function toggleModShortcuts() {
        areModShortcutsVisible = !areModShortcutsVisible;
        console.log(`Toggling mod shortcuts: areModShortcutsVisible=${areModShortcutsVisible}`);

        // Update all mod shortcuts visibility
        const containers = document.querySelectorAll('.mod-shortcuts-container');
        containers.forEach(container => {
            container.style.display = areModShortcutsVisible ? 'inline-flex' : 'none';
        });

        // Update OP toggle button
        const opToggleButton = document.querySelector('.toggle-shortcuts-button.op-toggle');
        if (opToggleButton) {
            opToggleButton.textContent = areModShortcutsVisible ? 'Hide Mod Shortcuts' : 'Show Mod Shortcuts';
            opToggleButton.style.backgroundColor = areModShortcutsVisible ? '#e0e0e0' : '#f0f0f0';
        }

        // Update non-OP toggle text
        const toggleTexts = document.querySelectorAll('.toggle-shortcuts-text .toggle-text-inner');
        toggleTexts.forEach(text => {
            text.textContent = areModShortcutsVisible ? 'Mod' : 'Mod';
        });
    }

    // Function to add toggle button to OP post
    function addToggleButton() {
        const opPost = document.querySelector('.innerOP');
        if (!opPost) {
            console.log('No .innerOP found for toggle button');
            return;
        }

        const opTitle = opPost.querySelector('.opHead.title, .postInfo.title');
        if (!opTitle) {
            console.log('No .opHead.title or .postInfo.title found in .innerOP');
            return;
        }

        // Remove existing toggle button to prevent duplicates
        const existingButton = opTitle.querySelector('.toggle-shortcuts-button.op-toggle');
        if (existingButton) {
            existingButton.remove();
        }

        // Create toggle button
        const toggleButton = document.createElement('button');
        toggleButton.type = 'button'; // Prevent form submission
        toggleButton.className = 'toggle-shortcuts-button op-toggle glowOnHover';
        toggleButton.textContent = areModShortcutsVisible ? 'Hide Mod Shortcuts' : 'Show Mod Shortcuts';
        toggleButton.style.cursor = 'pointer';
        toggleButton.style.marginLeft = '10px';
        toggleButton.style.padding = '6px 12px'; // Larger for OP
        toggleButton.style.border = '2px solid #ccc';
        toggleButton.style.backgroundColor = areModShortcutsVisible ? '#e0e0e0' : '#f0f0f0';
        toggleButton.style.borderRadius = '5px';
        toggleButton.style.verticalAlign = 'middle';
        toggleButton.style.fontSize = '16px'; // Larger for OP

        // Toggle functionality
        toggleButton.addEventListener('click', (event) => {
            event.preventDefault(); // Prevent any default behavior
            event.stopPropagation(); // Prevent bubbling
            console.log('OP toggle button clicked');
            toggleModShortcuts();
        });

        // Append button to OP title bar
        opTitle.appendChild(toggleButton);
        console.log('OP toggle button added to OP title bar');
    }

    // Function to add clickable toggle text to non-OP posts
    function addNonOpToggleText(post) {
        const postInfo = post.querySelector('.postInfo.title');
        if (!postInfo) {
            console.log('No .postInfo.title found in non-OP post');
            return;
        }

        const spanId = postInfo.querySelector('.spanId');
        const linkSelf = postInfo.querySelector('.linkSelf');
        if (!spanId || !linkSelf) {
            console.log(`Non-OP toggle text not added: spanId=${!!spanId}, linkSelf=${!!linkSelf}`);
            return;
        }

        // Remove existing toggle text to prevent duplicates
        const existingText = postInfo.querySelector('.toggle-shortcuts-text');
        if (existingText) {
            existingText.remove();
        }

        // Create wrapper span for [text]
        const toggleTextWrapper = document.createElement('span');
        toggleTextWrapper.className = 'toggle-shortcuts-text';
        toggleTextWrapper.style.display = 'inline-block';
        toggleTextWrapper.style.marginLeft = '5px';
        toggleTextWrapper.style.marginRight = '5px';

        // Create inner clickable text
        const toggleTextInner = document.createElement('span');
        toggleTextInner.className = 'toggle-text-inner';
        toggleTextInner.textContent = areModShortcutsVisible ? 'Mod' : 'Mod';
        toggleTextInner.style.cursor = 'pointer';
        toggleTextInner.style.fontSize = '14px'; // Match header text
        toggleTextInner.style.color = '#3366cc'; // Subtle blue for clickability
        toggleTextInner.style.textDecoration = 'none'; // Clean look
        toggleTextInner.addEventListener('mouseover', () => {
            toggleTextInner.style.textDecoration = 'underline'; // Hover effect
        });
        toggleTextInner.addEventListener('mouseout', () => {
            toggleTextInner.style.textDecoration = 'none';
        });

        // Toggle functionality
        toggleTextInner.addEventListener('click', (event) => {
            event.preventDefault(); // Prevent any default behavior
            event.stopPropagation(); // Prevent bubbling
            console.log('Non-OP toggle text clicked');
            toggleModShortcuts();
        });

        // Assemble [text]
        toggleTextWrapper.appendChild(document.createTextNode('['));
        toggleTextWrapper.appendChild(toggleTextInner);
        toggleTextWrapper.appendChild(document.createTextNode(']'));

        // Insert text between spanId and linkSelf
        linkSelf.insertAdjacentElement('beforebegin', toggleTextWrapper);
        console.log('Non-OP toggle text added between spanId and linkSelf');
    }

    // Process all posts (OP and replies)
    function processPosts() {
        // Handle OP
        const opPost = document.querySelector('.innerOP');
        if (opPost) {
            addModShortcuts(opPost);
            addToggleButton();
        }

        // Handle replies
        const replyPosts = document.querySelectorAll('.innerPost');
        replyPosts.forEach(post => {
            addModShortcuts(post);
            addNonOpToggleText(post);
        });
    }

    // Initial processing
    processPosts();

    // Observe for dynamically added posts (e.g., via auto-refresh or new replies)
    const observer = new MutationObserver((mutations) => {
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        // Check if the added node is a post
                        if (node.classList.contains('innerPost')) {
                            addModShortcuts(node);
                            addNonOpToggleText(node);
                        } else if (node.classList.contains('innerOP')) {
                            addModShortcuts(node);
                            addToggleButton();
                        }
                        // Check for posts within the added node
                        node.querySelectorAll('.innerPost').forEach(post => {
                            addModShortcuts(post);
                            addNonOpToggleText(post);
                        });
                        node.querySelectorAll('.innerOP').forEach(post => {
                            addModShortcuts(post);
                            addToggleButton();
                        });
                    }
                });
            }
        });
    });

    // Observe changes in the thread list
    const threadList = document.getElementById('threadList');
    if (threadList) {
        observer.observe(threadList, {
            childList: true,
            subtree: true
        });
    } else {
        console.log('No #threadList found for MutationObserver');
    }
})();