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