teach/public/js/data.js
Ramon cb97dc2bb1
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 31s
feat: breid taalwidgets en plaatjesbibliotheek uit (v0.3.80-beta)
2026-07-16 16:27:38 +02:00

377 lines
17 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],
/* digits use the braille number patterns (aj) */
"1":[1], "2":[1,2], "3":[1,4], "4":[1,4,5], "5":[1,5],
"6":[1,2,4], "7":[1,2,4,5], "8":[1,2,5], "9":[2,4], "0":[2,4,5]
};
const VOWELS = "aeiou";
function dotGrid(letter){
const code = 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("✋👂👃👁️👄🦶😀😢😴🤧👶👦👧👵👴")
];