GitHub TOC

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

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

  1. // ==UserScript==
  2. // @name GitHub TOC
  3. // @version 1.0.1
  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. // modifiable title
  55. var title = GM_getValue("github-toc-title", "Table of Contents"),
  56.  
  57. container = document.createElement("div"),
  58. busy = false,
  59. tocInit = false,
  60.  
  61. // keyboard shortcuts
  62. keyboard = {
  63. toggle : "g+t",
  64. restore : "g+r",
  65. timer : null,
  66. lastKey : null,
  67. delay : 1000 // ms between keyboard shortcuts
  68. },
  69.  
  70. // drag variables
  71. drag = {
  72. el : null,
  73. pos : [ 0, 0 ],
  74. elm : [ 0, 0 ],
  75. time : 0,
  76. unsel: null
  77. },
  78. // drag code adapted from http://jsfiddle.net/tovic/Xcb8d/light/
  79. dragInit = function() {
  80. if (!container.classList.contains("collapsed")) {
  81. drag.el = container;
  82. drag.elm[0] = drag.pos[0] - drag.el.offsetLeft;
  83. drag.elm[1] = drag.pos[1] - drag.el.offsetTop;
  84. selectionToggle(true);
  85. } else {
  86. drag.el = null;
  87. }
  88. drag.time = new Date().getTime() + 500;
  89. },
  90. dragMove = function(event) {
  91. drag.pos[0] = document.all ? window.event.clientX : event.pageX;
  92. drag.pos[1] = document.all ? window.event.clientY : event.pageY;
  93. if (drag.el !== null) {
  94. drag.el.style.left = (drag.pos[0] - drag.elm[0]) + "px";
  95. drag.el.style.top = (drag.pos[1] - drag.elm[1]) + "px";
  96. drag.el.style.right = "auto";
  97. }
  98. },
  99. dragStop = function() {
  100. if (drag.el !== null) {
  101. dragSave();
  102. selectionToggle();
  103. }
  104. drag.el = null;
  105. },
  106. dragSave = function(clear) {
  107. var val = clear ? null : [container.style.left, container.style.top];
  108. GM_setValue("github-toc-location", val);
  109. },
  110.  
  111. // stop text selection while dragging
  112. selectionToggle = function(disable) {
  113. var sel,
  114. body = document.querySelector("body");
  115. if (disable) {
  116. // save current "unselectable" value
  117. drag.unsel = body.getAttribute("unselectable");
  118. body.setAttribute("unselectable", "on");
  119. body.classList.add("github-toc-no-selection");
  120. body.addEventListener("onselectstart", selectionStop);
  121. } else {
  122. if (drag.unsel) {
  123. body.setAttribute("unselectable", drag.unsel);
  124. }
  125. body.classList.remove("github-toc-no-selection");
  126. body.removeEventListener("onselectstart", selectionStop);
  127. }
  128. // remove text selection - http://stackoverflow.com/a/3171348/145346
  129. sel = window.getSelection ? window.getSelection() : document.selection;
  130. if ( sel ) {
  131. if ( sel.removeAllRanges ) {
  132. sel.removeAllRanges();
  133. } else if ( sel.empty ) {
  134. sel.empty();
  135. }
  136. }
  137. },
  138. selectionStop = function() {
  139. return false;
  140. },
  141.  
  142. tocShow = function() {
  143. container.classList.remove("collapsed");
  144. GM_setValue("github-toc-hidden", false);
  145. },
  146. tocHide = function() {
  147. container.classList.add("collapsed");
  148. GM_setValue("github-toc-hidden", true);
  149. },
  150. tocToggle = function() {
  151. // don't toggle content on long clicks
  152. if (drag.time > new Date().getTime()) {
  153. if (container.classList.contains("collapsed")) {
  154. tocShow();
  155. } else {
  156. tocHide();
  157. }
  158. }
  159. },
  160. // hide TOC entirely, if no rendered markdown detected
  161. tocView = function(mode) {
  162. var toc = document.querySelector(".github-toc");
  163. if (toc) {
  164. toc.style.display = mode || "none";
  165. }
  166. },
  167.  
  168. tocAdd = function() {
  169. // make sure the script is initialized
  170. init();
  171. if (!tocInit) {
  172. return;
  173. }
  174. if (document.querySelectorAll("#wiki-content, #readme")) {
  175. var indx, header, anchor, txt,
  176. content = "<ul>",
  177. anchors = document.querySelectorAll(".markdown-body .anchor"),
  178. len = anchors.length;
  179. if (len) {
  180. busy = true;
  181. for (indx = 0; indx < len; indx++) {
  182. anchor = anchors[indx];
  183. if (anchor.parentNode) {
  184. header = anchor.parentNode;
  185. // replace single & double quotes with right angled quotes
  186. txt = header.textContent.trim().replace(/'/g, "&#8217;").replace(/"/g, "&#8221;");
  187. content += [
  188. "<li class='github-toc-" + header.nodeName.toLowerCase() + "'>",
  189. // using a ZenHub class here to invert the icon for the dark theme
  190. "<span class='github-toc-icon octicon zh-octicon-grey'></span>",
  191. "<a href='" + anchor.hash + "' title='" + txt + "'>" + txt + "</a>",
  192. "</li>"
  193. ].join("");
  194. }
  195. }
  196. container.querySelector(".boxed-group-inner").innerHTML = content + "</ul>";
  197. tocView("block");
  198. listCollapsible();
  199. busy = false;
  200. } else {
  201. tocView();
  202. }
  203. } else {
  204. tocView();
  205. }
  206. },
  207.  
  208. addClass = function(els, name) {
  209. var indx,
  210. len = els.length;
  211. for (indx = 0; indx < len; indx++) {
  212. els[indx].classList.add(name);
  213. }
  214. },
  215.  
  216. removeClass = function(els, name) {
  217. var indx,
  218. len = els.length;
  219. for (indx = 0; indx < len; indx++) {
  220. els[indx].classList.remove(name);
  221. }
  222. },
  223.  
  224. listCollapsible = function() {
  225. var indx, el, next, count, num, group,
  226. els = container.querySelectorAll("li"),
  227. len = els.length;
  228. for (indx = 0; indx < len; indx++) {
  229. count = 0;
  230. group = [];
  231. el = els[indx];
  232. next = el && el.nextSibling;
  233. if (next) {
  234. num = el.className.match(/\d/)[0];
  235. while (next && !next.classList.contains("github-toc-h" + num)) {
  236. count += next.className.match(/\d/)[0] > num ? 1 : 0;
  237. group[group.length] = next;
  238. next = next.nextSibling;
  239. }
  240. if (count > 0) {
  241. el.className += " collapsible collapsible-" + indx;
  242. addClass(group, "github-toc-childof-" + indx);
  243. }
  244. }
  245. }
  246. group = [];
  247. container.addEventListener("click", function(event) {
  248. if (event.target.classList.contains("github-toc-icon")) {
  249. // click on icon, then target LI parent
  250. var item = event.target.parentNode,
  251. num = item.className.match(/collapsible-(\d+)/),
  252. els = num ? container.querySelectorAll(".github-toc-childof-" + num[1]) : null;
  253. if (els) {
  254. if (item.classList.contains("collapsed")) {
  255. item.classList.remove("collapsed");
  256. removeClass(els, "github-toc-hidden");
  257. } else {
  258. item.classList.add("collapsed");
  259. addClass(els, "github-toc-hidden");
  260. }
  261. }
  262. }
  263. });
  264. },
  265.  
  266. // keyboard shortcuts
  267. // not sure what GitHub uses, so rolling our own
  268. keyboardCheck = function(event) {
  269. clearTimeout(keyboard.timer);
  270. // use "g+t" to toggle the panel; "g+r" to reset the position
  271. // keypress may be needed for non-alphanumeric keys
  272. var tocToggle = keyboard.toggle.split("+"),
  273. tocReset = keyboard.restore.split("+"),
  274. key = String.fromCharCode(event.which).toLowerCase(),
  275. panelHidden = container.classList.contains("collapsed");
  276.  
  277. // press escape to close the panel
  278. if (event.which === 27 && !panelHidden) {
  279. tocHide();
  280. return;
  281. }
  282. // prevent opening panel while typing in comments
  283. if (/(input|textarea)/i.test(document.activeElement.nodeName)) {
  284. return;
  285. }
  286. // toggle TOC
  287. if (keyboard.lastKey === tocToggle[0] && key === tocToggle[1]) {
  288. if (panelHidden) {
  289. tocShow();
  290. } else {
  291. tocHide();
  292. }
  293. }
  294. // reset TOC window position
  295. if (keyboard.lastKey === tocReset[0] && key === tocReset[1]) {
  296. container.setAttribute("style", "");
  297. dragSave(true);
  298. }
  299. keyboard.lastKey = key;
  300. keyboard.timer = setTimeout(function() {
  301. keyboard.lastKey = null;
  302. }, keyboard.delay);
  303. },
  304.  
  305. init = function() {
  306. // there is no ".header" on github.com/contact; and some other pages
  307. if (!document.querySelector(".header") || tocInit) {
  308. return;
  309. }
  310. // insert TOC after header
  311. var tmp = GM_getValue("github-toc-location", null);
  312. // restore last position
  313. if (tmp) {
  314. container.style.left = tmp[0];
  315. container.style.top = tmp[1];
  316. container.style.right = "auto";
  317. }
  318.  
  319. // TOC saved state
  320. tmp = GM_getValue("github-toc-hidden", false);
  321. container.className = "github-toc boxed-group wiki-pages-box readability-sidebar" + (tmp ? " collapsed" : "");
  322. container.setAttribute("role", "navigation");
  323. container.setAttribute("unselectable", "on");
  324. container.innerHTML = [
  325. "<h3 class='js-wiki-toggle-collapse wiki-auxiliary-content' data-hotkey='g t'>",
  326. "<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> ",
  327. "<span>" + title + "</span>",
  328. "</h3>",
  329. "<div class='boxed-group-inner wiki-auxiliary-content wiki-auxiliary-content-no-bg'></div>"
  330. ].join("");
  331.  
  332. // add container
  333. tmp = document.querySelector(".header");
  334. tmp.parentNode.insertBefore(container, tmp);
  335.  
  336. // make draggable
  337. container.querySelector("h3").addEventListener("mousedown", dragInit);
  338. document.addEventListener("mousemove", dragMove);
  339. document.addEventListener("mouseup", dragStop);
  340. // toggle TOC
  341. container.querySelector(".github-toc-icon").addEventListener("mouseup", tocToggle);
  342. // prevent container content selection
  343. container.addEventListener("onselectstart", function() { return false; });
  344. // keyboard shortcuts
  345. // document.addEventListener("keypress", keyboardCheck);
  346. document.addEventListener("keydown", keyboardCheck);
  347. tocInit = true;
  348. },
  349.  
  350. // DOM targets - to detect GitHub dynamic ajax page loading
  351. targets = document.querySelectorAll([
  352. "#js-repo-pjax-container",
  353. // targeted by ZenHub
  354. "#js-repo-pjax-container > .container",
  355. "#js-pjax-container",
  356. ".js-preview-body"
  357. ].join(","));
  358.  
  359. // update TOC when content changes
  360. Array.prototype.forEach.call(targets, function(target) {
  361. new MutationObserver(function(mutations) {
  362. mutations.forEach(function(mutation) {
  363. // preform checks before adding code wrap to minimize function calls
  364. if (!busy && mutation.target === target) {
  365. tocAdd();
  366. }
  367. });
  368. }).observe(target, {
  369. childList: true,
  370. subtree: true
  371. });
  372. });
  373.  
  374. // Add GM options
  375. GM_registerMenuCommand("Set Table of Contents Title", function() {
  376. title = prompt("Table of Content Title:", title);
  377. GM_setValue("toc-title", title);
  378. container.querySelector("h3 span").textContent = title;
  379. });
  380.  
  381. tocAdd();
  382.  
  383. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址