diff --git a/VERSION b/VERSION
index 139b21a..a224768 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.3.22-beta
+0.3.23-beta
diff --git a/public/js/core.js b/public/js/core.js
index 67a720a..503e16d 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.22-beta";
+const VERSION = "0.3.23-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@@ -53,7 +53,8 @@ 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)…",
+ themeFolder:"Thema's en mappen",
+ exFixedFolder:"Deze map wordt automatisch beheerd.",
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",
@@ -225,7 +226,8 @@ 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)…",
+ themeFolder:"Themes and folders",
+ exFixedFolder:"This folder is managed automatically.",
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/explorer.js b/public/js/explorer.js
index a4d4fbe..b73f668 100644
--- a/public/js/explorer.js
+++ b/public/js/explorer.js
@@ -244,7 +244,7 @@
t.appendChild(c);
t.addEventListener("click", ()=>{ SEL = {type:"folder", key:f.name}; markSel(t); });
t.addEventListener("dblclick", ()=>{ PATH = [...PATH, f.name]; SEL = null; render(); msg(""); });
- makeDraggable(t, {type:"folder", key:f.name});
+ makeDraggable(t, {type:"folder", key:f.name, movable: f.fixed !== true});
gridEl.appendChild(t);
});
items.forEach(it=>{
diff --git a/public/js/widgets/letters.js b/public/js/widgets/letters.js
index ff0df00..3c83032 100644
--- a/public/js/widgets/letters.js
+++ b/public/js/widgets/letters.js
@@ -39,8 +39,7 @@ function mountLetterGame(root, initState, opts){
-
-
+
@@ -303,7 +302,7 @@ function mountLetterGame(root, initState, opts){
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"),
- edThemeMv = el(".ed-theme-mv"), edThemeFolder = el(".ed-theme-folder");
+ edThemeMv = el(".ed-theme-mv");
let edPic = "", edTab = "emoji", edCat = 0, edTheme = "g";
/* ---- word target: the general card or one of the themes ---- */
@@ -340,12 +339,9 @@ function mountLetterGame(root, initState, opts){
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", ()=>{
@@ -381,34 +377,129 @@ 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;
+ /* 🗂 opent de mappenverkenner voor thema's: zelfde Windows-stijl verkenner
+ als voor de borden (explorer.js), met de thema's van de actieve taal.
+ Nesting zit in het folder-veld als pad met "/"-scheiding. Ankerthema's
+ (id "an-...") staan vast in de map "⚓" - anchor.js zet die toch terug. */
+ function themesExplorerAdapter(){
+ const split = n => (n||"").split("/").map(s=>s.trim()).filter(Boolean);
+ const join = p => p.join("/");
+ const themes = ()=>THEMES[S.lang];
+ const isAnchor = th => typeof th.id === "string" && th.id.startsWith("an-");
+ /* mappen zijn afgeleid van themavelden; nieuwe lege mappen bestaan
+ sessie-lokaal tot er een thema in wordt gezet */
+ const pending = new Set();
+ const changed = ()=>{
persistWords();
- renderThemeSel();
document.dispatchEvent(new CustomEvent("wordschange"));
- }
- edThemeFolder.style.display = "none";
- setTimeout(()=>{ folderDone = false; }, 100);
+ };
+ return {
+ title: ()=>T("themeFolder"),
+ allowRootItems: true,
+ newItemLabel: ()=>T("newTheme"),
+ list(path){
+ const p = join(path);
+ const folderMap = new Map();
+ const items = [];
+ const seen = new Set(pending);
+ themes().forEach((th, i)=>{
+ const j = join(split(th.folder));
+ if(j === p){
+ items.push({
+ key: i, name: `${th.name} (${th.words.length})`,
+ icon: isAnchor(th) ? "⚓" : "★",
+ movable: !isAnchor(th), deletable: !isAnchor(th),
+ active: edTheme === i,
+ });
+ }else if(!p || j.startsWith(p + "/")){
+ seen.add(j);
+ }
+ });
+ seen.forEach(j=>{
+ if(!(j === p || (!p && j) || j.startsWith(p + "/"))) return;
+ if(j === p) return;
+ const seg = (p ? j.slice(p.length + 1) : j).split("/")[0];
+ if(!seg) return;
+ const under = themes().filter(t=>{ const tj = join(split(t.folder)); const full = p ? p+"/"+seg : seg; return tj === full || tj.startsWith(full + "/"); }).length;
+ folderMap.set(seg, under);
+ });
+ const folders = [...folderMap].map(([name, count])=>({name, count, fixed: name === "⚓" && !p}))
+ .sort((a,b)=>a.name.localeCompare(b.name));
+ return { folders, items };
+ },
+ createFolder(path, name){
+ const full = join([...path, name]);
+ if(this.list(path).folders.some(f=>f.name===name)) return T("exExists");
+ pending.add(full);
+ return null;
+ },
+ renameFolder(path, oldName, newName){
+ return this.moveFolder([...path, oldName], path, newName);
+ },
+ moveFolder(srcPath, targetPath, newName){
+ const from = join(srcPath);
+ if(from === "⚓") return T("exFixedFolder");
+ const to = join([...targetPath, newName || srcPath[srcPath.length-1]]);
+ if(from === to) return null;
+ if(themes().some(t=>{ const j = join(split(t.folder)); return j === to || j.startsWith(to + "/"); }) || pending.has(to)) return T("exExists");
+ themes().forEach(t=>{
+ const j = join(split(t.folder));
+ if(j === from || j.startsWith(from + "/")) t.folder = to + j.slice(from.length);
+ });
+ [...pending].forEach(j=>{
+ if(j === from || j.startsWith(from + "/")){ pending.delete(j); pending.add(to + j.slice(from.length)); }
+ });
+ changed();
+ return null;
+ },
+ deleteFolder(path, name){
+ const full = join([...path, name]);
+ if(full === "⚓") return T("exFixedFolder");
+ if(themes().some(t=>{ const j = join(split(t.folder)); return j === full || j.startsWith(full + "/"); })) return T("exFolderNotEmpty");
+ [...pending].forEach(j=>{ if(j === full || j.startsWith(full + "/")) pending.delete(j); });
+ return null;
+ },
+ createItem(path){
+ const nm = (S.lang==="nl" ? "Thema " : "Theme ") + (themes().length + 1);
+ const th = { name: nm, words: [] };
+ if(path.length) th.folder = join(path);
+ themes().push(th);
+ pending.delete(join(path));
+ changed();
+ return null;
+ },
+ renameItem(i, name){
+ const th = themes()[i];
+ if(!th || isAnchor(th)) return isAnchor(th) ? T("exFixedFolder") : null;
+ th.name = name;
+ changed();
+ return null;
+ },
+ moveItem(i, targetPath){
+ const th = themes()[i];
+ if(!th || isAnchor(th)) return th ? T("exFixedFolder") : null;
+ if(targetPath.length) th.folder = join(targetPath); else delete th.folder;
+ pending.delete(join(targetPath));
+ changed();
+ return null;
+ },
+ deleteItem(i){
+ const th = themes()[i];
+ if(!th || isAnchor(th)) return th ? T("exFixedFolder") : null;
+ themes().splice(i, 1);
+ if(edTheme === i) edTheme = "g";
+ else if(edTheme !== "g" && edTheme > i) edTheme--;
+ changed();
+ return null;
+ },
+ openItem(i){
+ if(themes()[i]) edTheme = i;
+ },
+ };
}
- 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); }
+ edThemeMv.addEventListener("click", ()=>{
+ openExplorer(themesExplorerAdapter(), ()=>{ renderThemeSel(); renderEdList(); });
});
- edThemeFolder.addEventListener("blur", commitFolder);
function selectPic(p, btn){
edPic = p; setPic(edPrev, p);