GitHub Collapse In Comment

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

目前為 2016-07-30 提交的版本,檢視 最新版本

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