Files
stiftung-management-system/app/templates/stiftung/vorlage_editor.html
SysAdmin Agent d7992558ee Fix Vorlagen preview: use click handler instead of Bootstrap tab event (STI-82)
The shown.bs.tab event never fired, leaving the preview spinner forever.
Switched to a direct click handler with setTimeout for reliability.
Also added explicit credentials and HTTP error handling to the fetch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 21:42:56 +00:00

340 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'base.html' %}
{% load static %}
{% block title %}{{ vorlage.bezeichnung }} Vorlage bearbeiten{% endblock %}
{% block extra_css %}
<!-- Summernote WYSIWYG (lokal) -->
<link rel="stylesheet" href="{% static 'stiftung/vendor/summernote/summernote-bs5.min.css' %}">
<style>
.preview-frame {
width: 100%;
height: 75vh;
border: 1px solid #dee2e6;
border-radius: 4px;
background: #fff;
}
.var-list {
max-height: 55vh;
overflow-y: auto;
}
.var-item {
cursor: pointer;
font-family: monospace;
font-size: 12px;
}
.var-item:hover {
background-color: #e9ecef;
}
.note-editor.note-frame {
border-radius: 4px;
}
.code-editor-textarea {
width: 100%;
height: 70vh;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 13px;
line-height: 1.5;
padding: 12px;
border: 1px solid #dee2e6;
border-radius: 4px;
resize: vertical;
tab-size: 4;
white-space: pre;
overflow-wrap: normal;
overflow-x: auto;
background: #f8f9fa;
}
.code-editor-textarea:focus {
outline: none;
border-color: #86b7fe;
box-shadow: 0 0 0 .25rem rgba(13,110,253,.25);
}
#tab-vorschau .preview-loading {
text-align: center;
padding: 80px 0;
color: #6c757d;
}
</style>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="d-sm-flex align-items-center justify-content-between mb-3">
<div>
<h1 class="h3 mb-0 text-gray-800">
<i class="fas fa-file-code me-2"></i>{{ vorlage.bezeichnung }}
</h1>
<small class="text-muted"><code>{{ vorlage.schluessel }}</code> &nbsp;·&nbsp;
Kategorie: {{ vorlage.get_kategorie_display }}</small>
</div>
<div class="d-flex gap-2">
<a href="{% url 'stiftung:vorlagen_liste' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Zurück
</a>
{% if hat_original %}
<form method="post" action="{% url 'stiftung:vorlage_zuruecksetzen' pk=vorlage.pk %}"
onsubmit="return confirm('Alle Änderungen verwerfen und auf Original zurücksetzen?')">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-warning">
<i class="fas fa-undo me-1"></i>Original wiederherstellen
</button>
</form>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Tabs: Editor | Vorschau -->
<ul class="nav nav-tabs mb-3" role="tablist">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-editor" type="button" role="tab">
<i class="fas fa-code me-1"></i>Editor
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-vorschau" type="button" role="tab" id="btn-tab-vorschau">
<i class="fas fa-eye me-1"></i>Vorschau
</button>
</li>
<li class="nav-item ms-auto">
<button type="submit" form="editor-form" class="btn btn-sm btn-primary my-1">
<i class="fas fa-save me-1"></i>Speichern
</button>
</li>
</ul>
<div class="tab-content">
<!-- Editor Tab -->
<div class="tab-pane fade show active" id="tab-editor" role="tabpanel">
<div class="row">
<div class="{% if variablen %}col-lg-9{% else %}col-12{% endif %}">
<form method="post" id="editor-form">
{% csrf_token %}
<textarea name="html_inhalt" id="code-editor"{% if use_code_editor %} class="code-editor-textarea"{% endif %}>{{ vorlage.html_inhalt }}</textarea>
<script type="application/json" id="vorlage-html-inhalt">{{ html_inhalt_json }}</script>
</form>
</div>
{% if variablen %}
<div class="col-lg-3">
<div class="card shadow mb-3">
<div class="card-header py-2">
<h6 class="m-0 fw-bold text-primary small">
<i class="fas fa-code me-1"></i>Variablen
</h6>
<small class="text-muted">Klick zum Einfügen</small>
</div>
<div class="card-body p-0">
<div class="var-list">
<table class="table table-sm mb-0">
<tbody>
{% for var, beschreibung in variablen.items %}
<tr class="var-item" data-var="{{ var }}">
<td class="ps-3 py-1">
<code>{% templatetag openvariable %} {{ var }} {% templatetag closevariable %}</code>
</td>
<td class="pe-3 py-1 text-muted small">{{ beschreibung }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="card shadow">
<div class="card-header py-2">
<h6 class="m-0 fw-bold text-secondary small">
<i class="fas fa-info-circle me-1"></i>Info
</h6>
</div>
<div class="card-body small">
<dl class="mb-0">
<dt>Zuletzt bearbeitet</dt>
<dd class="text-muted">{{ vorlage.zuletzt_bearbeitet_am|date:"d.m.Y H:i" }}</dd>
{% if vorlage.zuletzt_bearbeitet_von %}
<dt>Bearbeitet von</dt>
<dd class="text-muted">{{ vorlage.zuletzt_bearbeitet_von.get_full_name|default:vorlage.zuletzt_bearbeitet_von.username }}</dd>
{% endif %}
<dt>Erstellt am</dt>
<dd class="text-muted mb-0">{{ vorlage.erstellt_am|date:"d.m.Y" }}</dd>
</dl>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Vorschau Tab -->
<div class="tab-pane fade" id="tab-vorschau" role="tabpanel">
<div class="preview-loading" id="preview-loading">
<i class="fas fa-spinner fa-spin fa-2x mb-2 d-block"></i>
Vorschau wird geladen...
</div>
<iframe id="preview-frame" class="preview-frame" title="Vorschau" style="display:none"></iframe>
</div>
</div>
{% endblock %}
{% block javascript %}
<!-- jQuery (lokal) -->
<script src="{% static 'stiftung/vendor/jquery/jquery.min.js' %}"></script>
<!-- Summernote WYSIWYG (lokal) -->
<script src="{% static 'stiftung/vendor/summernote/summernote-bs5.min.js' %}"></script>
<script src="{% static 'stiftung/vendor/summernote/summernote-de-DE.min.js' %}"></script>
<script>
(function() {
var initialContent;
try {
initialContent = JSON.parse(document.getElementById('vorlage-html-inhalt').textContent);
} catch(e) {
initialContent = null;
}
var useCodeEditor = {{ use_code_editor|yesno:"true,false" }};
var editor = document.getElementById('code-editor');
var summernoteActive = false;
// Returns current HTML content regardless of editor mode
function getEditorContent() {
if (summernoteActive) {
return $('#code-editor').summernote('code');
}
return editor.value;
}
// ---- Editor setup ----
if (useCodeEditor) {
// Plain textarea for full HTML documents (Serienbrief)
if (initialContent) {
editor.value = initialContent;
}
editor.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
e.preventDefault();
var start = this.selectionStart;
var end = this.selectionEnd;
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 4;
}
});
// Variable insertion for code editor
document.querySelectorAll('.var-item').forEach(function(row) {
row.addEventListener('click', function() {
var varName = this.getAttribute('data-var');
var placeholder = String.fromCharCode(123,123) + ' ' + varName + ' ' + String.fromCharCode(125,125);
var start = editor.selectionStart;
editor.value = editor.value.substring(0, start) + placeholder + editor.value.substring(editor.selectionEnd);
editor.selectionStart = editor.selectionEnd = start + placeholder.length;
editor.focus();
});
});
} else if (typeof $ !== 'undefined' && typeof $.fn.summernote !== 'undefined') {
// Summernote WYSIWYG for HTML fragment templates
$('#code-editor').summernote({
lang: 'de-DE',
height: 520,
toolbar: [
['style', ['bold', 'italic', 'underline', 'strikethrough', 'clear']],
['para', ['style', 'ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'hr']],
['view', ['fullscreen', 'codeview', 'undo', 'redo']],
],
callbacks: {
onInit: function() {
if (initialContent) {
$('#code-editor').summernote('code', initialContent);
}
}
}
});
summernoteActive = true;
// Variable insertion for Summernote
document.querySelectorAll('.var-item').forEach(function(row) {
row.addEventListener('click', function() {
var varName = this.getAttribute('data-var');
var placeholder = String.fromCharCode(123,123) + ' ' + varName + ' ' + String.fromCharCode(125,125);
$('#code-editor').summernote('focus');
$('#code-editor').summernote('insertText', placeholder);
});
});
// Sync Summernote to textarea on form submit
document.getElementById('editor-form').addEventListener('submit', function() {
document.querySelector('textarea[name=html_inhalt]').value = getEditorContent();
});
} else {
// Fallback: Summernote not loaded — style textarea as code editor
if (editor) {
editor.style.height = '70vh';
editor.style.fontFamily = "'SFMono-Regular', Consolas, monospace";
editor.style.fontSize = '13px';
editor.style.padding = '12px';
editor.style.background = '#f8f9fa';
}
if (initialContent) {
editor.value = initialContent;
}
// Variable insertion for plain textarea fallback
document.querySelectorAll('.var-item').forEach(function(row) {
row.addEventListener('click', function() {
var varName = this.getAttribute('data-var');
var placeholder = String.fromCharCode(123,123) + ' ' + varName + ' ' + String.fromCharCode(125,125);
var start = editor.selectionStart;
editor.value = editor.value.substring(0, start) + placeholder + editor.value.substring(editor.selectionEnd);
editor.selectionStart = editor.selectionEnd = start + placeholder.length;
editor.focus();
});
});
}
// ---- Preview (always set up, independent of editor mode) ----
var previewFrame = document.getElementById('preview-frame');
var previewLoading = document.getElementById('preview-loading');
function loadPreview() {
var content = getEditorContent();
var csrfEl = document.querySelector('[name=csrfmiddlewaretoken]');
if (!csrfEl) {
previewLoading.innerHTML = '<i class="fas fa-exclamation-triangle text-danger fa-2x mb-2 d-block"></i>CSRF-Token nicht gefunden.';
return;
}
var formData = new FormData();
formData.append('html_inhalt', content);
formData.append('csrfmiddlewaretoken', csrfEl.value);
previewLoading.style.display = 'block';
previewFrame.style.display = 'none';
fetch('{% url "stiftung:vorlage_vorschau" pk=vorlage.pk %}', {
method: 'POST',
body: formData,
credentials: 'same-origin',
})
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.text();
})
.then(function(html) {
previewFrame.srcdoc = html;
previewFrame.style.display = 'block';
previewLoading.style.display = 'none';
})
.catch(function(err) {
previewLoading.innerHTML = '<i class="fas fa-exclamation-triangle text-danger fa-2x mb-2 d-block"></i>Vorschau fehlgeschlagen: ' + err;
});
}
// Load preview when clicking the Vorschau tab (direct click — more reliable than Bootstrap tab events)
document.getElementById('btn-tab-vorschau').addEventListener('click', function() {
// Small delay so the tab pane is visible before we load
setTimeout(loadPreview, 100);
});
})();
</script>
{% endblock %}