Quelltext des aktuellen Standes
<?php
$this->layout('layouts/main', [
'page_title' => $page_title,
'current_route' => 'netdiagram',
'full_width' => true,
'no_footer' => true,
]);
$xmlContent = $diagram['drawio_xml'] ?? '';
?>
<?php if (!$drawio_installed): ?>
<div style="max-width:600px;margin:4rem auto;text-align:center;padding:2rem;">
<div style="font-size:3rem;margin-bottom:1rem;">⚠</div>
<h2><?= $this->te('Diagram Editor nicht installiert') ?></h2>
<p style="color:var(--color-text-muted,var(--gray-500));margin-bottom:1.5rem;">
<?= $this->te('Der Diagram Editor muss zuerst installiert werden, bevor Netzwerkpläne bearbeitet werden können.') ?>
</p>
<?php if ($this->hasCapability('admin_netdiagram')): ?>
<a href="/admin/netdiagram/drawio" class="btn btn-primary"><?= $this->te('Diagram Editor installieren') ?></a>
<?php endif; ?>
<a href="/netdiagram" class="btn btn-default" style="margin-left:0.5rem;"><?= $this->te('← Zurück') ?></a>
</div>
<?php return; ?>
<?php endif; ?>
<div id="nd-editor-wrap">
<!-- Top bar -->
<div id="nd-topbar">
<a href="/netdiagram" class="nd-back-btn" title="<?= $this->te('Zurück zur Übersicht') ?>">←</a>
<span id="nd-plan-name" class="nd-plan-name"><?= $this->e($diagram['name']) ?></span>
<?php if ($can_edit): ?>
<button id="nd-rename-btn" class="nd-topbar-btn" title="<?= $this->te('Umbenennen') ?>">✎</button>
<?php endif; ?>
<span id="nd-save-status" class="nd-save-status"></span>
</div>
<!-- draw.io iframe -->
<iframe
id="drawio-frame"
src="/drawio/index.html?embed=1&spin=1&proto=json&noSaveBtn=<?= $can_edit ? '0' : '1' ?>&noExitBtn=1&saveAndExit=0"
frameborder="0"
allowfullscreen
oncontextmenu="return false;">
</iframe>
</div>
<?php if ($can_edit): ?>
<!-- Rename modal -->
<div id="nd-rename-modal" class="modal-overlay nd-modal" style="display:none;">
<div class="modal-card">
<div class="modal-header">
<h2><?= $this->te('Plan umbenennen') ?></h2>
<button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" id="nd-rename-close">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="nd-rename-input"><?= $this->te('Neuer Name') ?></label>
<input type="text" id="nd-rename-input" class="form-control"
value="<?= $this->e($diagram['name']) ?>" maxlength="200">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" id="nd-rename-cancel"><?= $this->te('Abbrechen') ?></button>
<button type="button" class="btn btn-primary" id="nd-rename-save"><?= $this->te('Speichern') ?></button>
</div>
</div>
</div>
<?php endif; ?>
<script>
(function () {
var DIAGRAM_ID = <?= (int)$diagram['id'] ?>;
var CSRF = <?= json_encode($this->csrf()) ?>;
var CAN_EDIT = <?= $can_edit ? 'true' : 'false' ?>;
var INITIAL_XML = <?= json_encode($xmlContent) ?>;
var frame = document.getElementById('drawio-frame');
var statusEl = document.getElementById('nd-save-status');
var initialized = false;
// ----------------------------------------------------------------
// draw.io postMessage communication
// ----------------------------------------------------------------
function sendToDrawio(msg) {
if (frame && frame.contentWindow) {
frame.contentWindow.postMessage(JSON.stringify(msg), '*');
}
}
function setStatus(text, color) {
statusEl.textContent = text;
statusEl.style.color = color || '';
}
function saveXml(xml) {
if (!CAN_EDIT) return;
setStatus('Wird gespeichert...', '#f59e0b');
// WICHTIG: Accept + X-Requested-With senden, damit der Server bei einem
// CSRF-/Rate-Limit-/Größen-Problem mit JSON (statt einem 302-Redirect auf
// eine HTML-Seite) antwortet – sonst scheiterte r.json() und JEDER Fehler
// erschien pauschal als „Fehler beim Speichern".
fetch('/netdiagram/' + DIAGRAM_ID + '/save-xml', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': CSRF
},
body: '_csrf=' + encodeURIComponent(CSRF) + '&xml=' + encodeURIComponent(xml)
})
.then(function(r) {
return r.json().catch(function() { return { ok: false, _status: r.status }; });
})
.then(function(d) {
if (d && d.ok) {
setStatus('Gespeichert ' + d.saved_at, '#10b981');
setTimeout(function() { setStatus(''); }, 4000);
} else if (d && (d._status === 403 || d._status === 419)) {
setStatus('Sitzung abgelaufen – bitte Seite neu laden (F5)', '#ef4444');
} else if (d && d._status === 429) {
setStatus('Zu viele Speichervorgänge – kurz warten…', '#f59e0b');
} else {
setStatus('Fehler beim Speichern' + (d && d.error ? ': ' + d.error : ''), '#ef4444');
}
})
.catch(function() { setStatus('Fehler beim Speichern (Netzwerk)', '#ef4444'); });
}
// ----------------------------------------------------------------
// Prevent browser back/forward swipe gesture on right-click drag
// ----------------------------------------------------------------
var wrap = document.getElementById('nd-editor-wrap');
wrap.addEventListener('mousedown', function(e) {
if (e.button === 2) e.preventDefault();
}, { passive: false });
wrap.addEventListener('contextmenu', function(e) {
e.preventDefault();
});
// Give iframe focus on click so pointer events are captured by draw.io
wrap.addEventListener('click', function() {
if (frame && frame.contentWindow) frame.contentWindow.focus();
});
window.addEventListener('message', function (evt) {
var data;
try {
data = typeof evt.data === 'string' ? JSON.parse(evt.data) : evt.data;
} catch (e) { return; }
if (!data || !data.event) return;
switch (data.event) {
case 'init':
// draw.io is ready — load diagram XML
if (!initialized) {
initialized = true;
sendToDrawio({
action: 'load',
xml: INITIAL_XML || '<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel>',
autosave: CAN_EDIT ? 1 : 0
});
}
break;
case 'save':
// User clicked save button
if (data.xml) saveXml(data.xml);
break;
case 'autosave':
// Auto-save on each change (only if diagram already has content)
if (data.xml && CAN_EDIT) saveXml(data.xml);
break;
case 'export':
// Triggered by save action in some draw.io versions
if (data.xml) saveXml(data.xml);
break;
}
});
// ----------------------------------------------------------------
// Rename
// ----------------------------------------------------------------
<?php if ($can_edit): ?>
var renameBtn = document.getElementById('nd-rename-btn');
var renameModal = document.getElementById('nd-rename-modal');
var renameInput = document.getElementById('nd-rename-input');
var renameClose = document.getElementById('nd-rename-close');
var renameCancel = document.getElementById('nd-rename-cancel');
var renameSave = document.getElementById('nd-rename-save');
var nameDisplay = document.getElementById('nd-plan-name');
renameBtn.addEventListener('click', function() {
renameInput.value = nameDisplay.textContent;
renameModal.style.display = 'flex';
setTimeout(function() { renameInput.focus(); renameInput.select(); }, 50);
});
function closeRenameModal() { renameModal.style.display = 'none'; }
renameClose.addEventListener('click', closeRenameModal);
renameCancel.addEventListener('click', closeRenameModal);
renameModal.addEventListener('click', function(e) { if (e.target === renameModal) closeRenameModal(); });
renameSave.addEventListener('click', function() {
var name = renameInput.value.trim();
if (!name) return;
fetch('/netdiagram/' + DIAGRAM_ID + '/rename', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: '_csrf=' + encodeURIComponent(CSRF) + '&name=' + encodeURIComponent(name)
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.ok) {
nameDisplay.textContent = d.name;
document.title = d.name + ' \u2013 Netzwerkplan';
closeRenameModal();
}
});
});
renameInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') renameSave.click();
if (e.key === 'Escape') closeRenameModal();
});
<?php endif; ?>
}());
</script>
<style>
/* Prevent browser swipe-back/forward gesture while inside the editor */
html, body {
overscroll-behavior: none;
}
/* Remove default body padding for full-height editor */
.main-content { padding: 0 !important; }
.page-content { padding: 0 !important; }
#nd-editor-wrap {
display: flex;
flex-direction: column;
height: calc(100vh - 60px); /* subtract main nav height */
overflow: hidden;
overscroll-behavior: none;
touch-action: none;
}
/* Top bar */
#nd-topbar {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0 1rem;
height: 44px;
background: var(--color-bg, var(--color-bg));
border-bottom: 1px solid var(--color-border, var(--gray-200));
flex-shrink: 0;
z-index: 10;
}
.nd-back-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 0.25rem;
color: var(--color-text-muted, var(--gray-500));
text-decoration: none;
font-size: 1.1rem;
transition: background 0.1s;
flex-shrink: 0;
}
.nd-back-btn:hover { background: var(--color-background, var(--gray-100)); color: var(--color-text, var(--gray-900)); text-decoration: none; }
.nd-plan-name {
font-weight: 600;
font-size: 0.9rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 300px;
}
.nd-topbar-btn {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem 0.375rem;
border-radius: 0.25rem;
color: var(--color-text-muted, var(--gray-500));
font-size: 0.9rem;
transition: background 0.1s;
flex-shrink: 0;
}
.nd-topbar-btn:hover { background: var(--color-background, var(--gray-100)); }
.nd-save-status {
margin-left: auto;
font-size: 0.8rem;
color: var(--color-text-muted, var(--gray-500));
}
/* iframe fills remaining height */
#drawio-frame {
flex: 1;
width: 100%;
border: none;
display: block;
min-height: 0;
}
/* Modal nutzt die zentralen Komponenten aus app.css (.modal-overlay/.modal-card);
lokal nur die modul-eigene Breite. Sichtbarkeit läuft über style.display. */
.nd-modal .modal-card { max-width: 440px; }
</style>