I am sorry this is such long post.
The great Kleinerpirat created a note type that uses Closet to handle multiple‑choice transformations for a forum user a couple years ago. I adapted that template to update a publicly shared radiology deck and make it more useful. (answer choice randomization, being able to select an answer, etc)
I am very much a beginner and I’m trying to learn as I go. I used an LLM to help me make a lot of the modifications so that may be a large contributing factor to my issues…but at any rate.
Occasionally, some cards on the back side fails to transform [[mc0::...]]
into the expected <li>
elements. Despite seeing “Closet executed” in the console (curiously always in under ~1.4ms when the issue occurs), the card still displays the raw [[mc0::...]]
markup. (this only happens on the back side of the card)
Because Closet is being called and looks like it is being executed in the AnkiWebView Inspector, I suspected a race condition. If I force the card to reload the content (by entering the card editor for example), when I click back to the card, the issue is corrected and Closet is executed in >1.4ms. I have not noticed this issue in AnkiDroid while testing the deck.
Things I’ve tried (bearing in mind i’m a total noob and may have flubbed these solutions at any point):
Delays & DOM Events
- Used
setTimeout()
with various delays (10 ms up to 300 ms). - Wrapped Closet code in
document.addEventListener("DOMContentLoaded", …)
. - Polling the DOM (waiting until
#mc
actually appears before initializing Closet). - Thought about trying a double-pass solution. After Closet runs, check if
[[mc0::...]]
is still present; if so, try re‑initializing Closet a second time. But I couldn’t actually get this to work and it just broke everything.
No dice.
Then I started Flagging the cards that displayed the issue and noticed it seemed to happen repeatedly with these cards. Not every time, but most of the time. But there is nothing special about these cards.
- no stray
<script>
tags or duplicate MC‑Translation blocks in the note fields. - template’s HTML is valid and consistent, with no mismatched tags or IDs.
I was planning on sharing the deck via AnkiHub, and they modify note templates when you upload them, so I cleared all their changes and disabled the add-on, but the issue persisted.
Here is the deck with Kleinerpirat’s note type + my modifications: Radiology In-service Deck on Google Drive
If you just want to check out the Back template:
<div class="question">{{Question}}</div>
<div class="image-gallery-container">
{{#Images}}
{{Images}}
{{/Images}}
<!-- Navigation Buttons -->
<button class="gallery-nav prev" onclick="navigateGallery(-1)">‹ Prev</button>
<button class="gallery-nav next" onclick="navigateGallery(1)">Next ›</button>
</div>
<script>
// Gallery Navigation
var currentImageIndex = 0;
var images = document.getElementsByClassName('image-gallery');
var prevButton = document.querySelector('.gallery-nav.prev');
var nextButton = document.querySelector('.gallery-nav.next');
if (!images.length || images.length === 1) {
prevButton.style.display = 'none';
nextButton.style.display = 'none';
}
function navigateGallery(direction) {
images[currentImageIndex].style.display = 'none'; // Hide current image
currentImageIndex = (currentImageIndex + direction + images.length) % images.length;
images[currentImageIndex].style.display = 'block'; // Show new image
}
</script>
<hr>
<div class="answer-container">
<ul class="answer-list" id="mc">
{{Answers}}
</ul>
</div>
<div class="explanation-container">
<ul class="answer-list" id="mc">
{{Explanation}}
</ul>
</div>
<div class="extra"></div>
<div class="source"></div>
<div class="proof">{{Extra}}</div>
<div id="anki-am" data-name="Assets by ASSET MANAGER" data-version="2.1">
<script data-name="MC-Translation" data-version="v0.1">
var mc = document.querySelector("#mc")
if ((list = mc.querySelector("ul")) != null) {
var items = [...list.children]
var correct = list.querySelectorAll("li > u")
var incorrect = items.filter((e) => {
return e.querySelector("u") == null
})
var output = "[[mc0"
correct.forEach(function (item, i) {
if (i == 0) output += "::" + item.innerHTML
else output += "||" + item.innerHTML
})
incorrect.forEach(function (item, i) {
if (i == 0) output += "::" + item.innerHTML
else output += "||" + item.innerHTML
})
output += "]]"
mc.innerHTML = output
}
</script>
<script data-name="Closet Setup" data-version="v0.1">
function closetUserLogic(closet, preset, chooseMemory) {
const elements = closet.template.anki.getQaChildNodes();
const memory = chooseMemory("closet__1");
const filterManager = closet.FilterManager.make(preset, memory.map);
const output = [[elements, memory, filterManager]];
/* here goes the setup - change it to fit your own needs */
filterManager.install(
closet.recipes.shuffle({ tagname: "mix" }),
closet.recipes.order({ tagname: "ord" }),
closet.flashcard.recipes.cloze({
tagname: "c",
defaultBehavior: closet.flashcard.behaviors.Show,
}),
closet.flashcard.recipes.multipleChoice({
tagname: "mc",
defaultBehavior: closet.flashcard.behaviors.Show,
}),
closet.flashcard.recipes.sort({
tagname: "sort",
defaultBehavior: closet.flashcard.behaviors.Show,
}),
closet.browser.recipes.rect({
tagname: "rect",
defaultBehavior: closet.flashcard.behaviors.Show,
}),
);
// Multiple Choice
const wrappedMultipleChoiceShow = closet.wrappers.aftermath(closet.flashcard.recipes.multipleChoice.show, (e, inter) => {
const keyword = 'fancyMultipleChoice'
if (!inter.environment.has(keyword)) {
inter.environment.set(keyword, true)
}
})
const wrapItem = (v, _i, cat) => `<li class="cl--item cl--category-${cat}">${v}</li>`
filterManager.install(wrappedMultipleChoiceShow({
tagname: 'mc',
frontStylizer: closet.Stylizer.make({
mapper: wrapItem,
separator: '',
processor: (v) => `<ol class="cl--list">${v}</ol>`,
}),
backStylizer: closet.Stylizer.make({
mapper: wrapItem,
separator: '',
processor: (v) => `<ol class="cl--list cl--reveal">${v}</ol>`,
}),
}));
return output;
}
var getAnkiPrefix = () =>
globalThis.ankiPlatform === "desktop"
? ""
: globalThis.AnkiDroidJS
? "https://appassets.androidplatform.net"
: ".";
var closetPromise = import(`${getAnkiPrefix()}/__closet-0.5.3.js`);
closetPromise
.then(
({ closet }) =>
closet.template.anki.initialize(
closet,
closetUserLogic,
"{{Card}}",
"{{Tags}}",
"back",
),
(error) => console.log("An error occured while loading Closet:", error),
)
.catch((error) =>
console.log("An error occured while executing Closet:", error),
);
if (globalThis.onUpdateHook) {
onUpdateHook.push(() => closetPromise);
}
</script>
</div>
<!-- BEGIN ANKIHUB MODFICATIONS -->
{{#ankihub_id}}
<a class='ankihub-view-note'
href='https://app.ankihub.net/decks/notes/{{ankihub_id}}'>
View Note on AnkiHub
</a>
<style>
.ankihub-view-note {
display: none;
}
.mobile .ankihub-view-note
{
display: block;
left: 50%;
margin-right: -50%;
padding: 8px;
border-radius: 50px;
background-color: #cde3f8;
font-size: 12px;
color: black;
text-decoration: none;
}
/* AnkiDroid (Android)
The button is fixed to the bottom of the screen. */
.android .ankihub-view-note {
position: fixed;
bottom: 5px;
transform: translate(-50%, -50%);
}
/* AnkiMobile (IPhone)
position: fixed doesn't work on AnkiMobile, so the button is just below the content instead. */
.iphone .ankihub-view-note,
.ipad .ankihub-view-note {
position: relative;
transform: translate(-50%, 0);
width: fit-content;
margin-top: 20px;
}
</style>
<script>
if(document.querySelector("html").classList.contains("android")) {
// Add a margin to the bottom of the card content so that the button doesn't
// overlap the content.
var container = document.querySelector('#qa');
var button = document.querySelector('.ankihub-view-note');
container.style.marginBottom = 2 * button.offsetHeight + "px";
}
</script>
{{/ankihub_id}}
<!-- END ANKIHUB MODFICATIONS -->
<!--
ANKIHUB_END
Text below this comment will not be modified by AnkiHub or AnKing add-ons.
Do not edit or remove this comment if you want to protect the content below.
-->
Haven’t seen Kleinerpirat around these parts for a while; so I would greatly appreciate any help!