Files

334 lines
14 KiB
JavaScript
Raw Permalink Normal View History

let nodes = [], edges = [], selId = null, dragId = null, dOffX, dOffY;
let isPan = false, pSX, pSY, pOffX = 0, pOffY = 0, zoom = 1;
let connFrom = null, tmpLine = null, nIdCnt = 0;
let undoStk = [], redoStk = [];
const wrap = document.getElementById('wrap'), vp = document.getElementById('viewport');
const cv = document.getElementById('canvas'), svg = document.getElementById('svg');
const props = document.getElementById('props');
function sn(v) { return Math.round(v / 20) * 20; }
function buildPalette() {
const p = document.getElementById('palette');
CATS.forEach(([cat, types]) => {
p.innerHTML += `<div class="cat">${cat}</div>`;
types.forEach(t => {
const info = NODE_TYPES[t];
p.innerHTML += `<div class="p-item" draggable="true" data-type="${t}">
<span class="p-icon" style="background:${info.color}33;color:${info.color}">${t.split('-')[0].substring(0,2).toUpperCase()}</span>
${info.label}</div>`;
});
});
document.querySelectorAll('.p-item').forEach(el => {
el.addEventListener('dragstart', e => { e.dataTransfer.setData('text/plain', el.dataset.type); });
});
}
function addNode(type, x, y, id, label) {
const def = NODE_TYPES[type]; if (!def) return null;
const nid = id || (++nIdCnt);
const el = document.createElement('div');
el.className = 'node'; el.dataset.id = nid;
el.style.cssText = `left:${sn(x)}px;top:${sn(y)}px;width:130px;border-color:${def.color}`;
el.innerHTML = `<div class="rm" onclick="rmNode(${nid})">×</div>
<div class="port port-in" data-n="${nid}"></div>
<div class="nl">${label||def.label}</div>
<div class="ns">${def.label}</div>
<div class="port port-out" data-n="${nid}"></div>`;
el.addEventListener('mousedown', e => {
if (e.target.closest('.rm,.port')) return;
e.stopPropagation(); selNode(nid);
const r = el.getBoundingClientRect();
dragId = nid; dOffX = e.clientX - r.left; dOffY = e.clientY - r.top;
el.style.zIndex = 20; el.style.cursor = 'grabbing';
saveSt();
});
el.querySelectorAll('.port').forEach(p => {
p.addEventListener('mousedown', e => {
e.stopPropagation(); e.preventDefault();
if (p.classList.contains('port-out')) {
connFrom = { id: +p.dataset.n, el: p };
const r = p.getBoundingClientRect(), wr = vp.getBoundingClientRect();
tmpLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
tmpLine.setAttribute('stroke', '#58a6ff'); tmpLine.setAttribute('stroke-width', '2');
tmpLine.setAttribute('stroke-dasharray', '6,4');
tmpLine.setAttribute('x1', (r.left - wr.left) / zoom);
tmpLine.setAttribute('y1', (r.top - wr.top + 4) / zoom);
tmpLine.setAttribute('x2', (e.clientX - wr.left) / zoom);
tmpLine.setAttribute('y2', (e.clientY - wr.top) / zoom);
svg.appendChild(tmpLine);
}
});
});
cv.appendChild(el);
nodes.push({ id: nid, type, x: sn(x), y: sn(y), label: label || def.label });
updStats(); return nid;
}
function rmNode(id) {
const el = cv.querySelector(`.node[data-id="${id}"]`);
if (el) el.remove();
nodes = nodes.filter(n => n.id !== id);
edges = edges.filter(e => e.from !== id && e.to !== id);
renEdges(); updStats(); if (selId === id) { selId = null; props.classList.remove('open'); }
saveSt();
}
function selNode(id) {
cv.querySelectorAll('.node').forEach(n => n.classList.remove('selected'));
const el = cv.querySelector(`.node[data-id="${id}"]`);
if (el) el.classList.add('selected');
selId = id; showProps(id);
}
function showProps(id) {
const n = nodes.find(x => x.id === id); if (!n) return;
props.classList.add('open');
document.getElementById('pLabel').value = n.label;
document.getElementById('pSub').value = n.sub || '';
document.getElementById('pWidth').value = 130;
const sel = document.getElementById('pType');
sel.innerHTML = '';
Object.keys(NODE_TYPES).forEach(k => {
const o = document.createElement('option');
o.value = k; o.textContent = NODE_TYPES[k].label;
if (k === n.type) o.selected = true;
sel.appendChild(o);
});
}
function up(f, v) {
const n = nodes.find(x => x.id === selId); if (!n) return;
const el = cv.querySelector(`.node[data-id="${selId}"]`); if (!el) return;
if (f === 'label') { n.label = v; el.querySelector('.nl').textContent = v; }
else if (f === 'type') {
const def = NODE_TYPES[v]; if (!def) return;
n.type = v; n.label = def.label;
el.style.borderColor = def.color;
el.querySelector('.nl').textContent = def.label;
el.querySelector('.ns').textContent = def.label;
document.getElementById('pLabel').value = def.label;
}
else if (f === 'w') { el.style.width = v + 'px'; }
renEdges(); saveSt();
}
function delNode() { if (selId !== null) rmNode(selId); }
function addEdge(from, to) {
if (from === to || edges.some(e => e.from === from && e.to === to)) return;
edges.push({ from, to }); renEdges(); updStats(); saveSt();
}
function renEdges() {
svg.querySelectorAll('.eg').forEach(g => g.remove());
const wr = vp.getBoundingClientRect();
edges.forEach((e, i) => {
const fEl = cv.querySelector(`.node[data-id="${e.from}"]`);
const tEl = cv.querySelector(`.node[data-id="${e.to}"]`);
if (!fEl || !tEl) return;
const fr = fEl.getBoundingClientRect(), tr = tEl.getBoundingClientRect();
const x1 = (fr.right - wr.left - 8) / zoom, y1 = (fr.top + fr.height/2 - wr.top) / zoom;
const x2 = (tr.left - wr.left + 8) / zoom, y2 = (tr.top + tr.height/2 - wr.top) / zoom;
const cp = Math.max(40, Math.abs(x2 - x1) * 0.4);
const d = `M${x1} ${y1} C${x1+cp} ${y1},${x2-cp} ${y2},${x2} ${y2}`;
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.classList.add('eg');
// glow
const gl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
gl.setAttribute('d', d); gl.setAttribute('stroke', '#58a6ff');
gl.setAttribute('stroke-width', '5'); gl.setAttribute('fill', 'none');
gl.setAttribute('opacity', '0.1');
g.appendChild(gl);
// main
const m = document.createElementNS('http://www.w3.org/2000/svg', 'path');
m.setAttribute('d', d); m.setAttribute('stroke', '#58a6ff');
m.setAttribute('stroke-width', '2'); m.setAttribute('fill', 'none');
m.setAttribute('stroke-linecap', 'round');
m.addEventListener('dblclick', () => { edges.splice(i,1); renEdges(); updStats(); saveSt(); });
g.appendChild(m);
// pulse
for (let j = 0; j < 2; j++) {
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
p.setAttribute('d', d); p.setAttribute('stroke', '#79c0ff');
p.setAttribute('stroke-width', '1.5'); p.setAttribute('fill', 'none');
p.setAttribute('stroke-dasharray', '6,14');
p.style.animation = `pulseF ${0.8 + j*0.1}s linear infinite`;
p.style.animationDelay = `-${j * 0.4}s`;
g.appendChild(p);
}
// arrow
const ang = Math.atan2(y2 - y1, x2 - x1);
const ax = x2 - 4, ay = y2 - 2;
const ar = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
ar.setAttribute('points', `${ax},${ay} ${ax-8*Math.cos(ang-.4)},${ay-8*Math.sin(ang-.4)} ${ax-8*Math.cos(ang+.4)},${ay-8*Math.sin(ang+.4)}`);
ar.setAttribute('fill', '#58a6ff');
g.appendChild(ar);
svg.appendChild(g);
});
}
document.addEventListener('mousemove', e => {
if (dragId !== null) {
const el = cv.querySelector(`.node[data-id="${dragId}"]`); if (!el) return;
const wr = vp.getBoundingClientRect();
let x = (e.clientX - wr.left - dOffX) / zoom, y = (e.clientY - wr.top - dOffY) / zoom;
x = sn(Math.max(0,x)); y = sn(Math.max(0,y));
el.style.left = x + 'px'; el.style.top = y + 'px';
const n = nodes.find(nn => nn.id === dragId); if (n) { n.x = x; n.y = y; }
renEdges();
}
if (tmpLine) {
const wr = vp.getBoundingClientRect();
tmpLine.setAttribute('x2', (e.clientX - wr.left)/zoom);
tmpLine.setAttribute('y2', (e.clientY - wr.top)/zoom);
}
if (isPan) {
pOffX += e.clientX - pSX; pOffY += e.clientY - pSY;
pSX = e.clientX; pSY = e.clientY;
vp.style.transform = `translate(${pOffX}px,${pOffY}px) scale(${zoom})`;
}
});
document.addEventListener('mouseup', e => {
if (dragId !== null) {
const el = cv.querySelector(`.node[data-id="${dragId}"]`);
if (el) el.style.cursor = 'move';
dragId = null;
}
if (tmpLine) {
tmpLine.remove(); tmpLine = null;
const t = e.target.closest('.port-in');
if (t && connFrom) addEdge(connFrom.id, +t.dataset.n);
connFrom = null;
}
if (isPan) { isPan = false; wrap.style.cursor = 'grab'; }
});
wrap.addEventListener('mousedown', e => {
if (e.target.closest('.node')) return;
cv.querySelectorAll('.node').forEach(n => n.classList.remove('selected'));
selId = null; props.classList.remove('open');
isPan = true; pSX = e.clientX; pSY = e.clientY; wrap.style.cursor = 'grabbing';
});
wrap.addEventListener('wheel', e => {
e.preventDefault();
const d = e.deltaY > 0 ? 0.9 : 1.1, nz = Math.max(0.2, Math.min(3, zoom * d));
const wr = wrap.getBoundingClientRect();
const vx = (e.clientX - wr.left - pOffX) / zoom, vy = (e.clientY - wr.top - pOffY) / zoom;
zoom = nz;
pOffX = e.clientX - wr.left - vx * zoom; pOffY = e.clientY - wr.top - vy * zoom;
vp.style.transform = `translate(${pOffX}px,${pOffY}px) scale(${zoom})`;
document.getElementById('zoom').textContent = Math.round(zoom*100) + '%';
}, { passive: false });
cv.addEventListener('dragover', e => e.preventDefault());
cv.addEventListener('drop', e => {
e.preventDefault();
const type = e.dataTransfer.getData('text/plain');
if (!type || !NODE_TYPES[type]) return;
const wr = vp.getBoundingClientRect();
let x = (e.clientX - wr.left)/zoom - 65, y = (e.clientY - wr.top)/zoom - 20;
addNode(type, sn(Math.max(0,x)), sn(Math.max(0,y)));
saveSt();
});
function saveSt() {
undoStk.push(JSON.stringify({ nodes: nodes.map(n => ({...n})), edges: [...edges] }));
if (undoStk.length > 50) undoStk.shift();
redoStk = [];
}
function undo() {
if (undoStk.length < 2) return;
redoStk.push(undoStk.pop());
loadSt(JSON.parse(undoStk[undoStk.length-1]));
}
function redo() {
if (!redoStk.length) return;
undoStk.push(redoStk.pop());
loadSt(JSON.parse(undoStk[undoStk.length-1]));
}
function loadSt(st) {
cv.querySelectorAll('.node').forEach(el => el.remove());
svg.querySelectorAll('.eg').forEach(g => g.remove());
nodes = []; edges = st.edges || [];
(st.nodes || []).forEach(n => addNode(n.type, n.x, n.y, n.id, n.label));
renEdges(); updStats();
}
function updStats() {
document.getElementById('stats').textContent = `${nodes.length} componenten · ${edges.length} connecties`;
}
function fit() {
if (!nodes.length) return;
const mx = Math.min(...nodes.map(n => n.x)), my = Math.min(...nodes.map(n => n.y));
const Mx = Math.max(...nodes.map(n => n.x + 130)), My = Math.max(...nodes.map(n => n.y + 44));
const wr = wrap.getBoundingClientRect();
zoom = Math.max(0.2, Math.min(wr.width/(Mx-mx+150), (wr.height-60)/(My-my+80), 1.5));
pOffX = -mx * zoom + 50; pOffY = -my * zoom + 30;
vp.style.transform = `translate(${pOffX}px,${pOffY}px) scale(${zoom})`;
document.getElementById('zoom').textContent = Math.round(zoom*100)+'%';
}
function filterPalette(q) {
q = q.toLowerCase();
document.querySelectorAll('.p-item').forEach(el => el.style.display = el.textContent.toLowerCase().includes(q) ? 'flex' : 'none');
document.querySelectorAll('.cat').forEach(el => {
let sib = el.nextElementSibling, has = false;
while (sib && sib.classList.contains('p-item')) { if (sib.style.display !== 'none') has = true; sib = sib.nextElementSibling; }
el.style.display = has ? 'block' : 'none';
});
}
async function save() {
const name = document.getElementById('diagramName').value;
const client_id = document.getElementById('clientSelect').value;
if (!name || !client_id) { setStatus('Naam en cliënt verplicht', 'err'); return; }
const data = { id: DIAGRAM_ID || null, client_id: +client_id, name,
nodes: nodes.map(n => ({...n})), edges: [...edges] };
try {
const r = await fetch('/python-editor/api/save', {
method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data)
});
const d = await r.json();
setStatus('Opgeslagen ✓', 'ok');
if (d.id && !DIAGRAM_ID) history.replaceState(null, '', '/python-editor/?id=' + d.id);
} catch(e) { setStatus('Fout bij opslaan', 'err'); }
}
function setStatus(msg, t) {
const el = document.getElementById('status');
el.textContent = msg; el.style.color = t === 'err' ? '#f85149' : '#3fb950';
setTimeout(() => { el.textContent = ''; }, 3000);
}
document.addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
if (e.key === 'Delete' || e.key === 'Backspace') { delNode(); e.preventDefault(); }
if ((e.ctrlKey||e.metaKey) && e.key === 'z') { e.shiftKey ? redo() : undo(); e.preventDefault(); }
if ((e.ctrlKey||e.metaKey) && e.key === 's') { save(); e.preventDefault(); }
});
const style = document.createElement('style');
style.textContent = `@keyframes pulseF{0%{stroke-dashoffset:20}100%{stroke-dashoffset:0}}`;
document.head.appendChild(style);
buildPalette();
fetch('/python-editor/api/clients').then(r=>r.json()).then(clients => {
const sel = document.getElementById('clientSelect');
clients.forEach(c => { sel.innerHTML += `<option value="${c.id}">${c.name}</option>`; });
});
if (NODES_JSON && NODES_JSON.length) {
NODES_JSON.forEach(n => addNode(n.type, n.x, n.y, n.id, n.label));
edges = EDGES_JSON || [];
renEdges(); updStats(); saveSt();
setTimeout(fit, 100);
}
nIdCnt = nodes.length;
updStats(); saveSt();