Script cycle through different elements issue

Hi!

I’m struggling a bit with a script. What I’m after is a simple way to cycle through a couple of different views, like so:

demo

I found the functionality here. That script is a lot more complex than what I need though. I just want a simple variant were the first view display the first (of specified) non-empty field in it’s styled div element, along with an indicator of how many more there are and what number the current one is. With a shorcut the next should be shown, and so on.


I got the script below to work somewhat…

demo-2

It does not exclude a field if it’s empty though. I tried adding some conditionals using {{^…}} and {{#…}} but could not get it working. Also the indicators are just statically assigned now.

Is there an easy way to do this?


<script>
  const definition = document.getElementById('definition');
  const primaryDictionary = document.getElementById('primaryDictionary');
  const secondaryDictionary = document.getElementById('secondaryDictionary');

  const elements = [definition, primaryDictionary, secondaryDictionary];
  let currentIndex = 0;

  document.addEventListener('keydown', (event) => {
    if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
      currentIndex = (currentIndex + (event.key === 'ArrowRight' ? 1 : -1) + elements.length) % elements.length;
      elements.forEach((element, index) => {
        element.classList.toggle('active', index === currentIndex);
        const indicator = element.querySelector('.indicator');
        indicator.textContent = `${index + 1}/${elements.length}`;
      });
    }
  });
</script>

<div id="content">
  <div id="definition" class="active">
    <span class="indicator">1/3</span>
    {{Definition}}
  </div>
  <div id="primaryDictionary">
    <span class="indicator">2/3</span>
    {{PrimaryDictionary}}
  </div>
  <div id="secondaryDictionary">
    <span class="indicator">3/3</span>
    {{SecondaryDictionary}}
  </div>
</div>

<style>
  #content {
    text-align: center;
    position: relative;
  }

  #definition,
  #primaryDictionary,
  #secondaryDictionary {
    display: none;
    position: relative;
  }

  #definition.active,
  #primaryDictionary.active,
  #secondaryDictionary.active {
    display: inline-block;
  }
</style>

I am new to javascript myself. So take everything with a grain of salt.
I think the problem is, that you’re trying to cycle through an array declared as a const.
Which is a good way to get your mechanism work, don’t get me wrong here.
But this array is a const, where there always would have to be three values to cycle through, as you try to make it consist of “definition”, “primaryDictionary” and “secondaryDictionary”. But if you don’t want it to cycle through three values all the time, to declare it as a const, doesn’t make too much sense in this matter.

  1. I think it would be better to check first, if there and how many filled fields there are, to create an array from this. Then you can add the class “active” to the first item from the array. This array only consists of the fields, which are filled.
  2. Another of your problems is the fact, that you make your indicator a static. It also doesn’t make sense to declare the indicator as a const. It’s dynamic. It will change it’s count.
    (But this is another problem for later, as of now your “const indicator” does nothing in the webview itself. It’s just being declared and initialized.)
        const indicator = element.querySelector('.indicator');
        indicator.textContent = `${index + 1}/${elements.length}`;

You would have to count how many items in the array are, to make sure, which numbers the indicator should have.

Basically, here is your solution:

<div id="content">
  <div id="definition" class="count">{{Definition}}</div>
  <div id="primaryDictionary" class="count">{{PrimaryDictionary}}</div>
  <div id="secondaryDictionary" class="count">{{SecondaryDictionary}}</div>
</div>

<style>
  #content {
    text-align: center;
    position: relative;
  }

  #definition,
  #primaryDictionary,
  #secondaryDictionary {
    display: none;
    position: relative;
  }

  #definition.active,
  #primaryDictionary.active,
  #secondaryDictionary.active {
    display: inline-block;
  }
</style>

<script>

let counter = document.getElementsByClassName("count");
console.log(counter.length)
let gallarray = Array.from(counter);
console.log(typeof gallarray)
let indicarray = [];
let finalarray = [];


//Here he does check, how many fields are filled in with text.
for (i = 0, x = counter.length; i < x; i++)
{
	if (gallarray[i].childNodes.length > 0)
  {
  	indicarray.push(gallarray[i])
  	console.log("test completed")
  }
}

//Here he creates the indicator according to the count of the filled fields.

for (i = 0, x = indicarray.length; i < x; i++)
{
	  let indiccresec = document.createElement('span');
    indiccresec.setAttribute("class", "indicator")
    z = indiccresec.innerHTML = `${i+1}/${x}`
		idnext = indicarray[i].id
    u = document.getElementById(idnext).appendChild(indiccresec)
    indicarray[0].classList.add('active');
    finalarray.push(indicarray[i])
}

 
let currentIndex = 0;

  document.addEventListener('keydown', (event) => {
    if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
      currentIndex = (currentIndex + (event.key === 'ArrowRight' ? 1 : -1) + finalarray.length) % finalarray.length;
      finalarray.forEach((element, index) => {
        element.classList.toggle('active', index === currentIndex);
      });
    }
  });
</script>
1 Like

Thank you for taking the time to answer. You hit the nail on the head there, my approach was not really in line with the desired behaviour (it was just the closest I could get on my own given my lack of knowledge). Your suggestion looks a lot more logical.

After testing it out it seems to have solved the problem with empty fields and the indicator well. However, the script only seems to work for the first card reviewed in a session (with subsequent cards being empty).

I searched the forum a bit for why that might be, and found this post by @abdo that might be relevant?

I tried changing the script you provided to use var instead:

<script>
  var counter = document.getElementsByClassName("count");
  console.log(counter.length);
  var gallarray = Array.from(counter);
  console.log(typeof gallarray);
  var indicarray = [];
  var finalarray = [];

  for (var i = 0, x = counter.length; i < x; i++) {
    if (gallarray[i].childNodes.length > 0) {
      indicarray.push(gallarray[i]);
      console.log("test completed");
    }
  }

  for (var i = 0, x = indicarray.length; i < x; i++) {
    var indiccresec = document.createElement('span');
    indiccresec.setAttribute("class", "indicator");
    z = indiccresec.innerHTML = `${i + 1}/${x}`;
    idnext = indicarray[i].id;
    u = document.getElementById(idnext).appendChild(indiccresec);
    indicarray[0].classList.add('active');
    finalarray.push(indicarray[i]);
  }

  var currentIndex = 0;

  document.addEventListener('keydown', (event) => {
    if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
      currentIndex = (currentIndex + (event.key === 'ArrowRight' ? 1 : -1) + finalarray.length) % finalarray.length;
      finalarray.forEach((element, index) => {
        element.classList.toggle('active', index === currentIndex);
      });
    }
  });
</script>

This seems to have fixed subsequent cards being totally blank. Unfortunately it still has some persistance issues. For the first pass through cards behaves as aspected, but when they show up the second time the script no longer works.

I attached an example deck with just the script and three example cards that illustrates this, in case that makes things any easier (let me know if the link expires):

1 Like

Could you explain what you mean by “when they show up the second time”.

Like, after you press the “show answer” button. Then it doesn’t work.
Or do you mean like… when the card is shown again to you, after you reviewed it already?

Sorry for the language, I’m a bit tired and can’t think straight. Yes, I mean the next time the card shows up during the same review sessions after rating it once before.

I only tested it on a deck with three cards though (having either 1/2/3 fields with content), so it might be an issue even on the “first reveiew” if there are more cards in the deck with different amount of fields with content, I haven’t checked that.

Okay. I know now what your problem is.

Look: Anki has this thing, where it never reloads the whole dom, just certain parts of it.
That is why we have those persistence problems.
We have to make sure, that the whole dom is already loaded, before actually loading our function. Afterwards we don’t have the persistence problems.

Here is your solution:

  var counter = document.getElementsByClassName("count");
  console.log(counter.length);
  var gallarray = Array.from(counter);
  console.log(typeof gallarray);
  var indicarray = [];
  var finalarray = [];


function iteratefront()
{
  for (var i = 0, x = counter.length; i < x; i++) {
    if (gallarray[i].childNodes.length > 0) {
      indicarray.push(gallarray[i]);
      console.log("test completed");
    }
  }

  for (var i = 0, x = indicarray.length; i < x; i++) {
    var indiccresec = document.createElement('span');
    indiccresec.setAttribute("class", "indicator");
    z = indiccresec.innerHTML = `${i + 1}/${x}`;
    idnext = indicarray[i].id;
    u = document.getElementById(idnext).appendChild(indiccresec);
    indicarray[0].classList.add('active');
    finalarray.push(indicarray[i]);
  }

  var currentIndex = 0;

  document.addEventListener('keydown', (event) => {
    if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
      currentIndex = (currentIndex + (event.key === 'ArrowRight' ? 1 : -1) + finalarray.length) % finalarray.length;
      finalarray.forEach((element, index) => {
        element.classList.toggle('active', index === currentIndex);
      });
    }
  });
}

function WaitForLoad()
{
    if (document.readyState === "complete")
    {
        iteratefront();
				 console.log("Sucessfully loaded!")
    }
    else
    {
        setTimeout(WaitForLoad, 99);
    }
}

WaitForLoad()

This here makes sure, your function loads in, after the DOM is fully loaded.
No persistence issues anymore.
JavaScript and Anki man. Everything is a bit trial and error. But this makes a lot of fun!

function WaitForLoad()
{
    if (document.readyState === "complete")
    {
        iteratefront();
				 console.log("Sucessfully loaded!")
    }
    else
    {
        setTimeout(WaitForLoad, 99);
    }
}


2 Likes

I hope this helps. If your issue is still there, please let me know.

I did not know that, and I’m sure I’ll be using that again for other things. I just gave the script a quick test now and it seems to work, I’ll test it a bit more tomorrow just to make sure. Thank you for the help, very much appreciated :+1:

2 Likes