Last active
April 13, 2022 10:15
-
-
Save hensm/1b973803f1f4efc238a82d3b26c4ea69 to your computer and use it in GitHub Desktop.
Comment Collapser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// jshint esnext: true | |
// ==UserScript== | |
// @name Reddit Comment Collapser | |
// @namespace https://matt.tf | |
// @author Matt Hensman <[email protected]> | |
// @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