All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m56s
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
147 lines
7.5 KiB
JavaScript
147 lines
7.5 KiB
JavaScript
/* 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}) };
|
||
}
|