GitHub TOC

A userscript that adds a table of contents to readme & wiki pages

当前为 2018-10-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub TOC
  3. // @version 1.2.20
  4. // @description A userscript that adds a table of contents to readme & wiki pages
  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. // @run-at document-idle
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_addStyle
  15. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=634242
  16. // @icon https://assets-cdn.github.com/pinned-octocat.svg
  17. // ==/UserScript==
  18. (() => {
  19. "use strict";
  20.  
  21. GM_addStyle(`
  22. /* z-index > 1000 to be above the */
  23. .ghus-toc { position:fixed; z-index:1001; min-width:200px; top:60px; right:10px; }
  24. .ghus-toc h3 { cursor:move; }
  25. /* icon toggles TOC container & subgroups */
  26. .ghus-toc h3 svg, .ghus-toc li.collapsible .ghus-toc-icon { cursor:pointer; vertical-align:baseline; }
  27. .ghus-toc .ghus-toc-docs { float:right; }
  28. /* move collapsed TOC to top right corner */
  29. .ghus-toc.collapsed {
  30. width:30px; height:30px; min-width:auto; overflow:hidden; top:16px !important; left:auto !important;
  31. right:10px !important; border:1px solid rgba(128, 128, 128, 0.5); border-radius:3px;
  32. }
  33. .ghus-toc.collapsed > h3 { cursor:pointer; padding-top:5px; border:none; background:#222; color:#ddd; }
  34. .ghus-toc.collapsed .ghus-toc-docs { display:none; }
  35. .ghus-toc.collapsed ~ .Header { padding-right: 30px !important; }
  36. /* move header text out-of-view when collapsed */
  37. .ghus-toc.collapsed > h3 svg { margin-bottom: 10px; }
  38. .ghus-toc-hidden, .ghus-toc.collapsed .boxed-group-inner,
  39. .ghus-toc li:not(.collapsible) .ghus-toc-icon { display:none; }
  40. .ghus-toc .boxed-group-inner { max-width:250px; max-height:400px; overflow-y:auto; overflow-x:hidden; }
  41. .ghus-toc ul { list-style:none; }
  42. .ghus-toc li { max-width:230px; white-space:nowrap; overflow-x:hidden; text-overflow:ellipsis; }
  43. .ghus-toc .ghus-toc-h1 { padding-left:15px; }
  44. .ghus-toc .ghus-toc-h2 { padding-left:30px; }
  45. .ghus-toc .ghus-toc-h3 { padding-left:45px; }
  46. .ghus-toc .ghus-toc-h4 { padding-left:60px; }
  47. .ghus-toc .ghus-toc-h5 { padding-left:75px; }
  48. .ghus-toc .ghus-toc-h6 { padding-left:90px; }
  49. /* anchor collapsible icon */
  50. .ghus-toc li.collapsible .ghus-toc-icon {
  51. width:16px; height:16px; display:inline-block; margin-left:-16px;
  52. background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSdvY3RpY29uJyBoZWlnaHQ9JzE0JyB2aWV3Qm94PScwIDAgMTIgMTYnPjxwYXRoIGQ9J00wIDVsNiA2IDYtNkgweic+PC9wYXRoPjwvc3ZnPg==) left center no-repeat;
  53. }
  54. /* on rotate, height becomes width, so this is keeping things lined up */
  55. .ghus-toc li.collapsible.collapsed .ghus-toc-icon { -webkit-transform:rotate(-90deg); transform:rotate(-90deg); height:10px; width:12px; margin-right:2px; }
  56. .ghus-toc-no-selection { -webkit-user-select:none !important; -moz-user-select:none !important; user-select:none !important; }
  57. `);
  58.  
  59. let tocInit = false,
  60.  
  61. // modifiable title
  62. title = GM_getValue("github-toc-title", "Table of Contents");
  63.  
  64. const container = document.createElement("div"),
  65.  
  66. // keyboard shortcuts
  67. keyboard = {
  68. toggle : "g+t",
  69. restore : "g+r",
  70. timer : null,
  71. lastKey : null,
  72. delay : 1000 // ms between keyboard shortcuts
  73. },
  74.  
  75. // drag variables
  76. drag = {
  77. el : null,
  78. pos : [0, 0],
  79. elm : [0, 0],
  80. time : 0,
  81. unsel: null
  82. };
  83.  
  84. // drag code adapted from http://jsfiddle.net/tovic/Xcb8d/light/
  85. function dragInit() {
  86. if (!container.classList.contains("collapsed")) {
  87. drag.el = container;
  88. drag.elm[0] = drag.pos[0] - drag.el.offsetLeft;
  89. drag.elm[1] = drag.pos[1] - drag.el.offsetTop;
  90. selectionToggle(true);
  91. } else {
  92. drag.el = null;
  93. }
  94. drag.time = new Date().getTime() + 500;
  95. }
  96.  
  97. function dragMove(event) {
  98. drag.pos[0] = document.all ? window.event.clientX : event.pageX;
  99. drag.pos[1] = document.all ? window.event.clientY : event.pageY;
  100. if (drag.el !== null) {
  101. drag.el.style.left = (drag.pos[0] - drag.elm[0]) + "px";
  102. drag.el.style.top = (drag.pos[1] - drag.elm[1]) + "px";
  103. drag.el.style.right = "auto";
  104. }
  105. }
  106.  
  107. function dragStop() {
  108. if (drag.el !== null) {
  109. dragSave();
  110. selectionToggle();
  111. }
  112. drag.el = null;
  113. }
  114.  
  115. function dragSave(clear) {
  116. let val = clear ? null : [container.style.left, container.style.top];
  117. GM_setValue("github-toc-location", val);
  118. }
  119.  
  120. // stop text selection while dragging
  121. function selectionToggle(disable) {
  122. const body = $("body");
  123. if (disable) {
  124. // save current "unselectable" value
  125. drag.unsel = body.getAttribute("unselectable");
  126. body.setAttribute("unselectable", "on");
  127. body.classList.add("ghus-toc-no-selection");
  128. on(body, "onselectstart", () => false);
  129. } else {
  130. if (drag.unsel) {
  131. body.setAttribute("unselectable", drag.unsel);
  132. }
  133. body.classList.remove("ghus-toc-no-selection");
  134. body.removeEventListener("onselectstart", () => false);
  135. }
  136. removeSelection();
  137. }
  138.  
  139. function removeSelection() {
  140. // remove text selection - http://stackoverflow.com/a/3171348/145346
  141. const sel = window.getSelection ? window.getSelection() : document.selection;
  142. if (sel) {
  143. if (sel.removeAllRanges) {
  144. sel.removeAllRanges();
  145. } else if (sel.empty) {
  146. sel.empty();
  147. }
  148. }
  149. }
  150.  
  151. function tocShow() {
  152. container.classList.remove("collapsed");
  153. GM_setValue("github-toc-hidden", false);
  154. }
  155.  
  156. function tocHide() {
  157. container.classList.add("collapsed");
  158. GM_setValue("github-toc-hidden", true);
  159. }
  160.  
  161. function tocToggle() {
  162. // don't toggle content on long clicks
  163. if (drag.time > new Date().getTime()) {
  164. if (container.classList.contains("collapsed")) {
  165. tocShow();
  166. } else {
  167. tocHide();
  168. }
  169. }
  170. }
  171. // hide TOC entirely, if no rendered markdown detected
  172. function tocView(mode) {
  173. const toc = $(".ghus-toc");
  174. if (toc) {
  175. toc.style.display = mode || "none";
  176. }
  177. }
  178.  
  179. function tocAdd() {
  180. // make sure the script is initialized
  181. init();
  182. if (!tocInit) {
  183. return;
  184. }
  185. if ($("#wiki-content, #readme")) {
  186. let indx, header, anchor, txt,
  187. content = "<ul>",
  188. anchors = $$(".markdown-body .anchor"),
  189. len = anchors.length;
  190. if (len > 2) {
  191. for (indx = 0; indx < len; indx++) {
  192. anchor = anchors[indx];
  193. if (anchor.parentNode) {
  194. header = anchor.parentNode;
  195. // replace single & double quotes with right angled quotes
  196. txt = header.textContent.trim().replace(/'/g, "&#8217;").replace(/"/g, "&#8221;");
  197. content += `
  198. <li class="ghus-toc-${header.nodeName.toLowerCase()}">
  199. <span class="ghus-toc-icon octicon ghd-invert"></span>
  200. <a href="${anchor.hash}" title="${txt}">${txt}</a>
  201. </li>
  202. `;
  203. }
  204. }
  205. $(".boxed-group-inner", container).innerHTML = content + "</ul>";
  206. tocView("block");
  207. listCollapsible();
  208. } else {
  209. tocView();
  210. }
  211. } else {
  212. tocView();
  213. }
  214. }
  215.  
  216. function listCollapsible() {
  217. let indx, el, next, count, num, group,
  218. els = $$("li", container),
  219. len = els.length;
  220. for (indx = 0; indx < len; indx++) {
  221. count = 0;
  222. group = [];
  223. el = els[indx];
  224. next = el && el.nextElementSibling;
  225. if (next) {
  226. num = el.className.match(/\d/)[0];
  227. while (next && !next.classList.contains("ghus-toc-h" + num)) {
  228. if (next.className.match(/\d/)[0] > num) {
  229. count++;
  230. group[group.length] = next;
  231. }
  232. next = next.nextElementSibling;
  233. }
  234. if (count > 0) {
  235. el.className += " collapsible collapsible-" + indx;
  236. addClass(group, "ghus-toc-childof-" + indx);
  237. }
  238. }
  239. }
  240. group = [];
  241. on(container, "click", event => {
  242. // click on icon, then target LI parent
  243. let els, name, indx,
  244. el = event.target.parentNode,
  245. collapse = el.classList.contains("collapsed");
  246. if (event.target.classList.contains("ghus-toc-icon")) {
  247. if (event.shiftKey) {
  248. name = el.className.match(/ghus-toc-h\d/);
  249. els = name ? $$("." + name, container) : [];
  250. indx = els.length;
  251. while (indx--) {
  252. collapseChildren(els[indx], collapse);
  253. }
  254. } else {
  255. collapseChildren(el, collapse);
  256. }
  257. removeSelection();
  258. }
  259. });
  260. }
  261. function collapseChildren(el, collapse) {
  262. let name = el && el.className.match(/collapsible-(\d+)/),
  263. children = name ? $$(".ghus-toc-childof-" + name[1], container) : null;
  264. if (children) {
  265. if (collapse) {
  266. el.classList.remove("collapsed");
  267. removeClass(children, "ghus-toc-hidden");
  268. } else {
  269. el.classList.add("collapsed");
  270. addClass(children, "ghus-toc-hidden");
  271. }
  272. }
  273. }
  274.  
  275. // keyboard shortcuts
  276. // GitHub hotkeys are set up to only go to a url, so rolling our own
  277. function keyboardCheck(event) {
  278. clearTimeout(keyboard.timer);
  279. // use "g+t" to toggle the panel; "g+r" to reset the position
  280. // keypress may be needed for non-alphanumeric keys
  281. let tocToggle = keyboard.toggle.split("+"),
  282. tocReset = keyboard.restore.split("+"),
  283. key = String.fromCharCode(event.which).toLowerCase(),
  284. panelHidden = container.classList.contains("collapsed");
  285.  
  286. // press escape to close the panel
  287. if (event.which === 27 && !panelHidden) {
  288. tocHide();
  289. return;
  290. }
  291. // prevent opening panel while typing in comments
  292. if (/(input|textarea)/i.test(document.activeElement.nodeName)) {
  293. return;
  294. }
  295. // toggle TOC (g+t)
  296. if (keyboard.lastKey === tocToggle[0] && key === tocToggle[1]) {
  297. if (panelHidden) {
  298. tocShow();
  299. } else {
  300. tocHide();
  301. }
  302. }
  303. // reset TOC window position (g+r)
  304. if (keyboard.lastKey === tocReset[0] && key === tocReset[1]) {
  305. container.setAttribute("style", "");
  306. dragSave(true);
  307. }
  308. keyboard.lastKey = key;
  309. keyboard.timer = setTimeout(() => {
  310. keyboard.lastKey = null;
  311. }, keyboard.delay);
  312. }
  313.  
  314. function init() {
  315. // there is no ".header" on github.com/contact; and some other pages
  316. if (!$(".header, .Header") || tocInit) {
  317. return;
  318. }
  319. // insert TOC after header
  320. let tmp = GM_getValue("github-toc-location", null);
  321. // restore last position
  322. if (tmp) {
  323. container.style.left = tmp[0];
  324. container.style.top = tmp[1];
  325. container.style.right = "auto";
  326. }
  327.  
  328. // TOC saved state
  329. tmp = GM_getValue("github-toc-hidden", false);
  330. container.className = "ghus-toc boxed-group wiki-pages-box readability-sidebar" + (tmp ? " collapsed" : "");
  331. container.setAttribute("role", "navigation");
  332. container.setAttribute("unselectable", "on");
  333. container.innerHTML = `
  334. <h3 class="js-wiki-toggle-collapse wiki-auxiliary-content" data-hotkey="g t">
  335. <svg class="octicon ghus-toc-icon" height="14" width="14" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 16 12">
  336. <path d="M2 13c0 .6 0 1-.6 1H.6c-.6 0-.6-.4-.6-1s0-1 .6-1h.8c.6 0 .6.4.6 1zm2.6-9h6.8c.6 0 .6-.4.6-1s0-1-.6-1H4.6C4 2 4 2.4 4 3s0 1 .6 1zM1.4 7H.6C0 7 0 7.4 0 8s0 1 .6 1h.8C2 9 2 8.6 2 8s0-1-.6-1zm0-5H.6C0 2 0 2.4 0 3s0 1 .6 1h.8C2 4 2 3.6 2 3s0-1-.6-1zm10 5H4.6C4 7 4 7.4 4 8s0 1 .6 1h6.8c.6 0 .6-.4.6-1s0-1-.6-1zm0 5H4.6c-.6 0-.6.4-.6 1s0 1 .6 1h6.8c.6 0 .6-.4.6-1s0-1-.6-1z"/>
  337. </svg>
  338. <span>${title}</span>
  339. <a class="ghus-toc-docs tooltipped tooltipped-w" aria-label="Go to documentation" href="https://github.com/Mottie/GitHub-userscripts/wiki/GitHub-table-of-contents">
  340. <svg class="octicon" xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 16 14">
  341. <path d="M6 10h2v2H6V10z m4-3.5c0 2.14-2 2.5-2 2.5H6c0-0.55 0.45-1 1-1h0.5c0.28 0 0.5-0.22 0.5-0.5v-1c0-0.28-0.22-0.5-0.5-0.5h-1c-0.28 0-0.5 0.22-0.5 0.5v0.5H4c0-1.5 1.5-3 3-3s3 1 3 2.5zM7 2.3c3.14 0 5.7 2.56 5.7 5.7S10.14 13.7 7 13.7 1.3 11.14 1.3 8s2.56-5.7 5.7-5.7m0-1.3C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7S10.86 1 7 1z" />
  342. </svg>
  343. </a>
  344. </h3>
  345. <div class="boxed-group-inner wiki-auxiliary-content wiki-auxiliary-content-no-bg"></div>
  346. `;
  347.  
  348. // add container
  349. tmp = $(".header, .Header");
  350. tmp.parentNode.insertBefore(container, tmp);
  351.  
  352. // make draggable
  353. on($("h3", container), "mousedown", dragInit);
  354. on(document, "mousemove", dragMove);
  355. on(document, "mouseup", dragStop);
  356. // toggle TOC
  357. on($(".ghus-toc-icon", container), "mouseup", tocToggle);
  358. // prevent container content selection
  359. on(container, "onselectstart", () => false );
  360. // keyboard shortcuts
  361. on(document, "keydown", keyboardCheck);
  362. tocInit = true;
  363. }
  364.  
  365. function $(str, el) {
  366. return (el || document).querySelector(str);
  367. }
  368.  
  369. function $$(str, el) {
  370. return Array.from((el || document).querySelectorAll(str));
  371. }
  372.  
  373. function on(el, name, handler) {
  374. el.addEventListener(name, handler);
  375. }
  376.  
  377. function addClass(els, name) {
  378. let indx,
  379. len = els.length;
  380. for (indx = 0; indx < len; indx++) {
  381. els[indx].classList.add(name);
  382. }
  383. }
  384.  
  385. function removeClass(els, name) {
  386. let indx,
  387. len = els.length;
  388. for (indx = 0; indx < len; indx++) {
  389. els[indx].classList.remove(name);
  390. }
  391. }
  392.  
  393. // Add GM options
  394. GM_registerMenuCommand("Set Table of Contents Title", () => {
  395. title = prompt("Table of Content Title:", title);
  396. GM_setValue("github-toc-title", title);
  397. $("h3 span", container).textContent = title;
  398. });
  399.  
  400. on(document, "ghmo:container", tocAdd);
  401. on(document, "ghmo:preview", tocAdd);
  402. tocAdd();
  403.  
  404. })();