/* widget: Mindmap */
/* =========================================================
Mindmap widget — spider, tree or word web
==========================================================*/
function mountMind(root, initState){
const S = { layout:"spider", nodes:[{id:1, x:0.5, y:0.45, t:"", parent:null}], nextId:2 };
if(initState){
S.layout = initState.layout || "spider";
if(Array.isArray(initState.nodes) && initState.nodes.length) S.nodes = initState.nodes;
S.nextId = initState.nextId || (Math.max(...S.nodes.map(n=>n.id))+1);
}
const PALETTE = ["#3b7dd8","#3fae6a","#e0554d","#9b59b6","#f28c38"];
root.innerHTML = `
`;
const mmEl = root.querySelector(".mm");
/* toolbar row wrapper */
const topRow = document.createElement("div");
topRow.className = "mm-top";
mmEl.insertBefore(topRow, mmEl.firstChild);
const mmGrip = document.createElement("span");
mmGrip.className = "mm-grip"; mmGrip.textContent = "⠿";
topRow.appendChild(mmGrip);
topRow.appendChild(root.querySelector(".mm-seg"));
topRow.appendChild(root.querySelector(".mm-hint"));
mmGrip.addEventListener("pointerdown", e=>{
e.preventDefault(); e.stopPropagation();
const mr = mmEl.getBoundingClientRect();
const sc = mr.width / mmEl.offsetWidth; /* effective zoom on this widget */
const ox = e.clientX/sc - topRow.offsetLeft, oy = e.clientY/sc - topRow.offsetTop;
const mv = ev=>{
topRow.style.left = Math.max(0, Math.min(mmEl.offsetWidth - topRow.offsetWidth, ev.clientX/sc - ox))+"px";
topRow.style.top = Math.max(0, Math.min(mmEl.offsetHeight - topRow.offsetHeight, ev.clientY/sc - oy))+"px";
};
const up = ()=>{ document.removeEventListener("pointermove",mv); document.removeEventListener("pointerup",up); };
document.addEventListener("pointermove",mv); document.addEventListener("pointerup",up);
});
const stage = root.querySelector(".mm-stage"),
svg = stage.querySelector("svg");
function nodeById(id){ return S.nodes.find(n=>n.id===id); }
function depth(n){ let d=0, c=n; while(c && c.parent!=null){ c = nodeById(c.parent); d++; } return d; }
function drawLines(){
const w = stage.clientWidth, h = stage.clientHeight;
let paths = "";
S.nodes.forEach(n=>{
if(n.parent==null) return;
const p = nodeById(n.parent);
if(!p) return;
const x1 = p.x*w, y1 = p.y*h, x2 = n.x*w, y2 = n.y*h;
const col = n.lc || PALETTE[(depth(n)-1) % PALETTE.length];
const dash = n.ls==="dash" ? ' stroke-dasharray="11 8"' : n.ls==="dot" ? ' stroke-dasharray="2.5 7"' : "";
if(S.layout==="spider"){
const mx = (x1+x2)/2 + (y2-y1)*0.18, my = (y1+y2)/2 - (x2-x1)*0.18;
paths += ``;
}else if(S.layout==="tree"){
const my = (y1+y2)/2;
paths += ``;
}else{
paths += ``;
}
});
svg.innerHTML = paths;
}
function applyNodeStyle(n, d){
if(n.parent!=null) d.style.borderColor = n.c || PALETTE[(depth(n)-1) % PALETTE.length];
else if(n.c) d.style.borderColor = n.c;
d.style.fontSize = (n.fs || (n.parent==null ? 17 : 15))+"px";
}
function buildNode(n){
const d = document.createElement("div");
d.className = "mm-node" + (n.parent==null ? " root" : "");
applyNodeStyle(n, d);
d.dataset.id = n.id;
const span = document.createElement("span");
span.contentEditable = "true";
span.spellcheck = false;
span.textContent = n.t || (n.parent==null ? T("mmRoot") : "");
span.addEventListener("input", ()=>{ n.t = span.textContent; });
span.addEventListener("keydown", e=>e.stopPropagation());
span.addEventListener("pointerdown", e=>e.stopPropagation());
d.appendChild(span);
const add = document.createElement("button");
add.className = "mm-add"; add.textContent = "+";
add.addEventListener("pointerdown", e=>e.stopPropagation());
add.addEventListener("click", ()=>{
const a = Math.random()*Math.PI*2;
S.nodes.push({
id: S.nextId++, parent: n.id,
x: Math.min(.95, Math.max(.05, n.x + Math.cos(a)*0.17)),
y: Math.min(.92, Math.max(.06, n.y + Math.sin(a)*0.2)),
t: ""
});
render();
});
d.appendChild(add);
const cog = document.createElement("button");
cog.className = "mm-cog"; cog.textContent = "🎨";
cog.addEventListener("pointerdown", e=>e.stopPropagation());
cog.addEventListener("click", ()=>openNodePop(n, d));
d.appendChild(cog);
if(n.parent!=null){
const del = document.createElement("button");
del.className = "mm-del"; del.textContent = "✕";
del.addEventListener("pointerdown", e=>e.stopPropagation());
del.addEventListener("click", ()=>{
const doomed = new Set([n.id]);
let grew = true;
while(grew){
grew = false;
S.nodes.forEach(m=>{
if(m.parent!=null && doomed.has(m.parent) && !doomed.has(m.id)){ doomed.add(m.id); grew = true; }
});
}
S.nodes = S.nodes.filter(m=>!doomed.has(m.id));
render();
});
d.appendChild(del);
}
/* drag */
d.addEventListener("pointerdown", e=>{
if(e.pointerType==="mouse" && e.button!==0) return;
e.preventDefault();
const r = stage.getBoundingClientRect();
const mv = ev=>{
n.x = Math.min(.98, Math.max(.02, (ev.clientX-r.left)/r.width));
n.y = Math.min(.96, Math.max(.04, (ev.clientY-r.top)/r.height));
d.style.left = (n.x*100)+"%";
d.style.top = (n.y*100)+"%";
drawLines();
};
const up = ()=>{ document.removeEventListener("pointermove",mv); document.removeEventListener("pointerup",up); };
document.addEventListener("pointermove",mv);
document.addEventListener("pointerup",up);
});
d.style.left = (n.x*100)+"%";
d.style.top = (n.y*100)+"%";
return d;
}
/* per-node settings popover: colour, font size, line colour & style */
function closeNodePop(){ stage.querySelectorAll(".mm-pop").forEach(x=>x.remove()); }
function openNodePop(n, nodeEl){
closeNodePop();
const pop = document.createElement("div");
pop.className = "mm-pop";
const w = stage.clientWidth, h = stage.clientHeight;
pop.style.left = Math.min(n.x*w + 30, w-190)+"px";
pop.style.top = Math.min(n.y*h + 24, h-150)+"px";
pop.addEventListener("pointerdown", e=>e.stopPropagation());
const restyle = ()=>{ applyNodeStyle(n, nodeEl); drawLines(); refreshPop(); };
const row = (lbl, title)=>{
const r = document.createElement("div");
r.className = "mp-row"; r.title = title;
const l = document.createElement("span");
l.className = "mp-lbl"; l.textContent = lbl;
r.appendChild(l);
pop.appendChild(r);
return r;
};
/* node colour */
const r1 = row("⬤", T("nodeColor"));
PALETTE.concat(["#26344a"]).forEach(c=>{
const b = document.createElement("button");
b.className = "mp-sw"; b.dataset.v = c; b.style.background = c;
b.addEventListener("click", ()=>{ n.c = (n.c===c ? null : c); restyle(); });
r1.appendChild(b);
});
const xBtn = document.createElement("button");
xBtn.className = "mp-x"; xBtn.textContent = "✕";
xBtn.addEventListener("click", closeNodePop);
r1.appendChild(xBtn);
/* font size */
const r2 = row("A", T("nodeFont"));
[["A",13],["A",16],["A",20]].forEach(([txt,fs],i)=>{
const b = document.createElement("button");
b.className = "mp-fs"; b.dataset.v = fs;
b.textContent = txt; b.style.fontSize = (11+i*3)+"px";
b.addEventListener("click", ()=>{ n.fs = fs; restyle(); });
r2.appendChild(b);
});
if(n.parent!=null){
/* line colour */
const r3 = row("╱", T("lineColor"));
PALETTE.concat(["#26344a"]).forEach(c=>{
const b = document.createElement("button");
b.className = "mp-sw"; b.dataset.v = "l"+c; b.style.background = c;
b.addEventListener("click", ()=>{ n.lc = (n.lc===c ? null : c); restyle(); });
r3.appendChild(b);
});
/* line style */
const r4 = row("┄", T("lineStyle"));
[["solid",""],["dash","dash"],["dot","dot"]].forEach(([id,val])=>{
const b = document.createElement("button");
b.className = "mp-ls"; b.dataset.ls = id; b.dataset.v = "s"+id;
b.appendChild(document.createElement("i"));
b.addEventListener("click", ()=>{ n.ls = val || null; restyle(); });
r4.appendChild(b);
});
}
function refreshPop(){
pop.querySelectorAll(".mp-sw").forEach(b=>{
const v = b.dataset.v;
b.classList.toggle("on", v.startsWith("l") ? n.lc===v.slice(1) : n.c===v);
});
pop.querySelectorAll(".mp-fs").forEach(b=>
b.classList.toggle("on", +b.dataset.v === (n.fs || (n.parent==null?17:15))));
pop.querySelectorAll(".mp-ls").forEach(b=>
b.classList.toggle("on", (n.ls||"") === (b.dataset.ls==="solid" ? "" : b.dataset.ls)));
}
refreshPop();
stage.appendChild(pop);
}
document.addEventListener("pointerdown", e=>{
if(!stage.querySelector(".mm-pop")) return;
if(!e.target.closest(".mm-pop") && !e.target.closest(".mm-cog")) closeNodePop();
});
function render(){
mmEl.className = "mm "+S.layout;
root.querySelectorAll(".mm-seg button").forEach(b=>{
b.textContent = T(b.dataset.m==="spider" ? "mmSpider" : b.dataset.m==="tree" ? "mmTree" : "mmWeb");
b.classList.toggle("on", b.dataset.m===S.layout);
});
root.querySelector(".mm-hint").textContent = T("mmHint");
closeNodePop();
stage.querySelectorAll(".mm-node").forEach(x=>x.remove());
S.nodes.forEach(n=>stage.appendChild(buildNode(n)));
drawLines();
}
root.querySelectorAll(".mm-seg button").forEach(b=>{
b.addEventListener("click", ()=>{ S.layout = b.dataset.m; render(); });
});
new ResizeObserver(drawLines).observe(stage);
document.addEventListener("langchange", render);
render();
return {
getState: ()=>({ layout:S.layout, nodes:S.nodes, nextId:S.nextId }),
setState: st=>{ if(st && st.nodes){ S.layout=st.layout||"spider"; S.nodes=st.nodes; S.nextId=st.nextId||S.nextId; render(); } }
};
}