teach/public/js/widgets/sentence.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

75 lines
6 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: zinsbouwer met aanpasbare woordbanken */
function mountSentence(root, initState){
const year=learningYearGroup(initState?.yearGroup);
const early=year&&year<=3,advanced=year&&year>=7;
const defaults=LANG==='nl' ? (early ? {
who:['De hond','Ik','Sam','De juf'],does:['leest','speelt','maakt','ziet'],what:['een boek','buiten','een huis','de bal'],
} : advanced ? {
who:['Hoewel de bron','De schrijver','Onze klas','Volgens de onderzoeker'],does:['vergelijkt','beargumenteert','verklaart','beoordeelt'],what:['de informatie kritisch','twee standpunten','de uitkomst zorgvuldig','het bewijs in de tekst'],
} : {
who:['De hond','Mijn vriend','De juf','Sara'], does:['lees','speelt','maakt','ziet'],
what:['een boek','op het plein','een mooie tekening','de rode bal'],
}) : (early ? {
who:['The dog','I','Sam','The teacher'],does:['reads','plays','makes','sees'],what:['a book','outside','a house','the ball'],
} : advanced ? {
who:['Although the source','The writer','Our class','According to the researcher'],does:['compares','argues','explains','assesses'],what:['the information critically','two viewpoints','the result carefully','the evidence in the text'],
} : {
who:['The dog','My friend','The teacher','Sarah'], does:['reads','plays','makes','sees'],
what:['a book','in the playground','a beautiful drawing','the red ball'],
});
const legacyNl={who:['De hond','Mijn vriend','De juf','Sara'],does:['lees','speelt','maakt','ziet'],what:['een boek','op het plein','een mooie tekening','de rode bal']};
const legacyEn={who:['The dog','My friend','The teacher','Sarah'],does:['reads','plays','makes','sees'],what:['a book','in the playground','a beautiful drawing','the red ball']};
const savedBanks=initState?.banks,isLegacyDefault=savedBanks&&[legacyNl,legacyEn].some(bank=>JSON.stringify(bank)===JSON.stringify(savedBanks));
const chosenBanks=initState?.managedLearningLevel&&isLegacyDefault?defaults:(savedBanks||defaults);
const S={banks:JSON.parse(JSON.stringify(chosenBanks)),sentence:[...(initState?.sentence||[])]};
root.innerHTML=`
<div class="sb">
<div class="sb-sentence" aria-live="polite"></div>
<div class="sb-banks"></div>
<details class="edu-settings widget-gear"><summary>⚙ <span class="sb-add-label"></span></summary>
<div class="sb-add edu-controls">
<select class="sb-kind"><option value="who"></option><option value="does"></option><option value="what"></option></select>
<input class="sb-word" type="text" maxlength="40">
<button class="tbtn ghost sb-add-btn" type="button"></button>
</div></details>
<div class="sb-actions"><button class="tbtn ghost sb-surprise" type="button"></button><button class="tbtn ghost sb-back" type="button">↶</button><button class="tbtn ghost sb-clear" type="button"></button><button class="tbtn sb-speak" type="button"></button></div>
</div>`;
const q=s=>root.querySelector(s), sentenceEl=q('.sb-sentence'), banksEl=q('.sb-banks'), kind=q('.sb-kind'), word=q('.sb-word');
function sentenceText(){ return S.sentence.map(x=>x.word).join(' ').replace(/\s+([,.!?])/g,'$1'); }
function renderSentence(){
sentenceEl.innerHTML="";
if(!S.sentence.length){ sentenceEl.textContent=T('sentenceHint'); sentenceEl.classList.add('empty'); return; }
sentenceEl.classList.remove('empty');
S.sentence.forEach(part=>{const span=document.createElement('span');span.className='sb-part '+part.kind;span.textContent=part.word;sentenceEl.appendChild(span);});
}
function renderBanks(){
banksEl.innerHTML="";
['who','does','what'].forEach(k=>{
const group=document.createElement('section'); group.className='sb-bank '+k;
const h=document.createElement('h3'); h.textContent=T('sentence_'+k); group.appendChild(h);
const chips=document.createElement('div'); chips.className='sb-chips';
S.banks[k].forEach((w,i)=>{
const wrap=document.createElement('span'); wrap.className='sb-chip-wrap';
const chip=document.createElement('button'); chip.type='button'; chip.className='sb-chip'; chip.textContent=w;
chip.addEventListener('click',()=>{S.sentence.push({kind:k,word:w});renderSentence();});
const remove=document.createElement('button'); remove.type='button'; remove.className='sb-chip-remove'; remove.textContent='×'; remove.title=T('sentenceRemove');
remove.addEventListener('click',()=>{S.banks[k].splice(i,1);renderBanks();}); wrap.append(chip,remove); chips.appendChild(wrap);
});
group.appendChild(chips); banksEl.appendChild(group);
});
}
function addWord(){const w=word.value.trim();if(!w)return;S.banks[kind.value].push(w);word.value='';renderBanks();}
q('.sb-add-btn').addEventListener('click',addWord); word.addEventListener('keydown',e=>{e.stopPropagation();if(e.key==='Enter')addWord();});
q('.sb-back').addEventListener('click',()=>{S.sentence.pop();renderSentence();});
q('.sb-clear').addEventListener('click',()=>{S.sentence=[];renderSentence();});
q('.sb-surprise').addEventListener('click',()=>{S.sentence=['who','does','what'].filter(k=>S.banks[k].length).map(kind=>({kind,word:S.banks[kind][Math.floor(Math.random()*S.banks[kind].length)]}));renderSentence();sndGood();confetti(sentenceEl)});
q('.sb-speak').addEventListener('click',()=>{const text=sentenceText();if(text&&'speechSynthesis'in window){speechSynthesis.cancel();const u=new SpeechSynthesisUtterance(text);u.lang=LANG==='nl'?'nl-NL':'en-GB';speechSynthesis.speak(u);}});
function labels(){
q('.sb-add-label').textContent=T('widgetSettings');
[...kind.options].forEach(o=>o.textContent=T('sentence_'+o.value)); word.placeholder=T('sentenceAdd');
q('.sb-surprise').textContent=T('sentenceSurprise');q('.sb-clear').textContent=T('sentenceClear'); q('.sb-speak').textContent=T('sentenceSpeak'); renderSentence();renderBanks();
}
root.classList.toggle("managed-learning-level",!!initState?.managedLearningLevel);
document.addEventListener('langchange',labels);labels();
return {getState:()=>({banks:S.banks,sentence:S.sentence})};
}