Kanka Automatic Table of Contents

Automatically adds a table of contents to Kanka entity pages under the Pins sidebar.

目前為 2021-11-09 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Kanka Automatic Table of Contents
// @namespace    http://tampermonkey.net/
// @version      7
// @description  Automatically adds a table of contents to Kanka entity pages under the Pins sidebar.
// @author       Salvatos
// @match        https://kanka.io/*
// @exclude      */html-export
// @icon         https://www.google.com/s2/favicons?domain=kanka.io
// @grant        GM_addStyle
// ==/UserScript==

// Run only on entity Story pages
if (document.getElementById('app').parentNode.classList.contains("entity-story")) {
    GM_addStyle(`
	.entity-links {
		margin-bottom: 20px;
	}
	div#toc h3 {
	    text-align: center;
	    margin: 0 0 10px;
        padding-bottom: 10px;
	    font-family: Source Sans Pro,Helvetica Neue,Helvetica,Arial,sans-serif;
	    font-weight: bold;
	    font-size: 17px;
	    color: var(--header-text);
        border-bottom: 1px solid var(--box-with-border-bottom-border);
	}
	#tableofcontents, #tableofcontents ul {
	    list-style: none;
	    padding: 0;
	    text-indent: -5px;
	}
	#tableofcontents {
	    padding: 0 10px;
	    overflow: hidden;
	    word-wrap: anywhere;
	}
	#tableofcontents ul {
	    padding-left: 10px;
	}
	#tableofcontents a {
	    font-size: 14px;
	}
	#tableofcontents li.toc-level-0 a {
	    font-weight: bold;
	}
    #tableofcontents .text-muted {
        display: none;
    }
	.to-top {
	    vertical-align: super;
	    font-variant: all-petite-caps;
	    font-size: 10px;
	}
	`);

	/* Set arrays and prefs */
	var headings = [];
	var tag_names = { h1:1, h2:1, h3:1, h4:1, h5:1, h6:1 };
    const addTopLink = true;

    /* Pre-cleaning: remove stray line breaks left by Summernote at the end of headings so our TOC link doesn't get pushed to a new line */
    $('h1 br:last-child, h2 br:last-child, h3 br:last-child, h4 br:last-child, h5 br:last-child, h6 br:last-child').remove();

	/* Walks through DOM looking for selected elements */
	function walk( root ) {
	    if( root.nodeType === 1 && root.nodeName !== 'script' && !root.classList.contains("calendar") && !root.classList.contains("modal") ) { // Added a check to exclude modals and calendar dates
	        if( tag_names.hasOwnProperty(root.nodeName.toLowerCase()) ) {
	            headings.push( root );
	        } else {
	            for( var i = 0; i < root.childNodes.length; i++ ) {
	                walk( root.childNodes[i] );
	            }
	        }
	    }
	}
	walk( document.getElementsByClassName('entity-story-block')[0] );

	/* Start main list */
	var level = 0;
	var past_level = 0;
	var hList = `
		<div id='toc' class='box box-solid' style='padding:10px;'>
		<h3>Table of contents</h3>
		<ul id='tableofcontents'>
	`;

	/* Create sublists to reflect heading level */
	for( var i = 0; i < headings.length; i++ ) {
	    // "Entry", Mentions and post titles act as level-0 headers
	    level = (headings[i].classList.contains("box-title")) ? 0 : headings[i].nodeName.substr(1);

	    if (level > past_level) { // Go down a level
	        for(var j = 0; j < level - past_level; j++) {
	            hList += "<li><ul>";
	        }
	    }
	    else if (level < past_level) { // Go up a level
	        for(var j = 0; j < past_level - level; j++) {
	            hList += "</ul></li>";
	        }
	    }

        /* Handle heading text (it gets complicated with Timeline elements and inline tags) */
        var headingText = headings[i],
            child = headingText.firstChild,
            texts = [];
        // Iterate through heading nodes
        while (child) {
            // Not a tag (text node)
            if (!child.tagName) {
                texts.push(child.data);
            }
            // Identify and manage HTML tags
            else {
                // Text-muted tag, i.e. a Timeline date
                if ($(child).hasClass("text-muted")) {
                    texts.push('<span class="text-muted">' + child.innerText + '</span>');
                }
                // Screenreader prompt
                else if ($(child).hasClass("sr-only")) {
                    // exclude
                }
                else {
                    texts.push(child.innerText);
                }
            }
            child = child.nextSibling;
        }

        headingText = texts.join("");

	    /* Create ID anchor on heading */
	    headings[i].id = "h" + level + "-" + headingText.replace(/\s/g, "");

        /* Create link in TOC */
	    hList += "<li class='toc-level-" + level + "'><a href='#" + headings[i].id + "' parent-post='" + $(headings[i]).parent().parent().attr('id') + "'>" + headingText + "</a></li>";

        /* Add "toc" link to non-box headings */
        if (addTopLink && level > 0) {
            headings[i].insertAdjacentHTML("beforeend", "<a class='to-top' href='#toc' title='Back to table of contents'>&nbsp;^&nbsp;toc</a>");
        }

        /* Update past_level */
	    past_level = level;
	}

	/* Close sublists per current level */
	for(var k = 0; k < past_level; k++) {
	    hList += "</li></ul>";
	}
    /* Close TOC */
	hList += "</div>";

	/* Insert element after Pins (and entity links) */
	/* Calendars use only one sidebar */
	if (document.getElementById('app').parentNode.classList.contains("kanka-entity-calendar")) {
	    document.getElementsByClassName('entity-sidebar-submenu')[0].insertAdjacentHTML("beforeend", hList);
	}
	/* Everything else */
	else {
	    document.getElementsByClassName('entity-sidebar-pins')[0].insertAdjacentHTML("beforeend", hList);
	}

    /* Listener: If the target heading is in a collapsed post, expand it first */
    $("#tableofcontents :not(.toc-level-0) a").click(function() {
        var targetPost = $(this).attr('parent-post');
        if (!$("#" + targetPost)[0].classList.contains("in")) {
            $("h3[data-target='#" + targetPost + "']").click();
        }
    });
    $("#tableofcontents .toc-level-0 a").click(function() {
        var targetPost = $(this).attr('parent-post');
        if (targetPost != "undefined" && !$("#" + targetPost + " > .box-header > h3")[0].classList.contains("in")) {
            $("#" + targetPost + " > .box-header > h3").click();
        }
    });
}