OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

page-editor.js

61 KB
/*
 * Seiten-Editor der Dokumentation.
 *
 * Wird nur auf den Editor-Seiten geladen. Die serverseitigen Angaben - ob
 * bearbeitet oder neu angelegt wird, ob TinyMCE vorliegt, ob der KI-Assistent
 * eingeschaltet ist, die Seiten-ID und ein gefundener Entwurf - stehen als
 * JSON im Markup; dieses Skript liest sie einmal und arbeitet damit.
 *
 * Die vier Abschnitte laufen unabhaengig voneinander: Jeder ist fuer sich
 * abgeschirmt, damit ein Fehler in einem die uebrigen nicht mitreisst -
 * so, wie es vier getrennte Skriptbloecke taeten. Ein Fehler bleibt in der
 * Konsole sichtbar.
 *
 * `deletePageAction` haengt bewusst am `window`: Der Loeschknopf ruft sie
 * ueber ein onclick-Attribut auf.
 */
(function () {
    var _daten = document.getElementById('page-editor-data');
    var cfg;
    try {
        cfg = _daten ? JSON.parse(_daten.textContent) : {};
    } catch (e) {
        cfg = {};
    }
    cfg.isEdit       = !!cfg.isEdit;
    cfg.tinymce      = !!cfg.tinymce;
    cfg.aiEnabled    = !!cfg.aiEnabled;
    cfg.canDelete    = !!cfg.canDelete;
    cfg.pageId       = parseInt(cfg.pageId, 10) || 0;
    cfg.draftContent = cfg.draftContent || null;

    /**
     * Der KI-Dialog wird im Abschnitt "KI-Assistent" gebaut und aus der
     * TinyMCE-Schaltflaeche im Abschnitt "Editor" gerufen - er braucht daher
     * einen Platz, den beide sehen.
     */
    var openAiEditorModal = function () {};

    /** Fuehrt einen Abschnitt aus und laesst einen Fehler dort enden. */
    function abschnitt(name, fn) {
        try {
            fn();
        } catch (e) {
            if (window.console && console.error) {
                console.error('Seiten-Editor, Abschnitt "' + name + '":', e);
            }
        }
    }


    abschnitt('Editor', function () {
        // CSRF-Token Heartbeat: Alle 15 Minuten erneuern, damit beim Speichern kein Fehler auftritt
        (function() {
            function refreshCsrf() {
                fetch('/api/csrf-token', {credentials: 'same-origin'})
                    .then(function(r) { return r.json(); })
                    .then(function(d) {
                        if (d.token) {
                            var csrfInputs = document.querySelectorAll('input[name="_csrf"]');
                            csrfInputs.forEach(function(el) { el.value = d.token; });
                        }
                    })
                    .catch(function() {});
            }
            // Alle 15 Minuten Token erneuern
            setInterval(refreshCsrf, 15 * 60 * 1000);
        })();

        document.addEventListener('DOMContentLoaded', function() {
            // Sidebar Toggle
            (function() {
                var layout = document.querySelector('.editor-layout');
                var toggleBtn = document.getElementById('sidebarToggleBtn');
                if (!layout || !toggleBtn) return;

                var storageKey = 'editor-sidebar-collapsed';
                if (localStorage.getItem(storageKey) === '1') {
                    layout.classList.add('sidebar-collapsed');
                }

                function toggleSidebar() {
                    layout.classList.toggle('sidebar-collapsed');
                    localStorage.setItem(storageKey, layout.classList.contains('sidebar-collapsed') ? '1' : '0');
                }

                toggleBtn.addEventListener('click', toggleSidebar);

                layout.querySelectorAll('.sidebar-section .section-header').forEach(function(header) {
                    header.addEventListener('click', function() {
                        if (layout.classList.contains('sidebar-collapsed')) {
                            toggleSidebar();
                        }
                    });
                });
            })();

            var _formReadyToSubmit = false;

            function doSubmit(form) {
                var submitBtn = form.querySelector('.btn-save');
                if (submitBtn) {
                    submitBtn.disabled = true;
                    submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gespeichert…';
                }
                if (typeof tinymce !== 'undefined' && tinymce.activeEditor) {
                    tinymce.activeEditor.uploadImages().then(function() {
                        tinymce.activeEditor.save();
                        form.submit();
                    }).catch(function() {
                        tinymce.activeEditor.save();
                        form.submit();
                    });
                } else {
                    if (window._fallbackEditor) {
                        var ta = document.getElementById('content-editor');
                        if (ta) ta.value = window._fallbackEditor.getContent();
                    }
                    form.submit();
                }
            }

            // Form-Submit-Handler
            var form = document.getElementById('page-form');
            var isEditMode = cfg.isEdit;

            if (form) {
                form.addEventListener('submit', function(e) {
                    var title = document.getElementById('title');
                    var topicId = document.querySelector('select[name="topic_id"], input[name="topic_id"]');

                    if (!title || !title.value.trim()) {
                        e.preventDefault();
                        title && title.focus();
                        alert('Bitte einen Titel eingeben');
                        return false;
                    }

                    if (topicId && !topicId.value) {
                        e.preventDefault();
                        alert(window.onitText('Bitte ein Themengebiet auswählen'));
                        return false;
                    }

                    // Bei Edit-Modus: Änderungsgrund-Modal anzeigen (außer wenn schon bereit)
                    if (isEditMode && !_formReadyToSubmit) {
                        e.preventDefault();
                        openChangeReasonModal(form);
                        return false;
                    }

                    // Immer über doSubmit gehen: dort wird der Editor-Inhalt in das
                    // Textfeld übernommen – bei TinyMCE ebenso wie beim einfachen
                    // Editor. Ohne diesen Weg würde beim Anlegen einer Seite mit dem
                    // einfachen Editor der ungefüllte Ausgangswert gespeichert.
                    if (!_formReadyToSubmit) {
                        e.preventDefault();
                        doSubmit(form);
                        return false;
                    }
                });
            }

            // Änderungsgrund-Modal
            function openChangeReasonModal(form) {
                var modal = document.getElementById('change-reason-modal');
                var input = document.getElementById('cr-modal-input');
                var errorEl = document.getElementById('cr-modal-error');
                if (!modal) return;

                // Vorherigen Wert wiederherstellen falls vorhanden
                var existing = document.getElementById('change_reason');
                if (existing && existing.value) input.value = existing.value;

                errorEl.style.display = 'none';
                modal.style.display = 'flex';
                setTimeout(function() { input.focus(); }, 50);

                document.getElementById('cr-modal-confirm').onclick = function() {
                    var reason = input.value.trim();
                    // Dieselbe Untergrenze wie serverseitig – sonst nimmt das Modal
                    // eine Eingabe an, die beim Speichern abgelehnt wird.
                    if (reason.length < 3) {
                        errorEl.style.display = 'flex';
                        input.focus();
                        return;
                    }
                    document.getElementById('change_reason').value = reason;
                    modal.style.display = 'none';
                    _formReadyToSubmit = true;
                    doSubmit(form);
                };

                document.getElementById('cr-modal-cancel').onclick = function() {
                    modal.style.display = 'none';
                };

                // Klick auf den Hintergrund schließt den Dialog ebenfalls.
                modal.onclick = function(e) {
                    if (e.target === modal) { modal.style.display = 'none'; }
                };

                // ESC schließt Modal
                input.onkeydown = function(e) {
                    if (e.key === 'Enter') {
                        document.getElementById('cr-modal-confirm').click();
                    } else if (e.key === 'Escape') {
                        modal.style.display = 'none';
                    }
                };
            }

            function calcEditorHeight() {
                var topbar = document.querySelector('.editor-topbar');
                var breadcrumb = document.querySelector('.editor-breadcrumb');
                var navbar = document.querySelector('.main-header') || document.querySelector('.navbar');
                var navH = navbar ? navbar.offsetHeight : 57;
                var topH = topbar ? topbar.offsetHeight : 58;
                var bcH  = breadcrumb ? breadcrumb.offsetHeight : 40;
                return Math.max(400, window.innerHeight - navH - topH - bcH - 10);
            }

            if (cfg.tinymce) {
            tinymce.init({
                selector: '#content-editor',
                height: calcEditorHeight(),
                min_height: 400,
                language: 'de',
                language_url: '/assets/tinymce/langs/de.js',

                plugins: [
                    'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
                    'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
                    'insertdatetime', 'media', 'table', 'help', 'wordcount', 'codesample',
                    'emoticons', 'pagebreak', 'nonbreaking', 'quickbars'
                ],

                toolbar: [
                    'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | forecolor backcolor removeformat',
                    'alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media table',
                    'codesample blockquote hr pagebreak | charmap emoticons | searchreplace | fullscreen code help' + (cfg.aiEnabled ? ' | ai_editor' : '')
                ],

                menubar: 'file edit view insert format tools table help',
                contextmenu: 'link image table',

                quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote',
                quickbars_insert_toolbar: 'quickimage quicktable',

                font_family_formats: 'Arial=arial,helvetica,sans-serif; Courier New=courier new,courier,monospace; Georgia=georgia,palatino; Tahoma=tahoma,arial,helvetica,sans-serif; Times New Roman=times new roman,times; Verdana=verdana,geneva; Impact=impact,chicago',

                font_size_formats: '8pt 9pt 10pt 11pt 12pt 14pt 16pt 18pt 20pt 24pt 30pt 36pt 48pt',

                block_formats: 'Paragraph=p; Heading 1=h1; Heading 2=h2; Heading 3=h3; Heading 4=h4; Heading 5=h5; Heading 6=h6; Preformatted=pre; Blockquote=blockquote',

                table_toolbar: 'tableprops tabledelete | tableinsertrowbefore tableinsertrowafter tabledeleterow | tableinsertcolbefore tableinsertcolafter tabledeletecol',
                table_appearance_options: true,
                table_advtab: true,
                table_cell_advtab: true,
                table_row_advtab: true,

                codesample_languages: [
                    { text: 'HTML/XML', value: 'markup' },
                    { text: 'JavaScript', value: 'javascript' },
                    { text: 'CSS', value: 'css' },
                    { text: 'PHP', value: 'php' },
                    { text: 'Python', value: 'python' },
                    { text: 'Java', value: 'java' },
                    { text: 'C#', value: 'csharp' },
                    { text: 'SQL', value: 'sql' },
                    { text: 'Bash', value: 'bash' },
                    { text: 'JSON', value: 'json' },
                ],

                content_css: '/assets/css/fonts.css',
                content_style: `
                    body {
                        font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                        font-size: 16px;
                        line-height: 1.7;
                        /* Dieser Block gestaltet den Editor-Rahmen (eigenes Dokument),
                           der die Design-Variablen der Seite nicht sieht – daher hier
                           bewusst feste Farbwerte. */
                        color: #1f2937;
                        padding: 1.5rem 2rem;
                    }
                    h1 { font-size: 2rem; font-weight: 700; margin: 2rem 0 1rem; color: #111827; }
                    h2 { font-size: 1.5rem; font-weight: 600; margin: 1.75rem 0 0.75rem; color: #111827; border-bottom: 2px solid #e5e7eb; padding-bottom: 0.5rem; }
                    h3 { font-size: 1.25rem; font-weight: 600; margin: 1.5rem 0 0.5rem; color: #1f2937; }
                    p { margin: 0 0 1rem; }
                    ul, ol { margin: 0 0 1rem; padding-left: 1.5rem; }
                    a { color: #2563eb; }
                    blockquote { border-left: 4px solid #2563eb; margin: 1.5rem 0; padding: 0.75rem 1.25rem; background: #f0f7ff; color: #1e40af; }
                    pre { background: #1e293b; color: #e2e8f0; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; }
                    code { background: #f1f5f9; color: #0f172a; padding: 0.15em 0.4em; border-radius: 0.25rem; }
                    pre code { background: transparent; color: inherit; padding: 0; }
                    table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
                    th, td { border: 1px solid #d1d5db; padding: 0.75rem 1rem; text-align: left; }
                    th { background: #f3f4f6; font-weight: 600; }
                    img { max-width: 100%; height: auto; border-radius: 0.5rem; }
                `,

                promotion: false,
                branding: false,
                license_key: 'gpl',
                resize: true,
                statusbar: true,
                elementpath: true,

                // Bild-Upload - WICHTIG: images_upload_handler fuer mehr Kontrolle
                automatic_uploads: true,
                images_upload_url: '/upload/image',
                images_upload_credentials: true,
                images_reuse_filename: true,

                // Custom upload handler fuer bessere Fehlerbehandlung
                images_upload_handler: function(blobInfo, progress) {
                    return new Promise(function(resolve, reject) {
                        var xhr = new XMLHttpRequest();
                        xhr.open('POST', '/upload/image');
                        xhr.withCredentials = true;

                        xhr.upload.onprogress = function(e) {
                            progress(e.loaded / e.total * 100);
                        };

                        xhr.onload = function() {
                            if (xhr.status === 403) {
                                reject({ message: 'Keine Berechtigung zum Hochladen', remove: true });
                                return;
                            }

                            if (xhr.status < 200 || xhr.status >= 300) {
                                reject('HTTP Error: ' + xhr.status);
                                return;
                            }

                            var json;
                            try {
                                json = JSON.parse(xhr.responseText);
                            } catch (e) {
                                reject('Ungueltige Server-Antwort');
                                return;
                            }

                            if (!json || typeof json.location !== 'string') {
                                reject('Ungueltige JSON-Antwort: ' + xhr.responseText);
                                return;
                            }

                            resolve(json.location);
                        };

                        xhr.onerror = function() {
                            reject('Bild-Upload fehlgeschlagen. Netzwerkfehler.');
                        };

                        var formData = new FormData();
                        formData.append('file', blobInfo.blob(), blobInfo.filename());

                        // CSRF Token hinzufuegen
                        var csrfToken = document.querySelector('input[name="_csrf"]');
                        if (csrfToken) {
                            formData.append('_csrf', csrfToken.value);
                        }

                        xhr.send(formData);
                    });
                },

                paste_data_images: true,

                setup: function(editor) {
                    editor.on('change input', function() {
                        var btn = document.querySelector('.btn-save');
                        if (btn) btn.classList.add('has-changes');
                        var hint = document.getElementById('change-reason-hint');
                        if (hint) hint.style.display = 'inline-flex';
                    });
                    editor.on('init', function() {
                        // Höhe nach Init nochmal anpassen (Toolbar kann sich verschoben haben)
                        setTimeout(function() {
                            var newH = calcEditorHeight();
                            editor.getContainer().style.height = newH + 'px';
                            editor.getWin().frameElement && (editor.getWin().frameElement.style.height = '');
                        }, 100);
                    });

                    if (cfg.aiEnabled) {
                    // KI-Assistent Button registrieren
                    editor.ui.registry.addButton('ai_editor', {
                        text: '✨ KI',
                        tooltip: window.onitText('KI-Assistent: Inhalt erstellen, bearbeiten oder ergänzen'),
                        onAction: function() {
                            openAiEditorModal(editor);
                        }
                    });
                    }
                }
            });
            } else {
            // wysiwyg.js Fallback
            (function() {
                var ta = document.getElementById('content-editor');
                if (ta && typeof OpenNIT !== 'undefined' && OpenNIT.WysiwygEditor) {
                    var wrap = document.createElement('div');
                    wrap.id = 'content-editor-wrap';
                    ta.parentNode.insertBefore(wrap, ta);
                    wrap.appendChild(ta);
                    var csrfToken = (document.querySelector('input[name="_csrf"]') || {value: ''}).value;
                    window._fallbackEditor = new OpenNIT.WysiwygEditor(wrap, {
                        placeholder: 'Inhalt eingeben...',
                        uploadUrl: '/upload/image',
                        csrfToken: csrfToken,
                        onChange: function() {
                            var btn = document.querySelector('.btn-save');
                            if (btn) btn.classList.add('has-changes');
                            var hint = document.getElementById('change-reason-hint');
                            if (hint) hint.style.display = 'inline-flex';
                        }
                    });
                }
            })();
            }

            // Editor bei Fenster-Resize anpassen
            var _resizeTimer;
            window.addEventListener('resize', function() {
                clearTimeout(_resizeTimer);
                _resizeTimer = setTimeout(function() {
                    if (typeof tinymce !== 'undefined' && tinymce.activeEditor) {
                        var newH = calcEditorHeight();
                        tinymce.activeEditor.getContainer().style.height = newH + 'px';
                    }
                }, 150);
            });

            // ========================================
            // Datei-Upload Handler
            // ========================================
            // Immer frischen CSRF-Token aus dem DOM lesen (nie stale)
            function getCsrf() {
                var el = document.querySelector('input[name="_csrf"]');
                return el ? el.value : '';
            }

            if (cfg.isEdit) {
            var pageId = cfg.pageId;
            var fileInput = document.getElementById('file-input');
            var fileList = document.getElementById('file-list');
            var dropzone = document.getElementById('file-dropzone');
            var progressContainer = document.getElementById('upload-progress');
            var progressFill = document.getElementById('progress-fill');
            var progressText = document.getElementById('progress-text');

            if (fileInput) {
                fileInput.addEventListener('change', function(e) {
                    var files = e.target.files;
                    if (files.length > 0) {
                        uploadFiles(files);
                    }
                });
            }

            // Drag & Drop
            if (dropzone) {
                dropzone.addEventListener('dragover', function(e) {
                    e.preventDefault();
                    dropzone.classList.add('dragover');
                });

                dropzone.addEventListener('dragleave', function(e) {
                    e.preventDefault();
                    dropzone.classList.remove('dragover');
                });

                dropzone.addEventListener('drop', function(e) {
                    e.preventDefault();
                    dropzone.classList.remove('dragover');
                    var files = e.dataTransfer.files;
                    if (files.length > 0) {
                        uploadFiles(files);
                    }
                });
            }

            function uploadFiles(files) {
                var totalFiles = files.length;
                var uploadedCount = 0;
                var filesArray = Array.from(files);

                progressContainer.style.display = 'block';
                dropzone.style.display = 'none';
                progressText.textContent = 'Hochladen... 0 von ' + totalFiles;
                progressFill.style.width = '0%';

                function uploadNext(index) {
                    if (index >= filesArray.length) {
                        // Alle Dateien verarbeitet
                        progressFill.style.width = '100%';
                        if (uploadedCount === totalFiles) {
                            progressText.textContent = 'Fertig! ' + totalFiles + ' Datei(en) hochgeladen';
                        } else {
                            progressText.textContent = 'Fertig! ' + uploadedCount + ' von ' + totalFiles + ' Datei(en) hochgeladen';
                        }

                        // Dateiliste immer vom Server neu laden nach Abschluss
                        refreshFileList();

                        setTimeout(function() {
                            progressContainer.style.display = 'none';
                            dropzone.style.display = 'block';
                            progressFill.style.width = '0%';
                            fileInput.value = '';
                        }, 2000);
                        return;
                    }

                    var currentFile = filesArray[index];
                    progressText.textContent = 'Hochladen: ' + currentFile.name + ' (' + (index + 1) + '/' + totalFiles + ')';

                    uploadFile(currentFile, function(success) {
                        if (success) {
                            uploadedCount++;
                        }
                        var percent = Math.round(((index + 1) / totalFiles) * 100);
                        progressFill.style.width = percent + '%';

                        // Naechste Datei hochladen
                        uploadNext(index + 1);
                    });
                }

                uploadNext(0);
            }

            function uploadFile(file, callback) {
                var formData = new FormData();
                formData.append('file', file);
                formData.append('_csrf', getCsrf());

                var xhr = new XMLHttpRequest();
                xhr.open('POST', '/pages/' + pageId + '/files');

                xhr.onload = function() {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        var responseText = xhr.responseText;
                        try {
                            // Manchmal gibt PHP Notices/Warnings vor dem JSON aus
                            var jsonStart = responseText.indexOf('{');
                            if (jsonStart > 0) {
                                responseText = responseText.substring(jsonStart);
                            }
                            var response = JSON.parse(responseText);
                            if (response.success || response.file) {
                                if (response.file) {
                                    addFileToList(response.file);
                                }
                                callback(true);
                                return;
                            }
                        } catch(e) {
                            console.error('Parse error:', e, responseText);
                        }
                        // Status 2xx aber unerwartete Antwort - trotzdem als Erfolg werten
                        callback(true);
                    } else {
                        var error = 'Upload fehlgeschlagen';
                        try {
                            var resp = JSON.parse(xhr.responseText);
                            error = resp.error || error;
                        } catch(e) {}
                        alert(error + ' (' + file.name + ')');
                        callback(false);
                    }
                };

                xhr.onerror = function() {
                    alert('Netzwerkfehler beim Upload von ' + file.name);
                    callback(false);
                };

                xhr.ontimeout = function() {
                    alert('Timeout beim Upload von ' + file.name);
                    callback(false);
                };

                xhr.timeout = 60000; // 60 Sekunden Timeout
                xhr.send(formData);
            }

            function updateFileCounter() {
                var counter = document.getElementById('file-counter');
                if (counter) {
                    counter.textContent = '(' + fileList.querySelectorAll('.file-item').length + ')';
                }
            }

            function addFileToList(file) {
                // Duplikat verhindern
                if (fileList.querySelector('[data-file-id="' + file.id + '"]')) {
                    return;
                }

                var li = document.createElement('li');
                li.className = 'file-item file-item-new';
                li.setAttribute('data-file-id', file.id);
                li.innerHTML = '<span class="file-icon"><i class="fas fa-file" aria-hidden="true"></i></span>' +
                    '<a href="/files/' + file.id + '" class="file-name" target="_blank">' + escapeHtml(file.name) + '</a>' +
                    '<span class="file-size">' + formatFileSize(file.size) + '</span>' +
                    '<button type="button" class="file-delete" data-file-id="' + file.id + '" title="Loeschen">&times;</button>';
                fileList.appendChild(li);
                updateFileCounter();

                // Highlight-Animation fuer neue Datei
                requestAnimationFrame(function() {
                    li.classList.add('file-item-highlight');
                    setTimeout(function() {
                        li.classList.remove('file-item-highlight', 'file-item-new');
                    }, 2000);
                });

                // Zur neuen Datei scrollen
                li.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            }

            // Dateiliste komplett vom Server neu laden
            function refreshFileList() {
                var xhr = new XMLHttpRequest();
                xhr.open('GET', '/pages/' + pageId + '/files/list');

                xhr.onload = function() {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        try {
                            var response = JSON.parse(xhr.responseText);
                            if (response.files && Array.isArray(response.files)) {
                                // Bestehende Liste leeren und neu aufbauen
                                fileList.innerHTML = '';
                                response.files.forEach(function(file) {
                                    var li = document.createElement('li');
                                    li.className = 'file-item';
                                    li.setAttribute('data-file-id', file.id);
                                    li.innerHTML = '<span class="file-icon"><i class="fas fa-file" aria-hidden="true"></i></span>' +
                                        '<a href="/files/' + file.id + '" class="file-name" target="_blank">' + escapeHtml(file.name) + '</a>' +
                                        '<span class="file-size">' + formatFileSize(file.size) + '</span>' +
                                        '<button type="button" class="file-delete" data-file-id="' + file.id + '" title="Loeschen">&times;</button>';
                                    fileList.appendChild(li);
                                });
                                updateFileCounter();
                            }
                        } catch(e) {
                            console.error('Dateiliste konnte nicht geladen werden:', e);
                        }
                    }
                };

                xhr.onerror = function() {
                    console.error('Netzwerkfehler beim Laden der Dateiliste');
                };

                xhr.send();
            }

            function formatFileSize(bytes) {
                if (bytes < 1024) return bytes + ' B';
                if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
                return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
            }

            function escapeHtml(text) {
                var div = document.createElement('div');
                div.textContent = text;
                return div.innerHTML;
            }

            // Datei loeschen
            document.addEventListener('click', function(e) {
                if (e.target.classList.contains('file-delete')) {
                    var fileId = e.target.getAttribute('data-file-id');
                    if (confirm('Datei wirklich loeschen?')) {
                        deleteFile(fileId, e.target.closest('.file-item'));
                    }
                }
            });

            function deleteFile(fileId, listItem) {
                // Lade-Indikator
                listItem.style.opacity = '0.5';
                listItem.style.pointerEvents = 'none';

                var formData = new FormData();
                formData.append('_method', 'DELETE');
                formData.append('_csrf', getCsrf());

                var xhr = new XMLHttpRequest();
                xhr.open('POST', '/files/' + fileId);

                xhr.onload = function() {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        listItem.remove();
                        updateFileCounter();
                    } else {
                        listItem.style.opacity = '1';
                        listItem.style.pointerEvents = 'auto';
                        var error = 'Loeschen fehlgeschlagen';
                        try {
                            var resp = JSON.parse(xhr.responseText);
                            error = resp.error || error;
                        } catch(e) {}
                        alert(error);
                    }
                };

                xhr.onerror = function() {
                    listItem.style.opacity = '1';
                    listItem.style.pointerEvents = 'auto';
                    alert('Netzwerkfehler beim Loeschen');
                };

                xhr.send(formData);
            }
            }
        });

        if (cfg.isEdit && cfg.canDelete) {
        window.deletePageAction = function () {
            if (!confirm('Seite wirklich archivieren? Die Seite kann im Admin-Bereich wiederhergestellt werden.')) return;
            var form = document.createElement('form');
            form.method = 'POST';
            form.action = '/pages/' + cfg.pageId;
            // Frischen CSRF-Token aus dem DOM holen (nie den PHP-hardcodierten verwenden)
            form.innerHTML = '<input type="hidden" name="_csrf" value="' + (document.querySelector('input[name="_csrf"]') ? document.querySelector('input[name="_csrf"]').value : '') + '">' +
                             '<input type="hidden" name="_method" value="DELETE">';
            document.body.appendChild(form);
            form.submit();
        };
        }

        // === Ansprechpartner-Suche ===
        (function() {
            var searchInput = document.getElementById('ap-search');
            var resultsDiv = document.getElementById('ap-search-results');
            var typeField = document.getElementById('ap-type');
            var idField = document.getElementById('ap-id');
            var currentDiv = document.getElementById('ap-current');
            var searchWrapper = document.getElementById('ap-search-wrapper');
            var selectedName = document.getElementById('ap-selected-name');
            var removeBtn = document.getElementById('ap-remove');

            if (!searchInput) return;

            var timer = null;

            function escHtml(s) {
                var d = document.createElement('div');
                d.textContent = s || '';
                return d.innerHTML;
            }

            searchInput.addEventListener('input', function() {
                clearTimeout(timer);
                var q = this.value.trim();
                if (q.length < 2) {
                    resultsDiv.style.display = 'none';
                    return;
                }
                timer = setTimeout(function() {
                    fetch('/api/ansprechpartner/search?q=' + encodeURIComponent(q))
                        .then(function(r) { return r.json(); })
                        .then(function(data) {
                            if (!data.length) {
                                resultsDiv.innerHTML = '<div class="ap-result-empty">Keine Ergebnisse</div>';
                            } else {
                                resultsDiv.innerHTML = data.map(function(item) {
                                    var icon = item.type === 'company'
                    ? '<i class="fas fa-building" aria-hidden="true"></i>'
                    : '<i class="fas fa-user" aria-hidden="true"></i>';
                                    var imgHtml = item.image
                                        ? '<img src="' + escHtml(item.image) + '" class="ap-result-img">'
                                        : '<span class="ap-result-icon">' + icon + '</span>';
                                    return '<div class="ap-result-item" data-type="' + escHtml(item.type) + '" data-id="' + item.id + '" data-label="' + escHtml(item.label) + '" data-sublabel="' + escHtml(item.sublabel) + '">' +
                                        imgHtml +
                                        '<div class="ap-result-info">' +
                                            '<span class="ap-result-name">' + escHtml(item.label) + '</span>' +
                                            (item.sublabel ? '<span class="ap-result-sub">' + escHtml(item.sublabel) + '</span>' : '') +
                                        '</div>' +
                                        '<span class="ap-result-type">' + (item.type === 'company' ? 'Firma' : item.type === 'directory' ? 'Verzeichnis' : item.type === 'user' ? 'Intern' : 'Kontakt') + '</span>' +
                                    '</div>';
                                }).join('');

                                resultsDiv.querySelectorAll('.ap-result-item').forEach(function(el) {
                                    el.addEventListener('click', function() {
                                        typeField.value = this.getAttribute('data-type');
                                        idField.value = this.getAttribute('data-id');
                                        var label = this.getAttribute('data-label');
                                        var sub = this.getAttribute('data-sublabel');
                                        selectedName.innerHTML = escHtml(label);
                                        if (sub) {
                                            selectedName.innerHTML += ' <small>(' + escHtml(sub) + ')</small>';
                                        }
                                        currentDiv.style.display = '';
                                        searchWrapper.style.display = 'none';
                                        searchInput.value = '';
                                        resultsDiv.style.display = 'none';
                                    });
                                });
                            }
                            resultsDiv.style.display = 'block';
                        });
                }, 300);
            });

            if (removeBtn) {
                removeBtn.addEventListener('click', function() {
                    typeField.value = '';
                    idField.value = '0';
                    selectedName.textContent = '';
                    currentDiv.style.display = 'none';
                    searchWrapper.style.display = '';
                });
            }

            document.addEventListener('click', function(e) {
                if (!searchInput.contains(e.target) && !resultsDiv.contains(e.target)) {
                    resultsDiv.style.display = 'none';
                }
            });
        })();
    });

    abschnitt('KI-Assistent', function () {
        // Tag-Auswahl
        (function() {
            var input = document.getElementById('tagSearchInput');
            var dropdown = document.getElementById('tagDropdown');
            var selectedList = document.getElementById('tagSelectedList');
            if (!input || !dropdown) return;

            var allItems = Array.from(dropdown.querySelectorAll('.tag-dropdown-item'));

            function getSelectedIds() {
                return Array.from(selectedList.querySelectorAll('input[name="tags[]"]')).map(function(i) {
                    return parseInt(i.value);
                });
            }

            function isSelected(id) {
                return getSelectedIds().indexOf(parseInt(id)) !== -1;
            }

            // Nur Hex-Farben in das style-Attribut übernehmen – ein freier Wert könnte
            // sonst weitere CSS-Eigenschaften einschleusen.
            function safeColor(value) {
                return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(String(value || '').trim())
                    ? String(value).trim()
                    : '#9ca3af';
            }

            function addTag(id, name, color) {
                if (isSelected(id)) return;
                color = safeColor(color);
                var chip = document.createElement('span');
                chip.className = 'tag-chip';
                chip.dataset.id = id;
                chip.style.cssText = 'background:' + color + '20;color:' + color + ';border:1px solid ' + color + '40;';
                chip.innerHTML = escHtml(name) +
                    '<button type="button" class="tag-chip-remove" data-id="' + id + '">&times;</button>' +
                    '<input type="hidden" name="tags[]" value="' + id + '">';
                selectedList.appendChild(chip);
                updateDropdownState();
            }

            function removeTag(id) {
                var chip = selectedList.querySelector('.tag-chip[data-id="' + id + '"]');
                if (chip) chip.remove();
                updateDropdownState();
            }

            function updateDropdownState() {
                allItems.forEach(function(item) {
                    if (isSelected(item.dataset.id)) {
                        item.classList.add('selected');
                    } else {
                        item.classList.remove('selected');
                    }
                });
            }

            function filterDropdown(query) {
                var q = query.toLowerCase();
                var hasMatch = false;
                allItems.forEach(function(item) {
                    var name = (item.dataset.name || '').toLowerCase();
                    var show = !q || name.indexOf(q) !== -1;
                    item.style.display = show ? '' : 'none';
                    if (show) hasMatch = true;
                });

                // "Erstellen" Option
                var createBtn = dropdown.querySelector('.tag-dropdown-create');
                if (createBtn) createBtn.remove();
                if (q && !hasMatch) {
                    var btn = document.createElement('div');
                    btn.className = 'tag-dropdown-create';
                    btn.textContent = '+ "' + query + '" erstellen';
                    btn.addEventListener('click', function() {
                        createNewTag(query);
                    });
                    dropdown.appendChild(btn);
                }
            }

            function createNewTag(name) {
                fetch('/api/tags', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-Token': (document.querySelector('input[name="_csrf"]') || {value: ''}).value,
                        'X-Requested-With': 'XMLHttpRequest'
                    },
                    body: JSON.stringify({ name: name })
                })
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    if (data.tag) {
                        // Neuen Tag zum Dropdown hinzufuegen
                        var item = document.createElement('div');
                        item.className = 'tag-dropdown-item';
                        item.dataset.id = data.tag.id;
                        item.dataset.name = data.tag.name;
                        item.dataset.color = safeColor(data.tag.color);
                        item.innerHTML = '<span class="tag-dropdown-color" style="background:' + safeColor(data.tag.color) + ';"></span>' +
                            escHtml(data.tag.name) +
                            '<span class="tag-dropdown-count">0</span>';
                        dropdown.insertBefore(item, dropdown.firstChild);
                        allItems = Array.from(dropdown.querySelectorAll('.tag-dropdown-item'));

                        item.addEventListener('click', function() {
                            toggleTag(this);
                        });

                        addTag(data.tag.id, data.tag.name, data.tag.color);
                        input.value = '';
                        dropdown.style.display = 'none';
                    }
                });
            }

            function toggleTag(item) {
                var id = item.dataset.id;
                if (isSelected(id)) {
                    removeTag(id);
                } else {
                    addTag(id, item.dataset.name, item.dataset.color);
                }
            }

            function escHtml(str) {
                var div = document.createElement('div');
                div.textContent = str || '';
                return div.innerHTML;
            }

            // Events
            input.addEventListener('focus', function() {
                filterDropdown(this.value);
                updateDropdownState();
                dropdown.style.display = '';
            });

            input.addEventListener('input', function() {
                filterDropdown(this.value);
                dropdown.style.display = '';
            });

            allItems.forEach(function(item) {
                item.addEventListener('click', function() {
                    toggleTag(this);
                });
            });

            selectedList.addEventListener('click', function(e) {
                var btn = e.target.closest('.tag-chip-remove');
                if (btn) {
                    removeTag(btn.dataset.id);
                }
            });

            document.addEventListener('click', function(e) {
                if (!input.contains(e.target) && !dropdown.contains(e.target) && !selectedList.contains(e.target)) {
                    dropdown.style.display = 'none';
                }
            });

            updateDropdownState();
        })();

        // === Vorlagen-Auswahl (Sidebar-Dropdown) ===
        (function() {
            var select = document.getElementById('template-select');
            if (!select) return;

            select.addEventListener('change', function() {
                var templateId = this.value;
                if (!templateId) return;

                if (typeof tinymce !== 'undefined' && tinymce.activeEditor && tinymce.activeEditor.getContent().trim()) {
                    if (!confirm('Vorhandenen Inhalt mit Vorlage ersetzen?')) {
                        this.value = '';
                        return;
                    }
                } else if (window._fallbackEditor && window._fallbackEditor.getContent().trim()) {
                    if (!confirm('Vorhandenen Inhalt mit Vorlage ersetzen?')) {
                        this.value = '';
                        return;
                    }
                }

                var xhr = new XMLHttpRequest();
                xhr.open('GET', '/api/page-templates/' + templateId);
                xhr.onload = function() {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        try {
                            var data = JSON.parse(xhr.responseText);
                            if (data.content_html !== undefined) {
                                if (typeof tinymce !== 'undefined' && tinymce.activeEditor) {
                                    tinymce.activeEditor.setContent(data.content_html);
                                } else if (window._fallbackEditor) {
                                    window._fallbackEditor.setContent(data.content_html);
                                }
                            }
                        } catch(e) {
                            console.error('Vorlagen-Parse-Fehler:', e);
                        }
                    }
                };
                xhr.send();
            });
        })();

        if (cfg.aiEnabled) {
        // === KI-Assistent Modal ===
        openAiEditorModal = function (editor) {
            var modal = document.getElementById('ai-editor-modal');
            if (!modal) return;

            var promptEl   = document.getElementById('ai-prompt');
            var errorEl    = document.getElementById('ai-modal-error');
            var submitBtn  = document.getElementById('ai-modal-submit');
            var submitText = document.getElementById('ai-modal-submit-text');
            var loadingEl  = document.getElementById('ai-modal-loading');
            var cancelBtn  = document.getElementById('ai-modal-cancel');
            var closeBtn   = document.getElementById('ai-modal-close');

            errorEl.style.display = 'none';
            promptEl.value = '';
            modal.style.display = 'flex';
            setTimeout(function() { promptEl.focus(); }, 50);

            function closeModal() {
                modal.style.display = 'none';
            }

            function setLoading(on) {
                submitBtn.disabled = on;
                submitText.style.display = on ? 'none' : '';
                loadingEl.style.display  = on ? '' : 'none';
            }

            function handleSubmit() {
                var instruction = promptEl.value.trim();
                if (!instruction) {
                    errorEl.textContent = 'Bitte eine Anweisung eingeben.';
                    errorEl.style.display = '';
                    promptEl.focus();
                    return;
                }

                errorEl.style.display = 'none';
                setLoading(true);

                // Gesamter Editor-Inhalt als Kontext
                var currentContent = editor.getContent();
                var csrf = (document.querySelector('input[name="_csrf"]') || {value: ''}).value;

                fetch('/api/ai-chat/page-content', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                        'X-Requested-With': 'XMLHttpRequest'
                    },
                    body: 'instruction='    + encodeURIComponent(instruction)
                        + '&current_content='+ encodeURIComponent(currentContent)
                        + '&_csrf='          + encodeURIComponent(csrf)
                })
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    setLoading(false);
                    if (data.error) {
                        errorEl.textContent = data.error;
                        errorEl.style.display = '';
                        return;
                    }
                    if (data.content) {
                        editor.setContent(data.content);
                        // Änderungsmarkierung setzen
                        var btn = document.querySelector('.btn-save');
                        if (btn) btn.classList.add('has-changes');
                        var hint = document.getElementById('change-reason-hint');
                        if (hint) hint.style.display = 'inline-flex';
                        closeModal();
                    }
                })
                .catch(function(err) {
                    setLoading(false);
                    errorEl.textContent = 'Netzwerkfehler: ' + err.message;
                    errorEl.style.display = '';
                });
            }

            submitBtn.onclick  = handleSubmit;
            cancelBtn.onclick  = closeModal;
            closeBtn.onclick   = closeModal;

            // ESC schließt Modal, Ctrl+Enter sendet
            promptEl.onkeydown = function(e) {
                if (e.key === 'Escape') closeModal();
                if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) handleSubmit();
            };

            // Klick auf Backdrop schließt Modal
            modal.onclick = function(e) {
                if (e.target === modal) closeModal();
            };
        }
        }
    });

    abschnitt('Sperre', function () {
        (function() {
            var pageId  = cfg.pageId;
            var timer   = null;
            var released = false;

            function post(url) {
                // Immer frischen CSRF-Token lesen (nie stale nach langer Session)
                var csrf = (document.querySelector('input[name="_csrf"]') || {value: ''}).value;
                return fetch(url, {
                    method: 'POST',
                    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                    body: '_csrf=' + encodeURIComponent(csrf),
                    keepalive: true,
                });
            }

            function showBanner(text) {
                var b = document.getElementById('lock-banner');
                if (!b) return;
                document.getElementById('lock-banner-text').textContent = text;
                b.style.display = 'flex';
                document.body.style.paddingTop = b.offsetHeight + 'px';
            }
            function hideBanner() {
                var b = document.getElementById('lock-banner');
                if (b) b.style.display = 'none';
                document.body.style.paddingTop = '';
            }

            function keepalive() {
                post('/pages/' + pageId + '/lock/keepalive')
                    .then(function(r) { return r.json(); })
                    .then(function(d) {
                        if (!d.ok) {
                            showBanner('⚠ Sperre verloren – ' + (d.locked_by || 'anderer Benutzer') + ' bearbeitet diese Seite jetzt.');
                            clearInterval(timer);
                        } else {
                            hideBanner();
                        }
                    })
                    .catch(function() { /* netzwerkfehler ignorieren */ });
            }

            function release() {
                if (released) return;
                released = true;
                clearInterval(timer);
                // sendBeacon ist zuverlässiger beim Seitenwechsel
                var data = new FormData();
                data.append('_csrf', (document.querySelector('input[name="_csrf"]') || {value: ''}).value);
                data.append('_method', 'DELETE');
                navigator.sendBeacon('/pages/' + pageId + '/lock', data);
            }

            // Heartbeat alle 60 Sekunden
            timer = setInterval(keepalive, 60000);

            // Lock freigeben wenn Seite verlassen wird
            window.addEventListener('pagehide', release);
            window.addEventListener('beforeunload', release);

            // Beim Absenden des Formulars: Lock wird serverseitig gelöscht, kein manueller Release nötig
            document.getElementById('page-form')?.addEventListener('submit', function() {
                clearInterval(timer);
                released = true;
            });
        })();
    });

    abschnitt('Entwurf', function () {
        // === Autosave ===
        if (cfg.isEdit) {
        (function() {
            var pageId      = cfg.pageId;
            var draftContent = cfg.draftContent;
            var inactivityTimer = null;
            var intervalTimer   = null;
            var statusEl = document.getElementById('autosave-status');

            function setStatus(text, type) {
                if (!statusEl) return;
                statusEl.textContent = text;
                statusEl.className = 'autosave-status autosave-status-' + (type || 'idle');
                statusEl.style.display = text ? 'inline-flex' : 'none';
            }

            // Einheitlicher Zugriff auf den Inhalt, unabhängig von der Editor-Variante:
            // TinyMCE, der einfache Editor oder – solange keiner bereit ist – das
            // Textfeld selbst. Ohne diese Abstraktion liefe das Zwischenspeichern nur
            // mit TinyMCE.
            function editorApi() {
                if (typeof tinymce !== 'undefined' && tinymce.activeEditor) {
                    var ed = tinymce.activeEditor;
                    return {
                        get: function () { return ed.getContent(); },
                        set: function (html) { ed.setContent(html); },
                        onChange: function (cb) { ed.on('change input keyup', cb); }
                    };
                }
                if (window._fallbackEditor) {
                    var fe = window._fallbackEditor;
                    return {
                        get: function () { return fe.getContent(); },
                        set: function (html) { fe.setContent(html); },
                        onChange: function (cb) {
                            // Der einfache Editor kennt genau einen Änderungs-Rückruf –
                            // den vorhandenen erhalten und den eigenen anhängen.
                            var prev = fe.options && fe.options.onChange;
                            if (fe.options) {
                                fe.options.onChange = function (content) {
                                    if (typeof prev === 'function') { prev(content); }
                                    cb();
                                };
                            }
                        }
                    };
                }
                var ta = document.getElementById('content-editor');
                if (ta) {
                    return {
                        get: function () { return ta.value; },
                        set: function (html) { ta.value = html; },
                        onChange: function (cb) { ta.addEventListener('input', cb); }
                    };
                }
                return null;
            }

            function doAutosave() {
                var api = editorApi();
                if (!api) return;
                var content = api.get();
                var formData = new FormData();
                formData.append('content', content);
                formData.append('_csrf', (document.querySelector('input[name="_csrf"]') || {value: ''}).value);

                setStatus('Speichert…', 'saving');

                fetch('/pages/' + pageId + '/autosave', {
                    method: 'POST',
                    body: formData,
                    credentials: 'same-origin'
                })
                .then(function(r) { return r.json(); })
                .then(function(d) {
                    if (d.ok) {
                        var now = new Date();
                        var time = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0');
                        setStatus('Entwurf gespeichert ' + time, 'saved');
                    }
                })
                .catch(function() {
                    setStatus('Autosave fehlgeschlagen', 'error');
                });
            }

            // Warten bis ein Editor bereit ist (TinyMCE initialisiert asynchron).
            var initAttempts = 0;
            var checkInit = setInterval(function() {
                var api = editorApi();
                if (!api) {
                    // Nach rund 15 Sekunden aufgeben, damit der Timer nicht dauerhaft läuft.
                    if (++initAttempts > 50) { clearInterval(checkInit); }
                    return;
                }
                clearInterval(checkInit);

                // Bei jeder Änderung: nach 10s Inaktivität speichern
                api.onChange(function() {
                    clearTimeout(inactivityTimer);
                    inactivityTimer = setTimeout(doAutosave, 10000);
                });

                // Zusätzlich alle 60s
                intervalTimer = setInterval(doAutosave, 60000);
            }, 300);

            // Entwurf-Dialog sofort verdrahten – unabhängig davon, ob der Editor schon
            // bereit ist. Ein früher Klick landete sonst ins Leere.
            (function() {
                var draftModal = document.getElementById('draft-modal');
                var restoreBtn = document.getElementById('draft-restore-btn');
                var discardBtn = document.getElementById('draft-discard-btn');

                function closeDraftModal() {
                    if (draftModal) { draftModal.style.display = 'none'; }
                }

                if (restoreBtn && draftContent) {
                    restoreBtn.addEventListener('click', function() {
                        var api = editorApi();
                        if (api) { api.set(draftContent); }
                        closeDraftModal();
                        var saveBtn = document.querySelector('.btn-save');
                        if (saveBtn) saveBtn.classList.add('has-changes');
                        var hint = document.getElementById('change-reason-hint');
                        if (hint) hint.style.display = 'inline-flex';
                        setStatus('Entwurf wiederhergestellt', 'saved');
                    });
                }

                if (discardBtn) {
                    discardBtn.addEventListener('click', function() {
                        closeDraftModal();
                        // Draft serverseitig leeren
                        var fd = new FormData();
                        fd.append('content', '');
                        fd.append('_csrf', (document.querySelector('input[name="_csrf"]') || {value: ''}).value);
                        fetch('/pages/' + pageId + '/autosave', {method: 'POST', body: fd, credentials: 'same-origin'});
                    });
                }

                if (draftModal) {
                    draftModal.addEventListener('click', function(e) {
                        if (e.target === draftModal) { closeDraftModal(); }
                    });
                    document.addEventListener('keydown', function(e) {
                        if (e.key === 'Escape' && draftModal.style.display !== 'none') { closeDraftModal(); }
                    });
                }
            })();

            // Autosave-Timer beim Absenden stoppen
            document.getElementById('page-form') && document.getElementById('page-form').addEventListener('submit', function() {
                clearTimeout(inactivityTimer);
                clearInterval(intervalTimer);
            });
        })();
        }
    });

})();