NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name GitLab Collapse Markdown // @version 0.1.0 // @description A userscript that collapses markdown headers // @license MIT // @author Rob Garrison // @namespace https://gitlab.com/Mottie // @include https://gitlab.com/* // @include https://about.gitlab.com/* // @run-at document-idle // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @icon https://gitlab.com/assets/gitlab_logo-7ae504fe4f68fdebb3c2034e36621930cd36ea87924c11ff65dbcb8ed50dca58.png // @updateURL https://gitlab.com/Mottie/GitLab-userscripts/raw/master/gitlab-collapse-markdown.user.js // @downloadURL https://gitlab.com/Mottie/GitLab-userscripts/raw/master/gitlab-collapse-markdown.user.js // ==/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 = [ ".wiki", // gitlab.com/:user/:repo/wikis ".note-text.md", // issue comments ".md-preview", // issue preview ".documentation-index", // gitlab.com/help/ ".md-page", // about.gitlab.com "" // leave empty string at the end ], headers = "H1 H2 H3 H4 H5 H6".split(" "), // toggled class name collapsed = "glcm-collapsed", arrowColors = document.createElement("style"); let startCollapsed = GM_getValue("glcm-collapsed", false), colors = GM_getValue("glcm-colors", defaultColors); 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; width:calc(100% - 5px); } ${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); } .glcm-hidden { display:none !important; } `); function addColors() { arrowColors.textContent = ` ${blocks.join(" h1:after,").slice(0, -1)} { color:${colors[0]} } ${blocks.join(" h2:after,").slice(0, -1)} { color:${colors[1]} } ${blocks.join(" h3:after,").slice(0, -1)} { color:${colors[2]} } ${blocks.join(" h4:after,").slice(0, -1)} { color:${colors[3]} } ${blocks.join(" h5:after,").slice(0, -1)} { color:${colors[4]} } ${blocks.join(" h6:after,").slice(0, -1)} { color:${colors[5]} } `; } function toggle(el, shifted) { if (el) { el.classList.toggle(collapsed); let els; const name = el.nodeName || "", 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, "glcm-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("glcm-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; }); els.forEach(el => { 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 = closest("svg", target); } // check if element is inside a header target = closest(headers.join(","), event.target); if (target && headers.indexOf(target.nodeName || "") > -1) { // make sure the header is inside of markdown if (closest(`${blocks.join(",").slice(0, -1)}`, target)) { toggle(target, event.shiftKey); } } }); window.addEventListener("hashchange", () => { if (startCollapsed) { checkHash(); } }); } function checkHash() { let el, els, md; // don't collapse H1 blocks const mds = $$(`${blocks.slice(1).join(",").slice(0, -1)}`), tmp = (window.location.hash || "").replace(/#/, ""); for (md of mds) { els = $$(headers.slice(1).join(","), md); if (els.length > 1) { for (el of els) { if (el && !el.classList.contains(collapsed)) { toggle(el, true); } } } } // open up if (tmp) { els = $(`#${tmp}`); if (els && els.classList.contains("anchor")) { 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 init() { document.querySelector("head").appendChild(arrowColors); checkColors(); addColors(); addBinding(); if (startCollapsed) { checkHash(); } } function $(selector, el) { return (el || document).querySelector(selector); } function $$(selectors, el) { return Array.from((el || document).querySelectorAll(selectors)); } function closest(selector, el) { while (el && el.nodeType === 1) { if (el.matches(selector)) { return el; } el = el.parentNode; } return null; } // Add GM options GM_registerMenuCommand("Set GitLab collapse markdown state", () => { const val = prompt( "Set initial state to (c)ollapsed or (e)xpanded (first letter only):", startCollapsed ? "collapsed" : "expanded" ); if (val !== null) { startCollapsed = /^c/i.test(val); GM_setValue("glcm-collapsed", startCollapsed); console.log( `GitLab Collapse Markdown: Headers will` + `${startCollapsed ? "be" : "not be"} initially collapsed` ); } }); GM_registerMenuCommand("Set GitLab 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("glcm-colors", colors); console.log("GitLab Collapse Markdown: colors set to", colors); addColors(); return; } console.error( "GitLab Collapse Markdown: invalid color definition (6 colors)", val ); // reset colors to default (in case colors variable is corrupted) checkColors(); } catch (err) { console.error("GitLab Collapse Markdown: invalid JSON"); } } }); init(); })();