teach/public/js/data.js
Ramon f2243c13c4
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 18s
v0.3.21-beta: groepeer woordthema's in mappen
- Thema's krijgen een optioneel folder-veld; de theme-dropdown in de
  woorden-editor en de kaart-dropdowns in de widgets (fillWordSourceSel)
  groeperen per map met optgroups, mapl0ze thema's bovenaan
- Nieuw 📁-knopje in de editor zet het gekozen thema in een map (leeg veld
  haalt het eruit); zichtbaarheid volgt de themakeuze
- Ankerthema's komen automatisch in de vaste map "" (id-koppeling met de
  ankerwidget blijft onaangeroerd)
- Nieuwe i18n-teksten themeFolder/themeFolderPh (nl/en)
2026-07-14 16:35:22 +02:00

155 lines
6.7 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;
}
/* built-in general words: ONE card per language (extendable via the editor) */
const WORDS = {
nl:[
["aap","🐵"],["vis","🐟"],["zon","☀️"],["kat","🐈"],["bal","⚽"],["ei","🥚"],
["boom","🌳"],["huis","🏠"],["maan","🌙"],["boot","⛵"],["kip","🐔"],["muis","🐭"],
["boek","📕"],["ster","⭐"],["roos","🌹"],["vuur","🔥"],["fiets","🚲"],["taart","🎂"],
["jas","🧥"],["bril","👓"],["klok","⏰"],["peer","🍐"],["kaas","🧀"],["eend","🦆"],
["hand","✋"],["voet","🦶"],["neus","👃"],["oog","👁️"],["oor","👂"],["mond","👄"]
],
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","🥛"]
]
};
/* 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:[]};
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" && (p.startsWith("data:") || p.startsWith("http"));
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("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("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("cardGeneral");
sel.appendChild(og);
/* 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("✋👂👃👁️👄🦶😀😢😴🤧👶👦👧👵👴")
];