Files
newssubmission-bundle/contao/templates/frontend/news_submission.html.twig
T
Jürgen Mummert 0ea1a9a8ac Initial release
2026-03-02 21:28:17 +01:00

510 lines
18 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "@Contao/frontend_module/_base.html.twig" %}
{% block content %}
<link rel="stylesheet" href="https://unpkg.com/filepond/dist/filepond.min.css">
<style>
.news-editor-toolbar {
display: flex;
flex-wrap: wrap;
gap: .5rem;
margin: .5rem 0;
}
.news-editor-toolbar button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.2rem;
padding: .4rem .65rem;
border: 1px solid #cbd5e1;
border-radius: .375rem;
background: #f8fafc;
color: #1f2937;
font-size: .9rem;
font-weight: 600;
cursor: pointer;
line-height: 1;
}
.news-editor-toolbar button img {
width: 1rem;
height: 1rem;
display: block;
filter: brightness(0) saturate(100%) invert(18%) sepia(18%) saturate(522%) hue-rotate(177deg) brightness(94%) contrast(91%);
transition: filter .15s ease;
}
.news-editor-toolbar button:hover {
background: #eef2ff;
border-color: #a5b4fc;
}
.news-editor-toolbar button:hover img {
filter: brightness(0) saturate(100%) invert(24%) sepia(57%) saturate(2496%) hue-rotate(231deg) brightness(87%) contrast(105%);
}
.news-editor-toolbar button.is-active {
background: #4f46e5;
border-color: #4338ca;
color: #fff;
}
.news-editor-toolbar button.is-active img {
filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(7486%) hue-rotate(63deg) brightness(106%) contrast(101%);
}
.news-editor-toolbar button:disabled {
opacity: .45;
cursor: not-allowed;
background: #f1f5f9;
border-color: #d1d5db;
color: #6b7280;
}
.news-editor-toolbar button:disabled img {
filter: brightness(0) saturate(100%) invert(49%) sepia(8%) saturate(616%) hue-rotate(175deg) brightness(91%) contrast(88%);
}
.news-editor {
border: 1px solid #cbd5e1;
border-radius: .375rem;
padding: .75rem;
min-height: 14rem;
background: #fff;
}
.news-editor .ProseMirror {
outline: none;
min-height: 12rem;
}
.news-editor-counter {
margin-top: .5rem;
font-size: .85rem;
color: #475569;
}
.news-editor-counter.is-over-limit {
color: #b91c1c;
font-weight: 600;
}
.news-editor-shortcuts {
margin-top: .6rem;
font-size: .85rem;
color: #334155;
}
.news-editor-shortcuts ul {
margin: .25rem 0 0;
padding-left: 1.1rem;
}
.news-error-summary {
margin: 0 0 1rem;
padding: .75rem 1rem;
border: 1px solid #dc2626;
border-radius: .375rem;
background: #fef2f2;
color: #7f1d1d;
}
.news-error-summary ul {
margin: .4rem 0 0;
padding-left: 1.15rem;
}
.news-form-hint {
margin: 0 0 .85rem;
color: #334155;
font-size: .9rem;
}
.ns-tag-switches {
display: grid;
gap: .5rem;
margin-top: .35rem;
}
.ns-tag-switch-item {
display: flex;
align-items: center;
gap: .65rem;
}
.ns-tag-switch-item label {
margin: 0;
}
.visually-hidden {
position: absolute !important;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
white-space: nowrap;
}
.ns-tag-switch-input {
appearance: none;
width: 2.5rem;
height: 1.4rem;
border-radius: 999px;
border: 1px solid #94a3b8;
background: #e2e8f0;
position: relative;
cursor: pointer;
transition: background-color .15s ease;
}
.ns-tag-switch-input::after {
content: '';
position: absolute;
top: .1rem;
left: .12rem;
width: 1rem;
height: 1rem;
border-radius: 999px;
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, .2);
transition: transform .15s ease;
}
.ns-tag-switch-input:checked {
background: #4f46e5;
border-color: #4338ca;
}
.ns-tag-switch-input:checked::after {
transform: translateX(1.05rem);
}
.ns-tag-switch-input:focus-visible {
outline: 2px solid #1d4ed8;
outline-offset: 2px;
}
</style>
{% if errorMessage %}
<p role="alert">{{ errorMessage }}</p>
{% endif %}
{{ form_start(form, {
action: app.request.uri,
attr: {
enctype: 'multipart/form-data',
id: 'news-submission-form',
'aria-describedby': 'news-form-hint'
}
}) }}
<input type="hidden" name="REQUEST_TOKEN" value="{{ requestToken }}">
<p id="news-form-hint" class="news-form-hint">Pflichtfelder sind mit einem Stern markiert.</p>
{% if form.vars.submitted and not form.vars.valid %}
<div class="news-error-summary" role="alert" aria-labelledby="news-error-summary-title" tabindex="-1">
<strong id="news-error-summary-title">Bitte korrigieren Sie die markierten Felder:</strong>
<ul>
{% for field in form %}
{% if field.vars.errors|length > 0 and field.vars.id is not empty %}
<li>
<a href="#{{ field.vars.id }}">{{ field.vars.label|default(field.vars.name) }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
{{ form_row(form.headline, { attr: { autocomplete: 'off' } }) }}
{{ form_row(form.subheadline, { attr: { autocomplete: 'off' } }) }}
{{ form_row(form.externalLink, { attr: { autocomplete: 'url', inputmode: 'url' } }) }}
<fieldset class="widget" aria-describedby="news-tags-help news-tags-errors">
<legend>{{ form.tags.vars.label }}</legend>
<p id="news-tags-help" class="news-form-hint">Mehrfachauswahl möglich. Mit Leertaste ein- und ausschalten.</p>
<div class="ns-tag-switches">
{% for tagField in form.tags %}
{% set tagLabelId = tagField.vars.id ~ '-label' %}
<div class="ns-tag-switch-item">
{{ form_widget(tagField, { attr: { 'aria-describedby': tagField.vars.id ~ '-state', 'aria-labelledby': tagLabelId } }) }}
{{ form_label(tagField, null, { label_attr: { id: tagLabelId } }) }}
<span
id="{{ tagField.vars.id }}-state"
class="visually-hidden"
data-switch-state-for="{{ tagField.vars.id }}"
>
{{ tagField.vars.checked ? 'Ein' : 'Aus' }}
</span>
</div>
{% endfor %}
</div>
<div id="news-tags-errors">{{ form_errors(form.tags) }}</div>
</fieldset>
<div class="widget">
{{ form_label(form.articleText, null, { label_attr: { id: form.articleText.vars.id ~ '-label' } }) }}
<div
class="news-editor-toolbar"
data-news-editor-toolbar="{{ form.articleText.vars.id }}"
role="toolbar"
aria-label="Formatierungswerkzeuge Artikeltext"
aria-orientation="horizontal"
aria-describedby="{{ form.articleText.vars.id }}-shortcuts"
aria-controls="{{ form.articleText.vars.id }}-editor"
>
<button type="button" data-action="paragraph" title="Absatz">
<img src="{{ asset('bundles/newssubmission/icons/paragraph.svg') }}" alt="" aria-hidden="true">
<span class="visually-hidden">Absatz</span>
</button>
<button type="button" data-action="h2" title="Überschrift H2">
<img src="{{ asset('bundles/newssubmission/icons/h2.svg') }}" alt="" aria-hidden="true">
<span class="visually-hidden">H2</span>
</button>
<button type="button" data-action="h3" title="Überschrift H3">
<img src="{{ asset('bundles/newssubmission/icons/h3.svg') }}" alt="" aria-hidden="true">
<span class="visually-hidden">H3</span>
</button>
<button type="button" data-action="bold" title="Fett (Strg/Cmd+B)" aria-keyshortcuts="Control+B Meta+B">
<img src="{{ asset('bundles/newssubmission/icons/bold.svg') }}" alt="" aria-hidden="true">
<span class="visually-hidden">Fett</span>
</button>
<button type="button" data-action="italic" title="Kursiv (Strg/Cmd+I)" aria-keyshortcuts="Control+I Meta+I">
<img src="{{ asset('bundles/newssubmission/icons/italic.svg') }}" alt="" aria-hidden="true">
<span class="visually-hidden">Kursiv</span>
</button>
<button type="button" data-action="underline" title="Unterstrichen (Strg/Cmd+U)" aria-keyshortcuts="Control+U Meta+U">
<img src="{{ asset('bundles/newssubmission/icons/underline.svg') }}" alt="" aria-hidden="true">
<span class="visually-hidden">Unterstrichen</span>
</button>
<button type="button" data-action="bulletList" title="Liste">
<img src="{{ asset('bundles/newssubmission/icons/ul.svg') }}" alt="" aria-hidden="true">
<span class="visually-hidden">Liste</span>
</button>
<button type="button" data-action="orderedList" title="Nummerierte Liste">
<img src="{{ asset('bundles/newssubmission/icons/ol.svg') }}" alt="" aria-hidden="true">
<span class="visually-hidden">Nummerierte Liste</span>
</button>
<button type="button" data-action="indent" title="Einzug vergrößern">
<img src="{{ asset('bundles/newssubmission/icons/indent.svg') }}" alt="" aria-hidden="true">
<span class="visually-hidden">Einzug vergrößern</span>
</button>
<button type="button" data-action="outdent" title="Einzug verkleinern">
<img src="{{ asset('bundles/newssubmission/icons/outdent.svg') }}" alt="" aria-hidden="true">
<span class="visually-hidden">Einzug verkleinern</span>
</button>
<button type="button" data-action="undo" title="Rückgängig (Strg/Cmd+Z)" aria-keyshortcuts="Control+Z Meta+Z">
<img src="{{ asset('bundles/newssubmission/icons/undo.svg') }}" alt="" aria-hidden="true">
<span class="visually-hidden">Rückgängig</span>
</button>
<button type="button" data-action="redo" title="Wiederholen (Strg/Cmd+Shift+Z)" aria-keyshortcuts="Control+Shift+Z Meta+Shift+Z">
<img src="{{ asset('bundles/newssubmission/icons/redo.svg') }}" alt="" aria-hidden="true">
<span class="visually-hidden">Wiederholen</span>
</button>
</div>
<div id="{{ form.articleText.vars.id }}-shortcuts" class="news-editor-shortcuts">
<strong>Tastaturkürzel:</strong>
<ul>
<li><span aria-hidden="true">Strg/Cmd+B</span> <span class="visually-hidden">Tastenkürzel Strg oder Command plus B</span> Fett</li>
<li><span aria-hidden="true">Strg/Cmd+I</span> <span class="visually-hidden">Tastenkürzel Strg oder Command plus I</span> Kursiv</li>
<li><span aria-hidden="true">Strg/Cmd+U</span> <span class="visually-hidden">Tastenkürzel Strg oder Command plus U</span> Unterstrichen</li>
<li><span aria-hidden="true">Strg/Cmd+Z</span> <span class="visually-hidden">Tastenkürzel Strg oder Command plus Z</span> Rückgängig</li>
<li><span aria-hidden="true">Strg/Cmd+Shift+Z</span> <span class="visually-hidden">Tastenkürzel Strg oder Command plus Umschalt plus Z</span> Wiederholen</li>
<li>Pfeiltasten links/rechts in der Toolbar zwischen Buttons wechseln</li>
</ul>
</div>
<div
id="{{ form.articleText.vars.id }}-editor"
class="news-editor"
data-news-editor="tiptap"
data-textarea-id="{{ form.articleText.vars.id }}"
role="textbox"
aria-multiline="true"
aria-labelledby="{{ form.articleText.vars.id }}-label"
aria-describedby="{{ form.articleText.vars.id }}-shortcuts {{ form.articleText.vars.id }}-counter {{ form.articleText.vars.id }}-errors"
></div>
<div id="{{ form.articleText.vars.id }}-counter" class="news-editor-counter" data-news-editor-counter-for="{{ form.articleText.vars.id }}" role="status" aria-live="polite"></div>
{{ form_widget(form.articleText, { attr: { class: 'js-news-article-source', rows: 8, 'aria-hidden': 'true', tabindex: '-1' } }) }}
<div id="{{ form.articleText.vars.id }}-errors">{{ form_errors(form.articleText) }}</div>
</div>
<div class="widget" aria-describedby="news-image-toggle-help {{ form.addImage.vars.id }}-state">
<div class="ns-tag-switch-item">
{{ form_widget(form.addImage, { attr: {
'aria-describedby': form.addImage.vars.id ~ '-state',
'aria-controls': 'news-image-fields',
'aria-expanded': form.addImage.vars.checked ? 'true' : 'false',
'aria-labelledby': form.addImage.vars.id ~ '-label'
} }) }}
{{ form_label(form.addImage, null, { label_attr: { id: form.addImage.vars.id ~ '-label' } }) }}
<span
id="{{ form.addImage.vars.id }}-state"
class="visually-hidden"
data-switch-state-for="{{ form.addImage.vars.id }}"
>
{{ form.addImage.vars.checked ? 'Ein' : 'Aus' }}
</span>
</div>
<p id="news-image-toggle-help" class="news-form-hint">Wenn aktiviert, werden Felder für Bild, Fotograf und Alternativtext eingeblendet.</p>
{{ form_errors(form.addImage) }}
</div>
<div id="news-image-fields" style="display:none;" hidden aria-hidden="true" role="group" aria-label="Bildangaben" aria-live="polite">
{{ form_row(form.imageUpload) }}
{{ form_row(form.imageToken) }}
{{ form_row(form.photographer) }}
{{ form_row(form.altText) }}
</div>
{% if form.publicName is defined %}
{{ form_row(form.publicName) }}
{% endif %}
{% if form.publicEmail is defined %}
{{ form_row(form.publicEmail) }}
{% endif %}
{{ form_row(form.honeypot) }}
{% if showAltcha %}
<div class="widget">
<label for="ns-altcha" id="ns-altcha-label">Sicherheitsprüfung</label>
<altcha-widget
id="ns-altcha"
name="altcha"
challengeurl="{{ altchaChallengeUrl }}"
maxnumber="{{ altchaMaxNumber }}"
aria-labelledby="ns-altcha-label"
></altcha-widget>
</div>
<script type="module" src="https://cdn.jsdelivr.net/npm/altcha/dist/altcha.min.js"></script>
{% endif %}
<button type="submit">News einreichen</button>
{{ form_end(form) }}
<script src="https://unpkg.com/filepond/dist/filepond.min.js"></script>
<script type="module" src="{{ asset('bundles/newssubmission/editor.js') }}?v=9"></script>
<script>
(function () {
const errorSummary = document.querySelector('.news-error-summary');
const addImage = document.getElementById('{{ form.addImage.vars.id }}');
const imageWrap = document.getElementById('news-image-fields');
const photographer = document.getElementById('{{ form.photographer.vars.id }}');
const fileInput = document.querySelector('input.js-news-filepond');
const tokenInput = document.getElementById('{{ form.imageToken.vars.id }}');
const tagSwitches = document.querySelectorAll('input.ns-tag-switch-input[role="switch"]');
if (errorSummary) {
requestAnimationFrame(() => {
errorSummary.focus();
});
}
const syncSwitchState = (input) => {
const isChecked = !!input.checked;
input.setAttribute('aria-checked', isChecked ? 'true' : 'false');
const stateNode = document.querySelector(`[data-switch-state-for="${input.id}"]`);
if (stateNode) {
stateNode.textContent = isChecked ? 'Ein' : 'Aus';
}
};
tagSwitches.forEach((input) => {
syncSwitchState(input);
input.addEventListener('change', () => syncSwitchState(input));
});
const updateImageState = () => {
if (!addImage || !imageWrap) {
return;
}
const active = addImage.checked;
addImage.setAttribute('aria-checked', active ? 'true' : 'false');
addImage.setAttribute('aria-expanded', active ? 'true' : 'false');
const addImageStateNode = document.querySelector(`[data-switch-state-for="${addImage.id}"]`);
if (addImageStateNode) {
addImageStateNode.textContent = active ? 'Ein' : 'Aus';
}
imageWrap.style.display = active ? '' : 'none';
imageWrap.hidden = !active;
imageWrap.setAttribute('aria-hidden', active ? 'false' : 'true');
if (!active && imageWrap.contains(document.activeElement)) {
addImage.focus();
}
if (photographer) {
photographer.required = active;
}
if (!active && tokenInput) {
tokenInput.value = '';
}
};
if (addImage) {
addImage.addEventListener('change', updateImageState);
updateImageState();
}
if (fileInput && tokenInput && typeof FilePond !== 'undefined') {
const pond = FilePond.create(fileInput, {
allowMultiple: false,
instantUpload: true,
acceptedFileTypes: ['image/jpeg', 'image/png', 'image/webp'],
maxFileSize: '6MB',
credits: false,
server: {
process: {
url: '{{ uploadProcessUrl }}',
method: 'POST',
ondata: (formData) => {
formData.append('REQUEST_TOKEN', '{{ requestToken }}');
return formData;
},
headers: {
'X-CSRF-Token': '{{ requestToken }}'
},
onload: (response) => {
tokenInput.value = response;
return response;
}
},
revert: {
url: '{{ uploadRevertUrl }}',
method: 'POST',
headers: {
'X-CSRF-Token': '{{ requestToken }}'
},
onload: () => {
tokenInput.value = '';
}
}
}
});
if (addImage) {
addImage.addEventListener('change', () => {
if (!addImage.checked) {
pond.removeFiles();
}
});
}
}
})();
</script>
{% endblock %}