Bitbucket Collapse Markdown

A userscript that collapses markdown headers

  1. // ==UserScript==
  2. // @name Bitbucket Collapse Markdown
  3. // @version 0.1.1
  4. // @description A userscript that collapses markdown headers
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://bitbucket.org/*
  9. // @run-at document-idle
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_registerMenuCommand
  14. // @icon https://bitbucket.org/mottie/bitbucket-userscripts/raw/HEAD/images/bitbucket.svg
  15. // ==/UserScript==
  16. (() => {
  17. "use strict";
  18.  
  19. const defaultColors = [
  20. // palette generated by http://tools.medialab.sciences-po.fr/iwanthue/
  21. // (colorblind friendly, soft)
  22. "#6778d0", "#ac9c3d", "#b94a73", "#56ae6c", "#9750a1", "#ba543d"
  23. ],
  24.  
  25. headers = "H1 H2 H3 H4 H5 H6".split(" "),
  26. collapsed = "bbcm-collapsed",
  27. arrowColors = document.createElement("style");
  28.  
  29. let startCollapsed = GM_getValue("bbcm-collapsed", false),
  30. colors = GM_getValue("bbcm-colors", defaultColors);
  31.  
  32. GM_addStyle(`
  33. .wiki-content h1, .wiki-content h2, .wiki-content h3,
  34. .wiki-content h4, .wiki-content h5, .wiki-content h6 {
  35. position:relative;
  36. padding-right:.8em;
  37. cursor:pointer;
  38. }
  39. .wiki-content h1:after, .wiki-content h2:after, .wiki-content h3:after,
  40. .wiki-content h4:after, .wiki-content h5:after, .wiki-content h6:after {
  41. display:inline-block;
  42. position:absolute;
  43. right:0;
  44. top:calc(50% - .5em);
  45. font-size:.8em;
  46. content:"\u25bc";
  47. }
  48. .wiki-content .${collapsed}:after {
  49. transform: rotate(90deg);
  50. }
  51. .bbcm-hidden {
  52. display:none !important;
  53. }
  54. `);
  55.  
  56. function addColors() {
  57. arrowColors.textContent = `
  58. .wiki-content h1:after { color:${colors[0]} }
  59. .wiki-content h2:after { color:${colors[1]} }
  60. .wiki-content h3:after { color:${colors[2]} }
  61. .wiki-content h4:after { color:${colors[3]} }
  62. .wiki-content h5:after { color:${colors[4]} }
  63. .wiki-content h6:after { color:${colors[5]} }
  64. `;
  65. }
  66.  
  67. function toggle(el, shifted) {
  68. if (el) {
  69. el.classList.toggle(collapsed);
  70. let els;
  71. const name = el.nodeName || "",
  72. level = parseInt(name.replace(/[^\d]/, ""), 10),
  73. isCollapsed = el.classList.contains(collapsed);
  74. if (shifted) {
  75. // collapse all same level anchors
  76. els = $$(`.wiki-content ${name}`);
  77. for (el of els) {
  78. nextHeader(el, level, isCollapsed);
  79. }
  80. } else {
  81. nextHeader(el, level, isCollapsed);
  82. }
  83. removeSelection();
  84. }
  85. }
  86.  
  87. function nextHeader(el, level, isCollapsed) {
  88. el.classList.toggle(collapsed, isCollapsed);
  89. const selector = headers.slice(0, level).join(","),
  90. name = [collapsed, "bbcm-hidden"],
  91. els = [];
  92. el = el.nextElementSibling;
  93. while (el && !el.matches(selector)) {
  94. els[els.length] = el;
  95. el = el.nextElementSibling;
  96. }
  97. if (els.length) {
  98. if (isCollapsed) {
  99. els.forEach(el => {
  100. el.classList.add("bbcm-hidden");
  101. });
  102. } else {
  103. els.forEach(el => {
  104. el.classList.remove(...name);
  105. });
  106. }
  107. }
  108. }
  109.  
  110. function removeSelection() {
  111. // remove text selection - https://stackoverflow.com/a/3171348/145346
  112. const sel = window.getSelection ?
  113. window.getSelection() :
  114. document.selection;
  115. if (sel) {
  116. if (sel.removeAllRanges) {
  117. sel.removeAllRanges();
  118. } else if (sel.empty) {
  119. sel.empty();
  120. }
  121. }
  122. }
  123.  
  124. function addBinding() {
  125. document.addEventListener("click", event => {
  126. const target = event.target;
  127. if (target && headers.indexOf(target.nodeName || "") > -1) {
  128. // make sure the header is inside of markdown
  129. if (closest(".wiki-content", target)) {
  130. toggle(target, event.shiftKey);
  131. }
  132. }
  133. });
  134. }
  135.  
  136. function checkColors() {
  137. if (!colors || colors.length !== 6) {
  138. colors = [].concat(defaultColors);
  139. }
  140. }
  141.  
  142. function init() {
  143. document.querySelector("head").appendChild(arrowColors);
  144. checkColors();
  145. addColors();
  146. addBinding();
  147. }
  148.  
  149. function $$(selectors, el) {
  150. return Array.from((el || document).querySelectorAll(selectors));
  151. }
  152.  
  153. function closest(selector, el) {
  154. while (el && el.nodeType === 1) {
  155. if (el.matches(selector)) {
  156. return el;
  157. }
  158. el = el.parentNode;
  159. }
  160. return null;
  161. }
  162.  
  163. // Add GM options
  164. GM_registerMenuCommand("Set Bitbucket collapse markdown state", () => {
  165. const val = prompt(
  166. "Set initial state to (c)ollapsed or (e)xpanded (first letter only):",
  167. startCollapsed ? "collapsed" : "expanded"
  168. );
  169. if (val !== null) {
  170. startCollapsed = /^c/i.test(val);
  171. GM_setValue("bbcm-collapsed", startCollapsed);
  172. console.log(
  173. `Bitbucket Collapse Markdown: Headers will ` +
  174. `${startCollapsed ? "be" : "not be"} initially collapsed`
  175. );
  176. }
  177. });
  178.  
  179. GM_registerMenuCommand("Set Bitbucket collapse markdown colors", () => {
  180. let val = prompt("Set header arrow colors:", JSON.stringify(colors));
  181. if (val !== null) {
  182. // allow pasting in a JSON format
  183. try {
  184. val = JSON.parse(val);
  185. if (val && val.length === 6) {
  186. colors = val;
  187. GM_setValue("bbcm-colors", colors);
  188. console.log("Bitbucket Collapse Markdown: colors set to", colors);
  189. addColors();
  190. return;
  191. }
  192. console.error(
  193. "Bitbucket Collapse Markdown: invalid color definition (6 colors)",
  194. val
  195. );
  196. // reset colors to default (in case colors variable is corrupted)
  197. checkColors();
  198. } catch (err) {
  199. console.error("Bitbucket Collapse Markdown: invalid JSON");
  200. }
  201. }
  202. });
  203.  
  204. init();
  205.  
  206. })();