Files
survey-bundle/contao/templates/frontend/show_survey.html.twig
T
admin.juergenandClaude Fable 5 6077832efc Honor the mandatory flag with explicit no-answer options
Symfony's required default forced an answer to every question no matter
what the mandatory checkbox said. Optional choice questions now offer a
dedicated 'Keine Antwort' option, optional range and text questions a
'Keine Angaben machen' checkbox (preselected for ranges, because a slider
always submits a value); the side that is not chosen gets dimmed but stays
clickable. Mandatory ranges start their value display at '?' and reject
the submit until the slider was actually used, so the browser's midpoint
default no longer leaks into the results.

Empty answers are stored as a deliberate 'no answer', kept out of counts,
averages and charts, and reported separately on the results page and in
the PDF export. Also gives the reader textarea its full width and field
styling, adds grid overflow guards for unbreakable content and styles the
form error box.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 14:36:38 +02:00

158 lines
10 KiB
Twig

{% trans_default_domain 'messages' %}
{% include '@Survey/frontend/_survey_assets.html.twig' %}
{% include '@Survey/frontend/_survey_branding.html.twig' %}
{% set moduleHeadline = (headline is iterable ? headline.text|default('') : headline|default(''))|striptags|trim %}
<section class="survey-shell survey-shell--reader survey-grid">
{% if moduleHeadline %}
<header><h2>{{ moduleHeadline }}</h2></header>
{% endif %}
{% if errorMessage %}
<div class="survey-alert error">{{ errorMessage }}</div>
{% elseif survey %}
{% set showIntro = isFirstQuestion|default(false) and not isComplete %}
<article class="survey-card">
<div class="survey-meta">
<h3>{{ survey.title|striptags|trim }}</h3>
{% if isPreview %}<span class="survey-badge warning">{{ 'survey.survey.preview_badge'|trans }}</span>{% endif %}
</div>
{# Der Einleitungstext erscheint nur auf der ersten Seite. #}
{% if showIntro %}
{% if survey.description|survey_has_text %}
<div class="survey-rich-text survey-intro">{{ survey.description|survey_rich }}</div>
{% else %}
<p>{{ 'survey.survey.default_description'|trans }}</p>
{% endif %}
{% endif %}
{% if isPreview %}
<div class="survey-alert warning">{{ 'survey.survey.preview_notice'|trans }}</div>
{% endif %}
{% if progress.total > 0 %}
<div class="survey-progress"><span style="width: {{ progress.percentage }}%"></span></div>
{% endif %}
</article>
{% if isComplete %}
<article class="survey-card">
<h3>{{ 'survey.survey.thanks_title'|trans }}</h3>
<p>{{ isPreview ? 'survey.survey.preview_thanks_text'|trans : 'survey.survey.thanks_text'|trans }}</p>
{% if answers %}
<div class="survey-grid">
{% for answer in answers %}
<article class="survey-card">
<div class="survey-badge neutral">{{ answer.questionType }}</div>
<h4>{{ answer.question|striptags|trim }}</h4>
<p>{{ answer.value|striptags|trim }}</p>
</article>
{% endfor %}
</div>
{% endif %}
</article>
{% elseif question %}
<article class="survey-card">
{% if progress.sectionTitle %}
<div class="survey-section-context">
<strong>{{ progress.sectionTitle }}</strong>
<span class="survey-section-counter">{{ 'survey.survey.section_progress'|trans({'%current%': progress.sectionCurrent, '%total%': progress.sectionTotal, '%section%': progress.sectionTitle}) }}</span>
</div>
{% endif %}
<h3>{{ question.question|striptags|trim }}</h3>
{% if question.description|survey_has_text %}<div class="survey-rich-text">{{ question.description|survey_rich }}</div>{% endif %}
{{ form_start(surveyForm, {attr: {class: 'survey-form-grid', id: 'survey-answer-form-' ~ question.id, 'data-survey-answer-group': '1'}}) }}
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
{% if isPreview %}
<input type="hidden" name="preview_question" value="{{ question.id }}">
<input type="hidden" name="preview_history" value="{{ historyState }}">
{% endif %}
{# Die Option "Keine Antwort" (nur bei optionalen Auswahlfragen) wird
als eigene Karte unterhalb der echten Antworten dargestellt. #}
{% set answerChildren = surveyForm.answer is defined ? surveyForm.answer|filter(child => child.vars.value is not defined or child.vars.value != '__no_answer__') : [] %}
{% set noAnswerChildren = surveyForm.answer is defined ? surveyForm.answer|filter(child => child.vars.value is defined and child.vars.value == '__no_answer__') : [] %}
{% if question.type == 'yes_no_maybe' %}
<div class="survey-grid" style="grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));" data-survey-answer-zone>
{% for child in answerChildren %}
<label class="survey-card" style="cursor: pointer;">
<div class="survey-meta"><strong>{{ child.vars.label|striptags|trim }}</strong>{{ form_widget(child) }}</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>
{% elseif question.type == 'choice' %}
<div class="survey-grid" style="grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));" data-survey-answer-zone>
{% for child in answerChildren %}
<label class="survey-card" style="cursor: pointer;">
<div class="survey-meta"><strong>{{ child.vars.label|striptags|trim }}</strong>{{ form_widget(child) }}</div>
</label>
{% endfor %}
</div>
{% elseif question.type == 'range' %}
{# Ohne bestätigten Wert zeigt die Anzeige "?" - der Slider selbst hat
technisch immer einen Wert (Browser-Vorgabe: Skalenmitte). #}
{% set rangeViewValue = surveyForm.answer.vars.value|default('') %}
{% set rangeAttributes = {'data-survey-range': '1', 'data-survey-range-target': 'survey-range-output-' ~ question.id} %}
{% if rangeViewValue == '' %}
{% set rangeAttributes = rangeAttributes|merge({'data-survey-range-untouched': '1'}) %}
{% endif %}
<div class="survey-grid survey-range-block" data-survey-answer-zone>
<div class="survey-range-value-card">
<div class="survey-range-value-label">{{ 'survey.survey.current_value'|trans }}</div>
<div id="survey-range-output-{{ question.id }}" class="survey-range-output">{{ rangeViewValue != '' ? rangeViewValue : '?' }}</div>
</div>
{{ form_widget(surveyForm.answer, {attr: rangeAttributes}) }}
<div class="survey-range-scale"><span class="survey-range-boundary">{{ question.rangeMin }}</span><span class="survey-range-boundary">{{ question.rangeMax }}</span></div>
</div>
{% if surveyForm.answerTouched is defined %}
{{ form_widget(surveyForm.answerTouched, {attr: {'data-survey-range-touched-field': '1'}}) }}
{% endif %}
{% else %}
<div data-survey-answer-zone>{{ form_widget(surveyForm.answer) }}</div>
{% endif %}
{% for child in noAnswerChildren %}
<label class="survey-card survey-no-answer-card" style="cursor: pointer;" data-survey-no-answer-zone>
<div class="survey-meta"><strong>{{ child.vars.label|striptags|trim }}</strong>{{ form_widget(child, {attr: {'data-survey-no-answer': '1'}}) }}</div>
<div class="survey-optional-hint">{{ 'survey.survey.optional_hint'|trans }}</div>
</label>
{% endfor %}
{% if surveyForm.noAnswer is defined %}
<label class="survey-card survey-no-answer-card" style="cursor: pointer;" data-survey-no-answer-zone>
<div class="survey-meta"><strong>{{ 'survey.survey.no_answer_skip'|trans }}</strong>{{ form_widget(surveyForm.noAnswer, {attr: {'data-survey-no-answer': '1'}}) }}</div>
<div class="survey-optional-hint">{{ 'survey.survey.optional_hint'|trans }}</div>
</label>
{% endif %}
{{ form_errors(surveyForm.answer) }}
{{ form_end(surveyForm) }}
<div class="survey-button-row survey-button-row--navigation{{ canGoBack ? ' has-back' : '' }}">
{% if canGoBack %}
<form method="post" class="survey-navigation-form survey-navigation-form--back">
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
<input type="hidden" name="_survey_navigation" value="back">
<input type="hidden" name="_navigation_token" value="{{ csrf_token((isPreview ? 'survey-preview-back-' : 'survey-back-') ~ survey.id) }}">
{% if isPreview %}
<input type="hidden" name="preview_question" value="{{ question.id }}">
<input type="hidden" name="preview_history" value="{{ historyState }}">
{% endif %}
<button type="submit" class="survey-button secondary">{{ 'survey.survey.back'|trans }}</button>
</form>
{% endif %}
<button type="submit" form="{{ 'survey-answer-form-' ~ question.id }}" class="survey-button primary survey-navigation-submit">{{ 'survey.survey.next'|trans }}</button>
</div>
</article>
{% endif %}
{% endif %}
</section>