teach/public/js/widgets/ball.js
Ramon d8610b010d
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
feat: compacter instellingenmenu voor acht widgets (v0.4.80-beta)
Bij veel widgets nam de instellingenstrook te veel hoogte in, vooral bij een
smal widgetvenster waar de rij naar meerdere regels uitwaaierde. Nieuw
gedeeld patroon: besturing die voortdurend nodig is (kleur, niveau,
woordbron) blijft in één compacte rij zichtbaar; besturing die maar zelden
wordt aangepast (thema, geluid aan/uit, invoerwijze, bereik) gaat achter een
inklapbaar ⚙-paneel. Dit patroon bestond al voor klok/geld/rekengetallen/
wereld (learningShell() in learning.js) - nu als widgetGear()/gearField() in
core.js beschikbaar voor de rest van de app, met dezelfde .edu-settings-
opmaak zodat het overal hetzelfde oogt en werkt.

- kleurplaten: twee volle rijen (thema/plaat + kwast/emmer/gum/kleur/dikte/
  hulp/reset/klaar) teruggebracht naar één vaste rij (gereedschap, kleur,
  dikte, klaar) plus het ingeklapte paneel (thema, plaat, kleurhulp, reset).
  Losstaand: coloring.css gebruikte @media(max-width:620px) in plaats van de
  @container widget-conventie die de rest van de app gebruikt, waardoor die
  regels nooit afgingen bij het verkleinen van het widgetvenster zelf - nu
  gecorrigeerd.
- letterblokken: geluid, invoerwijze en aantal kaarten in het paneel; niveau
  en woordbron blijven vast.
- rekenwidgets (optellen/aftrekken/keer/delen/procenten): geluid en
  invoerwijze in het paneel; niveau blijft vast.
- ballonnenwoord: geluid in het paneel.
- flitswoorden: flitstijd in het paneel.
- getallenlijn: het hele bereik-instelblok (van/tot/stap/nieuw) in het
  paneel - dat stond toch al uit voor leerlingen met een beheerd niveau.
- zinsbouwer: het rijtje voor eigen woorden toevoegen in het paneel; de
  speelknoppen (verrassing/ongedaan/wissen/voorlezen) blijven vast.
- timer: geluid en weergavevorm in het paneel; starttijden en start/reset
  blijven vast.
- geluidsmeter: de twee drempelschuifjes plus de privacynotitie in het
  paneel.

Widgets die dit patroon al hadden (klok, geld, breuken, plaatswaarde,
woordsoorten, bomen/vogels/planten, topografie, stemming, stemming-poll,
dagplanning) of waar de rij al compact genoeg was (schrijfblad, hakken en
plakken, dobbelstenen, notities, naamtrekker) zijn ongewijzigd gelaten.

Geverifieerd door elke aangepaste widget live via de echte makeWidget()/
REGISTRY in een headless browser te openen, het paneel open en dicht te
klappen en te controleren op consolefouten - ook op de minimale
widgetbreedte (coloring, minW 390). 202 tests groen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
2026-07-30 16:46:37 +02:00

132 lines
6.2 KiB
JavaScript

/* widget: Ballonnenwoord */
/* =========================================================
Balloon word widget — guess before the balloons pop
==========================================================*/
function mountBall(root, initState, opts){
const onProgress = opts && opts.onProgress;
const S = { level:1, yearGroup:null, managedLearningLevel:false, card:0, snd:true, lives:6, guessed:new Set(), over:false, order:[], idx:0, stars:0, assignmentWords:null, assignmentThemeName:"", wrong:[] };
if(initState){S.level=initState.level||1;S.yearGroup=initState.yearGroup||null;S.managedLearningLevel=!!initState.managedLearningLevel;S.card = initState.card ?? 0; S.snd = initState.snd !== false;S.assignmentWords=Array.isArray(initState.assignmentWords)?initState.assignmentWords:null;S.assignmentThemeName=typeof initState.assignmentThemeName==="string"?initState.assignmentThemeName:"";}
const assignedRules=languageTaskRules(S.yearGroup,S.level),maxLives=assignedRules?.lives||6;
const play = f=>{ if(S.snd) f(); };
root.innerHTML = `
<div class="mt">
<div class="mt-top">
<select class="fl-sel bl-src"></select>
<div class="mt-score bl-score"></div>
</div>
<div class="mt-card">
<div class="fl-pic bl-pic"></div>
<div class="bl-balloons"></div>
<div class="bl-word"></div>
<div class="bl-msg"></div>
<button class="lg-next bl-next"></button>
</div>
<div class="lg-tray bl-kbd lg-kbd" style="display:flex;"></div>
</div>`;
const el = s => root.querySelector(s);
const sndBtn=document.createElement("button");sndBtn.type="button";sndBtn.className="lg-snd";
el(".mt-top").insertAdjacentElement("beforebegin",widgetGear([gearField(T("sndWidget"),sndBtn)]));
const srcSel = el(".bl-src"), balloonsEl = el(".bl-balloons"), picEl=el(".bl-pic"),
wordEl = el(".bl-word"), msgEl = el(".bl-msg"),
nextBtn = el(".bl-next"), kbdEl = el(".bl-kbd"),
scoreEl = el(".bl-score");
function list(){
if(S.assignmentWords?.length)return languageWidgetWords(S.assignmentWords,S.yearGroup,S.level,"ball");
S.card = normWordSource(S.card);const selected=languageWidgetWords(wordSourceList(S.card),S.yearGroup,S.level,"ball");
if(selected.length)return selected;S.card="g";return languageWidgetWords(wordSourceList("g"),S.yearGroup,S.level,"ball");
}
function buildSrc(){if(S.assignmentWords?.length){srcSel.innerHTML="";srcSel.appendChild(new Option("🎯 "+(S.assignmentThemeName||T("castTheme")),"assignment"));srcSel.disabled=true;}else{srcSel.disabled=false;fillWordSourceSel(srcSel,S.card,S.yearGroup,S.level);S.card=srcSel.value||"g";}}
"abcdefghijklmnopqrstuvwxyz".split("").forEach(l=>{
const b = document.createElement("button");
b.textContent = l; b.dataset.l = l;
b.className = VOWELS.includes(l) ? "v" : "c";
b.addEventListener("click", ()=>guess(l, b));
kbdEl.appendChild(b);
});
function word(){ return list()[S.order[S.idx]][0]; }
function drawWord(reveal){
wordEl.textContent = word().split("").map(l=>(reveal || S.guessed.has(l)) ? l : "_").join(" ");
}
function drawBalloons(){
balloonsEl.textContent = "🎈".repeat(S.lives) + "💥".repeat(maxLives-S.lives);
}
function newWord(){
if(!S.order.length || S.idx >= S.order.length){
S.order = shuffle(list().map((_,i)=>i));
S.idx = 0;
}
S.lives = maxLives; S.guessed = new Set(); S.over = false; S.wrong = [];
msgEl.textContent = ""; msgEl.className = "bl-msg";
nextBtn.classList.remove("show");
kbdEl.querySelectorAll("button").forEach(b=>{ b.disabled = false; b.style.opacity = 1; });
const current=list()[S.order[S.idx]];setPic(picEl,current[1]);picEl.title=current[3]||current[0];
drawWord(false); drawBalloons();
scoreEl.textContent = S.stars ? S.stars+" ★" : "";
}
function guess(l, btn){
if(S.over || btn.disabled) return;
btn.disabled = true; btn.style.opacity = .3;
if(word().includes(l)){
S.guessed.add(l);
play(sndGood);
drawWord(false);
if(word().split("").every(x=>S.guessed.has(x))){
S.over = true; S.stars++;
msgEl.textContent = T("ballWin");
msgEl.classList.add("win");
play(sndWin); confetti(el(".mt-card"));
nextBtn.classList.add("show");
scoreEl.textContent = S.stars+" ★";
if(onProgress) onProgress(languageProgressPayload({
word: word(), mistakes: S.wrong.map(x=>({given:x, expected:word()})),
answer: S.wrong.join(", ") || word(), expected: word(), solved: true, stars: 1 }));
}
}else{
S.lives--;
S.wrong.push(l);
play(sndBad);
drawBalloons();
if(S.lives <= 0){
S.over = true;
drawWord(true);
msgEl.textContent = `${T("ballLose")} ${word()}`;
msgEl.classList.add("lose");
nextBtn.classList.add("show");
/* een kapotgegokt woord leverde tot nu toe géén enkele voortgang op -
precies de opdracht die de leerkracht wél zou willen zien */
if(onProgress) onProgress(languageProgressPayload({
word: word(), mistakes: S.wrong.map(x=>({given:x, expected:word()})),
answer: word().split("").map(x=>S.guessed.has(x)?x:"_").join(""),
expected: word(), solved: false }));
}
}
}
nextBtn.addEventListener("click", ()=>{
S.idx++;
if(S.idx >= S.order.length) celebrateAvatar(root);
newWord();
});
srcSel.addEventListener("change", ()=>{
S.card = srcSel.value;
S.order = []; newWord();
});
sndBtn.addEventListener("click", ()=>{
S.snd = !S.snd;
sndBtn.textContent = S.snd ? "🔔" : "🔕";
sndBtn.classList.toggle("off", !S.snd);
});
function labels(){
sndBtn.textContent = S.snd ? "🔔" : "🔕";
sndBtn.classList.toggle("off", !S.snd);
sndBtn.title = T("sndWidget");
nextBtn.textContent = T("next");
buildSrc();
}
document.addEventListener("langchange", ()=>{ if(!S.assignmentWords)S.card=0; S.order=[]; labels(); newWord(); });
document.addEventListener("wordschange", ()=>{ buildSrc(); S.order=[]; newWord(); });
labels(); newWord();
root.classList.toggle("managed-learning-level",S.managedLearningLevel);
return { getState: ()=>({card:S.card, snd:S.snd}) };
}