Skip to content

Instantly share code, notes, and snippets.

@pax
Created February 28, 2024 13:00
Show Gist options
  • Save pax/38cafb525544991b0d365096a70edd1c to your computer and use it in GitHub Desktop.
Save pax/38cafb525544991b0d365096a70edd1c to your computer and use it in GitHub Desktop.
Creates a Table of Contents with relative links to H* elements found inside target element or the whole document
.hx-0{margin-top:1.25ex;border-bottom:1pxdotted#999;font-size:1.2em;}
.hx-1{padding-left:.25ex;margin-left:.25ex;margin-top:1ex;font-size:1.1em;border-bottom:1pxdotted#DDD;}
.hx-2{padding-left:.75ex;margin-left:.5ex;margin-top:.5ex;border-left:1pxsolid#EEE;}
.hx-3{padding-left:1ex;margin-left:1ex;margin-top:.25ex;border-left:1pxsolid#EEE;}
.hx-0+.hx-0{margin-top:.25ex;}
.hx-2+.hx-2{margin-top:.5ex;}
.hx-1+.hx-1{margin-top:1ex;}
.hx-1:before {content: '\2022\00A0 '; color: #DCC;}
.hx-2:before {content: '\00A0\203A\00A0\00A0 '; color: #DCC;}
.hx-3:before {content: '– '; color: #DCC;}
.hx-4:before {content: '--- '; color: #DCC;}
@media screen and (min-width: 900px) {
.toc-element-auto-generated {position: sticky; top: 1em; background-color: rgba(255,255,255,.75);}
}
.toc-title, .toc-element-auto-generated .toc-title{border-bottom: 1px dashed #DDD; padding-bottom: 1ex;}
.no-headers-found {display: none;}
.toc-element-auto-generated {position: sticky; right: 1em; top: 1em; background-color: rgba(255,255,255,.75); padding: .5em 1em; display: inline-block;}
/*
- creates a Table of Contents with relative links to H* elements found inside `searchBlockSelector`
- `tocSelector` and `searchBlockSelector` accept CSS paths, but are optional
- if paths not found or not given it looks for any of the ['#primary', '#main', 'main', '.site-main', 'article:first-of-type', '.entry-content:first-of-type']
you need to add hx-* classes to your css - replace * and ... with actual values
.hx-*{margin-top: ...;margin-left: ...;font-size: ...;}.hx-1+.hx-1{margin-top: ...;}.hx-*+.hx-*{margin-top: ...;}
#toc.no-headers-found {display: none;} .toc-element-auto-generated {position: sticky; left: 1em; top: 1em; background-color: rgba(255,255,255,.75); padding: .5em 1em;}
# TODO
- [x] create TOC placeholder if it does not exist and add error message - or have some fall-back options
- [ ] add a hide control - maybe a checkbox so it only needs CSS?
- [ ] style active element, also on scroll
- [ ] smooth scrolling
- [ ] bookmarklet
*/
function htmlTableOfContents(tocSelector, searchBlockSelector, hlevel = 'h1, h2, h3, h4') {
var documentRef = document;
var toc = documentRef.querySelector(tocSelector);
var searchBlock = documentRef.querySelector(searchBlockSelector);
var fallbackSelectors = ['#primary', '#main', 'main', '.site-main', 'article:first-of-type', '.entry-content:first-of-type'];
// Function to find first available fallback element
function findFallbackElement(selectors) {
for (var i = 0; i < selectors.length; i++) {
var foundElement = documentRef.querySelector(selectors[i]);
if (foundElement) return foundElement;
}
return null;
}
// Create TOC container if not found or not specified, and add "Table of Contents" header
if (!toc || !tocSelector) {
var fallbackElement = findFallbackElement(fallbackSelectors);
if (fallbackElement) {
toc = documentRef.createElement('div');
toc.setAttribute('class', 'toc-container');
toc.classList.add('toc-element-auto-generated');
var tocHeader = documentRef.createElement('h2');
tocHeader.textContent = 'Table of Contents';
toc.appendChild(tocHeader);
fallbackElement.insertBefore(toc, fallbackElement.firstChild);
} else {
console.error('No suitable fallback element for TOC found.');
return;
}
}
// Use the document as search block if not found or not specified
if (!searchBlock) {
searchBlock = findFallbackElement(fallbackSelectors) || documentRef.body;
}
var headings = [].slice.call(searchBlock.querySelectorAll(hlevel));
// Determine the smallest header level found
var smallestHeaderLevel = Math.min(...headings.map(heading => parseInt(heading.tagName.substring(1), 10)));
if (headings.length === 0) {
toc.classList.add('no-headers-found');
} else {
headings.forEach(function(heading, index) {
if (heading.textContent.trim() && !heading.classList.contains('hideToc')) {
var anchor = documentRef.createElement('a');
anchor.setAttribute('name', 'toc' + index);
anchor.setAttribute('id', 'toc' + index);
var link = documentRef.createElement('a');
link.setAttribute('href', '#toc' + index);
// Remove trailing ' #' from heading text if present
var headingText = heading.textContent.trim().endsWith(' #') ? heading.textContent.trim().slice(0, -2) : heading.textContent.trim();
link.textContent = headingText;
var headerLevel = parseInt(heading.tagName.substring(1), 10) - smallestHeaderLevel;
var div = documentRef.createElement('div');
div.setAttribute('class', 'hx-' + headerLevel); // Adjust based on smallest header level
div.appendChild(link);
toc.appendChild(div);
heading.parentNode.insertBefore(anchor, heading);
}
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment