/* widgets: Rekenen (+ - x : %) */
/* =========================================================
Math practice engine โ add / sub / mul / div / percentages
==========================================================*/
function mountMath(root, initState, mode, opts){
const ROUND = 10;
const onProgress = opts && opts.onProgress;
const S = { level:1, snd:true, amode:"blocks", q:null, tries:0, done:0, good:0, input:"", busy:false };
if(initState){
S.level = initState.level||1;
S.snd = initState.snd!==false;
S.amode = initState.amode||"blocks";
}
const play = f=>{ if(S.snd) f(); };
const rnd = n => Math.floor(Math.random()*n);
root.innerHTML = `
`;
const el = s => root.querySelector(s);
const mtEl = el(".mt"), qEl = el(".mt-q"), aEl = el(".mt-a"), fbEl = el(".mt-fb"),
scoreEl = el(".mt-score"), doneEl = el(".mt-done"),
sndBtn = el(".lg-snd"), modeBtn = el(".lg-mode"),
padEl = el(".mt-pad"), slotsEl = el(".mt-slots"), trayEl = el(".mt-tray");
/* ---- digit blocks with dot patterns (like the letter blocks) ---- */
function makeDigitBlock(d){
const b = document.createElement("div");
b.className = "blok sm";
b.dataset.digit = d;
b.innerHTML = `${d}
`;
const dg = dotGrid(d); dg.classList.add("blok-dots");
b.appendChild(dg);
attachDigitDrag(b);
return b;
}
"0123456789".split("").forEach(d=>trayEl.appendChild(makeDigitBlock(d)));
function makeDigitSlot(d){
const s = document.createElement("div");
s.className = "slot sm";
s.dataset.digit = d;
s.innerHTML = ``;
const dg = dotGrid(d); dg.classList.add("slot-dots");
s.appendChild(dg);
s.addEventListener("click", ()=>{
const placed = s.querySelector(".blok");
if(placed && Date.now() - (+placed.dataset.placedAt||0) > 400){
placed.remove();
s.classList.remove("filled");
}
});
return s;
}
function attachDigitDrag(block){
block.addEventListener("pointerdown", e=>{
if(e.pointerType==="mouse" && e.button!==0) return;
if(S.busy || doneEl.classList.contains("show")) return;
e.preventDefault();
try{ block.releasePointerCapture(e.pointerId); }catch(_){}
const clone = block.cloneNode(true);
clone.id = "dragClone";
document.body.appendChild(clone);
const moveTo = ev=>{ const zf = VZ(); clone.style.left = (ev.clientX/zf)+"px"; clone.style.top = (ev.clientY/zf)+"px"; };
moveTo(e);
let hovered = null;
const hit = ev=>{
const pad = 8;
let found = null;
slotsEl.querySelectorAll(".slot:not(.filled)").forEach(s=>{
const r = s.getBoundingClientRect();
if(ev.clientX>=r.left-pad && ev.clientX<=r.right+pad &&
ev.clientY>=r.top-pad && ev.clientY<=r.bottom+pad) found = s;
});
return found;
};
const move = ev=>{
moveTo(ev);
const s = hit(ev);
if(hovered && hovered!==s) hovered.classList.remove("droptarget");
hovered = s;
if(hovered) hovered.classList.add("droptarget");
};
const up = ev=>{
document.removeEventListener("pointermove",move);
document.removeEventListener("pointerup",up);
document.removeEventListener("pointercancel",cancel);
clone.remove();
if(hovered) hovered.classList.remove("droptarget");
const target = hit(ev);
if(target){
if(target.dataset.digit === block.dataset.digit){
const copy = block.cloneNode(true);
copy.classList.add("in-slot");
copy.dataset.placedAt = Date.now();
target.appendChild(copy);
target.classList.add("filled");
play(sndGood);
if(!slotsEl.querySelector(".slot:not(.filled)")) blocksSolved();
}else{
target.classList.add("shake");
setTimeout(()=>target.classList.remove("shake"), 400);
play(sndBad);
}
}
};
const cancel = ()=>{
document.removeEventListener("pointermove",move);
document.removeEventListener("pointerup",up);
document.removeEventListener("pointercancel",cancel);
clone.remove();
if(hovered) hovered.classList.remove("droptarget");
};
document.addEventListener("pointermove",move);
document.addEventListener("pointerup",up);
document.addEventListener("pointercancel",cancel);
});
}
function blocksSolved(){
S.busy = true;
S.good++;
fbEl.textContent = T("mGood");
play(sndWin);
confetti(el(".mt-card"));
setTimeout(next, 1000);
}
function gen(){
const L = S.level;
let a, b, ans, txt;
if(mode==="add"){
const max = L===1 ? 10 : L===2 ? 20 : 100;
a = rnd(max+1); b = rnd(max-a+1); ans = a+b;
txt = `${a} + ${b} =`;
}else if(mode==="sub"){
const max = L===1 ? 10 : L===2 ? 20 : 100;
a = rnd(max+1); b = rnd(a+1); ans = a-b;
txt = `${a} โ ${b} =`;
}else if(mode==="mul"){
const tmax = L===1 ? 5 : L===2 ? 10 : 12;
a = 1+rnd(tmax); b = 1+rnd(L===3 ? 12 : 10); ans = a*b;
txt = `${a} ร ${b} =`;
}else if(mode==="div"){
const tmax = L===1 ? 5 : L===2 ? 10 : 12;
const t = 1+rnd(tmax), q = 1+rnd(L===3 ? 12 : 10);
a = t*q; ans = q;
txt = `${a} ${S.lang==="nl" || LANG==="nl" ? ":" : "รท"} ${t} =`;
}else{ /* pct */
const sets = L===1 ? [10,50,100] : L===2 ? [10,20,25,50,75,100] : [5,10,20,25,30,40,50,60,70,75,80,90];
const p = sets[rnd(sets.length)];
const step = {5:20,10:10,20:5,25:4,30:10,40:5,50:2,60:5,70:10,75:4,80:5,90:10,100:1}[p];
const base = step*(1+rnd(12));
ans = p*base/100;
txt = `${p}% ${T("mOf")} ${base} =`;
}
return { txt, ans };
}
function newQuestion(){
let q = gen();
if(S.q && q.txt===S.q.txt) q = gen(); /* avoid the same sum twice in a row */
S.q = q; S.tries = 0; S.input = ""; S.busy = false;
qEl.textContent = q.txt;
aEl.textContent = "";
aEl.classList.remove("ok");
fbEl.textContent = ""; fbEl.classList.remove("ans");
slotsEl.innerHTML = "";
if(S.amode==="blocks"){
String(q.ans).split("").forEach(d=>slotsEl.appendChild(makeDigitSlot(d)));
}
updateScore();
}
function updateScore(){
scoreEl.textContent = `${S.good} ${T("mGoed")} ยท ${Math.min(S.done+1,ROUND)}/${ROUND}`;
}
function endRound(){
doneEl.querySelector(".dt").textContent = T("mDoneT");
doneEl.querySelector(".ds").textContent = `${S.good}/${ROUND} ${T("mGoed")}`;
doneEl.querySelector(".dg").textContent = S.good>=8 ? "๐" : S.good>=5 ? "๐" : "๐ช";
doneEl.classList.add("show");
if(S.good>=8) play(sndWin);
if(onProgress) onProgress({ attempts: ROUND, correct: S.good, stars: 0 });
}
function next(){
S.done++;
if(S.done >= ROUND){ endRound(); }
else newQuestion();
}
function check(){
if(S.busy || S.input==="") return;
if(+S.input === S.q.ans){
S.busy = true;
S.good++;
aEl.classList.add("ok");
fbEl.textContent = T("mGood");
play(sndGood);
confetti(el(".mt-card"));
setTimeout(next, 900);
}else{
S.tries++;
aEl.classList.add("shake");
setTimeout(()=>aEl.classList.remove("shake"), 400);
play(sndBad);
S.input = ""; aEl.textContent = "";
if(S.tries >= 3){
S.busy = true;
fbEl.textContent = `${T("mAnsWas")} ${S.q.ans}`;
fbEl.classList.add("ans");
setTimeout(next, 1800);
}
}
}
["7","8","9","โซ","4","5","6","0","1","2","3","โ"].forEach(k=>{
const b = document.createElement("button");
b.textContent = k;
if(k==="โ") b.className = "ok";
b.addEventListener("click", ()=>{
if(doneEl.classList.contains("show")) return;
if(k==="โซ"){ if(!S.busy){ S.input = S.input.slice(0,-1); aEl.textContent = S.input; } }
else if(k==="โ") check();
else if(!S.busy && S.input.length<4){ S.input += k; aEl.textContent = S.input; }
});
padEl.appendChild(b);
});
el(".mt-again").addEventListener("click", ()=>{
S.done = 0; S.good = 0;
doneEl.classList.remove("show");
newQuestion();
});
root.querySelectorAll(".lg-levels button").forEach(b=>{
b.addEventListener("click", ()=>{
S.level = +b.dataset.l;
S.done = 0; S.good = 0;
doneEl.classList.remove("show");
labels(); newQuestion();
});
});
sndBtn.addEventListener("click", ()=>{
S.snd = !S.snd;
sndBtn.textContent = S.snd ? "๐" : "๐";
sndBtn.classList.toggle("off", !S.snd);
});
modeBtn.addEventListener("click", ()=>{
S.amode = S.amode==="blocks" ? "type" : "blocks";
labels(); newQuestion();
});
function labels(){
root.querySelectorAll(".lg-levels button").forEach(b=>{
b.textContent = T("lvl"+b.dataset.l);
b.classList.toggle("active", +b.dataset.l===S.level);
});
sndBtn.textContent = S.snd ? "๐" : "๐";
sndBtn.classList.toggle("off", !S.snd);
sndBtn.title = T("sndWidget");
modeBtn.textContent = S.amode==="blocks" ? "๐งฑ" : "โจ๏ธ";
modeBtn.title = T("amode");
mtEl.classList.toggle("blocks", S.amode==="blocks");
el(".mt-again").textContent = T("mAgain");
updateScore();
}
document.addEventListener("langchange", ()=>{ labels(); newQuestion(); });
labels();
newQuestion();
return {
getState: ()=>({ level:S.level, snd:S.snd, amode:S.amode }),
setState: st=>{ if(st){ S.level=st.level||1; S.snd=st.snd!==false; S.amode=st.amode||"blocks"; labels(); newQuestion(); } }
};
}