Context Bar to show Cloze note context dynamically

For longer cloze notes, it can be a bit tedious to figure out the general context of single cards. For that I figured a good solution would be an on-screen context bar that

  • shows where you are and

  • changes depends where you scroll to. (Kind of like the context bar on the left in Wikepdia)

  • be also editable during Review (like the Edit during Review Cloze addon)

Does there exist something like this anywhere for Anki :question:

1 Like

Tooltips might work here as well: https://ankiweb.net/shared/info/1840818335

2 Likes

1-I changed the shortcut but now it just keeps on repeating the word over and over again like you see here in white

image


2-Also, every time I am using the shortcut, the cursor just disappears and suddenly I can’t edit any more text at all.


3-Also how can I get this to work with Edit during Review Cloze addon, because the usage where it would be really very helpful :pray:

I think the sidebar scrolling functionality is dependent on using a specific structure in the content. On Wikipedia it’s like this:

  • Each section has a <h3> with a unique id attribute
  • The header is duplicated in the sidebar and it links to the header.
  • When scrolling past a specific point the sidebar header gets added a vector-toc-list-item-active class

The <h3> in the content

In the sidebar when active

In action

tUJvjz9eHj

So, to make the sidebar your content needs some structure in it that defines what “stopping points” what the sidebar should link to. Either you

  • manually add some <h1> headers at places so you have specific structure defining the sidebar. Lots of manual work required, unless your content already happens to be favorably structured.
  • or use some hacky technique roughly like get the first 8 words of the start of each paragraph and use those as headers which’ll produce undesirable results at times. Would work without editing your cloze notes but will be messy.

This seems to be way more complicated than I thought…

Here is what a typical note of mine looks like. In this case the main heading is Calcium, the two subheadings are Hyperkaliame and Hypokaliame. I think duplicating first of all linking all the headers to the sidebar would be a good start

Ah, yeah you even have different level headings so you need a proper tree structure. Well, with the help of Copilot I made this code which might work:

Change your card Front and Back to this structure

Your headers could be part of the card template or in the {{Field}} values, it shouldn’t matter.

<div class="container">
  <div class="sidebar">
    <ul id="sidebar-list">
      <!-- Sidebar will be populated by running populateSideBar() -->
    </ul>
  </div>
  <div class="content-container">
      <!-- Your previous card content goes here -->
    </div>
  </div>
</div>

Additional styling for the new structure

.container {
  display: flex;
}
.sidebar {
  // Adjust sidebar width as needed
  width: 200px;
  // width: 20vw;
  // Styling the border between sidebar and content
  border-right: 1px solid black;
  // Adjust padding as you like
  padding: 10px;
}
.content-container {
  flex-grow: 1;
  // Adjust padding as you like
  padding: 10px;
}

Script for populating a sidebar using the <h1>, <h2> etc. in your card content, used in Front and Back


<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;
    headers.forEach((header) => {
      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;
    });
  }


  populateSidebar();
</script>

Alternative to simply doing populateSidebar();

Use in case you need to wait for the card to fully render. Probably not needed

  // Initial setup, prevent multiple adds on desktop
  var loadListenerAdded = false;
  if (!loadListenerAdded) {
    window.addEventListener("load", () => {
      populateSidebar();
    });
    loadListenerAdded = true;
  }
1 Like

Hmm there doesnt seem to be any change…I have also added the CSS styling to the styling tab

The idea was that you copy-paste your current card content to where it says <!-- Your previous card content goes here -->

1 Like

YES!! Now it works. But how could i keep it to be sticky the entire time. I keep scrolling and the headings keep going upwards

(While also keeping in mind that the list of subheadings is very long as you can see, so something like a scroll inside a scroll would be nice, idk how that would work ;P, kind of like how Wikipedia does it)

The css for making things sticky is in fact position: sticky (also position: absolute). Adding a scrollbar to an element is controlled with overflow

I think editing the css for .sidebar like this will make it sticky and separately scrollable:

.sidebar {
  // Make sidebar sticky
  position: sticky;
  // Position to sticky is to top left edge
  top: 0;
  left: 0;
  // Make the sidebar vertically scrollable
  // Max height needed so that the element cuts the content off
  max-height: 100vh;
  overflow-y: auto;
  // Adjust sidebar width as needed
  width: 200px;
  // width: 20vw;
  // Styling the border between sidebar and content
  border-right: 1px solid black;
  // Adjust padding as you like
  padding: 10px;
}

Edit: I remembered you need to set max-height to ensure an element will cut content off (so the content overflows) and a scrollbar is shown.

1 Like

Hmmm it did not work. It remained as it is


I tried something and I noticed that the class .sidebar is not even working. I changed the width to something absurd or the padding or tried playing with in general, and no changes were observed at all

I tried something and I noticed that the class .sidebar is not even working.

Hmm, something’s going wrong with the css targeting then. If you use inspect on the sidebar and edit the element.style to set the position: sticky etc. on it, does it then work? Also, inspecting it should tell you whether the div has the sidebar class.

Well it has the sidebar class alright, but if I look downward here on the style part I see everything crossed through :confused:

Ah, right. It’s broken because of the comment syntax. I keep forgetting you can’t use // for comments, needs to be /* */. I’m used to writing scss where // is allowed.

.sidebar {
  /* Make sidebar sticky */
  position: sticky;
  /* Position to sticky is to top left edge */
  top: 0;
  left: 0;
  /* Make the sidebar vertically scrollable */
  /* Max height needed so that the element cuts the content off */
  max-height: 100vh;
  overflow-y: auto;
  /* Adjust sidebar width as needed */
  width: 200px;
  /* vw  is width relative to window width */
  /* width: 20vw; */
  /* Styling the border between sidebar and content */
  border-right: 1px solid black;
  /* Adjust padding as you like */
  padding: 10px;
}
1 Like

It works beautifully now!!! :smiley:

I have two other asks:

  • Do you think you could get the subheading or heading be highlighted depending on where I am scrolling on the right like Wikipedia does it, and in turn, the sidebar on the left should also automatically scroll to the highlighted heading or subheading.

  • This may be too much, but do you think there is an alternative way of adding headings to the card using the Edit Card during Review (Cloze) addon for them to show up in the sidebar. I like to use this addon, but the problem is that it doesnt allow for formatting text into h1, h2 or h3…

  • (sometimes, i think I would like an image like a flowchart to remain showing all the time for orientation, but i dont think that would work as with an h1 formatting…idk :P)

I used Github Copilot to generate this using the below prompt where I pasted the previous code along with some instructions on what to do. You could use this as an example on how to get code from ChatGPT in the future.

Prompt

Assuming a html structure like below:

<div class="container">
  <div class="sidebar">
    <ul id="sidebar-list">
      <!-- Sidebar will be populated by running populateSideBar() -->
    </ul>
  </div>
  <div class="content-container">
      <!-- Arbitrary content that contains h1, h2, h3 etc. headers at places -->
    </div>
  </div>
</div>

And populateSideBar being run once the html is loaded like this:

<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;
    headers.forEach((header) => {
      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;
    });
  }


  populateSidebar();
</script>

Write a function highlightHeaderInView used in window.addEventListener("scroll", highlightHeaderInView) that will

  • identify the header element that is closest to the viewport top (the current “active” header)
  • add the css class active-header to the <li> corresponding to that header
  • remove the active-header class from any other <li> element
    Include comments in the code explaining each step so that a javascript beginner could understand how it works.

JS to update sidebar on scrolling

The event listener part at the end was edited by me though. I figured it’d be hard to explain the weirdness of the desktop Anki JS environment that requires this addition to avoid buggy behaviour.

<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;

// 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;
  // If this distance is the smallest positive value so far, update closestHeader and closestHeaderDistance
  if (distance < closestHeaderDistance && distance >= 0) {
    closestHeader = header;
    closestHeaderDistance = distance;
  }
});

// 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;
}

Additional css to the style the active-header

/* Styling for the active header */
.sidebar-list .active-header {
  font-weight: bold;
} 

Debugging

I didn’t test this code, so please try use inspect to debug it (you should be seeing class="active-header" getting added to a <li>. To debug the code itself, you can add console.log(...) calls for variables to see what’s up. For example, you could do

 const headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
 console.log(headers)

To check whether it’s correctly getting the headers in the card.

1 Like

I indeed see an active-header class that is changing with the header as I scroll up or down. However the header on the left is not highlighted in any form at all. No styling is changed

Is the additional styling for .sidebar-list .active-header working? Maybe change the targeter to .sidebar-list li temporarily. That should style every <li> in the list

I changed it, yet I cannot seem to find any changes to the styling at all
image

:confused:

Ah, I see the mistake in the code. I added the sidebar-list as the element id= and not class=
image

CSS syntax tips:

  • .sidebar-list = targets a class
  • #sidebar-list = targets an id

So, either

  • keep the current html, change the css to do #sidebar-list
  • keep the current css, change the html to do <ul class="sidebar-list" >
1 Like