GitLab Collapse In Comment

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

  1. // ==UserScript==
  2. // @name GitLab Collapse In Comment
  3. // @version 0.1.0
  4. // @description A userscript that adds a header that can toggle long code and quote blocks in comments
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://gitlab.com/Mottie
  8. // @include https://gitlab.com/*
  9. // @run-at document-idle
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_registerMenuCommand
  14. // @icon https://gitlab.com/assets/gitlab_logo-7ae504fe4f68fdebb3c2034e36621930cd36ea87924c11ff65dbcb8ed50dca58.png
  15. // ==/UserScript==
  16. (() => {
  17. "use strict";
  18. // hide code/quotes longer than this number of lines
  19. let minLines = GM_getValue("glic-max-lines", 10),
  20. startCollapsed = GM_getValue("glic-start-collapsed", true);
  21.  
  22. GM_addStyle(`
  23. .glic-block {
  24. border:#eee 1px solid;
  25. padding:2px 8px 2px 10px;
  26. border-radius:5px 5px 0 0;
  27. position:relative;
  28. top:1px;
  29. cursor:pointer;
  30. font-weight:bold;
  31. display:block;
  32. text-transform:capitalize;
  33. }
  34. .glic-block + .highlight {
  35. border-top:none;
  36. }
  37. .glic-block + .glic-has-toggle {
  38. margin-top:0 !important;
  39. }
  40. .glic-block:after {
  41. content:"\u25bc ";
  42. float:right;
  43. }
  44. .glic-block-closed {
  45. border-radius:5px;
  46. margin-bottom:10px;
  47. }
  48. .glic-block-closed:after {
  49. transform: rotate(90deg);
  50. }
  51. .glic-block-closed + .glic-has-toggle,
  52. .glic-block-closed + pre {
  53. display:none;
  54. }
  55. `);
  56.  
  57. function addToggles() {
  58. // issue comments
  59. if ($(".issue-details")) {
  60. let indx = 0;
  61. const block = document.createElement("a"),
  62. els = $$(`
  63. .wiki pre, .note-body pre, .md-preview pre,
  64. .wiki blockquote, .note-body blockquote, .md-preview blockquote
  65. `),
  66. len = els.length;
  67. block.className = `glic-block border${
  68. startCollapsed ? " glic-block-closed" : ""
  69. }`;
  70. block.href = "#";
  71.  
  72. // loop with delay to allow user interaction
  73. const loop = () => {
  74. let el, wrap, node, syntaxClass, numberOfLines,
  75. // max number of DOM insertions per loop
  76. max = 0;
  77. while (max < 20 && indx < len) {
  78. if (indx >= len) {
  79. return;
  80. }
  81. el = els[indx];
  82. if (el && !el.classList.contains("glic-has-toggle")) {
  83. numberOfLines = el.innerHTML.split("\n").length;
  84. if (numberOfLines > minLines) {
  85. syntaxClass = "";
  86. wrap = closest(".highlight", el);
  87. if (wrap && wrap.classList.contains("highlight")) {
  88. syntaxClass = wrap.getAttribute("lang");
  89. } else {
  90. // no syntax highlighter defined (not wrapped)
  91. wrap = el;
  92. }
  93. node = block.cloneNode();
  94. node.innerHTML = `${syntaxClass || "Block"}
  95. (${numberOfLines} lines)`;
  96. wrap.parentNode.insertBefore(node, wrap);
  97. el.classList.add("glic-has-toggle");
  98. if (startCollapsed) {
  99. el.display = "none";
  100. }
  101. max++;
  102. }
  103. }
  104. indx++;
  105. }
  106. if (indx < len) {
  107. setTimeout(() => {
  108. loop();
  109. }, 200);
  110. }
  111. };
  112. loop();
  113. }
  114. }
  115.  
  116. function addBindings() {
  117. document.addEventListener("click", event => {
  118. let els, indx, flag;
  119. const el = event.target;
  120. if (el && el.classList.contains("glic-block")) {
  121. event.preventDefault();
  122. // shift + click = toggle all blocks in a single comment
  123. // shift + ctrl + click = toggle all blocks on page
  124. if (event.shiftKey) {
  125. els = $$(
  126. ".glic-block",
  127. event.ctrlKey ? "" : closest(".wiki, .note-body", el)
  128. );
  129. indx = els.length;
  130. flag = el.classList.contains("glic-block-closed");
  131. while (indx--) {
  132. els[indx].classList.toggle("glic-block-closed", !flag);
  133. }
  134. } else {
  135. el.classList.toggle("glic-block-closed");
  136. }
  137. removeSelection();
  138. }
  139. });
  140. }
  141.  
  142. function update() {
  143. let toggles = $$(".glic-block"),
  144. indx = toggles.length;
  145. while (indx--) {
  146. toggles[indx].parentNode.removeChild(toggles[indx]);
  147. }
  148. toggles = $$(".glic-has-toggle");
  149. indx = toggles.length;
  150. while (indx--) {
  151. toggles[indx].classList.remove("glic-has-toggle");
  152. }
  153. addToggles();
  154. }
  155.  
  156. function $(selector, el) {
  157. return (el || document).querySelector(selector);
  158. }
  159.  
  160. function $$(selector, el) {
  161. return Array.from((el || document).querySelectorAll(selector));
  162. }
  163.  
  164. function closest(selector, el) {
  165. while (el && el.nodeType === 1) {
  166. if (el.matches(selector)) {
  167. return el;
  168. }
  169. el = el.parentNode;
  170. }
  171. return null;
  172. }
  173.  
  174. function removeSelection() {
  175. // remove text selection - https://stackoverflow.com/a/3171348/145346
  176. const sel = window.getSelection ?
  177. window.getSelection() :
  178. document.selection;
  179. if (sel) {
  180. if (sel.removeAllRanges) {
  181. sel.removeAllRanges();
  182. } else if (sel.empty) {
  183. sel.empty();
  184. }
  185. }
  186. }
  187.  
  188. GM_registerMenuCommand("Set GitLab Collapse In Comment Max Lines", () => {
  189. let val = prompt("Minimum number of lines before adding a toggle:",
  190. minLines);
  191. val = parseInt(val, 10);
  192. if (val) {
  193. minLines = val;
  194. GM_setValue("glic-max-lines", val);
  195. update();
  196. }
  197. });
  198.  
  199. GM_registerMenuCommand("Set GitLab Collapse In Comment Initial State", () => {
  200. let val = prompt(
  201. "Start with blocks (c)ollapsed or (e)xpanded (first letter necessary):",
  202. startCollapsed ? "collapsed" : "expanded"
  203. );
  204. if (val) {
  205. val = /^c/.test(val || "");
  206. startCollapsed = val;
  207. GM_setValue("glic-start-collapsed", val);
  208. update();
  209. }
  210. });
  211.  
  212. addBindings();
  213. addToggles();
  214. // adding a timeout to check the code blocks again... because code blocks in
  215. // the original issue appear to be rendered, then replaced while the comment
  216. // code blocks are not
  217. setTimeout(() => {
  218. addToggles();
  219. }, 500);
  220.  
  221. // bind to GitLab event using jQuery; "markdown-preview:show" callback
  222. // is executed by trigger-handler, so no event is actually triggered
  223. jQuery(document).on("markdown-preview:show", () => {
  224. // preview is shown, but not yet rendered...
  225. setTimeout(() => {
  226. addToggles();
  227. }, 300);
  228. });
  229.  
  230. })();