- 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>
98 lines
3.5 KiB
JavaScript
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| |<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, '&').replace(/</g, '<').replace(/>/g, '>').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();
|
|
}
|
|
})();
|