Cloze one by one uncovering

@kleinerpirat
There is a problem with Mathjax. I can’t have mathjax equation, and no correct latex equation too (the size is not normal).
Mathjax is correctly displayed in clozes other than the cloze that is running. (first 1) to 5) in the image) But is not displayed for the clozes that are running (last 1) to 5) in the image) .

Your solution seems to be the most effective for learning vs the Anking card because the clozes are always of the same shape. There is no little box that I will know the answer or too big equations that are not completely covered. And as you said in some post, there is no duplication of code for each cloze.
But without a way to write mathematics symbol, I can’t use it.
Do you think it is possible to add mathjax to your card ?

If I can help, I found this part of code on the back of 'Enhanced Cloze" (for Anki 2.1) card type.

// rerun mathjax on the document so that the cloze text gets formatted
// … for MathJax 2
try {
MathJax.Hub.Queue([“Typeset”, MathJax.Hub]);
} catch {}
// … for MathJax 3
try {
MathJax.typesetPromise()
} catch {}
The template can be found here https://ankiweb.net/shared/info/1990296174

Does the problem of multiplying lines that you identified is still there ?

Extract from the add-on page:

For each cloze you add this add-on copies the whole text. So if you paste 1000 words into the first field of an enhanced cloze note and do 20 clozes the note will have ultimately 21000 words. If you do this often the size of your database will increase substantially.

That alone is a big no-go

@kleinerpirat I’ve been using a closet note type that you helped set up some time ago, and I’d like your help again please.


I want to add a keyboard shortcut that would incrementally reveal the [[cl::]] and [[cx::]] type clozes. I looked into the “Reveal Clozes with Key Presses” script on the closet website, but it allows the clozes to reveal only with the keyboard shortcut, and not on clicking, which makes reviewing impossible on the mobile apps. Plus it’s not been updated since the major closet update which separated the styling and scripting, so I can’t make it work with the current closet template anway; or make enough sense of it to integrate into the click to reveal script, with my almost nil JS knowledge.

I also tried modifying the Anking one-by-one script, by making it target the closet cloze classes instead of the default cloze class, but it fails to work, maybe because it’s script executes before/after closet, not sure though.

Here's the code I'd added to the "Closet Setup" script for the cl and cx tags to work:
/** Click to reveal cloze */

const removeObscure = function(event) {
  if (event.currentTarget.classList.contains('cl--obscure-clickable')) {
    event.currentTarget.classList.remove('cl--obscure')
    event.currentTarget.classList.remove('cl--obscure-hint')
    event.currentTarget.classList.remove('cl--obscure-fix')
  }
}

const wrappedClozeShow = closet.wrappers.aftermath(closet.flashcard.recipes.cloze.show, () => {
  document.querySelectorAll('.cl--obscure')
  .forEach(tag => {
    tag.addEventListener('click', removeObscure, {
      once: true,
    })
  })
})

const obscureAndClick = (t) => {
  return [`<span class="cl--obscure cl--obscure-hint cl--obscure-clickable">${t.values[0]}</span>`]
}

const obscureAndClickFix = (t) => {
  return [`<span class="cl--obscure cl--obscure-fix cl--obscure-clickable"><span>${t.values[0]}</span></span>`]
}

const frontStylizer = closet.Stylizer.make({
  processor: v => `<span style="color: var(--cloze-color)">${v}</span>`,
})

filterManager.install(
  wrappedClozeShow({
    tagname: 'cl',
    frontEllipser: obscureAndClick,
    frontStylizer: frontStylizer,
  }),

  wrappedClozeShow({
    tagname: 'cx',
    frontEllipser: obscureAndClickFix,
    frontStylizer: frontStylizer,
  }),
)

You spoke of your note template that can do it all? If it does support both key and click revealing incremental clozes, I’d like to see it.

(I think this in on topic for the thread, let me know if it is not though.)

I created a brief guide about user input, as I think a lot of questions in this forum could be prevented if more people knew how easy this stuff is in reality (programming is not black magic) - and I’m confident you’re more than able to learn these JS basics @shallash :wink:

That’s my private note type (or “study solution”, as it also uses a companion add-on), which I’ve been working on for 1½ years now. It bends the concept of the Anki note a bit and writing a guide for how to use it is not in my capacity I’m afraid.

I’m planning to release it to the public eventually, but currently, it is still way too complex and specific for general use. That’s why I like to create these “baby note types” for the forum, which contain isolated features, so others may benefit from the cool things I’m adding to my private note type.

Consider me very intrigued. :slightly_smiling_face:

I was able to solve the issue after reading the guide, and a few hours of stumbling around, and here’s what i came up with, for anyone interested in using clozes that reveal incrementally on keypress, and also reveal on clicking/tapping them, with Closet clozes.

The Script

In the end I went with a quite different approach from the script I found on closetengine.com . Mostly because I could not make head or tails of it.

if (typeof doc_keyUp !== "function") {
  var doc_keyUp = function(e) {
    if (e.key === "z" || e.code === "Numpad0") {
      flipVisibility();
    } else if (e.code === "NumpadDecimal") {
      removeObscureOnKd();
    }
  };
  document.addEventListener("keyup", doc_keyUp, false);
}

function flipVisibility() {
  if (document.getElementById('hintzu').style.display == 'none') {
    document.getElementById('hintzu').style.display = 'block';
  } else {
    document.getElementById('hintzu').style.display = 'none';
  }
}

function removeObscureOnKd() {
  var firstCloze = document.getElementsByClassName('cl--obscure')[0];
  firstCloze.classList.remove('cl--obscure')
  firstCloze.classList.remove('cl--obscure-hint')
  firstCloze.classList.remove('cl--obscure-fix')
}

This script should work fine if you’re using the closet setup script found in my previous post on this thread.

The relevant function here is removeObscureOnKd, so you can change the keyboard shortcut to whatever you want to using https://keyjs.dev .

The flipVisibility function is for a button to toggle the visibility of some content, it functions basically like the <details> tag, except with a keyboard shortcut too. If someone wants i’ll post how to make it work for your notetype. I used one eventlistener for both functions because it seems efficent to do so.

3 Likes

I updated the note type: [Forum] Incremental Cloze Note Type - Sample Note - AnkiWeb.


Touch controls have been implemented:

  • Two buttons on the bottom right (for thumb control on mobile)
  • Touch input on the cloze text advances the reveal process too
  • The keyboard shortcuts are easily customizable (look for var shortcuts in the template)

Done.

Assuming you mean handling a card like native Anki cards if it only has a single cloze, that is implemented too.

The new touch controls make this usable on AnkiDroid and AnkiMobile. However, MathJax is only supported on AnkiMobile, because AnkiDroid doesn’t expose MathJax (or I haven’t found the right way).

Edit: I just tested again and MathJax actually works on AnkiDroid :neutral_face: A bit too tired I guess :sweat_smile:

The note type now supports fully wrapped MathJax equations. You can also use MathJax for hints. The incremental reveal of individual variables within an equation is an entirely different beast though and I don’t know if it’s possible tbh.

3 Likes

Whoa ! You rock !
Math seems to be correctly displayed.
It’s easy to reveal one by one, touching anywhere .
The buttons behave correctly. But when I study a long chapter, I need to scroll to the end to find it. Anyway, as I can touch the screen anywhere it works.
One thing is disturbing me. The text appear centered. But I want to read like in a book. I tried to modify the “align” property, but that didn’t do anything. I suspect it is hiding inside boxes, but I don’t know how to reach them.
Could you make this property visible, so one could have the choice to display left or centered ?
Oh, and maybe is it possible to make the card appear at once ? without the side “Incremental reveal. Flip to backside” ?

That’s really super cool of you ! I was afraid of card taking too much memory on ankiweb, and the anking card is great but not so elegant as yours. :wink:

I was thinking about merging the functionality of this note type with that one: Anki as a knowledge base (with a "massive cloze note") - #39 by kleinerpirat

Then you’d have auto-scroll and lots of other nice possibilities.

I think that’s because of the giant button I wrapped the cloze text with (for iOS tap-support, because AnkiMobile blocks click events from all elements other than <button>). By default, text inside buttons is centered, but that can be overwritten with !important. I personally don’t like centered text either, so I’ll update that tomorrow.

This is not possible without an Add-on, I’m afraid. Native clozes are converted before I can access them from the template - and on the front side, they’re lacking the information needed for a reveal. So we’re stuck with the back side. (Ideally, the reveal should happen on the front and then automatically switch to the backside once everything has been revealed.)

With the use of Python commands, one could probably achieve that, but that’s not a cross-platform solution.

AnkiDroid offers a JavaScript API that allows for such things, but the rest of the Anki ecosystem doesn’t…

Amazing! Like it a lot, thank you for implementing those things in a day! :slight_smile:
I played around abit. At first the reveal button were at the bottom of my cards. And since I don’t use the reveal-all function I deleted that one as well. At first there was something odd: The Text of the whole card was pasted onto a button next to the Reveal-Next/Reveal-All Button, but nothing happened when I clicked it. I also got an error message saying that {{#Back Extra}} wasn’t working or something. But after trying out what to delete and what not, I managed it^^
Big thank you :slight_smile:

P.S. it was more tricky than I thought since I wanted to implement it into another template. This is probably why things got a bit complicated^^

I think there is a way if you want for the card to appear at once without the side “Incremental reveal. Flip to backside”, like if you use basic note type and in front side you can reveal one by one and if you want to reveal all at once press normal show answer button that go to back side which all clozes get revealed, but if there only one cloze from the start => disable keys and touch response so you can show the answer only by click show answer
Also: that will have only c1 clozes (and if you want c2 c3 you should do that manually with adding another new notes with different c1 clozes , I think that more user friendly when reviewing cards, despite of the small effort for the way to add c2 c3 clozes By copy and paste and put different c1 clozes.

Sleeping on it, I think one needs a “previous” button. Something to reverse any mistake while clicking too fast. Or something to reach the last clozes by revealing all and then click a few times on “previous”.

[quote=“mathieu993, post:18, topic:12584”]
But when I study a long chapter, I need to scroll to the end to find it.
[/quote]
I was thinking about merging the functionality of this note type with that one: Anki as a knowledge base (with a “massive cloze note”) - #39 by kleinerpirat

Is auto-scroll not for “massive cloze note” ? Here I need to read everything, starting from the start. and throughout the text.

And I just found the place for “text-align: left !important;”, inside .dummy-btn.

That’s really cool now !
Thank you very much !

Is there a way I can have certain fields (like extra info or an image field) appear only when the last cloze of a card is revealed?
I tried to get a hang of it but couldn’t figure it out myself so far :sweat_smile:

@MmZ1 you’re right, there is a way with some trickery.

The problem is that the content of a {{cloze:Field}} is already translated by Anki → and that translation lacks the cloze answers on the front side.

For example, let’s say Field has the following content:

The answer is {{c1::42}}.

{{cloze::Field}} on the front side:

The answer is <span class="cloze">[...]</span>.

See how the information is lost? It’s only available on the back side:

The answer is <span class="cloze">42</span>.

We can circumvent this by only using {{Field}} (without cloze:: prefix). This returns only the raw content of the field without Anki’s pre-template translation. We’d have to implement our own cloze parser though - which sounds worse than it really is. It even opens up the possibility of nested clozes and incremental MathJax equations (reveal variable per variable instead of wrapping the whole equation in a cloze).

I’m not sold on that one :smiley: When you clicked too fast, you got spoiled anyway and reversing won’t fix that. I feel similarly about an option to reset the whole thing to the start, because I don’t see any learning benefit there.

I might as well attach a click EventListener to each cloze, so you can reveal them in any order you want. I implemented similar functionality in the soon to be released AnKing IO-one by one note type, where the order you should reveal the occlusions is pre-defined, but you can still reveal any other occlusion before, if you like.

You said you had to scroll to see the answers of later chapters. I could at least implement an auto-scroll to the active cloze to prevent that unnecessary manual work.

Great to see you solved it yourself! I was too tired to check yesterday.

The sample note type already has this feature. Since you’re using my code on your own note type, I can’t really help you with that. I can however comment the code, so it’s easier for you to make sense of.

1 Like

this would be the backside of the template I’m using:

<div id="kard">
<div class=mystyle-head><div class=mystyle-headhov>{{#Tags}}<button class="button-tags" onclick="myFunction('button-tags')"><strong>Tags</strong></button>{{/Tags}}</div> <div class=mystyle-headhov><div id="button-tags" class="generalclass" style="display:none;">{{Tags}}</div></div> | <div class=mystyle-headhov><strong>{{Source}}</strong></div> | <div class=mystyle-headhov><strong>{{Date Stamp}}</strong></div></div> <hr>
   {{edit:cloze:Text}}
  

<br>
<span class="timer" id="s2" style='font-size:16px; color: #A6ABB9;'></span>
<script>
function countdown( elementName, minutes, seconds )
{
    var element, endTime, hours, mins, msLeft, time;
    function twoDigits( n )
    {
        return (n <= 9 ? "0" + n : n); 
    }
    function updateTimer()
    {
        msLeft = endTime - (+new Date);
        if ( msLeft < 1000 ) {
            element.innerHTML = "<span style='color:#CC5B5B'>TIME'S UP</span>";
        } else {
            time = new Date( msLeft );
            hours = time.getUTCHours();
            mins = time.getUTCMinutes();
            element.innerHTML = (hours ? hours + ':' + twoDigits( mins ) : mins) + ':' + twoDigits( time.getUTCSeconds() );
            setTimeout( updateTimer, time.getUTCMilliseconds() + 500 );
        }
    }
    element = document.getElementById( elementName );
    endTime = (+new Date) + 1000 * (60*minutes + seconds) + 500;
    updateTimer();
}
countdown("s2", 0, 15 ); //2nd value is the minute, 3rd is the seconds
</script>
<br>

<button class="dummy-btn noSelect" id="controls">
  <div class="reveal-btn" id="clozes">Reveal Next</div>
  </button>

<div>&nbsp;</div>
    {{#Extra}}<div id='extra'>{{edit:Extra}}</div><br>{{/Extra}}
    <div id="button-hammer" class="generalclass" style="display:none;">{{#Hammer}}<div class=mystyle-hammer>{{edit:Hammer}}</div><br>{{/Hammer}}</div>
    <div id="button-img" class="generalclass">{{#Image}}<div id='image'>{{edit:Image}}</div><br>{{/Image}}</div>  

{{#AMBOSS-Link}}<a href= {{AMBOSS-Link}}><button class="button-amboss" id="button-amboss"><img src="_amboss-icon_16x16.png">Mehr zu diesem Thema</button></a>{{/AMBOSS-Link}}
{{#Klinik}}<button class="button-klinik" onclick="myFunction('button-klinik')"><img src="_amboss-stethoskop.icon_v2.png">Klinik</button>{{/Klinik}}
{{#Hammer}}<button class="button-hammer" onclick="myFunction('button-hammer')"><img src="_amboss-hammer.icon_v2.png">Hammer</button>{{/Hammer}}
{{#Image}}<button class="button-img" onclick="myFunction('button-img')"><img src="_amboss-abb.icon_v2.png">Abbildung</button>{{/Image}}
<div>&nbsp;</div>


<div id="button-klinik" class="generalclass" style="display:none;">
{{#Klinik}}<div class=mystyle-klinik>{{edit:Klinik}}</div><br>{{/Klinik}}
</div>




<div id="anki-am" data-name="Assets by ASSET MANAGER" data-version="2.1">
    <script data-name="Anki Persistence" data-version="v0.5.3">
        if (typeof(window.Persistence) === 'undefined') {
          var _persistenceKey = 'github.com/SimonLammer/anki-persistence/';
          var _defaultKey = '_default';
          window.Persistence_sessionStorage = function() { // used in android, iOS, web
            var isAvailable = false;
            try {
              if (typeof(window.sessionStorage) === 'object') {
                isAvailable = true;
                this.clear = function() {
                  for (var i = 0; i < sessionStorage.length; i++) {
                    var k = sessionStorage.key(i);
                    if (k.indexOf(_persistenceKey) == 0) {
                      sessionStorage.removeItem(k);
                      i--;
                    }
                  };
                };
                this.setItem = function(key, value) {
                  if (value == undefined) {
                    value = key;
                    key = _defaultKey;
                  }
                  sessionStorage.setItem(_persistenceKey + key, JSON.stringify(value));
                };
                this.getItem = function(key) {
                  if (key == undefined) {
                    key = _defaultKey;
                  }
                  return JSON.parse(sessionStorage.getItem(_persistenceKey + key));
                };
                this.removeItem = function(key) {
                  if (key == undefined) {
                    key = _defaultKey;
                  }
                  sessionStorage.removeItem(_persistenceKey + key);
                };
              }
            } catch(err) {}
            this.isAvailable = function() {
              return isAvailable;
            };
          };
          window.Persistence_windowKey = function(persistentKey) { // used in windows, linux, mac
            var obj = window[persistentKey];
            var isAvailable = false;
            if (typeof(obj) === 'object') {
              isAvailable = true;
              this.clear = function() {
                obj[_persistenceKey] = {};
              };
              this.setItem = function(key, value) {
                if (value == undefined) {
                  value = key;
                  key = _defaultKey;
                }
                obj[_persistenceKey][key] = value;
              };
              this.getItem = function(key) {
                if (key == undefined) {
                  key = _defaultKey;
                }
                return obj[_persistenceKey][key] == undefined ? null : obj[_persistenceKey][key];
              };
              this.removeItem = function(key) {
                if (key == undefined) {
                  key = _defaultKey;
                }
                delete obj[_persistenceKey][key];
              };

              if (obj[_persistenceKey] == undefined) {
                this.clear();
              }
            }
            this.isAvailable = function() {
              return isAvailable;
            };
          };
          /*
           *   client  | sessionStorage | persistentKey | useful location |
           * ----------|----------------|---------------|-----------------|
           * web       |       YES      |       -       |       NO        |
           * windows   |       NO       |       py      |       NO        |
           * android   |       YES      |       -       |       NO        |
           * linux 2.0 |       NO       |       qt      |       YES       |
           * linux 2.1 |       NO       |       qt      |       YES       |
           * mac 2.0   |       NO       |       py      |       NO        |
           * mac 2.1   |       NO       |       qt      |       YES       |
           * iOS       |       YES      |       -       |       NO        |
           */
          window.Persistence = new Persistence_sessionStorage(); // android, iOS, web
          if (!Persistence.isAvailable()) {
            window.Persistence = new Persistence_windowKey("py"); // windows, mac (2.0)
          }
          if (!Persistence.isAvailable()) {
            var titleStartIndex = window.location.toString().indexOf('title'); // if titleStartIndex > 0, window.location is useful
            var titleContentIndex = window.location.toString().indexOf('main', titleStartIndex);
            if (titleStartIndex > 0 && titleContentIndex > 0 && (titleContentIndex - titleStartIndex) < 10) {
              window.Persistence = new Persistence_windowKey("qt"); // linux, mac (2.1)
            }
          }
        }
    </script>
    <script data-name="Incremental Reveal" data-version="v0.1">
        // alt for incremental reveal
        // .  for full reveal
        // for custom keycodes: https://keycode.info/
        var shortcuts = {
            "next": {
                "name": "alt",
                "keycode": 18,
            },
            "all": {
                "name": ".",
                "keycode": 190,
            }
        };

        (function () {
            let remaining = Array.prototype.slice.call(
                document.getElementsByClassName("cloze"))
            if (remaining.length > 1) {
                let content = []
                remaining.forEach((cloze, i) => {
                    console.log(cloze.innerHTML)
                    content.push(cloze.innerHTML)
                    if (Persistence.isAvailable()) {
                        cloze.innerHTML = Persistence.getItem(`hint-${i}`)
                    }
                    else cloze.innerHTML = globalThis.clozeContents[i]
                })
                remaining[0].classList.add("active")
                remaining[0].scrollIntoView({
                    behavior: "smooth",
                    block: "center"
                })

                function revealAll() {
                    while (remaining) {
                        revealNext()
                    }
                }

                function revealNext() {
                    let cloze = remaining.shift()
                    cloze.innerHTML = content.shift()
                    cloze.classList.remove("active")
                    MathJax.typesetPromise([cloze])
                    if (remaining.length > 0) {
                        remaining[0].classList.add("active")
                    }
                    else showExtra()
                }

                setupControls()

            } else showExtra()
            
            let clean = []
            let tags = document.getElementById("tags")

            if (tags.innerText) {
                document.getElementById("tags-heading").classList.remove("hidden")

                for (let tag of document.getElementById("tags").innerText.split(/\s/)) {
                    clean.push(tag.split("::").pop().replace("_", " "))
                }
                tags.innerHTML = clean.join(", ")
            }

            for (header of document.getElementsByClassName("header")) {
                header.addEventListener("click", revealNextElement)
            }

            function setupControls() {
                document.getElementById("clozes").addEventListener("click", revealNext)
                document.addEventListener("keydown", (event) => {
                    if (event.keyCode == shortcuts.next.keycode) {
                        event.preventDefault()
                        revealNext()
                    }
                    else if (event.keyCode == shortcuts.all.keycode) revealAll()
                })

                let next = document.getElementById("reveal-next")
                let nextKey = shortcuts.next.name
                next.title = `Reveal next cloze (${nextKey})`
                next.addEventListener("click", revealNext)

                let all = document.getElementById("reveal-all")
                let allKey = shortcuts.all.name
                all.title = `Reveal all clozes (${allKey})`
                all.addEventListener("click", revealAll)
            }

            function revealNextElement() {
                this.nextElementSibling.classList.remove("hidden")
            }

            function showExtra() {
                document.getElementById("controls").classList.add("hidden")
                document.getElementById("extras").removeAttribute("hidden")
            }

        })()
    </script>
</div>

:slight_smile:

Wrap all the content you want to appear after the last reveal inside the following element:

<div id="extras" hidden>
   ... All your extra stuff ...
</div>

Then it should work automatically, as the function showExtra is already in your template. It is called when the last cloze is revealed.

function showExtra() {
    document.getElementById("controls").classList.add("hidden")
    document.getElementById("extras").removeAttribute("hidden")
}

thanks, worked like a charm :+1:

I see, so {{c1::}} is taken by anki and run in first so you get empty string from it when you wanting to run some JavaScript on it, can it be done by running javascript that connect with sqlite database to get the content of {{c1::}} from the field with the same note id and run the JavaScript code after anki action done and replace the value with the new one?
(I don’t know if it’s possible to send calls to anki sqlite database by JavaScript but maybe? :sweat_smile:)

That would over-complicate things :relaxed:
Like mentioned above, the easiest thing would be to parse the raw cloze syntax myself. That is, write a JS algorithm that takes the innerHTML of an element and creates more useful cloze spans out of the raw field input than what Anki does natively.

Then I can use the front side for the incremental reveal and make users click “Show Answer” on the last cloze.


As a side note: I do not use clozes myself, because I personally prefer Q&A style cards, but helping you guys out with this is very gratifying for me, so I’ll continue to do it in my free time :grinning_face_with_smiling_eyes:
Massive and Incremental Cloze are two nice side projects that many people seem to be interested in, so I might create a more robust, general-use Cloze+ note type one day that takes the best of both worlds.

Thanks for the inspiration and feedback to all of you!

2 Likes

Thanks for your help,
By the way, if you interest in Q&A cards more I have a note type that is useful for you maybe, the main purpose of it that when you have two information that related to each other but if you put the questions all along you will spoil the first answer like that so it’s small nested questions and to reveal all press show answer,like this:
16-00-31-ezgif-7-98abd8aa1631
And the input is easy and user friendly:


It’s formating needs a little adjustment, if you want I can send you sample template