// ==UserScript== // @name GitHub Collapse Markdown // @version 1.2.3 // @description A userscript that collapses markdown headers // @license MIT // @author Rob Garrison // @namespace https://github.com/Mottie // @match https://github.com/* // @match https://gist.github.com/* // @match https://help.github.com/* // @run-at document-idle // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163 // @icon https://github.githubassets.com/pinned-octocat.svg // @updateURL https://raw.githubusercontent.com/Mottie/GitHub-userscripts/master/github-collapse-markdown.user.js // @downloadURL https://raw.githubusercontent.com/Mottie/GitHub-userscripts/master/github-collapse-markdown.user.js // @supportURL https://github.com/Mottie/GitHub-userscripts/issues // ==/UserScript== (() => { "use strict"; const defaultColors = [ // palette generated by http://tools.medialab.sciences-po.fr/iwanthue/ // (colorblind friendly, soft) "#6778d0", "#ac9c3d", "#b94a73", "#56ae6c", "#9750a1", "#ba543d" ], blocks = [ ".markdown-body", ".markdown-format", "" // leave empty string at the end ], headers = "H1 H2 H3 H4 H5 H6".split(" "), collapsed = "ghcm-collapsed", arrowColors = document.createElement("style"); let startCollapsed = GM_getValue("ghcm-collapsed", false), colors = GM_getValue("ghcm-colors", defaultColors); // .markdown-body h1:after, .markdown-format h1:after, ... {} GM_addStyle(` ${blocks.join(" h1,")} ${blocks.join(" h2,")} ${blocks.join(" h3,")} ${blocks.join(" h4,")} ${blocks.join(" h5,")} ${blocks.join(" h6,").slice(0, -1)} { position:relative; padding-right:.8em; cursor:pointer; } ${blocks.join(" h1:after,")} ${blocks.join(" h2:after,")} ${blocks.join(" h3:after,")} ${blocks.join(" h4:after,")} ${blocks.join(" h5:after,")} ${blocks.join(" h6:after,").slice(0, -1)} { display:inline-block; position:absolute; right:0; top:calc(50% - .5em); font-size:.8em; content:"\u25bc"; } ${blocks.join(" ." + collapsed + ":after,").slice(0, -1)} { transform: rotate(90deg); } /* clicking on header link won't pass svg as the event.target */ .octicon-link, .octicon-link > * { pointer-events:none; } .ghcm-hidden, .ghcm-no-content:after { display:none !important; } `); function addColors() { let sel, styles = ""; headers.forEach((header, indx) => { sel = `${blocks.join(" " + header + ":after,").slice(0, -1)}`; styles += `${sel} { color:${colors[indx]} }`; }); arrowColors.textContent = styles; } function toggle(el, shifted) { if (el && !el.classList.contains("ghcm-no-content")) { el.classList.toggle(collapsed); let els; const name = el.nodeName || "", // convert H# to # level = parseInt(name.replace(/[^\d]/, ""), 10), isCollapsed = el.classList.contains(collapsed); if (shifted) { // collapse all same level anchors els = $$(`${blocks.join(" " + name + ",").slice(0, -1)}`); for (el of els) { nextHeader(el, level, isCollapsed); } } else { nextHeader(el, level, isCollapsed); } removeSelection(); } } function nextHeader(el, level, isCollapsed) { el.classList.toggle(collapsed, isCollapsed); const selector = headers.slice(0, level).join(","), name = [collapsed, "ghcm-hidden"], els = []; el = el.nextElementSibling; while (el && !el.matches(selector)) { els[els.length] = el; el = el.nextElementSibling; } if (els.length) { if (isCollapsed) { els.forEach(el => { el.classList.add("ghcm-hidden"); }); } else { els.forEach(el => { el.classList.remove(...name); }); } } } // show siblings of hash target function siblings(target) { let el = target.nextElementSibling, els = [target]; const level = parseInt((target.nodeName || "").replace(/[^\d]/, ""), 10), selector = headers.slice(0, level - 1).join(","); while (el && !el.matches(selector)) { els[els.length] = el; el = el.nextElementSibling; } el = target.previousElementSibling; while (el && !el.matches(selector)) { els[els.length] = el; el = el.previousElementSibling; } if (els.length) { els = els.filter(el => { return el.nodeName === target.nodeName; }); for (el of els) { el.classList.remove("glcm-hidden"); } } nextHeader(target, level, false); } function removeSelection() { // remove text selection - https://stackoverflow.com/a/3171348/145346 const sel = window.getSelection ? window.getSelection() : document.selection; if (sel) { if (sel.removeAllRanges) { sel.removeAllRanges(); } else if (sel.empty) { sel.empty(); } } } function addBinding() { document.addEventListener("click", event => { let target = event.target; const name = (target && (target.nodeName || "")).toLowerCase(); if (name === "path") { target = target.closest("svg"); } if (!target || target.classList.contains("anchor") || name === "a" || name === "img" || // add support for "pointer-events:none" applied to "anchor" in // https://github.com/StylishThemes/GitHub-FixedHeader target.classList.contains("octicon-link")) { return; } // check if element is inside a header target = event.target.closest(headers.join(",")); if (target && headers.indexOf(target.nodeName || "") > -1) { // make sure the header is inside of markdown if (target.closest(blocks.slice(0, -1).join(","))) { toggle(target, event.shiftKey); } } }); document.addEventListener("ghmo:container", () => { // init after a short delay to allow rendering of file list setTimeout(() => { ignoreEmptyHeaders(); }, 200); }); } function checkHash() { let el, els, md; const mds = $$(blocks.slice(0, -1).join(",")), id = (window.location.hash || "").replace(/#/, ""); for (md of mds) { els = $$(headers.join(","), md); if (els.length > 1) { for (el of els) { if (el && !el.classList.contains(collapsed)) { toggle(el, true); } } } } if (id) { openHash(id); } } // open header matching hash function openHash(id) { const els = $(`#user-content-${id}`); if (els && els.classList.contains("anchor")) { let el = els.parentNode; if (el.matches(headers.join(","))) { siblings(el); document.documentElement.scrollTop = el.offsetTop; // set scrollTop a second time, in case of browser lag setTimeout(() => { document.documentElement.scrollTop = el.offsetTop; }, 500); } } } function checkColors() { if (!colors || colors.length !== 6) { colors = [].concat(defaultColors); } } function ignoreEmptyHeaders() { $$("a.anchor").forEach(el => { const parent = el.parentNode; if (parent && parent.matches(headers.join(",")) && !parent.nextElementSibling) { parent.classList.add("ghcm-no-content"); } }); } function init() { document.querySelector("head").appendChild(arrowColors); checkColors(); addColors(); addBinding(); ignoreEmptyHeaders(); if (startCollapsed) { checkHash(); } } function $(selector, el) { return (el || document).querySelector(selector); } function $$(selectors, el) { return [...(el || document).querySelectorAll(selectors)]; } // Add GM options GM_registerMenuCommand("Set collapse markdown state", () => { const val = prompt( "Set initial state to (c)ollapsed or (e)xpanded (first letter necessary):", startCollapsed ? "collapsed" : "expanded" ); if (val !== null) { startCollapsed = /^c/i.test(val); GM_setValue("ghcm-collapsed", startCollapsed); console.log( `GitHub Collapse Markdown: Headers will ${startCollapsed ? "be" : "not be"} initially collapsed` ); } }); GM_registerMenuCommand("Set collapse markdown colors", () => { let val = prompt("Set header arrow colors:", JSON.stringify(colors)); if (val !== null) { // allow pasting in a JSON format try { val = JSON.parse(val); if (val && val.length === 6) { colors = val; GM_setValue("ghcm-colors", colors); console.log("GitHub Collapse Markdown: colors set to", colors); addColors(); return; } console.error( "GitHub Collapse Markdown: invalid color definition (6 colors)", val ); // reset colors to default (in case colors variable is corrupted) checkColors(); } catch (err) { console.error("GitHub Collapse Markdown: invalid JSON"); } } }); init(); })();