teach/public/js/data.js
Ramon bab1018142
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
fix: cijferblokken kregen elk een eigen puntenpatroon + niveaus voor plaatswaarde (v0.4.16-beta)
- De cijferblokken (oefenklok, geldwidget) hergebruikten de braille a-j-
  patronen, die nooit de onderste rij (posities 3/6) gebruiken - daardoor
  leken alle tien blokken op elkaar. Eigen DIGIT_DOTCODE-tabel die alle
  zes stippen inzet, geeft elk cijfer nu een echt eigen vorm.
- Plaatswaardetabel-widget had geen enkele oefenvorm (alleen een vrij
  invoerveld); nieuwe "Bouwen"-stand met drie niveaus (T/E, H/T/E, of
  HD/TD/D/H/T/E) laat kinderen een getal opbouwen door cijferblokken in
  de juiste kolom te slepen, met score/reeks en directe feedback - zelfde
  bouwsteen als de oefenklok/het geldwidget. De vrije verkenstand blijft
  ongewijzigd bestaan.
- Bijvangst: instellingenvelden met .hidden (bv. "24-uursklok" bij de
  oefenklok) werden nooit echt verborgen door een CSS-specificiteitsbotsing
  (.edu-controls label versloeg [hidden]); toegevoegde regel herstelt dit
  voor alle leerwidgets.
2026-07-18 15:46:24 +02:00

382 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

/* teach - woorddata: algemene kaart, thema's, gedeelde woordbronnen */
/* =========================================================
Letter game data
==========================================================*/
const DOTCODE = {
a:[1], b:[1,2], c:[1,4], d:[1,4,5], e:[1,5], f:[1,2,4], g:[1,2,4,5], h:[1,2,5],
i:[2,4], j:[2,4,5], k:[1,3], l:[1,2,3], m:[1,3,4], n:[1,3,4,5], o:[1,3,5],
p:[1,2,3,4], q:[1,2,3,4,5], r:[1,2,3,5], s:[2,3,4], t:[2,3,4,5], u:[1,3,6],
v:[1,2,3,6], w:[2,4,5,6], x:[1,3,4,6], y:[1,3,4,5,6], z:[1,3,5,6]
};
/* cijferblokken gebruikten hiervoor de braille a-j-patronen, maar die
gebruiken nooit posities 3/6 (de onderste rij) - op een kind-oog leken
alle tien blokken daardoor op elkaar. Een eigen codetabel die alle zes
stippen inzet, geeft elk cijfer een echt eigen vorm. */
const DIGIT_DOTCODE = {
"0":[], "1":[2], "2":[2,5], "3":[1,2,3], "4":[1,3,4,6],
"5":[1,2,3,4,5], "6":[1,2,3,4,5,6], "7":[1,2,4,5], "8":[1,2,3,4,6], "9":[1,4,6]
};
const VOWELS = "aeiou";
function dotGrid(letter){
const code = (Object.prototype.hasOwnProperty.call(DIGIT_DOTCODE, letter) ? DIGIT_DOTCODE[letter] : DOTCODE[letter]) || [];
const grid = document.createElement("div");
grid.className = "dots";
[1,4,2,5,3,6].forEach(p=>{
const d = document.createElement("i");
d.className = code.includes(p) ? "on" : "off";
grid.appendChild(d);
});
return grid;
}
/* De taalplaatjes hebben één centrale bron. Daardoor gebruiken de spelletjes,
de themakiezer en de afbeeldingenbibliotheek altijd dezelfde woorden en
verdwijnen oude plaatjes niet wanneer de woordenschat wordt uitgebreid. */
const LANGUAGE_THEME_DEFS = [
{id:"animals", icon:"🐾", nl:"Dieren", en:"Animals", colors:["#eaf7df","#cdeeb5"]},
{id:"food", icon:"🍎", nl:"Eten & drinken", en:"Food & drink", colors:["#fff0dc","#ffd9aa"]},
{id:"nature", icon:"🌿", nl:"Natuur & weer", en:"Nature & weather", colors:["#e4f7ee","#bfead4"]},
{id:"travel", icon:"🚲", nl:"Onderweg", en:"Getting around", colors:["#e6f3ff","#beddf8"]},
{id:"home", icon:"🎒", nl:"Thuis & school", en:"Home & school", colors:["#f1ecff","#d9ccff"]},
{id:"body", icon:"👕", nl:"Lichaam & kleding", en:"Body & clothes", colors:["#ffeaf1","#ffcbdc"]},
{id:"actions", icon:"🏃", nl:"Doen", en:"Actions", colors:["#fff8d9","#f6e8a4"]},
{id:"feelings", icon:"🙂", nl:"Gevoelens", en:"Feelings", colors:["#e9f5ff","#c8e4fa"]}
];
const LANGUAGE_CONCEPTS = [
["monkey","animals","aap","monkey","🐵"],
["cat","animals","kat","cat","🐈"],
["dog","animals","hond","dog","🐕"],
["fish","animals","vis","fish","🐟"],
["hen","animals","kip","hen","🐔"],
["mouse","animals","muis","mouse","🐭"],
["bear","animals","beer","bear","🐻"],
["duck","animals","eend","duck","🦆"],
["frog","animals","kikker","frog","🐸"],
["bee","animals","bij","bee","🐝"],
["cow","animals","koe","cow","🐄"],
["pig","animals","varken","pig","🐷"],
["owl","animals","uil","owl","🦉"],
["goat","animals","geit","goat","🐐"],
["hare","animals","haas","hare","🐇"],
["horse","animals","paard","horse","🐴"],
["lion","animals","leeuw","lion","🦁"],
["ant","animals","mier","ant","🐜"],
["egg","food","ei","egg","🥚"],
["apple","food","appel","apple","🍎"],
["pear","food","peer","pear","🍐"],
["cheese","food","kaas","cheese","🧀"],
["milk","food","melk","milk","🥛"],
["cake","food","taart","cake","🎂"],
["bread","food","brood","bread","🍞"],
["soup","food","soep","soup","🥣"],
["rice","food","rijst","rice","🍚"],
["corn","food","mais","corn","🌽"],
["grape","food","druif","grape","🍇"],
["strawberry","food","aardbei","strawberry","🍓"],
["candy","food","snoep","candy","🍬"],
["lemon","food","citroen","lemon","🍋"],
["pizza","food","pizza","pizza","🍕"],
["juice","food","sap","juice","🧃"],
["cookie","food","koek","cookie","🍪"],
["carrot","food","wortel","carrot","🥕"],
["sun","nature","zon","sun","☀️"],
["moon","nature","maan","moon","🌙"],
["star","nature","ster","star","⭐"],
["tree","nature","boom","tree","🌳"],
["rose","nature","roos","rose","🌹"],
["fire","nature","vuur","fire","🔥"],
["flower","nature","bloem","flower","🌷"],
["leaf","nature","blad","leaf","🍃"],
["cloud","nature","wolk","cloud","☁️"],
["rain","nature","regen","rain","🌧️"],
["snow","nature","sneeuw","snow","❄️"],
["water","nature","water","water","💧"],
["stone","nature","steen","stone","🪨"],
["sand","nature","zand","sand","🏖️"],
["sea","nature","zee","sea","🌊"],
["forest","nature","bos","forest","🌲"],
["grass","nature","gras","grass","🌱"],
["wind","nature","wind","wind","💨"],
["rainbow","nature","regenboog","rainbow","🌈"],
["car","travel","auto","car","🚗"],
["bike","travel","fiets","bike","🚲"],
["boat","travel","boot","boat","⛵"],
["train","travel","trein","train","🚂"],
["bus","travel","bus","bus","🚌"],
["taxi","travel","taxi","taxi","🚕"],
["truck","travel","vrachtauto","truck","🚚"],
["jet","travel","vliegtuig","plane","✈️"],
["tram","travel","tram","tram","🚋"],
["ship","travel","schip","ship","🚢"],
["rocket","travel","raket","rocket","🚀"],
["road","travel","weg","road","🛣️"],
["wheel","travel","wiel","wheel","🛞"],
["sail","travel","zeil","sail","⛵"],
["scooter","travel","step","scooter","🛴"],
["tractor","travel","tractor","tractor","🚜"],
["helicopter","travel","helikopter","helicopter","🚁"],
["house","home","huis","house","🏠"],
["book","home","boek","book","📕"],
["pen","home","pen","pen","🖊️"],
["pencil","home","potlood","pencil","✏️"],
["bag","home","tas","bag","🎒"],
["coat","home","jas","coat","🧥"],
["chair","home","stoel","chair","🪑"],
["bed","home","bed","bed","🛏️"],
["door","home","deur","door","🚪"],
["clock","home","klok","clock","⏰"],
["glasses","home","bril","glasses","👓"],
["ball","home","bal","ball","⚽"],
["doll","home","pop","doll","🪆"],
["block","home","blok","block","🧱"],
["key","home","sleutel","key","🔑"],
["lamp","home","lamp","lamp","💡"],
["cup","home","beker","cup","🥤"],
["brush","home","borstel","brush","🪥"],
["drum","home","trommel","drum","🥁"],
["gift","home","cadeau","gift","🎁"],
["paint","home","verf","paint","🎨"],
["scissors","home","schaar","scissors","✂️"],
["desk","home","bureau","desk","🗄️"],
["school","home","school","school","🏫"],
["hand","body","hand","hand","✋"],
["foot","body","voet","foot","🦶"],
["nose","body","neus","nose","👃"],
["eye","body","oog","eye","👁️"],
["ear","body","oor","ear","👂"],
["mouth","body","mond","mouth","👄"],
["hair","body","haar","hair","🧑‍🦱"],
["head","body","hoofd","head","🙂"],
["arm","body","arm","arm","💪"],
["leg","body","been","leg","🦵"],
["cap","body","pet","cap","🧢"],
["sock","body","sok","sock","🧦"],
["shoe","body","schoen","shoe","👟"],
["dress","body","jurk","dress","👗"],
["shirt","body","shirt","shirt","👕"],
["glove","body","want","glove","🧤"],
["pants","body","broek","trousers","👖"],
["skirt","body","rok","skirt","👗"],
["tooth","body","tand","tooth","🦷"],
["read","actions","lezen","read","📖"],
["eat","actions","eten","eat","🍽️"],
["drink","actions","drinken","drink","🥤"],
["sleep","actions","slapen","sleep","😴"],
["walk","actions","lopen","walk","🚶"],
["swim","actions","zwemmen","swim","🏊"],
["jump","actions","springen","jump","🤸"],
["laugh","actions","lachen","laugh","😄"],
["cry","actions","huilen","cry","😢"],
["sing","actions","zingen","sing","🎤"],
["dance","actions","dansen","dance","💃"],
["draw","actions","tekenen","draw","🖍️"],
["write","actions","schrijven","write","✍️"],
["play","actions","spelen","play","🧸"],
["sit","actions","zitten","sit","🧎"],
["run","actions","rennen","run","🏃"],
["climb","actions","klimmen","climb","🧗"],
["wave","actions","zwaaien","wave","👋"],
["look","actions","kijken","look","👀"],
["think","actions","denken","think","🤔"],
["happy","feelings","blij","happy","😀"],
["sad","feelings","verdrietig","sad","😢"],
["angry","feelings","boos","angry","😠"],
["scared","feelings","bang","scared","😨"],
["tired","feelings","moe","tired","🥱"],
["sick","feelings","ziek","sick","🤒"],
["proud","feelings","trots","proud","😌"],
["calm","feelings","rustig","calm","🙂"],
["surprised","feelings","verrast","surprised","😮"],
["excited","feelings","enthousiast","excited","🤩"]
].map(([id,theme,nl,en,pic])=>({id,theme,nl,en,pic}));
/* De vertrouwde startkaart blijft exact dezelfde 30 woorden houden. */
const START_WORD_KEYS = {
nl:["monkey","fish","sun","cat","ball","egg","tree","house","moon","boat","hen","mouse","book","star","rose","fire","bike","cake","coat","glasses","clock","pear","cheese","duck","hand","foot","nose","eye","ear","mouth"],
en:["cat","dog","sun","egg","ball","fish","tree","moon","boat","star","book","cake","house","mouse","apple","train","fire","rose","bear","duck","frog","bee","cow","pig","hand","foot","nose","eye","ear","milk"]
};
const WORDS = Object.fromEntries(["nl","en"].map(lang=>[
lang, START_WORD_KEYS[lang].map(id=>{
const concept=LANGUAGE_CONCEPTS.find(item=>item.id===id);
return [concept[lang],concept.pic];
})
]));
function languageThemeList(lang=LANG){
return LANGUAGE_THEME_DEFS.map(theme=>({
...theme,
name:theme[lang],
words:LANGUAGE_CONCEPTS.filter(item=>item.theme===theme.id).map(item=>[item[lang],item.pic])
}));
}
function languageEmojiDataUrl(emoji,colors=["#f4f8ff","#dce9fa"]){
const svg=`<svg xmlns="http://www.w3.org/2000/svg" width="320" height="320" viewBox="0 0 320 320"><defs><linearGradient id="g" x2="1" y2="1"><stop stop-color="${colors[0]}"/><stop offset="1" stop-color="${colors[1]}"/></linearGradient></defs><rect width="320" height="320" rx="48" fill="url(#g)"/><circle cx="160" cy="160" r="112" fill="#fff" opacity=".92"/><text x="160" y="177" text-anchor="middle" dominant-baseline="middle" font-family="Apple Color Emoji,Segoe UI Emoji,Noto Color Emoji,sans-serif" font-size="148">${emoji}</text></svg>`;
return "data:image/svg+xml;charset=utf-8,"+encodeURIComponent(svg);
}
function languagePictureCatalog(lang=LANG){
const builtinThemes=languageThemeList(lang).map((theme,index)=>({
id:-200-index,nameNl:theme.nl,nameEn:theme.en,scope:"global",filterOnly:true,
languageBuiltin:true,wordPicture:true,icon:theme.icon
}));
const themeIds=new Map(LANGUAGE_THEME_DEFS.map((theme,index)=>[theme.id,-200-index]));
const images=LANGUAGE_CONCEPTS.map((item,index)=>{
const theme=LANGUAGE_THEME_DEFS.find(def=>def.id===item.theme);
return {
id:-1000-index,name:item[lang],folder:`Taal/Woordenschat/${theme[lang]}`,
scope:"global",themes:[themeIds.get(item.theme)],tags:[item.nl,item.en,theme.nl,theme.en],
src:languageEmojiDataUrl(item.pic,theme.colors),value:item.pic,canManage:false,canDelete:false,
wordPicture:true,builtin:true
};
});
const ownThemes=[];
const customImages=[];
let customId=-5000;
const addOwn=(words,name,folder,themeId)=>{
(words||[]).forEach(([word,pic])=>{
if(!word||!pic)return;
customImages.push({
id:customId--,name:word,folder,scope:"personal",themes:[themeId],tags:[word,name],
src:isImg(pic)?pic:languageEmojiDataUrl(pic),value:pic,canManage:false,canDelete:false,
wordPicture:true,builtin:false
});
});
};
const generalThemeId=-400;
ownThemes.push({id:generalThemeId,nameNl:"Mijn woordkaarten",nameEn:"My word cards",scope:"personal",filterOnly:true,wordPicture:true,icon:"⭐"});
addOwn(GENERAL_EXTRA[lang],lang==="nl"?"Mijn woordkaarten":"My word cards","Taal/Mijn woordkaarten",generalThemeId);
THEMES[lang].forEach((theme,index)=>{
const id=-401-index;
ownThemes.push({id,nameNl:theme.name,nameEn:theme.name,scope:"personal",filterOnly:true,wordPicture:true,icon:"📁"});
addOwn(theme.words,theme.name,["Taal/Mijn themas",theme.folder,theme.name].filter(Boolean).join("/"),id);
});
return {themes:builtinThemes.concat(ownThemes),images:images.concat(customImages)};
}
/* teacher's own additions to the general card (persisted per user) */
let GENERAL_EXTRA = {nl:[], en:[]};
function generalList(){ return WORDS[LANG].concat(GENERAL_EXTRA[LANG]); }
const shuffle = a => { a=a.slice(); for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]];} return a; };
/* custom themes: folders of own words.
{nl:[{name:"Herfst", words:[[word,pic],...]}], en:[...]} */
let THEMES = {nl:[], en:[]};
/* ankerbibliotheek: los van borden bewaarde ankerdiagrammen, herbruikbaar op
elk bord. [{id, name, folder?, cells, pics}] - cells/pics in dezelfde vorm
als de widget-staat van anchor.js; folder is een pad ("A/B") zoals bij de
thema's en borden in de mappenverkenner. */
let ANCHOR_LIB = [];
function themesFromData(data){
if(!data) return {nl:[],en:[]};
if(data.themes) return data.themes;
/* migrate old flat word lists into one theme */
const t = {nl:[],en:[]};
if(data.words) ["nl","en"].forEach(l=>{
if(Array.isArray(data.words[l]) && data.words[l].length)
t[l] = [{name: l==="nl" ? "Mijn woorden" : "My words", words: data.words[l]}];
});
return t;
}
const isImg = p => typeof p==="string" && /^(?:data:|https?:|blob:|\/)/.test(p);
function setPic(el, pic){
if(isImg(pic)){ el.innerHTML = ""; const im = document.createElement("img"); im.src = pic; im.alt=""; el.appendChild(im); }
else el.textContent = pic;
}
/* ---- ARASAAC pictogram lookup (shared by the word editor + anchor grid) ---- */
async function arasaacSearch(term, lang){
const resp = await fetch(`https://api.arasaac.org/api/pictograms/${lang}/search/${encodeURIComponent(term)}`);
if(!resp.ok) throw new Error("http "+resp.status);
return resp.json();
}
const arasaacUrl = id => `https://static.arasaac.org/pictograms/${id}/${id}_300.png`;
/* ---- shared word source (general card + themes + anchor grids) ---- */
/* anchor-word widgets register their words here so every language widget
can use them as a theme automatically */
let ANCHORS = []; /* [{el, name, words:[[w,pic],...]}] */
function anchorSources(){
ANCHORS = ANCHORS.filter(a=>a.el && document.contains(a.el));
return ANCHORS.filter(a=>a.words.length);
}
function normWordSource(card){
if(typeof card==="string"){
if(card.startsWith("b:")){
if(languageThemeList().some(theme=>theme.id===card.slice(2))) return card;
}else if(card.startsWith("t")){
const th = THEMES[LANG][+card.slice(1)];
if(th && th.words.length) return card;
}else if(card.startsWith("a")){
if(anchorSources()[+card.slice(1)]) return card;
}
}
return "g";
}
function wordSourceList(card){
card = normWordSource(card);
if(card.startsWith("b:")) return languageThemeList().find(theme=>theme.id===card.slice(2)).words;
if(card.startsWith("t")) return THEMES[LANG][+card.slice(1)].words;
if(card.startsWith("a")) return anchorSources()[+card.slice(1)].words;
return generalList();
}
function fillWordSourceSel(sel, card){
sel.innerHTML = "";
const og = document.createElement("option");
og.value = "g"; og.textContent = "⭐ "+T("cardStart");
sel.appendChild(og);
const builtinGroup = document.createElement("optgroup");
builtinGroup.label = "📚 "+T("wordThemes");
languageThemeList().forEach(theme=>{
const option=document.createElement("option");
option.value="b:"+theme.id;
option.textContent=`${theme.icon} ${theme.name} (${theme.words.length})`;
builtinGroup.appendChild(option);
});
sel.appendChild(builtinGroup);
/* thema's zonder map bovenaan, daarna per map gegroepeerd (optgroup) -
zelfde indeling als de theme-dropdown in de woorden-editor */
const groups = new Map();
THEMES[LANG].forEach((th,i)=>{
if(!th.words.length) return;
const f = th.folder || "";
if(!groups.has(f)) groups.set(f, []);
groups.get(f).push([th, i]);
});
const addOpt = (parent, th, i)=>{
const o = document.createElement("option");
o.value = "t"+i; o.textContent = "★ "+th.name;
parent.appendChild(o);
};
(groups.get("") || []).forEach(([th,i])=>addOpt(sel, th, i));
[...groups.keys()].filter(f=>f).sort().forEach(f=>{
const grp = document.createElement("optgroup");
grp.label = "📁 " + f;
groups.get(f).forEach(([th,i])=>addOpt(grp, th, i));
sel.appendChild(grp);
});
anchorSources().forEach((a,i)=>{
const o = document.createElement("option");
o.value = "a"+i; o.textContent = "⚓ "+a.name;
sel.appendChild(o);
});
sel.value = normWordSource(card);
if(sel.selectedIndex < 0) sel.selectedIndex = 0;
}
/* emoji palette (grouped) */
const splitEmoji = s => {
const out = [];
for(const ch of s){
if(out.length && (ch==="" || ch==="")) out[out.length-1]+=ch;
else if(out.length && out[out.length-1].endsWith("")) out[out.length-1]+=ch;
else out.push(ch);
}
return out;
};
const EMOJI_CATS = [
splitEmoji("🐵🐶🐱🐭🐹🐰🦊🐻🐼🐨🐯🦁🐮🐷🐸🐔🐧🐦🦆🦉🐴🦄🐝🐛🦋🐌🐞🐠🐟🐬🐳🐢🐍🦎🐘🦒"),
splitEmoji("🍎🍐🍊🍋🍌🍉🍇🍓🍒🍑🥝🍅🥕🌽🥔🧀🥚🍞🥐🥞🍕🍟🍦🍪🎂🍫🍬🥛🧃🍯"),
splitEmoji("🌳🌲🌴🌵🌷🌹🌻🌸💐🍄⭐🌙☀️⛅🌈❄️🔥💧🌊🪨"),
splitEmoji("🚗🚕🚌🚑🚒🚜🚲🛴🏍️🚂✈️🚁⛵🚀🛶🚢"),
splitEmoji("⚽🏀🎈🎁🧸📕✏️🖍️✂️🎨🥁🎸👓⌚⏰💡🔑🧤🧢👟👗🧥🛏️🪑🚪🏠"),
splitEmoji("✋👂👃👁️👄🦶😀😢😴🤧👶👦👧👵👴")
];