Summernote WYSIWYG was mangling Django template syntax ({{ }}, {% %})
on save, causing content to revert to corrupted state. Switched all
template types to the plain code editor textarea which preserves
content exactly as-is.
Also removed jQuery/Summernote JS dependencies from the editor page,
and fixed getEditorContent reference in preview code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
257 lines
10 KiB
HTML
257 lines
10 KiB
HTML
{% extends 'base.html' %}
|
||
{% block title %}{{ vorlage.bezeichnung }} – Vorlage bearbeiten{% endblock %}
|
||
|
||
{% block extra_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;
|
||
}
|
||
.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> ·
|
||
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" class="code-editor-textarea">{{ 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 %}
|
||
<script>
|
||
(function() {
|
||
var initialContent;
|
||
try {
|
||
initialContent = JSON.parse(document.getElementById('vorlage-html-inhalt').textContent);
|
||
} catch(e) {
|
||
initialContent = null;
|
||
}
|
||
|
||
var editor = document.getElementById('code-editor');
|
||
|
||
// ---- Editor setup: plain code editor for all templates ----
|
||
// Always load content from JSON (the textarea's Django-rendered value may be HTML-escaped)
|
||
if (initialContent !== null) {
|
||
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
|
||
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 = editor.value;
|
||
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 %}
|