Skip to content

Instantly share code, notes, and snippets.

@hensm
Last active April 13, 2022 10:15
Show Gist options
  • Save hensm/1b973803f1f4efc238a82d3b26c4ea69 to your computer and use it in GitHub Desktop.
Save hensm/1b973803f1f4efc238a82d3b26c4ea69 to your computer and use it in GitHub Desktop.
Comment Collapser
// jshint esnext: true
// ==UserScript==
// @name Reddit Comment Collapser
// @namespace https://matt.tf
// @author Matt Hensman <m@matt.tf>
// @include /^https?:\/\/(?:www|old|ssl|pay|[a-z]{2})\.reddit\.com\/(?:r\/(?:\w{2,21}|reddit\.com)\/)?comments\/.*$/
// @version 1.6.1
// @updateURL https://gist.github.com/hensm/1b973803f1f4efc238a82d3b26c4ea69/raw/comment_collapser.user.js
// @downloadURL https://gist.github.com/hensm/1b973803f1f4efc238a82d3b26c4ea69/raw/comment_collapser.user.js
// @grant GM_addStyle
// ==/UserScript==
(() => {
// Find a comment to base sizes on
const testComment = document.querySelector(`
.commentarea > .sitetable > .comment:not(.deleted):not(.collapsed),
.commentarea > .sitetable > #listings > .comment:not(.deleted):not(.collapsed)`);
// Exit early if no valid comments
if (!testComment) return;
const midcol = testComment.querySelector(".midcol");
// Computed styles for exact sizes
const testCommentStyle = window.getComputedStyle(testComment);
const midcolStyle = window.getComputedStyle(midcol);
// Height and margin of the voting buttons
const midcolHeight =
parseInt(midcolStyle.marginTop) +
parseInt(midcolStyle.marginBottom) +
midcol.getBoundingClientRect().height;
const testCommentPaddingHeight =
parseInt(testCommentStyle.paddingTop) +
parseInt(testCommentStyle.paddingBottom);
const offsetHeight = midcolHeight + testCommentPaddingHeight;
// Get size of collapsed comment
const expand = testComment.querySelector(".tagline > .expand");
unsafeWindow.togglecomment(expand);
const collapsedHeight = window.getComputedStyle(testComment).height;
unsafeWindow.togglecomment(expand);
GM_addStyle(`
.comment,
.res-commentBoxes .nestedlisting .comment {
display: block !important;
overflow: hidden !important;
position: relative !important;
transition: height 100ms ease;
}
.comments-page .comment > .collapser {
cursor: pointer;
height: calc(100% - ${offsetHeight}px);
margin-bottom: initial !important;
margin-top: ${midcolHeight}px !important;
position: absolute;
z-index: 99;
}
.res-expando-box .res-media-independent {
z-index: 100 !important;
}
.comments-page .comment.deleted > .collapser {
height: calc(100% - ${testCommentPaddingHeight}px);
margin-top: initial !important;
opacity: 0.35 !important;
}
.comments-page .comment.noncollapsed:not(.deleted) > .collapser,
.comments-page .comment.deleted.noncollapsed > .collapser {
visibility: visible !important;
}
.commentarea .comment a.expand {
position: relative !important;
}
.comments-page .comment.deleted > .midcol:not(.collapser) {
pointer-events: none !important;
}
.comment > .collapser::before {
border-left: 1px dashed rgba(0, 0, 0, 0.5);
content: "";
display: block;
margin-left: calc(50% - 1px);
height: 100%;
opacity: 0.65;
width: 0;
}
.comment > .collapser:hover::before {
opacity: 1;
}
.res-nightmode .comment > .collapser::before {
border-left-color: rgba(255, 255, 255, 0.5);
}
`);
/**
* Toggle collapsed state on a given comment element. Handles animated
* transition, and calls Reddit's collapse function to integrate properly.
*
* @param el Comment element
*/
function toggleComment (el) {
const expand = el.querySelector(".tagline > .expand");
const rect = el.getBoundingClientRect();
if (!el.classList.contains("collapsed")) {
const height = window.getComputedStyle(el).height;
// Set initial height for transition
el.style.height = height;
// Timeout with 0ms delay to trigger transition
setTimeout(() => {
el.style.height = collapsedHeight;
el.addEventListener("transitionend", function onTransitionEnd () {
unsafeWindow.togglecomment(expand);
el.style.height = "";
el.removeEventListener("transitionend", onTransitionEnd);
});
}, 0);
} else {
unsafeWindow.togglecomment(expand);
}
let pinnedHeight = 0;
const pinned = document.querySelector(".pinnable-content.pinned");
if (pinned) {
pinnedHeight = pinned.getBoundingClientRect().height;
}
/**
* If top of comment chain is out of viewport (or beneath pinned
* content), scroll to it.
*/
if (rect.top < pinnedHeight) {
// Viewport Y position in document
const scrollY = window.scrollY - pinnedHeight + rect.top;
try {
window.scrollTo({
top: scrollY,
behavior: "smooth"
});
} catch (e) {
window.scrollTo({
top: scrollY
});
}
}
}
/**
* Create a collapser handle at a given comment element.
*
* @param comment Comment element
* @param depth Comment depth in tree for style class
*/
function createHandle(comment, depth) {
// Prevent duplicates
if (comment.querySelector(".collapser")) {
return;
}
const collapserEl = document.createElement("div");
// Match existing .midcol for theme compat
collapserEl.classList.add(
"midcol", "unvoted", "collapser", `depth-${depth}`);
collapserEl.addEventListener("click", () => {
toggleComment(comment);
});
comment.insertBefore(collapserEl, comment.querySelector(".entry"));
// Add shortcut key
comment.addEventListener("click", ev => {
if (ev.ctrlKey && ev.altKey) {
toggleComment(comment);
ev.stopPropagation();
}
});
}
/**
* Create collapser handles at a given site table, then recursively add handles
* to child comments in the tree.
*
* @param siteTable Site table or comment element containing a site table
* @param depth Comment depth for style class
* @param watchChildList Register a mutation observer for newly added comments
*/
function createHandlesForTree(
siteTable, depth = 0, watchChildList = true) {
// If passed a comment element, find a child site table
if (siteTable.matches(".comment")) {
siteTable = siteTable.querySelector(".child > .sitetable");
if (!siteTable) {
return;
}
}
// Root comment siteTable sometimes has an extra nested element
const comments = Array.from(siteTable.querySelectorAll(`
:scope > .comment,
:scope > #listings > .comment`));
for (const comment of comments) {
createHandle(comment, depth);
createHandlesForTree(comment, depth + 1);
}
// If there are other comments to be loaded, register childList observers
if (watchChildList && (!comments.length ||
siteTable.querySelector(".morecomments"))) {
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLElement &&
node.matches(".comment")) {
createHandle(node, depth);
createHandlesForTree(node, depth + 1);
}
}
}
});
observer.observe(siteTable, {
childList: true
});
}
}
// Comments are sometimes split into multiple siteTable elements
const rootSiteTables = document.querySelectorAll(".commentarea > .sitetable");
for (const siteTable of rootSiteTables) {
createHandlesForTree(siteTable);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment