GitHub Toggle Diff Comments

A userscript that toggles diff/PR and commit comments

当前为 2017-12-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Toggle Diff Comments
  3. // @version 0.1.1
  4. // @description A userscript that toggles diff/PR and commit comments
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://github.com/*
  9. // @run-at document-idle
  10. // @grant GM.addStyle
  11. // @grant GM_addStyle
  12. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  13. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=234970
  14. // @icon https://assets-cdn.github.com/pinned-octocat.svg
  15. // ==/UserScript==
  16. (() => {
  17. "use strict";
  18.  
  19. let timer,
  20. ignoreEvents = false;
  21. const targets = {
  22. // PR has notes (added to <div id="diff-00" class="file ...">)
  23. headerHasNotes: ".has-inline-notes:not(.hide-file-notes-toggle)",
  24. // show comments wrapper for each file
  25. headerComment: "show-file-notes",
  26. // show comments checkbox
  27. headerCheckbox: "js-toggle-file-notes",
  28. // comment block row - class added to TR containing the comment
  29. rowComment: "inline-comments"
  30. },
  31. icons = {
  32. "show": `<svg xmlns="http://www.w3.org/2000/svg" class="octicon ghtc-primary ghtc-comment-hide" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><g fill="#777"><path d="M6 9h1L6 8 5 7v1c0 .6.5 1 1 1z"/><path d="M9 11H4.5L3 12.5V11H1V5h2L2 4H1a1 1 0 0 0-1 1v6c0 .6.5 1 1 1h1v3l3-3h4c.3 0 .6-.1.7-.3l-.7-.8v.1zM15 1H6a1 1 0 0 0-1 1v.3l1 1V2h9v6h-2v1.5L11.5 8h-.8l3.3 3.2V9h1c.6 0 1-.5 1-1V2c0-.6-.4-1-1-1z"/></g><path d="M.4.9L13.7 14h1.7L2 .9z"/></svg>
  33. <svg xmlns="http://www.w3.org/2000/svg" class="octicon ghtc-secondary ghtc-comment-show" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path fill-rule="evenodd" d="M15 1H6c-.55 0-1 .45-1 1v2H1c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1h1v3l3-3h4c.55 0 1-.45 1-1V9h1l3 3V9h1c.55 0 1-.45 1-1V2c0-.55-.45-1-1-1zM9 11H4.5L3 12.5V11H1V5h4v3c0 .55.45 1 1 1h3v2zm6-3h-2v1.5L11.5 8H6V2h9v6z"></path></svg>`,
  34. "collapse": `<svg xmlns="http://www.w3.org/2000/svg" class="octicon ghtc-primary ghtc-collapse" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><g fill="#777"><path d="M4.2 12.8L5.6 11H4.5L3 12.5V11H1V5h4v3c0 .6.5 1 1 1h1.2L8 8H6V5.4L5 4H1a1 1 0 0 0-1 1v6c0 .6.5 1 1 1h1v3l2.2-2.2zM6 2.2V2h.6V1H6a1 1 0 0 0-1 1v.2h1zM15 1h-4.6v1H15v6h-2v1.5L11.5 8H9.1l.9 1.2V9h1l3 3V9h1c.6 0 1-.5 1-1V2c0-.6-.4-1-1-1z"/></g><path d="M11.5 3h-2V1h-2v2h-2l3 4zM5.5 13h2v2h2v-2h2l-3-4z"/></svg><svg xmlns="http://www.w3.org/2000/svg" class="octicon ghtc-secondary ghtc-expand" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><g fill="#777"><path d="M6 5.8H5V8c0 .6.5 1 1 1h.8V8H6V5.8z"/><path d="M4.6 11h-.1L3 12.5V11H1V5h3.3L6 2.9V2h.7l.8-1H6a1 1 0 0 0-1 1v2H1a1 1 0 0 0-1 1v6c0 .6.5 1 1 1h1v3l3-3h.3l-.7-1zM15 1h-5l.7 1H15v6h-2v1.5L11.5 8h-1v1h.5l.8.8h1.8l-.8 1L14 12V9h1c.6 0 1-.5 1-1V2c0-.6-.4-1-1-1z"/></g><path d="M11.7 11h-2V9h-2v2h-2l3 4zM11.7 5h-2v2h-2V5h-2l3-4z"/></svg>`
  35. },
  36. activeClass = "ghtc-active",
  37. button = document.createElement("div");
  38. button.className = "btn btn-sm BtnGroup-item ghtc-toggle tooltipped tooltipped-s";
  39.  
  40. // Using small black triangles because Windows doesn't
  41. // replace them with ugly emoji images
  42. GM.addStyle(`
  43. td.js-quote-selection-container {
  44. position: relative;
  45. }
  46. .review-thread:before {
  47. content: "\\25be";
  48. font-size: 2rem;
  49. position: absolute;
  50. right: 10px;
  51. top: -1rem;
  52. pointer-events: none;
  53. }
  54. .ghtc-collapsed .review-thread:before {
  55. content: "\\25c2";
  56. }
  57. .ghtc-collapsed .review-thread {
  58. padding: 0 0 5px;
  59. border: 0;
  60. }
  61. .ghtc-collapsed .review-thread:last-child {
  62. margin-bottom: 16px;
  63. }
  64. .ghtc-toggle .ghtc-secondary,
  65. .ghtc-toggle.${activeClass} .ghtc-primary,
  66. .ghtc-toggle input ~ .ghtc-secondary,
  67. .ghtc-toggle input:checked ~ .ghtc-primary,
  68. .ghtc-collapsed .review-thread > *,
  69. .ghtc-collapsed .last-review-thread,
  70. .ghtc-collapsed .inline-comment-form-container {
  71. display: none;
  72. }
  73. .ghtc-collapsed td.line-comments {
  74. padding: 0 5px;
  75. cursor: pointer;
  76. }
  77. .pr-toolbar .pr-review-tools.float-right .diffbar-item + .diffbar-item {
  78. margin-left: 10px;
  79. }
  80. .ghtc-toggle {
  81. height: 28px;
  82. }
  83. .ghtc-toggle svg {
  84. display: inline-block;
  85. max-height: 16px;
  86. pointer-events: none;
  87. vertical-align: baseline !important;
  88. }
  89. .ghtc-toggle.${activeClass} .ghtc-secondary,
  90. .ghtc-toggle input:checked ~ .ghtc-secondary {
  91. display: block;
  92. }`
  93. );
  94.  
  95. function toggleSingleComment(el) {
  96. // Toggle individual inline comment
  97. el.parentNode.classList.toggle("ghtc-collapsed");
  98. }
  99.  
  100. function toggleMultipleComments(wrapper, state) {
  101. $(".ghtc-collapse-toggle-file", wrapper).classList.toggle(activeClass, state);
  102. $$(`tr.${targets.rowComment}`, wrapper).forEach(el => {
  103. el.classList.toggle("ghtc-collapsed", state);
  104. });
  105. }
  106.  
  107. function getState(el) {
  108. el.classList.toggle(activeClass);
  109. return el.classList.contains(activeClass);
  110. }
  111.  
  112. function toggleFile(el) {
  113. // Toggle all inline comments for one file
  114. const state = getState(el);
  115. toggleMultipleComments(el.closest(".file"), state);
  116. }
  117.  
  118. function toggleAll(el) {
  119. // Toggle all comments on page
  120. const state = getState(el);
  121. $("#ghtc-collapse-toggle-all").classList.toggle(activeClass, state);
  122. toggleMultipleComments(el.closest("#files_bucket"), state);
  123. $$(".ghtc-collapse-toggle-file").forEach(el => {
  124. el.classList.toggle(activeClass, state);
  125. });
  126. }
  127.  
  128. function showAll(el) {
  129. // Show/hide all comments on page
  130. const state = getState(el);
  131. $("#ghtc-show-toggle-all").classList.toggle(activeClass, state);
  132. $$("#files .js-toggle-file-notes").forEach(el => {
  133. el.checked = state;
  134. el.dispatchEvent(new Event("change", {bubbles: true}));
  135. });
  136. }
  137.  
  138. function createButton({id, className, icon, title}) {
  139. const btn = button.cloneNode(true);
  140. if (id) {
  141. btn.id = id;
  142. }
  143. btn.className += ` ${className || ""}`;
  144. btn.setAttribute("aria-label", title);
  145. btn.innerHTML = icons[icon];
  146. return btn;
  147. }
  148.  
  149. function execFunction(event, callback) {
  150. clearTimeout(timer);
  151. ignoreEvents = true;
  152. event.stopPropagation();
  153. event.preventDefault();
  154. callback(event.target);
  155. timer = setTimeout(() => {
  156. ignoreEvents = false;
  157. }, 250);
  158. }
  159.  
  160. function addListeners() {
  161. $(".repository-content").addEventListener("change", event => {
  162. const el = event.target;
  163. if (el && el.classList.contains(targets.headerCheckbox)) {
  164. el.parentNode.classList.toggle(activeClass, el.checked);
  165. }
  166. });
  167. $(".repository-content").addEventListener("click", event => {
  168. const el = event.target;
  169. if (!ignoreEvents && el) {
  170. const shift = event.shiftKey,
  171. toggle = el.classList.contains("ghtc-collapse-toggle-file"),
  172. show = el.nodeName === "LABEL",
  173. comment = el.classList.contains("js-quote-selection-container");
  174. if (el.id === "ghtc-collapse-toggle-all" || toggle && shift) {
  175. execFunction(event, toggleAll);
  176. } else if (el.id === "ghtc-show-toggle-all" || show && shift) {
  177. execFunction(event, showAll);
  178. } else if (toggle || comment && shift) {
  179. execFunction(event, toggleFile);
  180. } else if (comment) {
  181. execFunction(event, toggleSingleComment);
  182. }
  183. }
  184. });
  185. }
  186.  
  187. function addButtons() {
  188. $$(`.${targets.headerComment}`).forEach(wrapper => {
  189. if (!wrapper.classList.contains("ghtc-hidden")) {
  190. const label = $("label", wrapper),
  191. checkbox = $("input", wrapper);
  192. let btn;
  193. // Make span wrapper a button group
  194. wrapper.classList.add("ghtc-hidden", "BtnGroup");
  195. // Remove top margin
  196. wrapper.classList.remove("pt-1");
  197.  
  198. // Convert "Show Comments" label wrapping checkbox into a button
  199. label.className = "btn btn-sm BtnGroup-item ghtc-toggle tooltipped tooltipped-s";
  200. label.setAttribute("aria-label", "Show or hide all comments in this file");
  201. label.innerHTML = `
  202. <input type="checkbox" checked="checked" class="js-toggle-file-notes" hidden="true">
  203. ${icons.show}`;
  204.  
  205. // Add collapse all file comments button before label
  206. btn = createButton({
  207. className: "ghtc-collapse-toggle-file",
  208. icon: "collapse",
  209. title: "Expand or collapse all comments in this file"
  210. });
  211. label.parentNode.insertBefore(btn, label);
  212. // Hide checkbox
  213. checkbox.setAttribute("hidden", true);
  214. }
  215. });
  216. // Add collapse all comments on the page - test adding global toggle on
  217. // https://github.com/openstyles/stylus/pull/150/files (edit.js)
  218. if (!$("#ghtc-collapse-toggle-all")) {
  219. const wrapper = document.createElement("div"),
  220. // insert before Unified/Split button group
  221. diffmode = $(".pr-review-tools .diffbar-item, #toc .toc-diff-stats");
  222. let btn;
  223. wrapper.className = "BtnGroup " +
  224. // PR: diffbar-item; commit: toc-diff-stats
  225. (diffmode.classList.contains("diffbar-item") ? "diffbar-item" : "float-right pr-2");
  226. diffmode.parentNode.insertBefore(wrapper, diffmode);
  227. // collapse/expand all comments
  228. btn = createButton({
  229. id: "ghtc-collapse-toggle-all",
  230. icon: "collapse",
  231. title: "Expand or collapse all comments"
  232. });
  233. wrapper.appendChild(btn);
  234. // show/hide all comments
  235. btn = createButton({
  236. id: "ghtc-show-toggle-all",
  237. icon: "show",
  238. className: activeClass,
  239. title: "Show or hide all comments"
  240. });
  241. wrapper.appendChild(btn);
  242. }
  243. }
  244.  
  245. function $(str, el) {
  246. return (el || document).querySelector(str);
  247. }
  248.  
  249. function $$(str, el) {
  250. return [...(el || document).querySelectorAll(str)];
  251. }
  252.  
  253. function init() {
  254. if ($("#files") && $(targets.headerHasNotes)) {
  255. addButtons();
  256. addListeners();
  257. }
  258. }
  259.  
  260. document.addEventListener("ghmo:container", init);
  261. document.addEventListener("ghmo:diff", init);
  262. init();
  263.  
  264. })();