Fix Vorlagen editor: drop Summernote, use code editor for all templates (STI-82)
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>
This commit is contained in:
@@ -57,10 +57,9 @@ def vorlage_editor(request, pk):
|
|||||||
html_json = json.dumps(vorlage.html_inhalt)
|
html_json = json.dumps(vorlage.html_inhalt)
|
||||||
html_json = html_json.replace("<", "\\u003c").replace(">", "\\u003e")
|
html_json = html_json.replace("<", "\\u003c").replace(">", "\\u003e")
|
||||||
|
|
||||||
# Serienbrief templates are full HTML documents with Django template tags
|
# All templates contain Django template tags ({{ }}, {% %}) that
|
||||||
# ({% for %}, {% if %}) — Summernote WYSIWYG mangles these.
|
# Summernote WYSIWYG mangles on save. Use plain code editor for all.
|
||||||
# Use a plain code editor textarea instead.
|
use_code_editor = True
|
||||||
use_code_editor = vorlage.kategorie == "serienbrief"
|
|
||||||
|
|
||||||
return render(request, "stiftung/vorlage_editor.html", {
|
return render(request, "stiftung/vorlage_editor.html", {
|
||||||
"vorlage": vorlage,
|
"vorlage": vorlage,
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}{{ vorlage.bezeichnung }} – Vorlage bearbeiten{% endblock %}
|
{% block title %}{{ vorlage.bezeichnung }} – Vorlage bearbeiten{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<!-- Summernote WYSIWYG (lokal) -->
|
|
||||||
<link rel="stylesheet" href="{% static 'stiftung/vendor/summernote/summernote-bs5.min.css' %}">
|
|
||||||
<style>
|
<style>
|
||||||
.preview-frame {
|
.preview-frame {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -26,9 +22,6 @@
|
|||||||
.var-item:hover {
|
.var-item:hover {
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
}
|
}
|
||||||
.note-editor.note-frame {
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.code-editor-textarea {
|
.code-editor-textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 70vh;
|
height: 70vh;
|
||||||
@@ -113,7 +106,7 @@
|
|||||||
<div class="{% if variablen %}col-lg-9{% else %}col-12{% endif %}">
|
<div class="{% if variablen %}col-lg-9{% else %}col-12{% endif %}">
|
||||||
<form method="post" id="editor-form">
|
<form method="post" id="editor-form">
|
||||||
{% csrf_token %}
|
{% 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" class="code-editor-textarea">{{ 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>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,11 +172,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
{% 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>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
var initialContent;
|
var initialContent;
|
||||||
@@ -193,22 +181,11 @@
|
|||||||
initialContent = null;
|
initialContent = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var useCodeEditor = {{ use_code_editor|yesno:"true,false" }};
|
|
||||||
var editor = document.getElementById('code-editor');
|
var editor = document.getElementById('code-editor');
|
||||||
var summernoteActive = false;
|
|
||||||
|
|
||||||
// Returns current HTML content regardless of editor mode
|
// ---- Editor setup: plain code editor for all templates ----
|
||||||
function getEditorContent() {
|
// Always load content from JSON (the textarea's Django-rendered value may be HTML-escaped)
|
||||||
if (summernoteActive) {
|
if (initialContent !== null) {
|
||||||
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.value = initialContent;
|
||||||
}
|
}
|
||||||
editor.addEventListener('keydown', function(e) {
|
editor.addEventListener('keydown', function(e) {
|
||||||
@@ -220,7 +197,7 @@
|
|||||||
this.selectionStart = this.selectionEnd = start + 4;
|
this.selectionStart = this.selectionEnd = start + 4;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Variable insertion for code editor
|
// Variable insertion
|
||||||
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');
|
||||||
@@ -231,73 +208,13 @@
|
|||||||
editor.focus();
|
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) ----
|
// ---- Preview (always set up, independent of editor mode) ----
|
||||||
var previewFrame = document.getElementById('preview-frame');
|
var previewFrame = document.getElementById('preview-frame');
|
||||||
var previewLoading = document.getElementById('preview-loading');
|
var previewLoading = document.getElementById('preview-loading');
|
||||||
|
|
||||||
function loadPreview() {
|
function loadPreview() {
|
||||||
var content = getEditorContent();
|
var content = editor.value;
|
||||||
var csrfEl = document.querySelector('[name=csrfmiddlewaretoken]');
|
var csrfEl = document.querySelector('[name=csrfmiddlewaretoken]');
|
||||||
if (!csrfEl) {
|
if (!csrfEl) {
|
||||||
previewLoading.innerHTML = '<i class="fas fa-exclamation-triangle text-danger fa-2x mb-2 d-block"></i>CSRF-Token nicht gefunden.';
|
previewLoading.innerHTML = '<i class="fas fa-exclamation-triangle text-danger fa-2x mb-2 d-block"></i>CSRF-Token nicht gefunden.';
|
||||||
|
|||||||
Reference in New Issue
Block a user