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);