Files
survey-bundle/public/js/survey-rich-text.js
T
Claude Fable 5 833a8964cf Add topic sections, ordered inserts, rich-text descriptions and grouped results (Ausbaustufe 3)
- section content type with structure service, reader context and grouped results/PDF/Excel
- new questions append at the end (backend oncreate) and per-section add buttons
- rich-text descriptions: Quill 2 (frontend), reduced TinyMCE (backend), server-side whitelist sanitizer
- externalize inline assets, bundle Chart.js locally, harden debug access, validate range bounds

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 07:57:40 +02:00

98 lines
3.5 KiB
JavaScript

/*
* Rich-Text-Editor (Quill 2) für Beschreibungsfelder im Frontend-Editor.
*
* Jedes <textarea data-survey-rich-text> wird durch einen Quill-Editor mit
* reduzierter Werkzeugleiste ersetzt: Fett, Kursiv, Aufzählung, Nummerierung.
* Der HTML-Inhalt (p, br, ul, ol, li, strong, em) wird bei jeder Änderung in das
* versteckte Textarea zurückgeschrieben, damit Formular-Submit und das
* Dirty-Tracking unverändert funktionieren. Serverseitig wird zusätzlich über
* eine Whitelist bereinigt.
*/
(function () {
var FORMATS = ['bold', 'italic', 'list'];
var TOOLBAR = [['bold', 'italic'], [{ list: 'bullet' }, { list: 'ordered' }], ['clean']];
function toSemanticHtml(quill) {
var html = typeof quill.getSemanticHTML === 'function' ? quill.getSemanticHTML() : quill.root.innerHTML;
// Leerer Editor: kein "<p></p>" speichern.
if (/^\s*(<p>(\s|&nbsp;|<br\s*\/?>)*<\/p>\s*)*$/i.test(html)) {
return '';
}
return html;
}
function initRichText(textarea) {
if (textarea._surveyQuill || typeof Quill !== 'function') {
return;
}
var wrapper = document.createElement('div');
wrapper.className = 'survey-rich-editor';
var editorHost = document.createElement('div');
editorHost.className = 'survey-rich-editor__host';
wrapper.appendChild(editorHost);
textarea.parentNode.insertBefore(wrapper, textarea);
textarea.hidden = true;
textarea.setAttribute('aria-hidden', 'true');
textarea.tabIndex = -1;
var quill = new Quill(editorHost, {
theme: 'snow',
formats: FORMATS,
placeholder: textarea.getAttribute('placeholder') || '',
modules: {
toolbar: TOOLBAR,
clipboard: { matchVisual: false }
}
});
var initialHtml = (textarea.value || '').trim();
if (initialHtml !== '') {
// Klartext aus Altdaten in Absätze überführen, damit nichts zusammenläuft.
if (!/<[a-z][\s\S]*>/i.test(initialHtml)) {
initialHtml = initialHtml.split(/\n{2,}/).map(function (paragraph) {
return '<p>' + paragraph.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>') + '</p>';
}).join('');
}
var delta = quill.clipboard.convert({ html: initialHtml });
quill.setContents(delta, 'silent');
}
// Der gespeicherte Zustand soll dem entsprechen, was Quill selbst erzeugt,
// sonst meldet das Dirty-Tracking sofort "ungespeicherte Änderungen".
textarea.value = toSemanticHtml(quill);
quill.on('text-change', function () {
textarea.value = toSemanticHtml(quill);
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.dispatchEvent(new Event('change', { bubbles: true }));
});
var form = textarea.closest('form');
if (form) {
form.addEventListener('submit', function () {
textarea.value = toSemanticHtml(quill);
});
}
textarea._surveyQuill = quill;
}
function initAll() {
document.querySelectorAll('textarea[data-survey-rich-text]').forEach(initRichText);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAll, { once: true });
} else {
initAll();
}
})();