All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 20s
54 lines
13 KiB
JavaScript
54 lines
13 KiB
JavaScript
/* 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:10,m:0,level:'quarter',style:'both',game:'set',task:false,targetH:3,targetM:30,score:0,streak:0},init||{}),level=document.createElement('select'),style=document.createElement('select'),game=document.createElement('select'),h=num(S.h,1,12),m=document.createElement('select'),stage=document.createElement('div');
|
||
const add=(el,a)=>a.forEach(([v,k])=>el.add(new Option(T(k),v)));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 steps=()=>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),time=(a,b)=>`${a}:${String(b).padStart(2,'0')}`;
|
||
const fill=()=>{const old=+m.value;m.innerHTML='';steps().forEach(v=>m.add(new Option(String(v).padStart(2,'0'),v)));S.m=steps().includes(old)?old:steps().includes(S.m)?S.m:0;m.value=S.m};
|
||
const words=(a,b)=>{const n=a===12?1:a+1;if(!b)return T('clockWordsHour').replace('{hour}',a);if(b===15)return T('clockWordsQuarterPast').replace('{hour}',a);if(b===30)return T('clockWordsHalf').replace('{hour}',n);if(b===45)return T('clockWordsQuarterTo').replace('{hour}',n);return T(b<30?'clockWordsPast':'clockWordsTo').replace('{minutes}',b<30?b:60-b).replace('{hour}',b<30?a:n)};
|
||
const random=()=>{const v=steps();return{h:1+Math.floor(Math.random()*12),m:v[Math.floor(Math.random()*v.length)]}};
|
||
const face=(a,b)=>{const f=document.createElement('div');f.className='clock';f.setAttribute('role','img');f.setAttribute('aria-label',time(a,b));for(let i=0;i<60;i++){const x=document.createElement('i');x.className=i%5?'clock-tick minor':'clock-tick';x.style.transform=`rotate(${i*6}deg)`;f.append(x)}[12,1,2,3,4,5,6,7,8,9,10,11].forEach((n,i)=>{const x=document.createElement('b');x.textContent=n;x.style.setProperty('--clock-angle',`${i*30}deg`);f.append(x)});const ah=document.createElement('i'),am=document.createElement('i'),pin=document.createElement('span');ah.className='clock-hand clock-hour';am.className='clock-hand clock-min';pin.className='clock-pin';ah.style.transform=`rotate(${a*30+b/2}deg)`;am.style.transform=`rotate(${b*6}deg)`;f.append(ah,am,pin);if(S.game==='set'){f.classList.add('interactive');const move=e=>{const r=f.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(f.dataset.hand==='minute'){const wanted=Math.round(deg/6)%60,allowed=steps();S.m=allowed.reduce((best,v)=>Math.min(Math.abs(v-wanted),60-Math.abs(v-wanted))<Math.min(Math.abs(best-wanted),60-Math.abs(best-wanted))?v:best);m.value=S.m}else{S.h=(Math.round((deg-S.m/2)/30)+11)%12+1;h.value=S.h}ah.style.transform=`rotate(${S.h*30+S.m/2}deg)`;am.style.transform=`rotate(${S.m*6}deg)`;const digital=stage.querySelector('.clock-digital'),written=stage.querySelector('.clock-words');if(digital)digital.textContent=time(S.h,S.m);if(written)written.textContent=words(S.h,S.m)};f.onpointerdown=e=>{const r=f.getBoundingClientRect(),dx=e.clientX-r.left-r.width/2,dy=e.clientY-r.top-r.height/2;f.dataset.hand=Math.hypot(dx,dy)>r.width*.22?'minute':'hour';f.setPointerCapture(e.pointerId);move(e)};f.onpointermove=e=>{if(f.hasPointerCapture(e.pointerId))move(e)};f.onpointerup=e=>{move(e);f.releasePointerCapture(e.pointerId);delete f.dataset.hand;draw()};}return f};
|
||
const feedback=ok=>{S.streak=ok?S.streak+1:0;if(ok)S.score++;ok?sndGood():sndBad();const p=stage.querySelector('.clock-feedback');p.textContent=T(ok?'numberlineGood':'clockTry').replace('{time}',time(S.targetH,S.targetM));p.className='clock-feedback '+(ok?'ok':'bad');stage.querySelector('.clock-score').textContent=T('clockScore').replace('{score}',S.score).replace('{streak}',S.streak)};
|
||
const check=()=>{if(S.task)feedback(+h.value===S.targetH&&+m.value===S.targetM)},newTask=()=>{const x=random();S.targetH=x.h;S.targetM=x.m;S.task=true;S.h=12;S.m=0;h.value=S.h;fill();m.value=S.m;draw()};
|
||
const draw=()=>{S.level=level.value;S.style=style.value;S.game=game.value;S.h=Math.max(1,Math.min(12,+h.value||12));S.m=+m.value||0;stage.className='clock-wrap';stage.innerHTML='';const x=S.task&&S.game!=='set'?{h:S.targetH,m:S.targetM}:{h:S.h,m:S.m},title=document.createElement('h3');title.className='clock-prompt';title.textContent=!S.task?T('clockStart'):S.game==='set'?T('clockSet').replace('{time}',time(S.targetH,S.targetM)):S.game==='read'?T('clockRead'):T('clockChoose');stage.append(title);const lesson=document.createElement('div');lesson.className='clock-lesson';if(['analogue','both'].includes(S.style))lesson.append(face(x.h,x.m));if(['digital','both'].includes(S.style)){const o=document.createElement('output');o.className='clock-digital';o.textContent=time(x.h,x.m);lesson.append(o)}if(['words','both'].includes(S.style)){const p=document.createElement('p');p.className='clock-words';p.textContent=words(x.h,x.m);lesson.append(p)}stage.append(lesson);if(S.game==='set'&&['analogue','both'].includes(S.style)){const hint=document.createElement('small');hint.className='clock-drag-hint';hint.textContent=T('clockDragHint');stage.append(hint)}if(S.task&&S.game==='choose'){const a=[{h:S.targetH,m:S.targetM}];while(a.length<3){const c=random();if(!a.some(x=>x.h===c.h&&x.m===c.m))a.push(c)}a.sort(()=>Math.random()-.5);const choices=document.createElement('div');choices.className='clock-choices';a.forEach(c=>choices.append(btn(time(c.h,c.m),()=>feedback(c.h===S.targetH&&c.m===S.targetM))));stage.append(choices)}const status=document.createElement('p'),score=document.createElement('small');status.className='clock-feedback';score.className='clock-score';score.textContent=T('clockScore').replace('{score}',S.score).replace('{streak}',S.streak);stage.append(status,score)};
|
||
level.onchange=()=>{S.level=level.value;fill();S.task=false;draw()};style.onchange=draw;game.onchange=()=>{S.task=false;draw()};h.oninput=m.onchange=draw;fill();learningShell(root,[field(T('clockLevel'),level),field(T('clockLearningStyle'),style),field(T('clockGame'),game),field(T('clockHour'),h),field(T('clockMinute'),m),btn(T('numberlineNew'),newTask),btn(T('numberlineCheck'),check,'')],stage);draw();return{getState:()=>({...S})};
|
||
}
|
||
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}))})};
|
||
}
|