Laravel Docs - sticky table of contents

Adds fixed Table of Contents containing each heading to each docs page for easy navigation / jumping between elements

  1. // ==UserScript==
  2. // @name Laravel Docs - sticky table of contents
  3. // @namespace https://laravel.com/
  4. // @version 2025-02-17
  5. // @description Adds fixed Table of Contents containing each heading to each docs page for easy navigation / jumping between elements
  6. // @author Mave
  7. // @match https://laravel.com/docs/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=laravel.com
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. function htmlToElement(html) {
  16. const template = document.createElement('template');
  17. html = html.trim();
  18. template.innerHTML = html;
  19.  
  20. return template.content.firstChild;
  21. }
  22.  
  23. function getStylesFor(elem) {
  24. switch (elem.tagName) {
  25. case 'H2':
  26. return 'line-height: 32px;';
  27. case 'H3':
  28. return 'line-height: 24px; margin-left: 16px; font-size: 0.9rem;';
  29. case 'H4':
  30. return 'line-height: 24px; margin-left: 32px; font-size: 0.9rem; font-weight: 500;';
  31. }
  32.  
  33. return '';
  34. }
  35.  
  36. setTimeout(() => {
  37. const sectionMain = document.querySelector('.docs_main div#main-content');
  38. if (!sectionMain) {
  39. return;
  40. }
  41.  
  42. const headings = sectionMain.querySelectorAll('h2, h3, h4, h5');
  43. if (!headings.length) {
  44. return;
  45. }
  46.  
  47. const container = htmlToElement(`<div style="position: fixed; top: 48px; right: 16px; padding: 16px; border: 2px solid grey; border-radius: 2px; max-height: calc(90vh - 48px); overflow: auto;"><div class="contents"></div></div>`);
  48. document.body.append(container);
  49.  
  50. const containerContents = container.querySelector('div.contents');
  51.  
  52. const collapseButton = htmlToElement('<span style="position: absolute; top: 8px; right: 8px; padding: 2px 8px; border: 2px solid #bbb; color: #bbb; cursor: pointer; border-radius: 2px;">&times;</span>');
  53. collapseButton.addEventListener('click', () => {
  54. const isCollapsed = containerContents.style?.display === 'none';
  55. console.log(isCollapsed);
  56. if (isCollapsed) {
  57. containerContents.style.display = 'block';
  58. container.style.width = 'auto';
  59. container.style.height = 'auto';
  60.  
  61. return;
  62. }
  63.  
  64. containerContents.style.display = 'none';
  65. container.style.width = '80px';
  66. container.style.height = '80px';
  67. });
  68.  
  69. container.append(collapseButton);
  70. containerContents.append(htmlToElement('<h1>Table of Contents</h1>'));
  71.  
  72. headings.forEach((heading) => {
  73. const html = heading.outerHTML;
  74. if (!html.includes('<a')) {
  75. return;
  76. }
  77.  
  78. const newElem = htmlToElement(html);
  79. const currentId = newElem.getAttribute('id');
  80. newElem.setAttribute('id', currentId + '--toc');
  81. newElem.style = `${getStylesFor(heading)}`;
  82.  
  83. containerContents.append(newElem);
  84.  
  85. const isActive = currentId === window.location.hash.replace('#', '');
  86. if (!isActive) {
  87. return;
  88. }
  89. setTimeout(() => {
  90. newElem.scrollIntoView();
  91. }, 100);
  92. });
  93. }, 100);
  94. })();