Search code examples
javascripthtmlcountertableofcontents

TOC Hierarchical numbering (based on h2 and h3 tag). Problem with counters and numbering


I am currently using paged.js to paginate content in the browser to create PDF output from my HTML content. I am using the following html:

<div class="no-break table-of-contents">
  <h1 class="toc-header" id="toc-start">Table of Contents</h1>
  <ol id="toc">  
  </ol>
</div> 

and javascript function:

function createToc(config) {
    const content = config.content;
    const tocElement = config.tocElement;
    const titleElements = config.titleElements;

    let tocElementDiv = content.querySelector(tocElement);
    let tocUl = document.createElement("ul");
    tocUl.id = "list-toc-generated";
    tocElementDiv.appendChild(tocUl);

    let topLevelCounters = [0]; // Array to store the counters for each level

    // add class to all title elements
    let tocElementNbr = 0;
    for (var i = 0; i < titleElements.length; i++) {
        let titleHierarchy = i + 1;
        let titleElement = content.querySelectorAll(titleElements[i]);

        titleElement.forEach(function(element) {
            // add classes to the element
            element.classList.add("title-element");
            element.setAttribute("data-title-level", titleHierarchy);

            // add id if doesn't exist
            tocElementNbr++;
            idElement = element.id;
            if (idElement == "") {
                element.id = "title-element-" + tocElementNbr;
            }
            let newIdElement = element.id;

            // Determine the current level based on the titleHierarchy
            let currentLevel = titleHierarchy - 1;

            // Increment the counter for the current level
            topLevelCounters[currentLevel] =
                topLevelCounters[currentLevel] === undefined ?
                1 :
                topLevelCounters[currentLevel] + 1;

            // Reset the counters for the lower levels
            topLevelCounters = topLevelCounters.slice(0, currentLevel + 1);

            // Add number to original heading element
            let titleNumber = document.createElement("span");
            titleNumber.classList.add("title-number");

            // Construct the number based on the current level and counters
            let number = topLevelCounters
                .map(function(counter) {
                    return counter.toString();
                })
                .join(".");

            titleNumber.textContent = number + ".";
            element.prepend(titleNumber);
        });
    }

    // create toc list
    let tocElements = content.querySelectorAll(".title-element");

    for (var i = 0; i < tocElements.length; i++) {
        let tocElement = tocElements[i];

        let tocNewLi = document.createElement("li");

        // Add class for the hierarchy of toc
        tocNewLi.classList.add("toc-element");
        tocNewLi.classList.add("toc-element-level-" + tocElement.dataset.titleLevel);

        // Keep class of title elements
        let classTocElement = tocElement.classList;
        for (var n = 0; n < classTocElement.length; n++) {
            if (classTocElement[n] != "title-element") {
                tocNewLi.classList.add(classTocElement[n]);
            }
        }

        // Create the element
        tocNewLi.innerHTML =
            '<a href="#' +
            tocElement.id +
            '">' +
            tocElement.innerHTML +
            "</a>";
        tocUl.appendChild(tocNewLi);
    }
} 

to generate a table of contents based on the h2 and h2 tags in my html. The idea is that it also add the generated numbers to the h2 and h3 elements in the main content.

So far the generated TOC reads like this:

1.Slab geometry  
  9.1.Concrete  
  9.2.Reinforcement  
  9.3.Additional Reinforcement Guidelines  
2.Subbase  
3.Diagrams  
  9.4.Section - Internal  
  9.5.Section - Edge  
  9.6.Plan  

When it should read like this:

1.Slab geometry
  1.1.Concrete
  1.2.Reinforcement
  1.3.Additional Reinforcement Guidelines
2.Subbase
3.Diagrams
 3.1.Section - Internal
 3.2.Section - Edge
 3.3.Plan

Any ideas where I am messing up my counters and numbering?


Solution

  • In the OP, there is no definition of config. It appears to be an Object Literal so in the example below, I have created an Object that stores all of the settings needed to customize the list.

    The following CSS is really the only thing needed to have the requested counter pattern.

    .list.list {
      list-style: none;
    }
    
    .list.list::before {
      content: '';
      counter-reset: item;
    }
    
    .item.item::before {
      counter-increment: item;
      content: counters(item, ".")" ";
    }
    

    Details are commented in example

    /**
     * CSS rulesets that number all list items with "level" and
     * "level.level" pattern. The classes: .list and .item are doubled
     * up to increase selectivity (without the use of !important).
     */
     
    const rulesets = `
      :root {
        font: 2.25ch/1.15 "Segoe UI";
      }
      
      h3 {
        font-size: 1.25rem;
      }
      
      .list.list {
        list-style: none;
      }
    
      .list.list::before {
        content: '';
        counter-reset: item;
      }
    
      .item.item::before {
        counter-increment: item;
        content: counters(item, ".")" ";
      }
    `;
    
    /**
     * An Object Literal that stores customizable settings which
     * is passed to createToc() and partially to linkTitles().
     *
     * @prop {String} target - A selector used to reference
     *       an element in which to insert the TOC.
     * @prop {Object} header - Main title.
     *       @prop {String} node - tagName of main title.
     *       @prop {String} title - Text of node.
     * @prop {Object} root - Main menu list -- the start of list.
     *       @prop {String} node - tagName of main menu list.
     *       @prop {String} cls - className of node.
     * @prop {Array} branch - An Array of Objects that stores the
     *       setttings of all of the lists and items bewteen the
     *       beginning (root) and ending (leaf).
     *       @prop {String} node - tagName of node.
     *       @prop {String} cls - className of node.
     * @prop {Array} text - A multi-dimensional array that stores
     *       the text of all of the lists and items.
     * @prop {Array} link - A multi-dimensional array that stores
     *       the href of all items.
     */
    const config = {
      target: "main",
      header: { node: "h1", title: "Table of Contents" },
      root: { node: "menu", cls: "list"},
      branch: [
        { node: "h2", cls: "item" },
        { node: "ol", cls: "list" }
      ],
      leaf: { node: "h3", cls: "item" },
      text: [ 
        [ "Slab Geometry", [ "Concrete", "Reinforcement", "Additional Reinforcement Guidlines" ] ], 
        [ "Slabbase", [] ], 
        [ "Diagrams", [ "Section - Internal", "Section - Edge", "Plans"] ]
      ],
      link: [ "slabGeometry", [ "concrete", "reinforcement", "additionalReinforcementGuidelines" ], [ "slabbase", [] ], [ "diagrams", [ "sectionInternal", "sectionEdge", "plans" ] ]
      ]
    };
    
    /**
     * This is added because the plugin, Paged.js appears to be 
     * heavy on styles. Just in case Paged.js styles overrides
     * your styles, this function will render a `<style>` tag at the
     * bottom of </head> with critical CSSDeclarations (rulesets),
     * making those rulests a higher priority ([style] attribute and
     * !important has higher priority but anything in a *.css file 
     * will have a lower priority).
     * 
     * @param {String} rsets - CSS rulesets to be parsed in a <style>
     * that's inserted within the <head>. See `rulesets` above.
     */
    function injectCss(rsets) {
      const css = document.createElement("style");
      const txt = document.createTextNode(rsets);
      css.append(txt);
      document.head.append(css);
    }
    
    
    /**
     * This function dynamically renders a nested list and passes an
     * Object Literal that is used for various settings.
     * 
     * @param {Object} cfg - An Object Literal that stores settings.
     */
    function createToc(cfg) {
      let frag = document.createDocumentFragment();
    
      const header = document.createElement(cfg.header.node);
      header.textContent = cfg.header.title;
      
      const root = document.createElement(cfg.root.node);
      root.id = cfg.root.id;
      root.className = cfg.root.cls;
      
      cfg.text.forEach(subArray => {
      
        const B1 = document.createElement(cfg.branch[0].node);
        B1.className = cfg.branch[0].cls;
        B1.textContent = subArray[0];
        
        const B2 = document.createElement(cfg.branch[1].node);
        B2.className = cfg.branch[1].cls;
        
        subArray[1].forEach(title => {
        
          const L = document.createElement(cfg.leaf.node);
          L.className = cfg.leaf.cls;
          L.textContent = title;
          B2.append(L);
        });
        root.append(B1, B2);
      });
      frag.append(header, root);
      document.querySelector(cfg.target).append(frag);
    }
    
    /**
     * This function should be ran after the list from createToc() has been
     * rendered. It will wrap a target node in an <a>nchor and assign
     * a pre-defined href value.
     *
     * @param {String} selector - A selector to find all targets.
     * @param {Array} strArray - An Array of Strings that represent 
     *        the href of each .item.
     */
    function linkTitles(selector, strArray) {
      const titles = document.querySelectorAll(selector);
      const links = strArray.flat(Infinity);
      titles.forEach((H, I) => {
        let txt = H.textContent;
        let lnk = `<a href="#${links[I]}">${txt}</a>`;
        H.replaceChildren();
        H.insertAdjacentHTML("beforeend", lnk);
      });
    }
    
    injectCss(rulesets);
    createToc(config);
    linkTitles(".item", config.link);
    <main></main>