Reddit - Old School

Allows for easy navigation through a sub by pressing the left/right arrow keys, amongst other useful experience improvements. (Only works for old.reddit.com, so this script automatically redirects you there.)

  1. // ==UserScript==
  2. // @name Reddit - Old School
  3. // @version 1.5.1
  4. // @grant none
  5. // @include https://*.reddit.com/*
  6. // @namespace selbi
  7. // @description Allows for easy navigation through a sub by pressing the left/right arrow keys, amongst other useful experience improvements. (Only works for old.reddit.com, so this script automatically redirects you there.)
  8. // @license MIT License
  9. // ==/UserScript==
  10.  
  11. // Make sure the script is only run once
  12. if (window.top === window.self) {
  13. redditOldSchool();
  14. }
  15.  
  16. function redditOldSchool() {
  17. ////////////////////////
  18. // ENTRY POINT
  19.  
  20. // Matches any URL that isn't on the "old" reddit
  21. const NON_OLD_SUBDOMAIN_REGEX = /^https?:\/\/((?!old)\w+)/i;
  22.  
  23. // Setup script, or redirect if this isn't the old reddit
  24. let url = window.location.href;
  25.  
  26. // Abort everything for media links, as those are only evailable on new reddit
  27. if (url.includes("/media?url=")) {
  28. return;
  29. }
  30.  
  31.  
  32. let matches = url.match(NON_OLD_SUBDOMAIN_REGEX);
  33. if (matches != null && matches.length > 1) {
  34. let subdomain = matches[1]; // always the second match
  35. url = url.replace(subdomain, "old");
  36. window.location.href = url;
  37. } else {
  38. setup();
  39. }
  40.  
  41. ////////////////////////
  42. // SETUP
  43.  
  44. // Directions
  45. const SCROLL_RIGHT = 1;
  46. const SCROLL_LEFT = -1;
  47.  
  48. // Main setup function
  49. function setup() {
  50. if (url.includes("/comments/")) {
  51. redditScrollSetupClickComments();
  52. } else {
  53. const siteTable = document.getElementById("siteTable");
  54. redditScrollSetupClickPost(siteTable);
  55. redditScrollSetupClickableArrows(siteTable);
  56. redditScrollSetupKeys();
  57. }
  58. }
  59.  
  60. // Open/close post by clicking it
  61. function redditScrollSetupClickPost(siteTable) {
  62. siteTable.addEventListener('click', (event) => {
  63. let targetElem = event.target;
  64. scrollToTarget(targetElem);
  65. });
  66. }
  67.  
  68. // Clickable arrows at the bottom right
  69. function redditScrollSetupClickableArrows(siteTable) {
  70. let rightClickArrow = document.createElement("div");
  71. rightClickArrow.classList.add("clickableNavigationArrow")
  72. rightClickArrow.innerHTML = "►";
  73. rightClickArrow.onclick = () => leftRightScroll(SCROLL_RIGHT);
  74. siteTable.appendChild(rightClickArrow);
  75.  
  76. let leftClickArrow = document.createElement("div");
  77. leftClickArrow.classList.add("clickableNavigationArrow", "clickableNavigationArrowLeft")
  78. leftClickArrow.innerHTML = "◄";
  79. leftClickArrow.onclick = () => leftRightScroll(SCROLL_LEFT);
  80. siteTable.appendChild(leftClickArrow);
  81. }
  82.  
  83. // Scroll by pressing left/right/+/- arrow keys on the keyboard
  84. function redditScrollSetupKeys() {
  85. document.onkeydown = (e) => {
  86. // Fetch the key and only allow left/right/+/-
  87. const ARR_LEFT = 37;
  88. const ARR_RIGHT = 39;
  89. const NUM_PLUS = 107;
  90. const NUM_MINUS = 109;
  91.  
  92. const key = e.keyCode;
  93. if (key === ARR_LEFT) {
  94. leftRightScroll(SCROLL_LEFT);
  95. } else if (key === ARR_RIGHT) {
  96. leftRightScroll(SCROLL_RIGHT);
  97. } else if (key === NUM_PLUS) {
  98. browseMultiImagePost(SCROLL_RIGHT);
  99. } else if (key === NUM_MINUS) {
  100. browseMultiImagePost(SCROLL_LEFT);
  101. }
  102. }
  103. }
  104.  
  105. ////////////////////////
  106.  
  107. // Open/close comments by clicking them
  108. function redditScrollSetupClickComments() {
  109. // NTS: #siteTable and .sitetable are two very different elements!
  110. document.querySelector(".commentarea .sitetable").addEventListener('click', (event) => {
  111. let targetElem = event.target;
  112. if (isIgnoredElem(targetElem) || window.getSelection().toString() !== "") {
  113. return;
  114. }
  115. let entry = findParentElemByClass(targetElem, "entry", 5);
  116. if (entry != null) {
  117. entry.querySelector(".expand").click();
  118. scrollToY(entry);
  119. }
  120. });
  121. }
  122.  
  123. ////////////////////////
  124.  
  125. // Variable to keep track of the currently selected post
  126. let currentPost = null;
  127.  
  128. // Main logic to scroll through reddit with left/right arrows
  129. function leftRightScroll(direction) {
  130. // Don't scroll the page if we're currently in a text box
  131. if (isIgnoredElem(document.activeElement)) {
  132. return;
  133. }
  134.  
  135. // If no post is set yet, jump to the very top one
  136. if (currentPost == null) {
  137. scrollToTarget(document.querySelector("#siteTable .entry"));
  138. return;
  139. }
  140.  
  141. // Find the parent container for the post
  142. let post = findParentElemByClass(currentPost, "thing", 2);
  143. if (post == null) {
  144. return;
  145. }
  146.  
  147. // Set the relative browsing methods depending on whether left or right was pressed
  148. let sibling, child;
  149. if (direction === SCROLL_LEFT) {
  150. sibling = (post) => post.previousElementSibling;
  151. child = (post) => post.lastChild;
  152. } else if (direction === SCROLL_RIGHT) {
  153. sibling = (post) => post.nextElementSibling;
  154. child = (post) => post.firstChild;
  155. }
  156.  
  157. // Find the new sibling post relative to the currently opened one
  158. // (Plus some fluff to make page transitions seamless and skipping over non-expandable posts)
  159. do {
  160. let siblingPost = sibling(post);
  161. if (siblingPost == null) {
  162. post = post.parentElement;
  163. } else if (siblingPost.classList.contains("sitetable")) {
  164. post = child(siblingPost);
  165. } else {
  166. post = siblingPost;
  167. }
  168. if (post == null) {
  169. return;
  170. }
  171. } while (!post.classList.contains("thing") || !post.querySelector(".expando-button") || post.classList.contains("promoted"));
  172.  
  173. // Close the previous post, if it was still open
  174. let expando = currentPost.querySelector(".expando-button");
  175. if (expando.classList.contains("expanded")) {
  176. expando.click();
  177. }
  178. // Open the new post and scroll to it
  179. let scrollTarget = post.querySelector(".entry");
  180. scrollToTarget(scrollTarget);
  181. }
  182.  
  183. // For easy navigation of multi-image posts with the +/- Numpad keys
  184. function browseMultiImagePost(direction) {
  185. // Don't do anything if there's no open post or if we're currently in a text box
  186. if (currentPost == null || isIgnoredElem(document.activeElement)) {
  187. return;
  188. }
  189.  
  190. // Find out if the currently open post is a multi-image one
  191. let stepContainer = currentPost.querySelector(".res-step-container");
  192. if (stepContainer) {
  193. // Click the applicable previous/next buttons
  194. if (direction === SCROLL_LEFT) {
  195. stepContainer.querySelector(".res-step-previous").click();
  196. } else if (direction === SCROLL_RIGHT) {
  197. stepContainer.querySelector(".res-step-next").click();
  198. }
  199. }
  200. }
  201.  
  202. ////////////////////////
  203. // All kinds of helper functions
  204.  
  205. const MAX_PARENT_DEPTH = 7;
  206. function scrollToTarget(targetElem) {
  207. if (targetElem.classList.contains("expando-button")) {
  208. scrollToY(targetElem.parentElement);
  209. } else {
  210. if (!targetElem.classList.contains("res-step")) {
  211. let entry = findParentElemByClass(targetElem, "entry", MAX_PARENT_DEPTH);
  212. if (entry != null) {
  213. entry.querySelector(".expando-button").click();
  214. currentPost = entry;
  215. }
  216. }
  217. }
  218. }
  219.  
  220. function findParentElemByClass(elem, className, maxSearchDepth) {
  221. if (elem == null || maxSearchDepth <= 0) {
  222. return null;
  223. } else if (elem.classList.contains(className)) {
  224. return elem;
  225. }
  226. return findParentElemByClass(elem.parentElement, className, maxSearchDepth - 1);
  227. }
  228.  
  229. function scrollToY(elem) {
  230. let scroll = elem.getBoundingClientRect().top + window.scrollY;
  231. window.scroll({
  232. top: scroll,
  233. left: 0,
  234. behavior: "smooth"
  235. });
  236. }
  237.  
  238. const IGNORED_TAG_TYPES = ["a", "textarea", "input"];
  239. function isIgnoredElem(elem) {
  240. let tag = elem.tagName.toLowerCase();
  241. return IGNORED_TAG_TYPES.includes(tag);
  242. }
  243.  
  244. ////////////////////////
  245.  
  246. function addGlobalStyle(css) {
  247. let style = document.createElement("style");
  248. style.innerHTML = css;
  249.  
  250. let head = document.querySelector("head");
  251. if (head) {
  252. head.appendChild(style);
  253. }
  254. }
  255.  
  256. addGlobalStyle(`
  257. body {
  258. overflow-x: hidden;
  259. }
  260.  
  261. .entry {
  262. transition: 0.06s ease;
  263. }
  264. .entry:hover, .res-nightmode .entry.res-selected:hover {
  265. background-color: rgba(128,128,128, 0.2) !important;
  266. cursor: pointer;
  267. }
  268.  
  269. .NERPageMarker {
  270. display: none;
  271. }
  272.  
  273. :root {
  274. --scroll-arrow-width: 6vw;
  275. }
  276.  
  277. .clickableNavigationArrow {
  278. position: fixed;
  279. bottom: 0;
  280. right: 0;
  281. width: var(--scroll-arrow-width);
  282. font-size: var(--scroll-arrow-width);
  283. text-align: center;
  284. opacity: 0.02;
  285. transition: 0.1s ease;
  286. user-select: none;
  287. color: gray;
  288. }
  289.  
  290. .clickableNavigationArrow:hover {
  291. opacity: 0.8;
  292. cursor: pointer;
  293. }
  294.  
  295. .clickableNavigationArrowLeft {
  296. right: var(--scroll-arrow-width);
  297. }
  298. `);
  299.  
  300. ////////////////////////
  301. }