-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

4
  • 1
    Can you provide a minimal example (with code)? const styles = window.getComputedStyle(element, '::before'); should do the trick. Commented Jan 12, 2023 at 12:32
  • Well, that reports CSS2Properties(347) with 347 lines of properties; isn't really minimal ;-) Commented Jan 12, 2023 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 stackoverflow.com/a/55258061 Commented Jan 12, 2023 at 15:31
  • Hi, I had some time to kill and made a new solution including numbering. See answer below. Commented Jan 12, 2023 at 19:57

3 Answers 3

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>

Sign up to request clarification or add additional context in comments.

1 Comment

I reread your question. I guess you need the numbering too (1, 1.1, etc…) in front of the toc items
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

3 Comments

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.
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).
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.
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>

7 Comments

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?
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.
Edit to make it side effect free and removed global namespace pollution.
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)
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.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.