Greasy Fork is available in English.

8chan Nested Inline Reply

Make Nested Inline Reply like 4chanX

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