diff --git a/github-toggle-diff-comments.user.js b/github-toggle-diff-comments.user.js new file mode 100644 index 0000000..9a9d3b6 --- /dev/null +++ b/github-toggle-diff-comments.user.js @@ -0,0 +1,257 @@ +// ==UserScript== +// @name GitHub Toggle Diff Comments +// @version 0.1.0 +// @description A userscript that toggles diff/PR comments +// @license MIT +// @author Rob Garrison +// @namespace https://github.com/Mottie +// @include https://github.com/* +// @run-at document-idle +// @grant none +// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js +// @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=198500 +// @icon https://assets-cdn.github.com/pinned-octocat.svg +// @updateURL https://raw.githubusercontent.com/Mottie/Github-userscripts/master/github-toggle-diff-comments.user.js +// @downloadURL https://raw.githubusercontent.com/Mottie/Github-userscripts/master/github-toggle-diff-comments.user.js +// ==/UserScript== +(() => { + "use strict"; + + let timer, + ignoreEvents = false; + const targets = { + // show comments wrapper for each file + headerComment: "show-file-notes", + // show comments checkbox + headerCheckbox: "js-toggle-file-notes", + // comment block row - class added to TR containing the comment + rowComment: "inline-comments" + }, + icons = { + "show": ` + `, + "collapse": `` + }, + activeClass = "ghtc-active", + button = document.createElement("div"); + button.className = "btn btn-sm BtnGroup-item ghtc-toggle"; + + // Using small black triangles because Windows doesn't + // replace them with ugly emoji images + GM.addStyle(` + td.js-quote-selection-container { + position: relative; + } + .review-thread:before { + content: "\\25be"; + font-size: 2rem; + position: absolute; + right: 10px; + top: -1rem; + pointer-events: none; + } + .ghtc-collapsed .review-thread:before { + content: "\\25c2"; + } + .ghtc-collapsed .review-thread { + padding: 0 0 5px; + border: 0; + } + .ghtc-toggle .ghtc-secondary, + .ghtc-toggle.${activeClass} .ghtc-primary, + .ghtc-toggle input ~ .ghtc-secondary, + .ghtc-toggle input:checked ~ .ghtc-primary, + .ghtc-collapsed .review-thread > *, + .ghtc-collapsed .last-review-thread, + .ghtc-collapsed .inline-comment-form-container { + display: none; + } + .ghtc-collapsed td.line-comments { + padding: 0 5px; + cursor: pointer; + } + .pr-toolbar .pr-review-tools.float-right .diffbar-item + .diffbar-item { + margin-left: 10px; + } + .ghtc-toggle { + height: 28px; + } + .ghtc-toggle svg { + display: inline-block; + max-height: 16px; + pointer-events: none; + vertical-align: baseline !important; + } + .ghtc-toggle.${activeClass} .ghtc-secondary, + .ghtc-toggle input:checked ~ .ghtc-secondary { + display: block; + }` + ); + + function toggleSingleComment(el) { + // Toggle individual inline comment + el.parentNode.classList.toggle("ghtc-collapsed"); + } + + function toggleMultipleComments(wrapper, state) { + $(".ghtc-collapse-toggle-file", wrapper).classList.toggle(activeClass, state); + $$(`tr.${targets.rowComment}`, wrapper).forEach(el => { + el.classList.toggle("ghtc-collapsed", state); + }); + } + + function getState(el) { + el.classList.toggle(activeClass); + return el.classList.contains(activeClass); + } + + function toggleFile(el) { + // Toggle all inline comments for one file + const state = getState(el); + toggleMultipleComments(el.closest(".file"), state); + } + + function toggleAll(el) { + // Toggle all comments on page + const state = getState(el); + $("#ghtc-collapse-toggle-all").classList.toggle(activeClass, state); + toggleMultipleComments(el.closest("#files_bucket"), state); + $$(".ghtc-collapse-toggle-file").forEach(el => { + el.classList.toggle(activeClass, state); + }); + } + + function showAll(el) { + // Show/hide all comments on page + const state = getState(el); + $("#ghtc-show-toggle-all").classList.toggle(activeClass, state); + $$("#files_bucket .js-toggle-file-notes").forEach(el => { + el.checked = state; + el.dispatchEvent(new Event("change", {bubbles: true})); + }); + } + + function createButton({id, className, icon, title}) { + const btn = button.cloneNode(true); + if (id) { + btn.id = id; + } + btn.className += ` ${className || ""}`; + btn.title = title; + btn.innerHTML = icons[icon]; + return btn; + } + + function execFunction(event, callback) { + clearTimeout(timer); + ignoreEvents = true; + event.stopPropagation(); + event.preventDefault(); + callback(event.target); + timer = setTimeout(() => { + ignoreEvents = false; + }, 250); + } + + function addListeners() { + $("#files_bucket").addEventListener("change", event => { + const el = event.target; + if (el && el.classList.contains(targets.headerCheckbox)) { + el.parentNode.classList.toggle(activeClass, el.checked); + } + }); + $("#files_bucket").addEventListener("click", event => { + const el = event.target; + if (!ignoreEvents && el) { + const shift = event.shiftKey, + toggle = el.classList.contains("ghtc-collapse-toggle-file"), + show = el.nodeName === "LABEL", + comment = el.classList.contains("js-quote-selection-container"); + if (el.id === "ghtc-collapse-toggle-all" || toggle && shift) { + execFunction(event, toggleAll); + } else if (el.id === "ghtc-show-toggle-all" || show && shift) { + execFunction(event, showAll); + } else if (toggle || comment && shift) { + execFunction(event, toggleFile); + } else if (comment) { + execFunction(event, toggleSingleComment); + } + } + }); + } + + function addButtons() { + $$(`.${targets.headerComment}`).forEach(wrapper => { + if (!wrapper.classList.contains("ghtc-hidden")) { + const label = $("label", wrapper), + checkbox = $("input", wrapper); + let btn; + // Make span wrapper a button group + wrapper.classList.add("ghtc-hidden", "BtnGroup"); + // Remove top margin + wrapper.classList.remove("pt-1"); + + // Convert "Show Comments" label wrapping checkbox into a button + label.className = "btn btn-sm BtnGroup-item ghtc-toggle"; + label.title = "Show or hide all comments in this file"; + label.innerHTML = ` + + ${icons.show}`; + + // Add collapse all file comments button before label + btn = createButton({ + className: "ghtc-collapse-toggle-file", + icon: "collapse", + title: "Expand or collapse all comments in this file" + }); + label.parentNode.insertBefore(btn, label); + // Hide checkbox + checkbox.setAttribute("hidden", true); + } + }); + // Add collapse all comments on the page + if (!$("#ghtc-collapse-toggle-all")) { + const wrapper = document.createElement("div"), + // insert before Unified/Split button group + diffmode = $(".pr-review-tools .diffbar-item"); + let btn; + wrapper.className = "BtnGroup diffbar-item"; + diffmode.parentNode.insertBefore(wrapper, diffmode); + // collapse/expand all comments + btn = createButton({ + id: "ghtc-collapse-toggle-all", + icon: "collapse", + title: "Expand or collapse all comments" + }); + wrapper.appendChild(btn); + // show/hide all comments + btn = createButton({ + id: "ghtc-show-toggle-all", + icon: "show", + className: activeClass, + title: "Show or hide all comments" + }); + wrapper.appendChild(btn); + } + } + + function $(str, el) { + return (el || document).querySelector(str); + } + + function $$(str, el) { + return [...(el || document).querySelectorAll(str)]; + } + + function init() { + if ($("#files_bucket") && $(".pr-toolbar")) { + addButtons(); + addListeners(); + } + } + + document.addEventListener("ghmo:container", init); + document.addEventListener("ghmo:diff", init); + init(); + +})(); diff --git a/images/github-toggle-diff-comments.gif b/images/github-toggle-diff-comments.gif new file mode 100644 index 0000000..e5661c0 Binary files /dev/null and b/images/github-toggle-diff-comments.gif differ