GitHub TOC

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

当前为 2016-06-06 提交的版本,查看 最新版本

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