NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Reddit Comment Navigator // @namespace http://www.reddit.com/ // @version 1.01 // @description Used for nagivation of reddit comment threads // @match http://www.reddit.com/r/*/comments/* // @match http://www.reddit.com/user/* // @require http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js // @require http://martixy.no-ip.biz/CDN/jq.hotkeys-0.1.0/jquery.hotkeys.js // @copyright 2014+, martixy // @license GPLv2 | gnu.org/licenses/gpl-2.0.html // ==/UserScript== // USAGE GUIDELINES // Simple really: // J collapses the next top-level comment // K expands the next top-level comment // Left - Collapse/Move to parent // Right - Expand/Move to child // Up/Down - Next/Prev sibling // Script works on both comment pages and user overview pages. Maybe in more places too, but you'll have to add those in the pattern matching up top. // Changelog: // v1.01 // * Small filter fix // Known issues: // Sometimes it doesn't skip over pre-collapsed comments. No idea why. Line 127 seems to fail in some manner //TIL: You have to define it like this to be able to call it via dot(a.k.a. chain it). var extendjQuerythingamajig = function () { $.fn.thing = function() { return this.hasClass("thing") ? this : this.parents(".thing:first"); }, //Debug bit $.fn.box = function() { return this.css("border", "2px solid red"); }, // Original function: Timothy Perez, modified by me // http://stackoverflow.com/a/12742737/802363 // http://lions-mark.com/jquery/scrollTo/ $.fn.scrollTo = function( target, options, callback ){ if(typeof options == 'function' && arguments.length == 2){ callback = options; options = target; } var settings = $.extend({ scrollTarget : target, offsetTop : 20, duration : 200, easing : 'swing', lazyThreshold : 0.7 }, options); return this.each(function(){ var scrollPane = $(this); var scrollY = 0; // var scrollTarget = (typeof settings.scrollTarget == "number") ? settings.scrollTarget : $(settings.scrollTarget); // var scrollY = (typeof scrollTarget == "number") ? scrollTarget : scrollTarget.offset().top - parseInt(settings.offsetTop);// + scrollPane.scrollTop() - parseInt(settings.offsetTop); if(typeof settings.scrollTarget == "number") { scrollY = settings.scrollTarget; } else { var scrollTarget = $(settings.scrollTarget); if((scrollTarget.offset().top > $(window).scrollTop() + $(window).height() * settings.lazyThreshold) || scrollTarget.offset().top < $(window).scrollTop()) { scrollY = scrollTarget.offset().top - parseInt(settings.offsetTop); } } if(scrollY != 0) scrollPane.animate({scrollTop : scrollY }, parseInt(settings.duration), settings.easing, function(){ if (typeof callback == 'function') { callback.call(this); } }); }); } }(); var allThings = $(".thing"); var first = allThings.filter(".comment:first"); var last = allThings.filter(".comment:last"); var post = allThings.filter(".link"); var toplevel = first; var current = first; var next = current; var i = 0; // Needs a marker (half-transparent in some corner) var markerDiv = $("<div></div>"); var markerStyle = { "position" : "fixed", "bottom" : "20px", "left" : "20px", "width" : "15px", "height" : "15px", "border-radius" : "50%", "background-color" : "#369", "opacity" : "0.5" } markerDiv.css(markerStyle); // Custom bits cuz mixing mouse and keyboard navigation can mess up the internal state var customClassCollapsed = "navigator-collapsed"; $(".comment").each(function() { if(isCollapsed($(this))) $(this).addClass(customClassCollapsed); }); $(".expand").bind("click", function() { $(this).thing().toggleClass(customClassCollapsed); $(this).thing().children().find(".thing").toggleClass(customClassCollapsed); }); function isCollapsed(e) { return $(e).children(".entry").find(".collapsed").css("display") == "block"; } // For arrow-key navigation var highlightCSS = { "text-decoration" : "none", "color" : "white", "background-color" : "#369" } function highlight(e) { if(!$(e).hasClass("link")) $(e).children(".entry").find(".expand").css(highlightCSS); else $(e).children(".entry").find(".comments").css(highlightCSS); // Highlights the string that says how many comments there are } function delight(e) { $(e).children(".entry").find(".expand, .comments").attr("style",""); } //================================== // Keybinds //================================== $(document).bind("keydown", "J", function() { if(first.find(".noncollapsed").css("display") == "block") { hidecomment(first); } else { toplevel = first.siblings(".thing").children(".entry").find(".noncollapsed").filter(function() { return $(this).css("display") == "block"; }).first().thing(); hidecomment(toplevel); } }); $(document).bind("keydown", "K", function() { toplevel = first.siblings(".thing").children(".entry").find(".collapsed").filter(function() { return $(this).css("display") == "block"; }).last().thing(); if(toplevel.length == 0) { showcomment(first); } else { showcomment(toplevel); } }); // TIL: // Bad: $(".class:first") // Good: $(".class").first() // Better: $("div.class").filter(":first") // Best: $(".class").filter(":first") // 1. Use jQ methods to reduce your set(not selectors) // 2. Be specific on the right-hand side of your selector, and less specific on the left. $(document).bind("keydown", "up", function() { if(i == 0) { console.log("up"); current = allThings.filter(".link"); //Be sneaky (a.k.a. select post itself so below code will wrap up and start from bottom) i = 1; $( "body" ).append(markerDiv); } // When does it deviate from index navigation: // 1. When it's on a deeply collapsed node if(current.parents(".thing").filter(":first").hasClass(customClassCollapsed)) { console.log("up: step-out"); next = current.parents("." + customClassCollapsed).last(); //Get topmost collapsed node } // 2. When it's wrapping up else if(current.hasClass("link")) { console.log("up: wrap"); next = allThings.eq(-1); } else { console.log("up: step-index"); next = allThings.eq(allThings.index(current) - 1); } // 3. When it's about to enter a deeply collapsed node, but not when exiting it! if(next.hasClass(customClassCollapsed) && !current.parents(".thing").filter(":first").hasClass(customClassCollapsed) && next.parents(".thing").length > 0) { // So check if we've done so and exit to topmost collapsed. Also for the love of god - sanity check console.log("up: step-over"); next = next.parents("." + customClassCollapsed).last(); } $("body").scrollTo(next); delight(current); highlight(next); current = next; return false; //Override the default action }); $(document).bind("keydown", "down", function() { if(i == 0) { console.log("down"); current = allThings.filter(".link"); //Be sneaky (a.k.a. select post itself so below code will wrap up and start from top) i = 1; $( "body" ).append(markerDiv); } //next = current; //Does have to be true, need not be explicit unless you mess with it if(current.hasClass(customClassCollapsed)) { //We're on a collapsed node right now if(current.parents(".thing").filter(":first").hasClass(customClassCollapsed)) { //In fact we're very deep inside next = current.parents("." + customClassCollapsed).last(); //Get topmost collapsed node console.log("down: step-out"); } // We're on the topmost collapsed node right now (this guarantees that its sibling nodes and any of its parents sibling nodes are visible and are therefore valid navigation targets) if(next.nextAll(".thing").length == 0) { // but this node does not have any siblings for us to navigate to next = next.parents(".thing").filter(function(index) { //So we take a list of the node's parents return $(this).nextAll(".thing").length > 0; // We filter only for parents that have siblings }).filter(":first"); //We return the first parent that has siblings console.log("down: step-over"); } if(next.nextAll(".thing").length == 0) { //If we have not found a higher node with siblings this means we're at the end of the thread && current == next next = post; // Therefore wrap around console.log("down: wrap-sibling"); } else { next = next.nextAll(".thing").filter(":first"); //Otherwise get the returned node's next sibling console.log("down: step-sibling"); } } else if(allThings.index(current) + 1 == allThings.length) { next = post; console.log("down: wrap-index"); } else { next = allThings.eq(allThings.index(current) + 1); console.log("down: step-index"); } $("body").scrollTo(next); delight(current); highlight(next); current = next; return false; //Override the default action }); $(document).bind("keydown", "right", function() { if(i == 1) { console.log("right"); if(isCollapsed(current)) { showcomment(current); } else { next = current.children(".child").find(".thing:first"); if(next.length > 0) { $("body").scrollTo(next); delight(current); highlight(next); current = next; } } } return false; //Override the default action }); $(document).bind("keydown", "left", function() { if(i == 1) { console.log("left"); if(!isCollapsed(current)) { hidecomment(current); } else if(!current.parent().hasClass("nestedlisting")) { next = current.parents(".thing:first"); $("body").scrollTo(next); delight(current); highlight(next); current = next; } } return false; //Override the default action }); $(document).bind("keydown", "esc", function() { if(i == 1) { console.log("esc"); delight(current); i = 0; $(markerDiv).remove(); } return false; //Override the default action }); // Shameless copy cuz I don't wanna deal with injecting myself in the page. Encapsulation win I guess? // Did modify to add my custom class function hidecomment(t) { //var t = $(e).thing(); return t.hide().find(".noncollapsed:first, .midcol:first").hide().end().show().find(".entry:first .collapsed").show(), t.hasClass("message") ? $.request("collapse_message", { id: $(t).thing_id() }) : t.find(".child:first").hide(), t.addClass(customClassCollapsed), t.children().find(".thing").addClass(customClassCollapsed), !1 } function showcomment(t) { //var t = $(e).thing(); return t.find(".entry:first .collapsed").hide().end().find(".noncollapsed:first, .midcol:first").show().end().show(), t.hasClass("message") ? $.request("uncollapse_message", { id: $(t).thing_id() }) : t.find(".child:first").show(), t.removeClass(customClassCollapsed), t.children().find(".thing").removeClass(customClassCollapsed), !1 } // Disregard for now function thing_id(t) { t = e.with_default(t, "thing"); var n = this.hasClass("thing") ? this : this.thing(); t != "thing" && (n = n.find("." + t + ":first")); if (n.length) { var r = e.grep(n.get(0).className.match(/\S+/g), function (e) { return e.match(/^id-/); }); return r.length ? r[0].slice(3, r[0].length) : ""; } return ""; } function with_default(t, n) { return e.defined(t) ? t : n; } function defined(e) { return typeof e != "undefined"; }