v0.3.21-beta: groepeer woordthema's in mappen
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 18s

- 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)
This commit is contained in:
Ramon 2026-07-14 16:35:22 +02:00
parent 18775ae0ec
commit f2243c13c4
6 changed files with 81 additions and 8 deletions

View file

@ -1 +1 @@
0.3.20-beta 0.3.21-beta

View file

@ -582,7 +582,7 @@
flex:1; min-width:0; border:1px solid var(--line); border-radius:10px; padding:9px 10px; flex:1; min-width:0; border:1px solid var(--line); border-radius:10px; padding:9px 10px;
font:inherit; font-weight:700; font-size:15px; outline:none; background:var(--surface); cursor:pointer; font:inherit; font-weight:700; font-size:15px; outline:none; background:var(--surface); cursor:pointer;
} }
.ed-theme-name{ .ed-theme-name, .ed-theme-folder{
border:2px solid var(--blue); border-radius:10px; padding:9px 10px; border:2px solid var(--blue); border-radius:10px; padding:9px 10px;
font:inherit; font-size:15px; outline:none; width:180px; font:inherit; font-size:15px; outline:none; width:180px;
} }
@ -591,6 +591,10 @@
width:40px; height:40px; cursor:pointer; font-size:15px; flex:none; width:40px; height:40px; cursor:pointer; font-size:15px; flex:none;
} }
.ed-theme-del:hover{background:var(--red); color:#fff;} .ed-theme-del:hover{background:var(--red); color:#fff;}
.ed-theme-mv{
border:none; background:var(--surface-2, #eef); color:var(--ink); border-radius:10px;
width:40px; height:40px; cursor:pointer; font-size:15px; flex:none;
}
.lg-progress{margin-left:auto; font-size:13.5px; font-weight:700; color:var(--muted); display:flex; align-items:center; gap:8px; white-space:nowrap;} .lg-progress{margin-left:auto; font-size:13.5px; font-weight:700; color:var(--muted); display:flex; align-items:center; gap:8px; white-space:nowrap;}
.lg-stars{color:#d9a400; font-size:14px; min-width:34px; text-align:right;} .lg-stars{color:#d9a400; font-size:14px; min-width:34px; text-align:right;}
.lg-instr{ .lg-instr{

View file

@ -2,7 +2,7 @@
"use strict"; "use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline). /* version shown until /api/version resolves (or if the fetch fails, e.g. offline).
Kept in sync by hand with the VERSION file at the repo root on every release. */ Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.3.20-beta"; const VERSION = "0.3.21-beta";
(function(){ (function(){
const tag = document.getElementById("verTag"); const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION; tag.textContent = "v"+VERSION;
@ -53,6 +53,7 @@ const I18N = {
myWords:"Mijn woorden", manage:"Thema's en woorden beheren", addWord:" Toevoegen", myWords:"Mijn woorden", manage:"Thema's en woorden beheren", addWord:" Toevoegen",
pickPic:"Kies een plaatje", pickPic:"Kies een plaatje",
newTheme:"Nieuw thema", themeNamePh:"naam van het thema…", delTheme:"Thema verwijderen", newTheme:"Nieuw thema", themeNamePh:"naam van het thema…", delTheme:"Thema verwijderen",
themeFolder:"Thema in een map zetten", themeFolderPh:"mapnaam (leeg = geen map)…",
numWords:"Woorden per spel", all:"Alle", noThemes:"— nog geen thema's —", numWords:"Woorden per spel", all:"Alle", noThemes:"— nog geen thema's —",
muteAll:"Alle geluiden aan/uit", sndWidget:"Geluid van deze widget aan/uit", muteAll:"Alle geluiden aan/uit", sndWidget:"Geluid van deze widget aan/uit",
catTaal:"Taal", catRekenen:"Rekenen", catClass:"Feest & klas", catTools:"Hulpmiddelen", catMedia:"Media", catTaal:"Taal", catRekenen:"Rekenen", catClass:"Feest & klas", catTools:"Hulpmiddelen", catMedia:"Media",
@ -220,6 +221,7 @@ const I18N = {
myWords:"My words", manage:"Manage themes and words", addWord:" Add", myWords:"My words", manage:"Manage themes and words", addWord:" Add",
pickPic:"Choose a picture", pickPic:"Choose a picture",
newTheme:"New theme", themeNamePh:"name of the theme…", delTheme:"Delete theme", newTheme:"New theme", themeNamePh:"name of the theme…", delTheme:"Delete theme",
themeFolder:"Put theme in a folder", themeFolderPh:"folder name (empty = no folder)…",
numWords:"Words per game", all:"All", noThemes:"— no themes yet —", numWords:"Words per game", all:"All", noThemes:"— no themes yet —",
muteAll:"All sounds on/off", sndWidget:"This widget's sound on/off", muteAll:"All sounds on/off", sndWidget:"This widget's sound on/off",
catTaal:"Language", catRekenen:"Maths", catClass:"Celebration & class", catTools:"Tools", catMedia:"Media", catTaal:"Language", catRekenen:"Maths", catClass:"Celebration & class", catTools:"Tools", catMedia:"Media",

View file

@ -104,11 +104,26 @@ function fillWordSourceSel(sel, card){
const og = document.createElement("option"); const og = document.createElement("option");
og.value = "g"; og.textContent = T("cardGeneral"); og.value = "g"; og.textContent = T("cardGeneral");
sel.appendChild(og); 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)=>{ THEMES[LANG].forEach((th,i)=>{
if(!th.words.length) return; 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"); const o = document.createElement("option");
o.value = "t"+i; o.textContent = "★ "+th.name; o.value = "t"+i; o.textContent = "★ "+th.name;
sel.appendChild(o); 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)=>{ anchorSources().forEach((a,i)=>{
const o = document.createElement("option"); const o = document.createElement("option");

View file

@ -65,8 +65,10 @@ function mountAnchor(root, initState, opts){
return; return;
} }
const name = "⚓ " + reg.name; const name = "⚓ " + reg.name;
if(idx < 0) list.push({ id: S.themeId, name, words: withPic.slice() }); /* vaste map "" zodat ankerthema's gegroepeerd staan in de editor- en
else{ list[idx].name = name; list[idx].words = withPic.slice(); } widget-dropdowns (zie de optgroup-weergave in letters.js/data.js) */
if(idx < 0) list.push({ id: S.themeId, name, folder: "⚓", words: withPic.slice() });
else{ list[idx].name = name; list[idx].folder = "⚓"; list[idx].words = withPic.slice(); }
} }
function syncSource(quiet){ function syncSource(quiet){
const center = S.cells["0,0"]; const center = S.cells["0,0"];

View file

@ -39,6 +39,8 @@ function mountLetterGame(root, initState, opts){
<select class="ed-theme-sel"></select> <select class="ed-theme-sel"></select>
<button class="tbtn ed-theme-add"></button> <button class="tbtn ed-theme-add"></button>
<input type="text" class="ed-theme-name" maxlength="16" style="display:none;"> <input type="text" class="ed-theme-name" maxlength="16" style="display:none;">
<button class="ed-theme-mv">📁</button>
<input type="text" class="ed-theme-folder" maxlength="16" style="display:none;">
<button class="ed-theme-del">🗑</button> <button class="ed-theme-del">🗑</button>
</div> </div>
<div class="lg-ed-form"> <div class="lg-ed-form">
@ -300,7 +302,8 @@ function mountLetterGame(root, initState, opts){
edFile = el(".ed-p-upload input"), edPSearch = el(".ed-psearch"), edFile = el(".ed-p-upload input"), edPSearch = el(".ed-psearch"),
edPGrid = el(".ed-pgrid"), edStatus = el(".ed-status"), edPGrid = el(".ed-pgrid"), edStatus = el(".ed-status"),
edThemeSel = el(".ed-theme-sel"), edThemeAdd = el(".ed-theme-add"), edThemeSel = el(".ed-theme-sel"), edThemeAdd = el(".ed-theme-add"),
edThemeName = el(".ed-theme-name"), edThemeDel = el(".ed-theme-del"); edThemeName = el(".ed-theme-name"), edThemeDel = el(".ed-theme-del"),
edThemeMv = el(".ed-theme-mv"), edThemeFolder = el(".ed-theme-folder");
let edPic = "", edTab = "emoji", edCat = 0, edTheme = "g"; let edPic = "", edTab = "emoji", edCat = 0, edTheme = "g";
/* ---- word target: the general card or one of the themes ---- */ /* ---- word target: the general card or one of the themes ---- */
@ -312,18 +315,37 @@ function mountLetterGame(root, initState, opts){
og.value = "g"; og.value = "g";
og.textContent = `${T("cardGeneral")} (${generalList().length})`; og.textContent = `${T("cardGeneral")} (${generalList().length})`;
edThemeSel.appendChild(og); edThemeSel.appendChild(og);
/* thema's zonder map bovenaan, daarna per map gegroepeerd - zelfde
indeling als fillWordSourceSel (data.js) in de widget-dropdowns */
const groups = new Map();
list.forEach((th,i)=>{ list.forEach((th,i)=>{
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"); const o = document.createElement("option");
o.value = i; o.textContent = `${th.name} (${th.words.length})`; o.value = i; o.textContent = `${th.name} (${th.words.length})`;
edThemeSel.appendChild(o); parent.appendChild(o);
};
(groups.get("") || []).forEach(([th,i])=>addOpt(edThemeSel, 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));
edThemeSel.appendChild(grp);
}); });
edThemeSel.value = edTheme; edThemeSel.value = edTheme;
edThemeAdd.title = T("newTheme"); edThemeAdd.title = T("newTheme");
edThemeDel.title = T("delTheme"); edThemeDel.title = T("delTheme");
edThemeMv.title = T("themeFolder");
edThemeName.placeholder = T("themeNamePh"); edThemeName.placeholder = T("themeNamePh");
edThemeFolder.placeholder = T("themeFolderPh");
edThemeMv.style.display = edTheme === "g" ? "none" : "";
} }
edThemeSel.addEventListener("change", ()=>{ edThemeSel.addEventListener("change", ()=>{
edTheme = edThemeSel.value==="g" ? "g" : +edThemeSel.value; edTheme = edThemeSel.value==="g" ? "g" : +edThemeSel.value;
edThemeMv.style.display = edTheme === "g" ? "none" : "";
renderEdList(); renderEdList();
}); });
edThemeAdd.addEventListener("click", ()=>{ edThemeAdd.addEventListener("click", ()=>{
@ -359,6 +381,34 @@ function mountLetterGame(root, initState, opts){
renderThemeSel(); renderEdList(); renderThemeSel(); renderEdList();
document.dispatchEvent(new CustomEvent("wordschange")); document.dispatchEvent(new CustomEvent("wordschange"));
}); });
/* thema in een map zetten (of eruit halen met een leeg veld): 📁 toont een
inline invoer, voorgevuld met de huidige map van het gekozen thema */
edThemeMv.addEventListener("click", ()=>{
if(edTheme==="g") return;
edThemeFolder.style.display = "block";
edThemeFolder.value = THEMES[S.lang][edTheme].folder || "";
edThemeFolder.focus();
});
let folderDone = false;
function commitFolder(){
if(folderDone) return; folderDone = true;
if(edTheme !== "g" && THEMES[S.lang][edTheme]){
const f = edThemeFolder.value.trim();
const th = THEMES[S.lang][edTheme];
if(f) th.folder = f; else delete th.folder;
persistWords();
renderThemeSel();
document.dispatchEvent(new CustomEvent("wordschange"));
}
edThemeFolder.style.display = "none";
setTimeout(()=>{ folderDone = false; }, 100);
}
edThemeFolder.addEventListener("keydown", ev=>{
ev.stopPropagation();
if(ev.key==="Enter") commitFolder();
if(ev.key==="Escape"){ folderDone=true; edThemeFolder.style.display="none"; setTimeout(()=>{folderDone=false;},100); }
});
edThemeFolder.addEventListener("blur", commitFolder);
function selectPic(p, btn){ function selectPic(p, btn){
edPic = p; setPic(edPrev, p); edPic = p; setPic(edPrev, p);