-1

(Similar to How to refer a css property value from another element or How to get a DOM element's ::before content with JavaScript?)

I have a HTML page with headings (H1 to H3), and I've added the usual CSS rules to automatically prepend a hierarchical number to the headings. That works nicely.

I also wrote some JavaScript that populates an empty span element with a table of contents derived from the page headings' textContent. That works nicely, too.

However the table of contents lacks the numbers automatically assigned. I saw some code to retrieve the :before part of an element using JavaScript, but that gives the style rules in my case (e.g. for a <H3>: counter(c_h1) "." counter(c_h2) "." counter(c_h3) " "), and not the value built from those style rules (e.g.: 1.3.1 ). So I think that does not help me anything in JavaScript.

Is there any way to have the same heading numbers in the TOC created by JavaScript as those automatically added by CSS rules?

Example

.pp {
  font-family: sans-serif
}

h1,
h2,
h3 {
  font-family: sans-serif;
  page-break-after: avoid;
  break-after: avoid
}


/* counters */

:root {
  counter-reset: c_h1 0 c_h2 0 c_h3 0 c_h4 0
}

h1:before {
  counter-reset: c_h2 0 c_h3 0 c_h4 0;
  counter-increment: c_h1;
  content: counter(c_h1)" "
}

h2:before {
  counter-reset: c_h3 0 c_h4 0;
  counter-increment: c_h2;
  content: counter(c_h1)"."counter(c_h2)" "
}

h3:before {
  counter-reset: c_h4 0;
  counter-increment: c_h3;
  content: counter(c_h1)"."counter(c_h2)"."counter(c_h3)" "
}


/* table of contents */

span.toc:not(:empty) {
  border-style: solid;
  border-width: thin;
  border-color: black;
  padding: 1ex;
  margin-top: 1em;
  margin-bottom: 1em;
  display: inline-block
}

div.toc-title {
  font-family: sans-serif;
  font-weight: bold;
  padding-bottom: 1ex
}

div.toc-H1 {
  padding-left: 1em
}

div.toc-H2 {
  padding-left: 2em
}

div.toc-H3 {
  padding-left: 3em
}
<!DOCTYPE html
    PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="de-DE" xml:lang="de-DE">

<head>
  <title>Example</title>
  <script>
"use strict";

// Create table of contents from headings
function make_TOC(target_ID) {
  const candidate = /^H[1-9]$/i; {
    let list = document.getElementsByTagName("H1");
    let toc = document.getElementById(target_ID);
    let seq = 0;
    var e;

    if (list.length > 0) {
      e = list.item(0);
      let div = document.createElement("div");

      div.setAttribute("class", "toc-title");
      div.textContent = "Inhaltsverzeichnis";
      toc.appendChild(div);
    }
    while (e !== null) {
      let n = e.tagName;
      if (n.search(candidate) !== -1) {
        let div = document.createElement("div");
        let link = document.createElement("a");
        let id = e.id;

        div.setAttribute("class", "toc-" + n);
        div.appendChild(link);
        link.textContent = e.textContent;
        if (id === "") { // add id
          id = "H." + ++seq;
          e.id = id;
        }
        link.setAttribute("href", "#" + id);
        toc.appendChild(div);
      }
      e = e.nextElementSibling;
    }
  }
}
  </script>
</head>

<body>
  <span class="toc" id="toc"></span>
  <h1 class="pp">H1: D...</h1>
  <h2 class="pp">H2: Z...</h2>
  <p class="pp">D...</p>
  <ul class="pp">
    <li class="pp">B...</li>
    <li class="pp">U...</li>
  </ul>
  <p class="pp">D...</p>
  <h2 class="pp">H2: C...</h2>
  <p class="pp">D...</p>
  <p class="pp">A...</p>
  <h2 class="pp">H2: E...</h2>
  <h3 class="pp">H3: S...</h3>
  <p class="pp">U...</p>
  <h3 class="pp">H3: R...</h3>
  <p class="pp">S...</p>
  <h3 class="pp">H3: L...</h3>
  <p class="pp">S...</p>
  <h3 class="pp">H3: W...</h3>
  <p class="pp">N...</p>
  <script type="text/javascript">
    make_TOC("toc")
  </script>
</body>

</html>

So here is how the example should be displayed (indent is done via classes and CSS, my JavaScript knowledge is poor, also):

Screenshot from Firefox

U. Windl
  • 3,480
  • 26
  • 54
  • 1
    Can you provide a minimal example (with code)? `const styles = window.getComputedStyle(element, '::before');` should do the trick. – metatron Jan 12 '23 at 12:32
  • Well, that reports `CSS2Properties(347)` with 347 lines of properties; isn't really minimal ;-) – U. Windl Jan 12 '23 at 12:42
  • 2
    You can't get the _computed_ value of the `content` since that is defined in CSS. As you saw, you can only get the _written_ value of `content` which in your case includes `counter()` functions. I'd look to recreate the same `counter()` rules in your table of contents block, assuming that those will always have the same DOM structure. See https://stackoverflow.com/a/55258061 – romellem Jan 12 '23 at 15:31
  • Hi, I had some time to kill and made a new solution including numbering. See answer below. – metatron Jan 12 '23 at 19:57

3 Answers3

1

The Javascript that I used from the link you provided didn't return the counter values indeed.

From what I read I think there is no way you can access the css counter values: How can I read the applied CSS-counter value?

What you could do is recreate the outline of the document (TOC) with Javascript including the css numbering. I guess that's what you're asking, right?

Does this code fit the bill?

const elems = Array.from(document.querySelectorAll('body > *'))

let ch1 = 0
let ch2 = 0
let ch3 = 0

let s = ''

elems
    .filter(el =>
        el.tagName.toLowerCase() == 'h1'
        || el.tagName.toLowerCase() == 'h2'
        || el.tagName.toLowerCase() =='h3')
    .forEach(el => {
        if(el.tagName.toLowerCase() == 'h1') {
            ch1++
            ch2 = 0
            ch3 = 0
        }
        if(el.tagName.toLowerCase() == 'h2') {
            ch2++
            ch3 = 0
        }
        if(el.tagName.toLowerCase() == 'h3') ch3++

        if(ch2 == 0 && ch3 == 0) {
            s += `<div>${ch1}. ${el.textContent}</div>`
        }
        else if (ch3 == 0) {
            s += `<div>${ch1}.${ch2}. ${el.textContent}</div>`
        }
        else {
            s += `<div>${ch1}.${ch2}.${ch3}. ${el.textContent}</div>`
        }
    })

let toc = document.createElement('div');
toc.innerHTML = s;
document.body.appendChild(toc)
<h1 class="pp">H1: D...</h1>
<h2 class="pp">H2: Z...</h2>
<p class="pp">D...</p>
<ul class="pp">
    <li class="pp">B...</li>
    <li class="pp">U...</li>
</ul>
<p class="pp">D...</p>
<h2 class="pp">H2: C...</h2>
<p class="pp">D...</p>
<p class="pp">A...</p>
<h2 class="pp">H2: E...</h2>
<h3 class="pp">H3: S...</h3>
<p class="pp">U...</p>
<h3 class="pp">H3: R...</h3>
<p class="pp">S...</p>
<h3 class="pp">H3: L...</h3>
<p class="pp">S...</p>
<h1 class="pp">H1: W...</h1>
<p class="pp">N...</p>
metatron
  • 896
  • 7
  • 14
  • I reread your question. I guess you need the numbering too (1, 1.1, etc…) in front of the toc items – metatron Jan 12 '23 at 17:48
0

Just as an exercise and for the fun of it I made another version using recursion.

Anyway, user romellem made a good point about using css counters in the toc too so maybe that's a better solution.

const el = document.querySelector('section')

const createTocDiv = (el) => {
    const buildTocArr = (el, tocEntries = []) => {
        if(el == null) return

        let tocEntry = null
        
        if(el.tagName.toLowerCase() == 'section') {
            // if element is a branch then start to look at first leaf
            buildTocArr(el.children[0], tocEntries)
        }
        else if(el.tagName.toLowerCase().search('h[1-9]') == 0)  {
            // if element is a heading then make a new entry
            tocEntry = {
                level : parseInt(el.tagName.charAt(1)),
                text : el.textContent
            }
        } 

        // accumulate entries
        if(tocEntry != null) tocEntries.push(tocEntry)
        buildTocArr(el.nextElementSibling, tocEntries)

        return tocEntries
    }

    const tocLevelsArr = []
    const tocLevelsArrEntry = []

    buildTocArr(el).forEach(entry => {
        while(tocLevelsArrEntry.length < entry.level) tocLevelsArrEntry.push(0)
        tocLevelsArrEntry.length = entry.level
        tocLevelsArrEntry[entry.level-1]++
        tocLevelsArr.push(`${tocLevelsArrEntry.join('.')} ${entry.text}`)
    })

    const tocDiv = document.createElement('div');
    tocDiv.innerHTML = `<div>${tocLevelsArr.join('</div><div>')}</div>`
    return tocDiv
}

document.body.append(createTocDiv(el))
<section>
    <h1 class="pp">H1: A...</h1>
    <h2 class="pp">H2: B...</h2>
    <p class="pp">D...</p>
    <ul class="pp">
        <li class="pp">B...</li>
        <li class="pp">U...</li>
    </ul>
    <section>
        <h1 class="pp">H1: C...</h1>
        <section>
            <h2 class="pp">H2: D...</h2>
            <h3 class="pp">H3: E...</h3>
            <h2 class="pp">H2: F...</h2>
            <section>
                <section>
                    <h5>H3: G...</h5>
                </section>
            </section>
        </section>
    </section>
    <p class="pp">D...</p>
    <h2 class="pp">H2: H...</h2>
    <p class="pp">D...</p>
    <p class="pp">A...</p>
    <h2 class="pp">H2: I...</h2>
    <h9>test</h9>
    <p class="pp">N...</p>
    <h3>test</h3>
    <h3>test</h3>
    <h1>test</h1>
</section>
metatron
  • 896
  • 7
  • 14
  • I think that (specifically for beginners with JavaScript and DOM) your code would benefit from having added some comments. For example what `const isHeading = (secondChar != '' && !isNaN(secondChar))` is intended to do: Is it matching `/^H[1-9]$/i`? – U. Windl Jan 13 '23 at 07:32
  • 1
    I added some comments. My regex is rusty. I guess it does match the regex pattern. I don't like the double negation at !isNaN but unfortunatly JS does not provide an isNumeric function. – metatron Jan 13 '23 at 09:20
  • Edit to make it side effect free and removed global namespace pollution. – metatron Jan 13 '23 at 10:52
  • Is there a specific reason you are using recursion when processing the next sibling? (I've modified my original code to move from sequential processing (siblings) to full tree processing meanwhile, but I did it without recursion) – U. Windl Jan 13 '23 at 13:18
  • Recursion seems a good fit for when the mark-up of the page wouldn't be flat but in a tree structure. Recursion and trees tend to go well together. – metatron Jan 13 '23 at 14:21
  • But in fact you are doing end-recursion (your function returns after returning from the recursive call), which is equivalent to a loop. – U. Windl Jan 16 '23 at 07:07
  • Yes I used it as a "flat" loop. I edited the solution a bit just now so it can handle a tree structure (as an exercise for myself - it's not an answer anymore, abusing SO a bit :)). If you have any remarks about the code feel free to share. – metatron Jan 18 '23 at 16:54
0

Well, that's not exactly the solution I was looking for, but instead of copying the original numbers from the source rendering, one can create a duplicate (possible, because the generated TOC has the correct classes assigned).

The solution just adds some more CSS rules (and counters):

span.toc:not(:empty) { counter-reset: c_t1 0 c_t2 0 c_t3 0 c_t4 0; border-style: solid; border-width: thin; border-color: black; padding: 1ex; margin-top: 1em; margin-bottom: 1em; display: inline-block }
span.toc div.toc-H1:before { counter-reset: c_t2 0 c_t3 0 c_t4 0; counter-increment: c_t1; content: counter(c_t1)" " }
span.toc div.toc-H2:before { counter-reset: c_t3 0 c_t4 0; counter-increment: c_t2; content: counter(c_t1)"."counter(c_t2)" " }
span.toc div.toc-H3:before { counter-reset: c_t4 0; counter-increment: c_t3; content: counter(c_t1)"."counter(c_t2)"."counter(c_t3)" " }

The resulting TOC looks like this for the example:

Screenshot showing Table of Contents with numbering

U. Windl
  • 3,480
  • 26
  • 54
  • Not to be pedantic but you specifically asked for a Javascript solution. But I agree in a real life situation the css solution is more straightforward and more manageable. I guess we end up in one of those SO situations where your submission is not an answer to the original question but the solution is preferable. – metatron Jan 13 '23 at 09:05
  • Well, actually the OP asked whether it's *possible* to **copy** the numbers from `:before`; as that seems to be impossible, this seems the closest solution to me (creating the numbers in JavaScript instead would be also a different solution that *does not copy* the numbers crated by CSS). – U. Windl Jan 13 '23 at 13:07
  • Ah, I see, it's in the title. In your text you wrote: "Is there any way to have the same heading numbers in the TOC created by JavaScript as those automatically added by CSS rules?" which put me on the wrong foot. – metatron Jan 18 '23 at 16:50