Files
mek-tech-consulting/python-editor/static/editor.js
T

334 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();