teach/public/js/widgets/hak.js
Ramon 50b9f32e91
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m56s
feat: voeg voortgangdetail toe aan de taalwidgets (v0.4.63-beta)
Gewijzigde functionaliteiten:

- Letterblokken logt per afgerond woord hoeveel pogingen het kostte en welke
  letter op welke plek fout ging (beide invoerwijzen: blokken slepen en typen).

- Ballonnenwoord logt nu ook een verloren woord; dat leverde tot nu toe geen
  enkele voortgang op. Een gewonnen woord telt de fout geraden letters mee.

- Flitswoorden stuurt de bestaande zelfcheck ("Gelukt" / "Nog oefenen") door
  naar de voortgang; die uitkomst werd eerder weggegooid.

- Hakken & plakken krijgt dezelfde zelfcheck en daarmee als laatste taalwidget
  een goed/fout-moment; de oefening zelf verandert niet.

- Voortgang-tab toont de fouten van taal: naast fout afgesloten opdrachten ook
  "na N pogingen goed" (letterblokken kent geen faalmoment) en zelfchecks.

- Reeksstappen met flitswoorden of hakken & plakken wachten nu op een echt
  afgerond moment in plaats van meteen te completeren.

Tests: 192 geslaagd (7 nieuw), plus live nagelopen in de leerlingweergave met
echte interactie per widget en een screenshot van de Voortgang-tab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126hjBPD2ggZ2LEoDYZSW7S
2026-07-26 03:16:28 +02:00

147 lines
7.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* widget: Hakken & plakken */
/* Veelvoorkomende spellingpatronen blijven als herkenbare klankgroep bij elkaar.
De langste groep wint, zodat bijvoorbeeld 'sch' niet als 's' + 'ch' eindigt. */
function soundChunks(word, lang=LANG){
const patterns=(lang==='nl'
? ['sch','aai','ooi','oei','eeuw','ieuw','ng','nk','ch','aa','ee','oo','uu','oe','ie','ei','ij','ou','au','ui','eu']
: ['tch','igh','sh','ch','th','ph','ng','ee','oo','ea','ou','ow','ai','ay','oi']).sort((a,b)=>b.length-a.length);
const chunks=[];
for(let i=0;i<word.length;){
const rest=word.slice(i).toLocaleLowerCase(lang==='nl'?'nl':'en');
const match=patterns.find(pattern=>rest.startsWith(pattern));
const length=match?match.length:1;
chunks.push(word.slice(i,i+length)); i+=length;
}
return chunks;
}
/* =========================================================
Chop & blend widget — words in sounds, with read-aloud
==========================================================*/
function mountHak(root, initState, opts){
const onProgress = opts && opts.onProgress;
const S = { level:1, yearGroup:null, managedLearningLevel:false, card:0, idx:0, order:[], chopped:false, assignmentWords:null, assignmentThemeName:"" };
/* deze widget had als enige taalwidget geen goed/fout-moment: hakken, plakken
en voorlezen kun je niet fout doen. De zelfcheck van flits.js (lukte het
zelf?) geeft de leerling er één zonder de oefening zelf te veranderen -
hij verschijnt pas nadat het woord onthuld is, dus na een eerlijke poging */
let assessed = false;
if(initState){S.level=initState.level||1;S.yearGroup=initState.yearGroup||null;S.managedLearningLevel=!!initState.managedLearningLevel;S.card = initState.card ?? 0;S.assignmentWords=Array.isArray(initState.assignmentWords)?initState.assignmentWords:null;S.assignmentThemeName=typeof initState.assignmentThemeName==="string"?initState.assignmentThemeName:"";}
const canSpeak = "speechSynthesis" in window;
root.innerHTML = `
<div class="mt">
<div class="mt-top">
<select class="fl-sel hk-src"></select>
<div class="mt-score hk-cnt"></div>
</div>
<p class="play-mission hk-hint"></p>
<div class="mt-card">
<div class="fl-pic hk-pic"></div>
<div class="hk-word"></div>
</div>
<div class="hk-reflect" hidden>
<p class="hk-question"></p>
<div><button class="tbtn hk-got"></button><button class="tbtn ghost hk-again"></button></div>
</div>
<div class="trow" style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap;flex:none;">
<button class="tbtn hk-say" ${canSpeak?"":'style="display:none"'}></button>
<button class="tbtn ghost hk-chop"></button>
<button class="tbtn ghost hk-glue"></button>
<button class="tbtn ghost hk-next">▸</button>
</div>
</div>`;
const el = s => root.querySelector(s);
const srcSel = el(".hk-src"), wordEl = el(".hk-word"),
picEl = el(".hk-pic"), cntEl = el(".hk-cnt"), reflect = el(".hk-reflect");
function list(){
if(S.assignmentWords?.length)return languageWidgetWords(S.assignmentWords,S.yearGroup,S.level,"hak");
S.card = normWordSource(S.card);const selected=languageWidgetWords(wordSourceList(S.card),S.yearGroup,S.level,"hak");
if(selected.length)return selected;S.card="g";return languageWidgetWords(wordSourceList("g"),S.yearGroup,S.level,"hak");
}
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";}}
function cur(){ return list()[S.order[S.idx]]; }
function newRound(){ S.order = shuffle(list().map((_,i)=>i)); S.idx = 0; }
function display(){
const [w, pic,, anchor] = cur();
const letters=languageTaskRules(S.yearGroup,S.level)?.literacyMode==="letters";
wordEl.innerHTML = "";
if(letters&&!S.chopped){
const s=document.createElement("span");s.className="hk-whole";s.textContent="?";wordEl.appendChild(s);
}else if(S.chopped){
soundChunks(w).forEach((l,i)=>{
if(i>0){
const sep = document.createElement("span");
sep.className = "hk-sep"; sep.textContent = "";
wordEl.appendChild(sep);
}
const s = document.createElement("span");
s.className = "hk-l " + (/[aeiouyáéíóúäëïöü]/i.test(l) ? "v" : "c");
s.textContent = l;
s.style.animationDelay = (i*0.08)+"s";
wordEl.appendChild(s);
});
}else{
const s = document.createElement("span");
s.className = "hk-whole";
s.textContent = w;
wordEl.appendChild(s);
}
setPic(picEl, pic);
picEl.title = anchor||w;
cntEl.textContent = `${T("word")} ${S.idx+1}/${S.order.length}`;
}
function speak(text, rate){
if(!canSpeak) return;
speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text);
u.lang = LANG==="nl" ? "nl-NL" : "en-GB";
u.rate = rate;
speechSynthesis.speak(u);
}
el(".hk-say").addEventListener("click", ()=>{
const [word,,,anchor]=cur(),letters=languageTaskRules(S.yearGroup,S.level)?.literacyMode==="letters";
if(letters)speak(anchor||word,0.75);
else if(S.chopped)speak(soundChunks(word).join(", "),0.7);
else speak(word,0.85);
});
function assess(ok){
if(assessed) return;
assessed = true;
if(ok){ sndGood(); confetti(el(".mt-card")); } else sndBad();
if(onProgress) onProgress(languageProgressPayload({
word: cur()[0], solved: ok, stars: ok?1:0, selfCheck: true }));
reflect.hidden = true;
}
function askSelfCheck(){ reflect.hidden = assessed; }
el(".hk-chop").addEventListener("click", ()=>{ S.chopped = true; display(); play(sndGood); askSelfCheck(); });
el(".hk-glue").addEventListener("click", ()=>{ S.chopped = false; display(); play(sndGood); askSelfCheck(); });
el(".hk-got").addEventListener("click", ()=>assess(true));
el(".hk-again").addEventListener("click", ()=>assess(false));
el(".hk-next").addEventListener("click", ()=>{
S.idx++; if(S.idx >= S.order.length) newRound();
S.chopped = false; assessed = false; reflect.hidden = true; display();
});
const play = f=>f();
srcSel.addEventListener("change", ()=>{
S.card = srcSel.value;
newRound(); S.chopped = false; assessed = false; reflect.hidden = true; display();
});
function labels(){
const letters=languageTaskRules(S.yearGroup,S.level)?.literacyMode==="letters";
el(".hk-question").textContent = T(letters?"hakCheckLetter":"hakCheck");
el(".hk-got").textContent = T("flGotIt");
el(".hk-again").textContent = T("flAgain");
el(".hk-say").textContent = T("hakSay");
el(".hk-chop").textContent = letters?(LANG==="nl"?"Toon letter":"Show letter"):T("hakChop");
el(".hk-glue").textContent = letters?(LANG==="nl"?"Verberg letter":"Hide letter"):T("hakGlue");
el(".hk-hint").textContent = letters?(LANG==="nl"?"Luister naar het woord. Welke letter hoor je vooraan?":"Listen to the word. Which letter do you hear first?"):T("hakHint");
buildSrc();
}
document.addEventListener("langchange", ()=>{ if(!S.assignmentWords)S.card=0; labels(); newRound(); S.chopped=false; assessed=false; reflect.hidden=true; display(); });
document.addEventListener("wordschange", ()=>{ buildSrc(); newRound(); S.chopped=false; assessed=false; reflect.hidden=true; display(); });
labels(); newRound(); display();
root.classList.toggle("managed-learning-level",S.managedLearningLevel);
return { getState: ()=>({card:S.card}) };
}