v0.3.23-beta: beheer woordthema's in dezelfde mappenverkenner
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 33s
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 33s
- 🗂-knop in de woorden-editor opent de Windows-stijl verkenner met de thema's van de actieve taal (★-tegels met woordaantal); dubbelklik selecteert het thema in de editor - Geneste mappen via het folder-veld als pad; nieuwe lege mappen bestaan sessie-lokaal tot er een thema in wordt gezet (mappen zijn afgeleid van de themavelden) - Ankerthema's staan vast in de map "⚓": niet versleepbaar, hernoemen/ verwijderen geweigerd (anchor.js beheert ze) - Het 📁-knopje + inline map-invoer uit v0.3.21 zijn vervangen door de verkenner; de optgroup-weergave in de dropdowns blijft en toont het pad
This commit is contained in:
parent
1151c71049
commit
e327049f1b
4 changed files with 128 additions and 35 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.22-beta
|
||||
0.3.23-beta
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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=>{
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ function mountLetterGame(root, initState, opts){
|
|||
<select class="ed-theme-sel"></select>
|
||||
<button class="tbtn ed-theme-add">+</button>
|
||||
<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-mv">🗂</button>
|
||||
<button class="ed-theme-del">🗑</button>
|
||||
</div>
|
||||
<div class="lg-ed-form">
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue