Auf Entscheidung von Umut werden Tabs jetzt mit JavaScript umgesetzt. Die Regel "der Seiteninhalt braucht kein Browser-JavaScript" bleibt dabei eingehalten: der Server liefert eine Ankerliste und alle Panels mit eigenen Überschriften, versteckt nichts, und ohne Skript ist jeder Inhalt lesbar und über die Tastatur erreichbar. public/assets/tabs.js ergänzt tablist/tab/tabpanel, Auswahl per Pfeiltasten, Home und Ende sowie aria-selected und wählt bei einem passenden Sprungziel das zugehörige Panel. Die strenge Richtlinie bleibt Voreinstellung: öffentliche Seiten senden weiterhin script-src 'none'. Nur eine Seite mit Tab-Block sendet script-src 'self' und bindet die eine externe Datei ein; Inline-Skript ist nie erlaubt. Geprüft: 109 PHP-Tests / 395 Assertions unter PHP 8.2.12 und 8.4.22, 14 JavaScript-Modelltests, Lint/Format/Container. Das Skript selbst wurde nicht ausgeführt: Tastaturbedienung und ARIA-Verhalten sind ohne Browserlauf ungeprüft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
104 lines
7.6 KiB
Twig
104 lines
7.6 KiB
Twig
{% set type = block.type %}
|
|
{% set props = block.props|default({}) %}
|
|
{% set layout = block.layout|default({}) %}
|
|
{% set designClasses = '' %}
|
|
{% 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 }}">
|
|
{% 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({})} only %}{% endfor %}
|
|
{% if type == 'section' %}</div>{% endif %}
|
|
</{{ tag }}>
|
|
{% elseif type == 'heading' %}
|
|
<h{{ props.level }} id="{{ block.id }}" class="{{ designClasses|trim }}">{{ props.text }}</h{{ props.level }}>
|
|
{% elseif type == 'text' %}
|
|
<p id="{{ block.id }}" class="block-text{{ designClasses }}">{{ props.text }}</p>
|
|
{% elseif type == 'button' %}
|
|
<a id="{{ block.id }}" class="button{{ designClasses }}" href="{{ props.href }}">{{ props.text }} <span aria-hidden="true">↗</span></a>
|
|
{% elseif type == 'image' %}
|
|
<figure id="{{ block.id }}" class="block-image{{ designClasses }}"><img src="{{ props.src }}" alt="{{ props.alt }}" width="{{ props.width }}" height="{{ props.height }}" loading="lazy" decoding="async"{% if props.sources|default([]) %} srcset="{% for source in props.sources %}{{ source.src }} {{ source.width }}w{% if not loop.last %}, {% endif %}{% endfor %}" sizes="(max-width: 650px) 100vw, (max-width: 1024px) 50vw, 800px"{% 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 }}">{{ 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>{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({})} 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 }}">{{ props.buttonText }}</a>
|
|
</div>
|
|
{% elseif type == 'contact' %}
|
|
<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>
|
|
{% 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 == '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><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({})} 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({})} 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({})} 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({})} only %}{% endfor %}
|
|
</div>
|
|
</details>
|
|
{% elseif type == 'postlist' %}
|
|
<div id="{{ block.id }}" class="block-postlist{{ designClasses }}">
|
|
{% include 'blocks/post-list.html.twig' with {posts: postLists[block.id]|default([])} only %}
|
|
</div>
|
|
{% elseif type == 'gallery' %}
|
|
<div id="{{ block.id }}" class="block-gallery{{ designClasses }}" role="group" aria-label="Bildergalerie">
|
|
{% for child in block.children|default([]) %}{% include 'blocks/block.html.twig' with {block: child, editing: editing|default(false), postLists: postLists|default({})} only %}{% endfor %}
|
|
</div>
|
|
{% 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" role="note">Baustein „{{ type }}“ ist in dieser Installation nicht verfügbar. Der Inhalt bleibt gespeichert.</p>{% endif %}
|
|
{% endif %}
|