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>
This commit is contained in:
@@ -9,13 +9,13 @@
|
|||||||
<style>
|
<style>
|
||||||
.preview-frame {
|
.preview-frame {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 580px;
|
height: 75vh;
|
||||||
border: 1px solid #dee2e6;
|
border: 1px solid #dee2e6;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.var-list {
|
.var-list {
|
||||||
max-height: 400px;
|
max-height: 55vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.var-item {
|
.var-item {
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
.code-editor-textarea {
|
.code-editor-textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 520px;
|
height: 70vh;
|
||||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@@ -50,6 +50,11 @@
|
|||||||
border-color: #86b7fe;
|
border-color: #86b7fe;
|
||||||
box-shadow: 0 0 0 .25rem rgba(13,110,253,.25);
|
box-shadow: 0 0 0 .25rem rgba(13,110,253,.25);
|
||||||
}
|
}
|
||||||
|
#tab-vorschau .preview-loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 80px 0;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -66,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<a href="{% url 'stiftung:vorlagen_liste' %}" class="btn btn-sm btn-outline-secondary">
|
<a href="{% url 'stiftung:vorlagen_liste' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
<i class="fas fa-arrow-left me-1"></i>Zurück zur Liste
|
<i class="fas fa-arrow-left me-1"></i>Zurück
|
||||||
</a>
|
</a>
|
||||||
{% if hat_original %}
|
{% if hat_original %}
|
||||||
<form method="post" action="{% url 'stiftung:vorlage_zuruecksetzen' pk=vorlage.pk %}"
|
<form method="post" action="{% url 'stiftung:vorlage_zuruecksetzen' pk=vorlage.pk %}"
|
||||||
@@ -82,42 +87,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<!-- Tabs: Editor | Vorschau -->
|
||||||
<!-- Editor (links) -->
|
<ul class="nav nav-tabs mb-3" role="tablist">
|
||||||
<div class="col-lg-8">
|
<li class="nav-item">
|
||||||
<form method="post" id="editor-form">
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-editor" type="button" role="tab">
|
||||||
{% csrf_token %}
|
<i class="fas fa-code me-1"></i>Editor
|
||||||
<div class="d-flex gap-2 mb-2 justify-content-end">
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btn-preview">
|
</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
|
<i class="fas fa-eye me-1"></i>Vorschau
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" class="btn btn-sm btn-primary">
|
</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
|
<i class="fas fa-save me-1"></i>Speichern
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
<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>
|
<script type="application/json" id="vorlage-html-inhalt">{{ html_inhalt_json }}</script>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Vorschau-Bereich (initial versteckt) -->
|
|
||||||
<div id="preview-area" class="mt-3" style="display:none">
|
|
||||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
||||||
<strong>Vorschau <span class="badge bg-secondary">Beispieldaten</span></strong>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btn-close-preview">
|
|
||||||
<i class="fas fa-times me-1"></i>Vorschau schließen
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<iframe id="preview-frame" class="preview-frame" title="Vorschau"></iframe>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Seitenleiste (rechts) -->
|
|
||||||
<div class="col-lg-4">
|
|
||||||
{% if variablen %}
|
{% if variablen %}
|
||||||
|
<div class="col-lg-3">
|
||||||
<div class="card shadow mb-3">
|
<div class="card shadow mb-3">
|
||||||
<div class="card-header py-2">
|
<div class="card-header py-2">
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
<h6 class="m-0 fw-bold text-primary small">
|
||||||
<i class="fas fa-code me-1"></i>Verfügbare Variablen
|
<i class="fas fa-code me-1"></i>Variablen
|
||||||
</h6>
|
</h6>
|
||||||
<small class="text-muted">Klick zum Einfügen</small>
|
<small class="text-muted">Klick zum Einfügen</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,11 +143,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="card shadow">
|
<div class="card shadow">
|
||||||
<div class="card-header py-2">
|
<div class="card-header py-2">
|
||||||
<h6 class="m-0 font-weight-bold text-secondary">
|
<h6 class="m-0 fw-bold text-secondary small">
|
||||||
<i class="fas fa-info-circle me-1"></i>Info
|
<i class="fas fa-info-circle me-1"></i>Info
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,6 +163,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -180,15 +195,22 @@
|
|||||||
|
|
||||||
var useCodeEditor = {{ use_code_editor|yesno:"true,false" }};
|
var useCodeEditor = {{ use_code_editor|yesno:"true,false" }};
|
||||||
var editor = document.getElementById('code-editor');
|
var editor = document.getElementById('code-editor');
|
||||||
|
var summernoteActive = false;
|
||||||
|
|
||||||
// Code-Editor-Modus: Plain textarea fuer vollstaendige HTML-Dokumente
|
// Returns current HTML content regardless of editor mode
|
||||||
// (Serienbrief-Vorlagen mit DOCTYPE, Template-Tags usw.)
|
function getEditorContent() {
|
||||||
|
if (summernoteActive) {
|
||||||
|
return $('#code-editor').summernote('code');
|
||||||
|
}
|
||||||
|
return editor.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Editor setup ----
|
||||||
if (useCodeEditor) {
|
if (useCodeEditor) {
|
||||||
|
// Plain textarea for full HTML documents (Serienbrief)
|
||||||
if (initialContent) {
|
if (initialContent) {
|
||||||
editor.value = initialContent;
|
editor.value = initialContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab-Taste einfügen statt Fokus wechseln
|
|
||||||
editor.addEventListener('keydown', function(e) {
|
editor.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Tab') {
|
if (e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -198,8 +220,7 @@
|
|||||||
this.selectionStart = this.selectionEnd = start + 4;
|
this.selectionStart = this.selectionEnd = start + 4;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Variable insertion for code editor
|
||||||
// Variablen einfügen bei Klick
|
|
||||||
document.querySelectorAll('.var-item').forEach(function(row) {
|
document.querySelectorAll('.var-item').forEach(function(row) {
|
||||||
row.addEventListener('click', function() {
|
row.addEventListener('click', function() {
|
||||||
var varName = this.getAttribute('data-var');
|
var varName = this.getAttribute('data-var');
|
||||||
@@ -210,37 +231,8 @@
|
|||||||
editor.focus();
|
editor.focus();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} else if (typeof $ !== 'undefined' && typeof $.fn.summernote !== 'undefined') {
|
||||||
// Vorschau
|
// Summernote WYSIWYG for HTML fragment templates
|
||||||
var previewArea = document.getElementById('preview-area');
|
|
||||||
var previewFrame = document.getElementById('preview-frame');
|
|
||||||
document.getElementById('btn-preview').addEventListener('click', function() {
|
|
||||||
var formData = new FormData();
|
|
||||||
formData.append('html_inhalt', editor.value);
|
|
||||||
formData.append('csrfmiddlewaretoken', document.querySelector('[name=csrfmiddlewaretoken]').value);
|
|
||||||
fetch('{% url "stiftung:vorlage_vorschau" pk=vorlage.pk %}', { method: 'POST', body: formData })
|
|
||||||
.then(function(r) { return r.text(); })
|
|
||||||
.then(function(html) {
|
|
||||||
previewFrame.srcdoc = html;
|
|
||||||
previewArea.style.display = 'block';
|
|
||||||
previewArea.scrollIntoView({behavior: 'smooth'});
|
|
||||||
})
|
|
||||||
.catch(function(err) { alert('Vorschau fehlgeschlagen: ' + err); });
|
|
||||||
});
|
|
||||||
document.getElementById('btn-close-preview').addEventListener('click', function() {
|
|
||||||
previewArea.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
return; // Skip Summernote initialization
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof $ === 'undefined' || typeof $.fn.summernote === 'undefined') {
|
|
||||||
// Fallback: Summernote nicht geladen — Textarea sichtbar lassen
|
|
||||||
if (editor) { editor.style.height = '520px'; editor.style.fontFamily = 'monospace'; editor.style.fontSize = '13px'; }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summernote initialisieren (für HTML-Fragment-Vorlagen: E-Mail, PDF-Fragmente)
|
|
||||||
$('#code-editor').summernote({
|
$('#code-editor').summernote({
|
||||||
lang: 'de-DE',
|
lang: 'de-DE',
|
||||||
height: 520,
|
height: 520,
|
||||||
@@ -259,52 +251,80 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
summernoteActive = true;
|
||||||
|
|
||||||
// Variablen einfügen bei Klick
|
// Variable insertion for Summernote
|
||||||
document.querySelectorAll('.var-item').forEach(function(row) {
|
document.querySelectorAll('.var-item').forEach(function(row) {
|
||||||
row.addEventListener('click', function() {
|
row.addEventListener('click', function() {
|
||||||
const varName = this.getAttribute('data-var');
|
var varName = this.getAttribute('data-var');
|
||||||
const placeholder = String.fromCharCode(123,123) + ' ' + varName + ' ' + String.fromCharCode(125,125);
|
var placeholder = String.fromCharCode(123,123) + ' ' + varName + ' ' + String.fromCharCode(125,125);
|
||||||
$('#code-editor').summernote('focus');
|
$('#code-editor').summernote('focus');
|
||||||
$('#code-editor').summernote('insertText', placeholder);
|
$('#code-editor').summernote('insertText', placeholder);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Vorschau
|
// Sync Summernote to textarea on form submit
|
||||||
const previewArea = document.getElementById('preview-area');
|
document.getElementById('editor-form').addEventListener('submit', function() {
|
||||||
const previewFrame = document.getElementById('preview-frame');
|
document.querySelector('textarea[name=html_inhalt]').value = getEditorContent();
|
||||||
const btnPreview = document.getElementById('btn-preview');
|
});
|
||||||
const btnClosePreview = document.getElementById('btn-close-preview');
|
} 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
btnPreview.addEventListener('click', function() {
|
// ---- Preview (always set up, independent of editor mode) ----
|
||||||
const content = $('#code-editor').summernote('code');
|
var previewFrame = document.getElementById('preview-frame');
|
||||||
const formData = new FormData();
|
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('html_inhalt', content);
|
||||||
formData.append('csrfmiddlewaretoken', document.querySelector('[name=csrfmiddlewaretoken]').value);
|
formData.append('csrfmiddlewaretoken', document.querySelector('[name=csrfmiddlewaretoken]').value);
|
||||||
|
|
||||||
|
previewLoading.style.display = 'block';
|
||||||
|
previewFrame.style.display = 'none';
|
||||||
|
|
||||||
fetch('{% url "stiftung:vorlage_vorschau" pk=vorlage.pk %}', {
|
fetch('{% url "stiftung:vorlage_vorschau" pk=vorlage.pk %}', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
.then(r => r.text())
|
.then(function(r) { return r.text(); })
|
||||||
.then(html => {
|
.then(function(html) {
|
||||||
previewFrame.srcdoc = html;
|
previewFrame.srcdoc = html;
|
||||||
previewArea.style.display = 'block';
|
previewFrame.style.display = 'block';
|
||||||
previewArea.scrollIntoView({behavior: 'smooth'});
|
previewLoading.style.display = 'none';
|
||||||
|
previewLoaded = true;
|
||||||
})
|
})
|
||||||
.catch(err => alert('Vorschau fehlgeschlagen: ' + err));
|
.catch(function(err) {
|
||||||
|
previewLoading.innerHTML = '<i class="fas fa-exclamation-triangle text-danger fa-2x mb-2 d-block"></i>Vorschau fehlgeschlagen: ' + err;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
btnClosePreview.addEventListener('click', function() {
|
// Load preview when switching to the Vorschau tab
|
||||||
previewArea.style.display = 'none';
|
document.getElementById('btn-tab-vorschau').addEventListener('shown.bs.tab', function() {
|
||||||
});
|
loadPreview();
|
||||||
|
|
||||||
// Formular-Submit: Summernote-Inhalt in Textarea schreiben
|
|
||||||
document.getElementById('editor-form').addEventListener('submit', function() {
|
|
||||||
// Summernote schreibt den Inhalt automatisch in die Textarea beim Submit
|
|
||||||
// Sicherheitshalber explizit synchronisieren:
|
|
||||||
const content = $('#code-editor').summernote('code');
|
|
||||||
document.querySelector('textarea[name=html_inhalt]').value = content;
|
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user