- 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>
261 lines
19 KiB
Twig
261 lines
19 KiB
Twig
{% trans_default_domain 'messages' %}
|
|
|
|
{% include '@Survey/frontend/_survey_assets.html.twig' %}
|
|
{% include '@Survey/frontend/_survey_question_sort_assets.html.twig' %}
|
|
{% include '@Survey/frontend/_survey_rich_text_assets.html.twig' %}
|
|
<link rel="stylesheet" href="{{ asset('assets/handorgel/css/handorgel.min.css') }}">
|
|
{% include '@Survey/frontend/_survey_branding.html.twig' %}
|
|
|
|
{% set moduleHeadline = (headline is iterable ? headline.text|default('') : headline|default(''))|striptags|trim %}
|
|
|
|
<section class="survey-shell survey-grid">
|
|
{% for message in app.flashes('success') %}
|
|
<div class="survey-alert success">{{ message }}</div>
|
|
{% endfor %}
|
|
|
|
{% for message in app.flashes('error') %}
|
|
<div class="survey-alert error">{{ message }}</div>
|
|
{% endfor %}
|
|
|
|
{% if moduleHeadline %}
|
|
<header>
|
|
<h2>{{ moduleHeadline }}</h2>
|
|
</header>
|
|
{% endif %}
|
|
|
|
{% if loginRequired %}
|
|
<div class="survey-alert warning">{{ 'survey.edit.login_required'|trans }}</div>
|
|
{% elseif entryNotAllowed|default(false) %}
|
|
<div class="survey-card survey-grid">
|
|
<div class="survey-alert warning">{{ 'survey.edit.entry_not_allowed'|trans }}</div>
|
|
<div class="survey-button-row">
|
|
{% if backUrl %}<a class="survey-button secondary" href="{{ backUrl }}">{{ 'survey.edit.back_to_list'|trans }}</a>{% endif %}
|
|
</div>
|
|
</div>
|
|
{% elseif createMode %}
|
|
<div class="survey-card">
|
|
<h3>{{ 'survey.edit.create_title'|trans }}</h3>
|
|
{{ form_start(surveyForm, {attr: {class: 'survey-form-grid'}}) }}
|
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
|
{{ form_widget(surveyForm._token) }}
|
|
<label class="survey-label">{{ 'survey.edit.label_survey_title'|trans }}{{ form_widget(surveyForm.title) }}{{ form_errors(surveyForm.title) }}</label>
|
|
<label class="survey-label">{{ 'survey.edit.label_description'|trans }}{{ form_widget(surveyForm.description) }}</label>
|
|
<div class="survey-button-row">
|
|
<button class="survey-button primary">{{ 'survey.edit.create_submit'|trans }}</button>
|
|
{% if backUrl %}<a class="survey-button secondary" href="{{ backUrl }}">{{ 'survey.edit.back_to_list'|trans }}</a>{% endif %}
|
|
</div>
|
|
{{ form_end(surveyForm, {render_rest: false}) }}
|
|
</div>
|
|
{% else %}
|
|
{% set surveyMetaFormId = 'survey-meta-form' %}
|
|
{% set newQuestionFormId = 'survey-question-form-new' %}
|
|
{% set canQuestionReorder = canQuestionReorder|default(false) %}
|
|
{% set isLocked = structureLocked|default(false) %}
|
|
|
|
<div class="survey-button-row survey-button-row--editor-actions">
|
|
{% if backUrl %}<a class="survey-button secondary" href="{{ backUrl }}">{{ 'survey.edit.back_to_list'|trans }}</a>{% endif %}
|
|
</div>
|
|
|
|
{% if not survey.published %}
|
|
<div class="survey-card survey-editor-meta-card">
|
|
{{ form_start(surveyForm, {action: metadataFormActionUrl, attr: {class: 'survey-form-grid', id: surveyMetaFormId, 'data-survey-dirty-form': '1'}}) }}
|
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
|
{{ form_widget(surveyForm._token) }}
|
|
{{ form_errors(surveyForm) }}
|
|
<label class="survey-label">{{ 'survey.edit.label_survey_title'|trans }}{{ form_widget(surveyForm.title) }}{{ form_errors(surveyForm.title) }}</label>
|
|
<label class="survey-label">{{ 'survey.edit.label_intro'|trans }}{{ form_widget(surveyForm.description) }}</label>
|
|
<div class="survey-field-hint">{{ 'survey.edit.intro_hint'|trans }}</div>
|
|
<div class="survey-button-row survey-button-row--form-actions">
|
|
<button class="survey-button primary">{{ 'survey.edit.save_metadata'|trans }}</button>
|
|
<div class="survey-dirty-hint" data-survey-dirty-hint-for="{{ surveyMetaFormId }}" hidden>{{ 'survey.edit.dirty_metadata'|trans }}</div>
|
|
</div>
|
|
{{ form_end(surveyForm, {render_rest: false}) }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if isLocked and not survey.published %}
|
|
<div class="survey-alert warning">{{ 'survey.edit.structure_locked'|trans }}</div>
|
|
{% endif %}
|
|
|
|
<div class="survey-grid-columns survey-editor-layout">
|
|
<div class="survey-question-editor-shell" data-survey-question-editor>
|
|
{% if canQuestionReorder %}
|
|
<form method="post" action="{{ metadataFormActionUrl }}" class="survey-question-order-save-shell" data-survey-question-order-shell hidden>
|
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
|
<input type="hidden" name="_survey_action" value="reorder-questions">
|
|
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
|
<input type="hidden" name="_token" value="{{ csrf_token('reorder-questions-' ~ survey.id) }}">
|
|
<input type="hidden" name="question_order" value="" data-survey-question-order-input>
|
|
<button class="survey-button primary survey-question-order-save">{{ 'survey.edit.save_question_order'|trans }}</button>
|
|
</form>
|
|
{% endif %}
|
|
|
|
{% if not isLocked %}
|
|
<div class="survey-button-row survey-structure-actions">
|
|
<a class="survey-button secondary" href="{{ editBaseUrl }}&question=new&newType=section#survey-question-new">{{ 'survey.edit.add_section'|trans }}</a>
|
|
{% if not hasSections %}
|
|
<span class="survey-field-hint">{{ 'survey.edit.sections_intro'|trans }}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="survey-accordion-list handorgel" data-survey-handorgel{% if canQuestionReorder %} data-survey-question-sortable="1"{% endif %}>
|
|
{% for entry in outline %}
|
|
{% set question = entry.model %}
|
|
{% set questionAnchor = 'survey-question-' ~ question.id %}
|
|
{% set isOpen = activeQuestionId == question.id %}
|
|
{% set questionItemForm = questionForms[question.id]|default(null) %}
|
|
{% set questionFormId = 'survey-question-form-' ~ question.id %}
|
|
{% set isSection = entry.isSection %}
|
|
<article id="{{ questionAnchor }}" class="survey-accordion-item{% if isSection %} survey-accordion-item--section{% endif %}"{% if canQuestionReorder %} data-survey-question-item data-question-id="{{ question.id }}"{% endif %}{% if isSection %} data-survey-section-item{% endif %}{% if isOpen %} data-survey-open="1"{% endif %}>
|
|
<h4 class="survey-accordion-header handorgel__header">
|
|
<div class="survey-accordion-header-row">
|
|
<button
|
|
type="button"
|
|
class="survey-accordion-toggle handorgel__header__button"
|
|
>
|
|
<span class="survey-accordion-heading">
|
|
{% if isSection %}
|
|
<span class="survey-accordion-index survey-accordion-index--section" aria-hidden="true">§</span>
|
|
<span class="survey-accordion-copy">
|
|
<span class="survey-section-kicker">{{ 'survey.edit.section_badge'|trans }}</span>
|
|
<strong>{{ question.question|striptags|trim ?: ('survey.edit.section_fallback'|trans) }}</strong>
|
|
</span>
|
|
{% else %}
|
|
<span class="survey-accordion-index">{{ entry.position }}.</span>
|
|
<span class="survey-accordion-copy">
|
|
<strong>{{ question.question|striptags|trim ?: ('survey.edit.question_fallback'|trans({'%id%': question.id})) }}</strong>
|
|
</span>
|
|
{% endif %}
|
|
</span>
|
|
</button>
|
|
{% if canQuestionReorder %}
|
|
<button type="button" class="survey-accordion-sort-handle" data-survey-question-sort-handle aria-label="{{ (isSection ? 'survey.edit.reorder_section' : 'survey.edit.reorder_question')|trans }}" title="{{ (isSection ? 'survey.edit.reorder_section' : 'survey.edit.reorder_question')|trans }}">
|
|
<span class="survey-sort-icon" aria-hidden="true">
|
|
<svg viewBox="0 0 18 24" role="presentation" focusable="false">
|
|
<path d="M9 3 11.6 5.8H6.4z" fill="currentColor"></path>
|
|
<path d="M9 21 6.4 18.2h5.2z" fill="currentColor"></path>
|
|
<path d="M4.5 12h9" stroke="currentColor" stroke-linecap="round" stroke-width="1.8"></path>
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</h4>
|
|
<div class="survey-accordion-panel handorgel__content">
|
|
<div class="handorgel__content__inner" data-survey-question-editor>
|
|
{% if isLocked %}
|
|
<div class="survey-grid">
|
|
{% if isSection %}
|
|
<p class="survey-field-hint">{{ 'survey.edit.section_locked_hint'|trans }}</p>
|
|
{% else %}
|
|
<div class="survey-meta">
|
|
<span class="survey-badge neutral">{{ ('survey.survey.question_type_' ~ question.type)|trans }}</span>
|
|
{% if question.mandatory %}<span class="survey-badge warning">{{ 'survey.edit.mandatory_badge'|trans }}</span>{% endif %}
|
|
{% if entry.sectionTitle %}<span class="survey-badge neutral">{{ entry.sectionTitle }}</span>{% endif %}
|
|
</div>
|
|
{% if question.description|survey_has_text %}<div class="survey-rich-text">{{ question.description|survey_rich }}</div>{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
{% elseif questionItemForm %}
|
|
{% set questionType = questionItemForm.type.vars.value ?: question.type ?: 'yes_no_maybe' %}
|
|
{% include '@Survey/frontend/_survey_question_form.html.twig' with {
|
|
form: questionItemForm,
|
|
formId: questionFormId,
|
|
questionType: questionType,
|
|
isNew: false,
|
|
dirtyHint: 'survey.edit.dirty_question'|trans,
|
|
submitLabel: 'survey.edit.save_this_question'|trans
|
|
} only %}
|
|
|
|
<div class="survey-button-row survey-button-row--form-actions survey-button-row--question-actions">
|
|
<button class="survey-button primary" type="submit" form="{{ questionFormId }}">{{ (isSection ? 'survey.edit.save_this_section' : 'survey.edit.save_this_question')|trans }}</button>
|
|
<form method="post" class="survey-question-action-form">
|
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
|
<input type="hidden" name="_survey_action" value="delete-question">
|
|
<input type="hidden" name="item_id" value="{{ question.id }}">
|
|
<input type="hidden" name="_token" value="{{ csrf_token('delete-question-' ~ question.id) }}">
|
|
<button class="survey-button danger" onclick="return confirm('{{ (isSection ? 'survey.edit.delete_section_confirm' : 'survey.edit.delete_question_confirm')|trans|e('js') }}');">{{ 'survey.edit.delete'|trans }}</button>
|
|
</form>
|
|
<div class="survey-dirty-hint" data-survey-dirty-hint-for="{{ questionFormId }}" hidden>{{ 'survey.edit.dirty_question'|trans }}</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
{% if entry.isLastInSection and not isLocked and (entry.sectionId > 0 or hasSections) %}
|
|
<div class="survey-section-add-row" data-survey-section-add>
|
|
<a class="survey-button ghost" href="{{ editBaseUrl }}&question=new&after={{ entry.id }}#survey-question-new">
|
|
{% if entry.sectionTitle %}
|
|
{{ 'survey.edit.add_question_to_section'|trans({'%section%': entry.sectionTitle}) }}
|
|
{% else %}
|
|
{{ 'survey.edit.add_question_before_sections'|trans }}
|
|
{% endif %}
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div>{{ 'survey.edit.no_questions'|trans }}</div>
|
|
{% endfor %}
|
|
|
|
{% if not isLocked %}
|
|
{% set newType = newQuestionContext.type|default('yes_no_maybe') %}
|
|
<article id="survey-question-new" class="survey-accordion-item survey-accordion-item--new"{% if createQuestionMode %} data-survey-open="1"{% endif %}>
|
|
<h4 class="survey-accordion-header handorgel__header">
|
|
<button
|
|
type="button"
|
|
class="survey-accordion-toggle handorgel__header__button"
|
|
>
|
|
<span class="survey-accordion-heading">
|
|
<span class="survey-accordion-index">+</span>
|
|
<span class="survey-accordion-copy">
|
|
<strong>{{ (newType == 'section' ? 'survey.edit.new_section' : 'survey.edit.new_question')|trans }}</strong>
|
|
</span>
|
|
</span>
|
|
</button>
|
|
</h4>
|
|
<div class="survey-accordion-panel handorgel__content">
|
|
<div class="handorgel__content__inner" data-survey-question-editor>
|
|
{% if newQuestionForm %}
|
|
{% set questionType = newQuestionForm.type.vars.value ?: newType %}
|
|
{% include '@Survey/frontend/_survey_question_form.html.twig' with {
|
|
form: newQuestionForm,
|
|
formId: newQuestionFormId,
|
|
questionType: questionType,
|
|
isNew: true,
|
|
newQuestionContext: newQuestionContext,
|
|
dirtyHint: 'survey.edit.dirty_new_question'|trans,
|
|
submitLabel: (questionType == 'section' ? 'survey.edit.save_new_section' : 'survey.edit.save_new_question')|trans
|
|
} only %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="survey-grid survey-editor-side">
|
|
<div class="survey-card">
|
|
<div class="survey-meta survey-section-head"><h3>{{ 'survey.edit.conditions_title'|trans }}</h3></div>
|
|
<div class="survey-grid">
|
|
{% for condition in conditions %}
|
|
<article class="survey-card survey-condition-card">
|
|
{% set sourceLabel = (condition.sourceQuestionLabel|default('')|striptags|trim) ?: ('survey.edit.question_fallback'|trans({'%id%': condition.sourceQuestion})) %}
|
|
{% set targetLabel = (condition.targetQuestionLabel|default('')|striptags|trim) ?: ('survey.edit.question_fallback'|trans({'%id%': condition.targetQuestion})) %}
|
|
<div><strong>Frage {{ condition.sourcePosition ?: condition.sourceQuestion }}: {{ sourceLabel }}</strong></div>
|
|
<div>{{ 'survey.edit.condition_prefix'|trans({'%value%': condition.answerValue|default('')|striptags|trim|capitalize}) }}</div>
|
|
<div>weiter zu Frage {{ condition.targetPosition ?: condition.targetQuestion }}: {{ targetLabel }}</div>
|
|
</article>
|
|
{% else %}
|
|
<div>{{ 'survey.edit.no_conditions'|trans }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
<script src="{{ asset('assets/handorgel/js/handorgel.min.js') }}"></script>
|