The menu editor gets "Als Button darstellen" per entry: shape, appearance and size from the button design, fill and ink with hover and palette, icon colour, icon from the dialog, motion, icon-only and where the button sits on the phone (in the opened menu, beside the burger, or there as an icon only). Colours go into the page stylesheet under a stable per-button id because the CSP forbids inline styles. ADR 110. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
329 lines
31 KiB
Twig
329 lines
31 KiB
Twig
{% set type = block.type %}
|
|
{% set props = block.props|default({}) %}
|
|
{% set layout = block.layout|default({}) %}
|
|
{% set designClasses = layout.visibility|default('all') != 'all' ? ' visibility-' ~ layout.visibility : '' %}
|
|
{% for device, settings in layout.design|default({}) %}
|
|
{% for key, value in settings %}{% set designClasses = designClasses ~ ' d-' ~ device ~ '-' ~ key ~ '-' ~ value %}{% endfor %}
|
|
{% endfor %}
|
|
{% for key, value in layout.effects|default({}) %}{% set designClasses = designClasses ~ ' fx-' ~ key ~ '-' ~ value %}{% endfor %}
|
|
{% if type in ['section', 'row', 'column'] %}
|
|
{% set tag = type == 'section' ? 'section' : 'div' %}
|
|
<{{ tag }} id="{{ block.id }}" class="block-{{ type }} tone-{{ layout.tone|default('paper') }} width-{{ layout.width|default('normal') }}{{ designClasses }}{{ block.anchor|default('') ? ' has-anchor' : '' }}">
|
|
{% if block.anchor|default('') %}<span id="{{ block.anchor }}" class="block-anchor" aria-hidden="true"></span>{% endif %}
|
|
{% if type == 'section' %}<div class="shell section-inner">{% endif %}
|
|
{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
|
|
{% if type == 'section' %}</div>{% endif %}
|
|
</{{ tag }}>
|
|
{% elseif type == 'heading' %}
|
|
<h{{ props.level }} id="{{ block.id }}" class="{{ designClasses|trim }}">{% if richHeadings|default(false) %}{{ props.text|raw }}{% else %}{{ props.text }}{% endif %}</h{{ props.level }}>
|
|
{% elseif type == 'text' %}
|
|
{% if richText|default(false) %}
|
|
{# Already filtered by RichText when it was stored, so printing it unescaped is safe.
|
|
A div, not a p: the allowlist includes lists and paragraphs, which may not nest in one. #}
|
|
<div id="{{ block.id }}" class="block-text{{ designClasses }}">{{ props.text|raw }}</div>
|
|
{% else %}
|
|
<p id="{{ block.id }}" class="block-text{{ designClasses }}">{{ props.text }}</p>
|
|
{% endif %}
|
|
{% elseif type == 'button' %}
|
|
{% for device, settings in layout.button|default({}) %}{% for key, value in settings %}{% set designClasses = designClasses ~ ' b-' ~ device ~ '-' ~ key ~ '-' ~ value %}{% endfor %}{% endfor %}
|
|
{# Schema v25: Symbol wählbar. Ohne Angabe der Pfeil wie bisher; er behält die Klasse
|
|
button-arrow, damit eigenes CSS älterer Websites (ausblenden, drehen) weiter greift. #}
|
|
{% set buttonIcon = props.icon|default('arrow-up-right') %}
|
|
{% set iconMarkup = '' %}
|
|
{% if buttonIcon != 'none' %}{% set iconMarkup %}<span class="button-icon{{ buttonIcon == 'arrow-up-right' ? ' button-arrow' }}" aria-hidden="true">{% include 'blocks/icon.html.twig' with {name: buttonIcon} only %}</span>{% endset %}{% endif %}
|
|
{% set iconFirst = props.iconPosition|default('end') == 'start' %}
|
|
{# Schema v26: Bewegung beim Überfahren und ein Button nur aus Symbol. Der Text steht dann
|
|
in einer Beschriftung ohne Breite und bleibt der Name des Links für Vorlesesoftware. #}
|
|
{% set iconOnly = iconMarkup and props.iconOnly|default(false) %}
|
|
{% if iconMarkup %}{% set designClasses = designClasses ~ (iconFirst ? ' icon-start' : '') ~ (props.iconMotion|default('none') != 'none' ? ' icon-motion-' ~ props.iconMotion : '') ~ (iconOnly ? ' icon-only' : '') %}{% endif %}
|
|
<a id="{{ block.id }}" class="button cms-button{{ designClasses }}" href="{{ props.href }}"{% include 'blocks/link-attributes.html.twig' with {newTab: props.newTab|default(false)} only %}>{% if iconMarkup and iconFirst %}{{ iconMarkup }} {% endif %}{% if iconOnly %}<span class="button-label">{{ props.text }}</span>{% else %}{{ props.text }}{% endif %}{% if iconMarkup and not iconFirst %} {{ iconMarkup }}{% endif %}</a>
|
|
{% elseif type == 'image' %}
|
|
{# Wie breit das Bild tatsaechlich erscheint. Steht im Block eine feste Breite - ein Logo mit
|
|
190px, ein Symbol mit 72px -, dann ist das die Antwort, und der Browser laedt die kleine
|
|
Variante statt der grossen. Ohne feste Breite bleibt es bei der Annahme nach Spaltenbreite. #}
|
|
{% set festeBreite = block.style.desktop.maxWidth|default('') %}
|
|
{% set eager = eagerImage|default(null) == block.id %}
|
|
{# „auto“ heißt: der Browser misst selbst nach, wie breit das Bild tatsächlich erscheint, und
|
|
wählt genau dazu die Variante. Das geht nur bei verzögert geladenen Bildern - beim
|
|
vorgezogenen Bild steht die Größe noch nicht fest. Die Angaben dahinter sind der Rückfall
|
|
für Browser, die „auto“ noch nicht kennen (heute Safari). #}
|
|
{% set annahme = '(max-width: 650px) 100vw, (max-width: 1024px) 50vw, 800px' %}
|
|
{% set imageSizes = festeBreite ends with 'px' ? festeBreite : (eager or inHeader|default(false) ? annahme : 'auto, ' ~ annahme) %}
|
|
{# Ohne eigene Wahl folgt ein Bild seiner Galerie: ist sie vergrößerbar, ist es das Bild auch.
|
|
Eine eigene Wahl - auch „Nichts tun“ - geht vor, ebenso ein Link. #}
|
|
{% set klick = props.click|default(props.href|default('') ? 'link' : (galleryZoom|default(false) ? 'vergroessern' : 'keine')) %}
|
|
{% set imageLinked = klick == 'link' and props.href|default('') %}
|
|
{% set grossesBild = props.sources|default([]) ? props.sources|last.src : props.src %}
|
|
<figure id="{{ block.id }}" class="block-image{{ designClasses }}">
|
|
{%- if klick == 'vergroessern' -%}
|
|
{# Ohne Skript öffnet dieser Verweis schlicht das große Bild. Mit Skript fängt der
|
|
Betrachter den Klick ab - die Seite bleibt also in jedem Fall benutzbar. #}
|
|
<a class="image-link image-zoom" href="{{ grossesBild }}" data-zoom data-caption="{{ props.caption|default('') }}" aria-label="{{ props.alt|default('Bild') }} vergrößern">
|
|
{%- elseif imageLinked -%}
|
|
<a class="image-link" href="{{ props.href }}"{% include 'blocks/link-attributes.html.twig' with {newTab: props.newTab|default(false)} only %}>
|
|
{%- endif -%}<img src="{{ props.src }}" alt="{{ props.alt }}" width="{{ props.width }}" height="{{ props.height }}" loading="{{ eager or inHeader|default(false) ? 'eager' : 'lazy' }}"{{ eager ? ' fetchpriority="high"' : '' }} decoding="async"{% if props.sources|default([]) %} srcset="{% for source in props.sources %}{{ source.src }} {{ source.width }}w{% if not loop.last %}, {% endif %}{% endfor %}" sizes="{{ imageSizes }}"{% endif %}>{% if klick == 'vergroessern' or imageLinked %}</a>{% endif %}{% if props.caption|default('') %}<figcaption>{{ props.caption }}</figcaption>{% endif %}</figure>
|
|
{% elseif type == 'iconcard' %}
|
|
<div id="{{ block.id }}" class="block-iconcard{{ designClasses }}">
|
|
{% include 'blocks/icon.html.twig' with {name: props.icon} only %}
|
|
<h3>{% if props.href|default('') %}<a href="{{ props.href }}"{% include 'blocks/link-attributes.html.twig' with {newTab: props.newTab|default(false)} only %}>{{ props.title }}</a>{% else %}{{ props.title }}{% endif %}</h3>
|
|
{% if props.text|default('') %}<p>{{ props.text }}</p>{% endif %}
|
|
</div>
|
|
{% elseif type == 'stat' %}
|
|
<p id="{{ block.id }}" class="block-stat{{ designClasses }}"><strong>{{ props.value }}</strong><span>{{ props.label }}</span></p>
|
|
{% elseif type == 'quote' %}
|
|
<figure id="{{ block.id }}" class="block-quote{{ designClasses }}">
|
|
{% if props.rating|default(0) > 0 %}
|
|
<p class="quote-rating"><span class="visually-hidden">Bewertung: {{ props.rating }} von 5</span><span aria-hidden="true">{% for i in 1..5 %}{{ i <= props.rating ? '★' : '☆' }}{% endfor %}</span></p>
|
|
{% endif %}
|
|
<blockquote><p>{{ props.text }}</p></blockquote>
|
|
<figcaption>{{ props.author }}{% if props.role|default('') %} <span>{{ props.role }}</span>{% endif %}</figcaption>
|
|
</figure>
|
|
{% elseif type == 'logos' %}
|
|
<ul id="{{ block.id }}" class="block-logos{{ designClasses }}">
|
|
{% for child in block.children|default([]) %}<li class="visibility-{{ child.layout.visibility|default('all') }}">{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}</li>{% endfor %}
|
|
</ul>
|
|
{% elseif type == 'cta' %}
|
|
<div id="{{ block.id }}" class="block-cta{{ designClasses }}">
|
|
<div>
|
|
<h2>{{ props.title }}</h2>
|
|
{% if props.text|default('') %}<p>{{ props.text }}</p>{% endif %}
|
|
</div>
|
|
<a class="button" href="{{ props.href }}"{% include 'blocks/link-attributes.html.twig' with {newTab: props.newTab|default(false)} only %}>{{ props.buttonText }}</a>
|
|
</div>
|
|
{% elseif type == 'contact' %}
|
|
{% if not (props.phone|default('') or props.email|default('') or props.address|default('')) %}
|
|
{% if editing|default(false) %}<p id="{{ block.id }}" class="block-unavailable{{ designClasses }}" role="note">Kontaktdaten: Telefon, E-Mail oder Adresse unter Inhalt eintragen.</p>{% endif %}
|
|
{% else %}
|
|
<address id="{{ block.id }}" class="block-contact{{ designClasses }}">
|
|
{% if props.phone|default('') %}<span>{% include 'blocks/icon.html.twig' with {name: 'phone'} only %}<a href="tel:{{ props.phone|replace({' ': '', '(': '', ')': '', '/': '', '-': ''}) }}">{{ props.phone }}</a></span>{% endif %}
|
|
{% if props.email|default('') %}<span>{% include 'blocks/icon.html.twig' with {name: 'mail'} only %}<a href="mailto:{{ props.email }}">{{ props.email }}</a></span>{% endif %}
|
|
{% if props.address|default('') %}<span>{% include 'blocks/icon.html.twig' with {name: 'pin'} only %}{{ props.address }}</span>{% endif %}
|
|
</address>
|
|
{% endif %}
|
|
{% elseif type == 'divider' %}
|
|
<hr id="{{ block.id }}" class="block-divider{{ designClasses }}">
|
|
{% elseif type == 'spacer' %}
|
|
<div id="{{ block.id }}" class="block-spacer spacer-{{ props.size }}{{ designClasses }}" aria-hidden="true"></div>
|
|
{% elseif type == 'menu' %}
|
|
{% set links = navigation[props.slot]|default([]) %}
|
|
{% if links is not empty %}
|
|
{# The layout is a class, and the burger variant is a details element: a menu that needs a
|
|
script to open would be a menu that does not open on a page without one. #}
|
|
{% set menuLayout = props.layout|default('horizontal') %}
|
|
{% set collapse = menuLayout == 'horizontal' ? props.collapse|default('tablet') : 'never' %}
|
|
<nav id="{{ block.id }}" class="block-menu menu-{{ menuLayout }} menu-collapse-{{ collapse }}{{ designClasses }}"
|
|
aria-label="{{ {header: 'Hauptnavigation', secondary: 'Zweite Navigation', footer: 'Fußzeile', legal: 'Rechtliches'}[props.slot]|default('Navigation') }}">
|
|
{% if menuLayout == 'burger' or collapse != 'never' %}
|
|
{# Buttons, die auf dem Telefon neben dem Menü-Knopf stehen bleiben (ADR 110). Sichtbar nur,
|
|
solange das Menü zusammengeklappt ist; im aufgeklappten Menü fehlen sie dann. #}
|
|
{% set barLinks = links|filter(link => link.button is defined and link.button.mobile|default('menu') in ['bar', 'bar-icon']) %}
|
|
{% if barLinks is not empty %}<ul class="menu-actions">{% for link in barLinks %}<li>{% include 'menu-button.html.twig' with {link: link, forceIconOnly: link.button.mobile == 'bar-icon'} only %}</li>{% endfor %}</ul>{% endif %}
|
|
<details class="menu-burger">
|
|
{# Nur Symbol behält den Namen für Vorlesesoftware; sichtbar ist dann nur der Strich. #}
|
|
{% set burger = props.burgerLabel|default('both') %}
|
|
<summary>{% if burger != 'text' %}<span class="burger-lines" aria-hidden="true"></span>{% endif %}<span class="burger-text{{ burger == 'icon' ? ' visually-hidden' : '' }}">Menü</span></summary>
|
|
<div class="menu-burger-body">{% include 'nav.html.twig' with {links: links} only %}</div>
|
|
</details>
|
|
{% endif %}
|
|
{% if menuLayout != 'burger' %}
|
|
{% include 'nav.html.twig' with {links: links} only %}
|
|
{% endif %}
|
|
</nav>
|
|
{% endif %}
|
|
{% elseif type == 'partialref' %}
|
|
{# A linked section: its blocks come from the partial, resolved by the controller. One change
|
|
there reaches every page that links it, which is the difference to a copied section. #}
|
|
{% set section = sections[props.partial]|default(null) %}
|
|
{% if props.partial|default('') == '' %}
|
|
{% if editing|default(false) %}<p id="{{ block.id }}" class="block-unavailable{{ designClasses }}" role="note">Verknüpfte Sektion: unter Inhalt die Adresse einer Sektion eintragen.</p>{% endif %}
|
|
{% elseif section %}
|
|
<div id="{{ block.id }}" class="block-partialref{{ designClasses }}">
|
|
{% for child in section.blocks %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
|
|
</div>
|
|
{% elseif editing|default(false) %}
|
|
<p id="{{ block.id }}" class="block-unavailable{{ designClasses }}" role="note">Die verknüpfte Sektion „{{ props.partial }}“ gibt es nicht mehr. Der Verweis bleibt gespeichert.</p>
|
|
{% else %}
|
|
<!-- Verknüpfte Sektion {{ props.partial }} ist nicht vorhanden. -->
|
|
{% endif %}
|
|
{% elseif type == 'breadcrumb' %}
|
|
{% if breadcrumbs|default([])|length > 1 %}
|
|
<nav id="{{ block.id }}" class="block-breadcrumb{{ designClasses }}" aria-label="Brotkrumen">
|
|
<ol>
|
|
<li><a href="/">Start</a></li>
|
|
{% for crumb in breadcrumbs %}
|
|
<li>{% if loop.last %}<span aria-current="page">{{ crumb.name }}</span>{% else %}<a href="{{ crumb.href }}">{{ crumb.name }}</a>{% endif %}</li>
|
|
{% endfor %}
|
|
</ol>
|
|
</nav>
|
|
{% endif %}
|
|
{% elseif type == 'form' %}
|
|
{% if props.form|default('') == '' %}
|
|
{% if editing|default(false) %}<p id="{{ block.id }}" class="block-unavailable{{ designClasses }}" role="note">Formular: unter Inhalt die Adresse eines Formulars eintragen (siehe Verwaltung → Formulare).</p>{% endif %}
|
|
{% else %}
|
|
<div id="{{ block.id }}" class="block-formwrap{{ designClasses }}">
|
|
{% include 'blocks/form.html.twig' with (forms[block.id]|default({form: null, sent: false, errors: {}, values: {}, optionImages: {}, blockId: block.id, page: {path: ''}, issued: 0, issuedSignature: ''})) only %}
|
|
</div>
|
|
{% endif %}
|
|
{% elseif type == 'tabs' %}
|
|
{# Without the script this is an anchor list followed by every panel: complete and readable.
|
|
tabs.js turns it into a tab widget; it only changes presentation, never content. #}
|
|
<div id="{{ block.id }}" class="block-tabs{{ designClasses }}" data-tabs>
|
|
<ul class="tabs-nav">
|
|
{% for child in block.children|default([]) %}<li class="visibility-{{ child.layout.visibility|default('all') }}"><a href="#{{ child.id }}">{{ child.props.title }}</a></li>{% endfor %}
|
|
</ul>
|
|
{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
|
|
</div>
|
|
{% elseif type == 'tabitem' %}
|
|
<section id="{{ block.id }}" class="block-tabitem{{ designClasses }}">
|
|
<h3 class="tabs-heading">{{ props.title }}</h3>
|
|
{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
|
|
</section>
|
|
{% elseif type == 'accordion' %}
|
|
<div id="{{ block.id }}" class="block-accordion{{ designClasses }}">
|
|
{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
|
|
</div>
|
|
{% elseif type == 'accordionitem' %}
|
|
<details id="{{ block.id }}" class="block-accordionitem{{ designClasses }}"{% if props.open|default(false) %} open{% endif %}>
|
|
<summary>{{ props.title }}</summary>
|
|
<div class="accordion-body">
|
|
{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
|
|
</div>
|
|
</details>
|
|
{% elseif type == 'icon' %}
|
|
{# A single icon. Decorative ones are hidden from assistive technology; one that means
|
|
something carries its label as text for screen readers. #}
|
|
{% set iconBody %}{% include 'blocks/icon.html.twig' with {name: props.icon} only %}{% if not props.decorative %}<span class="visually-hidden">{{ props.label }}</span>{% endif %}{% endset %}
|
|
{% if props.href|default('') %}
|
|
<a id="{{ block.id }}" class="block-icon icon-size-{{ props.size }}{{ designClasses }}" href="{{ props.href }}"{% include 'blocks/link-attributes.html.twig' with {newTab: props.newTab|default(false)} only %}{{ props.decorative ? ' aria-hidden="true" tabindex="-1"' : '' }}>{{ iconBody }}</a>
|
|
{% else %}
|
|
<span id="{{ block.id }}" class="block-icon icon-size-{{ props.size }}{{ designClasses }}"{{ props.decorative ? ' aria-hidden="true"' : '' }}>{{ iconBody }}</span>
|
|
{% endif %}
|
|
{% elseif type == 'social' %}
|
|
{# Die Profile kommen aus den Einstellungen, der Block sagt nur, wie sie aussehen. Eine
|
|
Liste, weil es eine ist; ohne Beschriftung trägt jeder Link seinen Namen unsichtbar. #}
|
|
{% set profiles = siteSettings.load().social|default({}) %}
|
|
{% set chosen = props.networks|default([]) %}
|
|
{% set shown = [] %}
|
|
{% for key, name in constant('App\\Content\\SocialProfiles::NETWORKS') %}
|
|
{% if profiles[key]|default('') and (chosen is empty or key in chosen) %}{% set shown = shown|merge([{key: key, name: name, url: profiles[key]}]) %}{% endif %}
|
|
{% endfor %}
|
|
{% if shown is not empty %}
|
|
<ul id="{{ block.id }}" class="block-social social-{{ props.layout|default('horizontal') }} social-shape-{{ props.shape|default('plain') }} social-size-{{ props.size|default(24) }}{{ props.labels|default(false) ? ' social-labels' : '' }}{{ designClasses }}" aria-label="Soziale Profile">
|
|
{% for profile in shown %}
|
|
<li><a href="{{ profile.url }}"{% include 'blocks/link-attributes.html.twig' with {newTab: props.newTab is defined ? props.newTab : true} only %}>{% include 'blocks/social-icon.html.twig' with {network: profile.key} only %}<span{{ props.labels|default(false) ? '' : ' class="visually-hidden"' }}>{{ profile.name }}</span></a></li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% elseif editing|default(false) %}
|
|
<p id="{{ block.id }}" class="block-unavailable{{ designClasses }}" role="note">Noch keine sozialen Profile eingetragen. Sie stehen unter Einstellungen → Unternehmensdaten → Soziale Profile.</p>
|
|
{% else %}
|
|
<!-- Block {{ block.id }} (social): keine Profile eingetragen. -->
|
|
{% endif %}
|
|
{% elseif type == 'progress' %}
|
|
{# A progress bar as a real meter: the number is in the markup, not only in the width of a
|
|
coloured div, so it is readable without seeing it. #}
|
|
<div id="{{ block.id }}" class="block-progress{{ designClasses }}">
|
|
<p class="progress-label">{{ props.label }}{% if props.showValue %} <span class="progress-value">{{ props.value }} %</span>{% endif %}</p>
|
|
<div class="progress-track" role="meter" aria-valuenow="{{ props.value }}" aria-valuemin="0" aria-valuemax="100" aria-label="{{ props.label }}">
|
|
<div class="progress-fill progress-fill-{{ props.value }}"></div>
|
|
</div>
|
|
</div>
|
|
{% elseif type == 'slider' %}
|
|
{# A slider without a script: a scroll container with scroll snapping. It works with a finger,
|
|
a trackpad, a mouse wheel and the keyboard, and it degrades to a scrollable strip on a
|
|
browser that ignores snapping. No autoplay - a moving carousel is something people ask to
|
|
stop, not something a page should start on its own. #}
|
|
<div id="{{ block.id }}" class="block-slider{{ designClasses }} slide-desktop-{{ props.desktopVisible }} slide-tablet-{{ props.tabletVisible }} slide-mobile-{{ props.mobileVisible }}{{ props.snap ? ' slider-snap' : '' }}{{ props.dots ?? false ? ' slider-dots' : '' }}"{% if props.autoplay|default(0) >= 3 %} data-autoplay="{{ props.autoplay }}"{% endif %}
|
|
tabindex="0" role="group" aria-label="Zum Blättern seitwärts scrollen">
|
|
{% for child in block.children|default([]) %}
|
|
<div class="slider-slide visibility-{{ child.layout.visibility|default('all') }}">{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% elseif type == 'embed' %}
|
|
{% if props.url|default('') == '' %}
|
|
{% if editing|default(false) %}<p id="{{ block.id }}" class="block-unavailable{{ designClasses }}" role="note">Externer Inhalt: unter Inhalt Adresse, Titel und Anbieter eintragen.</p>{% endif %}
|
|
{% else %}
|
|
{# Content from a third party. Until the group is granted, nothing of it is requested - no
|
|
iframe, no image, no preconnect. That is the whole point of the consent question: the
|
|
placeholder is not a nicety, it is what keeps the promise.
|
|
In the editor it always stays a placeholder: the administration's policy forbids foreign
|
|
frames, and a preview must not load anything from outside either. #}
|
|
{% if not editing|default(false) and consentGate.granted(app.request, props.group) %}
|
|
<div id="{{ block.id }}" class="block-embed{{ designClasses }}">
|
|
<iframe src="{{ props.url }}" title="{{ props.title }}" height="{{ props.height }}" loading="lazy" referrerpolicy="strict-origin-when-cross-origin" allow="fullscreen; picture-in-picture"></iframe>
|
|
</div>
|
|
{% else %}
|
|
<div id="{{ block.id }}" class="block-embed embed-blocked{{ designClasses }}">
|
|
<p class="embed-title">{{ props.title }}</p>
|
|
<p class="embed-hint">{{ props.hint ?: 'Dieser Inhalt wird von einem anderen Anbieter geladen. Dabei erfährt er deine IP-Adresse und welche Seite du ansiehst.' }}</p>
|
|
<p class="embed-provider">Anbieter: <strong>{{ props.provider ?: props.url|split('/')[2] }}</strong></p>
|
|
{% if editing|default(false) %}
|
|
<p class="embed-hint">Wird erst auf der veröffentlichten Seite geladen, und dort erst nach Einwilligung.</p>
|
|
{% else %}
|
|
<form method="post" action="{{ path('consent_save') }}">
|
|
<input type="hidden" name="back" value="{{ app.request.pathInfo }}">
|
|
<input type="hidden" name="choice" value="some">
|
|
{# The groups already granted stay granted; this button adds this one. #}
|
|
{% for granted in consentGate.state(app.request).groups %}{% if granted.granted %}<input type="hidden" name="groups[]" value="{{ granted.key }}">{% endif %}{% endfor %}
|
|
<input type="hidden" name="groups[]" value="{{ props.group }}">
|
|
<button class="consent-button" type="submit">Inhalt laden und einwilligen</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% elseif type == 'html' %}
|
|
{# Markup des Betreibers, unveraendert. Geprueft wurde es beim Speichern (CustomCode::checkHtml),
|
|
setzen darf es nur ein Administrator - deshalb steht hier `raw` und keine Filterung. #}
|
|
{% if props.html|default('')|trim == '' %}
|
|
{% if editing|default(false) %}<p id="{{ block.id }}" class="block-unavailable{{ designClasses }}" role="note">Eigenes HTML: unter Inhalt den Code einfügen.</p>{% endif %}
|
|
{% else %}
|
|
<div id="{{ block.id }}" class="block-html{{ designClasses }}">{{ props.html|raw }}</div>
|
|
{% endif %}
|
|
{% elseif type == 'postlist' %}
|
|
<div id="{{ block.id }}" class="block-postlist{{ designClasses }}">
|
|
{% set feed = postLists[block.id]|default([]) %}
|
|
{% include 'blocks/post-list.html.twig' with {editing: editing|default(false), posts: feed.posts|default(feed), pages: feed.pages|default(1), page: feed.page|default(1), total: feed.total|default(null), layout: block.props.layout|default('karten'), showMeta: block.props.showMeta ?? true, showExcerpt: block.props.showExcerpt ?? true, showTerms: block.props.showTerms ?? true} only %}
|
|
</div>
|
|
{% elseif type == 'postnav' %}
|
|
<div id="{{ block.id }}" class="block-postnav{{ designClasses }}">
|
|
{% include 'blocks/post-nav.html.twig' with {previous: neighbours.previous|default(null), next: neighbours.next|default(null), previousLabel: props.previousLabel|default('Vorheriger Beitrag'), nextLabel: props.nextLabel|default('Nächster Beitrag')} only %}
|
|
</div>
|
|
{% elseif type == 'legal' %}
|
|
{# Impressum, Datenschutz und - sofern schon gefragt wurde - der Weg zurück zur Einwilligung.
|
|
Als Baustein, damit die Zeile dort steht, wo sie hingehört, und die Farben ihres Bereichs
|
|
erbt statt als heller Streifen unter einem dunklen Fuß zu kleben. #}
|
|
{% set rechtliches = consentGate.legalLinks() %}
|
|
{% set widerruflich = not editing|default(false) and consentGate.askedGroups is not empty and not consentGate.needsBanner(app.request) %}
|
|
<nav id="{{ block.id }}" class="block-legal{{ designClasses }}" aria-label="{{ props.label|default('Rechtliches') }}">
|
|
{% for path, label in rechtliches %}{% if not loop.first %}<span aria-hidden="true"> · </span>{% endif %}<a href="/{{ path }}">{{ label }}</a>{% endfor %}
|
|
{% if widerruflich %}{% if rechtliches is not empty %}<span aria-hidden="true"> · </span>{% endif %}<a href="/einwilligung">Einwilligung ändern</a>{% endif %}
|
|
</nav>
|
|
{% elseif type == 'gallery' %}
|
|
<div id="{{ block.id }}" class="block-gallery{{ designClasses }}" role="group" aria-label="Bildergalerie"{% if props.lightbox|default(false) %} data-zoom-group{% endif %}>
|
|
{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, galleryZoom: props.lightbox|default(false), editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}
|
|
</div>
|
|
{% elseif kant_plugin_block(type) %}
|
|
{# Ein Baustein eines aktiven Plugins. Die Vorlage bekommt nur, was der Kern geprüft hat:
|
|
die Eigenschaften nach dem Schema des Manifests. Passen sie nicht, bleibt der Platz leer. #}
|
|
{% set plugin = kant_plugin_block(type) %}
|
|
{% set pluginProps = kant_plugin_props(block) %}
|
|
{% if pluginProps is null %}
|
|
<!-- Block {{ block.id }} ({{ type }}) hat Eigenschaften, die nicht zum Plugin passen, und wird nicht dargestellt. -->
|
|
{% if editing|default(false) %}<p id="{{ block.id }}" class="block-unavailable{{ designClasses }}" role="note">Baustein „{{ plugin.label }}“: die gespeicherten Eigenschaften passen nicht zum Plugin.</p>{% endif %}
|
|
{% else %}
|
|
{# Ein Container rendert seine Kinder hier, mit demselben Kontext wie jeder Kernbaustein, und
|
|
reicht sie der Vorlage fertig gerendert als `children` - so kommt eine Plugin-Vorlage nie
|
|
an Formulare, Beitragslisten oder die Navigation heran. #}
|
|
{% set childrenHtml %}{% if plugin.container %}{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({}), forms: forms|default({}), breadcrumbs: breadcrumbs|default([]), navigation: navigation|default({}), sections: sections|default({}), richText: richText|default(false), richHeadings: richHeadings|default(false), neighbours: neighbours|default({}), eagerImage: eagerImage|default(null), inHeader: inHeader|default(false)} only %}{% endfor %}{% endif %}{% endset %}
|
|
{% include plugin.template with {block: block, props: pluginProps, designClasses: designClasses, editing: editing|default(false), plugin: plugin, children: childrenHtml, settings: kant_plugin_settings(plugin.plugin)} only %}
|
|
{% endif %}
|
|
{% else %}
|
|
{#
|
|
A block whose plugin is not available. Its stored content stays in the document; the core
|
|
renders nothing from its unvalidated props. Visitors see no broken fragment, while the
|
|
editor preview shows a placeholder so the block can still be found and moved.
|
|
#}
|
|
<!-- Block {{ block.id }} ({{ type }}) wird von dieser Installation nicht dargestellt. -->
|
|
{% if editing|default(false) %}<p id="{{ block.id }}" class="block-unavailable{{ designClasses }}" role="note">Baustein „{{ type }}“ ist in dieser Installation nicht verfügbar. Der Inhalt bleibt gespeichert.</p>{% endif %}
|
|
{% endif %}
|