Guest User

8chan Collapsible Thread Chains (Toggle with Auto-Restore) (Updated by /alter/)

a guest
Apr 20th, 2025
26
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.53 KB | None | 0 0
  1. // ==UserScript==
  2. // @name 8chan Collapsible Thread Chains (Toggle with Auto-Restore) (Updated by /alter/)
  3. // @version 2.0.3
  4. // @description Make quote links collapsible with indented hierarchy, restore posts when toggled off
  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://greasyfork.org/users/1459581
  11. // @downloadURL https://update.greasyfork.org/scripts/533331/8chan%20Collapsible%20Thread%20Chains%20%28Toggle%20with%20Auto-Restore%29.user.js
  12. // @updateURL https://update.greasyfork.org/scripts/533331/8chan%20Collapsible%20Thread%20Chains%20%28Toggle%20with%20Auto-Restore%29.meta.js
  13. // ==/UserScript==
  14.  
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. GM_addStyle(`
  20. .collapsible-container {
  21. margin-left: 20px;
  22. padding-left: 5px;
  23. margin-top: 8px;
  24. }
  25. .post-content.collapsed {
  26. display: none;
  27. }
  28. .altBacklinks {
  29. display: none !important;
  30. }
  31. .postCell.post-content {
  32. border: none !important;
  33. }
  34. .innerPost {
  35. width: auto;
  36. max-width: none !important;#474b53
  37.  
  38. }
  39. .moved-post {
  40. position: relative;
  41. opacity: 0.9;
  42. }
  43. .linkQuote.toggled, .panelBacklinks a.toggled {
  44. color: #9a5;
  45. }
  46. .post-placeholder {
  47. display: none;
  48. padding: 5px;
  49. background: rgba(50, 50, 50, 0.3);
  50. border: 1px dashed #474b53;
  51. font-style: italic;
  52. color: #8c8c8c;
  53. text-align: center;
  54. margin: 5px 0;
  55. }
  56. .placeholder-visible {
  57. display: block;
  58. }
  59. .post-restored {
  60. border: 1px solid black;
  61. }
  62. `);
  63.  
  64. const movedPosts = new Map();
  65. const linkContainers = new Map();
  66. const originalPosts = new Map();
  67.  
  68. document.body.addEventListener('click', function(event) {
  69. const target = event.target;
  70.  
  71. if (target.classList.contains('linkQuote') && event.ctrlKey) {
  72. const postId = target.href.match(/#q?(\d+)/)?.[1];
  73. if (postId && typeof qr !== 'undefined' && qr.showQr) {
  74. qr.showQr(postId);
  75. event.preventDefault();
  76. return;
  77. }
  78. }
  79.  
  80. if (target.classList.contains('restore-post-link')) {
  81. event.preventDefault();
  82. const postId = target.dataset.postId;
  83. if (postId && movedPosts.has(postId)) {
  84. restorePost(postId);
  85. }
  86. return;
  87. }
  88.  
  89. if ((target.parentNode && target.parentNode.classList.contains('panelBacklinks')) && !event.ctrlKey) {
  90. const link = target.closest('a');
  91. if (!link) return;
  92.  
  93. const rawHash = link.hash.includes('?') ? link.hash.split('?')[0] : link.hash;
  94. const targetId = rawHash.substring(1).replace(/^q/, '');
  95.  
  96. if (movedPosts.has(targetId)) {
  97. event.preventDefault();
  98. restorePost(targetId);
  99. return;
  100. }
  101. }
  102.  
  103. if ((target.parentNode && target.parentNode.classList.contains('panelBacklinks'))) {
  104. event.preventDefault();
  105.  
  106. const link = target.closest('a');
  107. if (!link) return;
  108.  
  109. const rawHash = link.hash.includes('?') ? link.hash.split('?')[0] : link.hash;
  110. const targetId = rawHash.substring(1).replace(/^q/, '');
  111.  
  112. if (linkContainers.has(link)) {
  113. const container = linkContainers.get(link);
  114. const content = container.querySelector('.post-content');
  115.  
  116. if (content) {
  117. const wasCollapsed = content.classList.contains('collapsed');
  118. content.classList.toggle('collapsed');
  119. link.classList.toggle('toggled');
  120.  
  121. if (!wasCollapsed && movedPosts.has(targetId)) {
  122. const postData = movedPosts.get(targetId);
  123. postData.links.delete(link);
  124. if (postData.links.size === 0) {
  125. restorePost(targetId);
  126. }
  127. }
  128. }
  129. return;
  130. }
  131.  
  132. if (!originalPosts.has(targetId)) {
  133. let targetPost = document.getElementById(targetId);
  134. if (!targetPost) return;
  135. originalPosts.set(targetId, targetPost);
  136. }
  137.  
  138. let postToUse;
  139.  
  140. if (movedPosts.has(targetId)) {
  141. postToUse = movedPosts.get(targetId).element;
  142. } else {
  143. postToUse = document.getElementById(targetId) || originalPosts.get(targetId);
  144. if (!postToUse) return;
  145. }
  146.  
  147. const level = link.closest('.collapsible-container')?.dataset.level || 0;
  148. const container = document.createElement('div');
  149. container.className = 'collapsible-container';
  150. container.dataset.level = parseInt(level) + 1;
  151.  
  152. movePostToContainer(targetId, postToUse, container, link);
  153.  
  154. const postContainer = link.closest('.innerPost');
  155. if (postContainer) {
  156. postContainer.appendChild(container);
  157. } else {
  158. link.parentNode.insertBefore(container, link.nextSibling);
  159. }
  160.  
  161. linkContainers.set(link, container);
  162. link.classList.add('toggled');
  163. }
  164. });
  165.  
  166. function movePostToContainer(postId, postToUse, container, link) {
  167. if (movedPosts.has(postId)) {
  168. const postData = movedPosts.get(postId);
  169. const lightClone = postData.element.cloneNode(true);
  170. lightClone.classList.add('post-content', 'moved-post', 'post-restored'); // ← ADDED
  171. lightClone.setAttribute('data-original-id', postId);
  172. container.appendChild(lightClone);
  173. postData.links.add(link);
  174. return;
  175. }
  176.  
  177. if (!document.getElementById(postId) && originalPosts.has(postId)) {
  178. const originalPost = originalPosts.get(postId);
  179. const clone = originalPost.cloneNode(true);
  180. clone.classList.add('post-content', 'moved-post', 'post-restored'); // ← ADDED
  181. clone.setAttribute('data-original-id', postId);
  182. container.appendChild(clone);
  183. const placeholder = document.createElement('div');
  184. placeholder.className = 'post-placeholder';
  185. movedPosts.set(postId, {
  186. element: clone,
  187. placeholder: placeholder,
  188. links: new Set([link])
  189. });
  190. return;
  191. }
  192.  
  193. const placeholder = document.createElement('div');
  194. placeholder.className = 'post-placeholder placeholder-visible';
  195. placeholder.innerHTML = `Post moved <a href="#" class="restore-post-link" data-post-id="${postId}">Restore</a>`;
  196. postToUse.parentNode.insertBefore(placeholder, postToUse);
  197. postToUse.setAttribute('data-original-id', postId);
  198. const innerPost = postToUse.querySelector('.innerPost');
  199. if (innerPost) {
  200. innerPost.classList.add('post-restored');
  201. }
  202. container.appendChild(postToUse);
  203. movedPosts.set(postId, {
  204. element: postToUse,
  205. placeholder: placeholder,
  206. links: new Set([link])
  207. });
  208. }
  209.  
  210. function restorePost(postId) {
  211. if (!movedPosts.has(postId)) return;
  212.  
  213. const {element, placeholder, links} = movedPosts.get(postId);
  214.  
  215. links.forEach(link => {
  216. if (linkContainers.has(link)) {
  217. const container = linkContainers.get(link);
  218. container.remove();
  219. linkContainers.delete(link);
  220. link.classList.remove('toggled', 'post-restored');
  221. }
  222. });
  223.  
  224. document.querySelectorAll(`.moved-post[data-original-id="${postId}"]`).forEach(instance => {
  225. if (instance !== element) {
  226. instance.remove();
  227. }
  228. });
  229.  
  230. if (placeholder.parentNode) {
  231. placeholder.parentNode.insertBefore(element, placeholder);
  232. placeholder.remove();
  233. }
  234.  
  235. element.classList.remove('post-content', 'moved-post', 'post-restored');
  236. element.removeAttribute('data-original-id');
  237. const innerPost = element.querySelector('.innerPost');
  238. if (innerPost) {
  239. innerPost.classList.remove('post-restored');
  240. }
  241.  
  242. movedPosts.delete(postId);
  243.  
  244. if (!originalPosts.has(postId)) {
  245. originalPosts.set(postId, element);
  246. }
  247. }
  248.  
  249.  
  250.  
  251.  
  252. function cleanupBacklinks() {
  253. document.querySelectorAll('span.panelBacklinks a').forEach(link => {
  254. const href = link.getAttribute('href');
  255. if (href?.includes('#')) {
  256. link.href = `#${href.split('#')[1].split('?')[0]}`;
  257. }
  258. });
  259. }
  260.  
  261. const observer = new MutationObserver((mutations) => {
  262. let shouldProcess = false;
  263.  
  264. for (const mutation of mutations) {
  265. if (mutation.addedNodes.length) {
  266. for (const node of mutation.addedNodes) {
  267. if (node.nodeType === 1 &&
  268. (node.classList?.contains('post') ||
  269. node.querySelector?.('.post, .linkQuote, .panelBacklinks'))) {
  270. shouldProcess = true;
  271. break;
  272. }
  273. }
  274. if (shouldProcess) break;
  275. }
  276. }
  277.  
  278. if (shouldProcess) {
  279. cleanupBacklinks();
  280. }
  281. });
  282.  
  283. const threadContainer = document.querySelector('.thread');
  284. if (threadContainer) {
  285. observer.observe(threadContainer, { childList: true, subtree: true });
  286. } else {
  287. observer.observe(document.body, { childList: true, subtree: false });
  288. }
  289.  
  290. cleanupBacklinks();
  291. })();
Advertisement
Add Comment
Please, Sign In to add comment