Loading editor/html/css/editor.css +38 −0 Changes for editor/html/css/editor.css: 38 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1239,6 +1239,40 @@ dialog label { white-space: nowrap; } .survey-select-field { display: flex; gap: 6px; align-items: center; } .survey-select-name { flex: 1; padding: 6px 8px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); border-radius: 4px; font-size: 13px; min-height: 32px; box-sizing: border-box; } .survey-select-btn { padding: 6px 10px; background: var(--accent); color: var(--bg-primary); border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 600; white-space: nowrap; } .survey-select-btn:hover { background: var(--accent-hover); } /* Color Var / Custom Picker */ .color-mode-toggle { display: flex; Loading Loading @@ -1469,11 +1503,15 @@ dialog label { } .sm-q-panel .set-field { flex-direction: column; align-items: stretch; gap: 0.2em; margin-bottom: 0.5em; } .sm-q-panel label { display: block; min-width: 0; font-size: 0.75rem; color: var(--text-secondary); margin-bottom: 0.2em; Loading editor/html/js/properties.js +3 −3 Changes for editor/html/js/properties.js: 3 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -634,16 +634,16 @@ var PropertiesPanel = (function() { surveyHidden.value = value; var surveyWrap = document.createElement('div'); surveyWrap.style.cssText = 'display:flex;gap:6px;align-items:center;'; surveyWrap.className = 'survey-select-field'; var surveyName = document.createElement('span'); surveyName.style.cssText = 'flex:1;padding:6px 8px;border:1px solid #ddd;border-radius:4px;font-size:.9em;color:#555;background:#f9f9f9;min-height:32px;box-sizing:border-box;'; surveyName.className = 'survey-select-name'; surveyName.textContent = value || '(keine Umfrage)'; var surveyBtn = document.createElement('button'); surveyBtn.type = 'button'; surveyBtn.className = 'survey-select-btn'; surveyBtn.textContent = 'Umfragen verwalten'; surveyBtn.style.cssText = 'padding:6px 10px;background:#3b82f6;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:.85em;white-space:nowrap;'; (function(hidden, nameEl) { surveyBtn.addEventListener('click', function() { Loading editor/html/js/survey-manager.js +76 −42 Changes for editor/html/js/survey-manager.js: 76 added lines, 42 removed lines. Original line number Diff line number Diff line Loading @@ -144,36 +144,48 @@ var SurveyManager = (function() { renderCanvas(_layout.map(function(l) { return l.q; })); } function buildTree(questions) { var byId = {}; questions.forEach(function(q) { byId[q.id] = { q: q, children: [] }; }); var roots = []; questions.forEach(function(q) { var pid = q.parent_id; if (pid && byId[pid]) { byId[pid].children.push(byId[q.id]); } else { roots.push(byId[q.id]); } // Questions are always presented to visitors in a single sequential order // (order_idx); the only real branching is the "excludes" skip-logic on // options, drawn separately as side edges in renderCanvas(). function layoutSequential(questions) { _layout = []; var sorted = questions.slice().sort(function(a, b) { return (a.order_idx || 0) - (b.order_idx || 0); }); sorted.forEach(function(q, i) { _layout.push({ q: q, x: 20, y: i * (NODE_H + V_GAP) + 20 }); }); return roots; } function layoutTree(roots) { _layout = []; var col = 0; function place(node, depth) { var x = col * (NODE_W + H_GAP); var y = depth * (NODE_H + V_GAP) + 20; _layout.push({ q: node.q, x: x, y: y }); col++; node.children.forEach(function(child) { place(child, depth + 1); function collectExcludeEdges(byId) { var edges = []; _layout.forEach(function(n) { (n.q.options || []).forEach(function(opt) { if (!opt.excludes) return; opt.excludes.split(',').forEach(function(id) { id = id.trim(); var target = byId[id]; if (!id || !target || target === n) return; edges.push({ from: n, to: target, label: opt.label || opt.value }); }); }); }); return edges; } roots.forEach(function(r) { place(r, 0); }); function drawArrowHead(ctx, x, y, angle, color) { var size = 6; ctx.save(); ctx.translate(x, y); ctx.rotate(angle); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(-size, -size / 2); ctx.lineTo(-size, size / 2); ctx.closePath(); ctx.fillStyle = color; ctx.fill(); ctx.restore(); } var QUESTION_TYPE_LABELS = { Loading @@ -186,16 +198,20 @@ var SurveyManager = (function() { 'yesno': 'Ja/Nein' }; var EDGE_BOW = 90; // horizontal room reserved for exclude-logic curves function renderCanvas(questions) { if (!_canvas || !_ctx) return; var roots = buildTree(questions); layoutTree(roots); layoutSequential(questions); var byId = {}; _layout.forEach(function(n) { byId[n.q.id] = n; }); var excludeEdges = collectExcludeEdges(byId); // Compute required canvas size var maxX = 0, maxY = 0; _layout.forEach(function(n) { maxX = Math.max(maxX, n.x + NODE_W + 20); maxX = Math.max(maxX, n.x + NODE_W + 20 + (excludeEdges.length ? EDGE_BOW : 0)); maxY = Math.max(maxY, n.y + NODE_H + 20); }); _canvas.width = Math.max(maxX, 400); Loading @@ -209,27 +225,45 @@ var SurveyManager = (function() { var cBg = style.getPropertyValue('--bg-secondary').trim() || '#1e1e2e'; var cBorder = style.getPropertyValue('--border').trim() || '#444'; var cAccent = style.getPropertyValue('--accent').trim() || '#7aa2f7'; var cOnAccent = style.getPropertyValue('--bg-primary').trim() || '#1e1e2e'; var cText = style.getPropertyValue('--text-primary').trim() || '#cdd6f4'; var cSub = style.getPropertyValue('--text-secondary').trim() || '#888'; var cWarning = style.getPropertyValue('--warning').trim() || '#f9e2af'; // Draw edges first _layout.forEach(function(n) { if (!n.q.parent_id) return; var parentNode = null; for (var i = 0; i < _layout.length; i++) { if (_layout[i].q.id === n.q.parent_id) { parentNode = _layout[i]; break; } } if (!parentNode) return; // Sequential flow: a plain line from each question to the next for (var i = 0; i < _layout.length - 1; i++) { var a = _layout[i], b = _layout[i + 1]; ctx.beginPath(); ctx.strokeStyle = cBorder; ctx.lineWidth = 1.5; var sx = parentNode.x + NODE_W / 2; var sy = parentNode.y + NODE_H; var ex = n.x + NODE_W / 2; var ey = n.y; ctx.moveTo(a.x + NODE_W / 2, a.y + NODE_H); ctx.lineTo(b.x + NODE_W / 2, b.y); ctx.stroke(); } // Skip-logic ("excludes") edges: dashed curves bowing out to the right, // labeled with the answer option that triggers the skip. excludeEdges.forEach(function(edge) { var sx = edge.from.x + NODE_W, sy = edge.from.y + NODE_H / 2; var ex = edge.to.x + NODE_W, ey = edge.to.y + NODE_H / 2; var bow = EDGE_BOW * (0.6 + 0.4 * Math.min(3, Math.abs(edge.to.y - edge.from.y) / (NODE_H + V_GAP))); ctx.beginPath(); ctx.strokeStyle = cWarning; ctx.lineWidth = 1.5; ctx.setLineDash([4, 3]); ctx.moveTo(sx, sy); ctx.bezierCurveTo(sx, sy + V_GAP * 0.5, ex, ey - V_GAP * 0.5, ex, ey); ctx.bezierCurveTo(sx + bow, sy, ex + bow, ey, ex, ey); ctx.stroke(); ctx.setLineDash([]); drawArrowHead(ctx, ex, ey, Math.PI, cWarning); if (edge.label) { ctx.fillStyle = cWarning; ctx.font = '9px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(truncate(edge.label, 14), sx + 4, sy - 4); } }); // Draw nodes Loading @@ -255,14 +289,14 @@ var SurveyManager = (function() { ctx.lineWidth = sel ? 2 : 1; ctx.stroke(); ctx.fillStyle = sel ? '#fff' : cText; ctx.fillStyle = sel ? cOnAccent : cText; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'center'; ctx.fillText( truncate(n.q.label || '(leer)', 22), rx + rw / 2, ry + 20 ); ctx.fillStyle = sel ? 'rgba(255,255,255,0.8)' : cSub; ctx.fillStyle = sel ? cOnAccent : cSub; ctx.font = '10px sans-serif'; ctx.fillText( QUESTION_TYPE_LABELS[n.q.question_type] || n.q.question_type, Loading Loading
editor/html/css/editor.css +38 −0 Changes for editor/html/css/editor.css: 38 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1239,6 +1239,40 @@ dialog label { white-space: nowrap; } .survey-select-field { display: flex; gap: 6px; align-items: center; } .survey-select-name { flex: 1; padding: 6px 8px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); border-radius: 4px; font-size: 13px; min-height: 32px; box-sizing: border-box; } .survey-select-btn { padding: 6px 10px; background: var(--accent); color: var(--bg-primary); border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 600; white-space: nowrap; } .survey-select-btn:hover { background: var(--accent-hover); } /* Color Var / Custom Picker */ .color-mode-toggle { display: flex; Loading Loading @@ -1469,11 +1503,15 @@ dialog label { } .sm-q-panel .set-field { flex-direction: column; align-items: stretch; gap: 0.2em; margin-bottom: 0.5em; } .sm-q-panel label { display: block; min-width: 0; font-size: 0.75rem; color: var(--text-secondary); margin-bottom: 0.2em; Loading
editor/html/js/properties.js +3 −3 Changes for editor/html/js/properties.js: 3 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -634,16 +634,16 @@ var PropertiesPanel = (function() { surveyHidden.value = value; var surveyWrap = document.createElement('div'); surveyWrap.style.cssText = 'display:flex;gap:6px;align-items:center;'; surveyWrap.className = 'survey-select-field'; var surveyName = document.createElement('span'); surveyName.style.cssText = 'flex:1;padding:6px 8px;border:1px solid #ddd;border-radius:4px;font-size:.9em;color:#555;background:#f9f9f9;min-height:32px;box-sizing:border-box;'; surveyName.className = 'survey-select-name'; surveyName.textContent = value || '(keine Umfrage)'; var surveyBtn = document.createElement('button'); surveyBtn.type = 'button'; surveyBtn.className = 'survey-select-btn'; surveyBtn.textContent = 'Umfragen verwalten'; surveyBtn.style.cssText = 'padding:6px 10px;background:#3b82f6;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:.85em;white-space:nowrap;'; (function(hidden, nameEl) { surveyBtn.addEventListener('click', function() { Loading
editor/html/js/survey-manager.js +76 −42 Changes for editor/html/js/survey-manager.js: 76 added lines, 42 removed lines. Original line number Diff line number Diff line Loading @@ -144,36 +144,48 @@ var SurveyManager = (function() { renderCanvas(_layout.map(function(l) { return l.q; })); } function buildTree(questions) { var byId = {}; questions.forEach(function(q) { byId[q.id] = { q: q, children: [] }; }); var roots = []; questions.forEach(function(q) { var pid = q.parent_id; if (pid && byId[pid]) { byId[pid].children.push(byId[q.id]); } else { roots.push(byId[q.id]); } // Questions are always presented to visitors in a single sequential order // (order_idx); the only real branching is the "excludes" skip-logic on // options, drawn separately as side edges in renderCanvas(). function layoutSequential(questions) { _layout = []; var sorted = questions.slice().sort(function(a, b) { return (a.order_idx || 0) - (b.order_idx || 0); }); sorted.forEach(function(q, i) { _layout.push({ q: q, x: 20, y: i * (NODE_H + V_GAP) + 20 }); }); return roots; } function layoutTree(roots) { _layout = []; var col = 0; function place(node, depth) { var x = col * (NODE_W + H_GAP); var y = depth * (NODE_H + V_GAP) + 20; _layout.push({ q: node.q, x: x, y: y }); col++; node.children.forEach(function(child) { place(child, depth + 1); function collectExcludeEdges(byId) { var edges = []; _layout.forEach(function(n) { (n.q.options || []).forEach(function(opt) { if (!opt.excludes) return; opt.excludes.split(',').forEach(function(id) { id = id.trim(); var target = byId[id]; if (!id || !target || target === n) return; edges.push({ from: n, to: target, label: opt.label || opt.value }); }); }); }); return edges; } roots.forEach(function(r) { place(r, 0); }); function drawArrowHead(ctx, x, y, angle, color) { var size = 6; ctx.save(); ctx.translate(x, y); ctx.rotate(angle); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(-size, -size / 2); ctx.lineTo(-size, size / 2); ctx.closePath(); ctx.fillStyle = color; ctx.fill(); ctx.restore(); } var QUESTION_TYPE_LABELS = { Loading @@ -186,16 +198,20 @@ var SurveyManager = (function() { 'yesno': 'Ja/Nein' }; var EDGE_BOW = 90; // horizontal room reserved for exclude-logic curves function renderCanvas(questions) { if (!_canvas || !_ctx) return; var roots = buildTree(questions); layoutTree(roots); layoutSequential(questions); var byId = {}; _layout.forEach(function(n) { byId[n.q.id] = n; }); var excludeEdges = collectExcludeEdges(byId); // Compute required canvas size var maxX = 0, maxY = 0; _layout.forEach(function(n) { maxX = Math.max(maxX, n.x + NODE_W + 20); maxX = Math.max(maxX, n.x + NODE_W + 20 + (excludeEdges.length ? EDGE_BOW : 0)); maxY = Math.max(maxY, n.y + NODE_H + 20); }); _canvas.width = Math.max(maxX, 400); Loading @@ -209,27 +225,45 @@ var SurveyManager = (function() { var cBg = style.getPropertyValue('--bg-secondary').trim() || '#1e1e2e'; var cBorder = style.getPropertyValue('--border').trim() || '#444'; var cAccent = style.getPropertyValue('--accent').trim() || '#7aa2f7'; var cOnAccent = style.getPropertyValue('--bg-primary').trim() || '#1e1e2e'; var cText = style.getPropertyValue('--text-primary').trim() || '#cdd6f4'; var cSub = style.getPropertyValue('--text-secondary').trim() || '#888'; var cWarning = style.getPropertyValue('--warning').trim() || '#f9e2af'; // Draw edges first _layout.forEach(function(n) { if (!n.q.parent_id) return; var parentNode = null; for (var i = 0; i < _layout.length; i++) { if (_layout[i].q.id === n.q.parent_id) { parentNode = _layout[i]; break; } } if (!parentNode) return; // Sequential flow: a plain line from each question to the next for (var i = 0; i < _layout.length - 1; i++) { var a = _layout[i], b = _layout[i + 1]; ctx.beginPath(); ctx.strokeStyle = cBorder; ctx.lineWidth = 1.5; var sx = parentNode.x + NODE_W / 2; var sy = parentNode.y + NODE_H; var ex = n.x + NODE_W / 2; var ey = n.y; ctx.moveTo(a.x + NODE_W / 2, a.y + NODE_H); ctx.lineTo(b.x + NODE_W / 2, b.y); ctx.stroke(); } // Skip-logic ("excludes") edges: dashed curves bowing out to the right, // labeled with the answer option that triggers the skip. excludeEdges.forEach(function(edge) { var sx = edge.from.x + NODE_W, sy = edge.from.y + NODE_H / 2; var ex = edge.to.x + NODE_W, ey = edge.to.y + NODE_H / 2; var bow = EDGE_BOW * (0.6 + 0.4 * Math.min(3, Math.abs(edge.to.y - edge.from.y) / (NODE_H + V_GAP))); ctx.beginPath(); ctx.strokeStyle = cWarning; ctx.lineWidth = 1.5; ctx.setLineDash([4, 3]); ctx.moveTo(sx, sy); ctx.bezierCurveTo(sx, sy + V_GAP * 0.5, ex, ey - V_GAP * 0.5, ex, ey); ctx.bezierCurveTo(sx + bow, sy, ex + bow, ey, ex, ey); ctx.stroke(); ctx.setLineDash([]); drawArrowHead(ctx, ex, ey, Math.PI, cWarning); if (edge.label) { ctx.fillStyle = cWarning; ctx.font = '9px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(truncate(edge.label, 14), sx + 4, sy - 4); } }); // Draw nodes Loading @@ -255,14 +289,14 @@ var SurveyManager = (function() { ctx.lineWidth = sel ? 2 : 1; ctx.stroke(); ctx.fillStyle = sel ? '#fff' : cText; ctx.fillStyle = sel ? cOnAccent : cText; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'center'; ctx.fillText( truncate(n.q.label || '(leer)', 22), rx + rw / 2, ry + 20 ); ctx.fillStyle = sel ? 'rgba(255,255,255,0.8)' : cSub; ctx.fillStyle = sel ? cOnAccent : cSub; ctx.font = '10px sans-serif'; ctx.fillText( QUESTION_TYPE_LABELS[n.q.question_type] || n.q.question_type, Loading