// ==UserScript==
// @name GitHub Toggle Diff Comments
// @version 0.3.3
// @description A userscript that toggles diff/PR and commit comments
// @license MIT
// @author Rob Garrison
// @namespace https://github.com/Mottie
// @match https://github.com/*
// @run-at document-idle
// @grant GM.addStyle
// @grant GM_addStyle
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?updated=20180103
// @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163
// @require https://greasyfork.org/scripts/398877-utils-js/code/utilsjs.js?version=1079637
// @icon https://github.githubassets.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
// @supportURL https://github.com/Mottie/GitHub-userscripts/issues
// ==/UserScript==
/* global GM $ $$ on debounce make */
(() => {
"use strict";
const selectors = {
// PR has notes (added to
)
headerHasNotes: ".has-inline-notes:not(.hide-file-notes-toggle)",
// show comments wrapper for each file
headerComment: ".has-inline-notes .file-actions > .d-flex",
// show comments checkbox
headerCheckbox: "js-toggle-file-notes",
// button active
activeClass: "ghtc-active",
// td wrapper
tdWrapper: "js-quote-selection-container",
// first div inside td wrapper
tdDiv: "js-resolvable-timeline-thread-container",
// has row comments
rowComment: "tr.inline-comments",
// button class names
button: "btn btn-sm BtnGroup-item ghtc-toggle tooltipped tooltipped-s"
};
const actions = {
// Show or hide all comments on the page
toggleAllShowComments: {
check: (event, el) => {
const button = el.matches(`button.${selectors.headerCheckbox}`);
return el.id === "ghtc-show-toggle-all" || event.shiftKey && button;
},
exec: el => {
const state = getState(el);
$$(`#ghtc-show-toggle-all, button.${selectors.headerCheckbox}`).forEach(
el => el.classList.toggle(selectors.activeClass, state)
);
// Use built-in "Show comments" checkbox
$$(`#files input.${selectors.headerCheckbox}`).forEach(el => {
el.checked = state;
el.dispatchEvent(new Event("change", {bubbles: true}));
});
}
},
// Show or hide all comments in a file
toggleFileShowComments: {
check: (_, el) => {
return el.matches(`button.${selectors.headerCheckbox}`);
},
exec: el => {
const state = getState(el);
const box = $(`input.${selectors.headerCheckbox}`, el.closest(".d-flex"));
if (box) {
box.checked = state;
box.dispatchEvent(new Event("change", {bubbles: true}));
}
}
},
// Collapse or expand all comments on the page
collapsePageComments: {
check: (event, el) => {
const toggleAll = el.id === "ghtc-collapse-toggle-all";
const toggle = el.classList.contains("ghtc-collapse-toggle-file");
return toggleAll || (event.shiftKey && toggle);
},
exec: el => {
const state = getState(el);
$("#ghtc-collapse-toggle-all").classList.toggle(selectors.activeClass, state);
toggleMultipleComments(el.closest("#files_bucket"), state);
$$(".ghtc-collapse-toggle-file").forEach(el => {
el.classList.toggle(selectors.activeClass, state);
});
}
},
// Collapse or expand all comments within a file
collapseFileComments: {
check: (event, el) => {
const toggle = el.classList.contains("ghtc-collapse-toggle-file");
const container = el.classList.contains(selectors.tdDiv);
return toggle || (event.shiftKey && container);
},
exec: el => {
const state = getState(el);
toggleMultipleComments(el.closest(".file"), state);
}
},
// Collapse or expand single comment
collapseComment: {
check: (_, el) => el.classList.contains(selectors.tdDiv),
exec: el => el.closest("tr").classList.toggle("ghtc-collapsed"),
}
};
const icons = {
"show": `
`,
"collapse": `
`
};
// Using small black triangles because Windows doesn't
// replace them with ugly emoji images
GM.addStyle(`
td.${selectors.tdWrapper} {
position: relative;
}
td .${selectors.tdDiv} {
cursor: pointer;
}
.js-resolvable-thread-contents {
cursor: default;
}
td .${selectors.tdDiv}:before {
content: "\\25be";
font-size: 40px;
position: absolute;
right: 10px;
top: -10px;
pointer-events: none;
}
.ghtc-collapsed .${selectors.tdDiv}:before {
content: "\\25c2";
top: -20px;
}
.ghtc-collapsed .${selectors.tdDiv} {
padding: 10px;
border: 0;
min-height: 26px;
}
.ghtc-collapsed .${selectors.tdDiv}:last-child {
margin-bottom: 16px;
}
.ghtc-toggle .ghtc-secondary,
.ghtc-toggle.${selectors.activeClass} .ghtc-primary,
.ghtc-toggle input ~ .ghtc-secondary,
.ghtc-toggle input:checked ~ .ghtc-primary,
.ghtc-collapsed .${selectors.tdDiv} > *,
.ghtc-collapsed .last-${selectors.tdDiv},
.ghtc-collapsed .inline-comment-form-container:not(.open) {
display: none;
}
.diff-table .ghtc-collapsed td.line-comments {
padding: 0;
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.${selectors.activeClass} .ghtc-secondary,
.ghtc-toggle input:checked ~ .ghtc-secondary {
display: block;
}`
);
function toggleMultipleComments(wrapper, state) {
$(".ghtc-collapse-toggle-file", wrapper).classList.toggle(
selectors.activeClass, state
);
$$(selectors.rowComment, wrapper).forEach(el => {
el.classList.toggle("ghtc-collapsed", state);
});
}
function getState(el) {
el.classList.toggle(selectors.activeClass);
return el.classList.contains(selectors.activeClass);
}
function addListeners() {
const mainContent = $(".repository-content");
if (mainContent && !mainContent.classList.contains("ghtc-listeners")) {
mainContent.classList.add("ghtc-listeners");
on(mainContent, "change", debounce(event => {
const el = event.target;
if (el && el.classList.contains(selectors.headerCheckbox)) {
const button = $(selectors.headerCheckbox, el.closest(".d-flex"));
if (button) {
button.classList.toggle(selectors.activeClass, el.checked);
}
}
}));
on(mainContent, "click", debounce(event => {
const el = event.target;
if (el) {
Object.keys(actions).some(action => {
if (actions[action].check(event, el)) {
event.stopPropagation();
event.preventDefault();
actions[action].exec(el);
return true;
}
return false;
});
}
}));
}
}
function addButtons() {
$$(selectors.headerComment).forEach(wrapper => {
if (!wrapper.classList.contains("ghtc-show-comments")) {
wrapper.classList.add("ghtc-show-comments", "BtnGroup");
// Add a "Show Comments" button outside the dropdown
const show = make({
el: "button",
className: `${selectors.button} ${selectors.headerCheckbox} ${selectors.activeClass}`,
html: icons.show,
attrs: {
"aria-label": "Show or hide all comments in this file"
}
});
wrapper.prepend(show);
// Add collapse all file comments button before label
const collapse = make({
el: "button",
className: `${selectors.button} ghtc-collapse-toggle-file`,
html: icons.collapse,
attrs: {
type: "button",
"aria-label": "Expand or collapse all comments in this file"
},
});
wrapper.prepend(collapse);
}
});
// Add collapse all comments on the page - test adding global toggle on
// https://github.com/openstyles/stylus/pull/150/files (edit.js)
if (!$("#ghtc-collapse-toggle-all")) {
// insert before Unified/Split button group
const diffmode = $(".pr-review-tools .diffbar-item, #toc .toc-diff-stats");
const wrapper = make({
className: "BtnGroup " +
// PR: diffbar-item; commit: toc-diff-stats
(diffmode.classList.contains("diffbar-item")
? "diffbar-item"
: "float-right pr-2"
)
}, [
// collapse/expand all comments
make({
html: icons.collapse,
className: selectors.button,
attrs: {
id: "ghtc-collapse-toggle-all",
type: "button",
"aria-label": "Expand or collapse all comments"
}
}),
// show/hide all comments
make({
className: `${selectors.button} ${selectors.activeClass}`,
html: icons.show,
attrs: {
id: "ghtc-show-toggle-all",
type: "button",
"aria-label": "Show or hide all comments"
}
})
]);
diffmode.parentNode.insertBefore(wrapper, diffmode);
}
}
function init() {
if ($("#files") && $(selectors.headerHasNotes)) {
addListeners();
addButtons();
}
}
on(document, "ghmo:container", init);
on(document, "ghmo:diff", init);
init();
})();