All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 55s
- De math.js-pilot (v0.4.23-beta: eigen personage kort naast "Goed!" bij een juist antwoord) hergebruikt in vier andere widgets, zoals destijds al voorzien in de roadmap: klok (mountClock), geld (mountMoney), plaatswaarde (mountPlacevalue) en de wereldoriëntatie-spelopdracht (mountWorldQuiz - dekt bomen/vogels/planten in één keer - en mountTopography). - Zelfde patroon overal: bij S.feedback==='good' (of S.spellSolved) en een ingesteld personage verschijnt renderAvatarFace(...,"md") met de av-cheer-animatie naast het bestaande feedbackbericht - puur visueel, geen nieuw feedbacktype. Zonder personage blijft het gedrag exact ongewijzigd. - Geverifieerd met echte oplos-interacties (niet alleen mounten): klok (typemodus + toetsenbord), geld (typemodus + toetsenbord), plaatswaarde (blokken plaatsen via toetsenbord-Enter) en de spelopdracht (letters plaatsen via toetsenbord-Enter) - in alle vier verschijnt het personage daadwerkelijk in de feedback-HTML na een echt juist antwoord. Volledige testsuite: 112/112. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
833 lines
61 KiB
JavaScript
833 lines
61 KiB
JavaScript
const MONEY_LEVELS=Object.freeze({
|
||
"1": {
|
||
"min": 100,
|
||
"max": 900,
|
||
"step": 100,
|
||
"denominations": [
|
||
200,
|
||
100
|
||
],
|
||
"payments": [
|
||
500,
|
||
1000
|
||
],
|
||
"fixedIndices": [
|
||
0,
|
||
2,
|
||
3
|
||
],
|
||
"emptyDigits": [
|
||
0,
|
||
null,
|
||
0,
|
||
0
|
||
]
|
||
},
|
||
"2": {
|
||
"min": 50,
|
||
"max": 995,
|
||
"step": 5,
|
||
"denominations": [
|
||
500,
|
||
200,
|
||
100,
|
||
50,
|
||
20,
|
||
10,
|
||
5
|
||
],
|
||
"payments": [
|
||
500,
|
||
1000,
|
||
2000
|
||
],
|
||
"fixedIndices": [
|
||
0
|
||
],
|
||
"emptyDigits": [
|
||
0,
|
||
null,
|
||
null,
|
||
null
|
||
]
|
||
},
|
||
"3": {
|
||
"min": 1,
|
||
"max": 4999,
|
||
"step": 1,
|
||
"denominations": [
|
||
2000,
|
||
1000,
|
||
500,
|
||
200,
|
||
100,
|
||
50,
|
||
20,
|
||
10,
|
||
5,
|
||
2,
|
||
1
|
||
],
|
||
"payments": [
|
||
100,
|
||
200,
|
||
500,
|
||
1000,
|
||
2000,
|
||
5000
|
||
],
|
||
"fixedIndices": [],
|
||
"emptyDigits": [
|
||
null,
|
||
null,
|
||
null,
|
||
null
|
||
]
|
||
}
|
||
});
|
||
const MONEY_PRODUCTS=Object.freeze([
|
||
{
|
||
"id": "apple",
|
||
"icon": "🍎",
|
||
"category": "food",
|
||
"nl": "Appel",
|
||
"en": "Apple",
|
||
"prices": {
|
||
"1": [
|
||
100
|
||
],
|
||
"2": [
|
||
75,
|
||
100
|
||
],
|
||
"3": [
|
||
69,
|
||
79,
|
||
99
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "bananas",
|
||
"icon": "🍌",
|
||
"category": "food",
|
||
"nl": "Tros bananen",
|
||
"en": "Bunch of bananas",
|
||
"prices": {
|
||
"1": [
|
||
100,
|
||
200
|
||
],
|
||
"2": [
|
||
100,
|
||
150
|
||
],
|
||
"3": [
|
||
99,
|
||
145,
|
||
149
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "juice",
|
||
"icon": "🧃",
|
||
"category": "food",
|
||
"nl": "Pakje sap",
|
||
"en": "Juice box",
|
||
"prices": {
|
||
"1": [
|
||
200,
|
||
300
|
||
],
|
||
"2": [
|
||
200,
|
||
250
|
||
],
|
||
"3": [
|
||
199,
|
||
229,
|
||
259
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "sandwich",
|
||
"icon": "🥪",
|
||
"category": "food",
|
||
"nl": "Broodje",
|
||
"en": "Sandwich",
|
||
"prices": {
|
||
"1": [
|
||
400,
|
||
500
|
||
],
|
||
"2": [
|
||
350,
|
||
450,
|
||
550
|
||
],
|
||
"3": [
|
||
349,
|
||
449,
|
||
549
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "yogurt",
|
||
"icon": "🥣",
|
||
"category": "food",
|
||
"nl": "Yoghurt",
|
||
"en": "Yogurt",
|
||
"prices": {
|
||
"1": [
|
||
100,
|
||
200
|
||
],
|
||
"2": [
|
||
100,
|
||
150,
|
||
200
|
||
],
|
||
"3": [
|
||
99,
|
||
159,
|
||
189
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "cookie",
|
||
"icon": "🍪",
|
||
"category": "food",
|
||
"nl": "Koek",
|
||
"en": "Cookie",
|
||
"prices": {
|
||
"1": [
|
||
100,
|
||
200
|
||
],
|
||
"2": [
|
||
100,
|
||
150,
|
||
250
|
||
],
|
||
"3": [
|
||
99,
|
||
149,
|
||
249
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "water",
|
||
"icon": "💧",
|
||
"category": "food",
|
||
"nl": "Flesje water",
|
||
"en": "Bottle of water",
|
||
"prices": {
|
||
"1": [
|
||
100,
|
||
200
|
||
],
|
||
"2": [
|
||
100,
|
||
150,
|
||
200
|
||
],
|
||
"3": [
|
||
99,
|
||
149,
|
||
199
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "icecream",
|
||
"icon": "🍦",
|
||
"category": "food",
|
||
"nl": "IJsje",
|
||
"en": "Ice cream",
|
||
"prices": {
|
||
"1": [
|
||
200,
|
||
300
|
||
],
|
||
"2": [
|
||
200,
|
||
250,
|
||
350
|
||
],
|
||
"3": [
|
||
199,
|
||
249,
|
||
349
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "pencil",
|
||
"icon": "✏️",
|
||
"category": "school",
|
||
"nl": "Potlood",
|
||
"en": "Pencil",
|
||
"prices": {
|
||
"1": [
|
||
100
|
||
],
|
||
"2": [
|
||
50,
|
||
100
|
||
],
|
||
"3": [
|
||
49,
|
||
79,
|
||
99
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "eraser",
|
||
"icon": "🧽",
|
||
"category": "school",
|
||
"nl": "Gum",
|
||
"en": "Eraser",
|
||
"prices": {
|
||
"1": [
|
||
100
|
||
],
|
||
"2": [
|
||
50,
|
||
100
|
||
],
|
||
"3": [
|
||
49,
|
||
69,
|
||
99
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "ruler",
|
||
"icon": "📏",
|
||
"category": "school",
|
||
"nl": "Liniaal",
|
||
"en": "Ruler",
|
||
"prices": {
|
||
"1": [
|
||
100,
|
||
200
|
||
],
|
||
"2": [
|
||
100,
|
||
150,
|
||
200
|
||
],
|
||
"3": [
|
||
99,
|
||
149,
|
||
199
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "notebook",
|
||
"icon": "📓",
|
||
"category": "school",
|
||
"nl": "Schrift",
|
||
"en": "Notebook",
|
||
"prices": {
|
||
"1": [
|
||
300,
|
||
400
|
||
],
|
||
"2": [
|
||
250,
|
||
350,
|
||
450
|
||
],
|
||
"3": [
|
||
249,
|
||
349,
|
||
449
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "book",
|
||
"icon": "📘",
|
||
"category": "school",
|
||
"nl": "Boek",
|
||
"en": "Book",
|
||
"prices": {
|
||
"1": [
|
||
700,
|
||
900
|
||
],
|
||
"2": [
|
||
695,
|
||
895,
|
||
995
|
||
],
|
||
"3": [
|
||
699,
|
||
899,
|
||
1299
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "markers",
|
||
"icon": "🖍️",
|
||
"category": "school",
|
||
"nl": "Stiften",
|
||
"en": "Markers",
|
||
"prices": {
|
||
"1": [
|
||
500,
|
||
700
|
||
],
|
||
"2": [
|
||
495,
|
||
695,
|
||
895
|
||
],
|
||
"3": [
|
||
499,
|
||
699,
|
||
899
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "scissors",
|
||
"icon": "✂️",
|
||
"category": "school",
|
||
"nl": "Schaar",
|
||
"en": "Scissors",
|
||
"prices": {
|
||
"1": [
|
||
300,
|
||
400
|
||
],
|
||
"2": [
|
||
250,
|
||
350,
|
||
450
|
||
],
|
||
"3": [
|
||
249,
|
||
349,
|
||
449
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "backpack",
|
||
"icon": "🎒",
|
||
"category": "school",
|
||
"nl": "Rugzak",
|
||
"en": "Backpack",
|
||
"prices": {
|
||
"3": [
|
||
2499,
|
||
2999,
|
||
3499
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "calculator",
|
||
"icon": "🧮",
|
||
"category": "school",
|
||
"nl": "Rekenmachine",
|
||
"en": "Calculator",
|
||
"prices": {
|
||
"1": [
|
||
900
|
||
],
|
||
"2": [
|
||
895,
|
||
995
|
||
],
|
||
"3": [
|
||
899,
|
||
1299,
|
||
1799
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "ball",
|
||
"icon": "⚽",
|
||
"category": "play",
|
||
"nl": "Bal",
|
||
"en": "Ball",
|
||
"prices": {
|
||
"1": [
|
||
600,
|
||
900
|
||
],
|
||
"2": [
|
||
600,
|
||
900
|
||
],
|
||
"3": [
|
||
599,
|
||
899,
|
||
999
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "teddy",
|
||
"icon": "🧸",
|
||
"category": "play",
|
||
"nl": "Knuffel",
|
||
"en": "Teddy bear",
|
||
"prices": {
|
||
"1": [
|
||
900
|
||
],
|
||
"2": [
|
||
995
|
||
],
|
||
"3": [
|
||
999,
|
||
1499,
|
||
1899
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "puzzle",
|
||
"icon": "🧩",
|
||
"category": "play",
|
||
"nl": "Puzzel",
|
||
"en": "Puzzle",
|
||
"prices": {
|
||
"1": [
|
||
800,
|
||
900
|
||
],
|
||
"2": [
|
||
795,
|
||
995
|
||
],
|
||
"3": [
|
||
799,
|
||
999,
|
||
1499
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "rope",
|
||
"icon": "➰",
|
||
"category": "play",
|
||
"nl": "Springtouw",
|
||
"en": "Skipping rope",
|
||
"prices": {
|
||
"1": [
|
||
300,
|
||
600
|
||
],
|
||
"2": [
|
||
300,
|
||
600
|
||
],
|
||
"3": [
|
||
299,
|
||
599,
|
||
699
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "kite",
|
||
"icon": "🪁",
|
||
"category": "play",
|
||
"nl": "Vlieger",
|
||
"en": "Kite",
|
||
"prices": {
|
||
"1": [
|
||
700,
|
||
900
|
||
],
|
||
"2": [
|
||
695,
|
||
895
|
||
],
|
||
"3": [
|
||
699,
|
||
999,
|
||
1299
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "blocks",
|
||
"icon": "🧱",
|
||
"category": "play",
|
||
"nl": "Bouwblokken",
|
||
"en": "Building blocks",
|
||
"prices": {
|
||
"1": [
|
||
900
|
||
],
|
||
"2": [
|
||
895,
|
||
995
|
||
],
|
||
"3": [
|
||
999,
|
||
1499,
|
||
1999
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "boardgame",
|
||
"icon": "🎲",
|
||
"category": "play",
|
||
"nl": "Spel",
|
||
"en": "Game",
|
||
"prices": {
|
||
"1": [
|
||
900
|
||
],
|
||
"2": [
|
||
995
|
||
],
|
||
"3": [
|
||
999,
|
||
1499,
|
||
2499
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"id": "skateboard",
|
||
"icon": "🛹",
|
||
"category": "play",
|
||
"nl": "Skateboard",
|
||
"en": "Skateboard",
|
||
"prices": {
|
||
"3": [
|
||
2999,
|
||
3999,
|
||
4999
|
||
]
|
||
}
|
||
}
|
||
]);
|
||
|
||
const moneyPriceFitsLevel=(price,level)=>{const rule=MONEY_LEVELS[level];return Number.isInteger(price)&&price>=rule.min&&price<=rule.max&&(price-rule.min)%rule.step===0};
|
||
function createMoneyTask(level,game,category='all',previousCents=null,previousProductId=null,random=Math.random){
|
||
const rule=MONEY_LEVELS[level],rnd=max=>Math.floor(random()*max),randomAmount=()=>rule.min+rnd(Math.floor((rule.max-rule.min)/rule.step)+1)*rule.step;
|
||
let targetCents,productId=previousProductId;
|
||
if(game==='change'){
|
||
const pool=MONEY_PRODUCTS.filter(item=>(category==='all'||item.category===category)&&(item.prices[level]||[]).some(price=>moneyPriceFitsLevel(price,level)));
|
||
const pick=()=>{const product=pool[rnd(pool.length)],prices=product.prices[level].filter(price=>moneyPriceFitsLevel(price,level));return{product,price:prices[rnd(prices.length)]}};
|
||
let offer=pick();for(let attempt=0;attempt<8&&offer.product.id===previousProductId&&offer.price===previousCents;attempt++)offer=pick();
|
||
targetCents=offer.price;productId=offer.product.id;
|
||
}else{targetCents=randomAmount();if(targetCents===previousCents)targetCents=randomAmount()}
|
||
const paidCents=rule.payments.find(value=>value>targetCents)||rule.payments[rule.payments.length-1];
|
||
return{targetCents,paidCents,productId};
|
||
}
|
||
|
||
/* 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,activeD:0,activeN:0},init||{}), den=num(S.den,2,12), show=document.createElement('input'), stage=document.createElement('div');show.type='checkbox';show.checked=S.show;
|
||
const gcd=(a,b)=>b?gcd(b,a%b):a,summary=()=>{if(!S.activeD)return'';const divisor=gcd(S.activeN,S.activeD),fraction=`${S.activeN}/${S.activeD}`,simple=`${S.activeN/divisor}/${S.activeD/divisor}`;return T('fractionSelected').replace('{fraction}',divisor>1?`${fraction} = ${simple}`:fraction)};
|
||
const draw=()=>{S.den=Math.max(2,Math.min(12,+den.value||4));S.show=show.checked;if(S.activeD>S.den){S.activeD=0;S.activeN=0}stage.className='fraction-game';stage.innerHTML='';const hint=document.createElement('p');hint.className='play-mission';hint.textContent=T('fractionHint');const result=document.createElement('output');result.className='fraction-summary';result.textContent=summary();const wall=document.createElement('div');wall.className='fraction-wall';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.classList.toggle('selected',S.activeD===d&&n<S.activeN);c.setAttribute('aria-label',`${n+1}/${d}`);c.onclick=()=>{S.activeD=d;S.activeN=n+1;draw()};row.append(c)}wall.append(row)}const clear=btn(T('fractionClear'),()=>{S.activeD=0;S.activeN=0;draw()});clear.classList.add('fraction-clear');stage.append(hint,result,wall,clear)};
|
||
den.onchange=show.onchange=draw;learningShell(root,[field(T('fractionRows'),den),field(T('fractionLabels'),show)],stage);draw();return{getState:()=>({...S})};
|
||
}
|
||
const clockTurnDelta=(next,previous)=>((next-previous+540)%360)-180;
|
||
const clockHourCrossing=(next,previous)=>{const delta=clockTurnDelta(next,previous);return delta>0&&next<previous?1:delta<0&&next>previous?-1:0};
|
||
const clockStepHour=(hour,step,use24)=>use24?(hour+step+24)%24:((hour-1+step+12)%12)+1;
|
||
const clock24Angle=(angle,delta)=>(angle+delta+720)%720;
|
||
const clock24Hour=(angle,minute)=>((Math.round((angle-minute/2)/30)%24)+24)%24;
|
||
function mountClock(root,init){
|
||
const S=Object.assign({h:12,m:0,level:1,style:'both',game:'set',amode:'blocks',input:'',linkHands:true,use24:true,task:true,targetH:3,targetM:30,score:0,streak:0,solved:false,feedback:'',choices:[],answerDigits:[null,null,null,null]},init||{});
|
||
if(typeof S.level==='string')S.level={hour:1,quarter:2,five:2,minute:3}[S.level]||1;S.level=Math.max(1,Math.min(3,+S.level||1));S.amode=S.amode==='type'?'type':'blocks';S.input=String(S.input||'').replace(/\D/g,'').slice(0,4);
|
||
const style=document.createElement('select'),game=document.createElement('select'),linkHands=document.createElement('input'),use24=document.createElement('input'),stage=document.createElement('div'),add=(el,items)=>items.forEach(([value,key])=>el.add(new Option(T(key),value)));
|
||
add(style,[['analogue','clockStyleAnalogue'],['digital','clockStyleDigital'],['words','clockStyleWords'],['both','clockStyleBoth']]);add(game,[['set','clockGameSet'],['read','clockGameRead'],['choose','clockGameChoose']]);style.value=S.style;game.value=S.game;linkHands.type='checkbox';linkHands.checked=S.linkHands;linkHands.onchange=()=>{S.linkHands=linkHands.checked};use24.type='checkbox';use24.checked=S.use24;use24.onchange=()=>{S.use24=use24.checked;newTask()};const linkSetting=field(T('clockLinkHands'),linkHands),hour24Setting=field(T('clock24Hour'),use24),syncSettings=()=>{linkSetting.hidden=game.value!=='set';hour24Setting.hidden=S.level!==3};
|
||
const minuteValues=()=>S.level===1?[0,30]:S.level===2?Array.from({length:12},(_,i)=>i*5):Array.from({length:60},(_,i)=>i),time=(hour,minute)=>`${hour}:${String(minute).padStart(2,'0')}`;
|
||
const uses24=()=>S.level===3&&S.use24;
|
||
const daypartKey=hour=>hour<6?'clockNight':hour<12?'clockMorning':hour<18?'clockAfternoon':'clockEvening',clockHour=hour=>hour%12||12;
|
||
const words=(hour,minute)=>{const shown=clockHour(hour),next=shown===12?1:shown+1;let result;if(minute===0)result=T('clockWordsHour').replace('{hour}',shown);else if(minute===15)result=T('clockWordsQuarterPast').replace('{hour}',shown);else if(minute===30)result=T('clockWordsHalf').replace('{hour}',next);else if(minute===45)result=T('clockWordsQuarterTo').replace('{hour}',next);else result=T(minute<30?'clockWordsPast':'clockWordsTo').replace('{minutes}',minute<30?minute:60-minute).replace('{hour}',minute<30?shown:next);return uses24()?result+' '+T(daypartKey(hour)):result};
|
||
const randomTime=()=>{const values=minuteValues();return{h:uses24()?Math.floor(Math.random()*24):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);const dots=dotGrid(Number(value));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));const digits=String(hour).padStart(2,'0')+String(minute).padStart(2,'0');el.append(woodDigit(digits[0]),woodDigit(digits[1]));const colon=document.createElement('strong');colon.textContent=':';el.append(colon,woodDigit(digits[2]),woodDigit(digits[3]));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===0?'clock-tick hour':'clock-tick minor';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)paint();else{hourHand.style.transform=`rotate(${hour*30+minute/2}deg)`;minuteHand.style.transform=`rotate(${minute*6}deg)`}face.append(hourHand,minuteHand,pin);
|
||
if(interactive){
|
||
const updateAnswer=()=>{face.setAttribute('aria-label',time(S.h,S.m));const answer=stage.querySelector('.clock-live-answer');if(answer){answer.innerHTML='';answer.append(timeBlocks(S.h,S.m))}stage.querySelectorAll('.clock-period .tbtn').forEach(button=>{const active=+button.dataset.period===(S.h>=12?12:0);button.classList.toggle('active',active);button.setAttribute('aria-pressed',String(active))})};
|
||
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 previous=face._lastPointerAngle;if(Number.isFinite(previous)){const step=clockHourCrossing(deg,previous);if(step)S.h=clockStepHour(S.h,step,uses24())}face._lastPointerAngle=deg;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 if(uses24()){const previous=face._lastPointerAngle;if(Number.isFinite(previous)){face._dragAngle=clock24Angle(face._dragAngle,clockTurnDelta(deg,previous));if(S.linkHands){let bestH=0,bestM=0,bestDistance=720;for(let hour=0;hour<24;hour++)minuteValues().forEach(minute=>{const angle=hour*30+minute/2,difference=Math.min(Math.abs(angle-face._dragAngle),720-Math.abs(angle-face._dragAngle));if(difference<bestDistance){bestDistance=difference;bestH=hour;bestM=minute}});S.h=bestH;S.m=bestM}else S.h=clock24Hour(face._dragAngle,S.m)}face._lastPointerAngle=deg}
|
||
else if(S.linkHands){let bestH=12,bestM=0,bestDistance=360;for(let hour=1;hour<=12;hour++)minuteValues().forEach(minute=>{const angle=(hour%12)*30+minute/2,difference=Math.min(Math.abs(angle-deg),360-Math.abs(angle-deg));if(difference<bestDistance){bestDistance=difference;bestH=hour;bestM=minute}});S.h=bestH;S.m=bestM}
|
||
else S.h=(Math.round((deg-S.m/2)/30)+11)%12+1;paint();updateAnswer()};
|
||
const clearDrag=()=>{delete face.dataset.hand;delete face._dragAngle;delete face._lastPointerAngle};
|
||
const start=(e,hand)=>{if(S.solved)return;e.preventDefault();e.stopPropagation();face.dataset.hand=hand;if(hand==='minute')face._lastPointerAngle=null;else if(uses24()){face._dragAngle=S.h*30+S.m/2;face._lastPointerAngle=null}face.setPointerCapture(e.pointerId);move(e)};
|
||
hourHand.onpointerdown=e=>start(e,'hour');minuteHand.onpointerdown=e=>start(e,'minute');
|
||
face.onpointermove=e=>{if(face.hasPointerCapture(e.pointerId))move(e)};
|
||
face.onpointerup=e=>{move(e);face.releasePointerCapture(e.pointerId);clearDrag()};
|
||
face.onpointercancel=e=>{if(face.hasPointerCapture(e.pointerId))face.releasePointerCapture(e.pointerId);clearDrag()}
|
||
}
|
||
return face
|
||
};
|
||
const assignment=(hour,minute,compact=false)=>{const wrap=document.createElement('div');wrap.className='clock-assignment'+(compact?' compact':'');if(['analogue','both'].includes(S.style)){const clock=makeClock(hour,minute,false);if(compact)clock.classList.add('clock-reference');wrap.append(clock)}if(['digital','both'].includes(S.style)){const digital=document.createElement('output');digital.className='clock-task-digital';digital.textContent=time(hour,minute);wrap.append(digital)}if(['words','both'].includes(S.style)){const written=document.createElement('p');written.className='clock-task-words';written.textContent=words(hour,minute);wrap.append(written)}if(uses24()&&S.style==='analogue'){const period=document.createElement('span');period.className='clock-daypart';period.textContent=T(daypartKey(hour));wrap.append(period)}return wrap};
|
||
const periodControl=()=>{const control=document.createElement('div');control.className='clock-period';control.setAttribute('aria-label',T('clock24Hour'));[0,12].forEach(period=>{const button=btn(period?'12–23':'00–11',()=>{if(S.solved)return;const dial=clockHour(S.h);S.h=(dial%12)+period;S.feedback='';draw()},'ghost');button.dataset.period=period;button.title=period?'12:00–23:59':'00:00–11:59';const active=(S.h>=12?12:0)===period;button.classList.toggle('active',active);button.setAttribute('aria-pressed',String(active));control.append(button)});return control};
|
||
const blockAnswer=()=>{const answer=document.createElement('div');answer.className='clock-block-answer';const instruction=document.createElement('p');instruction.textContent=T('clockBuildHint');const slots=document.createElement('div');slots.className='clock-answer-slots';
|
||
/* zelfde patroon als letterblokken/rekenblokken: het vak toont altijd het
|
||
puntenpatroon van het júíste cijfer op die plek (niet van wat er nu in
|
||
staat) - dat is de tastbare hint, ongeacht of er al iets ligt. */
|
||
const targetDigits=String(S.targetH).padStart(2,'0')+String(S.targetM).padStart(2,'0');
|
||
const makeSlot=index=>{const slot=document.createElement('div');slot.className='slot sm clock-digit-slot'+(S.answerDigits[index]!==null?' filled':'');slot.dataset.index=index;const ghost=document.createElement('div');ghost.className='ghost';ghost.textContent=T('clockDigitGhost');const dots=dotGrid(targetDigits[index]);dots.classList.add('slot-dots');slot.append(ghost,dots);if(S.answerDigits[index]!==null){const placed=woodDigit(S.answerDigits[index]);placed.classList.add('in-slot','clock-drag-block');slot.append(placed);slot.onclick=()=>{if(!S.solved){S.answerDigits[index]=null;S.feedback='';draw()}}}return slot};slots.append(makeSlot(0),makeSlot(1));const colon=document.createElement('strong');colon.className='clock-slot-colon';colon.textContent=':';slots.append(colon,makeSlot(2),makeSlot(3));const tray=document.createElement('div');tray.className='clock-block-tray lg-tray';const attachDrag=block=>{block.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();try{block.releasePointerCapture(e.pointerId)}catch(_){}const clone=block.cloneNode(true);clone.id='dragClone';document.body.append(clone);block.classList.add('dragging');const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+'px';clone.style.top=ev.clientY/zoom+'px'},hit=ev=>{let found=null;slots.querySelectorAll('.clock-digit-slot').forEach(slot=>{const r=slot.getBoundingClientRect(),pad=8;if(ev.clientX>=r.left-pad&&ev.clientX<=r.right+pad&&ev.clientY>=r.top-pad&&ev.clientY<=r.bottom+pad)found=slot});return found};moveTo(e);let hovered=null;const move=ev=>{moveTo(ev);const target=hit(ev);if(hovered&&hovered!==target)hovered.classList.remove('droptarget');hovered=target;if(hovered)hovered.classList.add('droptarget')},clean=()=>{document.removeEventListener('pointermove',move);document.removeEventListener('pointerup',up);document.removeEventListener('pointercancel',cancel);clone.remove();block.classList.remove('dragging');if(hovered)hovered.classList.remove('droptarget')},up=ev=>{const target=hit(ev);clean();if(target){S.answerDigits[+target.dataset.index]=+block.dataset.digit;S.feedback='';draw()}},cancel=()=>clean();document.addEventListener('pointermove',move);document.addEventListener('pointerup',up);document.addEventListener('pointercancel',cancel)}};'0123456789'.split('').forEach(d=>{const block=woodDigit(+d);block.classList.add('clock-drag-block');block.dataset.digit=d;block.setAttribute('role','button');block.setAttribute('aria-label',T('clockDigit').replace('{digit}',d));block.tabIndex=0;block.onkeydown=e=>{if((e.key==='Enter'||e.key===' ')&&!S.solved){e.preventDefault();const index=S.answerDigits.indexOf(null);if(index>=0){S.answerDigits[index]=+d;draw()}}};attachDrag(block);tray.append(block)});answer.append(instruction,slots,tray);return answer};
|
||
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)return;if(S.game==='read'){if(S.amode==='blocks'){if(S.answerDigits.some(x=>x===null)){S.feedback='incomplete';sndBad();draw();return}S.h=Number(''+S.answerDigits[0]+S.answerDigits[1]);S.m=Number(''+S.answerDigits[2]+S.answerDigits[3])}else{if(S.input.length<4){S.feedback='incomplete';sndBad();draw();return}S.h=+S.input.slice(0,2);S.m=+S.input.slice(2,4)}}const expectedHour=S.game==='set'?(uses24()?S.targetH:clockHour(S.targetH)):S.targetH;setFeedback(S.h===expectedHour&&S.m===S.targetM)};
|
||
const typeAnswer=()=>{const wrap=document.createElement('div');wrap.className='clock-type-answer';const hint=document.createElement('p');hint.textContent=T('clockTypeHint');const display=document.createElement('output');display.className='clock-type-display';const shown=(S.input+'____').slice(0,4);display.textContent=shown.slice(0,2)+':'+shown.slice(2);const pad=document.createElement('div');pad.className='clock-keypad';['7','8','9','⌫','4','5','6','0','1','2','3','✓'].forEach(key=>{const button=btn(key,()=>{if(S.solved)return;if(key==='⌫')S.input=S.input.slice(0,-1);else if(key==='✓'){check();return}else if(S.input.length<4)S.input+=key;S.feedback='';draw()});if(key==='✓')button.classList.add('ok');pad.append(button)});wrap.append(hint,display,pad);return wrap};
|
||
const newTask=()=>{const target=randomTime();S.targetH=target.h;S.targetM=target.m;S.h=uses24()?0:12;S.m=0;S.task=true;S.solved=false;S.feedback='';S.input='';S.choices=[];S.answerDigits=[null,null,null,null];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 chooseLevel=value=>{S.level=value;S.score=0;S.streak=0;syncSettings();newTask()},toggleMode=()=>{S.amode=S.amode==='blocks'?'type':'blocks';S.feedback='';S.input='';S.answerDigits=[null,null,null,null];draw()};
|
||
const draw=()=>{S.style=style.value;S.game=game.value;stage.className='clock-app';stage.innerHTML='';const top=document.createElement('div');top.className='clock-top';const levelControl=document.createElement('div');levelControl.className='clock-level-control';const label=document.createElement('span');label.textContent=T('clockLevel');const levels=document.createElement('div');levels.className='lg-levels clock-levels';[1,2,3].forEach(value=>{const button=document.createElement('button');button.type='button';button.textContent=T('lvl'+value);button.title=T('clockLevel'+value+'Hint');button.classList.toggle('active',S.level===value);button.onclick=()=>chooseLevel(value);levels.append(button)});levelControl.append(label,levels);top.append(levelControl);if(S.game!=='set'){const mode=btn(S.amode==='blocks'?'🧱':'⌨️',toggleMode);mode.classList.add('clock-mode');mode.title=T('amode');top.append(mode)}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.game==='set'?T('clockSetTitle'):S.game==='read'?T('clockReadTitle'):T('clockChooseTitle');card.append(title);
|
||
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,assignment(S.targetH,S.targetM,true));card.append(prompt);const work=document.createElement('div');work.className='clock-work';const answer=document.createElement('div');answer.className='clock-set-answer';if(uses24())answer.append(periodControl());const live=document.createElement('div');live.className='clock-live-answer';live.append(timeBlocks(S.h,S.m));answer.append(live);work.append(makeClock(S.h,S.m,true),answer);card.append(work);}
|
||
if(S.task&&S.game==='read'){const layout=document.createElement('div');layout.className='clock-read-layout';layout.append(assignment(S.targetH,S.targetM),S.amode==='blocks'?blockAnswer():typeAnswer());card.append(layout)}
|
||
if(S.task&&S.game==='choose'){card.append(assignment(S.targetH,S.targetM));if(!S.solved){const choices=document.createElement('div');choices.className='clock-choices '+(S.amode==='blocks'?'wood':'plain');const asWords=['digital','both'].includes(S.style);S.choices.forEach(option=>{const choose=()=>setFeedback(option.h===S.targetH&&option.m===S.targetM);if(asWords){const button=btn(words(option.h,option.m),choose);button.classList.add('clock-word-choice');choices.append(button)}else if(S.amode==='blocks')choices.append(timeBlocks(option.h,option.m,choose));else{const button=btn(time(option.h,option.m),choose);button.classList.add('clock-digital-choice');choices.append(button)}});card.append(choices)}}
|
||
const feedback=document.createElement('p');feedback.className='clock-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');if(S.feedback)feedback.textContent=T(S.feedback==='good'?'clockCorrect':S.feedback==='incomplete'?'clockIncomplete':'clockTryAgain');if(S.feedback==='good'&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,'md');cheer.classList.add('av-cheer');feedback.append(cheer)}card.append(feedback);const actions=document.createElement('div');actions.className='clock-actions';if(S.task&&S.game!=='choose'&&!S.solved&&!(S.game==='read'&&S.amode==='type'))actions.append(btn(T('numberlineCheck'),check,''));actions.append(btn(T('clockNextTask'),newTask,'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)};
|
||
style.onchange=draw;game.onchange=()=>{syncSettings();newTask()};learningShell(root,[field(T('clockLearningStyle'),style),field(T('clockGame'),game),linkSetting,hour24Setting],stage);syncSettings();newTask();return{getState:()=>({...S,choices:S.choices.map(x=>({...x})),answerDigits:[...S.answerDigits]})};
|
||
}
|
||
const PLACE_COLUMNS=['HD','TD','D','H','T','E'];
|
||
const placeLevelDigits=level=>level===1?2:level===2?3:6;
|
||
function mountPlacevalue(root,init){
|
||
const S=Object.assign({number:1342,game:'explore',level:1,target:null,answerDigits:null,score:0,streak:0,solved:false,feedback:''},init||{});
|
||
S.level=Math.max(1,Math.min(3,+S.level||1));S.game=S.game==='build'?'build':'explore';if(!Array.isArray(S.answerDigits))S.answerDigits=null;
|
||
const input=num(S.number,0,999999),game=document.createElement('select'),stage=document.createElement('div');
|
||
game.add(new Option(T('placeGameExplore'),'explore'));game.add(new Option(T('placeGameBuild'),'build'));game.value=S.game;
|
||
const gameSetting=field(T('placeGame'),game),numberSetting=field(T('placeNumber'),input),diceBtn=btn('🎲',()=>{input.value=Math.floor(Math.random()*999999);drawExplore()});
|
||
const syncSettings=()=>{numberSetting.hidden=S.game!=='explore';diceBtn.hidden=S.game!=='explore'};
|
||
const activeCols=()=>PLACE_COLUMNS.slice(6-placeLevelDigits(S.level));
|
||
const maxForLevel=()=>Math.pow(10,placeLevelDigits(S.level))-1;
|
||
const woodDigit=value=>{const block=document.createElement('span');block.className='blok sm place-digit-block';const label=document.createElement('span');label.className='bl';label.textContent=String(value);const dots=dotGrid(Number(value));dots.classList.add('blok-dots');block.append(label,dots);return block};
|
||
const drawExplore=()=>{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='';PLACE_COLUMNS.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)})};
|
||
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()}drawBuild()};
|
||
const check=()=>{if(S.answerDigits.some(d=>d===null)){S.feedback='incomplete';sndBad();drawBuild();return}setFeedback(Number(S.answerDigits.join(''))===S.target)};
|
||
const needsNewTask=()=>S.target===null||!Array.isArray(S.answerDigits)||S.answerDigits.length!==activeCols().length;
|
||
const newTask=()=>{S.target=Math.floor(Math.random()*(maxForLevel()+1));S.answerDigits=activeCols().map(()=>null);S.solved=false;S.feedback='';drawBuild()};
|
||
const chooseLevel=value=>{S.level=value;S.score=0;S.streak=0;newTask()};
|
||
const buildAnswer=()=>{
|
||
const answer=document.createElement('div');answer.className='place-block-answer';const instruction=document.createElement('p');instruction.textContent=T('placeBuildHint');
|
||
const slots=document.createElement('div');slots.className='place-answer-slots';const cols=activeCols();
|
||
/* zelfde patroon als letterblokken/rekenblokken: het vak toont altijd het
|
||
puntenpatroon van het júíste cijfer op die plek. */
|
||
const targetDigits=String(S.target).padStart(cols.length,'0');
|
||
const makeSlot=index=>{const wrap=document.createElement('div');wrap.className='place-slot-col';const colLabel=document.createElement('b');colLabel.textContent=cols[index];
|
||
const slot=document.createElement('div');slot.className='slot sm place-digit-slot'+(S.answerDigits[index]!==null?' filled':'');slot.dataset.index=index;
|
||
const ghost=document.createElement('div');ghost.className='ghost';ghost.textContent=T('placeDigitGhost');const dots=dotGrid(targetDigits[index]);dots.classList.add('slot-dots');slot.append(ghost,dots);
|
||
if(S.answerDigits[index]!==null){const placed=woodDigit(S.answerDigits[index]);placed.classList.add('in-slot','place-drag-block');slot.append(placed);slot.onclick=()=>{if(!S.solved){S.answerDigits[index]=null;S.feedback='';drawBuild()}}}
|
||
wrap.append(colLabel,slot);return wrap};
|
||
cols.forEach((_,i)=>slots.append(makeSlot(i)));
|
||
const tray=document.createElement('div');tray.className='place-digit-tray lg-tray';
|
||
const attachDrag=block=>{block.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();try{block.releasePointerCapture(e.pointerId)}catch(_){}const clone=block.cloneNode(true);clone.id='dragClone';document.body.append(clone);block.classList.add('dragging');const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+'px';clone.style.top=ev.clientY/zoom+'px'},hit=ev=>{let found=null;slots.querySelectorAll('.place-digit-slot').forEach(slot=>{const r=slot.getBoundingClientRect(),pad=8;if(ev.clientX>=r.left-pad&&ev.clientX<=r.right+pad&&ev.clientY>=r.top-pad&&ev.clientY<=r.bottom+pad)found=slot});return found};moveTo(e);let hovered=null;const move=ev=>{moveTo(ev);const target=hit(ev);if(hovered&&hovered!==target)hovered.classList.remove('droptarget');hovered=target;if(hovered)hovered.classList.add('droptarget')},clean=()=>{document.removeEventListener('pointermove',move);document.removeEventListener('pointerup',up);document.removeEventListener('pointercancel',cancel);clone.remove();block.classList.remove('dragging');if(hovered)hovered.classList.remove('droptarget')},up=ev=>{const target=hit(ev);clean();if(target){S.answerDigits[+target.dataset.index]=+block.dataset.digit;S.feedback='';drawBuild()}},cancel=()=>clean();document.addEventListener('pointermove',move);document.addEventListener('pointerup',up);document.addEventListener('pointercancel',cancel)}};
|
||
'0123456789'.split('').forEach(d=>{const block=woodDigit(+d);block.classList.add('place-drag-block');block.dataset.digit=d;block.setAttribute('role','button');block.setAttribute('aria-label',T('clockDigit').replace('{digit}',d));block.tabIndex=0;block.onkeydown=e=>{if((e.key==='Enter'||e.key===' ')&&!S.solved){e.preventDefault();const index=S.answerDigits.indexOf(null);if(index>=0){S.answerDigits[index]=+d;drawBuild()}}};attachDrag(block);tray.append(block)});
|
||
answer.append(instruction,slots,tray);return answer;
|
||
};
|
||
const drawBuild=()=>{
|
||
stage.className='place-app';stage.innerHTML='';const top=document.createElement('div');top.className='place-top';
|
||
const levelControl=document.createElement('div');levelControl.className='place-level-control';const label=document.createElement('span');label.textContent=T('placeLevel');
|
||
const levels=document.createElement('div');levels.className='lg-levels place-levels';
|
||
[1,2,3].forEach(value=>{const button=document.createElement('button');button.type='button';button.textContent=T('lvl'+value);button.title=T('placeLevel'+value+'Hint');button.classList.toggle('active',S.level===value);button.onclick=()=>chooseLevel(value);levels.append(button)});
|
||
levelControl.append(label,levels);top.append(levelControl);
|
||
const score=document.createElement('span');score.className='place-score';score.textContent=T('placeScore').replace('{score}',S.score).replace('{streak}',S.streak);top.append(score);stage.append(top);
|
||
const card=document.createElement('section');card.className='place-task-card';const title=document.createElement('h3');title.textContent=T('placeBuildTitle');card.append(title);
|
||
const target=document.createElement('div');target.className='place-target';const targetLabel=document.createElement('span');targetLabel.textContent=T('placeYourTask');
|
||
const targetValue=document.createElement('output');targetValue.className='place-task-digital';targetValue.textContent=String(S.target);target.append(targetLabel,targetValue);card.append(target,buildAnswer());
|
||
const feedback=document.createElement('p');feedback.className='place-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');
|
||
if(S.feedback)feedback.textContent=T(S.feedback==='good'?'placeCorrect':S.feedback==='incomplete'?'placeIncomplete':'placeTryAgain');if(S.feedback==='good'&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,'md');cheer.classList.add('av-cheer');feedback.append(cheer)}card.append(feedback);
|
||
const actions=document.createElement('div');actions.className='place-actions';if(!S.solved)actions.append(btn(T('numberlineCheck'),check,''));actions.append(btn(T('placeNextTask'),newTask,'ghost'));card.append(actions);stage.append(card);
|
||
};
|
||
input.oninput=drawExplore;game.onchange=()=>{S.game=game.value;syncSettings();if(S.game!=='build')drawExplore();else if(needsNewTask())newTask();else drawBuild()};
|
||
learningShell(root,[gameSetting,numberSetting,diceBtn],stage);syncSettings();
|
||
if(S.game!=='build')drawExplore();else if(needsNewTask())newTask();else drawBuild();
|
||
return{getState:()=>({...S,answerDigits:S.answerDigits?[...S.answerDigits]:null})};
|
||
}
|
||
function mountMoney(root,init){
|
||
const legacy=init||{},S=Object.assign({level:1,style:'both',game:'make',category:'all',productId:'juice',amode:'blocks',targetCents:375,paidCents:500,selected:[],given:[],input:'',answerDigits:[0,null,0,0],score:0,streak:0,solved:false,feedback:''},legacy);
|
||
if(legacy.targetCents==null&&Number.isFinite(legacy.target))S.targetCents=Math.round(legacy.target*100);
|
||
S.level=Math.max(1,Math.min(3,+S.level||1));S.style=['amount','words','both'].includes(S.style)?S.style:'both';S.game=['make','count','change'].includes(S.game)?S.game:'make';S.category=['all','food','school','play'].includes(S.category)?S.category:'all';S.amode=S.amode==='type'?'type':'blocks';S.selected=Array.isArray(S.selected)?S.selected.map(Number).filter(Number.isFinite):[];S.given=Array.isArray(S.given)?S.given.map(Number).filter(Number.isFinite):[];
|
||
const style=document.createElement('select'),game=document.createElement('select'),category=document.createElement('select'),stage=document.createElement('div'),add=(el,items)=>items.forEach(([value,key])=>el.add(new Option(T(key),value)));
|
||
add(style,[['amount','moneyStyleAmount'],['words','moneyStyleWords'],['both','moneyStyleBoth']]);add(game,[['make','moneyGameMake'],['count','moneyGameCount'],['change','moneyGameChange']]);add(category,[['all','moneyCategoryAll'],['food','moneyCategoryFood'],['school','moneyCategorySchool'],['play','moneyCategoryPlay']]);style.value=S.style;game.value=S.game;category.value=S.category;
|
||
const styleSetting=field(T('moneyLearningStyle'),style),gameSetting=field(T('moneyGame'),game),categorySetting=field(T('moneyCategory'),category),syncSettings=()=>{styleSetting.hidden=S.game==='count';categorySetting.hidden=S.game!=='change'};
|
||
const format=cents=>{const value=(Math.max(0,cents)/100).toFixed(2);return '€ '+(LANG==='nl'?value.replace('.',','):value)};
|
||
const nlSmall=['nul','een','twee','drie','vier','vijf','zes','zeven','acht','negen','tien','elf','twaalf','dertien','veertien','vijftien','zestien','zeventien','achttien','negentien'],nlTens=['','','twintig','dertig','veertig','vijftig','zestig','zeventig','tachtig','negentig'];
|
||
const enSmall=['zero','one','two','three','four','five','six','seven','eight','nine','ten','eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen','eighteen','nineteen'],enTens=['','','twenty','thirty','forty','fifty','sixty','seventy','eighty','ninety'];
|
||
const numberWords=value=>{const n=Math.max(0,Math.min(99,value));if(LANG==='nl'){if(n<20)return nlSmall[n];const ones=n%10;return ones?nlSmall[ones]+nlTens[Math.floor(n/10)]:nlTens[Math.floor(n/10)]}if(n<20)return enSmall[n];const ones=n%10;return enTens[Math.floor(n/10)]+(ones?'-'+enSmall[ones]:'')};
|
||
const amountWords=cents=>{const euros=Math.floor(cents/100),rest=cents%100;if(LANG==='nl')return numberWords(euros)+' euro'+(rest?' en '+numberWords(rest)+' cent':'');return numberWords(euros)+' euro'+(euros===1?'':'s')+(rest?' and '+numberWords(rest)+' cent'+(rest===1?'':'s'):'')};
|
||
const levelRule=()=>MONEY_LEVELS[S.level],denominations=()=>levelRule().denominations;
|
||
const breakdown=amount=>{const result=[];let rest=amount;denominations().forEach(value=>{while(rest>=value&&result.length<24){result.push(value);rest-=value}});return result.sort(()=>Math.random()-.5)};
|
||
const fixedIndices=()=>levelRule().fixedIndices,emptyDigits=()=>[...levelRule().emptyDigits],currentProduct=()=>MONEY_PRODUCTS.find(item=>item.id===S.productId)||MONEY_PRODUCTS[0];
|
||
const legacyProduct=MONEY_PRODUCTS.find(item=>item.icon===legacy.product);if(!MONEY_PRODUCTS.some(item=>item.id===S.productId))S.productId=legacyProduct?.id||'juice';delete S.product;
|
||
const piece=(value,interactive=false)=>{const el=document.createElement(interactive?'button':'span');if(interactive)el.type='button';const type=value>=500?'note':'coin',extension=value>=500?'jpg':'png';el.className='money-piece '+type;el.dataset.value=value;const image=document.createElement('img'),fallback=document.createElement('span');image.className='money-piece-image';image.src=`/img/money/${type}-${value}.${extension}`;image.alt='';image.draggable=false;image.decoding='async';image.setAttribute('aria-hidden','true');image.onerror=()=>el.classList.add('image-missing');fallback.className='money-piece-fallback';fallback.textContent=value>=100?'€'+value/100:value+'c';el.append(image,fallback);el.setAttribute('aria-label',format(value));return el};
|
||
const pile=(values,interactive=false,wallet=null)=>{const wrap=document.createElement('div');wrap.className='money-pile'+(interactive?' interactive':' readonly');values.forEach((value,index)=>{const el=piece(value,interactive);if(interactive){el.title=T('moneyRemove');el.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();const startX=e.clientX,startY=e.clientY,clone=el.cloneNode(true);clone.id='dragClone';clone.classList.add('money-drag-clone','from-wallet');document.body.append(clone);const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+'px';clone.style.top=ev.clientY/zoom+'px'},over=ev=>{const r=wallet.getBoundingClientRect();return ev.clientX>=r.left&&ev.clientX<=r.right&&ev.clientY>=r.top&&ev.clientY<=r.bottom},hasMoved=ev=>Math.hypot(ev.clientX-startX,ev.clientY-startY)>=6;moveTo(e);const move=ev=>{moveTo(ev);wallet.classList.toggle('money-remove-target',hasMoved(ev)&&!over(ev))},clean=()=>{document.removeEventListener('pointermove',move);document.removeEventListener('pointerup',up);document.removeEventListener('pointercancel',cancel);wallet.classList.remove('money-remove-target');clone.remove()},remove=()=>{S.selected.splice(index,1);S.feedback='';draw()},up=ev=>{const moved=hasMoved(ev),outside=!over(ev);clean();if(!moved||(moved&&outside))remove()},cancel=()=>clean();document.addEventListener('pointermove',move);document.addEventListener('pointerup',up);document.addEventListener('pointercancel',cancel)};el.onkeydown=e=>{if((e.key==='Enter'||e.key===' '||e.key==='Delete'||e.key==='Backspace')&&!S.solved){e.preventDefault();S.selected.splice(index,1);S.feedback='';draw()}}}wrap.append(el)});return wrap};
|
||
const amountDisplay=(cents,compact=false)=>{const wrap=document.createElement('div');wrap.className='money-assignment'+(compact?' compact':'');if(['amount','both'].includes(S.style)){const output=document.createElement('output');output.className='money-task-amount';output.textContent=format(cents);wrap.append(output)}if(['words','both'].includes(S.style)){const words=document.createElement('p');words.className='money-task-words';words.textContent=amountWords(cents);wrap.append(words)}return wrap};
|
||
const woodDigit=value=>{const block=document.createElement('span');block.className='blok sm money-digit-block';block.dataset.digit=value;const label=document.createElement('span');label.className='bl';label.textContent=String(value);const dots=dotGrid(Number(value));dots.classList.add('blok-dots');block.append(label,dots);return block};
|
||
const answerValue=()=>{if(S.amode==='blocks'){if(S.answerDigits.some(value=>value===null))return null;return Number(S.answerDigits.join(''))}if(!S.input)return null;const parts=S.input.replace('.',',').split(',');if(parts.length>2||parts[1]?.length>2)return null;return (+parts[0]||0)*100+Number((parts[1]||'').padEnd(2,'0')||0)};
|
||
const digitAnswer=()=>{const answer=document.createElement('div');answer.className='money-digit-answer';const instruction=document.createElement('p');instruction.textContent=T('moneyBuildHint');const slots=document.createElement('div');slots.className='money-answer-slots';const fixed=new Set(fixedIndices());
|
||
/* zelfde patroon als letterblokken/rekenblokken: het vak toont altijd het
|
||
puntenpatroon van het júíste cijfer op die plek. */
|
||
const targetDigits=String(expected()).padStart(4,'0');
|
||
const makeSlot=index=>{const slot=document.createElement('div');slot.className='slot sm money-digit-slot'+(fixed.has(index)?' fixed':'')+(S.answerDigits[index]!==null?' filled':'');slot.dataset.index=index;const ghost=document.createElement('div');ghost.className='ghost';ghost.textContent='?';const dots=dotGrid(targetDigits[index]);dots.classList.add('slot-dots');slot.append(ghost,dots);if(S.answerDigits[index]!==null){const placed=woodDigit(S.answerDigits[index]);placed.classList.add('in-slot','money-drag-digit');slot.append(placed);if(!fixed.has(index))slot.onclick=()=>{if(!S.solved){S.answerDigits[index]=null;S.feedback='';draw()}}}return slot};
|
||
const euro=document.createElement('strong');euro.textContent='€';slots.append(euro,makeSlot(0),makeSlot(1));const comma=document.createElement('strong');comma.textContent=LANG==='nl'?',':'.';slots.append(comma,makeSlot(2),makeSlot(3));
|
||
const tray=document.createElement('div');tray.className='money-digit-tray lg-tray';const attachDrag=block=>{block.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();const clone=block.cloneNode(true);clone.id='dragClone';document.body.append(clone);block.classList.add('dragging');const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+'px';clone.style.top=ev.clientY/zoom+'px'},hit=ev=>{let found=null;slots.querySelectorAll('.money-digit-slot:not(.fixed)').forEach(slot=>{const r=slot.getBoundingClientRect(),pad=8;if(ev.clientX>=r.left-pad&&ev.clientX<=r.right+pad&&ev.clientY>=r.top-pad&&ev.clientY<=r.bottom+pad)found=slot});return found};moveTo(e);let hovered=null;const move=ev=>{moveTo(ev);const target=hit(ev);if(hovered&&hovered!==target)hovered.classList.remove('droptarget');hovered=target;if(hovered)hovered.classList.add('droptarget')},clean=()=>{document.removeEventListener('pointermove',move);document.removeEventListener('pointerup',up);document.removeEventListener('pointercancel',cancel);clone.remove();block.classList.remove('dragging');if(hovered)hovered.classList.remove('droptarget')},up=ev=>{const target=hit(ev);clean();if(target){S.answerDigits[+target.dataset.index]=+block.dataset.digit;S.feedback='';draw()}},cancel=()=>clean();document.addEventListener('pointermove',move);document.addEventListener('pointerup',up);document.addEventListener('pointercancel',cancel)}};'0123456789'.split('').forEach(digit=>{const block=woodDigit(+digit);block.classList.add('money-drag-digit');block.setAttribute('role','button');block.setAttribute('aria-label',T('clockDigit').replace('{digit}',digit));block.tabIndex=0;block.onkeydown=e=>{if((e.key==='Enter'||e.key===' ')&&!S.solved){e.preventDefault();const index=S.answerDigits.findIndex((value,i)=>value===null&&!fixed.has(i));if(index>=0){S.answerDigits[index]=+digit;draw()}}};attachDrag(block);tray.append(block)});answer.append(instruction,slots,tray);return answer};
|
||
const typeAnswer=()=>{const wrap=document.createElement('div');wrap.className='money-type-answer';const hint=document.createElement('p');hint.textContent=T('moneyTypeHint');const display=document.createElement('output');display.className='money-type-display';display.textContent='€ '+(S.input||'…');const pad=document.createElement('div');pad.className='money-keypad';['7','8','9','⌫','4','5','6',LANG==='nl'?',':'.','1','2','3','0'].forEach(key=>{const button=btn(key,()=>{if(S.solved)return;if(key==='⌫')S.input=S.input.slice(0,-1);else if(key===','||key==='.'){if(!/[,.]/.test(S.input))S.input=(S.input||'0')+key}else{const parts=S.input.split(/[,.]/);if((parts.length===1&&parts[0].length<2)||(parts.length===2&&parts[1].length<2))S.input+=key}S.feedback='';draw()});pad.append(button)});wrap.append(hint,display,pad);return wrap};
|
||
const addMoney=value=>{if(!S.solved){S.selected.push(value);S.feedback='';draw()}};
|
||
const moneyTray=wallet=>{const section=document.createElement('section');section.className='money-tray-wrap';const label=document.createElement('b');label.textContent=T('moneyTray');const tray=document.createElement('div');tray.className='money-cash-tray';denominations().forEach(value=>{const item=piece(value,true);item.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();const startX=e.clientX,startY=e.clientY,clone=item.cloneNode(true);clone.id='dragClone';clone.classList.add('money-drag-clone');document.body.append(clone);const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+'px';clone.style.top=ev.clientY/zoom+'px'},over=ev=>{const r=wallet.getBoundingClientRect(),pad=12;return ev.clientX>=r.left-pad&&ev.clientX<=r.right+pad&&ev.clientY>=r.top-pad&&ev.clientY<=r.bottom+pad};moveTo(e);const move=ev=>{moveTo(ev);wallet.classList.toggle('droptarget',over(ev))},clean=()=>{document.removeEventListener('pointermove',move);document.removeEventListener('pointerup',up);document.removeEventListener('pointercancel',cancel);wallet.classList.remove('droptarget');clone.remove()},up=ev=>{const tapped=Math.hypot(ev.clientX-startX,ev.clientY-startY)<6,target=over(ev);clean();if(tapped||target)addMoney(value)},cancel=()=>clean();document.addEventListener('pointermove',move);document.addEventListener('pointerup',up);document.addEventListener('pointercancel',cancel)};item.onkeydown=e=>{if((e.key==='Enter'||e.key===' ')&&!S.solved){e.preventDefault();addMoney(value)}};tray.append(item)});section.append(label,tray);return section};
|
||
const expected=()=>S.game==='change'?S.paidCents-S.targetCents:S.targetCents,setFeedback=ok=>{if(ok){S.solved=true;S.score++;S.streak++;S.feedback='good';sndGood()}else{S.streak=0;S.feedback='bad';sndBad()}draw()};
|
||
const check=()=>{const value=S.game==='make'?S.selected.reduce((sum,item)=>sum+item,0):answerValue();if(value===null){S.feedback='incomplete';sndBad();draw();return}setFeedback(value===expected())};
|
||
const feedbackText=()=>{if(S.feedback==='good')return T('moneyCorrect');if(S.feedback==='incomplete')return T('moneyIncomplete');if(S.feedback==='bad'&&S.game==='make'){const total=S.selected.reduce((sum,item)=>sum+item,0),difference=S.targetCents-total;return T(difference>0?'moneyNeed':'moneyTooMuch').replace('{amount}',format(Math.abs(difference)))}return T('moneyTryAgain')};
|
||
const resetAnswer=()=>{S.selected=[];S.input='';S.answerDigits=emptyDigits();S.feedback='';S.solved=false;draw()};
|
||
const newTask=()=>{const task=createMoneyTask(S.level,S.game,S.category,S.targetCents,S.productId);S.targetCents=task.targetCents;S.paidCents=task.paidCents;S.productId=task.productId;S.selected=[];S.given=breakdown(task.targetCents);S.input='';S.answerDigits=emptyDigits();S.solved=false;S.feedback='';draw()};
|
||
const chooseLevel=value=>{S.level=value;S.score=0;S.streak=0;newTask()},toggleMode=()=>{S.amode=S.amode==='blocks'?'type':'blocks';S.input='';S.answerDigits=emptyDigits();S.feedback='';draw()};
|
||
const draw=()=>{S.style=style.value;S.game=game.value;stage.className='money-app';stage.innerHTML='';const top=document.createElement('div');top.className='money-top';const levelControl=document.createElement('div');levelControl.className='money-level-control';const levelLabel=document.createElement('span');levelLabel.textContent=T('moneyLevel');const levels=document.createElement('div');levels.className='lg-levels money-levels';[1,2,3].forEach(value=>{const button=document.createElement('button');button.type='button';button.textContent=T('lvl'+value);button.title=T('moneyLevel'+value+'Hint');button.classList.toggle('active',S.level===value);button.onclick=()=>chooseLevel(value);levels.append(button)});levelControl.append(levelLabel,levels);top.append(levelControl);if(S.game!=='make'){const mode=btn(S.amode==='blocks'?'🧱':'⌨️',toggleMode);mode.classList.add('money-mode');mode.title=T('amode');top.append(mode)}const score=document.createElement('span');score.className='money-score';score.textContent=T('moneyScore').replace('{score}',S.score).replace('{streak}',S.streak);top.append(score);stage.append(top);
|
||
const card=document.createElement('section');card.className='money-task-card';const title=document.createElement('h3'),levelHint=document.createElement('p');title.textContent=T(S.game==='make'?'moneyMakeTitle':S.game==='count'?'moneyCountTitle':'moneyChangeTitle');levelHint.className='money-level-hint';levelHint.textContent=T('moneyLevel'+S.level+'Hint');card.append(title,levelHint);
|
||
if(S.game==='make'){const target=document.createElement('div');target.className='money-target';const label=document.createElement('span');label.textContent=T('moneyYourTask');target.append(label,amountDisplay(S.targetCents,true));card.append(target);const wallet=document.createElement('div');wallet.className='money-wallet';const walletHead=document.createElement('div');const walletLabel=document.createElement('b');walletLabel.textContent=T('moneyWallet');const total=document.createElement('output');total.textContent=format(S.selected.reduce((sum,item)=>sum+item,0));walletHead.append(walletLabel,total);wallet.append(walletHead);if(S.selected.length)wallet.append(pile(S.selected,true,wallet));else{const hint=document.createElement('p');hint.textContent=T('moneyWalletHint');wallet.append(hint)}card.append(wallet,moneyTray(wallet))}
|
||
if(S.game==='count'){const target=document.createElement('div');target.className='money-count-task';target.append(pile(S.given));card.append(target,S.amode==='blocks'?digitAnswer():typeAnswer())}
|
||
if(S.game==='change'){const item=currentProduct(),shop=document.createElement('div');shop.className='money-shop-task';const product=document.createElement('div');product.className='money-product';const emoji=document.createElement('span'),productName=document.createElement('strong');emoji.className='money-product-icon';emoji.textContent=item.icon;productName.className='money-product-name';productName.textContent=LANG==='nl'?item.nl:item.en;const priceLabel=document.createElement('b');priceLabel.textContent=T('moneyPrice');product.append(emoji,productName,priceLabel,amountDisplay(S.targetCents,true));const paid=document.createElement('div');paid.className='money-paid';const paidLabel=document.createElement('b');paidLabel.textContent=T('moneyPaid');const paidAmount=document.createElement('output');paidAmount.textContent=format(S.paidCents);paid.append(paidLabel,paidAmount,pile([S.paidCents]));shop.append(product,paid);card.append(shop,S.amode==='blocks'?digitAnswer():typeAnswer())}
|
||
const feedback=document.createElement('p');feedback.className='money-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');feedback.textContent=S.feedback?feedbackText():'';if(S.feedback==='good'&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,'md');cheer.classList.add('av-cheer');feedback.append(cheer)}card.append(feedback);const actions=document.createElement('div');actions.className='money-actions';if(!S.solved)actions.append(btn(T('numberlineCheck'),check,''));if(!S.solved)actions.append(btn(T('moneyClear'),resetAnswer,'ghost'));actions.append(btn(T('moneyNextTask'),newTask,'ghost'));card.append(actions);stage.append(card)};
|
||
style.onchange=draw;game.onchange=()=>{S.game=game.value;syncSettings();newTask()};category.onchange=()=>{S.category=category.value;newTask()};learningShell(root,[styleSetting,gameSetting,categorySetting],stage);syncSettings();newTask();return{getState:()=>({...S,selected:[...S.selected],given:[...S.given],answerDigits:[...S.answerDigits]})};
|
||
}
|
||
|
||
function mountWordtypes(root,init){
|
||
const S=Object.assign({sentence:LANG==='nl'?'De vrolijke hond rent snel':'The cheerful dog runs quickly',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 defaults=LANG==='nl'?[{time:'08:30',text:'Start'},{time:'10:15',text:'Pauze'}]:[{time:'08:30',text:'Start'},{time:'10:15',text:'Break'}],S=Object.assign({items:defaults},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 activityIcon=value=>{const text=value.toLocaleLowerCase();if(/pauze|break|buiten|outside/.test(text))return'🛝';if(/eten|lunch|fruit|eat/.test(text))return'🍎';if(/lees|taal|read|language/.test(text))return'📚';if(/reken|math/.test(text))return'🔢';if(/gym|sport|move/.test(text))return'⚽';if(/crea|teken|muziek|art|music/.test(text))return'🎨';return'⭐'};
|
||
const draw=()=>{stage.className='schedule';stage.innerHTML='';const status=document.createElement('p');status.className='class-total';status.textContent=T('scheduleCount').replace('{count}',S.items.length);stage.append(status);S.items.sort((a,b)=>a.time.localeCompare(b.time)).forEach((it,i)=>{const row=document.createElement('div');const icon=document.createElement('i');icon.className='schedule-icon';icon.textContent=activityIcon(it.text);const t=document.createElement('time');t.textContent=it.time;const s=document.createElement('span');s.textContent=it.text;row.append(icon,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='';const total=document.createElement('p');total.className='class-total';total.textContent=T('moodTotal').replace('{count}',S.votes.reduce((sum,n)=>sum+n,0));stage.append(total);faces.forEach((f,i)=>{const b=btn(f,()=>{S.votes[i]++;draw()});b.setAttribute('aria-label',T('moodVote').replace('{face}',f));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:LANG==='nl'?'Wat kies jij?':'What do you choose?',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;const total=S.options.reduce((sum,item)=>sum+item.votes,0),totalEl=document.createElement('p');totalEl.className='class-total';totalEl.textContent=T('pollTotal').replace('{count}',total);stage.append(h,totalEl);const max=Math.max(1,...S.options.map(x=>x.votes));S.options.forEach(o=>{const b=btn(o.text,()=>{o.votes++;draw()});b.setAttribute('aria-label',`${o.text}: ${o.votes}`);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}))})};
|
||
}
|