Verbeter grafische kwaliteit van kleurplaten (v0.4.67-beta) #5

Open
bes-r wants to merge 208 commits from bes-r/coloring-quality-v0.4.67 into main AGit
6 changed files with 476 additions and 217 deletions
Showing only changes of commit 1151c71049 - Show all commits

View file

@ -1 +1 @@
0.3.21-beta
0.3.22-beta

View file

@ -131,7 +131,7 @@
heeft toegewezen, als vaste (niet-sleepbare) kaarten in speelmodus */
#pupilView{display:none;}
body.pupil-mode #board,body.pupil-mode #fab,body.pupil-mode #fabBoardsBtn,
body.pupil-mode #boardStrip,body.pupil-mode #boardsPanel,body.pupil-mode #fabmenu,
body.pupil-mode #boardStrip,body.pupil-mode #fabmenu,
body.pupil-mode #hud,body.pupil-mode #btnFolders,body.pupil-mode #btnSave{display:none !important;}
body.pupil-mode #pupilView{
display:flex; flex-wrap:wrap; gap:18px; align-content:flex-start;
@ -209,41 +209,40 @@
color:#fff; background:var(--green); border-color:var(--green);
width:90px; min-height:130px; font-weight:900;
}
/* folder management panel (save menu) */
#boardsPanel{
position:fixed; right:16px; top:64px; z-index:5600; width:300px; max-height:66vh;
overflow:auto; display:none; flex-direction:column; gap:8px; padding:12px;
border-radius:16px; background:rgba(255,255,255,.96); backdrop-filter:blur(12px);
box-shadow:0 10px 30px rgba(30,50,90,.3); border:1px solid rgba(255,255,255,.7);
/* mappenverkenner (Windows-stijl, vervangt het oude mappenpaneel) */
.modal.explorer{width:min(860px,94vw);}
.ex-bar{display:flex; gap:10px; align-items:center; margin:10px 0 4px; flex-wrap:wrap;}
.ex-crumbs{display:flex; gap:4px; align-items:center; flex:1; min-width:0; flex-wrap:wrap;}
.ex-crumb{
border:none; background:var(--surface-2); border-radius:8px; padding:6px 10px;
cursor:pointer; font:inherit; font-weight:700; font-size:13.5px;
}
#boardsPanel.open{display:flex;}
.bp-folder{display:flex; align-items:center; gap:6px; font-weight:800; font-size:14px; margin-top:4px;}
.bp-folder .bpf-name{flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.bp-folder button{
border:none; background:var(--surface-2); border-radius:8px; padding:4px 8px; font:inherit;
font-weight:800; font-size:11.5px; cursor:pointer; color:var(--muted); flex:none;
.ex-crumb:hover{background:var(--surface-3);}
.ex-sep{color:var(--muted); font-weight:800;}
.ex-tools{display:flex; gap:8px; flex-wrap:wrap;}
.ex-msg{min-height:18px; color:var(--red); font-size:13px; font-weight:700;}
.ex-grid{
display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr));
gap:12px; margin-top:8px;
}
.bp-folder button:hover{background:var(--surface-3); color:var(--ink);}
.bp-boards{display:flex; flex-wrap:wrap; gap:6px;}
.bp-board{
display:flex; align-items:center; gap:5px; border:1px solid var(--line); background:var(--surface);
border-radius:10px; padding:5px 10px; font:inherit; font-weight:700; font-size:12.5px;
cursor:pointer; color:var(--muted);
.ex-tile{
border:1.5px solid var(--line); border-radius:14px; padding:10px; background:var(--surface);
display:flex; flex-direction:column; align-items:center; gap:6px; cursor:pointer;
user-select:none; touch-action:none; min-height:110px; justify-content:center;
}
.bp-board.active{border-color:var(--accent); background:var(--accent-soft); color:var(--accent-ink);}
.bp-board .bpb-x{border:none; background:transparent; color:var(--red); font-weight:900;
cursor:pointer; padding:0; font-size:11px; opacity:.8;}
.bp-board:hover .bpb-x{opacity:1;}
.bp-board .bpb-mv{color:var(--muted); font-weight:400;}
.bp-board .bpb-mv-sel{font:inherit; max-width:150px;}
.bp-board input{border:none; outline:none; font:inherit; width:80px; background:transparent;}
.bp-folder input{border:none; outline:none; font:inherit; width:110px; background:transparent; border-bottom:1px solid var(--line);}
.bp-new{display:flex; gap:6px; margin-top:6px;}
.bp-new input{
flex:1; min-width:0; border:1px solid var(--line); border-radius:10px; padding:7px 9px;
font:inherit; font-size:13px; outline:none;
.ex-tile:hover{border-color:var(--blue);}
.ex-tile.sel{border-color:var(--blue); background:var(--accent-soft);}
.ex-tile.active-item{border-color:var(--accent); border-width:2.5px;}
.ex-tile .ex-ico{font-size:36px; line-height:1;}
.ex-tile canvas{max-width:100%; border-radius:8px; border:1px solid var(--line);}
.ex-name{font-weight:700; font-size:13.5px; text-align:center; word-break:break-word;}
.ex-count{font-size:12px; color:var(--muted); font-weight:700;}
.ex-tile input{
border:2px solid var(--blue); border-radius:8px; font:inherit; font-size:13.5px;
width:100%; outline:none; padding:4px 6px; text-align:center;
}
.bp-hint{font-size:11px; color:var(--muted); font-weight:700;}
.ex-ghost{position:fixed; z-index:7000; opacity:.75; pointer-events:none; width:150px; left:-999px;}
.ex-drop{outline:3px solid var(--blue); outline-offset:2px;}
#fabmenu.open{display:flex;}
.fab-item{
display:flex; align-items:center; gap:10px; border:none;
@ -922,7 +921,7 @@
body.hc .lg-ed-form input[type=text],body.hc .ed-pictorow input{border-color:#000; color:#000;}
body.hc .np-result{background:#0047ab;}
body.hc .np-row label,body.hc .thint,body.hc .guestnote,body.hc .ed-credit,
body.hc .bp-hint,body.hc .gcard .gd,body.hc .st-footer{color:#222;}
body.hc .gcard .gd,body.hc .st-footer{color:#222;}
body.hc .gcard{border-color:#000; background:var(--surface);}
body.hc .gcat-title{color:#000;}
body.hc .tbtn.ghost{background:var(--surface); border:2px solid #000; color:#000;}
@ -935,8 +934,6 @@
body.hc .timer .presets button,body.hc .timer .main button,body.hc .ticon,
body.hc .hud-btn,body.hc .mm-seg button{background:var(--surface); border:1.5px solid #000; color:#000;}
body.hc .timer .main .bstart{background:#006b2d; border-color:#006b2d; color:#fff;}
body.hc .bp-board{border-color:#000; color:#000;}
body.hc .bp-board.active{background:#ffd400;}
body.hc .dock-item{color:#000;}
body.hc .media-msg{color:#ddd;}
/* =========== Flash words widget =========== */
@ -1182,9 +1179,6 @@ body.hc .am-reset,body.hc .am-reset-row{border:1.5px solid #000; background:var(
/* ---------- tablet (≤1024px) ---------- */
@media (max-width:1024px){
/* #boardsPanel had een vaste 300px-breedte die op smallere
schermen overflowt; #boardStrip deed dit al goed. */
#boardsPanel{width:min(300px, calc(100vw - 32px));}
}
/* ---------- telefoon (≤600px) ---------- */
@ -1193,7 +1187,6 @@ body.hc .am-reset,body.hc .am-reset-row{border:1.5px solid #000; background:var(
/* alleen de uil op kleine schermen, woordmerk verdwijnt */
.logo-word{display:none;}
.tbtn{padding:0 10px; font-size:13px;}
#boardsPanel{top:calc(var(--tbar) + 2px); max-height:min(66vh,480px);}
/* beheerpaneel: dichte rijen worden gestapeld i.p.v. verdrukt */
.am-row{flex-direction:column; align-items:stretch;}
@ -1240,8 +1233,7 @@ body.tb-left #board,body.tb-left #pupilView{inset:0 0 0 var(--tbar);}
body.tb-right #board,body.tb-right #pupilView{inset:0 var(--tbar) 0 0;}
body.tb-right #fab{right:calc(var(--tbar) + 16px);}
body.tb-right #fabBoardsBtn{right:calc(var(--tbar) + 88px);}
body.tb-right #boardStrip,body.tb-right #boardsPanel{right:calc(var(--tbar) + 12px);}
body.tb-left #boardsPanel,body.tb-right #boardsPanel{top:12px;}
body.tb-right #boardStrip{right:calc(var(--tbar) + 12px);}
body.tb-left #fabmenu{right:24px;}
body.tb-right #fabmenu{right:calc(var(--tbar) + 16px);}
@ -1250,7 +1242,7 @@ body.tb-right #fabmenu{right:calc(var(--tbar) + 16px);}
i.p.v. blur - rustiger en lichter voor oudere digiborden.
============================================================ */
body.no-glass .widget,body.no-glass #hud,body.no-glass .fab-item,
body.no-glass #boardStrip,body.no-glass #boardsPanel,body.no-glass .mm-top,
body.no-glass #boardStrip,body.no-glass .mm-top,
body.no-glass .widget.bare .widget-head{
backdrop-filter:none; -webkit-backdrop-filter:none;
background:var(--surface);
@ -1277,7 +1269,7 @@ body.dark .widget-head{background:rgba(255,255,255,.04); border-bottom-color:rgb
body.dark #hud{background:var(--glass); border-color:var(--glass-line);}
body.dark .fab-item{background:var(--glass-strong); color:var(--ink);}
body.dark .fab-item:hover{background:var(--surface-3);}
body.dark #boardStrip,body.dark #boardsPanel{background:var(--glass-strong); border-color:var(--glass-line);}
body.dark #boardStrip{background:var(--glass-strong); border-color:var(--glass-line);}
body.dark .widget.bare .widget-head{background:var(--glass-strong);}
body.dark .mm-top{background:var(--glass-strong);}
body.dark .mm-pop{background:var(--surface); box-shadow:var(--shadow-2);}

View file

@ -31,7 +31,17 @@
<button id="fab"></button>
<button id="fabBoardsBtn">🗂</button>
<div id="boardStrip"></div>
<div id="boardsPanel"></div>
<div class="modalwrap" id="explorerWrap" role="dialog" aria-modal="true" aria-labelledby="explorerTitle">
<div class="modal explorer">
<div class="gallery-head"><h2 id="explorerTitle"></h2><button class="wclose explorer-close" type="button"></button></div>
<div class="ex-bar">
<div class="ex-crumbs" id="exCrumbs"></div>
<div class="ex-tools" id="exTools"></div>
</div>
<div class="ex-msg" id="exMsg"></div>
<div class="ex-grid" id="exGrid"></div>
</div>
</div>
<div id="fabmenu">
<button class="fab-item" data-f="widgets"><span class="fi">🧩</span><span class="fl"></span></button>
<button class="fab-item" data-f="draw"><span class="fi">✏️</span><span class="fl"></span></button>
@ -233,6 +243,7 @@
<script src="js/core.js"></script>
<script src="js/permissions.js"></script>
<script src="js/data.js"></script>
<script src="js/explorer.js"></script>
<script src="js/picker.js"></script>
<script src="js/widgets/letters.js"></script>
<script src="js/widgets/write.js"></script>

View file

@ -991,7 +991,14 @@ function boardsFromData(data){
}
function curSlot(){
if(BS.f >= BS.folders.length) BS.f = 0;
const fo = BS.folders[BS.f];
let fo = BS.folders[BS.f];
/* lege mappen bestaan sinds de verkenner: wijs dan uit naar een map mét
borden, of geef de actieve lege map alsnog een vers bord */
if(!fo.boards.length){
const fi = BS.folders.findIndex(f=>f.boards.length);
if(fi >= 0){ BS.f = fi; fo = BS.folders[BS.f]; }
else fo.boards.push(newBoardEntry(T("boardName")+" 1"));
}
if(BS.b >= fo.boards.length) BS.b = 0;
return fo.boards[BS.b];
}
@ -1007,23 +1014,143 @@ function switchTo(f, b){
BS.f = f; BS.b = b;
loadCurrentSlot();
}
const boardsPanel = document.getElementById("boardsPanel");
const btnFolders = document.getElementById("btnFolders");
btnFolders.addEventListener("click", ()=>{
renderBoardsPanel();
boardsPanel.classList.toggle("open");
});
document.addEventListener("pointerdown", e=>{
if(!boardsPanel.classList.contains("open")) return;
if(e.target.closest("#btnFolders") || e.target.closest("#boardsPanel")) return;
boardsPanel.classList.remove("open");
stashCurrent(); /* verse thumbnails, ook van het actieve bord */
openExplorer(boardsExplorerAdapter());
});
function updateBoardsUI(){
btnFolders.title = `${T("boardsNav")} · ${BS.folders[BS.f].name} / ${curSlot().name}`;
fabBoardsBtn.title = `${BS.folders[BS.f].name} / ${curSlot().name}`;
if(boardsPanel.classList.contains("open")) renderBoardsPanel();
if(boardStrip.classList.contains("open")) renderStrip();
}
/* ---- verkenner-adapter: virtuele boom over de platte BS.folders-lijst;
nesting zit in de mapnaam als pad met "/"-scheiding ("Rekenen/Groep 4"),
dus bestaande data en de server (findBoardById op bord-id) blijven werken */
const splitBoardPath = n => n.split("/").map(s=>s.trim()).filter(Boolean);
const joinBoardPath = p => p.join("/");
function boardsExplorerAdapter(){
const norm = f => joinBoardPath(splitBoardPath(f.name));
const entryAt = path => BS.folders.find(f=>norm(f)===joinBoardPath(path));
const findBoard = id => {
for(let fi=0; fi<BS.folders.length; fi++){
const bi = BS.folders[fi].boards.findIndex(b=>b.id===id);
if(bi >= 0) return { fi, bi, fo: BS.folders[fi], bo: BS.folders[fi].boards[bi] };
}
return null;
};
const activeId = ()=>curSlot().id;
/* indices herstellen nadat entries verschoven/verwijderd zijn */
const relocate = (id)=>{
const hit = findBoard(id);
if(hit){ BS.f = hit.fi; BS.b = hit.bi; }
};
const mutated = ()=>{ markBoardChange(); updateBoardsUI(); };
return {
title: ()=>T("boardsNav"),
allowRootItems: false,
newItemLabel: ()=>T("newBoard"),
list(path){
const p = joinBoardPath(path);
const folderMap = new Map();
let items = [];
BS.folders.forEach((f, fi)=>{
const j = norm(f);
if(j === p){
items = f.boards.map((bo, bi)=>({
key: bo.id, name: bo.name,
active: fi===BS.f && bi===BS.b,
tile: ()=>boardThumb((fi===BS.f && bi===BS.b) ? serializeBoard() : bo.data),
}));
}else if(!p || j.startsWith(p + "/")){
const seg = (p ? j.slice(p.length + 1) : j).split("/")[0];
if(seg) folderMap.set(seg, (folderMap.get(seg)||0) + f.boards.length);
}
});
const folders = [...folderMap].map(([name, count])=>({name, count}))
.sort((a,b)=>a.name.localeCompare(b.name));
return { folders, items };
},
createFolder(path, name){
const full = [...path, name];
if(entryAt(full) || this.list(path).folders.some(f=>f.name===name)) return T("exExists");
BS.folders.push({ name: joinBoardPath(full), boards: [] });
mutated();
return null;
},
renameFolder(path, oldName, newName){
return this.moveFolder([...path, oldName], path, newName);
},
moveFolder(srcPath, targetPath, newName){
const from = joinBoardPath(srcPath);
const to = joinBoardPath([...targetPath, newName || srcPath[srcPath.length-1]]);
if(from === to) return null;
if(BS.folders.some(f=>{ const j = norm(f); return j === to || j.startsWith(to + "/"); })) return T("exExists");
const id = activeId();
BS.folders.forEach(f=>{
const j = norm(f);
if(j === from || j.startsWith(from + "/")) f.name = to + j.slice(from.length);
});
relocate(id);
mutated();
return null;
},
deleteFolder(path, name){
const full = joinBoardPath([...path, name]);
const under = BS.folders.filter(f=>{ const j = norm(f); return j === full || j.startsWith(full + "/"); });
if(under.some(f=>f.boards.length)) return T("exFolderNotEmpty");
const id = activeId();
BS.folders = BS.folders.filter(f=>!under.includes(f));
if(!BS.folders.length) BS.folders.push({ name: T("folderName")+" 1", boards: [] });
relocate(id);
mutated();
return null;
},
createItem(path){
let entry = entryAt(path);
if(!entry){ entry = { name: joinBoardPath(path), boards: [] }; BS.folders.push(entry); }
entry.boards.push(newBoardEntry(`${T("boardName")} ${entry.boards.length+1}`));
mutated();
return null;
},
renameItem(id, name){
const hit = findBoard(id);
if(!hit) return null;
hit.bo.name = name;
mutated();
return null;
},
moveItem(id, targetPath){
const hit = findBoard(id);
if(!hit) return null;
let entry = entryAt(targetPath);
if(!entry){ entry = { name: joinBoardPath(targetPath), boards: [] }; BS.folders.push(entry); }
if(entry === hit.fo) return null;
const act = activeId();
hit.fo.boards.splice(hit.bi, 1);
entry.boards.push(hit.bo);
relocate(act);
mutated();
return null;
},
deleteItem(id){
const hit = findBoard(id);
if(!hit) return null;
if(flatBoards().length <= 1) return T("exLastBoard");
const wasActive = id === activeId();
hit.fo.boards.splice(hit.bi, 1);
if(wasActive){ BS.f = 0; BS.b = 0; loadCurrentSlot(); }
else relocate(activeId());
mutated();
return null;
},
openItem(id){
const hit = findBoard(id);
if(hit && !(hit.fi===BS.f && hit.bi===BS.b)) switchTo(hit.fi, hit.bi);
},
};
}
function flatBoards(){
const out = [];
BS.folders.forEach((fo,fi)=>fo.boards.forEach((bo,bi)=>out.push({fi,bi,bo,fo})));
@ -1135,162 +1262,5 @@ function renderStrip(){
});
boardStrip.appendChild(add);
}
function renderBoardsPanel(){
boardsPanel.innerHTML = "";
BS.folders.forEach((fo, fi)=>{
const fr = document.createElement("div");
fr.className = "bp-folder";
const nm = document.createElement("span");
nm.className = "bpf-name"; nm.textContent = "📁 " + fo.name;
/* map hernoemen: zelfde dblclick-inline-input-patroon als bord hernoemen */
nm.addEventListener("dblclick", ()=>{
if(fr.querySelector("input")) return;
const inp = document.createElement("input");
inp.value = fo.name; inp.maxLength = 20;
fr.replaceChild(inp, nm);
inp.focus(); inp.select();
let done = false;
const commit = ()=>{
if(done) return; done = true;
fo.name = inp.value.trim() || fo.name;
markBoardChange();
renderBoardsPanel(); updateBoardsUI();
};
inp.addEventListener("keydown", ev=>{ ev.stopPropagation(); if(ev.key==="Enter") commit(); });
inp.addEventListener("blur", commit);
inp.addEventListener("pointerdown", ev=>ev.stopPropagation());
});
fr.appendChild(nm);
const add = document.createElement("button");
add.textContent = T("newBoard");
add.addEventListener("click", ()=>{
fo.boards.push(newBoardEntry(`${T("boardName")} ${fo.boards.length+1}`));
switchTo(fi, fo.boards.length-1);
renderBoardsPanel();
});
fr.appendChild(add);
if(BS.folders.length > 1){
const del = document.createElement("button");
del.textContent = "🗑";
del.addEventListener("click", ()=>{
if(fi === BS.f){ BS.folders.splice(fi,1); BS.f = 0; BS.b = 0; loadCurrentSlot(); }
else{ BS.folders.splice(fi,1); if(BS.f > fi) BS.f--; }
markBoardChange();
renderBoardsPanel();
});
fr.appendChild(del);
}
boardsPanel.appendChild(fr);
const row = document.createElement("div");
row.className = "bp-boards";
fo.boards.forEach((bo, bi)=>{
const chip = document.createElement("div");
chip.className = "bp-board" + (fi===BS.f && bi===BS.b ? " active" : "");
const lbl = document.createElement("span");
lbl.textContent = bo.name;
chip.appendChild(lbl);
chip.addEventListener("click", e=>{
if(e.target.classList.contains("bpb-x") || e.target.tagName==="INPUT") return;
if(fi===BS.f && bi===BS.b) return;
switchTo(fi, bi);
renderBoardsPanel();
});
chip.addEventListener("dblclick", ()=>{
if(chip.querySelector("input")) return;
const inp = document.createElement("input");
inp.value = bo.name; inp.maxLength = 20;
chip.replaceChild(inp, lbl);
inp.focus(); inp.select();
let done = false;
const commit = ()=>{
if(done) return; done = true;
bo.name = inp.value.trim() || bo.name;
markBoardChange();
renderBoardsPanel(); updateBoardsUI();
};
inp.addEventListener("keydown", ev=>{ ev.stopPropagation(); if(ev.key==="Enter") commit(); });
inp.addEventListener("blur", commit);
inp.addEventListener("pointerdown", ev=>ev.stopPropagation());
});
/* bord verplaatsen naar een andere map: klik toont een inline select
met de overige mappen; het actieve bord verhuist mee (indices volgen) */
if(BS.folders.length > 1){
const mv = document.createElement("button");
mv.className = "bpb-x bpb-mv"; mv.textContent = "📁";
mv.title = T("moveBoardTip");
mv.addEventListener("click", ev=>{
ev.stopPropagation();
if(chip.querySelector("select")) return;
const sel = document.createElement("select");
sel.className = "bpb-mv-sel";
const ph = document.createElement("option");
ph.value = ""; ph.textContent = T("moveBoardTip");
sel.appendChild(ph);
BS.folders.forEach((f2, f2i)=>{
if(f2i === fi) return;
const o = document.createElement("option");
o.value = String(f2i); o.textContent = "📁 " + f2.name;
sel.appendChild(o);
});
sel.addEventListener("click", e2=>e2.stopPropagation());
sel.addEventListener("pointerdown", e2=>e2.stopPropagation());
sel.addEventListener("blur", ()=>sel.remove());
sel.addEventListener("change", ()=>{
if(sel.value === "") return;
const ti = +sel.value;
const wasCur = (fi===BS.f && bi===BS.b);
fo.boards.splice(bi,1);
BS.folders[ti].boards.push(bo);
if(!fo.boards.length) fo.boards.push(newBoardEntry(T("boardName")+" 1"));
if(wasCur){ BS.f = ti; BS.b = BS.folders[ti].boards.length-1; }
else if(fi===BS.f && bi < BS.b) BS.b--;
markBoardChange();
renderBoardsPanel(); updateBoardsUI();
});
chip.appendChild(sel);
sel.focus();
});
chip.appendChild(mv);
}
if(fo.boards.length > 1 || BS.folders.length > 1){
const x = document.createElement("button");
x.className = "bpb-x"; x.textContent = "✕";
x.addEventListener("click", ev=>{
ev.stopPropagation();
const wasCur = (fi===BS.f && bi===BS.b);
fo.boards.splice(bi,1);
if(!fo.boards.length) fo.boards.push(newBoardEntry(T("boardName")+" 1"));
if(wasCur){ BS.b = 0; loadCurrentSlot(); }
else if(fi===BS.f && bi < BS.b) BS.b--;
markBoardChange();
renderBoardsPanel();
});
chip.appendChild(x);
}
row.appendChild(chip);
});
boardsPanel.appendChild(row);
});
const nf = document.createElement("div");
nf.className = "bp-new";
const inp = document.createElement("input");
inp.placeholder = T("folderPh"); inp.maxLength = 20;
const addFolder = ()=>{
const name = inp.value.trim() || `${T("folderName")} ${BS.folders.length+1}`;
BS.folders.push({ name, boards:[newBoardEntry(T("boardName")+" 1")] });
inp.value = "";
markBoardChange();
renderBoardsPanel();
};
inp.addEventListener("keydown", e=>{ e.stopPropagation(); if(e.key==="Enter") addFolder(); });
const nb = document.createElement("button");
nb.className = "tbtn"; nb.textContent = T("newFolder");
nb.addEventListener("click", addFolder);
nf.appendChild(inp); nf.appendChild(nb);
boardsPanel.appendChild(nf);
const hint = document.createElement("div");
hint.className = "bp-hint"; hint.textContent = T("renameTip");
boardsPanel.appendChild(hint);
}
document.addEventListener("langchange", updateBoardsUI);

View file

@ -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.21-beta";
const VERSION = "0.3.22-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@ -75,8 +75,12 @@ const I18N = {
boardsNav:"Borden en mappen", newBoard:" Bord", newFolder:" Map",
folderPh:"naam nieuwe map…", boardName:"Bord", folderName:"Map",
delBoard:"Bord verwijderen (2× tikken)",
renameTip:"Dubbelklik op een bord of map om te hernoemen",
moveBoardTip:"verplaats naar map…",
exFolder:"Map", exRename:"Hernoem", exDelete:"Verwijder", exConfirm:"zeker?",
exSelectFirst:"Klik eerst een map of bord aan.", exNoSlash:"Een naam mag geen / bevatten.",
exExists:"Die naam bestaat al.", exEmpty:"Deze map is leeg.",
exFolderNotEmpty:"Map is niet leeg - verplaats of verwijder eerst de inhoud.",
exIntoSelf:"Een map kan niet in zichzelf.", exNoRootItems:"Borden horen in een map.",
exLastBoard:"Het laatste bord kan niet weg.",
hudMin:"Menu inklappen/uitklappen", hudOrient:"Horizontaal of verticaal",
wFull:"Maximaliseren", wUnfull:"Terugzetten",
wZoom:"Inhoud groter of kleiner (voor slechtzienden)",
@ -243,8 +247,12 @@ const I18N = {
boardsNav:"Boards and folders", newBoard:" Board", newFolder:" Folder",
folderPh:"name of new folder…", boardName:"Board", folderName:"Folder",
delBoard:"Delete board (tap twice)",
renameTip:"Double-click a board or folder to rename it",
moveBoardTip:"move to folder…",
exFolder:"Folder", exRename:"Rename", exDelete:"Delete", exConfirm:"sure?",
exSelectFirst:"Select a folder or board first.", exNoSlash:"A name cannot contain /.",
exExists:"That name already exists.", exEmpty:"This folder is empty.",
exFolderNotEmpty:"Folder is not empty - move or delete its contents first.",
exIntoSelf:"A folder cannot go inside itself.", exNoRootItems:"Boards belong in a folder.",
exLastBoard:"The last board cannot be deleted.",
hudMin:"Collapse/expand menu", hudOrient:"Horizontal or vertical",
wFull:"Maximize", wUnfull:"Restore",
wZoom:"Content bigger or smaller (low vision)",

278
public/js/explorer.js Normal file
View file

@ -0,0 +1,278 @@
/* teach - generieke mappenverkenner (Windows-stijl): één modal met een
broodkruimelpad, werkbalk (nieuwe map / nieuw item / hernoem / verwijder)
en een raster van map- en itemtegels. Klik = selecteren, dubbelklik =
openen, slepen = verplaatsen (pointer-events, werkt ook op touch).
Nesting is virtueel: paden zijn arrays van segmenten; de adapter bepaalt
hoe die op de onderliggende (platte) data worden afgebeeld. Mutaties in de
adapter geven null terug bij succes of een foutmelding (string) voor de
gebruiker.
Adapter-interface:
title() - venstertitel
list(path) - {folders:[{name,count}], items:[{key,name,tile()?,icon?,active?,movable?,deletable?}]}
createFolder(path, name) - nieuwe (lege) map
renameFolder(path, oldName, newName)
moveFolder(srcPath, targetPath)
deleteFolder(path, name) - alleen als leeg
createItem?(path) - optioneel: nieuw item in deze map
newItemLabel?() - label voor de nieuw-item-knop
allowRootItems - mogen items in de root staan (anders alleen mappen)
renameItem(key, name)
moveItem(key, targetPath)
deleteItem(key)
openItem(key) - dubbelklik op een item (modal sluit daarna)
*/
"use strict";
(function(){
const wrap = document.getElementById("explorerWrap");
const titleEl = document.getElementById("explorerTitle");
const crumbsEl = document.getElementById("exCrumbs");
const toolsEl = document.getElementById("exTools");
const msgEl = document.getElementById("exMsg");
const gridEl = document.getElementById("exGrid");
let AD = null; /* actieve adapter */
let PATH = []; /* huidig pad (segmenten) */
let SEL = null; /* selectie: {type:"folder"|"item", key} */
let onCloseCb = null;
window.openExplorer = function(adapter, onClose){
AD = adapter; PATH = []; SEL = null; onCloseCb = onClose || null;
render();
msg("");
wrap.classList.add("open");
};
function closeExplorer(){
wrap.classList.remove("open");
AD = null; SEL = null;
if(onCloseCb){ const cb = onCloseCb; onCloseCb = null; cb(); }
}
wrap.addEventListener("click", e=>{ if(e.target===wrap) closeExplorer(); });
document.querySelector(".explorer-close").addEventListener("click", closeExplorer);
const msg = t => { msgEl.textContent = t || ""; };
const validName = (name)=>{
const n = (name||"").trim();
if(!n) return null;
if(n.includes("/")){ msg(T("exNoSlash")); return null; }
if(n.length > 20) return n.slice(0,20);
return n;
};
/* inline invoer in een tegel (nieuwe map / hernoemen) - zelfde
Enter/blur-commit + Escape-annuleer als de bord-hernoem-inputs */
function inlineInput(host, initial, commit){
const inp = document.createElement("input");
inp.value = initial; inp.maxLength = 20;
host.innerHTML = "";
host.appendChild(inp);
inp.focus(); inp.select();
let done = false;
const finish = ok=>{
if(done) return; done = true;
if(ok){ const n = validName(inp.value); if(n){ commit(n); return; } }
render();
};
inp.addEventListener("keydown", ev=>{
ev.stopPropagation();
if(ev.key==="Enter") finish(true);
if(ev.key==="Escape") finish(false);
});
inp.addEventListener("blur", ()=>finish(true));
inp.addEventListener("pointerdown", ev=>ev.stopPropagation());
}
/* slepen: tegel volgt de aanwijzer (ghost), maptegels en broodkruimels
zijn drop-doelen ([data-drop]) en lichten op onder de aanwijzer */
function makeDraggable(el, payload){
el.addEventListener("pointerdown", e=>{
if(e.target.closest("input,button")) return;
const sx = e.clientX, sy = e.clientY;
let dragging = false, ghost = null, target = null;
const move = ev=>{
if(!dragging){
if(Math.hypot(ev.clientX-sx, ev.clientY-sy) < 8) return;
if(payload.movable === false) return;
dragging = true;
ghost = el.cloneNode(true);
ghost.classList.add("ex-ghost");
document.body.appendChild(ghost);
}
ev.preventDefault();
const zf = VZ();
ghost.style.left = (ev.clientX/zf + 10) + "px";
ghost.style.top = (ev.clientY/zf + 10) + "px";
const under = document.elementFromPoint(ev.clientX, ev.clientY);
const drop = under && under.closest("[data-drop]");
if(target && target !== drop) target.classList.remove("ex-drop");
target = (drop && drop !== el) ? drop : null;
if(target) target.classList.add("ex-drop");
};
const up = ()=>{
document.removeEventListener("pointermove", move);
document.removeEventListener("pointerup", up);
if(!dragging) return;
ghost.remove();
if(target){
target.classList.remove("ex-drop");
const dest = target.dataset.drop === "" ? [] : target.dataset.drop.split("/");
doMove(payload, dest);
}
};
document.addEventListener("pointermove", move);
document.addEventListener("pointerup", up);
});
}
function doMove(payload, dest){
msg("");
let err = null;
if(payload.type === "folder"){
const src = [...PATH, payload.key];
/* niet in zichzelf of een submap slepen */
if(dest.length >= src.length && src.every((s,i)=>dest[i]===s)){ msg(T("exIntoSelf")); return; }
err = AD.moveFolder(src, dest);
}else{
if(!dest.length && !AD.allowRootItems){ msg(T("exNoRootItems")); return; }
err = AD.moveItem(payload.key, dest);
}
SEL = null;
render();
if(err) msg(err);
}
function tileEl(cls){
const d = document.createElement("div");
d.className = "ex-tile " + cls;
return d;
}
const nameEl = (t)=>{ const n = document.createElement("div"); n.className = "ex-name"; n.textContent = t; return n; };
/* let op: render() wist de melding niet - mutatie-handlers zetten hun
foutmelding juist de her-render (anders verdween die meteen weer) */
function render(){
if(!AD) return;
titleEl.textContent = AD.title();
/* broodkruimels: 🏠-root + één knop per segment, allemaal drop-doel */
crumbsEl.innerHTML = "";
const crumb = (label, path)=>{
const b = document.createElement("button");
b.type = "button"; b.className = "ex-crumb";
b.textContent = label;
b.dataset.drop = path.join("/");
b.addEventListener("click", ()=>{ PATH = path; SEL = null; render(); msg(""); });
crumbsEl.appendChild(b);
return b;
};
crumb("🏠", []);
PATH.forEach((seg, i)=>{
const sep = document.createElement("span");
sep.className = "ex-sep"; sep.textContent = "";
crumbsEl.appendChild(sep);
crumb(seg, PATH.slice(0, i+1));
});
/* werkbalk */
toolsEl.innerHTML = "";
const tool = (label, fn)=>{
const b = document.createElement("button");
b.type = "button"; b.className = "tbtn ghost";
b.textContent = label;
b.addEventListener("click", fn);
toolsEl.appendChild(b);
return b;
};
tool(" " + T("exFolder"), ()=>{
const t = tileEl("folder");
const ico = document.createElement("div");
ico.className = "ex-ico"; ico.textContent = "📁";
t.appendChild(ico);
const holder = document.createElement("div");
t.appendChild(holder);
gridEl.prepend(t);
inlineInput(holder, "", (n)=>{
const err = AD.createFolder(PATH, n);
render();
if(err) msg(err);
});
});
if(AD.createItem && (PATH.length || AD.allowRootItems)){
tool(" " + AD.newItemLabel(), ()=>{
const err = AD.createItem(PATH);
render();
if(err) msg(err);
});
}
tool("✏ " + T("exRename"), ()=>{
if(!SEL){ msg(T("exSelectFirst")); return; }
const t = gridEl.querySelector(".ex-tile.sel .ex-name");
if(!t) return;
const cur = SEL.type==="folder" ? SEL.key : t.textContent;
inlineInput(t, cur, (n)=>{
const err = SEL.type==="folder" ? AD.renameFolder(PATH, SEL.key, n) : AD.renameItem(SEL.key, n);
SEL = null;
render();
if(err) msg(err);
});
});
const delBtn = tool("🗑 " + T("exDelete"), ()=>{
if(!SEL){ msg(T("exSelectFirst")); return; }
if(!delBtn.classList.contains("danger")){
delBtn.classList.add("danger");
delBtn.textContent = "🗑 " + T("exConfirm");
return;
}
const err = SEL.type==="folder" ? AD.deleteFolder(PATH, SEL.key) : AD.deleteItem(SEL.key);
SEL = null;
render();
if(err) msg(err);
});
/* raster: eerst mappen, dan items */
gridEl.innerHTML = "";
const { folders, items } = AD.list(PATH);
folders.forEach(f=>{
const t = tileEl("folder" + (SEL && SEL.type==="folder" && SEL.key===f.name ? " sel" : ""));
t.dataset.drop = [...PATH, f.name].join("/");
const ico = document.createElement("div");
ico.className = "ex-ico"; ico.textContent = "📁";
t.appendChild(ico);
t.appendChild(nameEl(f.name));
const c = document.createElement("div");
c.className = "ex-count"; c.textContent = f.count;
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});
gridEl.appendChild(t);
});
items.forEach(it=>{
const t = tileEl("item" + (it.active ? " active-item" : "") + (SEL && SEL.type==="item" && SEL.key===it.key ? " sel" : ""));
if(it.tile){ t.appendChild(it.tile()); }
else{
const ico = document.createElement("div");
ico.className = "ex-ico"; ico.textContent = it.icon || "📄";
t.appendChild(ico);
}
t.appendChild(nameEl(it.name));
t.addEventListener("click", ()=>{ SEL = {type:"item", key:it.key}; markSel(t); });
t.addEventListener("dblclick", ()=>{ AD.openItem(it.key); closeExplorer(); });
makeDraggable(t, {type:"item", key:it.key, movable: it.movable !== false});
gridEl.appendChild(t);
});
if(!folders.length && !items.length){
const d = document.createElement("div");
d.className = "guestnote"; d.textContent = T("exEmpty");
gridEl.appendChild(d);
}
}
/* selectie visueel bijwerken zonder volledige her-render (behoudt dblclick) */
function markSel(tile){
gridEl.querySelectorAll(".ex-tile.sel").forEach(x=>x.classList.remove("sel"));
tile.classList.add("sel");
/* een nieuwe selectie ont-wapent de verwijderknop */
const db = [...toolsEl.children].find(b=>b.classList.contains("danger"));
if(db){ db.classList.remove("danger"); db.textContent = "🗑 " + T("exDelete"); }
}
})();