Verbeter anatomie en plaatsing van avataraccessoires (v0.4.46-beta) #1
6 changed files with 476 additions and 217 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.3.21-beta
|
0.3.22-beta
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@
|
||||||
heeft toegewezen, als vaste (niet-sleepbare) kaarten in speelmodus */
|
heeft toegewezen, als vaste (niet-sleepbare) kaarten in speelmodus */
|
||||||
#pupilView{display:none;}
|
#pupilView{display:none;}
|
||||||
body.pupil-mode #board,body.pupil-mode #fab,body.pupil-mode #fabBoardsBtn,
|
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 #hud,body.pupil-mode #btnFolders,body.pupil-mode #btnSave{display:none !important;}
|
||||||
body.pupil-mode #pupilView{
|
body.pupil-mode #pupilView{
|
||||||
display:flex; flex-wrap:wrap; gap:18px; align-content:flex-start;
|
display:flex; flex-wrap:wrap; gap:18px; align-content:flex-start;
|
||||||
|
|
@ -209,41 +209,40 @@
|
||||||
color:#fff; background:var(--green); border-color:var(--green);
|
color:#fff; background:var(--green); border-color:var(--green);
|
||||||
width:90px; min-height:130px; font-weight:900;
|
width:90px; min-height:130px; font-weight:900;
|
||||||
}
|
}
|
||||||
/* folder management panel (save menu) */
|
/* mappenverkenner (Windows-stijl, vervangt het oude mappenpaneel) */
|
||||||
#boardsPanel{
|
.modal.explorer{width:min(860px,94vw);}
|
||||||
position:fixed; right:16px; top:64px; z-index:5600; width:300px; max-height:66vh;
|
.ex-bar{display:flex; gap:10px; align-items:center; margin:10px 0 4px; flex-wrap:wrap;}
|
||||||
overflow:auto; display:none; flex-direction:column; gap:8px; padding:12px;
|
.ex-crumbs{display:flex; gap:4px; align-items:center; flex:1; min-width:0; flex-wrap:wrap;}
|
||||||
border-radius:16px; background:rgba(255,255,255,.96); backdrop-filter:blur(12px);
|
.ex-crumb{
|
||||||
box-shadow:0 10px 30px rgba(30,50,90,.3); border:1px solid rgba(255,255,255,.7);
|
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;}
|
.ex-crumb:hover{background:var(--surface-3);}
|
||||||
.bp-folder{display:flex; align-items:center; gap:6px; font-weight:800; font-size:14px; margin-top:4px;}
|
.ex-sep{color:var(--muted); font-weight:800;}
|
||||||
.bp-folder .bpf-name{flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
|
.ex-tools{display:flex; gap:8px; flex-wrap:wrap;}
|
||||||
.bp-folder button{
|
.ex-msg{min-height:18px; color:var(--red); font-size:13px; font-weight:700;}
|
||||||
border:none; background:var(--surface-2); border-radius:8px; padding:4px 8px; font:inherit;
|
.ex-grid{
|
||||||
font-weight:800; font-size:11.5px; cursor:pointer; color:var(--muted); flex:none;
|
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);}
|
.ex-tile{
|
||||||
.bp-boards{display:flex; flex-wrap:wrap; gap:6px;}
|
border:1.5px solid var(--line); border-radius:14px; padding:10px; background:var(--surface);
|
||||||
.bp-board{
|
display:flex; flex-direction:column; align-items:center; gap:6px; cursor:pointer;
|
||||||
display:flex; align-items:center; gap:5px; border:1px solid var(--line); background:var(--surface);
|
user-select:none; touch-action:none; min-height:110px; justify-content:center;
|
||||||
border-radius:10px; padding:5px 10px; font:inherit; font-weight:700; font-size:12.5px;
|
|
||||||
cursor:pointer; color:var(--muted);
|
|
||||||
}
|
}
|
||||||
.bp-board.active{border-color:var(--accent); background:var(--accent-soft); color:var(--accent-ink);}
|
.ex-tile:hover{border-color:var(--blue);}
|
||||||
.bp-board .bpb-x{border:none; background:transparent; color:var(--red); font-weight:900;
|
.ex-tile.sel{border-color:var(--blue); background:var(--accent-soft);}
|
||||||
cursor:pointer; padding:0; font-size:11px; opacity:.8;}
|
.ex-tile.active-item{border-color:var(--accent); border-width:2.5px;}
|
||||||
.bp-board:hover .bpb-x{opacity:1;}
|
.ex-tile .ex-ico{font-size:36px; line-height:1;}
|
||||||
.bp-board .bpb-mv{color:var(--muted); font-weight:400;}
|
.ex-tile canvas{max-width:100%; border-radius:8px; border:1px solid var(--line);}
|
||||||
.bp-board .bpb-mv-sel{font:inherit; max-width:150px;}
|
.ex-name{font-weight:700; font-size:13.5px; text-align:center; word-break:break-word;}
|
||||||
.bp-board input{border:none; outline:none; font:inherit; width:80px; background:transparent;}
|
.ex-count{font-size:12px; color:var(--muted); font-weight:700;}
|
||||||
.bp-folder input{border:none; outline:none; font:inherit; width:110px; background:transparent; border-bottom:1px solid var(--line);}
|
.ex-tile input{
|
||||||
.bp-new{display:flex; gap:6px; margin-top:6px;}
|
border:2px solid var(--blue); border-radius:8px; font:inherit; font-size:13.5px;
|
||||||
.bp-new input{
|
width:100%; outline:none; padding:4px 6px; text-align:center;
|
||||||
flex:1; min-width:0; border:1px solid var(--line); border-radius:10px; padding:7px 9px;
|
|
||||||
font:inherit; font-size:13px; outline:none;
|
|
||||||
}
|
}
|
||||||
.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;}
|
#fabmenu.open{display:flex;}
|
||||||
.fab-item{
|
.fab-item{
|
||||||
display:flex; align-items:center; gap:10px; border:none;
|
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 .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-result{background:#0047ab;}
|
||||||
body.hc .np-row label,body.hc .thint,body.hc .guestnote,body.hc .ed-credit,
|
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 .gcard{border-color:#000; background:var(--surface);}
|
||||||
body.hc .gcat-title{color:#000;}
|
body.hc .gcat-title{color:#000;}
|
||||||
body.hc .tbtn.ghost{background:var(--surface); border:2px solid #000; 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 .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 .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 .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 .dock-item{color:#000;}
|
||||||
body.hc .media-msg{color:#ddd;}
|
body.hc .media-msg{color:#ddd;}
|
||||||
/* =========== Flash words widget =========== */
|
/* =========== 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) ---------- */
|
/* ---------- tablet (≤1024px) ---------- */
|
||||||
@media (max-width: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) ---------- */
|
/* ---------- 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 */
|
/* alleen de uil op kleine schermen, woordmerk verdwijnt */
|
||||||
.logo-word{display:none;}
|
.logo-word{display:none;}
|
||||||
.tbtn{padding:0 10px; font-size:13px;}
|
.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 */
|
/* beheerpaneel: dichte rijen worden gestapeld i.p.v. verdrukt */
|
||||||
.am-row{flex-direction:column; align-items:stretch;}
|
.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 #board,body.tb-right #pupilView{inset:0 var(--tbar) 0 0;}
|
||||||
body.tb-right #fab{right:calc(var(--tbar) + 16px);}
|
body.tb-right #fab{right:calc(var(--tbar) + 16px);}
|
||||||
body.tb-right #fabBoardsBtn{right:calc(var(--tbar) + 88px);}
|
body.tb-right #fabBoardsBtn{right:calc(var(--tbar) + 88px);}
|
||||||
body.tb-right #boardStrip,body.tb-right #boardsPanel{right:calc(var(--tbar) + 12px);}
|
body.tb-right #boardStrip{right:calc(var(--tbar) + 12px);}
|
||||||
body.tb-left #boardsPanel,body.tb-right #boardsPanel{top:12px;}
|
|
||||||
body.tb-left #fabmenu{right:24px;}
|
body.tb-left #fabmenu{right:24px;}
|
||||||
body.tb-right #fabmenu{right:calc(var(--tbar) + 16px);}
|
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.
|
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 .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{
|
body.no-glass .widget.bare .widget-head{
|
||||||
backdrop-filter:none; -webkit-backdrop-filter:none;
|
backdrop-filter:none; -webkit-backdrop-filter:none;
|
||||||
background:var(--surface);
|
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 #hud{background:var(--glass); border-color:var(--glass-line);}
|
||||||
body.dark .fab-item{background:var(--glass-strong); color:var(--ink);}
|
body.dark .fab-item{background:var(--glass-strong); color:var(--ink);}
|
||||||
body.dark .fab-item:hover{background:var(--surface-3);}
|
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 .widget.bare .widget-head{background:var(--glass-strong);}
|
||||||
body.dark .mm-top{background:var(--glass-strong);}
|
body.dark .mm-top{background:var(--glass-strong);}
|
||||||
body.dark .mm-pop{background:var(--surface); box-shadow:var(--shadow-2);}
|
body.dark .mm-pop{background:var(--surface); box-shadow:var(--shadow-2);}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,17 @@
|
||||||
<button id="fab">+</button>
|
<button id="fab">+</button>
|
||||||
<button id="fabBoardsBtn">🗂</button>
|
<button id="fabBoardsBtn">🗂</button>
|
||||||
<div id="boardStrip"></div>
|
<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">
|
<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="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>
|
<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/core.js"></script>
|
||||||
<script src="js/permissions.js"></script>
|
<script src="js/permissions.js"></script>
|
||||||
<script src="js/data.js"></script>
|
<script src="js/data.js"></script>
|
||||||
|
<script src="js/explorer.js"></script>
|
||||||
<script src="js/picker.js"></script>
|
<script src="js/picker.js"></script>
|
||||||
<script src="js/widgets/letters.js"></script>
|
<script src="js/widgets/letters.js"></script>
|
||||||
<script src="js/widgets/write.js"></script>
|
<script src="js/widgets/write.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -991,7 +991,14 @@ function boardsFromData(data){
|
||||||
}
|
}
|
||||||
function curSlot(){
|
function curSlot(){
|
||||||
if(BS.f >= BS.folders.length) BS.f = 0;
|
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;
|
if(BS.b >= fo.boards.length) BS.b = 0;
|
||||||
return fo.boards[BS.b];
|
return fo.boards[BS.b];
|
||||||
}
|
}
|
||||||
|
|
@ -1007,23 +1014,143 @@ function switchTo(f, b){
|
||||||
BS.f = f; BS.b = b;
|
BS.f = f; BS.b = b;
|
||||||
loadCurrentSlot();
|
loadCurrentSlot();
|
||||||
}
|
}
|
||||||
const boardsPanel = document.getElementById("boardsPanel");
|
|
||||||
const btnFolders = document.getElementById("btnFolders");
|
const btnFolders = document.getElementById("btnFolders");
|
||||||
btnFolders.addEventListener("click", ()=>{
|
btnFolders.addEventListener("click", ()=>{
|
||||||
renderBoardsPanel();
|
stashCurrent(); /* verse thumbnails, ook van het actieve bord */
|
||||||
boardsPanel.classList.toggle("open");
|
openExplorer(boardsExplorerAdapter());
|
||||||
});
|
|
||||||
document.addEventListener("pointerdown", e=>{
|
|
||||||
if(!boardsPanel.classList.contains("open")) return;
|
|
||||||
if(e.target.closest("#btnFolders") || e.target.closest("#boardsPanel")) return;
|
|
||||||
boardsPanel.classList.remove("open");
|
|
||||||
});
|
});
|
||||||
function updateBoardsUI(){
|
function updateBoardsUI(){
|
||||||
btnFolders.title = `${T("boardsNav")} · ${BS.folders[BS.f].name} / ${curSlot().name}`;
|
btnFolders.title = `${T("boardsNav")} · ${BS.folders[BS.f].name} / ${curSlot().name}`;
|
||||||
fabBoardsBtn.title = `${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();
|
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(){
|
function flatBoards(){
|
||||||
const out = [];
|
const out = [];
|
||||||
BS.folders.forEach((fo,fi)=>fo.boards.forEach((bo,bi)=>out.push({fi,bi,bo,fo})));
|
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);
|
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);
|
document.addEventListener("langchange", updateBoardsUI);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
/* version — shown until /api/version resolves (or if the fetch fails, e.g. offline).
|
/* 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. */
|
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(){
|
(function(){
|
||||||
const tag = document.getElementById("verTag");
|
const tag = document.getElementById("verTag");
|
||||||
tag.textContent = "v"+VERSION;
|
tag.textContent = "v"+VERSION;
|
||||||
|
|
@ -75,8 +75,12 @@ const I18N = {
|
||||||
boardsNav:"Borden en mappen", newBoard:"+ Bord", newFolder:"+ Map",
|
boardsNav:"Borden en mappen", newBoard:"+ Bord", newFolder:"+ Map",
|
||||||
folderPh:"naam nieuwe map…", boardName:"Bord", folderName:"Map",
|
folderPh:"naam nieuwe map…", boardName:"Bord", folderName:"Map",
|
||||||
delBoard:"Bord verwijderen (2× tikken)",
|
delBoard:"Bord verwijderen (2× tikken)",
|
||||||
renameTip:"Dubbelklik op een bord of map om te hernoemen",
|
exFolder:"Map", exRename:"Hernoem", exDelete:"Verwijder", exConfirm:"zeker?",
|
||||||
moveBoardTip:"verplaats naar map…",
|
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",
|
hudMin:"Menu inklappen/uitklappen", hudOrient:"Horizontaal of verticaal",
|
||||||
wFull:"Maximaliseren", wUnfull:"Terugzetten",
|
wFull:"Maximaliseren", wUnfull:"Terugzetten",
|
||||||
wZoom:"Inhoud groter of kleiner (voor slechtzienden)",
|
wZoom:"Inhoud groter of kleiner (voor slechtzienden)",
|
||||||
|
|
@ -243,8 +247,12 @@ const I18N = {
|
||||||
boardsNav:"Boards and folders", newBoard:"+ Board", newFolder:"+ Folder",
|
boardsNav:"Boards and folders", newBoard:"+ Board", newFolder:"+ Folder",
|
||||||
folderPh:"name of new folder…", boardName:"Board", folderName:"Folder",
|
folderPh:"name of new folder…", boardName:"Board", folderName:"Folder",
|
||||||
delBoard:"Delete board (tap twice)",
|
delBoard:"Delete board (tap twice)",
|
||||||
renameTip:"Double-click a board or folder to rename it",
|
exFolder:"Folder", exRename:"Rename", exDelete:"Delete", exConfirm:"sure?",
|
||||||
moveBoardTip:"move to folder…",
|
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",
|
hudMin:"Collapse/expand menu", hudOrient:"Horizontal or vertical",
|
||||||
wFull:"Maximize", wUnfull:"Restore",
|
wFull:"Maximize", wUnfull:"Restore",
|
||||||
wZoom:"Content bigger or smaller (low vision)",
|
wZoom:"Content bigger or smaller (low vision)",
|
||||||
|
|
|
||||||
278
public/js/explorer.js
Normal file
278
public/js/explorer.js
Normal 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 ná 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"); }
|
||||||
|
}
|
||||||
|
})();
|
||||||
Loading…
Reference in a new issue