GitHub TOC

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

Verze ze dne 25. 05. 2016. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name          GitHub TOC
// @version       1.0.1
// @description   A userscript that adds a table of contents to readme & wiki pages
// @license       https://creativecommons.org/licenses/by-sa/4.0/
// @namespace     http://github.com/Mottie
// @include       https://github.com/*
// @run-at        document-idle
// @grant         GM_registerMenuCommand
// @grant         GM_getValue
// @grant         GM_setValue
// @grant         GM_addStyle
// @author        Rob Garrison
// ==/UserScript==
/* global GM_registerMenuCommand, GM_getValue, GM_setValue, GM_addStyle */
/*jshint unused:true */
(function() {
  "use strict";

  GM_addStyle([
    ".github-toc { position:fixed; z-index:75; min-width:200px; top:55px; right:10px; }",
    ".github-toc h3 { cursor:move; }",
    // icon toggles TOC container & subgroups
    ".github-toc h3 svg, .github-toc li.collapsible .github-toc-icon { cursor:pointer; }",
    // move collapsed TOC to top right corner
    ".github-toc.collapsed {",
      "width:30px; height:30px; min-width:auto; overflow:hidden; top:10px !important; left:auto !important;",
      "right:10px !important; border:1px solid #d8d8d8; border-radius:3px;",
    "}",
    ".github-toc.collapsed > h3 { cursor:pointer; padding-top:5px; border:none; }",
    // move header text out-of-view when collapsed
    ".github-toc.collapsed > h3 svg { margin-bottom: 10px; }",
    ".github-toc-hidden, .github-toc.collapsed .boxed-group-inner,",
     ".github-toc li:not(.collapsible) .github-toc-icon { display:none; }",
    ".github-toc .boxed-group-inner { max-width:250px; max-height:400px; overflow-y:auto; overflow-x:hidden; }",
    ".github-toc ul { list-style:none; }",
    ".github-toc li { max-width:230px; white-space:nowrap; overflow-x:hidden; text-overflow:ellipsis; }",
    ".github-toc .github-toc-h1 { padding-left:15px; }",
    ".github-toc .github-toc-h2 { padding-left:30px; }",
    ".github-toc .github-toc-h3 { padding-left:45px; }",
    ".github-toc .github-toc-h4 { padding-left:60px; }",
    ".github-toc .github-toc-h5 { padding-left:75px; }",
    ".github-toc .github-toc-h6 { padding-left:90px; }",
    // anchor collapsible icon
    ".github-toc li.collapsible .github-toc-icon {",
      "width:16px; height:16px; display:inline-block; margin-left:-16px;",
      "background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSdvY3RpY29uJyBoZWlnaHQ9JzE0JyB2aWV3Qm94PScwIDAgMTIgMTYnPjxwYXRoIGQ9J00wIDVsNiA2IDYtNkgweic+PC9wYXRoPjwvc3ZnPg==) left center no-repeat;",
    "}",
    // on rotate, height becomes width, so this is keeping things lined up
    ".github-toc li.collapsible.collapsed .github-toc-icon { -webkit-transform:rotate(-90deg); transform:rotate(-90deg); height:10px; width:12px; margin-right:2px; }",
    ".github-toc-no-selection { -webkit-user-select:none !important; -moz-user-select:none !important; user-select:none !important; }"
  ].join(""));

  // modifiable title
  var title = GM_getValue("github-toc-title", "Table of Contents"),

  container = document.createElement("div"),
  busy = false,
  tocInit = false,

  // keyboard shortcuts
  keyboard = {
    toggle  : "g+t",
    restore : "g+r",
    timer   : null,
    lastKey : null,
    delay   : 1000 // ms between keyboard shortcuts
  },

  // drag variables
  drag = {
    el   : null,
    pos  : [ 0, 0 ],
    elm  : [ 0, 0 ],
    time : 0,
    unsel: null
  },
  // drag code adapted from http://jsfiddle.net/tovic/Xcb8d/light/
  dragInit = function() {
    if (!container.classList.contains("collapsed")) {
      drag.el = container;
      drag.elm[0] = drag.pos[0] - drag.el.offsetLeft;
      drag.elm[1] = drag.pos[1] - drag.el.offsetTop;
      selectionToggle(true);
    } else {
      drag.el = null;
    }
    drag.time = new Date().getTime() + 500;
  },
  dragMove = function(event) {
    drag.pos[0] = document.all ? window.event.clientX : event.pageX;
    drag.pos[1] = document.all ? window.event.clientY : event.pageY;
    if (drag.el !== null) {
      drag.el.style.left = (drag.pos[0] - drag.elm[0]) + "px";
      drag.el.style.top = (drag.pos[1] - drag.elm[1]) + "px";
      drag.el.style.right = "auto";
    }
  },
  dragStop = function() {
    if (drag.el !== null) {
      dragSave();
      selectionToggle();
    }
    drag.el = null;
  },
  dragSave = function(clear) {
    var val = clear ? null : [container.style.left, container.style.top];
    GM_setValue("github-toc-location", val);
  },

  // stop text selection while dragging
  selectionToggle = function(disable) {
    var sel,
      body = document.querySelector("body");
    if (disable) {
      // save current "unselectable" value
      drag.unsel = body.getAttribute("unselectable");
      body.setAttribute("unselectable", "on");
      body.classList.add("github-toc-no-selection");
      body.addEventListener("onselectstart", selectionStop);
    } else {
      if (drag.unsel) {
        body.setAttribute("unselectable", drag.unsel);
      }
      body.classList.remove("github-toc-no-selection");
      body.removeEventListener("onselectstart", selectionStop);
    }
    // remove text selection - http://stackoverflow.com/a/3171348/145346
    sel = window.getSelection ? window.getSelection() : document.selection;
    if ( sel ) {
      if ( sel.removeAllRanges ) {
        sel.removeAllRanges();
      } else if ( sel.empty ) {
        sel.empty();
      }
    }
  },
  selectionStop = function() {
    return false;
  },

  tocShow = function() {
    container.classList.remove("collapsed");
    GM_setValue("github-toc-hidden", false);
  },
  tocHide = function() {
    container.classList.add("collapsed");
    GM_setValue("github-toc-hidden", true);
  },
  tocToggle = function() {
    // don't toggle content on long clicks
    if (drag.time > new Date().getTime()) {
      if (container.classList.contains("collapsed")) {
        tocShow();
      } else {
        tocHide();
      }
    }
  },
  // hide TOC entirely, if no rendered markdown detected
  tocView = function(mode) {
    var toc = document.querySelector(".github-toc");
    if (toc) {
      toc.style.display = mode || "none";
    }
  },

  tocAdd = function() {
    // make sure the script is initialized
    init();
    if (!tocInit) {
      return;
    }
    if (document.querySelectorAll("#wiki-content, #readme")) {
      var indx, header, anchor, txt,
        content = "<ul>",
        anchors = document.querySelectorAll(".markdown-body .anchor"),
        len = anchors.length;
      if (len) {
        busy = true;
        for (indx = 0; indx < len; indx++) {
          anchor = anchors[indx];
          if (anchor.parentNode) {
            header = anchor.parentNode;
            // replace single & double quotes with right angled quotes
            txt = header.textContent.trim().replace(/'/g, "&#8217;").replace(/"/g, "&#8221;");
            content += [
              "<li class='github-toc-" + header.nodeName.toLowerCase() + "'>",
                // using a ZenHub class here to invert the icon for the dark theme
                "<span class='github-toc-icon octicon zh-octicon-grey'></span>",
                "<a href='" + anchor.hash + "' title='" + txt + "'>" + txt + "</a>",
              "</li>"
            ].join("");
          }
        }
        container.querySelector(".boxed-group-inner").innerHTML = content + "</ul>";
        tocView("block");
        listCollapsible();
        busy = false;
      } else {
        tocView();
      }
    } else {
      tocView();
    }
  },

  addClass = function(els, name) {
    var indx,
      len = els.length;
    for (indx = 0; indx < len; indx++) {
      els[indx].classList.add(name);
    }
  },

  removeClass = function(els, name) {
    var indx,
      len = els.length;
    for (indx = 0; indx < len; indx++) {
      els[indx].classList.remove(name);
    }
  },

  listCollapsible = function() {
    var indx, el, next, count, num, group,
      els = container.querySelectorAll("li"),
      len = els.length;
    for (indx = 0; indx < len; indx++) {
      count = 0;
      group = [];
      el = els[indx];
      next = el && el.nextSibling;
      if (next) {
        num = el.className.match(/\d/)[0];
        while (next && !next.classList.contains("github-toc-h" + num)) {
          count += next.className.match(/\d/)[0] > num ? 1 : 0;
          group[group.length] = next;
          next = next.nextSibling;
        }
        if (count > 0) {
          el.className += " collapsible collapsible-" + indx;
          addClass(group, "github-toc-childof-" + indx);
        }
      }
    }
    group = [];
    container.addEventListener("click", function(event) {
      if (event.target.classList.contains("github-toc-icon")) {
        // click on icon, then target LI parent
        var item = event.target.parentNode,
          num = item.className.match(/collapsible-(\d+)/),
          els = num ? container.querySelectorAll(".github-toc-childof-" + num[1]) : null;
        if (els) {
          if (item.classList.contains("collapsed")) {
            item.classList.remove("collapsed");
            removeClass(els, "github-toc-hidden");
          } else {
            item.classList.add("collapsed");
            addClass(els, "github-toc-hidden");
          }
        }
      }
    });
  },

  // keyboard shortcuts
  // not sure what GitHub uses, so rolling our own
  keyboardCheck = function(event) {
    clearTimeout(keyboard.timer);
    // use "g+t" to toggle the panel; "g+r" to reset the position
    // keypress may be needed for non-alphanumeric keys
    var tocToggle = keyboard.toggle.split("+"),
      tocReset = keyboard.restore.split("+"),
      key = String.fromCharCode(event.which).toLowerCase(),
      panelHidden = container.classList.contains("collapsed");

    // press escape to close the panel
    if (event.which === 27 && !panelHidden) {
      tocHide();
      return;
    }
    // prevent opening panel while typing in comments
    if (/(input|textarea)/i.test(document.activeElement.nodeName)) {
      return;
    }
    // toggle TOC
    if (keyboard.lastKey === tocToggle[0] && key === tocToggle[1]) {
      if (panelHidden) {
        tocShow();
      } else {
        tocHide();
      }
    }
    // reset TOC window position
    if (keyboard.lastKey === tocReset[0] && key === tocReset[1]) {
      container.setAttribute("style", "");
      dragSave(true);
    }
    keyboard.lastKey = key;
    keyboard.timer = setTimeout(function() {
      keyboard.lastKey = null;
    }, keyboard.delay);
  },

  init = function() {
    // there is no ".header" on github.com/contact; and some other pages
    if (!document.querySelector(".header") || tocInit) {
      return;
    }
    // insert TOC after header
    var tmp = GM_getValue("github-toc-location", null);
    // restore last position
    if (tmp) {
      container.style.left = tmp[0];
      container.style.top = tmp[1];
      container.style.right = "auto";
    }

    // TOC saved state
    tmp = GM_getValue("github-toc-hidden", false);
    container.className = "github-toc boxed-group wiki-pages-box readability-sidebar" + (tmp ? " collapsed" : "");
    container.setAttribute("role", "navigation");
    container.setAttribute("unselectable", "on");
    container.innerHTML = [
      "<h3 class='js-wiki-toggle-collapse wiki-auxiliary-content' data-hotkey='g t'>",
        "<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> ",
        "<span>" + title + "</span>",
      "</h3>",
      "<div class='boxed-group-inner wiki-auxiliary-content wiki-auxiliary-content-no-bg'></div>"
    ].join("");

    // add container
    tmp = document.querySelector(".header");
    tmp.parentNode.insertBefore(container, tmp);

    // make draggable
    container.querySelector("h3").addEventListener("mousedown", dragInit);
    document.addEventListener("mousemove", dragMove);
    document.addEventListener("mouseup", dragStop);
    // toggle TOC
    container.querySelector(".github-toc-icon").addEventListener("mouseup", tocToggle);
    // prevent container content selection
    container.addEventListener("onselectstart", function() { return false; });
    // keyboard shortcuts
    // document.addEventListener("keypress", keyboardCheck);
    document.addEventListener("keydown", keyboardCheck);
    tocInit = true;
  },

  // DOM targets - to detect GitHub dynamic ajax page loading
  targets = document.querySelectorAll([
    "#js-repo-pjax-container",
    // targeted by ZenHub
    "#js-repo-pjax-container > .container",
    "#js-pjax-container",
    ".js-preview-body"
  ].join(","));

  // update TOC when content changes
  Array.prototype.forEach.call(targets, function(target) {
    new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        // preform checks before adding code wrap to minimize function calls
        if (!busy && mutation.target === target) {
          tocAdd();
        }
      });
    }).observe(target, {
      childList: true,
      subtree: true
    });
  });

  // Add GM options
  GM_registerMenuCommand("Set Table of Contents Title", function() {
    title = prompt("Table of Content Title:", title);
    GM_setValue("toc-title", title);
    container.querySelector("h3 span").textContent = title;
  });

  tocAdd();

})();