MediaWiki:MindMap.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
(function(){
function initMap(container){
var stage=container.querySelector('.mindmap-stage'); if(!stage)return;
var links=container.querySelector('.mm-links'); var detail=container.querySelector('.mm-detail');
var scale=1, tx=0, ty=0, panning=false, px=0, py=0;
var rafDraw=0;
function clamp(n,min,max){ return Math.min(max,Math.max(min,n)); }
function scheduleDraw(){
if(rafDraw) return;
rafDraw=window.requestAnimationFrame(function(){ rafDraw=0; drawLines(); });
}
function applyTransform(){ stage.style.transform='translate('+tx+'px,'+ty+'px) scale('+scale+')'; scheduleDraw(); }
function layoutBranches(){
if(container.clientWidth===0 || container.clientHeight===0) return;
var pad=24;
var gapX=140;
var gapY=34;
var center=stage.querySelector('.mm-center');
if(center){
center.style.left=pad+'px';
center.style.top=(container.clientHeight/2)+'px';
center.style.transform='translate(0, -50%)';
}
var cw=center ? center.offsetWidth : 220;
var cx=pad + cw + gapX;
var y=pad;
var branches=stage.querySelectorAll('.mm-branch');
branches.forEach(function(b){
b.style.left=cx+'px';
b.style.top=y+'px';
y+=b.offsetHeight+gapY;
});
scheduleDraw();
}
function zoomAt(clientX, clientY, nextScale){
var rect=container.getBoundingClientRect();
var mx=clientX-rect.left;
var my=clientY-rect.top;
var prev=scale;
var ns=clamp(nextScale,0.6,2.2);
if(ns===prev) return;
var wx=(mx-tx)/prev;
var wy=(my-ty)/prev;
scale=ns;
tx=mx-wx*scale;
ty=my-wy*scale;
applyTransform();
}
function centerOnEl(node){
if(!node)return;
var nr=node.getBoundingClientRect();
var cr=container.getBoundingClientRect();
var dx=(cr.left+cr.width/2)-(nr.left+nr.width/2);
var dy=(cr.top+cr.height/2)-(nr.top+nr.height/2);
tx+=dx;
ty+=dy;
applyTransform();
}
function showDetailForNode(node){
if(!detail || !node) return;
var d=node.getAttribute('data-detail');
if(!d){
var branch=node.parentElement && node.parentElement.classList && node.parentElement.classList.contains('mm-branch') ? node.parentElement : null;
if(branch){
var kids=branch.querySelectorAll(':scope > .mm-children > .mm-node, :scope > .mm-children > .mm-item > .mm-node');
if(kids && kids.length){
var labels=[];
kids.forEach(function(k){ labels.push((k.textContent||'').trim()); });
d=((node.textContent||'').trim())+' — '+labels.join(', ');
}
}
}
if(d){ detail.textContent=d; detail.classList.add('active'); } else { detail.classList.remove('active'); }
}
function drawLines(){
var svgW=container.clientWidth, svgH=container.clientHeight;
if(svgW===0 || svgH===0) return;
links.setAttribute('width',svgW); links.setAttribute('height',svgH);
links.innerHTML='';
function addCurve(a,b){
var ar=a.getBoundingClientRect(), br=b.getBoundingClientRect(), cr=container.getBoundingClientRect();
var x1=ar.right-cr.left;
var y1=(ar.top-cr.top)+ar.height/2;
var x2=br.left-cr.left;
var y2=(br.top-cr.top)+br.height/2;
var dist=Math.abs(x2-x1);
var dx=Math.max(40, Math.min(180, dist*0.45));
var c1x=x1+dx;
var c2x=x2-dx;
var path=document.createElementNS('http://www.w3.org/2000/svg','path');
path.setAttribute('d','M '+x1+' '+y1+' C '+c1x+' '+y1+', '+c2x+' '+y2+', '+x2+' '+y2);
path.setAttribute('fill','none');
path.setAttribute('stroke','rgba(255,107,157,.35)');
path.setAttribute('stroke-width','2');
links.appendChild(path);
}
var center=stage.querySelector('.mm-center');
if(!center) return;
var branches=stage.querySelectorAll('.mm-branch');
branches.forEach(function(b){
var head=b.querySelector(':scope > .mm-node');
if(!head) return;
addCurve(center,head);
if(b.classList.contains('open')){
var directChildren=b.querySelectorAll(':scope > .mm-children > .mm-node, :scope > .mm-children > .mm-item > .mm-node');
directChildren.forEach(function(c){ addCurve(head,c); });
var openItems=b.querySelectorAll(':scope > .mm-children > .mm-item.open');
openItems.forEach(function(item){
var itemNode=item.querySelector(':scope > .mm-node');
if(!itemNode) return;
var subs=item.querySelectorAll(':scope > .mm-subchildren > .mm-node');
subs.forEach(function(s){ addCurve(itemNode,s); });
});
}
});
}
var down=false, dragging=false, downId=null, startX=0, startY=0;
container.addEventListener('pointerdown',function(e){
if(e.button!==0) return;
if(e.target.closest('.mm-controls')) return;
down=true;
dragging=false;
downId=e.pointerId;
startX=e.clientX;
startY=e.clientY;
px=e.clientX;
py=e.clientY;
});
container.addEventListener('pointermove',function(e){
if(!down || downId!==e.pointerId) return;
var dx=e.clientX-px, dy=e.clientY-py;
px=e.clientX;
py=e.clientY;
if(!dragging){
var mx=e.clientX-startX, my=e.clientY-startY;
if(Math.abs(mx)+Math.abs(my) < 6) return;
dragging=true;
container.classList.add('mm-dragging');
try{ container.setPointerCapture(e.pointerId); }catch(err){}
}
tx+=dx;
ty+=dy;
applyTransform();
});
function endPointer(e){
if(downId!==e.pointerId) return;
down=false;
downId=null;
if(dragging){
dragging=false;
container.classList.remove('mm-dragging');
try{ container.releasePointerCapture(e.pointerId); }catch(err){}
}
}
container.addEventListener('pointerup',endPointer);
container.addEventListener('pointercancel',endPointer);
container.addEventListener('wheel',function(e){
e.preventDefault();
var next=scale+(e.deltaY<0?0.12:-0.12);
zoomAt(e.clientX,e.clientY,next);
},{passive:false});
var btnIn=container.querySelector('[data-mm-zoom="in"]');
var btnOut=container.querySelector('[data-mm-zoom="out"]');
var btnReset=container.querySelector('[data-mm-reset]');
function zoomAtCenter(next){
var cr=container.getBoundingClientRect();
zoomAt(cr.left+cr.width/2, cr.top+cr.height/2, next);
}
if(btnIn)btnIn.addEventListener('click',function(){ zoomAtCenter(scale+0.15); });
if(btnOut)btnOut.addEventListener('click',function(){ zoomAtCenter(scale-0.15); });
if(btnReset)btnReset.addEventListener('click',function(){ scale=1; tx=0; ty=0; applyTransform(); });
stage.querySelectorAll('.mm-node').forEach(function(n){
n.addEventListener('click',function(){
showDetailForNode(n);
});
});
stage.querySelectorAll('.mm-branch > .mm-node').forEach(function(head){
head.addEventListener('click',function(){
var branch=head.parentElement;
if(branch.classList.contains('open')){ branch.classList.remove('open'); } else { branch.classList.add('open'); }
layoutBranches();
scheduleDraw();
});
});
stage.addEventListener('click',function(e){
var node=e.target.closest('.mm-item > .mm-node');
if(!node) return;
var item=node.parentElement;
if(!item) return;
if(item.classList.contains('open')){ item.classList.remove('open'); } else { item.classList.add('open'); }
layoutBranches();
scheduleDraw();
});
window.addEventListener('resize', function(){ layoutBranches(); drawLines(); });
layoutBranches();
applyTransform();
container.__mmApi={
layout: layoutBranches,
draw: drawLines,
showDetail: showDetailForNode
};
}
function decorateLinkNodes(root){
(root||document).querySelectorAll('.mm-node[data-href]').forEach(function(node){
if(node.querySelector('a')) return;
var href=node.getAttribute('data-href');
if(!href) return;
var label=(node.textContent||'').trim();
node.textContent='';
var a=document.createElement('a');
a.href=href;
a.target='_blank';
a.rel='noopener';
a.textContent=label;
node.appendChild(a);
});
document.addEventListener('click', function(e){
var a=e.target.closest('.mm-node a');
if(!a) return;
e.stopPropagation();
}, true);
}
function renderLocationsTables(){
var root=document.getElementById('locations-table-root');
var stage=document.getElementById('mm-stage-locations');
if(!root || !stage) return;
var frag=document.createDocumentFragment();
var branches=stage.querySelectorAll('.mm-branch');
branches.forEach(function(branch){
var branchHead=branch.querySelector(':scope > .mm-node');
var branchTitle=(branchHead && branchHead.textContent ? branchHead.textContent : '').trim();
var theme=branch.id ? branch.id.replace(/^mm-locations-/, '') : 'default';
var card=document.createElement('section');
card.className='mm-table-card mm-table-card--'+theme;
var h=document.createElement('h3');
h.className='mm-table-title';
h.textContent=branchTitle || 'Locations';
card.appendChild(h);
var table=document.createElement('table');
table.className='mm-table';
table.setAttribute('data-theme', theme);
var thead=document.createElement('thead');
thead.innerHTML='<tr><th>Town</th><th>Location(s)</th></tr>';
table.appendChild(thead);
var tbody=document.createElement('tbody');
var items=branch.querySelectorAll(':scope > .mm-children > .mm-item');
items.forEach(function(item){
var townNode=item.querySelector(':scope > .mm-node');
var town=(townNode && townNode.textContent ? townNode.textContent : '').trim();
if(!town) return;
var tr=document.createElement('tr');
var tdTown=document.createElement('td');
tdTown.className='mm-td-town';
tdTown.textContent=town;
tr.appendChild(tdTown);
var tdLoc=document.createElement('td');
tdLoc.className='mm-td-locations';
var subs=item.querySelectorAll(':scope > .mm-subchildren > .mm-node');
if(subs && subs.length){
var ul=document.createElement('ul');
ul.className='mm-list';
subs.forEach(function(s){
var li=document.createElement('li');
li.textContent=((s.textContent||'').trim());
ul.appendChild(li);
});
tdLoc.appendChild(ul);
}else{
tdLoc.textContent='—';
}
tr.appendChild(tdLoc);
tbody.appendChild(tr);
});
table.appendChild(tbody);
card.appendChild(table);
frag.appendChild(card);
});
root.textContent='';
root.appendChild(frag);
}
function parseDetailTriplet(detail){
var raw=(detail||'').trim();
if(!raw) return { left:'', mid:'', right:'' };
var parts=raw.split(/\s*—\s*/g).map(function(s){ return s.trim(); }).filter(Boolean);
return {
left: parts[0] || '',
mid: parts[1] || '',
right: parts.slice(2).join(' — ')
};
}
function initServicesTable(){
var root=document.getElementById('services-table-root');
var stage=document.getElementById('mm-stage-services');
if(!root || !stage) return;
var rows=[];
stage.querySelectorAll('.mm-branch').forEach(function(branch){
var head=branch.querySelector(':scope > .mm-node');
var category=((head && head.textContent)||'').trim();
var children=branch.querySelectorAll(':scope > .mm-children > .mm-node');
children.forEach(function(n){
var name=((n.textContent)||'').trim();
var href=n.getAttribute('data-href') || '';
var d=parseDetailTriplet(n.getAttribute('data-detail')||'');
rows.push({
category: category,
name: name,
npc: d.mid,
purpose: d.right,
href: href,
detail: (n.getAttribute('data-detail')||'').trim()
});
});
});
root.innerHTML='';
var controls=document.createElement('div');
controls.className='mm-table-controls';
controls.innerHTML='\
<div class="mm-table-control">\
<label class="mm-table-label" for="services-table-search">Search</label>\
<input id="services-table-search" class="mm-table-input" type="search" placeholder="Type to filter…" autocomplete="off">\
</div>\
<div class="mm-table-control">\
<label class="mm-table-label" for="services-table-category">Category</label>\
<select id="services-table-category" class="mm-table-select">\
<option value="">All</option>\
</select>\
</div>\
<div class="mm-table-control mm-table-meta" id="services-table-meta"></div>';
root.appendChild(controls);
var tableWrap=document.createElement('div');
tableWrap.className='mm-table-wrap';
root.appendChild(tableWrap);
var table=document.createElement('table');
table.className='mm-table mm-table--enhanced';
table.innerHTML='\
<thead>\
<tr>\
<th scope="col" data-key="category">Category</th>\
<th scope="col" data-key="name">Function</th>\
<th scope="col" data-key="npc">Replaced NPC</th>\
<th scope="col" data-key="purpose">What\'s It For</th>\
<th scope="col" data-key="link">Link</th>\
</tr>\
</thead>\
<tbody></tbody>';
tableWrap.appendChild(table);
var tbody=table.querySelector('tbody');
var searchEl=root.querySelector('#services-table-search');
var categoryEl=root.querySelector('#services-table-category');
var metaEl=root.querySelector('#services-table-meta');
var sortKey='category';
var sortDir='asc';
function normalize(s){ return (s||'').toLowerCase(); }
function compare(a,b){
var av=normalize(a), bv=normalize(b);
if(av<bv) return -1;
if(av>bv) return 1;
return 0;
}
function apply(){
var q=normalize(searchEl && searchEl.value);
var cat=(categoryEl && categoryEl.value) || '';
var filtered=rows.filter(function(r){
if(cat && r.category!==cat) return false;
if(!q) return true;
var blob=(r.category+' '+r.name+' '+r.npc+' '+r.purpose+' '+r.detail).toLowerCase();
return blob.indexOf(q)!==-1;
});
filtered.sort(function(a,b){
var ka=(sortKey==='link'? (a.href? a.name : '') : a[sortKey]) || '';
var kb=(sortKey==='link'? (b.href? b.name : '') : b[sortKey]) || '';
var c=compare(ka,kb);
return sortDir==='asc' ? c : -c;
});
tbody.textContent='';
filtered.forEach(function(r){
var tr=document.createElement('tr');
var tdCat=document.createElement('td');
tdCat.className='mm-td-category';
tdCat.textContent=r.category;
tr.appendChild(tdCat);
var tdName=document.createElement('td');
tdName.className='mm-td-name';
tdName.textContent=r.name;
if(r.detail) tdName.title=r.detail;
tr.appendChild(tdName);
var tdNpc=document.createElement('td');
tdNpc.textContent=r.npc || '—';
tr.appendChild(tdNpc);
var tdPurpose=document.createElement('td');
tdPurpose.textContent=r.purpose || '—';
if(r.detail) tdPurpose.title=r.detail;
tr.appendChild(tdPurpose);
var tdLink=document.createElement('td');
if(r.href){
var a=document.createElement('a');
a.href=r.href;
a.target='_blank';
a.rel='noopener';
a.textContent='Open';
tdLink.appendChild(a);
}else{
tdLink.textContent='—';
}
tr.appendChild(tdLink);
tbody.appendChild(tr);
});
if(metaEl) metaEl.textContent=filtered.length+' / '+rows.length;
}
var categories={};
rows.forEach(function(r){ categories[r.category]=true; });
Object.keys(categories).sort().forEach(function(c){
var opt=document.createElement('option');
opt.value=c;
opt.textContent=c;
categoryEl.appendChild(opt);
});
table.querySelectorAll('th[data-key]').forEach(function(th){
th.tabIndex=0;
th.addEventListener('click', function(){
var k=th.getAttribute('data-key');
if(!k) return;
if(sortKey===k){ sortDir = sortDir==='asc' ? 'desc' : 'asc'; }
else { sortKey=k; sortDir='asc'; }
apply();
});
th.addEventListener('keydown', function(e){
if(e.key==='Enter' || e.key===' '){ e.preventDefault(); th.click(); }
});
});
if(searchEl) searchEl.addEventListener('input', apply);
if(categoryEl) categoryEl.addEventListener('change', apply);
apply();
}
function refreshMapsIn(root){
(root||document).querySelectorAll('.mindmap').forEach(function(m){
if(m.__mmApi){
if(m.offsetParent===null) return;
m.__mmApi.layout();
m.__mmApi.draw();
}
});
}
function initAll(){
decorateLinkNodes(document);
document.querySelectorAll('.mindmap').forEach(function(m){ initMap(m); });
renderLocationsTables();
initServicesTable();
refreshMapsIn(document);
}
if(document.readyState==='loading'){
document.addEventListener('DOMContentLoaded', initAll);
}else{
initAll();
}
document.addEventListener('click',function(e){
var tabBtn=e.target.closest('.nav-tab');
if(!tabBtn) return;
var tabId=tabBtn.getAttribute('data-tab');
setTimeout(function(){
if(tabId){
var tab=document.getElementById(tabId);
if(tab) refreshMapsIn(tab);
}else{
refreshMapsIn(document);
}
},250);
});
document.addEventListener('click',function(e){
var nested=e.target.closest('.nested-tab');
if(!nested) return;
var id=nested.getAttribute('data-tab');
setTimeout(function(){
if(id){
var el=document.getElementById(id);
if(el) refreshMapsIn(el);
}else{
refreshMapsIn(document);
}
},250);
});
document.querySelectorAll('.destaque-card').forEach(function(card){
var target=card.getAttribute('data-node-target'); var tabBtn=card.getAttribute('data-tab-button'); var tabId=card.getAttribute('data-tab-trigger');
if(target){ card.addEventListener('click',function(){
var btn=document.getElementById(tabBtn); if(btn)btn.click();
setTimeout(function(){
var targetEl=document.getElementById(target);
if(!targetEl) return;
var node=targetEl;
if(targetEl.classList.contains('mm-branch')){
node=targetEl.querySelector(':scope > .mm-node');
if(node && !targetEl.classList.contains('open')) targetEl.classList.add('open');
}
if(!node) return;
var mindmap=node.closest('.mindmap');
var api=mindmap && mindmap.__mmApi ? mindmap.__mmApi : null;
var branch=node.closest('.mm-branch');
var isHead=branch && branch.querySelector(':scope > .mm-node')===node;
var inChildren=node.parentElement && node.parentElement.classList && node.parentElement.classList.contains('mm-children');
if(branch && (inChildren || isHead) && !branch.classList.contains('open')) branch.classList.add('open');
if(api){ api.layout(); api.draw(); api.showDetail(node); }
},200);
}); }
});
})();