GitHub Collapse Markdown

A userscript that collapses markdown headers

目前為 2017-10-01 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name GitHub Collapse Markdown
  3. // @version 1.1.10
  4. // @description A userscript that collapses markdown headers
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://github.com/*
  9. // @include https://gist.github.com/*
  10. // @include https://help.github.com/*
  11. // @run-at document-idle
  12. // @grant GM_addStyle
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_registerMenuCommand
  16. // @icon https://github.com/fluidicon.png
  17. // ==/UserScript==
  18. (() => {
  19. "use strict";
  20.  
  21. const defaultColors = [
  22. // palette generated by http://tools.medialab.sciences-po.fr/iwanthue/
  23. // (colorblind friendly, soft)
  24. "#6778d0", "#ac9c3d", "#b94a73", "#56ae6c", "#9750a1", "#ba543d"
  25. ],
  26.  
  27. blocks = [
  28. ".markdown-body",
  29. ".markdown-format",
  30. "" // leave empty string at the end
  31. ],
  32.  
  33. headers = "H1 H2 H3 H4 H5 H6".split(" "),
  34. collapsed = "ghcm-collapsed",
  35. arrowColors = document.createElement("style");
  36.  
  37. let startCollapsed = GM_getValue("ghcm-collapsed", false),
  38. colors = GM_getValue("ghcm-colors", defaultColors);
  39.  
  40. // .markdown-body h1:after, .markdown-format h1:after, ... {}
  41. GM_addStyle(`
  42. ${blocks.join(" h1,")} ${blocks.join(" h2,")}
  43. ${blocks.join(" h3,")} ${blocks.join(" h4,")}
  44. ${blocks.join(" h5,")} ${blocks.join(" h6,").slice(0, -1)} {
  45. position:relative;
  46. padding-right:.8em;
  47. cursor:pointer;
  48. }
  49. ${blocks.join(" h1:after,")} ${blocks.join(" h2:after,")}
  50. ${blocks.join(" h3:after,")} ${blocks.join(" h4:after,")}
  51. ${blocks.join(" h5:after,")} ${blocks.join(" h6:after,").slice(0, -1)} {
  52. display:inline-block;
  53. position:absolute;
  54. right:0;
  55. top:calc(50% - .5em);
  56. font-size:.8em;
  57. content:"\u25bc";
  58. }
  59. ${blocks.join(" ." + collapsed + ":after,").slice(0, -1)} {
  60. transform: rotate(90deg);
  61. }
  62. /* clicking on header link won't pass svg as the event.target */
  63. .octicon-link, .octicon-link > * {
  64. pointer-events:none;
  65. }
  66. .ghcm-hidden {
  67. display:none !important;
  68. }
  69. `);
  70.  
  71. function addColors() {
  72. let sel,
  73. styles = "";
  74. headers.forEach((header, indx) => {
  75. sel = `${blocks.join(" " + header + ":after,").slice(0, -1)}`;
  76. styles += `${sel} { color:${colors[indx]} }`;
  77. });
  78. arrowColors.textContent = styles;
  79. }
  80.  
  81. function toggle(el, shifted) {
  82. if (el) {
  83. el.classList.toggle(collapsed);
  84. let els;
  85. const name = el.nodeName || "",
  86. // convert H# to #
  87. level = parseInt(name.replace(/[^\d]/, ""), 10),
  88. isCollapsed = el.classList.contains(collapsed);
  89. if (shifted) {
  90. // collapse all same level anchors
  91. els = $$(`${blocks.join(" " + name + ",").slice(0, -1)}`);
  92. for (el of els) {
  93. nextHeader(el, level, isCollapsed);
  94. }
  95. } else {
  96. nextHeader(el, level, isCollapsed);
  97. }
  98. removeSelection();
  99. }
  100. }
  101.  
  102. function nextHeader(el, level, isCollapsed) {
  103. el.classList.toggle(collapsed, isCollapsed);
  104. const selector = headers.slice(0, level).join(","),
  105. name = [collapsed, "ghcm-hidden"],
  106. els = [];
  107. el = el.nextElementSibling;
  108. while (el && !el.matches(selector)) {
  109. els[els.length] = el;
  110. el = el.nextElementSibling;
  111. }
  112. if (els.length) {
  113. if (isCollapsed) {
  114. els.forEach(el => {
  115. el.classList.add("ghcm-hidden");
  116. });
  117. } else {
  118. els.forEach(el => {
  119. el.classList.remove(...name);
  120. });
  121. }
  122. }
  123. }
  124.  
  125. // show siblings of hash target
  126. function siblings(target) {
  127. let el = target.nextElementSibling,
  128. els = [target];
  129. const level = parseInt((target.nodeName || "").replace(/[^\d]/, ""), 10),
  130. selector = headers.slice(0, level - 1).join(",");
  131. while (el && !el.matches(selector)) {
  132. els[els.length] = el;
  133. el = el.nextElementSibling;
  134. }
  135. el = target.previousElementSibling;
  136. while (el && !el.matches(selector)) {
  137. els[els.length] = el;
  138. el = el.previousElementSibling;
  139. }
  140. if (els.length) {
  141. els = els.filter(el => {
  142. return el.nodeName === target.nodeName;
  143. });
  144. for (el of els) {
  145. el.classList.remove("glcm-hidden");
  146. }
  147. }
  148. nextHeader(target, level, false);
  149. }
  150.  
  151. function removeSelection() {
  152. // remove text selection - https://stackoverflow.com/a/3171348/145346
  153. const sel = window.getSelection ? window.getSelection() : document.selection;
  154. if (sel) {
  155. if (sel.removeAllRanges) {
  156. sel.removeAllRanges();
  157. } else if (sel.empty) {
  158. sel.empty();
  159. }
  160. }
  161. }
  162.  
  163. function addBinding() {
  164. document.addEventListener("click", event => {
  165. let target = event.target;
  166. const name = (target && (target.nodeName || "")).toLowerCase();
  167. if (name === "path") {
  168. target = closest("svg", target);
  169. }
  170. if (!target || target.classList.contains("anchor") ||
  171. name === "a" || name === "img" ||
  172. // add support for "pointer-events:none" applied to "anchor" in
  173. // https://github.com/StylishThemes/GitHub-FixedHeader
  174. target.classList.contains("octicon-link")) {
  175. return;
  176. }
  177. // check if element is inside a header
  178. target = closest(headers.join(","), event.target);
  179. if (target && headers.indexOf(target.nodeName || "") > -1) {
  180. // make sure the header is inside of markdown
  181. if (closest(blocks.slice(0, -1).join(","), target)) {
  182. toggle(target, event.shiftKey);
  183. }
  184. }
  185. });
  186. }
  187.  
  188. function checkHash() {
  189. let el, els, md;
  190. const mds = $$(blocks.slice(0, -1).join(",")),
  191. id = (window.location.hash || "").replace(/#/, "");
  192. for (md of mds) {
  193. els = $$(headers.join(","), md);
  194. if (els.length > 1) {
  195. for (el of els) {
  196. if (el && !el.classList.contains(collapsed)) {
  197. toggle(el, true);
  198. }
  199. }
  200. }
  201. }
  202. if (id) {
  203. openHash(id);
  204. }
  205. }
  206.  
  207. // open header matching hash
  208. function openHash(id) {
  209. const els = $(`#user-content-${tmp}`);
  210. if (els && els.classList.contains("anchor")) {
  211. let el = els.parentNode;
  212. if (el.matches(headers.join(","))) {
  213. siblings(el);
  214. document.documentElement.scrollTop = el.offsetTop;
  215. // set scrollTop a second time, in case of browser lag
  216. setTimeout(() => {
  217. document.documentElement.scrollTop = el.offsetTop;
  218. }, 500);
  219. }
  220. }
  221. }
  222.  
  223. function checkColors() {
  224. if (!colors || colors.length !== 6) {
  225. colors = [].concat(defaultColors);
  226. }
  227. }
  228.  
  229. function init() {
  230. document.querySelector("head").appendChild(arrowColors);
  231. checkColors();
  232. addColors();
  233. addBinding();
  234. if (startCollapsed) {
  235. checkHash();
  236. }
  237. }
  238.  
  239. function $(selector, el) {
  240. return (el || document).querySelector(selector);
  241. }
  242.  
  243. function $$(selectors, el) {
  244. return [...(el || document).querySelectorAll(selectors)];
  245. }
  246.  
  247. function closest(selector, el) {
  248. while (el && el.nodeType === 1) {
  249. if (el.matches(selector)) {
  250. return el;
  251. }
  252. el = el.parentNode;
  253. }
  254. return null;
  255. }
  256.  
  257. // Add GM options
  258. GM_registerMenuCommand("Set collapse markdown state", () => {
  259. const val = prompt(
  260. "Set initial state to (c)ollapsed or (e)xpanded (first letter necessary):",
  261. startCollapsed ? "collapsed" : "expanded"
  262. );
  263. if (val !== null) {
  264. startCollapsed = /^c/i.test(val);
  265. GM_setValue("ghcm-collapsed", startCollapsed);
  266. console.log(
  267. `GitHub Collapse Markdown: Headers will ${startCollapsed ? "be" : "not be"} initially collapsed`
  268. );
  269. }
  270. });
  271.  
  272. GM_registerMenuCommand("Set collapse markdown colors", () => {
  273. let val = prompt("Set header arrow colors:", JSON.stringify(colors));
  274. if (val !== null) {
  275. // allow pasting in a JSON format
  276. try {
  277. val = JSON.parse(val);
  278. if (val && val.length === 6) {
  279. colors = val;
  280. GM_setValue("ghcm-colors", colors);
  281. console.log("GitHub Collapse Markdown: colors set to", colors);
  282. addColors();
  283. return;
  284. }
  285. console.error(
  286. "GitHub Collapse Markdown: invalid color definition (6 colors)",
  287. val
  288. );
  289. // reset colors to default (in case colors variable is corrupted)
  290. checkColors();
  291. } catch (err) {
  292. console.error("GitHub Collapse Markdown: invalid JSON");
  293. }
  294. }
  295. });
  296.  
  297. init();
  298.  
  299. })();