Fix frontend CSRF, clean reader URLs, and review findings

CSRF / actions:
- List action forms now send both REQUEST_TOKEN (Contao gate) and _token
  (Symfony, validated via isCsrfTokenValid) — mirrors the editor; fixes
  the 400/500 "Ungültiger CSRF-Token" on publish/toggle/duplicate/delete

Public reader / URLs:
- Reader & results read auto_item via Input::get (marks the route param used)
  so clean path URLs /<page>/<alias> no longer 404
- List controller generates path URLs via ['parameters' => '/'.$alias]
- Allow re-participation in the same session after a 60s cooldown

Review fixes:
- Results: merge answer snapshots by questionId+type (no more split cards on
  label edits); seed "Vielleicht" when allowMaybe so screen matches Excel
- Reader: replace generic answer hint with per-option "weiter zu Frage X"
  only when a jump is configured; strip HTML tags from survey description
- PDF: render free-text frequency summary; drop duplicate "Minimum" label
- Drag-sort: bundle SortableJS locally, drop the duplicated inline sort CSS/JS
  from _survey_assets, throttle the backend MutationObserver
- Backend: move hardcoded tl_survey operation labels to TL_LANG (de+en)
- Remove dead code: SurveyEditorVoter, SurveyPermissionService,
  categoryFilter/categorySummary + helpers, orphaned publishSurvey lang key

Reader layout:
- max-width 1000px (other views keep 1400px); back/next buttons fill the
  width as equal, card-sized halves; sort handle icon offset tweak

Docs: update AI_HANDOVER (dual-token CSRF, clean URLs) and README

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jürgen Mummert
2026-06-14 13:00:05 +02:00
parent 1b16675ae9
commit db7094fd1e
29 changed files with 230 additions and 456 deletions
+5 -18
View File
@@ -48,14 +48,14 @@ $GLOBALS['TL_DCA']['tl_survey'] = [
],
'operations' => [
'editheader' => [
'label' => ['Umfrage bearbeiten', 'Umfrage bearbeiten'],
'label' => &$GLOBALS['TL_LANG']['tl_survey']['editheader'],
'href' => 'act=edit',
'icon' => 'edit.svg',
'primary' => true,
'showInHeader' => false,
],
'edit' => [
'label' => ['Fragen bearbeiten', 'Fragen der Umfrage bearbeiten'],
'label' => &$GLOBALS['TL_LANG']['tl_survey']['edit'],
'href' => 'table=tl_survey_content',
'icon' => 'children.svg',
'primary' => true,
@@ -81,13 +81,13 @@ $GLOBALS['TL_DCA']['tl_survey'] = [
'button_callback' => [SurveyDcaListener::class, 'generateDuplicateSurveyButton'],
],
'delete' => [
'label' => ['Löschen', 'Eintrag wirklich löschen?'],
'label' => &$GLOBALS['TL_LANG']['tl_survey']['delete'],
'href' => 'act=delete',
'icon' => 'delete.svg',
'attributes' => 'onclick="if(!confirm(\'Eintrag wirklich löschen?\'))return false;Backend.getScrollOffset()"',
],
'show' => [
'label' => ['Details', 'Details des Elements ID %s anzeigen'],
'label' => &$GLOBALS['TL_LANG']['tl_survey']['show'],
'href' => 'act=show',
'icon' => 'show.svg',
],
@@ -148,17 +148,6 @@ $GLOBALS['TL_DCA']['tl_survey'] = [
'relation' => ['type' => 'hasMany', 'load' => 'lazy', 'table' => 'tl_survey_category', 'field' => 'id'],
'sql' => 'blob NULL',
],
'categoryFilter' => [
'label' => &$GLOBALS['TL_LANG']['tl_survey']['categoryFilter'],
'inputType' => 'select',
'options_callback' => [SurveyDcaListener::class, 'getUsedCategoryFilterOptions'],
'eval' => ['includeBlankOption' => true, 'chosen' => true, 'findInSet' => true],
'sql' => "varchar(255) NOT NULL default ''",
],
'categorySummary' => [
'label' => &$GLOBALS['TL_LANG']['tl_survey']['categorySummary'],
'sql' => 'text NULL',
],
'published' => [
'label' => &$GLOBALS['TL_LANG']['tl_survey']['published'],
'inputType' => 'checkbox',
@@ -253,7 +242,7 @@ if ('cli' !== PHP_SAPI) {
System::getContainer()->get('database_connection')->fetchFirstColumn('SHOW COLUMNS FROM tl_survey'),
);
$hasListMetadataColumns = !array_diff(
['categoryfilter', 'categorysummary', 'templatefilter', 'templatesummary', 'assignedmembersfilter', 'assignedmemberssummary'],
['templatefilter', 'templatesummary', 'assignedmembersfilter', 'assignedmemberssummary'],
$columnNames,
);
} catch (\Throwable) {
@@ -264,8 +253,6 @@ if ('cli' !== PHP_SAPI) {
$GLOBALS['TL_DCA']['tl_survey']['list']['label']['fields'] = ['title', 'alias'];
unset(
$GLOBALS['TL_DCA']['tl_survey']['fields']['categoryFilter'],
$GLOBALS['TL_DCA']['tl_survey']['fields']['categorySummary'],
$GLOBALS['TL_DCA']['tl_survey']['fields']['templateFilter'],
$GLOBALS['TL_DCA']['tl_survey']['fields']['templateSummary'],
$GLOBALS['TL_DCA']['tl_survey']['fields']['assignedMembersFilter'],
+4 -3
View File
@@ -3,8 +3,6 @@
$GLOBALS['TL_LANG']['tl_survey']['title'] = ['Titel', 'Titel der Umfrage.'];
$GLOBALS['TL_LANG']['tl_survey']['alias'] = ['Öffentlicher Link-Schlüssel', 'Wird automatisch als 20-stelliger Link-Schlüssel erzeugt.'];
$GLOBALS['TL_LANG']['tl_survey']['category'] = ['Kategorien', 'Ordnet die Umfrage einer oder mehreren Kategorien zu.'];
$GLOBALS['TL_LANG']['tl_survey']['categoryFilter'] = ['Kategorien', 'Filtert die Liste nach einer Umfrage-Kategorie.'];
$GLOBALS['TL_LANG']['tl_survey']['categorySummary'] = ['Kategorien', 'Zugeordnete Kategorien der Umfrage.'];
$GLOBALS['TL_LANG']['tl_survey']['isTemplate'] = ['Umfrage ist eine Vorlage', 'Wird den zugewiesenen Nutzern als Vorlage angezeigt.'];
$GLOBALS['TL_LANG']['tl_survey']['templateFilter'] = ['Vorlage', 'Filtert die Liste nach Vorlagenstatus.'];
$GLOBALS['TL_LANG']['tl_survey']['templateSummary'] = ['Vorlage', 'Zeigt an, ob die Umfrage als Vorlage markiert ist.'];
@@ -19,7 +17,6 @@ $GLOBALS['TL_LANG']['tl_survey']['meta_legend'] = 'Verwaltungsdaten';
$GLOBALS['TL_LANG']['tl_survey']['published'] = ['Veröffentlicht', 'Die Umfrage öffentlich freischalten.'];
$GLOBALS['TL_LANG']['tl_survey']['isActive'] = ['Vorübergehend aktiv', 'Schaltet eine bereits veröffentlichte Umfrage öffentlich an oder aus.'];
$GLOBALS['TL_LANG']['tl_survey']['isLocked'] = ['Gesperrt', 'Struktur ist wegen vorhandener Antworten gesperrt.'];
$GLOBALS['TL_LANG']['tl_survey']['publishSurvey'] = ['Umfrage veröffentlichen', 'Veröffentlicht die Umfrage endgültig.'];
$GLOBALS['TL_LANG']['tl_survey']['publishedWarning'] = ['Veröffentlichungshinweis', 'Zeigt einen Warnhinweis bei veröffentlichten Umfragen.'];
$GLOBALS['TL_LANG']['tl_survey']['toggleActive'] = ['Sichtbarkeit umschalten', 'Schaltet die veröffentlichte Umfrage vorübergehend an oder aus.'];
$GLOBALS['TL_LANG']['tl_survey']['assignedMembers'] = ['Zugewiesene Mitglieder', 'Wählen Sie die Mitglieder aus, die diese Umfrage bearbeiten dürfen.'];
@@ -30,6 +27,10 @@ $GLOBALS['TL_LANG']['tl_survey']['duplicateSurvey'] = ['Duplizieren', 'Diese Umf
$GLOBALS['TL_LANG']['tl_survey']['duplicateSurveyConfirm'] = 'Sind Sie sicher, dass Sie die Umfrage "%s" mit allen Fragen duplizieren wollen?';
$GLOBALS['TL_LANG']['tl_survey']['duplicateSurveyDone'] = 'Die Umfrage "%s" wurde dupliziert.';
$GLOBALS['TL_LANG']['tl_survey']['showResults'] = ['Ergebnisse anzeigen', 'Die Ergebnisse dieser Umfrage im Frontend anzeigen'];
$GLOBALS['TL_LANG']['tl_survey']['editheader'] = ['Umfrage bearbeiten', 'Umfrage bearbeiten'];
$GLOBALS['TL_LANG']['tl_survey']['edit'] = ['Fragen bearbeiten', 'Fragen der Umfrage bearbeiten'];
$GLOBALS['TL_LANG']['tl_survey']['delete'] = ['Löschen', 'Eintrag wirklich löschen?'];
$GLOBALS['TL_LANG']['tl_survey']['show'] = ['Details', 'Details des Elements ID %s anzeigen'];
$GLOBALS['TL_LANG']['tl_survey']['createdBy'] = ['Angelegt von', 'ID des erstellenden Mitglieds.'];
$GLOBALS['TL_LANG']['tl_survey']['updatedBy'] = ['Aktualisiert von', 'ID des zuletzt bearbeitenden Mitglieds.'];
$GLOBALS['TL_LANG']['tl_survey']['createdAt'] = ['Angelegt am', 'Zeitpunkt der Erstellung.'];
+4 -3
View File
@@ -3,8 +3,6 @@
$GLOBALS['TL_LANG']['tl_survey']['title'] = ['Title', 'Title of the survey.'];
$GLOBALS['TL_LANG']['tl_survey']['alias'] = ['Public link key', 'Automatically generated as a 20-character link key.'];
$GLOBALS['TL_LANG']['tl_survey']['category'] = ['Categories', 'Assigns the survey to one or more categories.'];
$GLOBALS['TL_LANG']['tl_survey']['categoryFilter'] = ['Categories', 'Filters the list by a survey category.'];
$GLOBALS['TL_LANG']['tl_survey']['categorySummary'] = ['Categories', 'Assigned categories of the survey.'];
$GLOBALS['TL_LANG']['tl_survey']['isTemplate'] = ['Survey is a template', 'Shown to assigned users as a template.'];
$GLOBALS['TL_LANG']['tl_survey']['templateFilter'] = ['Template', 'Filters the list by template status.'];
$GLOBALS['TL_LANG']['tl_survey']['templateSummary'] = ['Template', 'Shows whether the survey is marked as a template.'];
@@ -19,7 +17,6 @@ $GLOBALS['TL_LANG']['tl_survey']['meta_legend'] = 'Administrative data';
$GLOBALS['TL_LANG']['tl_survey']['published'] = ['Published', 'Make the survey publicly accessible.'];
$GLOBALS['TL_LANG']['tl_survey']['isActive'] = ['Temporarily active', 'Turns an already published survey on or off publicly.'];
$GLOBALS['TL_LANG']['tl_survey']['isLocked'] = ['Locked', 'Structure is locked because answers already exist.'];
$GLOBALS['TL_LANG']['tl_survey']['publishSurvey'] = ['Publish survey', 'Publishes the survey permanently.'];
$GLOBALS['TL_LANG']['tl_survey']['publishedWarning'] = ['Publication warning', 'Shows a warning hint for already published surveys.'];
$GLOBALS['TL_LANG']['tl_survey']['toggleActive'] = ['Toggle visibility', 'Temporarily switches the published survey on or off.'];
$GLOBALS['TL_LANG']['tl_survey']['assignedMembers'] = ['Assigned members', 'Choose the members who are allowed to edit this survey.'];
@@ -30,6 +27,10 @@ $GLOBALS['TL_LANG']['tl_survey']['duplicateSurvey'] = ['Duplicate', 'Duplicate t
$GLOBALS['TL_LANG']['tl_survey']['duplicateSurveyConfirm'] = 'Are you sure you want to duplicate the survey "%s" with all questions?';
$GLOBALS['TL_LANG']['tl_survey']['duplicateSurveyDone'] = 'The survey "%s" has been duplicated.';
$GLOBALS['TL_LANG']['tl_survey']['showResults'] = ['Show results', 'Display the results of this survey in the frontend'];
$GLOBALS['TL_LANG']['tl_survey']['editheader'] = ['Edit survey', 'Edit survey'];
$GLOBALS['TL_LANG']['tl_survey']['edit'] = ['Edit questions', 'Edit the survey questions'];
$GLOBALS['TL_LANG']['tl_survey']['delete'] = ['Delete', 'Really delete the entry?'];
$GLOBALS['TL_LANG']['tl_survey']['show'] = ['Details', 'Show the details of entry ID %s'];
$GLOBALS['TL_LANG']['tl_survey']['createdBy'] = ['Created by', 'ID of the creating member.'];
$GLOBALS['TL_LANG']['tl_survey']['updatedBy'] = ['Updated by', 'ID of the member who last edited it.'];
$GLOBALS['TL_LANG']['tl_survey']['createdAt'] = ['Created on', 'Creation time.'];
@@ -85,6 +85,12 @@
margin-bottom: 4em;
}
.survey-shell--reader {
max-width: 1000px;
margin-left: max(var(--survey-page-gutter), calc((100% - 1000px) / 2));
margin-right: max(var(--survey-page-gutter), calc((100% - 1000px) / 2));
}
.survey-shell--template {
background:
radial-gradient(circle at top left, rgba(236, 124, 50, 0.18), transparent 32%),
@@ -489,6 +495,28 @@
margin-left: auto;
}
/* Reader: Zurück/Weiter füllen die volle Breite und sind so groß wie die Ja/Nein-Karten */
.survey-shell--reader .survey-button-row--navigation {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.survey-shell--reader .survey-button-row--navigation.has-back {
grid-template-columns: 1fr 1fr;
}
.survey-shell--reader .survey-navigation-form--back {
width: 100%;
}
.survey-shell--reader .survey-button-row--navigation .survey-button {
width: 100%;
margin: 0;
padding: 1.25rem;
border-radius: 1.5rem;
}
.survey-publish-toggle-form {
margin: 0;
}
@@ -725,82 +753,6 @@
text-align: left;
}
.survey-accordion-sort-handle {
position: relative;
display: inline-flex;
flex: 0 0 auto;
min-width: 3.5rem;
padding: 0 1rem;
border: 0;
border-left: 1px solid rgba(162, 168, 180, 0.18);
background: transparent;
cursor: grab;
align-items: center;
justify-content: center;
align-self: stretch;
transition: background 0.2s ease, color 0.2s ease;
}
.survey-accordion-sort-handle:active {
cursor: grabbing;
}
.survey-accordion-item .handorgel__header__button:hover + .survey-accordion-sort-handle,
.survey-accordion-item .handorgel__header__button:focus + .survey-accordion-sort-handle,
.survey-accordion-item .handorgel__header--opened .survey-accordion-sort-handle,
.survey-accordion-sort-handle:hover,
.survey-accordion-sort-handle:focus {
background: var(--survey-blue);
color: #fff;
}
.survey-sort-icon {
display: inline-flex;
width: 0.95rem;
height: 1.28rem;
align-items: center;
justify-content: center;
color: #58a6da;
line-height: 0;
transform: translateY(1px);
}
.survey-sort-icon svg {
display: block;
width: 100%;
height: 100%;
transform: translateY(4px);
}
.survey-question-order-save-shell {
position: fixed;
top: 50%;
right: 1rem;
z-index: 40;
transform: translate(120%, -50%);
opacity: 0;
pointer-events: none;
transition: transform 0.26s ease, opacity 0.26s ease;
}
.survey-question-order-save-shell.is-visible {
transform: translate(0, -50%);
opacity: 1;
pointer-events: auto;
}
.survey-question-order-save {
box-shadow: 0 18px 40px rgba(0, 59, 102, 0.24);
}
.survey-accordion-item.is-reordering {
box-shadow: 0 20px 44px rgba(0, 59, 102, 0.18);
}
.survey-accordion-item.sortable-ghost {
opacity: 0.55;
}
.survey-accordion-item .handorgel__header__button,
.survey-accordion-item .handorgel__header__button:hover,
.survey-accordion-item .handorgel__header__button:focus {
@@ -1075,22 +1027,6 @@
padding: 0.95rem 1rem;
}
.survey-accordion-sort-handle {
min-width: 3.1rem;
padding: 0 0.8rem;
}
.survey-question-order-save-shell {
top: auto;
right: 0.85rem;
bottom: 0.85rem;
transform: translate(0, 130%);
}
.survey-question-order-save-shell.is-visible {
transform: translate(0, 0);
}
}
</style>
<script>
@@ -1249,133 +1185,6 @@
});
});
var initializeQuestionSorting = function () {
var styleId = 'survey-question-sort-icon-runtime';
if (!document.getElementById(styleId)) {
var runtimeStyle = document.createElement('style');
runtimeStyle.id = styleId;
runtimeStyle.textContent = '' +
'.survey-sort-icon{' +
'width:1.14rem !important;' +
'height:1.52rem !important;' +
'color:#58a6da !important;' +
'background:none !important;' +
'transform:translateY(1px) !important;' +
'}' +
'.survey-sort-icon::before,.survey-sort-icon::after{' +
'content:none !important;' +
'}' +
'.survey-sort-icon svg{' +
'display:block !important;' +
'width:100% !important;' +
'height:100% !important;' +
'transform:translateY(4px);' +
'}' +
'.survey-sort-icon svg path:first-child{' +
'transform:translateY(1.4px);' +
'transform-box:fill-box;' +
'transform-origin:center;' +
'}' +
'.survey-sort-icon svg path:nth-child(2){' +
'transform:translateY(-1.4px);' +
'transform-box:fill-box;' +
'transform-origin:center;' +
'}' +
'.survey-sort-icon svg path[stroke]{' +
'stroke-width:2px;' +
'}';
document.head.appendChild(runtimeStyle);
}
if (typeof Sortable !== 'function') {
return;
}
var sortableList = document.querySelector('[data-survey-question-sortable]');
var orderForm = document.querySelector('[data-survey-question-order-shell]');
var orderInput = document.querySelector('[data-survey-question-order-input]');
if (!sortableList || !orderForm || !orderInput) {
return;
}
if (sortableList._surveyQuestionSortable) {
return;
}
var getOrderedIds = function () {
return Array.from(sortableList.querySelectorAll('[data-survey-question-item]')).map(function (item) {
return item.getAttribute('data-question-id') || '';
}).filter(function (value) {
return value !== '';
});
};
var initialOrder = getOrderedIds().join(',');
var updateOrderField = function () {
orderInput.value = getOrderedIds().join(',');
};
var updateQuestionIndices = function () {
sortableList.querySelectorAll('[data-survey-question-item]').forEach(function (item, index) {
var indexNode = item.querySelector('.survey-accordion-index');
if (indexNode) {
indexNode.textContent = (index + 1) + '.';
}
});
};
var updateSaveVisibility = function () {
var currentOrder = getOrderedIds().join(',');
var isDirty = currentOrder !== initialOrder;
orderForm.hidden = !isDirty;
orderForm.classList.toggle('is-visible', isDirty);
updateOrderField();
};
sortableList.querySelectorAll('[data-survey-question-sort-handle]').forEach(function (handle) {
handle.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
});
});
updateQuestionIndices();
updateOrderField();
updateSaveVisibility();
var sortableInstance = Sortable.create(sortableList, {
animation: 180,
draggable: '[data-survey-question-item]',
handle: '[data-survey-question-sort-handle]',
ghostClass: 'sortable-ghost',
forceFallback: true,
fallbackTolerance: 3,
onStart: function (event) {
if (event.item) {
event.item.classList.add('is-reordering');
}
},
onEnd: function (event) {
if (event.item) {
event.item.classList.remove('is-reordering');
}
updateQuestionIndices();
updateSaveVisibility();
}
});
sortableList._surveyQuestionSortable = sortableInstance;
};
window.initializeSurveyQuestionSorting = initializeQuestionSorting;
if (typeof handorgel === 'function') {
document.querySelectorAll('[data-survey-handorgel]').forEach(function (element) {
var accordion = new handorgel(element, {
@@ -1395,7 +1204,6 @@
});
}
initializeQuestionSorting();
resetEditorViewport();
});
@@ -70,17 +70,17 @@
.survey-question-order-save-shell {
position: fixed;
top: 50%;
right: 1rem;
bottom: 1.5rem;
right: 1.5rem;
z-index: 40;
transform: translate(120%, -50%);
transform: translate(120%, 0);
opacity: 0;
pointer-events: none;
transition: transform 0.26s ease, opacity 0.26s ease;
}
.survey-question-order-save-shell.is-visible {
transform: translate(0, -50%);
transform: translate(0, 0);
opacity: 1;
pointer-events: auto;
}
@@ -115,5 +115,5 @@
}
}
</style>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
<script src="{{ asset('bundles/survey/js/sortable.min.js') }}"></script>
<script src="{{ asset('bundles/survey/js/survey-frontend-editor-sort.js') }}"></script>
@@ -51,6 +51,7 @@
<div class="survey-list-card__head-actions survey-list-card__head-actions--template">
<form method="post" class="survey-question-action-form">
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
<input type="hidden" name="_token" value="{{ csrf_token('duplicate-survey-' ~ survey.id) }}">
<input type="hidden" name="_survey_action" value="duplicate-survey">
<input type="hidden" name="item_id" value="{{ survey.id }}">
<button class="survey-button secondary" onclick="return confirm('{{ 'survey.list.duplicate_confirm'|trans({'%title%': survey.title|striptags|trim})|e('js') }}');">{{ 'survey.list.duplicate'|trans }}</button>
@@ -79,12 +80,14 @@
{% endif %}
<form method="post" class="survey-question-action-form">
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
<input type="hidden" name="_token" value="{{ csrf_token('duplicate-survey-' ~ survey.id) }}">
<input type="hidden" name="_survey_action" value="duplicate-survey">
<input type="hidden" name="item_id" value="{{ survey.id }}">
<button class="survey-button secondary" onclick="return confirm('{{ 'survey.list.duplicate_confirm'|trans({'%title%': survey.title|striptags|trim})|e('js') }}');">{{ 'survey.list.duplicate'|trans }}</button>
</form>
<form method="post" class="survey-question-action-form">
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
<input type="hidden" name="_token" value="{{ csrf_token('delete-survey-' ~ survey.id) }}">
<input type="hidden" name="_survey_action" value="delete-survey">
<input type="hidden" name="item_id" value="{{ survey.id }}">
<button class="survey-button danger" onclick="return confirm('{{ 'survey.list.delete_confirm'|trans|e('js') }}') && confirm('{{ 'survey.list.delete_confirm_second'|trans|e('js') }}');">{{ 'survey.list.delete'|trans }}</button>
@@ -94,6 +97,7 @@
<div class="survey-list-card__head-actions">
<form method="post" class="survey-list-publish-form">
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
<input type="hidden" name="_token" value="{{ csrf_token('publish-survey-' ~ survey.id) }}">
<input type="hidden" name="_survey_action" value="publish-survey">
<input type="hidden" name="item_id" value="{{ survey.id }}">
<button
@@ -111,6 +115,7 @@
<form method="post" class="survey-list-eye-form">
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
<input type="hidden" name="_token" value="{{ csrf_token('toggle-active-' ~ survey.id) }}">
<input type="hidden" name="_survey_action" value="toggle-active">
<input type="hidden" name="item_id" value="{{ survey.id }}">
<button type="submit" class="survey-visibility-dot{{ survey.isActive ? ' is-on' : '' }}" title="{{ survey.isActive ? 'survey.edit.deactivate_survey'|trans : 'survey.edit.activate_survey'|trans }}" aria-label="{{ survey.isActive ? 'survey.edit.deactivate_survey'|trans : 'survey.edit.activate_survey'|trans }}"></button>
@@ -5,7 +5,7 @@
{% set moduleHeadline = (headline is iterable ? headline.text|default('') : headline|default(''))|striptags|trim %}
<section class="survey-shell survey-grid">
<section class="survey-shell survey-shell--reader survey-grid">
{% if moduleHeadline %}
<header><h2>{{ moduleHeadline }}</h2></header>
{% endif %}
@@ -63,7 +63,10 @@
{% for child in surveyForm.answer %}
<label class="survey-card" style="cursor: pointer;">
<div class="survey-meta"><strong>{{ child.vars.label|striptags|trim }}</strong>{{ form_widget(child) }}</div>
<div>{{ 'survey.survey.answer_flow_hint'|trans }}</div>
{% set jumpHint = jumpHints|default({})[child.vars.value]|default(null) %}
{% if jumpHint %}
<div class="survey-jump-hint">{{ 'survey.survey.jump_to'|trans({'%position%': jumpHint.position, '%label%': jumpHint.label|striptags|trim}) }}</div>
{% endif %}
</label>
{% endfor %}
</div>