From f2243c13c491467f9d109166deed1ac594c310fe Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 14 Jul 2026 16:35:22 +0200 Subject: [PATCH] v0.3.21-beta: groepeer woordthema's in mappen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- VERSION | 2 +- public/css/teach.css | 6 +++- public/js/core.js | 4 ++- public/js/data.js | 17 +++++++++++- public/js/widgets/anchor.js | 6 ++-- public/js/widgets/letters.js | 54 ++++++++++++++++++++++++++++++++++-- 6 files changed, 81 insertions(+), 8 deletions(-) diff --git a/VERSION b/VERSION index 8d2cd64..9742761 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.20-beta +0.3.21-beta diff --git a/public/css/teach.css b/public/css/teach.css index 782523e..42068c1 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -582,7 +582,7 @@ 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; } - .ed-theme-name{ + .ed-theme-name, .ed-theme-folder{ border:2px solid var(--blue); border-radius:10px; padding:9px 10px; font:inherit; font-size:15px; outline:none; width:180px; } @@ -591,6 +591,10 @@ width:40px; height:40px; cursor:pointer; font-size:15px; flex:none; } .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-stars{color:#d9a400; font-size:14px; min-width:34px; text-align:right;} .lg-instr{ diff --git a/public/js/core.js b/public/js/core.js index fff38bb..98ee408 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -2,7 +2,7 @@ "use strict"; /* 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. */ -const VERSION = "0.3.20-beta"; +const VERSION = "0.3.21-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; @@ -53,6 +53,7 @@ const I18N = { myWords:"Mijn woorden", manage:"Thema's en woorden beheren", addWord:"οΌ‹ Toevoegen", pickPic:"Kies een plaatje", 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 β€”", muteAll:"Alle geluiden aan/uit", sndWidget:"Geluid van deze widget aan/uit", 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", pickPic:"Choose a picture", 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 β€”", muteAll:"All sounds on/off", sndWidget:"This widget's sound on/off", catTaal:"Language", catRekenen:"Maths", catClass:"Celebration & class", catTools:"Tools", catMedia:"Media", diff --git a/public/js/data.js b/public/js/data.js index 1051b31..3211ea9 100644 --- a/public/js/data.js +++ b/public/js/data.js @@ -104,11 +104,26 @@ function fillWordSourceSel(sel, card){ 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; - 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)=>{ const o = document.createElement("option"); diff --git a/public/js/widgets/anchor.js b/public/js/widgets/anchor.js index d510427..63fe607 100644 --- a/public/js/widgets/anchor.js +++ b/public/js/widgets/anchor.js @@ -65,8 +65,10 @@ function mountAnchor(root, initState, opts){ return; } const name = "βš“ " + reg.name; - if(idx < 0) list.push({ id: S.themeId, name, words: withPic.slice() }); - else{ list[idx].name = name; list[idx].words = withPic.slice(); } + /* vaste map "βš“" zodat ankerthema's gegroepeerd staan in de editor- en + 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){ const center = S.cells["0,0"]; diff --git a/public/js/widgets/letters.js b/public/js/widgets/letters.js index 58621a1..ff0df00 100644 --- a/public/js/widgets/letters.js +++ b/public/js/widgets/letters.js @@ -39,6 +39,8 @@ function mountLetterGame(root, initState, opts){ + +
@@ -300,7 +302,8 @@ function mountLetterGame(root, initState, opts){ edFile = el(".ed-p-upload input"), edPSearch = el(".ed-psearch"), edPGrid = el(".ed-pgrid"), edStatus = el(".ed-status"), 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"; /* ---- word target: the general card or one of the themes ---- */ @@ -312,18 +315,37 @@ function mountLetterGame(root, initState, opts){ og.value = "g"; og.textContent = `${T("cardGeneral")} (${generalList().length})`; 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)=>{ + 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 = 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; edThemeAdd.title = T("newTheme"); edThemeDel.title = T("delTheme"); + edThemeMv.title = T("themeFolder"); edThemeName.placeholder = T("themeNamePh"); + edThemeFolder.placeholder = T("themeFolderPh"); + edThemeMv.style.display = edTheme === "g" ? "none" : ""; } edThemeSel.addEventListener("change", ()=>{ edTheme = edThemeSel.value==="g" ? "g" : +edThemeSel.value; + edThemeMv.style.display = edTheme === "g" ? "none" : ""; renderEdList(); }); edThemeAdd.addEventListener("click", ()=>{ @@ -359,6 +381,34 @@ function mountLetterGame(root, initState, opts){ renderThemeSel(); renderEdList(); 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){ edPic = p; setPic(edPrev, p);