【问题标题】:Is there a JavaScript solution to generating a "table of contents" for a page?是否有用于为页面生成“目录”的 JavaScript 解决方案?
【发布时间】:2010-09-16 07:14:25
【问题描述】:

我在<h1><h6> 标签中有标题。有没有一种方法可以让我使用 JavaScript 为内容生成一个目录作为锚标记?

我希望输出类似于:

<ol>
    <li>Header 1</li>
    <li>Header 1</li>
        <li>Header 2</li>
            <li>Header 3</li>
</ol>

我目前没有使用 JavaScript 框架,但我不明白为什么我不能使用它。

我也在寻找完成的事情,因为我猜这是一个常见问题,但如果不是,我自己的起点会很好。

【问题讨论】:

  • 你能详细说明一下吗?也许 HTML sn-p 会有所帮助。
  • 应该很容易。请在 HTML 中指定所需的结构。
  • 另外,我们可以假设使用任何 Javascript 框架,如 Prototype 或 jQuery 吗?
  • 在您的示例中,缩进的&lt;li&gt; 元素应该嵌套在更多&lt;ol&gt; 元素中。

标签: javascript


【解决方案1】:

我无法抗拒快速实现。

在页面的任意位置添加以下脚本:

window.onload = function () {
    var toc = "";
    var level = 0;

    document.getElementById("contents").innerHTML =
        document.getElementById("contents").innerHTML.replace(
            /<h([\d])>([^<]+)<\/h([\d])>/gi,
            function (str, openLevel, titleText, closeLevel) {
                if (openLevel != closeLevel) {
                    return str;
                }

                if (openLevel > level) {
                    toc += (new Array(openLevel - level + 1)).join("<ul>");
                } else if (openLevel < level) {
                    toc += (new Array(level - openLevel + 1)).join("</ul>");
                }

                level = parseInt(openLevel);

                var anchor = titleText.replace(/ /g, "_");
                toc += "<li><a href=\"#" + anchor + "\">" + titleText
                    + "</a></li>";

                return "<h" + openLevel + "><a name=\"" + anchor + "\">"
                    + titleText + "</a></h" + closeLevel + ">";
            }
        );

    if (level) {
        toc += (new Array(level + 1)).join("</ul>");
    }

    document.getElementById("toc").innerHTML += toc;
};

您的页面结构应如下所示:

<body>
    <div id="toc">
        <h3>Table of Contents</h3>
    </div>
    <hr/>
    <div id="contents">
        <h1>Fruits</h1>
        <h2>Red Fruits</h2>
        <h3>Apple</h3>
        <h3>Raspberry</h3>
        <h2>Orange Fruits</h2>
        <h3>Orange</h3>
        <h3>Tangerine</h3>
        <h1>Vegetables</h1>
        <h2>Vegetables Which Are Actually Fruits</h2>
        <h3>Tomato</h3>
        <h3>Eggplant</h3>
    </div>
</body>

您可以在 https://codepen.io/scheinercc/pen/KEowRK 看到它的实际效果(旧链接:http://magnetiq.com/exports/toc.htm(适用于 IE、FF、Safari、Opera))

【讨论】:

  • 未测试,但看起来有些可靠。
  • 我不得不把脚本放在body标签的底部,这样DOM就有机会在脚本运行之前被初始化。
  • 链接失效了。
  • 感谢您的解决方案。请您提供一些提示如何修改 js 以覆盖嵌套标题,不幸的是我不擅长 JS
  • 如果标题定义了属性则不起作用,因为正则表达式太简单了。
【解决方案2】:

JQuery 被认为是一种快速简便的解决方案。对 jquery 目录 的快速谷歌搜索会产生两个有希望的结果:

【讨论】:

  • 所以,这些“解决方案”不会递归地重新创建结构(使用所有 h1..h6),对吧? -
【解决方案3】:

这是一个很棒的脚本:

https://github.com/matthewkastor/html-table-of-contents/wiki

使用它:

  1. 添加这个标签:

    <script src="./node_modules/html-table-of-contents/src/html-table-of-contents.js" type="text/javascript">
    
  2. 调用函数,比如在你body的onload属性中:

    <body onload="htmlTableOfContents();"> 
    

下面是生成方法的定义:

/**
 * Generates a table of contents for your document based on the headings
 *  present. Anchors are injected into the document and the
 *  entries in the table of contents are linked to them. The table of
 *  contents will be generated inside of the first element with the id `toc`.
 * @param {HTMLDOMDocument} documentRef Optional A reference to the document
 *  object. Defaults to `document`.
 * @author Matthew Christopher Kastor-Inare III
 * @version 20130726
 * @example
 * // call this after the page has loaded
 * htmlTableOfContents();
 */
function htmlTableOfContents (documentRef) {
    var documentRef = documentRef || document;
    var toc = documentRef.getElementById('toc');
    var headings = [].slice.call(documentRef.body.querySelectorAll('h1, h2, h3, h4, h5, h6'));
    headings.forEach(function (heading, index) {
        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);
        link.textContent = heading.textContent;

        var div = documentRef.createElement('div');
        div.setAttribute('class', heading.tagName.toLowerCase());

        div.appendChild(link);
        toc.appendChild(div);
        heading.parentNode.insertBefore(anchor, heading);
    });
}

try {
     module.exports = htmlTableOfContents;
} catch (e) {
    // module.exports is not defined
}

【讨论】:

  • 有经验吗?从响应的数量来看,可能存在怪癖,这段代码看起来很短。
  • 这段代码出奇的好。这里没有提到的是向元素添加样式表以获得嵌套外观的能力。我为 html5 符号稍微改进了 here 并使其更短!
【解决方案4】:

我已经修改了 AtesGoral 接受的答案中的函数,以输出正确的嵌套列表和有效的 HTML5。

只需将以下代码添加到您的脚本并在加载时调用 TableOfContents(container, output);,其中 container 是您的内容元素的类或 id,output 是类或TOC 元素的 id。默认值分别为“#contents”和“#toc”。

请参阅http://codepen.io/aufmkolk/pen/RWKLzr 以获得工作演示。

function TableOfContents(container, output) {
var toc = "";
var level = 0;
var container = document.querySelector(container) || document.querySelector('#contents');
var output = output || '#toc';

container.innerHTML =
    container.innerHTML.replace(
        /<h([\d])>([^<]+)<\/h([\d])>/gi,
        function (str, openLevel, titleText, closeLevel) {
            if (openLevel != closeLevel) {
                return str;
            }

            if (openLevel > level) {
                toc += (new Array(openLevel - level + 1)).join('<ul>');
            } else if (openLevel < level) {
                toc += (new Array(level - openLevel + 1)).join('</li></ul>');
            } else {
                toc += (new Array(level+ 1)).join('</li>');
            }

            level = parseInt(openLevel);

            var anchor = titleText.replace(/ /g, "_");
            toc += '<li><a href="#' + anchor + '">' + titleText
                + '</a>';

            return '<h' + openLevel + '><a href="#' + anchor + '" id="' + anchor + '">'
                + titleText + '</a></h' + closeLevel + '>';
        }
    );

if (level) {
    toc += (new Array(level + 1)).join('</ul>');
}
document.querySelector(output).innerHTML += toc;
};

【讨论】:

    【解决方案5】:

    我真的很喜欢d13's answer,但想稍微改进一下以使用 html5 符号并保留标头的现有 ID:

    document.addEventListener('DOMContentLoaded', function() {
        htmlTableOfContents();
    } );                        
    
    function htmlTableOfContents( documentRef ) {
        var documentRef = documentRef || document;
        var toc = documentRef.getElementById("toc");
    //  Use headings inside <article> only:
    //  var headings = [].slice.call(documentRef.body.querySelectorAll('article h1, article h2, article h3, article h4, article h5, article h6'));
        var headings = [].slice.call(documentRef.body.querySelectorAll('h1, h2, h3, h4, h5, h6'));
        headings.forEach(function (heading, index) {
            var ref = "toc" + index;
            if ( heading.hasAttribute( "id" ) ) 
                ref = heading.getAttribute( "id" );
            else
                heading.setAttribute( "id", ref );
    
            var link = documentRef.createElement( "a" );
            link.setAttribute( "href", "#"+ ref );
            link.textContent = heading.textContent;
    
            var div = documentRef.createElement( "div" );
            div.setAttribute( "class", heading.tagName.toLowerCase() );
            div.appendChild( link );
            toc.appendChild( div );
        });
    }
    
    try {
        module.exports = htmlTableOfContents;
    } catch (e) {
        // module.exports is not defined
    }
    

    您可以通过在标题中包含脚本来使用它。

    很棒的是,您可以在目录中使用样式表:

    <style>
        #toc div.h1 { margin-left: 0 }
        #toc div.h2 { margin-left: 1em }
        #toc div.h3 { margin-left: 2em }
        #toc div.h4 { margin-left: 3em }
    </style>
    

    在我的个人脚本中,我使用了一个稍微不同的选择器:

    var headings = [].slice.call(documentRef.body.querySelectorAll("article h1, article h2, article h3, article h4, article h5, h6"));
    

    主页面保存在&lt;article&gt;&lt;/article&gt; 中,脚本将仅搜索主文章中的标题。我可以在目录中使用标题,例如 &lt;nav id="toc"&gt;&lt;h3&gt;Table of contents&lt;/h3&gt;&lt;/nav&gt; 或在页脚/页眉中,而不显示在目录中。

    Gabriel Hautclocq下面的cmets中对文章代码进行了优化。他还指出,这并不是严格意义上的列表,而是类似列表的 div 结构,更简单,生成的代码更短。

    【讨论】:

    • 看起来您链接到的答案实际上是由@d13 给出的。 Sridhar-Sarnobat 只是一名评论员,事实上,他似乎对解决方案不太满意
    • 为什么不使用:var article = document.querySelector("article"); var headings = article ? [].slice.call(article.querySelectorAll( "h1, h2, h3, h4, h5, h6")) : [] (从而避免文章选择器的重复)?也就是说,它有效,但它使用 div,而不是问题要求的列表。
    【解决方案6】:

    所以我对@Ates Goral 和@Hendrik 提供的答案有疑问,我使用了所见即所得的方法,它在H1-h6 元素中添加了一些html sn-ps,例如br 标签等。因此代码中断并且不将其识别为有效的 h 元素,因为它与搜索模式不匹配。还有一些 WYSIWYG 会留下空的 h 标签,因为它们没有内容,所以不会被排除在外。随之而来的各种修改经常会遇到同样的问题。 我修复的一个主要错误是,如果您有一个标题,并且文本相同,它只会引用@Ates Goral 和@Hendrik 提供的第一个解决方案

    我应该声明,如果您从存储在数据库中的数据生成目录,则此解决方案很好。我使用了上面的一些解决方案并以此为基础,尤其是@Ates Goral 和@Hendrik

    function TableOfContents(container, output) {
            var txt = "toc-"; 
            var toc = "";
            var start = 0;
            var output = output || '#toc';
    
            var container = document.querySelector(container) || document.querySelector('#contents');
            var c = container.children;
    
            for (var i = 0; i < c.length; i++) {
            var isHeading = c[i].nodeName.match(/^H\d+$/) ;
            if(c[i].nodeName.match(/^H\d+$/)){
                var level = c[i].nodeName.substr(1);
    // get header content regardless of whether it contains a html or not that breaks the reg exp pattern
                var headerText = (c[i].textContent);
    // generate unique ids as tag anchors
                var anchor = txt+i;
    
                var tag = '<a href="#' + anchor + '" id="' + anchor + '">' + headerText + '</a>';
    
                c[i].innerHTML = tag;
    
                if(headerText){
                    if (level > start) {
                        toc += (new Array(level - start + 1)).join('<ul>');
                    } else if (level < start) {
                        toc += (new Array(start - level + 1)).join('</li></ul>');
                    } else {
                        toc += (new Array(start+ 1)).join('</li>');
                    }
                    start = parseInt(level);
                    toc += '<li><a href="#' + anchor + '">' + headerText + '</a>';
                }
            }
        }
        if (start) {
            toc += (new Array(start + 1)).join('</ul>');
        }
        document.querySelector(output).innerHTML += toc;
    }
    
    document.addEventListener('DOMContentLoaded', function() {
        TableOfContents();
      }
      ); 
    

    【讨论】:

      【解决方案7】:

      这是我为工作而想出的目录的香草 JavaScript 版本。

      它查找指定内容容器中的所有标题并为它们创建 ID,然后生成 TOC 链接。我让它添加样式类并使用嵌套列表来创建层次结构。

      window.addEventListener('DOMContentLoaded', function (event) { // Let the DOM content load before running the script.
      //Get all headings only from the actual contents.
      var contentContainer = document.getElementById('content'); // Add this div to the html
      var headings = contentContainer.querySelectorAll('h1,h2,h3,h4'); // You can do as many or as few headings as you need.
      
      var tocContainer = document.getElementById('toc'); // Add this div to the HTML
      // create ul element and set the attributes.
      var ul = document.createElement('ul');
      
      ul.setAttribute('id', 'tocList');
      ul.setAttribute('class', 'sidenav')
      
      // Loop through the headings NodeList
      for (i = 0; i <= headings.length - 1; i++) {
      
          var id = headings[i].innerHTML.toLowerCase().replace(/ /g, "-"); // Set the ID to the header text, all lower case with hyphens instead of spaces.
          var level = headings[i].localName.replace("h", ""); // Getting the header a level for hierarchy
          var title = headings[i].innerHTML; // Set the title to the text of the header
      
          headings[i].setAttribute("id", id)  // Set header ID to its text in lower case text with hyphens instead of spaces.
      
          var li = document.createElement('li');     // create li element.
          li.setAttribute('class', 'sidenav__item') // Assign a class to the li
      
          var a = document.createElement('a'); // Create a link
          a.setAttribute("href", "#" + id) // Set the href to the heading ID
          a.innerHTML = title; // Set the link text to the heading text
          
          // Create the hierarchy
          if(level == 1) {
              li.appendChild(a); // Append the link to the list item
              ul.appendChild(li);     // append li to ul.
          } else if (level == 2) {
              child = document.createElement('ul'); // Create a sub-list
              child.setAttribute('class', 'sidenav__sublist')
              li.appendChild(a); 
              child.appendChild(li);
              ul.appendChild(child);
          } else if (level == 3) {
              grandchild = document.createElement('ul');
              grandchild.setAttribute('class', 'sidenav__sublist')
              li.appendChild(a);
              grandchild.appendChild(li);
              child.appendChild(grandchild);
          } else if (level == 4) {
              great_grandchild = document.createElement('ul');
              great_grandchild.setAttribute('class', 'sidenav__sublist');
              li.append(a);
              great_grandchild.appendChild(li);
              grandchild.appendChild(great_grandchild);
          }
      }
      
      toc.appendChild(ul);       // add list to the container
      
      // Add a class to the first list item to allow for toggling active state.
      var links = tocContainer.getElementsByClassName("sidenav__item");
      
      links[0].classList.add('current');
      
      // Loop through the links and add the active class to the current/clicked link
      for (var i = 0; i < links.length; i++) {
          links[i].addEventListener("click", function() {
              var current = document.getElementsByClassName("current");
              current[0].className = current[0].className.replace(" current", "");
              this.className += " current";
          });
      }
      });
      

      我在CodePen 上有一个演示。

      【讨论】:

      • 非常感谢!
      【解决方案8】:

      您是在寻找预打包的解决方案,还是在询问如何实施?

      对于后者,您可以在&lt;h1&gt;&lt;h6&gt; 上递归使用getElementsByTagName() 来遍历所有&lt;h*&gt; 元素并构造相应的嵌套&lt;ul&gt;&lt;ol&gt;列表。您还必须将 &lt;a&gt; 标签添加到标题中。

      【讨论】:

      • 使用 Xpath 假定文档是格式良好的 XHTML。
      • 是的,它还假设浏览器支持 XPath。我猜这不是最通用的解决方案...
      【解决方案9】:

      在此页面上查看您要查找的组件:Re-inventing XMLHttpRequest: Cross-browser implementation with sniffing capabilities

      它遍历整个文档并创建一个目录,其中所有 h1-h6 元素都反映在一个可打开的(悬停时)结构中。该组件是独立的,不使用任何库。

      【讨论】:

        【解决方案10】:
         let headers = document.querySelectorAll('h1,h2,h3,h4,h5,h6');
          let list    = document.createElement('ol');
        
          let _el = list;
          for(i=0; i<headers.length; i++) {
            while(_el) {
              let li = document.createElement('li');
              li.innerText = headers[i].innerText;
              li.setAttribute('tagName', headers[i].tagName);
              if(_el.getAttribute('tagName') < headers[i].tagName) {
                let ol = _el.children.length > 0 ? ol = _el.querySelector('ol') : document.createElement('ol');
                ol.appendChild(li);
                _el.appendChild(ol);
                _el = li;
                break;
              } else {
                if(_el.tagName == 'OL') {
                 _el.appendChild(li);
                 _el = li;
                 break;
                } else if (!_el.parentNode.parentNode) {
                  _el.parentNode.appendChild(li);
                  _el = li;
                  break;
                }
                else {
                  _el = _el.parentNode.parentNode;
                }
              }
            }
          }
          console.log(list);
        

        【讨论】:

          【解决方案11】:
            this.insert = (el, h) => {
              let li = document.createElement('li');
              li.innerText = h.innerText;
              li.setAttribute('tagName', h.tagName);
              if(el.tagName == 'OL') {
                el.appendChild(li);
                return li;
              } else if(el.getAttribute('tagName') < h.tagName) {
                let ol = el.children.length > 0 ? ol = el.querySelector('ol') : document.createElement('ol');
                ol.appendChild(li);
                el.appendChild(ol);
                return li;
              } else if(!el.parentNode.parentNode) {
                el.parentNode.appendChild(li);
                return li;
              } else {
                return this.insert(el.parentNode.parentNode, h);
              }
            }
          
            this.parse = (headers) => {
              let list = document.createElement('ol');
              let el = list;
              for(i=0; i<headers.length; i++) {
                el = this.insert(el, headers[i]);
              }
              return list;
            }
            let headers = document.querySelectorAll('h1,h2,h3,h4,h5,h6');
            let toc = this.parse(headers);
            console.log(toc);
          

          【讨论】:

            【解决方案12】:

            这是一个基于 jQuery 的函数,它分析提供的内容中的标题元素 &lt;h1&gt;&lt;h2&gt; ... 并返回带有 TOC 层次结构的 jQuery 对象,您可以将其附加到页面:

            <div class="table_of_contents">
                <ul>
                    <a href="#Level1_Heading">Level1 Heading</a>
                    <li>
                        <a href="#Level2_Heading">Level2 Heading</a>
                        <ul>
                            ...
                    </li>
                </ul>
            </div>
            

            它还通过插入不可见的&lt;a name="..."&gt; 锚点来修改$content,用户在单击创建的目录项时会跳转到这些锚点。

            function generate_toc($content) {
                let $toc = $("<div>", {class: "table_of_contents"});
                let level2$toc_item = {0: $toc};
                let used_anchors = {};
                $content.find("h1, h2, h3, h4, h5").each(function() {
                    // find out the level of heading
                    let level = parseInt($(this).prop("tagName").replace(/[^0-9]/gi, ""));
                    let heading_text = $(this).text();
                    // define the unique anchor id
                    let heading_anchor = heading_text.replace(/[^a-z0-9]/gi, "_");
                    while (heading_anchor in used_anchors) {heading_anchor += "_";}
                    used_anchors[heading_anchor] = true;                
                    // add target point into main content
                    $(this).prepend($("<a>", {name: heading_anchor}));
                    // find the parent level for TOC item
                    let parent_level = level-1;
                    for (; !(parent_level in level2$toc_item); parent_level--); 
                    // remove all jumped over levels
                    for (let l in level2$toc_item) {
                        if (parseInt(l) > parent_level) {
                            delete level2$toc_item[l];
                        }
                    }
                    let $parent = level2$toc_item[parent_level];
                    // create new TOC item inside parent's <ul>
                    level2$toc_item[level] = $("<li>").appendTo(
                        $parent.children("ul").length == 1
                        ? $($parent.children("ul")[0])
                        : $("<ul>").appendTo($parent)
                    ).append($("<a>", {href: `#${heading_anchor}`}).text(heading_text));
                });
                return $toc;
            }
            

            使用示例:

            $("body").prepend(generate_toc("body"));
            

            【讨论】:

              【解决方案13】:

              关键是捕获标题的数据并将其转换为对象TocItem见下面代码

              至于TOC演示,可以使用ul或者依赖markmap.jsd3.js等生成。

              核心代码

              /**
               * @param {string} text
               * @param {int} level
               * @param {TocItem} parent
               * */
              function TocItem(text, level, parent = undefined) {
                this.text = text
                this.level = level
                this.id = undefined
                this.parent = parent
                this.children = []
              }
              
              /**
              * @param {[HTMLHeadingElement]} headingSet
              * */
              function parse(headingSet) {
                const tocData = []
                let curLevel = 0
                let preTocItem = undefined
              
                headingSet.forEach(heading => {
                  const hLevel = heading.outerHTML.match(/<h([\d]).*>/)[1]
                  const titleText = heading.innerText
              
                  switch (hLevel >= curLevel) {
                    case true:
                      if (preTocItem === undefined) {
                        preTocItem = new TocItem(titleText, hLevel)
                        tocData.push(preTocItem)
                      } else {
                        const curTocItem = new TocItem(titleText, hLevel)
                        const parent = curTocItem.level > preTocItem.level ? preTocItem : preTocItem.parent
                        curTocItem.parent = parent
                        parent.children.push(curTocItem)
                        preTocItem = curTocItem
                      }
                      break
                    case false:
                      // We need to find the appropriate parent node from the preTocItem
                      const curTocItem = new TocItem(titleText, hLevel)
                      while (1) {
                        if (preTocItem.level < curTocItem.level) {
                          preTocItem.children.push(curTocItem)
                          preTocItem = curTocItem
                          break
                        }
                        preTocItem = preTocItem.parent
              
                        if (preTocItem === undefined) {
                          tocData.push(curTocItem)
                          preTocItem = curTocItem
                          break
                        }
                      }
                      break
                  }
              
                  curLevel = hLevel
              
                  if (heading.id === "") {
                    heading.id = titleText.replace(/ /g, "-").toLowerCase()
                  }
                  preTocItem.id = heading.id
                })
              
                return tocData
              }
              

              示例

              <style>
                /* CSS is not necessary. That is for look better and easy to test. */
              
                /* Longer pages, so you can test to see if you can actually get to the specified location after clicking. */
                body {
                  min-height: 160rem
                }
              
                /* similar to the bootstrap */
                .fixed-top {
                  position: fixed;
                  top: 0;
                  right: 50vw;
                  z-index: 1000;
                }
              </style>
              
              <div id="target">
                <h1 id="my-app">App1</h1>
                <h2>Video</h2>
                <h3>mp4</h3>
                <h3>webm</h3>
              
                <h2>Audio</h2>
                <h3>Mp3</h3>
                <h3>m4a</h3>
              
                <h1>App2</h1>
                <h2>Overview</h2>
              </div>
              
              <script>
              
                class TOC {
                  /**
                   * @param {[HTMLHeadingElement]} headingSet
                   * */
                  static parse(headingSet) {
                    const tocData = []
                    let curLevel = 0
                    let preTocItem = undefined
              
                    headingSet.forEach(heading => {
                      const hLevel = heading.outerHTML.match(/<h([\d]).*>/)[1]
                      const titleText = heading.innerText
              
                      switch (hLevel >= curLevel) {
                        case true:
                          if (preTocItem === undefined) {
                            preTocItem = new TocItem(titleText, hLevel)
                            tocData.push(preTocItem)
                          } else {
                            const curTocItem = new TocItem(titleText, hLevel)
                            const parent = curTocItem.level > preTocItem.level ? preTocItem : preTocItem.parent
                            curTocItem.parent = parent
                            parent.children.push(curTocItem)
                            preTocItem = curTocItem
                          }
                          break
                        case false:
                          // We need to find the appropriate parent node from the preTocItem
                          const curTocItem = new TocItem(titleText, hLevel)
                          while (1) {
                            if (preTocItem.level < curTocItem.level) {
                              preTocItem.children.push(curTocItem)
                              preTocItem = curTocItem
                              break
                            }
                            preTocItem = preTocItem.parent
              
                            if (preTocItem === undefined) {
                              tocData.push(curTocItem)
                              preTocItem = curTocItem
                              break
                            }
                          }
                          break
                      }
              
                      curLevel = hLevel
              
                      if (heading.id === "") {
                        heading.id = titleText.replace(/ /g, "-").toLowerCase()
                      }
                      preTocItem.id = heading.id
                    })
              
                    return tocData
                  }
              
                  /**
                   * @param {[TocItem]} tocData
                   * @return {string}
                   * */
                  static build(tocData) {
                    let result = "<ul>"
                    tocData.forEach(toc => {
                      result += `<li><a href=#${toc.id}>${toc.text}</a></li>`
                      if (toc.children.length) {
                        result += `${TOC.build(toc.children)}`
                      }
                    })
                    return result + "</ul>"
                  }
                }
              
                /**
                 * @param {string} text
                 * @param {int} level
                 * @param {TocItem} parent
                 * */
                function TocItem(text, level, parent = undefined) {
                  this.text = text
                  this.level = level
                  this.id = undefined
                  this.parent = parent
                  this.children = []
                }
              
                window.onload = () => {
              
                  const headingSet = document.querySelectorAll("h1, h2, h3, h4, h5, h6") // You can also select only the titles you are interested in.
                  const tocData = TOC.parse(headingSet)
                  
                  console.log(tocData)
              
                  const tocHTMLContent = TOC.build(tocData)
                  const frag = document.createRange().createContextualFragment(`<fieldset class="fixed-top"><legend>TOC</legend>${tocHTMLContent}</fieldset>`)
                  document.querySelector(`body`).insertBefore(frag, document.querySelector(`body`).firstChild)
                }
              </script>

              原生 JavaScript 也是

              【讨论】:

                【解决方案14】:

                我可能有点迟到了,但我已经把自己的 jQuery 插件放在一起......

                可通过 jsDeliver CDN 获得:https://cdn.jsdelivr.net/npm/@thelevicole/toc.js@1/dist/toc.jquery.js

                页面结构的简单示例:

                <div class="table-of-contents"></div>
                <h1>Heading 1</h1>
                <h2>Heading 2</h2>
                <h3>Heading 3</h3>
                <h2>Heading 2</h2>
                <h2>Heading 2</h2>
                <h1>Heading 1</h1>
                

                现在启动插件:

                $('.table-of-contents').tableOfContents();
                

                哪个会输出这个列表:

                <ul class="toc-list">
                    <li class="toc-item">
                        <a href="#heading-1" class="toc-link">Heading 1</a>
                        <ul class="toc-list">
                            <li class="toc-item">
                                <a href="#heading-2" class="toc-link">Heading 2</a>
                                <ul class="toc-list">
                                    <li class="toc-item">
                                        <a href="#heading-3" class="toc-link">Heading 3</a>
                                    </li>
                                </ul>
                            </li>
                            <li class="toc-item">
                                <a href="#heading-4" class="toc-link">Heading 2</a>
                            </li>
                            <li class="toc-item">
                                <a href="#heading-5" class="toc-link">Heading 2</a>
                            </li>
                        </ul>
                    </li>
                    <li class="toc-item">
                        <a href="#heading-6" class="toc-link">Heading 1</a>
                    </li>
                </ul>
                

                截图供参考:


                有很多选项可以通过...

                $('.table-of-contents').tableOfContents({
                    contentTarget: $( document.body ), // The element with content.
                    selectors: 'h1$1; h2$2; h3$3; h4$4; h5$5; h6$6;', // Tree structure.
                    nestingDepth: -1, // How deep we'll allow nesting. -1 for infinate.
                    slugLength: 40, // The max number of chars in the hash slug.
                    anchors: true, // Add anchors to headings.
                    anchorText: '#', // The symbol added to headings.
                    orderedList: false // True to use <ol> instead of <ul>
                });
                

                selectors 选项允许您从其他选择器创建目录,而不仅仅是 h1、h2、h3 等。

                选择器选项接受选择器和深度的字符串、数组或对象:

                字符串

                $('.table-of-contents').tableOfContents({
                    // '{selector}${depth}; {selector}${depth}; ...'
                    selectors: 'h1$1; h2$2; h3$3; p:not(.my-class)$2; ...'
                });
                

                选择器模式相当简单,并且遵循一个简单的模式。我们首先有 DOM/CSS 选择器 {selector},然后是嵌套深度,它以美元符号 ${depth} 开始并以分号结束;

                用于嵌套标题的默认模式如下所示:'h1$1; h2$2; h3$3; h4$4; h5$5; h6$6;'

                数组

                $('.table-of-contents').tableOfContents({
                    selectors: [
                        // '{selector}${depth}'
                        'h1$1',
                        'h2$2',
                        'h3$3',
                        'p:not(.my-class)$2',
                        ...
                    ]
                });
                

                对象

                $('.table-of-contents').tableOfContents({
                    selectors: {
                        // '{selector}': {depth}
                        'h1': 1,
                        'h2': 2,
                        'h3': 3,
                        'p:not(.my-class)': 2,
                        ...
                    }
                });
                

                自定义选择器示例:

                <div class="table-of-contents"></div>
                
                <article>
                    <p class="level-1">I'm level 1</p>
                    <p class="level-2">I'm level 2</p>
                    <p class="level-1">I'm level 1 again</p>
                    <p class="level-2">I'm level 2 again</p>
                    <p class="level-3">I'm level 3</p>
                    <p><strong>I'm a div element</strong></p>
                    <p class="level-2">I'm level 2</p>
                </article>
                
                $('.table-of-contents').tableOfContents({
                    contentTarget: 'article',
                    selectors: '.level-1 $1; .level-2 $2; .level-3 $3; p > strong $4'
                });
                

                将导致...


                我还没有机会编写适当的文档,但您可以在 GitHub 上关注此项目:https://github.com/thelevicole/toc.js 或在此处查看主页:https://thelevicole.com/toc.js/

                【讨论】:

                  【解决方案15】:

                  页面加载后,循环浏览 DOM 并查找您感兴趣的元素。构建一个不错的锚点列表并将其添加到文档中您想要的位置。

                  【讨论】:

                    【解决方案16】:

                    您可以create dynamic table of contents for any HTML document using JavaScript,它可以显示从 h1 到 h6 的标题列表以及指向标题的链接,并使用以下步骤更轻松地浏览文档。

                    首先创建window.onload函数,当文档完成加载时自动运行,如下所示

                    window.onload=function(){
                    
                    function getSelectedText(){
                    if (window.getSelection)
                    return window.getSelection().toString()+"
                    "+document.URL;
                    else if (document.selection)
                     return document.selection.createRange().text+"
                    "+document.URL;
                    }
                    var toc=document.getElementById("TOC");
                    if(!toc) {
                     toc=document.createElement("div");
                     toc.id="TOC";
                     document.body.insertBefore(toc, document.body.firstChild);
                    }

                    将以下代码添加到函数中以查找所有通过标签并将它们设置为标题。

                    var headings;
                    if (document.querySelectorAll) 
                    headings=document.querySelectorAll("h1, h2, h3, h4, h5, h6");
                    else
                    headings=findHeadings(document.body, []);

                    使用以下 CSS 代码设计目录

                    #TOC {border:solid black 1px; margin:10px; padding:10px;}
                    .TOCEntry{font-family:sans-serief;}
                    .TOCEntry a{text-decoration:none;}
                    .TOCLevel1{font-size:17pt; font-weight:bold;}
                    .TOCLevel2{font-size:16pt; font-weight:bold;}
                    .TOCLevel3{font-size:15pt; font-weight:bold;}
                    .TOCLevel4{font-size:14pt; margin-left:.25in;}
                    .TOCSectNum{display:none;}
                    

                    这是在脚本标签中创建目录的完整 JavaScript 代码。

                    window.onload=function(){
                    
                    function getSelectedText(){
                    if (window.getSelection)
                    return window.getSelection().toString()+"<br/>"+document.URL;
                    else if (document.selection)
                     return document.selection.createRange().text+"<br/>"+document.URL;
                    }
                    
                    var toc=document.getElementById("TOC");
                    if(!toc) {
                     toc=document.createElement("div");
                     toc.id="TOC";
                     document.body.insertBefore(toc, document.body.firstChild);
                    }
                    var headings;
                    if (document.querySelectorAll) 
                    headings=document.querySelectorAll("h1, h2, h3, h4, h5, h6");
                    else
                    headings=findHeadings(document.body, []);
                    
                    function findHeadings(root, sects){
                     for(var c=root.firstChild; c!=null; c=c.nextSibling){
                    if (c.nodeType!==1) continue;
                    if (c.tagName.length==2 && c.tagName.charAt(0)=="H")
                    sects.push(c);
                    else
                    findHeadings(c, sects);
                    }
                    return sects;
                    }
                    
                    var sectionNumbers=[0,0,0,0,0,0];
                    
                    for(var h=0; h<headings.length; h++) {
                     var heading=headings[h];
                    
                    if(heading.parentNode==toc) continue;
                    
                    var level=parseInt(heading.tagName.charAt(1));
                    if (isNaN(level)||level<1||level>6) continue;
                    
                    sectionNumbers[level-1]++;
                    for(var i=level; i<6; i++) sectionNumbers[i]=0;
                    
                    var sectionNumber=sectionNumbers.slice(0, level).join(".");
                    
                    var span=document.createElement("span");
                    span.className="TOCSectNum";
                    span.innerHTML=sectionNumber;
                    heading.insertBefore(span, heading.firstChild);
                    heading.id="TOC"+sectionNumber;
                    var anchor=document.createElement("a");
                    heading.parentNode.insertBefore(anchor, heading);
                    anchor.appendChild(heading);
                    
                    var link=document.createElement("a");
                    link.href="#TOC"+sectionNumber; 
                    link.innerHTML=heading.innerHTML;
                    
                    var entry=document.createElement("div");
                    entry.className="TOCEntry TOCLevel" + level;
                    entry.appendChild(link);
                    
                    toc.appendChild(entry);
                    }
                    };
                    来源:How to Create Table of Contents Using JavaScript

                    【讨论】:

                      猜你喜欢
                      • 2018-02-11
                      • 1970-01-01
                      • 2019-11-19
                      • 1970-01-01
                      • 2022-01-03
                      • 1970-01-01
                      • 1970-01-01
                      • 2011-03-07
                      • 2013-08-15
                      相关资源
                      最近更新 更多