GitHub Toggle Diff Comments

A userscript that toggles diff/PR and commit comments

当前为 2024-02-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Toggle Diff Comments
  3. // @version 0.3.3
  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. // @match 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?updated=20180103
  13. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163
  14. // @require https://greasyfork.org/scripts/398877-utils-js/code/utilsjs.js?version=1079637
  15. // @icon https://github.githubassets.com/pinned-octocat.svg
  16. // @supportURL https://github.com/Mottie/GitHub-userscripts/issues
  17. // ==/UserScript==
  18.  
  19. /* global GM $ $$ on debounce make */
  20. (() => {
  21. "use strict";
  22.  
  23. const selectors = {
  24. // PR has notes (added to <div id="diff-00" class="file ...">)
  25. headerHasNotes: ".has-inline-notes:not(.hide-file-notes-toggle)",
  26. // show comments wrapper for each file
  27. headerComment: ".has-inline-notes .file-actions > .d-flex",
  28. // show comments checkbox
  29. headerCheckbox: "js-toggle-file-notes",
  30. // button active
  31. activeClass: "ghtc-active",
  32. // td wrapper
  33. tdWrapper: "js-quote-selection-container",
  34. // first div inside td wrapper
  35. tdDiv: "js-resolvable-timeline-thread-container",
  36. // has row comments
  37. rowComment: "tr.inline-comments",
  38. // button class names
  39. button: "btn btn-sm BtnGroup-item ghtc-toggle tooltipped tooltipped-s"
  40. };
  41.  
  42. const actions = {
  43. // Show or hide all comments on the page
  44. toggleAllShowComments: {
  45. check: (event, el) => {
  46. const button = el.matches(`button.${selectors.headerCheckbox}`);
  47. return el.id === "ghtc-show-toggle-all" || event.shiftKey && button;
  48. },
  49. exec: el => {
  50. const state = getState(el);
  51. $$(`#ghtc-show-toggle-all, button.${selectors.headerCheckbox}`).forEach(
  52. el => el.classList.toggle(selectors.activeClass, state)
  53. );
  54. // Use built-in "Show comments" checkbox
  55. $$(`#files input.${selectors.headerCheckbox}`).forEach(el => {
  56. el.checked = state;
  57. el.dispatchEvent(new Event("change", {bubbles: true}));
  58. });
  59. }
  60. },
  61. // Show or hide all comments in a file
  62. toggleFileShowComments: {
  63. check: (_, el) => {
  64. return el.matches(`button.${selectors.headerCheckbox}`);
  65. },
  66. exec: el => {
  67. const state = getState(el);
  68. const box = $(`input.${selectors.headerCheckbox}`, el.closest(".d-flex"));
  69. if (box) {
  70. box.checked = state;
  71. box.dispatchEvent(new Event("change", {bubbles: true}));
  72. }
  73. }
  74. },
  75. // Collapse or expand all comments on the page
  76. collapsePageComments: {
  77. check: (event, el) => {
  78. const toggleAll = el.id === "ghtc-collapse-toggle-all";
  79. const toggle = el.classList.contains("ghtc-collapse-toggle-file");
  80. return toggleAll || (event.shiftKey && toggle);
  81. },
  82. exec: el => {
  83. const state = getState(el);
  84. $("#ghtc-collapse-toggle-all").classList.toggle(selectors.activeClass, state);
  85. toggleMultipleComments(el.closest("#files_bucket"), state);
  86. $$(".ghtc-collapse-toggle-file").forEach(el => {
  87. el.classList.toggle(selectors.activeClass, state);
  88. });
  89. }
  90. },
  91. // Collapse or expand all comments within a file
  92. collapseFileComments: {
  93. check: (event, el) => {
  94. const toggle = el.classList.contains("ghtc-collapse-toggle-file");
  95. const container = el.classList.contains(selectors.tdDiv);
  96. return toggle || (event.shiftKey && container);
  97. },
  98. exec: el => {
  99. const state = getState(el);
  100. toggleMultipleComments(el.closest(".file"), state);
  101. }
  102. },
  103. // Collapse or expand single comment
  104. collapseComment: {
  105. check: (_, el) => el.classList.contains(selectors.tdDiv),
  106. exec: el => el.closest("tr").classList.toggle("ghtc-collapsed"),
  107. }
  108. };
  109.  
  110. const icons = {
  111. "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>
  112. <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>`,
  113. "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>`
  114. };
  115.  
  116. // Using small black triangles because Windows doesn't
  117. // replace them with ugly emoji images
  118. GM.addStyle(`
  119. td.${selectors.tdWrapper} {
  120. position: relative;
  121. }
  122. td .${selectors.tdDiv} {
  123. cursor: pointer;
  124. }
  125. .js-resolvable-thread-contents {
  126. cursor: default;
  127. }
  128. td .${selectors.tdDiv}:before {
  129. content: "\\25be";
  130. font-size: 40px;
  131. position: absolute;
  132. right: 10px;
  133. top: -10px;
  134. pointer-events: none;
  135. }
  136. .ghtc-collapsed .${selectors.tdDiv}:before {
  137. content: "\\25c2";
  138. top: -20px;
  139. }
  140. .ghtc-collapsed .${selectors.tdDiv} {
  141. padding: 10px;
  142. border: 0;
  143. min-height: 26px;
  144. }
  145. .ghtc-collapsed .${selectors.tdDiv}:last-child {
  146. margin-bottom: 16px;
  147. }
  148. .ghtc-toggle .ghtc-secondary,
  149. .ghtc-toggle.${selectors.activeClass} .ghtc-primary,
  150. .ghtc-toggle input ~ .ghtc-secondary,
  151. .ghtc-toggle input:checked ~ .ghtc-primary,
  152. .ghtc-collapsed .${selectors.tdDiv} > *,
  153. .ghtc-collapsed .last-${selectors.tdDiv},
  154. .ghtc-collapsed .inline-comment-form-container:not(.open) {
  155. display: none;
  156. }
  157. .diff-table .ghtc-collapsed td.line-comments {
  158. padding: 0;
  159. cursor: pointer;
  160. }
  161. .pr-toolbar .pr-review-tools.float-right .diffbar-item + .diffbar-item {
  162. margin-left: 10px;
  163. }
  164. .ghtc-toggle {
  165. height: 28px;
  166. }
  167. .ghtc-toggle svg {
  168. display: inline-block;
  169. max-height: 16px;
  170. pointer-events: none;
  171. vertical-align: baseline !important;
  172. }
  173. .ghtc-toggle.${selectors.activeClass} .ghtc-secondary,
  174. .ghtc-toggle input:checked ~ .ghtc-secondary {
  175. display: block;
  176. }`
  177. );
  178.  
  179. function toggleMultipleComments(wrapper, state) {
  180. $(".ghtc-collapse-toggle-file", wrapper).classList.toggle(
  181. selectors.activeClass, state
  182. );
  183. $$(selectors.rowComment, wrapper).forEach(el => {
  184. el.classList.toggle("ghtc-collapsed", state);
  185. });
  186. }
  187.  
  188. function getState(el) {
  189. el.classList.toggle(selectors.activeClass);
  190. return el.classList.contains(selectors.activeClass);
  191. }
  192.  
  193. function addListeners() {
  194. const mainContent = $(".repository-content");
  195. if (mainContent && !mainContent.classList.contains("ghtc-listeners")) {
  196. mainContent.classList.add("ghtc-listeners");
  197. on(mainContent, "change", debounce(event => {
  198. const el = event.target;
  199. if (el && el.classList.contains(selectors.headerCheckbox)) {
  200. const button = $(selectors.headerCheckbox, el.closest(".d-flex"));
  201. if (button) {
  202. button.classList.toggle(selectors.activeClass, el.checked);
  203. }
  204. }
  205. }));
  206. on(mainContent, "click", debounce(event => {
  207. const el = event.target;
  208. if (el) {
  209. Object.keys(actions).some(action => {
  210. if (actions[action].check(event, el)) {
  211. event.stopPropagation();
  212. event.preventDefault();
  213. actions[action].exec(el);
  214. return true;
  215. }
  216. return false;
  217. });
  218. }
  219. }));
  220. }
  221. }
  222.  
  223. function addButtons() {
  224. $$(selectors.headerComment).forEach(wrapper => {
  225. if (!wrapper.classList.contains("ghtc-show-comments")) {
  226. wrapper.classList.add("ghtc-show-comments", "BtnGroup");
  227. // Add a "Show Comments" button outside the dropdown
  228. const show = make({
  229. el: "button",
  230. className: `${selectors.button} ${selectors.headerCheckbox} ${selectors.activeClass}`,
  231. html: icons.show,
  232. attrs: {
  233. "aria-label": "Show or hide all comments in this file"
  234. }
  235. });
  236. wrapper.prepend(show);
  237. // Add collapse all file comments button before label
  238. const collapse = make({
  239. el: "button",
  240. className: `${selectors.button} ghtc-collapse-toggle-file`,
  241. html: icons.collapse,
  242. attrs: {
  243. type: "button",
  244. "aria-label": "Expand or collapse all comments in this file"
  245. },
  246. });
  247. wrapper.prepend(collapse);
  248. }
  249. });
  250. // Add collapse all comments on the page - test adding global toggle on
  251. // https://github.com/openstyles/stylus/pull/150/files (edit.js)
  252. if (!$("#ghtc-collapse-toggle-all")) {
  253. // insert before Unified/Split button group
  254. const diffmode = $(".pr-review-tools .diffbar-item, #toc .toc-diff-stats");
  255. const wrapper = make({
  256. className: "BtnGroup " +
  257. // PR: diffbar-item; commit: toc-diff-stats
  258. (diffmode.classList.contains("diffbar-item")
  259. ? "diffbar-item"
  260. : "float-right pr-2"
  261. )
  262. }, [
  263. // collapse/expand all comments
  264. make({
  265. html: icons.collapse,
  266. className: selectors.button,
  267. attrs: {
  268. id: "ghtc-collapse-toggle-all",
  269. type: "button",
  270. "aria-label": "Expand or collapse all comments"
  271. }
  272. }),
  273. // show/hide all comments
  274. make({
  275. className: `${selectors.button} ${selectors.activeClass}`,
  276. html: icons.show,
  277. attrs: {
  278. id: "ghtc-show-toggle-all",
  279. type: "button",
  280. "aria-label": "Show or hide all comments"
  281. }
  282. })
  283. ]);
  284. diffmode.parentNode.insertBefore(wrapper, diffmode);
  285. }
  286. }
  287.  
  288. function init() {
  289. if ($("#files") && $(selectors.headerHasNotes)) {
  290. addListeners();
  291. addButtons();
  292. }
  293. }
  294.  
  295. on(document, "ghmo:container", init);
  296. on(document, "ghmo:diff", init);
  297. init();
  298.  
  299. })();