Voeg 28 herkenbare kleurplaten en creatieve niveaus toe (v0.4.66-beta) #4
5 changed files with 250 additions and 148 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.3.29-beta
|
0.3.30-beta
|
||||||
|
|
|
||||||
|
|
@ -1015,9 +1015,29 @@ function switchTo(f, b){
|
||||||
loadCurrentSlot();
|
loadCurrentSlot();
|
||||||
}
|
}
|
||||||
const btnFolders = document.getElementById("btnFolders");
|
const btnFolders = document.getElementById("btnFolders");
|
||||||
|
/* ankerdiagram uit de bibliotheek openen vanuit de hoofdverkenner: plaatst
|
||||||
|
een nieuw ankerwidget op het huidige bord met dat diagram erin geladen */
|
||||||
|
function openAnchorOnBoard(entry){
|
||||||
|
const def = REGISTRY.find(d=>d.id==="anchor");
|
||||||
|
makeWidget(def, { state: {
|
||||||
|
cells: structuredClone(entry.cells || {}),
|
||||||
|
pics: structuredClone(entry.pics || {}),
|
||||||
|
}});
|
||||||
|
markBoardChange();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
btnFolders.addEventListener("click", ()=>{
|
btnFolders.addEventListener("click", ()=>{
|
||||||
stashCurrent(); /* verse thumbnails, ook van het actieve bord */
|
stashCurrent(); /* verse thumbnails, ook van het actieve bord */
|
||||||
openExplorer(withSharedBranches("board", boardsExplorerAdapter(), boardsSharedHooks()));
|
openExplorer(withBranch(
|
||||||
|
"⚓ " + T("anchorLibTitle"),
|
||||||
|
withSharedBranches("board", boardsExplorerAdapter(), boardsSharedHooks()),
|
||||||
|
withSharedBranches("anchor", anchorLibraryAdapter(openAnchorOnBoard), anchorLibrarySharedHooks(openAnchorOnBoard)),
|
||||||
|
{
|
||||||
|
wrongTypeMsg: T("exWrongBranch"),
|
||||||
|
branchCreateMsg: T("anchorLibCreateHint"),
|
||||||
|
branchCount: ()=>ANCHOR_LIB.length,
|
||||||
|
}
|
||||||
|
));
|
||||||
});
|
});
|
||||||
/* koppelstuk voor de gedeelde bibliotheek (zie withSharedBranches in
|
/* koppelstuk voor de gedeelde bibliotheek (zie withSharedBranches in
|
||||||
explorer.js): borden exporteren/importeren als {name, data}. Een kopie
|
explorer.js): borden exporteren/importeren als {name, data}. Een kopie
|
||||||
|
|
|
||||||
|
|
@ -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.29-beta";
|
const VERSION = "0.3.30-beta";
|
||||||
(function(){
|
(function(){
|
||||||
const tag = document.getElementById("verTag");
|
const tag = document.getElementById("verTag");
|
||||||
tag.textContent = "v"+VERSION;
|
tag.textContent = "v"+VERSION;
|
||||||
|
|
@ -90,6 +90,8 @@ const I18N = {
|
||||||
shNoCreateHere:"Nieuw maken kan alleen in je eigen mappen - sleep het daarna hierheen om te delen.",
|
shNoCreateHere:"Nieuw maken kan alleen in je eigen mappen - sleep het daarna hierheen om te delen.",
|
||||||
shReadonly:"Alleen lezen - kopieer het naar je eigen mappen om te bewerken.",
|
shReadonly:"Alleen lezen - kopieer het naar je eigen mappen om te bewerken.",
|
||||||
exLock:"Alleen lezen", shLockedOk:"Vergrendeld ✓", shUnlockedOk:"Ontgrendeld ✓",
|
exLock:"Alleen lezen", shLockedOk:"Vergrendeld ✓", shUnlockedOk:"Ontgrendeld ✓",
|
||||||
|
exWrongBranch:"Dat hoort niet in deze map (borden en ankerdiagrammen zijn gescheiden).",
|
||||||
|
anchorLibCreateHint:"Nieuw diagram maken: gebruik 💾 op een ankerwidget.",
|
||||||
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)",
|
||||||
|
|
@ -271,6 +273,8 @@ const I18N = {
|
||||||
shNoCreateHere:"Create it in your own folders first - then drag it here to share.",
|
shNoCreateHere:"Create it in your own folders first - then drag it here to share.",
|
||||||
shReadonly:"Read-only - copy it to your own folders to edit.",
|
shReadonly:"Read-only - copy it to your own folders to edit.",
|
||||||
exLock:"Read-only", shLockedOk:"Locked ✓", shUnlockedOk:"Unlocked ✓",
|
exLock:"Read-only", shLockedOk:"Locked ✓", shUnlockedOk:"Unlocked ✓",
|
||||||
|
exWrongBranch:"That doesn't belong in this folder (boards and anchor diagrams are separate).",
|
||||||
|
anchorLibCreateHint:"To create a new diagram: use 💾 on an anchor widget.",
|
||||||
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)",
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,82 @@
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
Combineert twee adapters tot één boom: sub verschijnt als vaste map
|
||||||
|
(rootLabel) in de root van main. Slepen tussen de twee takken wordt
|
||||||
|
geweigerd (ander soort inhoud). Item-sleutels van sub worden geprefixt
|
||||||
|
zodat ze nooit botsen met die van main.
|
||||||
|
opts: { wrongTypeMsg, branchCreateMsg, branchCount() }
|
||||||
|
==========================================================*/
|
||||||
|
function withBranch(rootLabel, main, sub, opts){
|
||||||
|
const ENC = "br|";
|
||||||
|
const enc = k => ENC + String(k);
|
||||||
|
const dec = k => { const raw = String(k).slice(ENC.length); return /^\d+$/.test(raw) ? +raw : raw; };
|
||||||
|
const isEnc = k => typeof k === "string" && k.startsWith(ENC);
|
||||||
|
const inBranch = path => path.length > 0 && path[0] === rootLabel;
|
||||||
|
const wrongType = ()=>(opts && opts.wrongTypeMsg) || T("exExists");
|
||||||
|
const shiftSel = sel => (sel && sel.type === "item" && isEnc(sel.key)) ? { type:"item", key: dec(sel.key) } : sel;
|
||||||
|
return {
|
||||||
|
title: main.title,
|
||||||
|
allowRootItems: main.allowRootItems,
|
||||||
|
newItemLabel: main.newItemLabel,
|
||||||
|
async init(){
|
||||||
|
if(main.init) await main.init();
|
||||||
|
if(sub.init) await sub.init();
|
||||||
|
},
|
||||||
|
list(path){
|
||||||
|
if(inBranch(path)){
|
||||||
|
const r = sub.list(path.slice(1));
|
||||||
|
return { folders: r.folders, items: r.items.map(it=>({ ...it, key: enc(it.key) })) };
|
||||||
|
}
|
||||||
|
const r = main.list(path);
|
||||||
|
if(path.length === 0){
|
||||||
|
r.folders.push({ name: rootLabel, count: opts && opts.branchCount ? opts.branchCount() : "", fixed: true });
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
createItem: main.createItem ? (path)=>{
|
||||||
|
if(inBranch(path)) return (opts && opts.branchCreateMsg) || T("shNoCreateHere");
|
||||||
|
return main.createItem(path);
|
||||||
|
} : undefined,
|
||||||
|
createFolder(path, name){
|
||||||
|
if(inBranch(path)) return sub.createFolder(path.slice(1), name);
|
||||||
|
if(path.length === 0 && name === rootLabel) return T("exExists");
|
||||||
|
return main.createFolder(path, name);
|
||||||
|
},
|
||||||
|
renameFolder(path, oldName, newName){
|
||||||
|
if(path.length === 0 && oldName === rootLabel) return T("exFixedFolder");
|
||||||
|
return inBranch(path) ? sub.renameFolder(path.slice(1), oldName, newName) : main.renameFolder(path, oldName, newName);
|
||||||
|
},
|
||||||
|
moveFolder(srcPath, targetPath, newName){
|
||||||
|
if(srcPath.length === 1 && srcPath[0] === rootLabel) return T("exFixedFolder");
|
||||||
|
const s = inBranch(srcPath), t = inBranch(targetPath);
|
||||||
|
if(s !== t) return wrongType();
|
||||||
|
return s ? sub.moveFolder(srcPath.slice(1), targetPath.slice(1), newName) : main.moveFolder(srcPath, targetPath, newName);
|
||||||
|
},
|
||||||
|
deleteFolder(path, name){
|
||||||
|
if(path.length === 0 && name === rootLabel) return T("exFixedFolder");
|
||||||
|
return inBranch(path) ? sub.deleteFolder(path.slice(1), name) : main.deleteFolder(path, name);
|
||||||
|
},
|
||||||
|
renameItem(k, name){ return isEnc(k) ? sub.renameItem(dec(k), name) : main.renameItem(k, name); },
|
||||||
|
moveItem(k, dest){
|
||||||
|
const s = isEnc(k), t = inBranch(dest);
|
||||||
|
if(s !== t) return wrongType();
|
||||||
|
return s ? sub.moveItem(dec(k), dest.slice(1)) : main.moveItem(k, dest);
|
||||||
|
},
|
||||||
|
deleteItem(k){ return isEnc(k) ? sub.deleteItem(dec(k)) : main.deleteItem(k); },
|
||||||
|
openItem(k){ return isEnc(k) ? sub.openItem(dec(k)) : main.openItem(k); },
|
||||||
|
lockLabel(path){
|
||||||
|
if(inBranch(path)) return sub.lockLabel ? sub.lockLabel(path.slice(1)) : null;
|
||||||
|
return main.lockLabel ? main.lockLabel(path) : null;
|
||||||
|
},
|
||||||
|
toggleLock(sel, path){
|
||||||
|
if(inBranch(path)) return sub.toggleLock ? sub.toggleLock(shiftSel(sel), path.slice(1)) : null;
|
||||||
|
return main.toggleLock ? main.toggleLock(sel, path) : null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
Gedeelde bibliotheek in de verkenner: wikkelt een "eigen inhoud"-adapter
|
Gedeelde bibliotheek in de verkenner: wikkelt een "eigen inhoud"-adapter
|
||||||
en voegt in de root twee vaste takken toe: 🌐 Systeem (systeembreed,
|
en voegt in de root twee vaste takken toe: 🌐 Systeem (systeembreed,
|
||||||
|
|
|
||||||
|
|
@ -228,105 +228,18 @@ function mountAnchor(root, initState, opts){
|
||||||
inp.addEventListener("blur", commit);
|
inp.addEventListener("blur", commit);
|
||||||
inp.addEventListener("pointerdown", ev=>ev.stopPropagation());
|
inp.addEventListener("pointerdown", ev=>ev.stopPropagation());
|
||||||
}
|
}
|
||||||
/* ---- ankerbibliotheek: diagram los van het bord bewaren en hergebruiken.
|
/* een bewaard/gedeeld diagram in dít widget laden (dubbelklik in de
|
||||||
Opslag in ANCHOR_LIB (eigen save-sectie "anchors"), beheer via dezelfde
|
verkenner die via het 🗂-knopje van dit widget is geopend) */
|
||||||
Windows-stijl mappenverkenner als de borden en thema's. */
|
function loadDiagram(entry){
|
||||||
function anchorLibAdapter(){
|
S.cells = structuredClone(entry.cells || {});
|
||||||
const split = n => (n||"").split("/").map(s=>s.trim()).filter(Boolean);
|
S.pics = structuredClone(entry.pics || {});
|
||||||
const join = p => p.join("/");
|
S.pan = {x:0, y:0};
|
||||||
const wordCount = it => Object.keys(it.cells||{}).length;
|
syncSource();
|
||||||
const pending = new Set();
|
render();
|
||||||
const changed = ()=>scheduleSave("anchors");
|
persistWords();
|
||||||
return {
|
/* programmatisch geladen: de bord-autosave-listeners zien dit niet */
|
||||||
title: ()=>T("anchorLibTitle"),
|
markBoardChange();
|
||||||
allowRootItems: true,
|
return null;
|
||||||
list(path){
|
|
||||||
const p = join(path);
|
|
||||||
const folderMap = new Map();
|
|
||||||
const items = [];
|
|
||||||
const seen = new Set(pending);
|
|
||||||
ANCHOR_LIB.forEach((it, i)=>{
|
|
||||||
const j = join(split(it.folder));
|
|
||||||
if(j === p) items.push({ key: i, name: `${it.name} (${wordCount(it)})`, icon: "⚓" });
|
|
||||||
else if(!p || j.startsWith(p + "/")) seen.add(j);
|
|
||||||
});
|
|
||||||
seen.forEach(j=>{
|
|
||||||
if(j === p) return;
|
|
||||||
if(p && !j.startsWith(p + "/")) return;
|
|
||||||
if(!p && !j) return;
|
|
||||||
const seg = (p ? j.slice(p.length + 1) : j).split("/")[0];
|
|
||||||
if(!seg) return;
|
|
||||||
const full = p ? p + "/" + seg : seg;
|
|
||||||
const under = ANCHOR_LIB.filter(it=>{ const ij = join(split(it.folder)); return ij === full || ij.startsWith(full + "/"); }).length;
|
|
||||||
folderMap.set(seg, under);
|
|
||||||
});
|
|
||||||
const folders = [...folderMap].map(([name, count])=>({name, count}))
|
|
||||||
.sort((a,b)=>a.name.localeCompare(b.name));
|
|
||||||
return { folders, items };
|
|
||||||
},
|
|
||||||
createFolder(path, name){
|
|
||||||
if(this.list(path).folders.some(f=>f.name===name)) return T("exExists");
|
|
||||||
pending.add(join([...path, name]));
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
renameFolder(path, oldName, newName){
|
|
||||||
return this.moveFolder([...path, oldName], path, newName);
|
|
||||||
},
|
|
||||||
moveFolder(srcPath, targetPath, newName){
|
|
||||||
const from = join(srcPath);
|
|
||||||
const to = join([...targetPath, newName || srcPath[srcPath.length-1]]);
|
|
||||||
if(from === to) return null;
|
|
||||||
if(ANCHOR_LIB.some(it=>{ const j = join(split(it.folder)); return j === to || j.startsWith(to + "/"); }) || pending.has(to)) return T("exExists");
|
|
||||||
ANCHOR_LIB.forEach(it=>{
|
|
||||||
const j = join(split(it.folder));
|
|
||||||
if(j === from || j.startsWith(from + "/")) it.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(ANCHOR_LIB.some(it=>{ const j = join(split(it.folder)); return j === full || j.startsWith(full + "/"); })) return T("exFolderNotEmpty");
|
|
||||||
[...pending].forEach(j=>{ if(j === full || j.startsWith(full + "/")) pending.delete(j); });
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
renameItem(i, name){
|
|
||||||
if(!ANCHOR_LIB[i]) return null;
|
|
||||||
ANCHOR_LIB[i].name = name;
|
|
||||||
changed();
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
moveItem(i, targetPath){
|
|
||||||
const it = ANCHOR_LIB[i];
|
|
||||||
if(!it) return null;
|
|
||||||
if(targetPath.length) it.folder = join(targetPath); else delete it.folder;
|
|
||||||
pending.delete(join(targetPath));
|
|
||||||
changed();
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
deleteItem(i){
|
|
||||||
if(!ANCHOR_LIB[i]) return null;
|
|
||||||
ANCHOR_LIB.splice(i, 1);
|
|
||||||
changed();
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
/* dubbelklik: bewaard diagram in dít widget laden */
|
|
||||||
openItem(i){
|
|
||||||
const it = ANCHOR_LIB[i];
|
|
||||||
if(!it) return;
|
|
||||||
S.cells = structuredClone(it.cells || {});
|
|
||||||
S.pics = structuredClone(it.pics || {});
|
|
||||||
S.pan = {x:0, y:0};
|
|
||||||
syncSource();
|
|
||||||
render();
|
|
||||||
persistWords();
|
|
||||||
/* programmatisch geladen: de bord-autosave-listeners zien dit niet */
|
|
||||||
markBoardChange();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if(!readonly){
|
if(!readonly){
|
||||||
saveBtn.title = T("anchorLibSave");
|
saveBtn.title = T("anchorLibSave");
|
||||||
|
|
@ -344,52 +257,8 @@ function mountAnchor(root, initState, opts){
|
||||||
saveBtn.textContent = "✓";
|
saveBtn.textContent = "✓";
|
||||||
setTimeout(()=>{ saveBtn.textContent = orig; }, 1200);
|
setTimeout(()=>{ saveBtn.textContent = orig; }, 1200);
|
||||||
});
|
});
|
||||||
/* koppelstuk voor de gedeelde bibliotheek: diagrammen als {name, data} */
|
libBtn.addEventListener("click", ()=>openExplorer(
|
||||||
const anchorSharedHooks = ()=>{
|
withSharedBranches("anchor", anchorLibraryAdapter(loadDiagram), anchorLibrarySharedHooks(loadDiagram))));
|
||||||
const split = n => (n||"").split("/").map(s=>s.trim()).filter(Boolean);
|
|
||||||
const join = p => p.join("/");
|
|
||||||
return {
|
|
||||||
exportItem(i){
|
|
||||||
const it = ANCHOR_LIB[i];
|
|
||||||
return it ? { name: it.name, data: { cells: structuredClone(it.cells||{}), pics: structuredClone(it.pics||{}) } } : null;
|
|
||||||
},
|
|
||||||
exportFolder(path){
|
|
||||||
const p = join(path);
|
|
||||||
const out = [];
|
|
||||||
ANCHOR_LIB.forEach(it=>{
|
|
||||||
const j = join(split(it.folder));
|
|
||||||
if(j === p || j.startsWith(p + "/")){
|
|
||||||
const rel = j === p ? [] : j.slice(p.length + 1).split("/");
|
|
||||||
out.push({ name: it.name, rel, data: { cells: structuredClone(it.cells||{}), pics: structuredClone(it.pics||{}) } });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return out;
|
|
||||||
},
|
|
||||||
copyIn(item, destPath){
|
|
||||||
const entry = {
|
|
||||||
id: "al-"+Math.random().toString(36).slice(2,10),
|
|
||||||
name: item.name,
|
|
||||||
cells: structuredClone(item.data.cells || {}),
|
|
||||||
pics: structuredClone(item.data.pics || {}),
|
|
||||||
};
|
|
||||||
if(destPath.length) entry.folder = join(destPath);
|
|
||||||
ANCHOR_LIB.push(entry);
|
|
||||||
scheduleSave("anchors");
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
openShared(item){
|
|
||||||
S.cells = structuredClone(item.data.cells || {});
|
|
||||||
S.pics = structuredClone(item.data.pics || {});
|
|
||||||
S.pan = {x:0, y:0};
|
|
||||||
syncSource();
|
|
||||||
render();
|
|
||||||
persistWords();
|
|
||||||
markBoardChange();
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
libBtn.addEventListener("click", ()=>openExplorer(withSharedBranches("anchor", anchorLibAdapter(), anchorSharedHooks())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new ResizeObserver(render).observe(stage);
|
new ResizeObserver(render).observe(stage);
|
||||||
|
|
@ -400,3 +269,136 @@ function mountAnchor(root, initState, opts){
|
||||||
return { getState: ()=>({ cells:S.cells, pan:S.pan, pics:S.pics, themeId:S.themeId }) };
|
return { getState: ()=>({ cells:S.cells, pan:S.pan, pics:S.pics, themeId:S.themeId }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- ankerbibliotheek: diagrammen los van een bord bewaren en hergebruiken.
|
||||||
|
Opslag in ANCHOR_LIB (eigen save-sectie "anchors"), beheer via de
|
||||||
|
Windows-stijl mappenverkenner. Op module-niveau zodat zowel het 🗂-knopje
|
||||||
|
op een widget (openen = in dát widget laden) als de vaste ⚓-tak in de
|
||||||
|
borden-verkenner (openen = nieuw widget op het huidige bord) dezelfde
|
||||||
|
adapter gebruiken - alleen de onOpen-actie verschilt. */
|
||||||
|
function anchorLibraryAdapter(onOpen){
|
||||||
|
const split = n => (n||"").split("/").map(s=>s.trim()).filter(Boolean);
|
||||||
|
const join = p => p.join("/");
|
||||||
|
const wordCount = it => Object.keys(it.cells||{}).length;
|
||||||
|
const pending = new Set();
|
||||||
|
const changed = ()=>scheduleSave("anchors");
|
||||||
|
return {
|
||||||
|
title: ()=>T("anchorLibTitle"),
|
||||||
|
allowRootItems: true,
|
||||||
|
list(path){
|
||||||
|
const p = join(path);
|
||||||
|
const folderMap = new Map();
|
||||||
|
const items = [];
|
||||||
|
const seen = new Set(pending);
|
||||||
|
ANCHOR_LIB.forEach((it, i)=>{
|
||||||
|
const j = join(split(it.folder));
|
||||||
|
if(j === p) items.push({ key: i, name: `${it.name} (${wordCount(it)})`, icon: "⚓" });
|
||||||
|
else if(!p || j.startsWith(p + "/")) seen.add(j);
|
||||||
|
});
|
||||||
|
seen.forEach(j=>{
|
||||||
|
if(j === p) return;
|
||||||
|
if(p && !j.startsWith(p + "/")) return;
|
||||||
|
if(!p && !j) return;
|
||||||
|
const seg = (p ? j.slice(p.length + 1) : j).split("/")[0];
|
||||||
|
if(!seg) return;
|
||||||
|
const full = p ? p + "/" + seg : seg;
|
||||||
|
const under = ANCHOR_LIB.filter(it=>{ const ij = join(split(it.folder)); return ij === full || ij.startsWith(full + "/"); }).length;
|
||||||
|
folderMap.set(seg, under);
|
||||||
|
});
|
||||||
|
const folders = [...folderMap].map(([name, count])=>({name, count}))
|
||||||
|
.sort((a,b)=>a.name.localeCompare(b.name));
|
||||||
|
return { folders, items };
|
||||||
|
},
|
||||||
|
createFolder(path, name){
|
||||||
|
if(this.list(path).folders.some(f=>f.name===name)) return T("exExists");
|
||||||
|
pending.add(join([...path, name]));
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
renameFolder(path, oldName, newName){
|
||||||
|
return this.moveFolder([...path, oldName], path, newName);
|
||||||
|
},
|
||||||
|
moveFolder(srcPath, targetPath, newName){
|
||||||
|
const from = join(srcPath);
|
||||||
|
const to = join([...targetPath, newName || srcPath[srcPath.length-1]]);
|
||||||
|
if(from === to) return null;
|
||||||
|
if(ANCHOR_LIB.some(it=>{ const j = join(split(it.folder)); return j === to || j.startsWith(to + "/"); }) || pending.has(to)) return T("exExists");
|
||||||
|
ANCHOR_LIB.forEach(it=>{
|
||||||
|
const j = join(split(it.folder));
|
||||||
|
if(j === from || j.startsWith(from + "/")) it.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(ANCHOR_LIB.some(it=>{ const j = join(split(it.folder)); return j === full || j.startsWith(full + "/"); })) return T("exFolderNotEmpty");
|
||||||
|
[...pending].forEach(j=>{ if(j === full || j.startsWith(full + "/")) pending.delete(j); });
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
renameItem(i, name){
|
||||||
|
if(!ANCHOR_LIB[i]) return null;
|
||||||
|
ANCHOR_LIB[i].name = name;
|
||||||
|
changed();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
moveItem(i, targetPath){
|
||||||
|
const it = ANCHOR_LIB[i];
|
||||||
|
if(!it) return null;
|
||||||
|
if(targetPath.length) it.folder = join(targetPath); else delete it.folder;
|
||||||
|
pending.delete(join(targetPath));
|
||||||
|
changed();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
deleteItem(i){
|
||||||
|
if(!ANCHOR_LIB[i]) return null;
|
||||||
|
ANCHOR_LIB.splice(i, 1);
|
||||||
|
changed();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
openItem(i){
|
||||||
|
const it = ANCHOR_LIB[i];
|
||||||
|
return it ? onOpen(it) : null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/* koppelstuk voor de gedeelde bibliotheek: diagrammen als {name, data} */
|
||||||
|
function anchorLibrarySharedHooks(onOpen){
|
||||||
|
const split = n => (n||"").split("/").map(s=>s.trim()).filter(Boolean);
|
||||||
|
const join = p => p.join("/");
|
||||||
|
return {
|
||||||
|
exportItem(i){
|
||||||
|
const it = ANCHOR_LIB[i];
|
||||||
|
return it ? { name: it.name, data: { cells: structuredClone(it.cells||{}), pics: structuredClone(it.pics||{}) } } : null;
|
||||||
|
},
|
||||||
|
exportFolder(path){
|
||||||
|
const p = join(path);
|
||||||
|
const out = [];
|
||||||
|
ANCHOR_LIB.forEach(it=>{
|
||||||
|
const j = join(split(it.folder));
|
||||||
|
if(j === p || j.startsWith(p + "/")){
|
||||||
|
const rel = j === p ? [] : j.slice(p.length + 1).split("/");
|
||||||
|
out.push({ name: it.name, rel, data: { cells: structuredClone(it.cells||{}), pics: structuredClone(it.pics||{}) } });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
copyIn(item, destPath){
|
||||||
|
const entry = {
|
||||||
|
id: "al-"+Math.random().toString(36).slice(2,10),
|
||||||
|
name: item.name,
|
||||||
|
cells: structuredClone(item.data.cells || {}),
|
||||||
|
pics: structuredClone(item.data.pics || {}),
|
||||||
|
};
|
||||||
|
if(destPath.length) entry.folder = join(destPath);
|
||||||
|
ANCHOR_LIB.push(entry);
|
||||||
|
scheduleSave("anchors");
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
openShared(item){
|
||||||
|
return onOpen({ name: item.name, cells: item.data.cells || {}, pics: item.data.pics || {} });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue