8chan Collapsible Thread Chains/Nested Inline Replies

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

As of 19/04/2025. See the latest version.

  1. // ==UserScript==
  2. // @name 8chan Collapsible Thread Chains/Nested Inline Replies
  3. // @version 1.5.9
  4. // @description Make quote links collapsible with indented hierarchy. Override panelBacklinks behavior.
  5. // @match https://8chan.moe/*/res/*
  6. // @match https://8chan.se/*/res/*
  7. // @grant GM_addStyle
  8. // @grant GM.addStyle
  9. // @license MIT
  10. // @namespace https://greatest.deepsurf.us/users/1459581
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. GM_addStyle(`
  17. .collapsible-container {
  18. margin-left: 20px;
  19. padding-left: 5px;
  20. margin-top: 8px;
  21. }
  22. .post-content.collapsed {
  23. display: none;
  24. }
  25. .altBacklinks {
  26. display: none !important;
  27. }
  28. .postCell.post-content {
  29. border: none !important;
  30. }
  31. .innerPost {
  32. border-top: 1px solid #474b53;
  33. border-left: 1px solid #474b53;
  34. width: auto;
  35. max-width: none !important;
  36. }
  37. .preview-tooltip {
  38. position: absolute;
  39. z-index: 9999;
  40. max-width: 500px;
  41. background: #1b1b1b;
  42. border: 1px solid #474b53;
  43. padding: 10px;
  44. pointer-events: none;
  45. box-shadow: 0 2px 5px rgba(0,0,0,0.5);
  46. }
  47. `);
  48.  
  49. const linkContainers = new WeakMap();
  50.  
  51. function handlequickreply(event) {
  52. const link = event.target.closest('a');
  53. qr.showQr(link.href.match(/#q(\d+)/)[1]);
  54. }
  55.  
  56. function handleQuoteClick(event) {
  57. event.preventDefault();
  58. event.stopPropagation();
  59. const link = event.target.closest('a');
  60. if (!link) return;
  61.  
  62. const rawHash = link.hash.includes('?') ? link.hash.split('?')[0] : link.hash;
  63. const targetId = rawHash.substring(1).replace(/^q/, '');
  64. const targetPost = document.getElementById(targetId);
  65.  
  66. if (!targetPost) return;
  67.  
  68. let container = linkContainers.get(link);
  69. if (container) {
  70. const clone = container.querySelector('.post-content');
  71. clone.classList.toggle('collapsed');
  72. return;
  73. }
  74.  
  75. const level = link.closest('.collapsible-container')?.dataset.level || 0;
  76. container = document.createElement('div');
  77. container.className = 'collapsible-container';
  78. container.dataset.level = parseInt(level) + 1;
  79.  
  80. const clone = targetPost.cloneNode(true);
  81. clone.removeAttribute('id');
  82. clone.classList.add('post-content');
  83.  
  84. processClonedElements(clone);
  85.  
  86. container.appendChild(clone);
  87.  
  88. const postContainer = link.closest('.innerPost');
  89. if (postContainer) {
  90. postContainer.appendChild(container);
  91. } else {
  92. link.parentNode.insertBefore(container, link.nextSibling);
  93. }
  94.  
  95. linkContainers.set(link, container);
  96. }
  97.  
  98. function processClonedElements(clone) {
  99. clone.querySelectorAll('a.linkQuote, .panelBacklinks a').forEach(link => {
  100. const href = link.getAttribute('href');
  101. if (href && href.includes('#')) {
  102. const cleanHash = href.split('#')[1].split('?')[0];
  103. link.href = `#${cleanHash}`;
  104. }
  105. link.addEventListener('click', handleQuoteClick);
  106. });
  107.  
  108. const firstQuoteLink = clone.querySelector('a.linkQuote');
  109. if (firstQuoteLink) {
  110. firstQuoteLink.addEventListener('click', handlequickreply);
  111. }
  112. }
  113.  
  114. function showPreview(link, pageX, pageY) {
  115. const href = link.getAttribute('href');
  116. if (!href) return;
  117.  
  118. const rawHash = href.includes('#') ? href.split('#')[1].split('?')[0] : '';
  119. const targetId = rawHash.replace(/^q/, '');
  120. if (!targetId) return;
  121.  
  122. const targetPost = document.getElementById(targetId);
  123. if (!targetPost) return;
  124.  
  125. const innerPost = targetPost.querySelector('.innerPost');
  126. if (!innerPost) return;
  127.  
  128. hidePreview();
  129.  
  130. const clone = innerPost.cloneNode(true);
  131. clone.classList.add('preview-tooltip');
  132. clone.style.left = `${pageX + 10}px`;
  133. clone.style.top = `${pageY + 10}px`;
  134.  
  135. processClonedElements(clone);
  136. document.body.appendChild(clone);
  137. }
  138.  
  139. function hidePreview() {
  140. const previews = document.querySelectorAll('.preview-tooltip');
  141. previews.forEach(preview => preview.remove());
  142. }
  143.  
  144. function initializeLinks() {
  145. document.querySelectorAll('a.linkQuote, .panelBacklinks a').forEach(link => {
  146. const href = link.getAttribute('href');
  147. if (href?.includes('#')) {
  148. link.href = `#${href.split('#')[1].split('?')[0]}`;
  149. }
  150. link.removeEventListener('click', handleQuoteClick);
  151. link.addEventListener('click', handleQuoteClick);
  152. });
  153.  
  154. document.querySelectorAll('span.panelBacklinks a').forEach(link => {
  155. link.addEventListener('click', function(e) {
  156. e.preventDefault();
  157. e.stopPropagation();
  158. });
  159. link.addEventListener('mouseenter', function(e) {
  160. showPreview(e.currentTarget, e.pageX, e.pageY);
  161. });
  162. link.addEventListener('mouseleave', hidePreview);
  163. });
  164. }
  165.  
  166. initializeLinks();
  167.  
  168. new MutationObserver((mutations) => {
  169. mutations.forEach((mutation) => {
  170. if (mutation.addedNodes.length) {
  171. initializeLinks();
  172. }
  173. });
  174. }).observe(document.body, { childList: true, subtree: true });
  175. })();