OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

edit.php

12 KB
<?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;">&#9888;</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&auml;ne bearbeitet werden k&ouml;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('&larr; 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') ?>">&#8592;</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') ?>">&#9998;</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">&times;</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>