JavaScript Code compatibility issue with AnkiMobile on iOS

The following code is a code I have generated based on a pre-existing co which allows for gradual cloze reveal. You tap the cloze to reveal on the front side and it remains revealed the next time you see the front side of the card. This works just fine on Desktop and Android, but not on iOS.

Could someone help me in finding out the reason why?
@jhhr @shigeyuki

Frontside


<style>
  .cloze {
    display: inline;
    padding: 0;
    margin: 0;
    white-space: nowrap;
    border: 1px solid white; /* Remove this after confirming the fix */
  }
</style>

<script>
// Get card number from body class
function getCardNumber() {
  var clz = document.body.className;
  const regex = /card(\d+)/gm;
  let m;
  if ((m = regex.exec(clz)) !== null) {
    return m[1];
  }
  return "0"; // Fallback
}

// Extract clozes from raw text
function getClozes(str, cardNumber) {
  const regex = new RegExp(`\\{\\{c${cardNumber}::((?:[^{}]|\\{\\{[^{}]*\\}\\})+)(?:::([^}]*))?\\}\\}`, 'gm');
  let m;
  const clozes = [];
  while ((m = regex.exec(str)) !== null) {
    const cleanAnswer = m[1].replace(/\{\{c\d+::(.*?)\}\}/g, '$1').trim();
    clozes.push({ 
      answer: cleanAnswer,
      hint: (typeof m[2] !== "undefined" && m[2].trim() !== "") ? m[2].trim() : "[...]" 
    });
  }
  return clozes;
}

// Global variables
var elements;
var clozes;
var revealed = [];

// Style cloze based on side and state
function styleCloze(el, isRevealed) {
  if (getCardNumber() === "1") { // Front side
    el.style.color = isRevealed ? "white" : "#BD00FD";
    el.style.backgroundColor = isRevealed ? "black" : "transparent";
  } else { // Back side
    el.style.color = isRevealed ? "white" : "gold";
    el.style.backgroundColor = isRevealed ? "black" : "transparent";
  }
}

// Apply revealed state to all clozes
function applyRevealedState() {
  elements.forEach((el, i) => {
    el.innerHTML = revealed[i] ? clozes[i].answer : clozes[i].hint;
    styleCloze(el, revealed[i]);
  });
}

// Save/load state to localStorage
function saveRevealedState() {
  localStorage.setItem("cloze_revealed_c" + getCardNumber(), JSON.stringify(revealed));
}
function loadRevealedState() {
  let stored = localStorage.getItem("cloze_revealed_c" + getCardNumber());
  return stored && JSON.parse(stored).length === elements.length ? JSON.parse(stored) : new Array(elements.length).fill(false);
}

// Toggle cloze with precise click detection
function revealCloze(i) {
  revealed[i] = !revealed[i];
  applyRevealedState();
  saveRevealedState();
}

// Reset all clozes to hidden
function resetClozes() {
  revealed = new Array(elements.length).fill(false);
  applyRevealedState();
  saveRevealedState();
}

// Create the "Reset Clozes" button
function createResetButton() {
  if (!document.getElementById('resetCloze')) {
    var resetBtn = document.createElement('button');
    resetBtn.id = 'resetCloze';
    resetBtn.textContent = 'Reset Clozes';
    resetBtn.className = 'reset-cloze-btn'; // Add this line to assign a class
    resetBtn.style.position = 'fixed';
    resetBtn.style.bottom = '10px';
    resetBtn.style.left = '50%';
    resetBtn.style.transform = 'translateX(-50%)';
    resetBtn.style.zIndex = '9999';
    resetBtn.style.padding = '80px 140px';
    resetBtn.style.cursor = 'pointer';
    document.body.appendChild(resetBtn);
    resetBtn.addEventListener('click', resetClozes);
  }
}

// Setup on card update
onUpdateHook.push(function() {
  var cardNumber = getCardNumber();
  var text = document.getElementById("rawText").innerHTML;
  clozes = getClozes(text, cardNumber);
  var clozeElements = document.querySelectorAll(".cloze");

  if (clozes.length !== clozeElements.length) {
    return; // Skip if mismatch
  }

  // Wrap each cloze in a precision container
  elements = Array.from(clozeElements).map(el => {
    const wrapper = document.createElement("span");
    wrapper.className = "cloze-wrapper";
    el.parentNode.insertBefore(wrapper, el);
    wrapper.appendChild(el);
    return el;
  });

  // Attach listeners with precise click detection
  elements.forEach((el, i) => {
    el.addEventListener('click', e => {
      e.stopPropagation();
      const rect = el.getBoundingClientRect();
      const clickX = e.clientX;
      const clickY = e.clientY;
      if (clickX >= rect.left && clickX <= rect.right && clickY >= rect.top && clickY <= rect.bottom) {
        revealCloze(i);
      }
    });
  });

  // Load and apply state
  revealed = loadRevealedState();
  applyRevealedState();

  // Create the reset button
  createResetButton();
});
</script>

<script id="rawText" type="text/plain">
{{Text}}
</script>

Backside

<script>
  window.removeEventListener('click', clickHandler);
</script>
1 Like

This is just a guess because I don’t use AnkiMobile: maybe localStorage can’t be used in AnkiMobile?

1 Like

According to Simon Lammer’s comment in the AnkiPersistence code, sessionStorage should work in iOS. Try switching the localStorage usage to sessionStorage and see if that changes anything.

1 Like

If you mean sessionStorage like the edit down below, I tried it and it sadly still did not end up working on iOS. No response to reveal cloze upon touch. :frowning:

<style>
  .cloze {
    display: inline;
    padding: 0;
    margin: 0;
    white-space: nowrap;
    border: 1px solid white; /* Remove this after confirming the fix */
  }
</style>

<script>
// Get card number from body class
function getCardNumber() {
  var clz = document.body.className;
  const regex = /card(\d+)/gm;
  let m;
  if ((m = regex.exec(clz)) !== null) {
    return m[1];
  }
  return "0"; // Fallback
}

// Extract clozes from raw text
function getClozes(str, cardNumber) {
  const regex = new RegExp(`\\{\\{c${cardNumber}::((?:[^{}]|\\{\\{[^{}]*\\}\\})+)(?:::([^}]*))?\\}\\}`, 'gm');
  let m;
  const clozes = [];
  while ((m = regex.exec(str)) !== null) {
    const cleanAnswer = m[1].replace(/\{\{c\d+::(.*?)\}\}/g, '$1').trim();
    clozes.push({ 
      answer: cleanAnswer,
      hint: (typeof m[2] !== "undefined" && m[2].trim() !== "") ? m[2].trim() : "[...]" 
    });
  }
  return clozes;
}

// Global variables
var elements;
var clozes;
var revealed = [];

// Style cloze based on side and state
function styleCloze(el, isRevealed) {
  if (getCardNumber() === "1") { // Front side
    el.style.color = isRevealed ? "white" : "#BD00FD";
    el.style.backgroundColor = isRevealed ? "black" : "transparent";
  } else { // Back side
    el.style.color = isRevealed ? "white" : "gold";
    el.style.backgroundColor = isRevealed ? "black" : "transparent";
  }
}

// Apply revealed state to all clozes
function applyRevealedState() {
  elements.forEach((el, i) => {
    el.innerHTML = revealed[i] ? clozes[i].answer : clozes[i].hint;
    styleCloze(el, revealed[i]);
  });
}

// Save/load state to sessionStorage
function saveRevealedState() {
  sessionStorage.setItem("cloze_revealed_c" + getCardNumber(), JSON.stringify(revealed));
}
function loadRevealedState() {
  let stored = sessionStorage.getItem("cloze_revealed_c" + getCardNumber());
  return stored && JSON.parse(stored).length === elements.length ? JSON.parse(stored) : new Array(elements.length).fill(false);
}

// Toggle cloze with precise click detection
function revealCloze(i) {
  revealed[i] = !revealed[i];
  applyRevealedState();
  saveRevealedState();
}

// Reset all clozes to hidden
function resetClozes() {
  revealed = new Array(elements.length).fill(false);
  applyRevealedState();
  saveRevealedState();
}

// Create the "Reset Clozes" button
function createResetButton() {
  if (!document.getElementById('resetCloze')) {
    var resetBtn = document.createElement('button');
    resetBtn.id = 'resetCloze';
    resetBtn.textContent = 'Reset Clozes';
    resetBtn.className = 'reset-cloze-btn'; // Add this line to assign a class
    resetBtn.style.position = 'fixed';
    resetBtn.style.bottom = '10px';
    resetBtn.style.left = '50%';
    resetBtn.style.transform = 'translateX(-50%)';
    resetBtn.style.zIndex = '9999';
    resetBtn.style.padding = '80px 1200px';
    resetBtn.style.cursor = 'pointer';
    document.body.appendChild(resetBtn);
    resetBtn.addEventListener('click', resetClozes);
  }
}

// Setup on card update
onUpdateHook.push(function() {
  var cardNumber = getCardNumber();
  var text = document.getElementById("rawText").innerHTML;
  clozes = getClozes(text, cardNumber);
  var clozeElements = document.querySelectorAll(".cloze");

  if (clozes.length !== clozeElements.length) {
    return; // Skip if mismatch
  }

  // Wrap each cloze in a precision container
  elements = Array.from(clozeElements).map(el => {
    const wrapper = document.createElement("span");
    wrapper.className = "cloze-wrapper";
    el.parentNode.insertBefore(wrapper, el);
    wrapper.appendChild(el);
    return el;
  });

  // Attach listeners with precise click detection
  elements.forEach((el, i) => {
    el.addEventListener('click', e => {
      e.stopPropagation();
      const rect = el.getBoundingClientRect();
      const clickX = e.clientX;
      const clickY = e.clientY;
      if (clickX >= rect.left && clickX <= rect.right && clickY >= rect.top && clickY <= rect.bottom) {
        revealCloze(i);
      }
    });
  });

  // Load and apply state
  revealed = loadRevealedState();
  applyRevealedState();

  // Create the reset button
  createResetButton();
});
</script>

<script id="rawText" type="text/plain">
{{Text}}
</script>

1 Like

You can try to debug the code with eruda (add the code to the top of your template Front): Development Guide · ankidroid/Anki-Android Wiki · GitHub

I assume eruda should work on iOS. Open the console view to see if any errors have been logged. If not, then I’d add start adding console.log calls into the code to see if any functions aren’t getting that should and what the variables are.

Stuff like this:

// Apply revealed state to all clozes
function applyRevealedState() {
  elements.forEach((el, i) => {
    console.log("applyRevealedState, i:"i,"revealed[i]", revealed[i], "clozes[i].answer", clozes[i]?.answer, "clozes[i].hint", clozes[i]?.hint);
    el.innerHTML = revealed[i] ? clozes[i].answer : clozes[i].hint;
    styleCloze(el, revealed[i]);
  });
}

Something somewhere isn’t getting the right stuff, adding enough logs should reveal it eventually…

Check if this is what you mean by adding console logs. With or without console logs, I am not seeing any logged errors on the eruda console…

<!-- Add Eruda at the top of the Front template -->
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>

<style>
  .cloze {
    display: inline;
    padding: 0;
    margin: 0;
    white-space: nowrap;
    border: 1px solid white; /* Remove this after confirming the fix */
  }
</style>

<script>
// Get card number from body class
function getCardNumber() {
  var clz = document.body.className;
  const regex = /card(\d+)/gm;
  let m;
  if ((m = regex.exec(clz)) !== null) {
    console.log("getCardNumber: Card number detected:", m[1]);
    return m[1];
  }
  console.log("getCardNumber: No card number found, defaulting to 0");
  return "0"; // Fallback
}

// Extract clozes from raw text
function getClozes(str, cardNumber) {
  const regex = new RegExp(`\\{\\{c${cardNumber}::((?:[^{}]|\\{\\{[^{}]*\\}\\})+)(?:::([^}]*))?\\}\\}`, 'gm');
  let m;
  const clozes = [];
  while ((m = regex.exec(str)) !== null) {
    const cleanAnswer = m[1].replace(/\{\{c\d+::(.*?)\}\}/g, '$1').trim();
    const hint = (typeof m[2] !== "undefined" && m[2].trim() !== "") ? m[2].trim() : "[...]";
    clozes.push({ answer: cleanAnswer, hint: hint });
    console.log("getClozes: Extracted cloze - answer:", cleanAnswer, "hint:", hint);
  }
  return clozes;
}

// Global variables
var elements;
var clozes;
var revealed = [];

// Style cloze based on side and state
function styleCloze(el, isRevealed) {
  if (getCardNumber() === "1") { // Front side
    el.style.color = isRevealed ? "white" : "#BD00FD";
    el.style.backgroundColor = isRevealed ? "black" : "transparent";
  } else { // Back side
    el.style.color = isRevealed ? "white" : "gold";
    el.style.backgroundColor = isRevealed ? "black" : "transparent";
  }
  console.log("styleCloze: Styled element - isRevealed:", isRevealed, "color:", el.style.color);
}

// Apply revealed state to all clozes
function applyRevealedState() {
  elements.forEach((el, i) => {
    console.log("applyRevealedState: Index:", i, "revealed:", revealed[i], "answer:", clozes[i]?.answer, "hint:", clozes[i]?.hint);
    el.innerHTML = revealed[i] ? clozes[i].answer : clozes[i].hint;
    styleCloze(el, revealed[i]);
  });
}

// Save/load state to localStorage
function saveRevealedState() {
  localStorage.setItem("cloze_revealed_c" + getCardNumber(), JSON.stringify(revealed));
  console.log("saveRevealedState: Saved to localStorage:", revealed);
}
function loadRevealedState() {
  let stored = localStorage.getItem("cloze_revealed_c" + getCardNumber());
  let result = stored && JSON.parse(stored).length === elements.length ? JSON.parse(stored) : new Array(elements.length).fill(false);
  console.log("loadRevealedState: Loaded from localStorage:", result);
  return result;
}

// Toggle cloze with precise click detection
function revealCloze(i) {
  revealed[i] = !revealed[i];
  console.log("revealCloze: Toggled index:", i, "new state:", revealed[i]);
  applyRevealedState();
  saveRevealedState();
}

// Reset all clozes to hidden
function resetClozes() {
  revealed = new Array(elements.length).fill(false);
  console.log("resetClozes: Reset revealed array:", revealed);
  applyRevealedState();
  saveRevealedState();
}

// Create the "Reset Clozes" button
function createResetButton() {
  if (!document.getElementById('resetCloze')) {
    var resetBtn = document.createElement('button');
    resetBtn.id = 'resetCloze';
    resetBtn.textContent = 'Reset Clozes';
    resetBtn.className = 'reset-cloze-btn';
    resetBtn.style.position = 'fixed';
    resetBtn.style.bottom = '10px';
    resetBtn.style.left = '50%';
    resetBtn.style.transform = 'translateX(-50%)';
    resetBtn.style.zIndex = '9999';
    resetBtn.style.padding = '80px 1200px'; // Note: This padding seems unusually large
    resetBtn.style.cursor = 'pointer';
    document.body.appendChild(resetBtn);
    resetBtn.addEventListener('click', resetClozes);
    console.log("createResetButton: Reset button created");
  }
}

// Setup on card update
onUpdateHook.push(function() {
  var cardNumber = getCardNumber();
  var text = document.getElementById("rawText").innerHTML;
  console.log("onUpdateHook: Card number:", cardNumber, "Raw text:", text);
  clozes = getClozes(text, cardNumber);
  var clozeElements = document.querySelectorAll(".cloze");

  if (clozes.length !== clozeElements.length) {
    console.log("onUpdateHook: Mismatch between clozes and elements, skipping - clozes:", clozes.length, "elements:", clozeElements.length);
    return; // Skip if mismatch
  }

  // Wrap each cloze in a precision container
  elements = Array.from(clozeElements).map(el => {
    const wrapper = document.createElement("span");
    wrapper.className = "cloze-wrapper";
    el.parentNode.insertBefore(wrapper, el);
    wrapper.appendChild(el);
    return el;
  });
  console.log("onUpdateHook: Wrapped clozes, elements length:", elements.length);

  // Attach listeners with precise click detection
  elements.forEach((el, i) => {
    el.addEventListener('click', e => {
      e.stopPropagation();
      const rect = el.getBoundingClientRect();
      const clickX = e.clientX;
      const clickY = e.clientY;
      console.log("Click event: Index:", i, "clickX:", clickX, "clickY:", clickY, "rect:", rect);
      if (clickX >= rect.left && clickX <= rect.right && clickY >= rect.top && clickY <= rect.bottom) {
        revealCloze(i);
      }
    });
  });

  // Load and apply state
  revealed = loadRevealedState();
  applyRevealedState();

  // Create the reset button
  createResetButton();
});
</script>

<script id="rawText" type="text/plain">
{{Text}}
</script>

I tried the eruda console on ipad and it seems that it is not showing the console button on ipad. Only on desktop and Android

Also BTW. Clicking on the undo button I made inside the code returns an “UndoEmpty” error…

I tried the eruda console on ipad and it seems that it is not showing the console button on ipad. Only on desktop and Android

That’s unfortunate, I assumed it’d work the same on iOS but guess not then. Well, turns out my old custom consoleLog code for mobile will get some use.

Replace the eruda script with the below code and then replace console.log with consoleLog. That should at least enable the ability to use logs to debug the code in iOS.

consoleLog code - adds a button to the top right where you can open the log
<script>
// Initialize consoleLogMessages array if it doesn't exist
globalThis.consoleLogMessages = globalThis.consoleLogMessages || [];
globalThis.consoleLogMessageCount = globalThis.consoleLogMessageCount || 0;

// Update console with all logged messages
function renderLogMessages() {
  const consoleElement = document.getElementById("custom-console");
  const startingMsgCount = Math.max(
    globalThis.consoleLogMessageCount - globalThis.consoleLogMessages.length,
    0
  );
  // Limit stored messages to the last 100 to limit lag
  globalThis.consoleLogMessages = (globalThis.consoleLogMessages || []).slice(
    -100
  );
  consoleElement.innerHTML =
    "" +
    globalThis.consoleLogMessages
      .map((msg, i) => {
        const msgNUm = startingMsgCount + i + 1;
        const digitCount = parseInt(Math.log10(msgNUm));
        let zeroes = "";
        for (let j = 0; j < 4 - digitCount; j++) {
          zeroes += "0";
        }
        return `<div style="margin-bottom: 2px; border-bottom: 1px solid black;"><span style="float: right; font-size: 0.7em; color: #858585;">${zeroes}${msgNUm}</span>${msg}</div>`;
      })
      .join("");
  // Auto-scroll to bottom
  consoleElement.scrollTop = consoleElement.scrollHeight;
  return consoleElement;
}

function insertCustomLogElements() {
  // Create custom console element
  let customConsole = document.getElementById("custom-console");
  if (!customConsole) {
    customConsole = document.createElement("div");

    customConsole.id = "custom-console";
    customConsole.style.cssText =
      "position: absolute; top: 25px; right: 0; width:80%; max-height: 500px; overflow-y: auto; background-color: #333; color: #fff; font-size: 0.5em; font-weight: normal; border: 1px solid #ccc; padding: 10px; z-index: 1000; display: none; text-align: left;";
    customConsole.innerHTML = (globalThis.consoleLogMessages || []).join("");

    document.body.insertBefore(customConsole, document.body.firstChild);
  }

  // Create toggle button for console
  let toggleConsoleBtn = document.getElementById("toggle-console-btn");
  if (!toggleConsoleBtn) {
    toggleConsoleBtn = document.createElement("button");
    toggleConsoleBtn.id = "toggle-console-btn";
    toggleConsoleBtn.style.cssText =
      "position: absolute; top: 0; right: 0; background-color: #007bff; color: #fff; border: none; cursor: pointer; z-index: 1001;margin: 0;";
    if (globalThis.ankiPlatform === "desktop") {
      toggleConsoleBtn.style.cssText +=
        " width: 15px; height: 15px; padding: 0px 0px 2px 0px;";
    } else {
      toggleConsoleBtn.style.cssText +=
        " width: 25px; height: 25px; padding: 4px 4px 6px 4px;";
    }
    toggleConsoleBtn.innerHTML = "&#9660;"; // Downward arrow by default

    // Toggle visibility of the custom console
    toggleConsoleBtn.addEventListener(
      "click",
      function toggleConsoleVisiblity() {
        const customConsoleCur = document.getElementById("custom-console");
        customConsoleCur.innerHTML = (
          globalThis.consoleLogMessages || []
        ).join(""); // Update console with all logged messages
        customConsoleCur.scrollTop = customConsoleCur.scrollHeight; // Auto-scroll to bottom

        const toggleBtn = document.getElementById("toggle-console-btn");
        if (customConsoleCur.style.display === "none") {
          customConsoleCur.style.display = "block";
          toggleBtn.innerHTML = "&#9650;"; // Upward arrow when expanded
          renderLogMessages();
        } else {
          customConsoleCur.style.display = "none";
          toggleBtn.innerHTML = "&#9660;"; // Downward arrow when collapsed
        }
      }
    );
    document.body.insertBefore(toggleConsoleBtn, document.body.firstChild);
  }
}

// Function to log messages to the custom console and store them in consoleLogMessages
function consoleLog(...args) {
  globalThis.consoleLogMessageCount = globalThis.consoleLogMessageCount || 0;
  globalThis.consoleLogMessageCount++;
  // Log to normal console.log also
  console.log(...args);

  var logHtml = "";
  args.forEach((arg, i) => {
    // Wrap first arg in span so that it's on the same line as the msg counter
    const wrapper = i === 0 ? "span" : "div";
    if (arg instanceof Error) {
      // Render stack trace for Error objects in red
      logHtml += `<${wrapper} style="color: #FF6B6B;">${arg.toString()}</${wrapper}>`;
      if (arg.stack) {
        logHtml += `<${wrapper} style="color: #FF6B6B;">${arg.stack.replace(
          /\n/g,
          "<br>"
        )}</${wrapper}>`;
      }
    } else if (
      (typeof arg === "object" && arg !== null) ||
      typeof arg === "function"
    ) {
      // Render objects and functions in light blue
      logHtml += `<${wrapper} style="color: #7FDBFF;">${stringifyLimited(
        arg
      )}</${wrapper}>`;
    } else if (typeof arg === "string") {
      // Render other types of data as plain text in white
      logHtml += `<${wrapper} style="color: #FFFFFF;">${arg}</${wrapper}>`;
    } else {
      // null, undefined, numbers in gray
      logHtml += `<${wrapper} style="color: #AAAAAA;">${arg}</${wrapper}>`;
    }
  });

  function stringifyLimited(obj, depth = 1) {
    let res = "<span>";
    if (depth < 2) {
      if (Array.isArray(obj)) {
        res += "[";
        obj.forEach((el, i) => {
          res += `<div>${[i]}: ${stringifyLimited(el, depth + 1)}</div>`;
        });
        res += "]";
      } else if (typeof obj === "object" && obj !== null) {
        res += "{";
        Object.entries(obj).forEach(([key, val]) => {
          res += `<div>${[key]}: ${stringifyLimited(val, depth + 1)}</div>`;
        });
        res += "}";
      } else if (typeof obj === "function") {
        res += `Ć’ <i>${obj.name}()<i>`;
      } else {
        res += JSON.stringify(obj);
      }
    } else {
      if (Array.isArray(obj)) {
        res += `Array[${obj.length}]`;
      } else if (typeof obj === "object" && obj !== null) {
        res += `Object[${Object.keys(obj).length}]`;
      } else if (typeof obj === "function") {
        res += `Ć’ <i>${obj.name}()</i>`;
      } else {
        res += JSON.stringify(obj);
      }
    }
    res += "</span>";
    return res;
  }

  globalThis.consoleLogMessages = globalThis.consoleLogMessages || [];
  globalThis.consoleLogMessages.push(logHtml); // Store the log message as HTML string
  // Only render message if the console is open
  const customConsoleCur = document.getElementById("custom-console");
  if (customConsoleCur.style.display === "block") {
    renderLogMessages();
  }
}
</script>

<script>
if (globalThis.ankiPlatform === "desktop") {
  consoleLog = console.log;
} else {
  insertCustomLogElements();
}
</script>

With or without console logs, I am not seeing any logged errors on the eruda console…

I tried the eruda console on ipad and it seems that it is not showing the console button on ipad. Only on desktop and Android

Hmm, that’s weird. Since the eruda console only worked on desktop and AnkiDroid, and on those the code was working, you should have seem some non-error logs at least. That means the console.logs weren’t getting called or eruda didn’t work in capturing them.

Can you test again using the consoleLog?

I have made Grok 3 make the necessary changes. I now see a drop down arrow on the top right of the Anki note. But I am not seeing anything when I click it.

Here is the console code alongside with my code in question with the Edit of console.log into consoleLog

<script>
// Initialize consoleLogMessages array if it doesn't exist
globalThis.consoleLogMessages = globalThis.consoleLogMessages || [];
globalThis.consoleLogMessageCount = globalThis.consoleLogMessageCount || 0;

// Update console with all logged messages
function renderLogMessages() {
  const consoleElement = document.getElementById("custom-console");
  const startingMsgCount = Math.max(
    globalThis.consoleLogMessageCount - globalThis.consoleLogMessages.length,
    0
  );
  // Limit stored messages to the last 100 to limit lag
  globalThis.consoleLogMessages = (globalThis.consoleLogMessages || []).slice(
    -100
  );
  consoleElement.innerHTML =
    "" +
    globalThis.consoleLogMessages
      .map((msg, i) => {
        const msgNUm = startingMsgCount + i + 1;
        const digitCount = parseInt(Math.log10(msgNUm));
        let zeroes = "";
        for (let j = 0; j < 4 - digitCount; j++) {
          zeroes += "0";
        }
        return `<div style="margin-bottom: 2px; border-bottom: 1px solid black;"><span style="float: right; font-size: 0.7em; color: #858585;">${zeroes}${msgNUm}</span>${msg}</div>`;
      })
      .join("");
  // Auto-scroll to bottom
  consoleElement.scrollTop = consoleElement.scrollHeight;
  return consoleElement;
}

function insertCustomLogElements() {
  // Create custom console element
  let customConsole = document.getElementById("custom-console");
  if (!customConsole) {
    customConsole = document.createElement("div");

    customConsole.id = "custom-console";
    customConsole.style.cssText =
      "position: absolute; top: 25px; right: 0; width:80%; max-height: 500px; overflow-y: auto; background-color: #333; color: #fff; font-size: 0.5em; font-weight: normal; border: 1px solid #ccc; padding: 10px; z-index: 1000; display: none; text-align: left;";
    customConsole.innerHTML = (globalThis.consoleLogMessages || []).join("");

    document.body.insertBefore(customConsole, document.body.firstChild);
  }

  // Create toggle button for console
  let toggleConsoleBtn = document.getElementById("toggle-console-btn");
  if (!toggleConsoleBtn) {
    toggleConsoleBtn = document.createElement("button");
    toggleConsoleBtn.id = "toggle-console-btn";
    toggleConsoleBtn.style.cssText =
      "position: absolute; top: 0; right: 0; background-color: #007bff; color: #fff; border: none; cursor: pointer; z-index: 1001;margin: 0;";
    if (globalThis.ankiPlatform === "desktop") {
      toggleConsoleBtn.style.cssText +=
        " width: 15px; height: 15px; padding: 0px 0px 2px 0px;";
    } else {
      toggleConsoleBtn.style.cssText +=
        " width: 25px; height: 25px; padding: 4px 4px 6px 4px;";
    }
    toggleConsoleBtn.innerHTML = "&#9660;"; // Downward arrow by default

    // Toggle visibility of the custom console
    toggleConsoleBtn.addEventListener(
      "click",
      function toggleConsoleVisiblity() {
        const customConsoleCur = document.getElementById("custom-console");
        customConsoleCur.innerHTML = (
          globalThis.consoleLogMessages || []
        ).join(""); // Update console with all logged messages
        customConsoleCur.scrollTop = customConsoleCur.scrollHeight; // Auto-scroll to bottom

        const toggleBtn = document.getElementById("toggle-console-btn");
        if (customConsoleCur.style.display === "none") {
          customConsoleCur.style.display = "block";
          toggleBtn.innerHTML = "&#9650;"; // Upward arrow when expanded
          renderLogMessages();
        } else {
          customConsoleCur.style.display = "none";
          toggleBtn.innerHTML = "&#9660;"; // Downward arrow when collapsed
        }
      }
    );
    document.body.insertBefore(toggleConsoleBtn, document.body.firstChild);
  }
}

// Function to log messages to the custom console and store them in consoleLogMessages
function consoleLog(...args) {
  globalThis.consoleLogMessageCount = globalThis.consoleLogMessageCount || 0;
  globalThis.consoleLogMessageCount++;
  // Log to normal console.log also
  console.log(...args);

  var logHtml = "";
  args.forEach((arg, i) => {
    // Wrap first arg in span so that it's on the same line as the msg counter
    const wrapper = i === 0 ? "span" : "div";
    if (arg instanceof Error) {
      // Render stack trace for Error objects in red
      logHtml += `<${wrapper} style="color: #FF6B6B;">${arg.toString()}</${wrapper}>`;
      if (arg.stack) {
        logHtml += `<${wrapper} style="color: #FF6B6B;">${arg.stack.replace(
          /\n/g,
          "<br>"
        )}</${wrapper}>`;
      }
    } else if (
      (typeof arg === "object" && arg !== null) ||
      typeof arg === "function"
    ) {
      // Render objects and functions in light blue
      logHtml += `<${wrapper} style="color: #7FDBFF;">${stringifyLimited(
        arg
      )}</${wrapper}>`;
    } else if (typeof arg === "string") {
      // Render other types of data as plain text in white
      logHtml += `<${wrapper} style="color: #FFFFFF;">${arg}</${wrapper}>`;
    } else {
      // null, undefined, numbers in gray
      logHtml += `<${wrapper} style="color: #AAAAAA;">${arg}</${wrapper}>`;
    }
  });

  function stringifyLimited(obj, depth = 1) {
    let res = "<span>";
    if (depth < 2) {
      if (Array.isArray(obj)) {
        res += "[";
        obj.forEach((el, i) => {
          res += `<div>${[i]}: ${stringifyLimited(el, depth + 1)}</div>`;
        });
        res += "]";
      } else if (typeof obj === "object" && obj !== null) {
        res += "{";
        Object.entries(obj).forEach(([key, val]) => {
          res += `<div>${[key]}: ${stringifyLimited(val, depth + 1)}</div>`;
        });
        res += "}";
      } else if (typeof obj === "function") {
        res += `Ć’ <i>${obj.name}()<i>`;
      } else {
        res += JSON.stringify(obj);
      }
    } else {
      if (Array.isArray(obj)) {
        res += `Array[${obj.length}]`;
      } else if (typeof obj === "object" && obj !== null) {
        res += `Object[${Object.keys(obj).length}]`;
      } else if (typeof obj === "function") {
        res += `Ć’ <i>${obj.name}()</i>`;
      } else {
        res += JSON.stringify(obj);
      }
    }
    res += "</span>";
    return res;
  }

  globalThis.consoleLogMessages = globalThis.consoleLogMessages || [];
  globalThis.consoleLogMessages.push(logHtml); // Store the log message as HTML string
  // Only render message if the console is open
  const customConsoleCur = document.getElementById("custom-console");
  if (customConsoleCur.style.display === "block") {
    renderLogMessages();
  }
}
</script>

<script>
if (globalThis.ankiPlatform === "desktop") {
  consoleLog = console.log;
} else {
  insertCustomLogElements();
}
</script>


<style>
  .cloze {
    display: inline;
    padding: 0;
    margin: 0;
    white-space: nowrap;
    border: 1px solid white; /* Remove this after confirming the fix */
  }
</style>

<script>
// Get card number from body class
function getCardNumber() {
  var clz = document.body.className;
  const regex = /card(\d+)/gm;
  let m;
  if ((m = regex.exec(clz)) !== null) {
    consoleLog("getCardNumber: Card number detected:", m[1]);
    return m[1];
  }
  consoleLog("getCardNumber: No card number found, defaulting to 0");
  return "0"; // Fallback
}

// Extract clozes from raw text
function getClozes(str, cardNumber) {
  const regex = new RegExp(`\\{\\{c${cardNumber}::((?:[^{}]|\\{\\{[^{}]*\\}\\})+)(?:::([^}]*))?\\}\\}`, 'gm');
  let m;
  const clozes = [];
  while ((m = regex.exec(str)) !== null) {
    const cleanAnswer = m[1].replace(/\{\{c\d+::(.*?)\}\}/g, '$1').trim();
    const hint = (typeof m[2] !== "undefined" && m[2].trim() !== "") ? m[2].trim() : "[...]";
    clozes.push({ answer: cleanAnswer, hint: hint });
    consoleLog("getClozes: Extracted cloze - answer:", cleanAnswer, "hint:", hint);
  }
  return clozes;
}

// Global variables
var elements;
var clozes;
var revealed = [];

// Style cloze based on side and state
function styleCloze(el, isRevealed) {
  if (getCardNumber() === "1") { // Front side
    el.style.color = isRevealed ? "white" : "#BD00FD";
    el.style.backgroundColor = isRevealed ? "black" : "transparent";
  } else { // Back side
    el.style.color = isRevealed ? "white" : "gold";
    el.style.backgroundColor = isRevealed ? "black" : "transparent";
  }
  consoleLog("styleCloze: Styled element - isRevealed:", isRevealed, "color:", el.style.color);
}

// Apply revealed state to all clozes
function applyRevealedState() {
  elements.forEach((el, i) => {
    consoleLog("applyRevealedState: Index:", i, "revealed:", revealed[i], "answer:", clozes[i]?.answer, "hint:", clozes[i]?.hint);
    el.innerHTML = revealed[i] ? clozes[i].answer : clozes[i].hint;
    styleCloze(el, revealed[i]);
  });
}

// Save/load state to localStorage
function saveRevealedState() {
  localStorage.setItem("cloze_revealed_c" + getCardNumber(), JSON.stringify(revealed));
  consoleLog("saveRevealedState: Saved to localStorage:", revealed);
}
function loadRevealedState() {
  let stored = localStorage.getItem("cloze_revealed_c" + getCardNumber());
  let result = stored && JSON.parse(stored).length === elements.length ? JSON.parse(stored) : new Array(elements.length).fill(false);
  consoleLog("loadRevealedState: Loaded from localStorage:", result);
  return result;
}

// Toggle cloze with precise click detection
function revealCloze(i) {
  revealed[i] = !revealed[i];
  consoleLog("revealCloze: Toggled index:", i, "new state:", revealed[i]);
  applyRevealedState();
  saveRevealedState();
}

// Reset all clozes to hidden
function resetClozes() {
  revealed = new Array(elements.length).fill(false);
  consoleLog("resetClozes: Reset revealed array:", revealed);
  applyRevealedState();
  saveRevealedState();
}

// Create the "Reset Clozes" button
function createResetButton() {
  if (!document.getElementById('resetCloze')) {
    var resetBtn = document.createElement('button');
    resetBtn.id = 'resetCloze';
    resetBtn.textContent = 'Reset Clozes';
    resetBtn.className = 'reset-cloze-btn';
    resetBtn.style.position = 'fixed';
    resetBtn.style.bottom = '10px';
    resetBtn.style.left = '50%';
    resetBtn.style.transform = 'translateX(-50%)';
    resetBtn.style.zIndex = '9999';
    resetBtn.style.padding = '20px 120px'; // Note: This padding seems unusually large
    resetBtn.style.cursor = 'pointer';
    document.body.appendChild(resetBtn);
    resetBtn.addEventListener('click', resetClozes);
    consoleLog("createResetButton: Reset button created");
  }
}

// Setup on card update
onUpdateHook.push(function() {
  var cardNumber = getCardNumber();
  var text = document.getElementById("rawText").innerHTML;
  consoleLog("onUpdateHook: Card number:", cardNumber, "Raw text:", text);
  clozes = getClozes(text, cardNumber);
  var clozeElements = document.querySelectorAll(".cloze");

  if (clozes.length !== clozeElements.length) {
    consoleLog("onUpdateHook: Mismatch between clozes and elements, skipping - clozes:", clozes.length, "elements:", clozeElements.length);
    return; // Skip if mismatch
  }

  // Wrap each cloze in a precision container
  elements = Array.from(clozeElements).map(el => {
    const wrapper = document.createElement("span");
    wrapper.className = "cloze-wrapper";
    el.parentNode.insertBefore(wrapper, el);
    wrapper.appendChild(el);
    return el;
  });
  consoleLog("onUpdateHook: Wrapped clozes, elements length:", elements.length);

  // Attach listeners with precise click detection
  elements.forEach((el, i) => {
    el.addEventListener('click', e => {
      e.stopPropagation();
      const rect = el.getBoundingClientRect();
      const clickX = e.clientX;
      const clickY = e.clientY;
      consoleLog("Click event: Index:", i, "clickX:", clickX, "clickY:", clickY, "rect:", rect);
      if (clickX >= rect.left && clickX <= rect.right && clickY >= rect.top && clickY <= rect.bottom) {
        revealCloze(i);
      }
    });
  });

  // Load and apply state
  revealed = loadRevealedState();
  applyRevealedState();

  // Create the reset button
  createResetButton();
});
</script>

<script id="rawText" type="text/plain">
{{Text}}
</script>

I now see a drop down arrow on the top right of the Anki note. But I am not seeing anything when I click it.

You mean on iOS? It should show stuff getting logged on AnkiDroid at least.

Hmm, I think the problem might be onUpdateHook. There’s no indication that it’s supported on AnkiMobile? Reviewer Javascript - Writing Anki Add-ons
AnkiMobile docs don’t mention anything about it. If it’s the case that onUpdateHook doesn’t get called at all on AnkiMobile, then that would explain why nothing is logged because it’s the entry point in the code.

Try getting getting the AI to update the code to not use ´onUpdateHook´

Nope, this time it is not working on AnkiDroid.

It gave me this, and this time it is not working at any of the platforms. What is so weird about iOS which is not the same as AnkiDroid or Anki Desktop :red_question_mark: :downcast_face_with_sweat:

<style>
  .cloze {
    display: inline;
    padding: 0;
    margin: 0;
    white-space: nowrap;
    border: 1px solid white; /* Remove after confirming the fix */
  }
  .cloze-wrapper {
    display: inline;
  }
</style>

<script>
// Get card number from body class
function getCardNumber() {
  var clz = document.body.className;
  const regex = /card(\d+)/gm;
  let m;
  if ((m = regex.exec(clz)) !== null) {
    return m[1];
  }
  return "0"; // Fallback
}

// Extract clozes from raw text
function getClozes(str, cardNumber) {
  const regex = new RegExp(`\\{\\{c${cardNumber}::((?:[^{}]|\\{\\{[^{}]*\\}\\})+)(?:::([^}]*))?\\}\\}`, 'gm');
  let m;
  const clozes = [];
  while ((m = regex.exec(str)) !== null) {
    const cleanAnswer = m[1].replace(/\{\{c\d+::(.*?)\}\}/g, '$1').trim();
    clozes.push({ 
      answer: cleanAnswer,
      hint: (typeof m[2] !== "undefined" && m[2].trim() !== "") ? m[2].trim() : "[...]" 
    });
  }
  return clozes;
}

// Global variables
var elements;
var clozes;
var revealed = [];

// Style cloze based on side and state
function styleCloze(el, isRevealed) {
  if (getCardNumber() === "1") { // Front side
    el.style.color = isRevealed ? "white" : "#BD00FD";
    el.style.backgroundColor = isRevealed ? "black" : "transparent";
  } else { // Back side
    el.style.color = isRevealed ? "white" : "gold";
    el.style.backgroundColor = isRevealed ? "black" : "transparent";
  }
}

// Apply revealed state to all clozes
function applyRevealedState() {
  elements.forEach((el, i) => {
    el.innerHTML = revealed[i] ? clozes[i].answer : clozes[i].hint;
    styleCloze(el, revealed[i]);
  });
}

// Save/load state to localStorage
function saveRevealedState() {
  localStorage.setItem("cloze_revealed_c" + getCardNumber(), JSON.stringify(revealed));
}
function loadRevealedState() {
  let stored = localStorage.getItem("cloze_revealed_c" + getCardNumber());
  return stored && JSON.parse(stored).length === elements.length ? JSON.parse(stored) : new Array(elements.length).fill(false);
}

// Toggle cloze with precise click detection
function revealCloze(i) {
  revealed[i] = !revealed[i];
  applyRevealedState();
  saveRevealedState();
}

// Reset all clozes to hidden
function resetClozes() {
  revealed = new Array(elements.length).fill(false);
  applyRevealedState();
  saveRevealedState();
}

// Create the "Reset Clozes" button
function createResetButton() {
  if (!document.getElementById('resetCloze')) {
    var resetBtn = document.createElement('button');
    resetBtn.id = 'resetCloze';
    resetBtn.textContent = 'Reset Clozes';
    resetBtn.className = 'reset-cloze-btn';
    resetBtn.style.position = 'fixed';
    resetBtn.style.bottom = '10px';
    resetBtn.style.left = '50%';
    resetBtn.style.transform = 'translateX(-50%)';
    resetBtn.style.zIndex = '9999';
    resetBtn.style.padding = '80px 140px';
    resetBtn.style.cursor = 'pointer';
    document.body.appendChild(resetBtn);
    resetBtn.addEventListener('click', resetClozes);
  }
}

// Setup function
function setupClozeReveal() {
  var cardNumber = getCardNumber();
  var rawTextElement = document.getElementById("rawText");
  if (!rawTextElement) return; // Exit if rawText is not found
  var text = rawTextElement.innerHTML;
  clozes = getClozes(text, cardNumber);
  var clozeElements = document.querySelectorAll(".cloze");

  if (clozes.length !== clozeElements.length) {
    return; // Skip if mismatch
  }

  // Wrap each cloze in a precision container
  elements = Array.from(clozeElements).map(el => {
    const wrapper = document.createElement("span");
    wrapper.className = "cloze-wrapper";
    el.parentNode.insertBefore(wrapper, el);
    wrapper.appendChild(el);
    return el;
  });

  // Attach listeners with precise click detection
  elements.forEach((el, i) => {
    el.addEventListener('click', e => {
      e.stopPropagation();
      const rect = el.getBoundingClientRect();
      const clickX = e.clientX;
      const clickY = e.clientY;
      if (clickX >= rect.left && clickX <= rect.right && clickY >= rect.top && clickY <= rect.bottom) {
        revealCloze(i);
      }
    });
  });

  // Load and apply state
  revealed = loadRevealedState();
  applyRevealedState();

  // Create the reset button
  createResetButton();
}

// Run setup when DOM is fully loaded
document.addEventListener("DOMContentLoaded", setupClozeReveal);
</script>

<script id="rawText" type="text/plain">
{{Text}}
</script>

What is so weird about iOS which is not the same as AnkiDroid or Anki Desktop

I think it’s Safari’s fault. AnkiDroid and desktop can use the same kind of browser to render the card webview but iOS has to use Safari which is different.

1 Like

Try changing this (this part is almost at the end)

// Run setup when DOM is fully loaded
document.addEventListener("DOMContentLoaded", setupClozeReveal);

to simply this:

setupClozeReveal();

As the DOMContentLoaded event doesn’t necessarily ever happen, depending on the template.

I made the change, but still…there is no change to be seen on iOS…

Well, I give up. If you’re ever in the market for a new tablet, instead of an iPad, I recommend getting a Windows 11 tablet because then you can run desktop Anki on it and be able to use addons.

1 Like

like most real problems, you can’t solve this with AI alone without knowing how JS, DOM, events and other web APIs work. you can consult MDN. if you’re completely okay with putting effort (and time!) into this, read further.

On a high level, you need to know is that all Anki clients work differently and share about most of frontend behaviour to support the simple styled card templates. to support more advanced card templates with custom behavior, we must take account into the technical differences. let’s continue troubleshooting.

have you tried eruda on its own on a basic card template? if it works, comment out the original card template script’s entry point like below. this will deactivate the script:

Deactivation snippet
<script>
/* --- contents of the first <script> element --- */
...
      resetBtn.style.zIndex = '9999';
      resetBtn.style.padding = '80px 140px';
      resetBtn.style.cursor = 'pointer';
      document.body.appendChild(resetBtn);
      resetBtn.addEventListener('click', resetClozes);
    }
  }

//   // Setup on card update
//   onUpdateHook.push(function() {
//     var cardNumber = getCardNumber();
//     var text = document.getElementById("rawText").innerHTML;
//     clozes = getClozes(text, cardNumber);
//     var clozeElements = document.querySelectorAll(".cloze");
  
//     if (clozes.length !== clozeElements.length) {
//       return; // Skip if mismatch
...
</script>

<script id="rawText" type="text/plain">
{{Text}}
</script>

try using eruda in the template again. if it doesn’t work, the effort needed to get your thing working just got a bit tedious. Ask your favourite AI to come up with “a simple log-to-HTML element function where the devtools panel is unavailable.”. It’ll likely won’t work to display HTML elements and other unserializable objects, so you need to pull out the values you want to see into a string.

try to learn how to debug not necessarily with a debugger. printing the state of things is fine. here’s some things I would check for:

  • Is onUpdateHook actually supported on AnkiMobile? print out its value in the console.
  • make the main logic code work again by uncommenting them piecewise. print the results as you allow lines to work again piece by piece. please don’t make mistakes.
  • does the click event actually work on ipad? there’s a few of those in the script.
  • does the Regexs actually work to obtain the right values?
  • does the click/tap detector actually work?
      // Attach listeners with precise click detection
      elements.forEach((el, i) => {
          el.addEventListener('click', e => {
          e.stopPropagation();
          const rect = el.getBoundingClientRect();
          const clickX = e.clientX;
          const clickY = e.clientY;
          if (clickX >= rect.left && clickX <= rect.right && clickY >= rect.top && clickY <= rect.bottom) {
              revealCloze(i);
          }
          });
      });
    
  • check the document HTML. does it meet your expectations?

i can’t do much than this because i don’t have AnkiMobile. You are mostly on your own until someone else that does use AnkiMobile can chime in.

2 Likes

So I found a workaround and now this code works on iOS as well. There is a problem though. There is a “Reset Cloze” button and I wanted it to be fixed to the bottom of the page but without it being scrolled with the text. On Desktop, this functions normally. On iOS, it doesnt. So could you help me find a workaround this time :slightly_smiling_face:.



<style>
  /* Your existing styles */
  .cloze { display: inline; padding: 0 px; margin: 0; white-space: nowrap; border-radius: 3px; }
  /* .cloze-wrapper { display: inline; padding: 0; margin: 0; } */ /* Wrapper not used */
  .reset-cloze-btn { position: fixed !important; bottom: 15px; left: 50%; transform: translateX(-50%); z-index: 99999 !important; display: none; padding: 10px 20px; font-size: 14px; color: white; background-color: #555; border: 1px solid #777; border-radius: 5px; cursor: pointer; opacity: 0.9; user-select: none; }
</style>

<script>
// --- Global variables ---
var elements = [];
var clozes = [];
var revealed = [];
var currentCardNumber = "0";
// **NEW**: Store previous elements and listener functions for cleanup
var previousElements = [];
var currentTouchendHandler = null; // Store the function currently attached
var currentClickHandler = null;  // Store the function currently attached

// --- All Helper Functions (getCardNumber through setupResetButton) ---
// --- PASTE ALL HELPER FUNCTIONS FROM THE PREVIOUS VERSION HERE ---
// (getCardNumber, getClozes, styleCloze, applyRevealedState, save/load state,
// revealCloze, resetClozes, handleResetInteraction, setupResetButton)
// --- Make sure handleClozeInteraction is NOT defined here anymore ---
// --- It will be defined dynamically inside setupClozes ---
function getCardNumber() { /* ... same ... */
  var clz = document.body.className; const regex = /card(\d+)/gm; let m;
  if ((m = regex.exec(clz)) !== null) { return m[1]; }
  if (document.body.classList.contains('front')) { return "1"; } return "0";
}
function getClozes(str, cardNumber) { /* ... same ... */
  if (!cardNumber || cardNumber === "0") return [];
  const regex = new RegExp(`\\{\\{c${cardNumber}::((?:[^{}]|\\{\\{[^{}]*\\}\\})+)(?:::([^}]*))?\\}\\}`, 'gm');
  let m; const clozes = [];
  while ((m = regex.exec(str)) !== null) {
    const cleanAnswer = m[1].replace(/\{\{c\d+::(.*?)(::.*?)?\}\}/g, '$1').trim();
    clozes.push({ answer: cleanAnswer, hint: (typeof m[2] !== "undefined" && m[2].trim() !== "") ? m[2].trim() : "[...]" });
  } return clozes;
}
function styleCloze(el, isRevealed) { /* ... same ... */
    const isFront = (currentCardNumber === "1");
    if (isRevealed) { el.style.color = "white"; el.style.backgroundColor = "black"; el.style.fontWeight = "normal"; el.style.textDecoration = "none"; el.style.cursor = "default"; }
    else { el.style.backgroundColor = "transparent"; el.style.textDecoration = "underline"; el.style.cursor = "pointer"; el.style.fontWeight = "bold"; el.style.color = isFront ? "#BD00FD" : "gold"; }
}
function applyRevealedState() { /* ... same ... */
  if (!elements || elements.length !== clozes.length) return;
  elements.forEach((el, i) => { if (clozes[i] && revealed[i] !== undefined) { el.innerHTML = revealed[i] ? clozes[i].answer : clozes[i].hint; styleCloze(el, revealed[i]); }});
}
function saveRevealedState() { /* ... same ... */ if (currentCardNumber !== "0" && elements && elements.length > 0) { localStorage.setItem("cloze_revealed_c" + currentCardNumber, JSON.stringify(revealed)); } }
function loadRevealedState() { /* ... same ... */
    const expectedLength = (elements && elements.length) ? elements.length : 0; if (currentCardNumber === "0" || expectedLength === 0) return [];
    let stored = localStorage.getItem("cloze_revealed_c" + currentCardNumber);
    if (stored) { try { const parsedState = JSON.parse(stored); if (Array.isArray(parsedState) && parsedState.length === expectedLength) return parsedState; else console.warn("State length mismatch."); } catch (e) { console.error("Error parsing state.", e); } }
    return new Array(expectedLength).fill(false);
}
function revealCloze(i) { /* ... same ... */ if (revealed[i] === undefined) return; revealed[i] = !revealed[i]; if (elements && elements[i] && clozes[i]) { el = elements[i]; el.innerHTML = revealed[i] ? clozes[i].answer : clozes[i].hint; styleCloze(el, revealed[i]); } saveRevealedState(); }
function resetClozes() { /* ... same ... */ if (!elements || elements.length === 0) return; revealed = new Array(elements.length).fill(false); applyRevealedState(); saveRevealedState(); }
// Note: handleResetInteraction might need cleanup too if issues persist
function handleResetInteraction(event) { event.preventDefault(); resetClozes(); }
// setupResetButton should ideally only add listeners *once* when creating the button
var resetButtonListenerAttached = false; // Flag for reset button

function setupResetButton() {
    let resetBtn = document.getElementById('resetCloze');
    if (!resetBtn) { resetBtn = document.createElement('button'); resetBtn.id = 'resetCloze'; resetBtn.textContent = 'Reset Clozes'; resetBtn.className = 'reset-cloze-btn';
        // Add listeners only ONCE during creation
        resetBtn.addEventListener('touchend', handleResetInteraction, { passive: false });
        resetBtn.addEventListener('click', handleResetInteraction);
        document.body.appendChild(resetBtn); resetButtonListenerAttached = true; }
    resetBtn.style.display = (elements && elements.length > 0) ? 'block' : 'none';
}


// --- Main Setup Logic (Revised with Cleanup) ---
function setupClozes() {
    // --- 1. CLEANUP: Remove listeners attached in the PREVIOUS run ---
    // console.log("--- Running setupClozes ---");
    if (previousElements.length > 0) {
        // console.log(`Cleaning up ${previousElements.length} old listeners.`);
        previousElements.forEach((oldEl) => {
            // Check if the element still exists and listeners were attached
            if (oldEl && currentTouchendHandler && currentClickHandler) {
                oldEl.removeEventListener('touchend', currentTouchendHandler);
                oldEl.removeEventListener('click', currentClickHandler);
            }
        });
    }
    // Clear references after cleanup
    previousElements = [];
    currentTouchendHandler = null;
    currentClickHandler = null;

    // --- 2. Reset state for the NEW card (Essential) ---
    elements = []; clozes = []; revealed = [];
    currentCardNumber = getCardNumber();
    // console.log("Card Number:", currentCardNumber);

    // --- 3. Get content, process clozes, validate (same as before) ---
    var rawTextEl = document.getElementById("rawText");
    if (!rawTextEl) { console.error("Raw text element not found."); setupResetButton(); return; }
    var text = rawTextEl.innerHTML;
    clozes = getClozes(text, currentCardNumber);
    var clozeElementsNodeList = document.querySelectorAll(".cloze");
    if (clozes.length === 0 || clozes.length !== clozeElementsNodeList.length) {
        console.warn("Cloze mismatch/none found."); setupResetButton(); return; }

    // --- 4. Assign elements directly (NO WRAPPER) ---
    elements = Array.from(clozeElementsNodeList);
    // console.log(`Assigned ${elements.length} elements.`);

    // --- 5. Load revealed state (same as before) ---
    revealed = loadRevealedState();

    // --- 6. Define and Attach NEW listeners ---
    // Define handlers *inside* setupClozes or ensure they correctly reference current 'elements'
    // Using inline functions that capture 'i' and call revealCloze is often easiest here
    // We still need references to remove them later.

    // Create unique listener functions for this run
    currentTouchendHandler = function(event) {
        // Find index based on the current 'elements' array
        const index = elements.indexOf(event.currentTarget);
        if (index !== -1) {
            event.preventDefault();
            revealCloze(index);
        }
    };
    currentClickHandler = function(event) {
        const index = elements.indexOf(event.currentTarget);
        if (index !== -1) {
            revealCloze(index);
        }
    };

    // console.log("Attaching new listeners...");
    elements.forEach((el) => {
        el.addEventListener('touchend', currentTouchendHandler, { passive: false });
        el.addEventListener('click', currentClickHandler);
    });

    // --- 7. Store references for NEXT cleanup ---
    previousElements = elements; // Store the *current* elements

    // --- 8. Apply Initial State & Setup Button ---
    applyRevealedState();
    setupResetButton(); // Ensure button visibility is correct
    // console.log("--- setupClozes Finished ---");
}


// --- REMOVE MutationObserver Setup ---
// Delete the entire block for MutationObserver, handleMutation, observer.observe etc.


// --- USE onUpdateHook Directly (NO DELAY) ---
if (typeof onUpdateHook !== "undefined") {
    // Avoid adding the hook function multiple times
    let hookExists = onUpdateHook.some(func => func === setupClozes);
    if (!hookExists) {
        // console.log("Registering setupClozes directly in onUpdateHook.");
        onUpdateHook.push(setupClozes); // Add setupClozes function directly
    }

     // Initial run - run directly, no delay
     // console.log("Performing initial setup call directly.");
     setupClozes();

} else {
  // Fallback for older Anki versions - run directly
  // console.log("Using fallback setup directly.");
  setupClozes();
}
// --- END OF onUpdateHook Setup ---

</script>

<script id="rawText" type="text/plain">{{Text}}</script>


The reset button is in this part of the code

function setupResetButton() {
    let resetBtn = document.getElementById('resetCloze');
    if (!resetBtn) { resetBtn = document.createElement('button'); resetBtn.id = 'resetCloze'; resetBtn.textContent = 'Reset Clozes'; resetBtn.className = 'reset-cloze-btn';
        // Add listeners only ONCE during creation
        resetBtn.addEventListener('touchend', handleResetInteraction, { passive: false });
        resetBtn.addEventListener('click', handleResetInteraction);
        document.body.appendChild(resetBtn); resetButtonListenerAttached = true; }
    resetBtn.style.display = (elements && elements.length > 0) ? 'block' : 'none';
}

Here is the CSS for it:

#resetCloze {
   /* --- Positioning: Static flow, centered --- */
   display: none; /* Initially hidden, shown via JS */
   /* Use block display and margin auto for horizontal centering */
   display: block; /* Overridden by JS later if needed */
   margin-left: auto;
   margin-right: auto;
   /* Add margin for spacing from content above/below */
   margin-top: 25px;
   margin-bottom: 15px;

   /* --- Appearance (Keep these, ensure background-color is valid) --- */
   padding: 10px 2000px;
   font-size: 14px;
   color: white;
   background-color: transparent; /* Corrected background color */
   border: 1px solid #777;
   border-radius: 5px;
   cursor: pointer;
   opacity: 0.9;
   user-select: none;
   max-width: 200px; /* Optional: constrain width */

}

There is also a style block in the html part

<style>
  /* Your existing styles */
  .cloze { display: inline; padding: 0 px; margin: 0; white-space: nowrap; border-radius: 3px; }
  /* .cloze-wrapper { display: inline; padding: 0; margin: 0; } */ /* Wrapper not used */
  .reset-cloze-btn { position: fixed !important; bottom: 15px; left: 50%; transform: translateX(-50%); z-index: 99999 !important; display: none; padding: 10px 20px; font-size: 14px; color: white; background-color: #555; border: 1px solid #777; border-radius: 5px; cursor: pointer; opacity: 0.9; user-select: none; }
</style>

After some diagnostic with AI, it thinks that CSS position: fixed is Broken. So @dae you might want to have a look at this.

Why can’t we “fix” it just for iOS?

  • CSS position: fixed is Broken: The fundamental mechanism is not behaving as expected on iOS in this context. We can’t force it to work if the browser/webview itself isn’t honoring it correctly due to ancestor styles/structure.
1 Like

How exactly does it not work on iOS? With some googling on “Safari position fixed” I found that this has been a problem for a long time. Some workarounds exist. html - Fixed position footer scrolls in iPhone

Maybe get the AI to read that article or prompt it to provide the suggested workaround “use the Safari workaround -webkit-overflow-scrolling to fix the position: fixed issue”

1 Like

With position:fixed it the button should appear in a fixed position uninfluenced by scroll. iOS instead disregards that and the button rests at the very end of the cloze note, at the very end of the scroll.

That did not work…And the other workarounds inside the article did not bring much success either…

Hello @kleinerpirat. Sorry to bother you. I have stumbled upon your old post because it seemed to be relevant to the issue I having now. I tried the suggested workaround but I am not able to get it to work. Could you help me :red_question_mark: