All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
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
132 lines
6.2 KiB
JavaScript
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}) };
|
|
}
|