GitHub TOC

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

当前为 2017-03-25 提交的版本,查看 最新版本

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