GitHub Collapse In Comment

A userscript that adds a header that can toggle long code and quote blocks in comments

当前为 2016-09-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Collapse In Comment
  3. // @version 1.0.2
  4. // @description A userscript that adds a header that can toggle long code and quote blocks in comments
  5. // @license https://creativecommons.org/licenses/by-sa/4.0/
  6. // @namespace https://github.com/Mottie
  7. // @include https://github.com/*
  8. // @include https://gist.github.com/*
  9. // @run-at document-idle
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_registerMenuCommand
  14. // @author Rob Garrison
  15. // ==/UserScript==
  16. /* global GM_addStyle, GM_getValue, GM_setValue, GM_registerMenuCommand */
  17. /* jshint esnext:true, unused:true */
  18. (() => {
  19. "use strict";
  20. /*
  21. Idea from: https://github.com/dear-github/dear-github/issues/166 & https://github.com/isaacs/github/issues/208
  22. examples:
  23. https://github.com/Mottie/tablesorter/issues/569
  24. https://github.com/jquery/jquery/issues/3195
  25. */
  26. let timer,
  27. busy = false,
  28.  
  29. // hide code/quotes longer than this number of lines
  30. minLines = GM_getValue("gcic-max-lines", 10),
  31. startCollapsed = GM_getValue("gcic-start-collapsed", true);
  32.  
  33. // syntax highlight class name lookup table
  34. const syntaxClass = {
  35. basic: "HTML",
  36. cs: "C#",
  37. fsharp: "F#",
  38. gfm: "Markdown",
  39. jq: "JSONiq",
  40. shell: "Bash (shell)",
  41. tcl: "Glyph",
  42. tex: "LaTex"
  43. };
  44.  
  45. GM_addStyle(`
  46. .gcic-block {
  47. border:#eee 1px solid;
  48. padding:2px 8px 2px 10px;
  49. border-radius:5px 5px 0 0;
  50. position:relative;
  51. top:1px;
  52. cursor:pointer;
  53. font-weight:bold;
  54. display:block;
  55. }
  56. .gcic-block + .highlight {
  57. border-top:none;
  58. }
  59. .gcic-block + .email-signature-reply {
  60. margin-top:0;
  61. }
  62. .gcic-block:after {
  63. content:"\u25bc ";
  64. float:right;
  65. }
  66. .gcic-block-closed {
  67. border-radius:5px;
  68. margin-bottom:10px;
  69. }
  70. .gcic-block-closed:after {
  71. transform: rotate(90deg);
  72. }
  73. .gcic-block-closed + .highlight, .gcic-block-closed + .email-signature-reply,
  74. .gcic-block-closed + pre {
  75. display:none;
  76. }
  77. `);
  78.  
  79. function makeToggle(name, lines) {
  80. /* full list of class names from (look at "tm_scope" value)
  81. https://github.com/github/linguist/blob/master/lib/linguist/languages.yml
  82. here are some example syntax highlighted class names:
  83. highlight-text-html-markdown-source-gfm-apib
  84. highlight-text-html-basic
  85. highlight-source-fortran-modern
  86. highlight-text-tex
  87. */
  88. let n = (name || "").replace(
  89. /(highlight[-\s]|(source-)|(text-)|(html-)|(markdown-)|(-modern))/g, ""
  90. );
  91. n = (syntaxClass[n] || n).toUpperCase().trim();
  92. return `${n || "Block"} (${lines} lines)`;
  93. }
  94.  
  95. function addToggles() {
  96. busy = true;
  97. // issue comments
  98. if ($("#discussion_bucket")) {
  99. let indx = 0;
  100. const block = document.createElement("a"),
  101. els = $$(".markdown-body pre, .email-signature-reply"),
  102. len = els.length;
  103.  
  104. // "flash" = blue box styling
  105. block.className = `gcic-block border flash${
  106. startCollapsed ? " gcic-block-closed" : ""
  107. }`;
  108. block.href = "#";
  109.  
  110. // loop with delay to allow user interaction
  111. const loop = () => {
  112. let el, wrap, node, syntaxClass, numberOfLines,
  113. // max number of DOM insertions per loop
  114. max = 0;
  115. while (max < 20 && indx < len) {
  116. if (indx >= len) {
  117. return;
  118. }
  119. el = els[indx];
  120. if (el && !el.classList.contains("gcic-has-toggle")) {
  121. numberOfLines = el.innerHTML.split("\n").length;
  122. if (numberOfLines > minLines) {
  123. syntaxClass = "";
  124. wrap = closest(el, ".highlight");
  125. if (wrap && wrap.classList.contains("highlight")) {
  126. syntaxClass = wrap.className;
  127. } else {
  128. // no syntax highlighter defined (not wrapped)
  129. wrap = el;
  130. }
  131. node = block.cloneNode();
  132. node.innerHTML = makeToggle(syntaxClass, numberOfLines);
  133. wrap.parentNode.insertBefore(node, wrap);
  134. el.classList.add("gcic-has-toggle");
  135. if (startCollapsed) {
  136. el.display = "none";
  137. }
  138. max++;
  139. }
  140. }
  141. indx++;
  142. }
  143. if (indx < len) {
  144. setTimeout(() => {
  145. loop();
  146. }, 200);
  147. }
  148. };
  149. loop();
  150. }
  151. busy = false;
  152. }
  153.  
  154. function addBindings() {
  155. document.addEventListener("click", event => {
  156. let els, indx, flag;
  157. const el = event.target;
  158. if (el && el.classList.contains("gcic-block")) {
  159. event.preventDefault();
  160. // shift + click = toggle all blocks in a single comment
  161. // shift + ctrl + click = toggle all blocks on page
  162. if (event.shiftKey) {
  163. els = $$(".gcic-block", event.ctrlKey ? "" : (el, ".markdown-body"));
  164. indx = els.length;
  165. flag = el.classList.contains("gcic-block-closed");
  166. while (indx--) {
  167. els[indx].classList[flag ? "remove" : "add"]("gcic-block-closed");
  168. }
  169. } else {
  170. el.classList.toggle("gcic-block-closed");
  171. }
  172. removeSelection();
  173. }
  174. });
  175. }
  176.  
  177. function update() {
  178. busy = true;
  179. let toggles = $$(".gcic-block"),
  180. indx = toggles.length;
  181. while (indx--) {
  182. toggles[indx].parentNode.removeChild(toggles[indx]);
  183. }
  184. toggles = $$(".gcic-has-toggle");
  185. indx = toggles.length;
  186. while (indx--) {
  187. toggles[indx].classList.remove("gcic-has-toggle");
  188. }
  189. addToggles();
  190. }
  191.  
  192. function $(selector, el) {
  193. return (el || document).querySelector(selector);
  194. }
  195.  
  196. function $$(selector, el) {
  197. return Array.from((el || document).querySelectorAll(selector));
  198. }
  199.  
  200. function closest(el, selector) {
  201. while (el && el.nodeName !== "BODY" && !el.matches(selector)) {
  202. el = el.parentNode;
  203. }
  204. return el && el.matches(selector) ? el : null;
  205. }
  206.  
  207. function removeSelection() {
  208. // remove text selection - http://stackoverflow.com/a/3171348/145346
  209. const sel = window.getSelection ? window.getSelection() : document.selection;
  210. if (sel) {
  211. if (sel.removeAllRanges) {
  212. sel.removeAllRanges();
  213. } else if (sel.empty) {
  214. sel.empty();
  215. }
  216. }
  217. }
  218.  
  219. GM_registerMenuCommand("Set GitHub Collapse In Comment Max Lines", () => {
  220. let val = prompt("Minimum number of lines before adding a toggle:",
  221. minLines);
  222. val = parseInt(val, 10);
  223. if (val) {
  224. minLines = val;
  225. GM_setValue("gcic-max-lines", val);
  226. update();
  227. }
  228. });
  229.  
  230. GM_registerMenuCommand("Set GitHub Collapse In Comment Initial State", () => {
  231. let val = prompt("Start with blocks collapsed?", !startCollapsed);
  232. if (val) {
  233. val = /^t/.test(val || "");
  234. startCollapsed = val;
  235. GM_setValue("gcic-start-collapsed", val);
  236. update();
  237. }
  238. });
  239.  
  240. $$("#js-repo-pjax-container, #js-pjax-container").forEach(target => {
  241. new MutationObserver(mutations => {
  242. mutations.forEach(mutation => {
  243. const mtarget = mutation.target;
  244. // preform checks before adding code wrap to minimize function calls
  245. // update after comments are edited
  246. if (!busy && (mtarget === target || mtarget.matches(
  247. ".js-comment-body, .js-preview-body"))) {
  248. clearTimeout(timer);
  249. timer = setTimeout(() => {
  250. addToggles();
  251. }, 100);
  252. }
  253. });
  254. }).observe(target, {
  255. childList: true,
  256. subtree: true
  257. });
  258. });
  259.  
  260. addBindings();
  261. addToggles();
  262. })();