Files
stiftung-management-system/app/templates/stiftung/vorlage_editor.html
SysAdmin Agent 0e129ae56a Fix Vorlagen editor: working preview tab and improved layout
- Fix preview bug: preview event handlers were never attached when
  Summernote failed to load (fallback returned early at line 240)
- Restructure layout with Bootstrap tabs (Editor | Vorschau) instead
  of stacked editor+hidden preview
- Preview loads automatically when switching to the Vorschau tab
- Editor content getter works in all modes (Summernote, code, fallback)
- Editor now uses full viewport height for more editing space
- Variables sidebar gets 3 cols (was 4) giving editor 9 cols (was 8)

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

332 lines
13 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');
var previewLoaded = false;
function loadPreview() {
var content = getEditorContent();
var formData = new FormData();
formData.append('html_inhalt', content);
formData.append('csrfmiddlewaretoken', document.querySelector('[name=csrfmiddlewaretoken]').value);
previewLoading.style.display = 'block';
previewFrame.style.display = 'none';
fetch('{% url "stiftung:vorlage_vorschau" pk=vorlage.pk %}', {
method: 'POST',
body: formData,
})
.then(function(r) { return r.text(); })
.then(function(html) {
previewFrame.srcdoc = html;
previewFrame.style.display = 'block';
previewLoading.style.display = 'none';
previewLoaded = true;
})
.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 switching to the Vorschau tab
document.getElementById('btn-tab-vorschau').addEventListener('shown.bs.tab', function() {
loadPreview();
});
})();
</script>
{% endblock %}