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