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.

  1. // ==UserScript==
  2. // @name 8chan.moe Mod Shortcuts
  3. // @namespace Violentmonkey Scripts
  4. // @match https://8chan.moe/mod.js?boardUri=*&threadId=*
  5. // @match https://8chan.moe/*/res/*
  6. // @grant none
  7. // @version 1.8
  8. // @author Anonymous
  9. // @license MIT
  10. // @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.
  11. // ==/UserScript==
  12.  
  13. /* eslint-env browser, es6 */
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // Track toggle state (hidden by default)
  19. let areModShortcutsVisible = false;
  20.  
  21. // Function to add a larger checkbox and ban button to a post
  22. function addModShortcuts(post) {
  23. const deletionCheckbox = post.querySelector('input.deletionCheckBox');
  24. if (!deletionCheckbox) return; // Skip if no deletion checkbox found
  25.  
  26. // Find the postInfo or opHead div to append the new checkbox
  27. const postInfo = post.querySelector('.postInfo.title, .opHead.title');
  28. if (!postInfo) return;
  29.  
  30. // Create a container for the large checkbox and ban button
  31. const modShortcutsContainer = document.createElement('span');
  32. modShortcutsContainer.className = 'mod-shortcuts-container';
  33. modShortcutsContainer.style.marginLeft = '12px';
  34. modShortcutsContainer.style.display = areModShortcutsVisible ? 'inline-flex' : 'none';
  35. modShortcutsContainer.style.verticalAlign = 'top';
  36. modShortcutsContainer.style.gap = '5px';
  37.  
  38. // Create the large checkbox
  39. const largeCheckbox = document.createElement('input');
  40. largeCheckbox.type = 'checkbox';
  41. largeCheckbox.style.width = '48px'; // 2x larger (24px * 2 = 48px)
  42. largeCheckbox.style.height = '48px';
  43. largeCheckbox.style.border = '4px solid black'; // Thicker border
  44. largeCheckbox.style.cursor = 'pointer';
  45.  
  46. // Sync the large checkbox with the original
  47. largeCheckbox.checked = deletionCheckbox.checked;
  48. largeCheckbox.addEventListener('change', () => {
  49. deletionCheckbox.checked = largeCheckbox.checked;
  50. // Trigger change event on original checkbox to ensure form functionality
  51. const event = new Event('change', { bubbles: true });
  52. deletionCheckbox.dispatchEvent(event);
  53. });
  54.  
  55. // Sync the original checkbox with the large one
  56. deletionCheckbox.addEventListener('change', () => {
  57. largeCheckbox.checked = deletionCheckbox.checked;
  58. });
  59.  
  60. // Create the ban button
  61. const banButton = document.createElement('button');
  62. banButton.type = 'button'; // Prevent form submission
  63. banButton.textContent = 'Ban';
  64. banButton.style.width = '48px';
  65. banButton.style.height = '48px';
  66. banButton.style.border = '4px solid black';
  67. banButton.style.cursor = 'pointer';
  68. banButton.style.backgroundColor = '#f0f0f0';
  69. banButton.style.borderRadius = '3px';
  70. banButton.style.fontSize = '14px';
  71. banButton.style.marginTop = '0.2em'; // Align with checkbox
  72.  
  73. // Ban button functionality
  74. banButton.addEventListener('click', (event) => {
  75. event.preventDefault();
  76. event.stopPropagation();
  77.  
  78. // Find the extraMenuButton for this post
  79. const extraMenuButton = post.querySelector('.extraMenuButton');
  80. if (!extraMenuButton) {
  81. console.log('No .extraMenuButton found for post');
  82. return;
  83. }
  84.  
  85. // Simulate click to open the floating menu
  86. extraMenuButton.click();
  87.  
  88. // Wait briefly for the menu to appear
  89. setTimeout(() => {
  90. const floatingMenu = post.querySelector('.floatingList.extraMenu');
  91. if (!floatingMenu) {
  92. console.log('No .floatingList.extraMenu found after clicking extraMenuButton');
  93. return;
  94. }
  95.  
  96. // Find the "Ban" menu item
  97. const banItem = Array.from(floatingMenu.querySelectorAll('li')).find(li => li.textContent === 'Ban');
  98. if (!banItem) {
  99. console.log('No "Ban" item found in extraMenu');
  100. return;
  101. }
  102.  
  103. // Simulate click on the Ban item
  104. banItem.click();
  105. console.log('Ban modal triggered for post');
  106. }, 100); // Delay to ensure menu appears
  107. });
  108.  
  109. // Append checkbox and ban button to the container
  110. modShortcutsContainer.appendChild(largeCheckbox);
  111. modShortcutsContainer.appendChild(banButton);
  112.  
  113. // Append the container to the postInfo div
  114. postInfo.appendChild(modShortcutsContainer);
  115. }
  116.  
  117. // Function to toggle mod shortcuts and update all buttons and text
  118. function toggleModShortcuts() {
  119. areModShortcutsVisible = !areModShortcutsVisible;
  120. console.log(`Toggling mod shortcuts: areModShortcutsVisible=${areModShortcutsVisible}`);
  121.  
  122. // Update all mod shortcuts visibility
  123. const containers = document.querySelectorAll('.mod-shortcuts-container');
  124. containers.forEach(container => {
  125. container.style.display = areModShortcutsVisible ? 'inline-flex' : 'none';
  126. });
  127.  
  128. // Update OP toggle button
  129. const opToggleButton = document.querySelector('.toggle-shortcuts-button.op-toggle');
  130. if (opToggleButton) {
  131. opToggleButton.textContent = areModShortcutsVisible ? 'Hide Mod Shortcuts' : 'Show Mod Shortcuts';
  132. opToggleButton.style.backgroundColor = areModShortcutsVisible ? '#e0e0e0' : '#f0f0f0';
  133. }
  134.  
  135. // Update non-OP toggle text
  136. const toggleTexts = document.querySelectorAll('.toggle-shortcuts-text .toggle-text-inner');
  137. toggleTexts.forEach(text => {
  138. text.textContent = areModShortcutsVisible ? 'Mod' : 'Mod';
  139. });
  140. }
  141.  
  142. // Function to add toggle button to OP post
  143. function addToggleButton() {
  144. const opPost = document.querySelector('.innerOP');
  145. if (!opPost) {
  146. console.log('No .innerOP found for toggle button');
  147. return;
  148. }
  149.  
  150. const opTitle = opPost.querySelector('.opHead.title, .postInfo.title');
  151. if (!opTitle) {
  152. console.log('No .opHead.title or .postInfo.title found in .innerOP');
  153. return;
  154. }
  155.  
  156. // Remove existing toggle button to prevent duplicates
  157. const existingButton = opTitle.querySelector('.toggle-shortcuts-button.op-toggle');
  158. if (existingButton) {
  159. existingButton.remove();
  160. }
  161.  
  162. // Create toggle button
  163. const toggleButton = document.createElement('button');
  164. toggleButton.type = 'button'; // Prevent form submission
  165. toggleButton.className = 'toggle-shortcuts-button op-toggle glowOnHover';
  166. toggleButton.textContent = areModShortcutsVisible ? 'Hide Mod Shortcuts' : 'Show Mod Shortcuts';
  167. toggleButton.style.cursor = 'pointer';
  168. toggleButton.style.marginLeft = '10px';
  169. toggleButton.style.padding = '6px 12px'; // Larger for OP
  170. toggleButton.style.border = '2px solid #ccc';
  171. toggleButton.style.backgroundColor = areModShortcutsVisible ? '#e0e0e0' : '#f0f0f0';
  172. toggleButton.style.borderRadius = '5px';
  173. toggleButton.style.verticalAlign = 'middle';
  174. toggleButton.style.fontSize = '16px'; // Larger for OP
  175.  
  176. // Toggle functionality
  177. toggleButton.addEventListener('click', (event) => {
  178. event.preventDefault(); // Prevent any default behavior
  179. event.stopPropagation(); // Prevent bubbling
  180. console.log('OP toggle button clicked');
  181. toggleModShortcuts();
  182. });
  183.  
  184. // Append button to OP title bar
  185. opTitle.appendChild(toggleButton);
  186. console.log('OP toggle button added to OP title bar');
  187. }
  188.  
  189. // Function to add clickable toggle text to non-OP posts
  190. function addNonOpToggleText(post) {
  191. const postInfo = post.querySelector('.postInfo.title');
  192. if (!postInfo) {
  193. console.log('No .postInfo.title found in non-OP post');
  194. return;
  195. }
  196.  
  197. const spanId = postInfo.querySelector('.spanId');
  198. const linkSelf = postInfo.querySelector('.linkSelf');
  199. if (!spanId || !linkSelf) {
  200. console.log(`Non-OP toggle text not added: spanId=${!!spanId}, linkSelf=${!!linkSelf}`);
  201. return;
  202. }
  203.  
  204. // Remove existing toggle text to prevent duplicates
  205. const existingText = postInfo.querySelector('.toggle-shortcuts-text');
  206. if (existingText) {
  207. existingText.remove();
  208. }
  209.  
  210. // Create wrapper span for [text]
  211. const toggleTextWrapper = document.createElement('span');
  212. toggleTextWrapper.className = 'toggle-shortcuts-text';
  213. toggleTextWrapper.style.display = 'inline-block';
  214. toggleTextWrapper.style.marginLeft = '5px';
  215. toggleTextWrapper.style.marginRight = '5px';
  216.  
  217. // Create inner clickable text
  218. const toggleTextInner = document.createElement('span');
  219. toggleTextInner.className = 'toggle-text-inner';
  220. toggleTextInner.textContent = areModShortcutsVisible ? 'Mod' : 'Mod';
  221. toggleTextInner.style.cursor = 'pointer';
  222. toggleTextInner.style.fontSize = '14px'; // Match header text
  223. toggleTextInner.style.color = '#3366cc'; // Subtle blue for clickability
  224. toggleTextInner.style.textDecoration = 'none'; // Clean look
  225. toggleTextInner.addEventListener('mouseover', () => {
  226. toggleTextInner.style.textDecoration = 'underline'; // Hover effect
  227. });
  228. toggleTextInner.addEventListener('mouseout', () => {
  229. toggleTextInner.style.textDecoration = 'none';
  230. });
  231.  
  232. // Toggle functionality
  233. toggleTextInner.addEventListener('click', (event) => {
  234. event.preventDefault(); // Prevent any default behavior
  235. event.stopPropagation(); // Prevent bubbling
  236. console.log('Non-OP toggle text clicked');
  237. toggleModShortcuts();
  238. });
  239.  
  240. // Assemble [text]
  241. toggleTextWrapper.appendChild(document.createTextNode('['));
  242. toggleTextWrapper.appendChild(toggleTextInner);
  243. toggleTextWrapper.appendChild(document.createTextNode(']'));
  244.  
  245. // Insert text between spanId and linkSelf
  246. linkSelf.insertAdjacentElement('beforebegin', toggleTextWrapper);
  247. console.log('Non-OP toggle text added between spanId and linkSelf');
  248. }
  249.  
  250. // Process all posts (OP and replies)
  251. function processPosts() {
  252. // Handle OP
  253. const opPost = document.querySelector('.innerOP');
  254. if (opPost) {
  255. addModShortcuts(opPost);
  256. addToggleButton();
  257. }
  258.  
  259. // Handle replies
  260. const replyPosts = document.querySelectorAll('.innerPost');
  261. replyPosts.forEach(post => {
  262. addModShortcuts(post);
  263. addNonOpToggleText(post);
  264. });
  265. }
  266.  
  267. // Initial processing
  268. processPosts();
  269.  
  270. // Observe for dynamically added posts (e.g., via auto-refresh or new replies)
  271. const observer = new MutationObserver((mutations) => {
  272. mutations.forEach(mutation => {
  273. if (mutation.addedNodes.length) {
  274. mutation.addedNodes.forEach(node => {
  275. if (node.nodeType === Node.ELEMENT_NODE) {
  276. // Check if the added node is a post
  277. if (node.classList.contains('innerPost')) {
  278. addModShortcuts(node);
  279. addNonOpToggleText(node);
  280. } else if (node.classList.contains('innerOP')) {
  281. addModShortcuts(node);
  282. addToggleButton();
  283. }
  284. // Check for posts within the added node
  285. node.querySelectorAll('.innerPost').forEach(post => {
  286. addModShortcuts(post);
  287. addNonOpToggleText(post);
  288. });
  289. node.querySelectorAll('.innerOP').forEach(post => {
  290. addModShortcuts(post);
  291. addToggleButton();
  292. });
  293. }
  294. });
  295. }
  296. });
  297. });
  298.  
  299. // Observe changes in the thread list
  300. const threadList = document.getElementById('threadList');
  301. if (threadList) {
  302. observer.observe(threadList, {
  303. childList: true,
  304. subtree: true
  305. });
  306. } else {
  307. console.log('No #threadList found for MutationObserver');
  308. }
  309. })();