teach/public/js/widgets/learning.js
Ramon 72ea486518
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 44s
feat: verbeter klokopdrachten en bediening
2026-07-15 20:23:21 +02:00

69 lines
16 KiB
JavaScript
Raw 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.

/* Acht compacte educatieve en klaswidgets. Alle invoer blijft tekstContent-safe. */
function learningShell(root, settings, stage){
root.innerHTML='<div class="edu"><details class="edu-settings"><summary>⚙ <span></span></summary><div class="edu-controls"></div></details><div class="edu-stage"></div></div>';
root.querySelector('summary span').textContent=T('widgetSettings');
root.querySelector('.edu-controls').append(...settings);
root.querySelector('.edu-stage').append(stage);
}
function field(label, el){const w=document.createElement('label');const s=document.createElement('span');s.textContent=label;w.append(s,el);return w}
function btn(text,fn,kind='ghost'){const b=document.createElement('button');b.type='button';b.className='tbtn '+kind;b.textContent=text;b.onclick=fn;return b}
function num(value,min,max){const i=document.createElement('input');i.type='number';i.value=value;i.min=min;i.max=max;return i}
function mountFractions(root,init){
const S=Object.assign({den:4,show:true},init||{}), den=num(S.den,2,12), show=document.createElement('input'), stage=document.createElement('div');show.type='checkbox';show.checked=S.show;
const draw=()=>{S.den=Math.max(2,Math.min(12,+den.value||4));S.show=show.checked;stage.className='fraction-wall';stage.innerHTML='';for(let d=1;d<=S.den;d++){const row=document.createElement('div');for(let n=0;n<d;n++){const c=document.createElement('button');c.type='button';c.style.setProperty('--fraction-hue',d*31);c.textContent=S.show?`1/${d}`:'';c.className='fraction-piece';c.onclick=()=>c.classList.toggle('selected');row.append(c)}stage.append(row)}};
den.onchange=show.onchange=draw;learningShell(root,[field(T('fractionRows'),den),field(T('fractionLabels'),show)],stage);draw();return{getState:()=>({...S})};
}
function mountClock(root,init){
const S=Object.assign({h:12,m:0,level:'quarter',style:'both',game:'set',task:false,targetH:3,targetM:30,score:0,streak:0,solved:false,feedback:'',choices:[]},init||{});
const level=document.createElement('select'),style=document.createElement('select'),game=document.createElement('select'),stage=document.createElement('div');
const add=(el,items)=>items.forEach(([value,key])=>el.add(new Option(T(key),value)));
add(level,[['hour','clockLevelHour'],['quarter','clockLevelQuarter'],['five','clockLevelFive'],['minute','clockLevelMinute']]);
add(style,[['analogue','clockStyleAnalogue'],['digital','clockStyleDigital'],['words','clockStyleWords'],['both','clockStyleBoth']]);
add(game,[['set','clockGameSet'],['read','clockGameRead'],['choose','clockGameChoose']]);
level.value=S.level;style.value=S.style;game.value=S.game;
const minuteValues=()=>S.level==='hour'?[0]:S.level==='quarter'?[0,15,30,45]:S.level==='five'?Array.from({length:12},(_,i)=>i*5):Array.from({length:60},(_,i)=>i);
const time=(hour,minute)=>`${hour}:${String(minute).padStart(2,'0')}`;
const words=(hour,minute)=>{const next=hour===12?1:hour+1;if(minute===0)return T('clockWordsHour').replace('{hour}',hour);if(minute===15)return T('clockWordsQuarterPast').replace('{hour}',hour);if(minute===30)return T('clockWordsHalf').replace('{hour}',next);if(minute===45)return T('clockWordsQuarterTo').replace('{hour}',next);return T(minute<30?'clockWordsPast':'clockWordsTo').replace('{minutes}',minute<30?minute:60-minute).replace('{hour}',minute<30?hour:next)};
const randomTime=()=>{const values=minuteValues();return{h:1+Math.floor(Math.random()*12),m:values[Math.floor(Math.random()*values.length)]}};
const woodDigit=value=>{const block=document.createElement('span');block.className='blok sm clock-digit-block';const label=document.createElement('span');label.className='bl';label.textContent=String(value).padStart(2,'0');const dots=dotGrid(Number(value)%10);dots.classList.add('blok-dots');block.append(label,dots);return block};
const timeBlocks=(hour,minute,click)=>{const el=document.createElement(click?'button':'div');if(click)el.type='button';el.className='clock-time-blocks';el.setAttribute('aria-label',time(hour,minute));el.append(woodDigit(hour));const colon=document.createElement('strong');colon.textContent=':';el.append(colon,woodDigit(minute));if(click)el.onclick=click;return el};
const makeClock=(hour,minute,interactive)=>{const face=document.createElement('div');face.className='clock'+(interactive?' interactive':'');face.setAttribute('role','img');face.setAttribute('aria-label',time(hour,minute));for(let i=0;i<60;i++){const tick=document.createElement('i');tick.className=i%5?'clock-tick minor':'clock-tick';tick.style.transform=`rotate(${i*6}deg)`;face.append(tick)}[12,1,2,3,4,5,6,7,8,9,10,11].forEach((n,i)=>{const mark=document.createElement('b');mark.textContent=n;mark.style.setProperty('--clock-angle',`${i*30}deg`);face.append(mark)});const hourHand=document.createElement('i'),minuteHand=document.createElement('i'),pin=document.createElement('span');hourHand.className='clock-hand clock-hour';minuteHand.className='clock-hand clock-min';pin.className='clock-pin';const paint=()=>{hourHand.style.transform=`rotate(${S.h*30+S.m/2}deg)`;minuteHand.style.transform=`rotate(${S.m*6}deg)`};if(!interactive){hourHand.style.transform=`rotate(${hour*30+minute/2}deg)`;minuteHand.style.transform=`rotate(${minute*6}deg)`}else paint();face.append(hourHand,minuteHand,pin);if(interactive){const move=e=>{const r=face.getBoundingClientRect(),dx=e.clientX-r.left-r.width/2,dy=e.clientY-r.top-r.height/2,deg=(Math.atan2(dx,-dy)*180/Math.PI+360)%360;if(face.dataset.hand==='minute'){const wanted=Math.round(deg/6)%60,allowed=minuteValues(),distance=v=>Math.min(Math.abs(v-wanted),60-Math.abs(v-wanted));S.m=allowed.reduce((best,v)=>distance(v)<distance(best)?v:best)}else S.h=(Math.round((deg-S.m/2)/30)+11)%12+1;paint();const answer=stage.querySelector('.clock-live-answer');if(answer){answer.innerHTML='';answer.append(timeBlocks(S.h,S.m))}};face.onpointerdown=e=>{if(S.solved)return;e.preventDefault();const r=face.getBoundingClientRect(),dx=e.clientX-r.left-r.width/2,dy=e.clientY-r.top-r.height/2;face.dataset.hand=Math.hypot(dx,dy)>r.width*.22?'minute':'hour';face.setPointerCapture(e.pointerId);move(e)};face.onpointermove=e=>{if(face.hasPointerCapture(e.pointerId))move(e)};face.onpointerup=e=>{move(e);face.releasePointerCapture(e.pointerId);delete face.dataset.hand};face.onpointercancel=e=>{if(face.hasPointerCapture(e.pointerId))face.releasePointerCapture(e.pointerId);delete face.dataset.hand}}return face};
const setFeedback=ok=>{if(S.solved)return;if(ok){S.solved=true;S.score++;S.streak++;S.feedback='good';sndGood()}else{S.streak=0;S.feedback='bad';sndBad()}draw()};
const check=()=>{if(S.task)setFeedback(S.h===S.targetH&&S.m===S.targetM)};
const newTask=()=>{const target=randomTime();S.targetH=target.h;S.targetM=target.m;S.h=12;S.m=0;S.task=true;S.solved=false;S.feedback='';S.choices=[];if(S.game==='choose'){S.choices=[target];while(S.choices.length<3){const option=randomTime();if(!S.choices.some(x=>x.h===option.h&&x.m===option.m))S.choices.push(option)}S.choices.sort(()=>Math.random()-.5)}draw()};
const stepper=(label,values,key)=>{const wrap=document.createElement('div');wrap.className='clock-stepper';const name=document.createElement('span');name.textContent=label;const controls=document.createElement('div'),back=btn('',()=>step(-1)),next=btn('+',()=>step(1));const value=woodDigit(S[key]);value.classList.add('clock-step-value');const step=delta=>{const index=values.indexOf(S[key]);S[key]=values[(index+delta+values.length)%values.length];S.feedback='';draw()};controls.append(back,value,next);wrap.append(name,controls);return wrap};
const draw=()=>{S.level=level.value;S.style=style.value;S.game=game.value;stage.className='clock-app';stage.innerHTML='';const top=document.createElement('div');top.className='clock-top';const score=document.createElement('span');score.className='clock-score';score.textContent=T('clockScore').replace('{score}',S.score).replace('{streak}',S.streak);top.append(score);stage.append(top);const card=document.createElement('section');card.className='clock-task-card';const title=document.createElement('h3');title.textContent=!S.task?T('clockWelcome'):S.game==='set'?T('clockSetTitle'):S.game==='read'?T('clockReadTitle'):T('clockChooseTitle');card.append(title);
if(!S.task){const intro=document.createElement('p');intro.className='clock-intro';intro.textContent=T('clockWelcomeHint');card.append(intro)}
if(S.task&&S.game==='set'){const prompt=document.createElement('div');prompt.className='clock-target';const label=document.createElement('span');label.textContent=T('clockYourTask');prompt.append(label);if(S.style==='analogue'){const example=makeClock(S.targetH,S.targetM,false);example.classList.add('clock-reference');prompt.append(example)}if(['digital','both'].includes(S.style))prompt.append(timeBlocks(S.targetH,S.targetM));if(['words','both'].includes(S.style)){const written=document.createElement('strong');written.className='clock-target-words';written.textContent=words(S.targetH,S.targetM);prompt.append(written)}card.append(prompt);const work=document.createElement('div');work.className='clock-work';work.append(makeClock(S.h,S.m,true));const live=document.createElement('div');live.className='clock-live-answer';live.append(timeBlocks(S.h,S.m));work.append(live);card.append(work)}
if(S.task&&S.game!=='set'){const work=document.createElement('div');work.className='clock-work';work.append(makeClock(S.targetH,S.targetM,false));card.append(work)}
if(S.task&&S.game==='read'){const answer=document.createElement('div');answer.className='clock-steppers';answer.append(stepper(T('clockHour'),Array.from({length:12},(_,i)=>i+1),'h'),stepper(T('clockMinute'),minuteValues(),'m'));card.append(answer)}
if(S.task&&S.game==='choose'&&!S.solved){const choices=document.createElement('div');choices.className='clock-choices wood';S.choices.forEach(option=>{const choose=()=>setFeedback(option.h===S.targetH&&option.m===S.targetM);if(S.style==='words'){const b=btn(words(option.h,option.m),choose);b.classList.add('clock-word-choice');choices.append(b)}else choices.append(timeBlocks(option.h,option.m,choose))});card.append(choices)}
const feedback=document.createElement('p');feedback.className='clock-feedback '+(S.feedback==='good'?'ok':S.feedback==='bad'?'bad':'');feedback.setAttribute('role','status');if(S.feedback)feedback.textContent=T(S.feedback==='good'?'clockCorrect':'clockTryAgain');card.append(feedback);const actions=document.createElement('div');actions.className='clock-actions';if(S.task&&S.game!=='choose'&&!S.solved)actions.append(btn(T('numberlineCheck'),check,''));actions.append(btn(T(S.task?'clockNextTask':'clockStartTask'),newTask,S.task?'ghost':''));card.append(actions);if(S.task&&S.game==='set'){const hint=document.createElement('small');hint.className='clock-drag-hint';hint.textContent=T('clockDragHint');card.append(hint)}stage.append(card)};
level.onchange=()=>{S.level=level.value;if(!minuteValues().includes(S.m))S.m=0;S.task=false;S.solved=false;S.feedback='';draw()};style.onchange=draw;game.onchange=()=>{S.task=false;S.feedback='';draw()};learningShell(root,[field(T('clockLevel'),level),field(T('clockLearningStyle'),style),field(T('clockGame'),game)],stage);draw();return{getState:()=>({...S,choices:S.choices.map(x=>({...x}))})};
}
function mountPlacevalue(root,init){
const S=Object.assign({number:1342},init||{}),input=num(S.number,0,999999),stage=document.createElement('div');
const draw=()=>{S.number=Math.max(0,Math.min(999999,+input.value||0));const digits=String(S.number).padStart(6,'0').split('');stage.className='place-grid';stage.innerHTML='';['HD','TD','D','H','T','E'].forEach((x,i)=>{const d=document.createElement('div');const b=document.createElement('b');b.textContent=x;const o=document.createElement('output');o.textContent=digits[i];d.append(b,o);stage.append(d)})};input.oninput=draw;learningShell(root,[field(T('placeNumber'),input),btn('🎲',()=>{input.value=Math.floor(Math.random()*999999);draw()})],stage);draw();return{getState:()=>({...S})};
}
function mountMoney(root,init){
const S=Object.assign({target:3.75,total:0},init||{}),stage=document.createElement('div'),out=document.createElement('output');
const draw=()=>{stage.className='money';stage.innerHTML='';const task=document.createElement('h3');task.textContent=T('moneyMake').replace('{amount}',S.target.toFixed(2));stage.append(task);[2,1,.5,.2,.1,.05].forEach(v=>stage.append(btn('€ '+v.toFixed(2),()=>{S.total=Math.round((S.total+v)*100)/100;renderTotal()})));stage.append(out,btn(T('sentenceClear'),()=>{S.total=0;renderTotal()}),btn(T('numberlineCheck'),()=>{S.total===S.target?sndGood():sndBad()},''));renderTotal()};const renderTotal=()=>out.textContent='€ '+S.total.toFixed(2);learningShell(root,[btn(T('numberlineNew'),()=>{S.target=(50+Math.floor(Math.random()*950))/100;S.total=0;draw()})],stage);draw();return{getState:()=>({...S})};
}
function mountWordtypes(root,init){
const S=Object.assign({sentence:'De vrolijke hond rent snel',answers:{}},init||{}),input=document.createElement('input'),stage=document.createElement('div');input.value=S.sentence;
const types=['noun','verb','adjective','other'],draw=()=>{S.sentence=input.value.slice(0,120);stage.className='wordtypes';stage.innerHTML='';S.sentence.split(/\s+/).filter(Boolean).forEach((word,i)=>{const w=document.createElement('button');w.type='button';w.textContent=word;const type=S.answers[i]||'other';w.dataset.type=type;w.onclick=()=>{S.answers[i]=types[(types.indexOf(w.dataset.type)+1)%types.length];draw()};stage.append(w)});const legend=document.createElement('p');legend.textContent=T('wordtypeHint');stage.append(legend)};input.oninput=draw;learningShell(root,[field(T('wordtypeSentence'),input)],stage);draw();return{getState:()=>({...S})};
}
function mountSchedule(root,init){
const S=Object.assign({items:[{time:'08:30',text:'Start'},{time:'10:15',text:'Pauze'}]},init||{}),time=document.createElement('input'),text=document.createElement('input'),stage=document.createElement('div');time.type='time';time.value='09:00';text.placeholder=T('scheduleActivity');
const draw=()=>{stage.className='schedule';stage.innerHTML='';S.items.sort((a,b)=>a.time.localeCompare(b.time)).forEach((it,i)=>{const row=document.createElement('div');const t=document.createElement('time');t.textContent=it.time;const s=document.createElement('span');s.textContent=it.text;row.append(t,s,btn('×',()=>{S.items.splice(i,1);draw()}));stage.append(row)});if(!S.items.length){const e=document.createElement('p');e.className='edu-empty';e.textContent=T('scheduleEmpty');stage.append(e)}};
learningShell(root,[time,text,btn(T('addWord'),()=>{if(text.value.trim()){S.items.push({time:time.value,text:text.value.trim().slice(0,50)});text.value='';draw()}})],stage);draw();return{getState:()=>({...S,items:S.items.map(x=>({...x}))})};
}
function mountMood(root,init){
const S=Object.assign({votes:[0,0,0,0,0]},init||{}),stage=document.createElement('div'),faces=['😢','😕','😐','🙂','🤩'];
const draw=()=>{stage.className='mood';stage.innerHTML='';faces.forEach((f,i)=>{const b=btn(f,()=>{S.votes[i]++;draw()});const n=document.createElement('span');n.textContent=S.votes[i];b.append(n);stage.append(b)})};learningShell(root,[btn(T('pollReset'),()=>{S.votes=[0,0,0,0,0];draw()})],stage);draw();return{getState:()=>({...S,votes:[...S.votes]})};
}
function mountPoll(root,init){
const S=Object.assign({question:'Wat kies jij?',options:[{text:'A',votes:0},{text:'B',votes:0}]},init||{}),question=document.createElement('input'),options=document.createElement('textarea'),stage=document.createElement('div');question.value=S.question;options.value=S.options.map(x=>x.text).join('\n');
const sync=()=>{S.question=question.value.slice(0,100);const old=new Map(S.options.map(x=>[x.text,x.votes]));S.options=options.value.split('\n').map(x=>x.trim()).filter(Boolean).slice(0,6).map(text=>({text,votes:old.get(text)||0}));draw()},draw=()=>{stage.className='poll';stage.innerHTML='';const h=document.createElement('h3');h.textContent=S.question;stage.append(h);const max=Math.max(1,...S.options.map(x=>x.votes));S.options.forEach(o=>{const b=btn(o.text,()=>{o.votes++;draw()});const bar=document.createElement('i');bar.style.width=(o.votes/max*100)+'%';const n=document.createElement('span');n.textContent=o.votes;b.append(bar,n);stage.append(b)})};question.oninput=options.oninput=sync;learningShell(root,[field(T('pollQuestion'),question),field(T('pollOptions'),options),btn(T('pollReset'),()=>{S.options.forEach(x=>x.votes=0);draw()})],stage);draw();return{getState:()=>({...S,options:S.options.map(x=>({...x}))})};
}