Context Bar to show Cloze note context dynamically

It works now!!! :partying_face:

Problem 1

Now I have this problem that after adding Headings to a card that already has had headings copied from a book, it freaks out and sends out my headings on the left flying.

Before:

After having added the heading TESTING in the middle of the card, observe the missing headings on the left

I return to the top of the card and see that the added heading with the other missing heading is sent flying to the right. These two headings remain there and do not remain in a sticky position like the headings in the context bar on the left.

What could be the problem? This only happens when I add my own headings to a card that has already had its own headings from the get go.

Problem 2

I changed the CSS. A second problem with that is that even though the highlight works, it works only for the front of the card and not the back (occasionally).

Here is an example

Front

Back

If it is relevant at all I am using a code to help auto scroll to the cloze

<script>
function runScriptTwice() {
  function scrollToCloze() {
    const element = document.getElementsByClassName('cloze')[0];
    const elementRect = element.getBoundingClientRect();
    const absoluteElementTop = elementRect.top + window.pageYOffset;
    const middle = absoluteElementTop - (window.innerHeight / 2);
    window.scrollTo(0, middle);
  }

  if (typeof onShownHook !== 'undefined') {
    // for Anki 2.1.x
    onShownHook.push(scrollToCloze);
  } else {
    // for AnkiDroid
    setTimeout(scrollToCloze, 10);
  }
}

// Call the function twice
runScriptTwice();
runScriptTwice();
runScriptTwice();
runScriptTwice();
</script>

If the highlight isn’t working, then there’s something going wrong in the highlightHeaderInView function. try the console.log technique:

Hard to say to what might be going wrong so maybe just add a console.log for each variable there are in the function:

  • headers
  • sideBarLinks
  • in the loop going through headers:
    • distance
  • after the loop closestHeader
  • in the loop going through sideBarLinks
    • listItem
    • link.getAttribute("href")

etc. Looking at all the variables should show where the process fails.

Tip: use console.log("variableName", variableName) so that you’ll see which printout is for which variable. With this, you might see, for example, "closestHeader", undefined printed out instead of just undefined which is certainly a lot more helpful.

Weird.

The highlight works normally for the front side of the card.
For the back side, it only works when I open up Inspect

Before:

After:


I went to the console and entered this. I don’t know what I am looking at :smile:

This seems to be normal
image

image

image

image

image

You add the console.logs to the javascript code in the card template, so like this:

<script>
// Function to highlight the header in view on the sidebar
function highlightHeaderInView() {
// Select all header elements in the content container
const headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
console.log("headers", headers);
// Select all links in the sidebar list
const sidebarLinks = document.querySelectorAll("#sidebar-list a");
console.log("sidebarLinks", sidebarLinks);

// Initialize variables to keep track of the closest header and its distance from the top
let closestHeader = null;
let closestHeaderDistance = Infinity;

// Loop through each header to find the one closest to the top of the viewport
headers.forEach(header => {
  // Calculate the distance from the top of the viewport to the header
  const distance = header.getBoundingClientRect().top;
  console.log("distance", distance);
  // If this distance is the smallest positive value so far, update closestHeader and closestHeaderDistance
  if (distance < closestHeaderDistance && distance >= 0) {
    closestHeader = header;
    closestHeaderDistance = distance;
  }
});

console.log("closestHeader", closestHeader);

// If a closest header is found, proceed to highlight the corresponding sidebar link
if (closestHeader) {
  // Loop through each link in the sidebar
  sidebarLinks.forEach(link => {
    // Get the parent <li> element of the link
    const listItem = link.parentElement;
    console.log("listItem", listItem);
    console.log("link.getAttribute(href)", link.getAttribute("href"));
    // Check if the link's href matches the id of the closest header
    if (link.getAttribute("href") === "#" + closestHeader.id) {
      // If it matches, add the 'active-header' class to highlight it
      listItem.classList.add("active-header");
    } else {
      // If it doesn't match, remove the 'active-header' class
      listItem.classList.remove("active-header");
    }
  });
}
}

// Attach the scroll event listener, prevent adding it multiple times on desktop
var addEventListenerAdded;
if (!addEventListenerAdded) {
  window.addEventListener("scroll", highlightHeaderInView);
  addEventListenerAdded = true;
}

I think I underrstand the issue but correct me if I am wrong.

What is persistent is that the highlight ONLY shows up after scrolling on the back side, sometimes as well on the front side of the card. So I think that is the crux of the issue.

It is only activated by

  • inspecting
  • or scrolling (for the backside of the card).

It should be only based on the position of the scroll, not the act of scrolling itself.

I think I am borderline tech illiterate or am just slow.

I put it in the code in the console and this is what I see

Yeah, that’s what addEventListener("scroll", highlightHeaderInView ) does, it triggers highlightHeaderInView to be executed on scrolling happening. You could additionally run the function separately at the end like this, so it gets executed once upon first rendering the card front or back:

// Attach the scroll event listener, prevent adding it multiple times on desktop
var addEventListenerAdded;
if (!addEventListenerAdded) {
  window.addEventListener("scroll", highlightHeaderInView);
  addEventListenerAdded = true;
}

// Also highlight the header once on initially rendering the card
highlightHeaderInView();

No, don’t put it in the console, edit your card template :slight_smile:

I changed the code in the card template as you said and also changed this part

// Attach the scroll event listener, prevent adding it multiple times on desktop
var addEventListenerAdded;
if (!addEventListenerAdded) {
  window.addEventListener("scroll", highlightHeaderInView);
  addEventListenerAdded = true;
}

// Also highlight the header once on initially rendering the card
highlightHeaderInView();

It still requires me to scroll all the way to the header or open up inspect for it to be activated.


Hmm now that I open up inspect I see an error now

When I click on the error it has showed me this huge list of distances and I found this
image
image

In some other cards I see these errors. I think this is the more relevant error as this is the error that shows up for cards that do indeed have this highlight activation problem. I do not find this error with cards where the highlight is activated as intended without me having to scroll

image
image
image

The “Failed to load resource” error doesn’t look related to the code in the template. The appendChild errors however are. Whenever an error occurs in javascript in the card template, all subsequent javascript is not executed at all. This is probably the cause for why it works sometimes - an error happens and the function doesn’t finish executing, so no highlighting happens.

Update your card template with this edited version of populateSideBar where I added some error checking for currentList not being defined.

<script>
  // Populate the sidebar with <li> elements
  function populateSidebar() {
    const sidebarList = document.getElementById("sidebar-list");
    const headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
    // Duplicate the header structure in the sidebar as a nested <ul> list
      let currentList = sidebarList;
      let previousLevel = 1;
    function addHeader(header) {
      if (!currentList) {
        return;
      }
      const level = parseInt(header.tagName[1]);
      const listItem = document.createElement("li");
      const link = document.createElement("a");
      link.textContent = header.textContent;
      // If the header doesn't have an id, assign it the text content
      if (!header.id) {
        header.id = header.textContent;
      }
      link.href = "#" + header.id;
      listItem.appendChild(link);
      if (level > previousLevel) {
        const newList = document.createElement("ul");
        currentList.appendChild(newList);
        currentList = newList;
      } else if (level < previousLevel) {
        for (let i = 0; i < previousLevel - level; i++) {
        currentList = currentList.parentElement.parentElement;
        }
      }
      currentList?.appendChild(listItem);
      previousLevel = level;
    }
    headers.forEach(addHeader);
  }


  populateSidebar();
</script>

I actually missed this part completely. Yes, this might be relevant. Calling window.scrollTo should trigger the “scroll” eventListener added but not if the eventListener hasn’t been added yet. Just make sure that this scrollToCloze
script is after the highlightHeaderInView script in your card template. That way it’s ensured that the “scroll” eventListener is added before scrollToCloze is called.

I replaced the populateSidebar code and changed the order of the scrolltoCloze code to be after the code for the sidebar.

The problem has become a bit better but there is still instances where the highlight is not even activated without first scrolling to the header.

I think those instances are when I reach the last header
The Context Sidebar seems to skip to the next header upon reaching a header.
Look at the example down below.

The heading I have here is Stratum pigmentosum.
which seems to be shown correctly on the sidebar

However the moment I scroll under the header so that it does not show up anymore, the highlight shifts directly to the next header. Is this an expected behaviour :question:

So by the time I reach the text under the last header, it is not highlighting anything since it has skipped to the next header (which is not existent in this case)

Indeed, now that I actually examine the code the AI made in highlightHeaderInView what it’s doing is checking the distance of headers from the top edge of the page downward. So, once you scroll down enough for a header to cross the top edge and go out of view, the next header below is detected as the new closest one.

The logic is then actually the reverse of what it should be. It should be checking whether the header has gone above the top edge.

I asked Github copilot to fix the code again.

Updated highlightHeaderInView script

  • The new code didn’t change much, just flipped the distance check for for negative distance (which is then distance above the page top edge.
  • An exception is made when no header is above the top edge, which would be the case right in the beginning before you’ve scrolled down enough. In that case the first header is considered active.
  • I guessed that this would make it so that the last header can never never active, if you can’t scroll down enough to make it go out of view. It seems Wikipedia’s sidebard has this flaw and they didn’t bother fixing it. Though it should be fixable, just check whether the scroll position is the very bottom.
    • Not sure if window.innerHeight + window.scrollY >= document.body.offsetHeight - 10 will work in the Anki reviewer.
  • I checked that Wikipedia makes the header active a bit before it crosses the top edge. So the distance check should actually allow the header to close to or past to the top edge to be considered active.
First prompt I used

The code in highlightHeaderInView is not highlighting the header in the desired way. Currently, the highlighted header is the closest one below the viewport top edge. Instead, the highlighted header should be the one that

  • has crossed (and is above) the viewport top and is out of view
  • or in the case where no header has yet been scrolled past, the active header is the first one.

Fix the code in highlightHeaderInView to implement this correct functionality.

Second prompt

In the updated code, the last header can never be active because you likely can’t scroll down enough to make it go past the viewport top. What would be the options to fix this?

Third prompt

Make it so that the header doesn’t need to all the way past the viewport top but is considered active once it’s close to it, say, 10% of the viewport height below the viewport top or higher.

<script>
// Function to highlight the header in view on the sidebar
function highlightHeaderInView() {
  // Select all header elements in the content container
  const headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
  // Select all links in the sidebar list
  const sidebarLinks = document.querySelectorAll("#sidebar-list a");

  // Initialize variables to keep track of the closest header and its distance from the top
  let closestHeader = null;
  let closestHeaderDistance = -Infinity; // Tracking the largest negative distance

  // Calculate 10% of the viewport height
  const viewportHeightThreshold = window.innerHeight * 0.1;

  // Loop through each header to find the one closest to the top of the viewport, from above
  headers.forEach(header => {
    const distance = header.getBoundingClientRect().top;


    // Update to find the header with the smallest distance less than the threshold
    if (distance < viewportHeightThreshold && distance > closestHeaderDistance) {
      closestHeader = header;
      closestHeaderDistance = distance;
    }
  });

  // If no header has been scrolled past, select the first one
  if (!closestHeader && headers.length > 0) {
    closestHeader = headers[0];
  }


  // If a closest header is found, proceed to highlight the corresponding sidebar link
  if (closestHeader) {
    // Loop through each link in the sidebar
    sidebarLinks.forEach(link => {
      // Get the parent <li> element of the link
      const listItem = link.parentElement;
      // Check if the link's href matches the id of the closest header
      if (link.getAttribute("href") === "#" + closestHeader.id) {
        // If it matches, add the 'active-header' class to highlight it
        listItem.classList.add("active-header");
      } else {
        // If it doesn't match, remove the 'active-header' class
        listItem.classList.remove("active-header");
      }
    });
  }
}

// Attach the scroll event listener, prevent adding it multiple times on desktop
var addEventListenerAdded;
if (!addEventListenerAdded) {
  window.addEventListener("scroll", highlightHeaderInView);
  addEventListenerAdded = true;
}


// Also highlight the header once on initially rendering the card
highlightHeaderInView();

</script>
1 Like

Yes now it makes much more sense now!!! So far it is working just fine. Thank you!!! :partying_face: :pray:


Could you please explain to me this weird behaviour

For some weird reason, sometimes headers in my card are not shown in the Context sidebar in a sticky position, but rather instead shown on the top right of the document and not in a sticky position when scrolling

Look to the right.

Now after scrolling down.

This also occurs when I sometimes try to add headers of my own to a card that already has had its own headers when I copied it.
That does not happen with a card without headers.

Ah, I think I know now.

The code is assuming that you are strictly setting headers in the correct structure.

See the Headings are important section here:

<h1> headings should be used for main headings, followed by <h2> headings, then the less important <h3> , and so on.

Problems occur, if the previous heading was an h2 and you add an h4. Problems also occur, if the first heading isn’t an h1 and you add an h1 somewhere.

The code in populateSideBar that does currentList = currentList.parentElement.parentElement is the problem. It’s going up two levels in the <ul> structure and ending up outside the sidebar when the heading structure is improper.

Updated populateSideBar with some error checking

  • In case the heading structure does not follow the proper order, this should at least stop it from adding stuff outside the sidebar
<script>
// Populate the sidebar with <li> elements
function populateSidebar() {
  const sidebarList = document.getElementById("sidebar-list");
  const headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
  // Duplicate the header structure in the sidebar as a nested <ul> list
  let currentList = sidebarList;
  let previousLevel = 1;
  function addHeader(header) {
    if (!currentList) {
      return;
    }
    const level = parseInt(header.tagName[1]);
    const listItem = document.createElement("li");
    const link = document.createElement("a");
    link.textContent = header.textContent;
    // If the header doesn't have an id, assign it the text content
    if (!header.id) {
      header.id = header.textContent;
    }
    link.href = "#" + header.id;
    listItem.appendChild(link);
    if (level > previousLevel) {
      const newList = document.createElement("ul");
      currentList.appendChild(newList);
      currentList = newList;
    } else if (level < previousLevel) {
      for (let i = 0; i < previousLevel - level; i++) {
        // We want to get currentList.parentElement.parentElement but not go beyond the top level
        // which is sideBarList
        currentList = currentList.parentElement
        if (currentList.id !== "sidebar-list") {
          currentList = currentList.parentElement
        }
      }
    }
    currentList?.appendChild(listItem);
    previousLevel = level;
  }
  headers.forEach(addHeader);
}


populateSidebar();
</script>
1 Like

The problem is partly fixed now

For example now here is a heading in the middle of the card.

After adding a Testing heading, it now changes the sidebar correctly (dont know why it is much more indented to the left but oh well)

The code still freaks out when I suddenly put an h1 header under some h2 or 3 header etc


And when I go to the end of the card
and a add a Testing header of my own, it does not show up on the sidebar

But rather again on the top right when I scroll back up.

Yeah, try to make your own additional headers follow maximum size in the existing content: if the biggest header is h2, then don’t add any h1s.

One last bug in the populateSideBar code. This part

} else if (level < previousLevel) {

changed to this

} else if (level < previousLevel && currentList.id !== "sidebar-list") {

Should finally prevent it from adding stuff outside the sidebar

Now it went to the very bottom left and it stays there :sweat_smile:


Yeah, try to make your own additional headers follow maximum size in the existing content: if the biggest header is h2, then don’t add any h1s.

Hmm but the maximum size is actually h1 and some h1 have h2 und h3 subheadings. Sometimes I do want to add h1 above these h2 und h3s to sepereate them from their original h1.

For example, I am under the h3 heading Fundus und Korpus now. It has two h4 headings (Hauptzellen , Belegzellen).

Now I want these to be put under a completely different main heading.

I did so by putting the new h1 heading so that it starts off a completely new branch und seperating the h4 headings from the old h3 heading (Fundus und Korpus).

Now the h3 heading remains and the h4 headings are stripped off of it. The new h1 heading with the stripped of h4 headings are thrown back to the top right again.

Do you think that the cause could probably be that there is a job between the new h1 und the old h4 (like they should be h2s instead).

Edit: I tried to change them to h2s and then adding the new h1 and still freaks out a lot more.

That’s really weird. I can’t see how it would be putting stuff outside the sidebar-list element now. Try use the console.log technique again (editing the highlightHeaderView code your card template) and check what’s going on with currentList especially

Here


  function addHeader(header) {
    console.log("currentList initial", currentList);
    if (!currentList) {
      return;
    }

And

      for (let i = 0; i < previousLevel - level; i++) {
        console.log("currentList before going up", i, currentList)
        // We want to get currentList.parentElement.parentElement but not go beyond the top level
        // which is sideBarList
        currentList = currentList.parentElement
        if (currentList.id !== "sidebar-list") {
          currentList = currentList.parentElement
        }
       console.log("currentList after going up", i,  currentList)

Where in this code exactly
 :question: :sweat_smile: :pray:

<script>
// Function to highlight the header in view on the sidebar
function highlightHeaderInView() {
  // Select all header elements in the content container
  const headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
  // Select all links in the sidebar list
  const sidebarLinks = document.querySelectorAll("#sidebar-list a");

  // Initialize variables to keep track of the closest header and its distance from the top
  let closestHeader = null;
  let closestHeaderDistance = -Infinity; // Tracking the largest negative distance

  // Calculate 10% of the viewport height
  const viewportHeightThreshold = window.innerHeight * 0.1;

  // Loop through each header to find the one closest to the top of the viewport, from above
  headers.forEach(header => {
    const distance = header.getBoundingClientRect().top;


    // Update to find the header with the smallest distance less than the threshold
    if (distance < viewportHeightThreshold && distance > closestHeaderDistance) {
      closestHeader = header;
      closestHeaderDistance = distance;
    }
  });

  // If no header has been scrolled past, select the first one
  if (!closestHeader && headers.length > 0) {
    closestHeader = headers[0];
  }


  // If a closest header is found, proceed to highlight the corresponding sidebar link
  if (closestHeader) {
    // Loop through each link in the sidebar
    sidebarLinks.forEach(link => {
      // Get the parent <li> element of the link
      const listItem = link.parentElement;
      // Check if the link's href matches the id of the closest header
      if (link.getAttribute("href") === "#" + closestHeader.id) {
        // If it matches, add the 'active-header' class to highlight it
        listItem.classList.add("active-header");
      } else {
        // If it doesn't match, remove the 'active-header' class
        listItem.classList.remove("active-header");
      }
    });
  }
}

// Attach the scroll event listener, prevent adding it multiple times on desktop
var addEventListenerAdded;
if (!addEventListenerAdded) {
  window.addEventListener("scroll", highlightHeaderInView);
  addEventListenerAdded = true;
}


// Also highlight the header once on initially rendering the card
highlightHeaderInView();

</script>