/* 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, movable: f.fixed !== true}); 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"); } } })();