Close the collapsible menu after choosing a link
Same-page anchors kept the details menu open. scrolled.js closes it on any link click and loads only where a collapsible menu holds such links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bb9d7c6b45
commit
dd5bc16413
4 files changed
+99
-3
No files matched your search
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
Stand: 12.10.2026. Arbeitstitel KantSites; Produktname offen.
|
Stand: 12.10.2026. Arbeitstitel KantSites; Produktname offen.
|
||||||
|
|
||||||
|
## ADR 119 – Das Menü schließt nach einer Wahl (12.10.2026)
|
||||||
|
|
||||||
|
Anlass: Umut: „Wenn man Links zu Ankern, also #kontakt, im Menü klickt, dann schließt sich das Menü nicht beim Mobilmenü. Also immer schließen, wenn man ausgewählt hat, egal was.“
|
||||||
|
|
||||||
|
**Ursache.** Das aufklappbare Menü ist ein `details` (ADR 117: öffnet ohne Skript). Ein Sprungziel auf derselben Seite lädt nicht neu, also bleibt `details` offen und liegt über dem Ziel. CSS kann ein `details` bei einem Linkklick nicht schließen.
|
||||||
|
|
||||||
|
**Entscheidung.** `scrolled.js`, das kleine Skript des Kopfbereichs, schließt das Menü beim Klick auf einen Link darin (`initMenus`). Es kommt nur dorthin, wo es gebraucht wird: wenn ein aufklappbares Menü (Burger oder waagerecht mit Zusammenklappen) einen Link auf ein Sprungziel **dieser** Seite trägt (`#…` oder der eigene Pfad mit `#…`). Ein Link auf eine andere Seite braucht nichts: Die Seite lädt neu, und weil öffentliche Seiten `no-store` senden, stellt „Zurück“ kein offenes Menü aus dem Seiten-Cache her (im Browser geprüft). So bleibt jede Seite ohne solche Sprünge bei `script-src 'none'`, und auf Seiten, die `scrolled.js` ohnehin laden (Nach-oben-Knopf, Kopf mit Scrollfarben), kommt kein Skript dazu.
|
||||||
|
|
||||||
|
**Geprüft:** Im Browser auf Hüfner (iPhone): „Kontakt“, „Angebot anfragen“ und „Leistungen“ schließen das Menü und springen; „Alpakas“ und „Zurück“ zeigen es geschlossen. Die bestehenden Tests, die `script-src 'none'` auf Seiten mit aufklappbarem Menü ohne Sprungziele verlangen, bleiben unverändert grün.
|
||||||
|
|
||||||
## ADR 118 – Inhaltspakete: Export und Import ohne die Konten (12.10.2026)
|
## ADR 118 – Inhaltspakete: Export und Import ohne die Konten (12.10.2026)
|
||||||
|
|
||||||
Anlass: Umut: „Können wir sowas wie einen Importer einbauen, weil sich nach jeder Anpassung sonst das Passwort ändert? Wo man auswählen muss, ob Passwort/Benutzer überschrieben werden sollen. Gerne auch als Plugin.“ Bisher wurde eine lokal gebaute Website als ganze SQLite-Datei hochgeladen – samt Konten (neues Passwort nach jedem Neuaufbau) und ohne das, was nur live entstanden war (Anfragen, Statistik).
|
Anlass: Umut: „Können wir sowas wie einen Importer einbauen, weil sich nach jeder Anpassung sonst das Passwort ändert? Wo man auswählen muss, ob Passwort/Benutzer überschrieben werden sollen. Gerne auch als Plugin.“ Bisher wurde eine lokal gebaute Website als ganze SQLite-Datei hochgeladen – samt Konten (neues Passwort nach jedem Neuaufbau) und ohne das, was nur live entstanden war (Anfragen, Statistik).
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# Projektstatus
|
# Projektstatus
|
||||||
|
|
||||||
|
## Fassung 1.32.1: Menü schließt nach einer Wahl (12.10.2026)
|
||||||
|
|
||||||
|
**Anlass:** Umut: Ein Link auf ein Sprungziel (#kontakt) im Mobilmenü ließ das Menü offen.
|
||||||
|
|
||||||
|
**Änderung (ADR 119):** `scrolled.js` schließt das aufgeklappte Menü beim Klick auf einen Link darin. Geladen wird es zusätzlich nur, wenn ein aufklappbares Menü Sprungziele derselben Seite enthält; Seiten ohne solche Links bleiben ohne Skript (Links auf andere Seiten laden neu, `no-store` verhindert ein offenes Menü nach „Zurück“).
|
||||||
|
|
||||||
|
**Geprüft:** PHPUnit 665 Tests grün, `npm test` 41 grün; im Browser auf Hüfner (iPhone): „Kontakt“, „Angebot anfragen“, „Leistungen“ schließen das Menü und springen. Bekannt und vorbestehend: `test:responsive-navigation` scheitert am Klick im Editor-Strukturbaum.
|
||||||
|
|
||||||
|
**Live Hüfner (12.10.2026):** Umut hat die Datenbank hochgeladen, ohne Medienordner – Bilder antworteten mit 404 (Kennungen vom letzten Neuaufbau). Lösung: `var/huefner-paket.zip` unter System → „Inhalte übertragen“ importieren; bringt die passenden Medien mit.
|
||||||
|
|
||||||
## Fassung 1.32.0: Inhaltspakete – Export und Import ohne die Konten (12.10.2026)
|
## Fassung 1.32.0: Inhaltspakete – Export und Import ohne die Konten (12.10.2026)
|
||||||
|
|
||||||
**Anlass:** Umut: Nach jedem Hochladen einer neu aufgebauten Datenbank galt live ein anderes Passwort; gewünscht ist ein Importer mit der Wahl, ob Benutzer/Passwort überschrieben werden („gerne auch als Plugin“).
|
**Anlass:** Umut: Nach jedem Hochladen einer neu aufgebauten Datenbank galt live ein anderes Passwort; gewünscht ist ein Importer mit der Wahl, ob Benutzer/Passwort überschrieben werden („gerne auch als Plugin“).
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
|
* Das kleine Skript des Kopfbereichs: markiert die gescrollte Seite und schließt das Menü nach
|
||||||
|
* einer Wahl (initMenus, unten).
|
||||||
|
*
|
||||||
* Marks the page as scrolled.
|
* Marks the page as scrolled.
|
||||||
*
|
*
|
||||||
* A header can carry a second set of colours for the scrolled page - transparent over the hero,
|
* A header can carry a second set of colours for the scrolled page - transparent over the hero,
|
||||||
@@ -31,4 +34,26 @@ export function initScrolled(doc = document) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.querySelector('#editor')) initScrolled();
|
/**
|
||||||
|
* Schließt das aufgeklappte Menü, sobald darin etwas gewählt wird (ADR 119).
|
||||||
|
*
|
||||||
|
* Ein Sprungziel wie #kontakt bleibt auf derselben Seite - das `details` blieb offen und lag über
|
||||||
|
* dem Ziel. Bei einem Link auf eine andere Seite stand es nach „Zurück“ (Seiten-Cache des
|
||||||
|
* Browsers) noch offen da. Ohne Skript bleibt es beim Klappen von Hand und beim Klick daneben.
|
||||||
|
*/
|
||||||
|
export function initMenus(doc = document) {
|
||||||
|
const onClick = (event) => {
|
||||||
|
const link = event.target.closest?.('.menu-burger-body a');
|
||||||
|
const menu = link?.closest('details.menu-burger');
|
||||||
|
if (menu) {
|
||||||
|
menu.open = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
doc.addEventListener('click', onClick);
|
||||||
|
return () => doc.removeEventListener('click', onClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!document.querySelector('#editor')) {
|
||||||
|
initScrolled();
|
||||||
|
initMenus();
|
||||||
|
}
|
||||||
@@ -326,7 +326,14 @@ final class PageController extends AbstractController
|
|||||||
// Only the header changes with the scroll position; a scrolled value anywhere in it asks
|
// Only the header changes with the scroll position; a scrolled value anywhere in it asks
|
||||||
// for the small script that marks the page as scrolled.
|
// for the small script that marks the page as scrolled.
|
||||||
// Den Nach-oben-Knopf blendet dasselbe Skript ein; ohne es bleibt er dauerhaft sichtbar.
|
// Den Nach-oben-Knopf blendet dasselbe Skript ein; ohne es bleibt er dauerhaft sichtbar.
|
||||||
$needsScrolled = (isset($areas['header']) && $this->hasScrolled($areas['header'])) || (bool) ($this->settings->load()['scrollTop'] ?? false);
|
// Dasselbe Skript schließt das aufgeklappte Menü nach einer Wahl (ADR 119) - gebraucht nur,
|
||||||
|
// wenn ein aufklappbares Menü auf ein Sprungziel dieser Seite zeigt: Ein Link auf eine
|
||||||
|
// andere Seite lädt neu (die Seiten senden no-store, also stellt „Zurück“ kein offenes
|
||||||
|
// Menü aus dem Seiten-Cache her). So bleibt eine Seite ohne solche Sprünge ohne Skript.
|
||||||
|
$navigation = $this->navigation($path);
|
||||||
|
$here = array_unique([rtrim($request->getPathInfo(), '/') ?: '/', $this->settings->publicPath($path, $this->settings->homePath())]);
|
||||||
|
$needsScrolled = (isset($areas['header']) && $this->hasScrolled($areas['header'])) || (bool) ($this->settings->load()['scrollTop'] ?? false)
|
||||||
|
|| [] !== array_filter($this->collapsibleMenuSlots($documents), fn (string $slot): bool => $this->jumpsWithin($navigation[$slot] ?? [], $here));
|
||||||
$styles = $this->theme->css($theme).$this->menus->buttonCss($theme['buttons']).$this->scrollTopStyles($this->settings->load(), $theme).$this->consent->styles($theme).$this->styles->compileDocument($page['document']).$this->code->compileDocument($page['document']);
|
$styles = $this->theme->css($theme).$this->menus->buttonCss($theme['buttons']).$this->scrollTopStyles($this->settings->load(), $theme).$this->consent->styles($theme).$this->styles->compileDocument($page['document']).$this->code->compileDocument($page['document']);
|
||||||
foreach ([...array_values($areas), ...array_values($sections)] as $extra) {
|
foreach ([...array_values($areas), ...array_values($sections)] as $extra) {
|
||||||
$styles .= $this->styles->compileDocument($extra).$this->code->compileDocument($extra);
|
$styles .= $this->styles->compileDocument($extra).$this->code->compileDocument($extra);
|
||||||
@@ -382,7 +389,7 @@ final class PageController extends AbstractController
|
|||||||
'canonical' => $canonical,
|
'canonical' => $canonical,
|
||||||
'alternates' => $alternates,
|
'alternates' => $alternates,
|
||||||
'pagination' => null === $blaettern || null === $adresse ? null : ['page' => $blaettern['page'], 'pages' => $blaettern['pages'], 'base' => $adresse],
|
'pagination' => null === $blaettern || null === $adresse ? null : ['page' => $blaettern['page'], 'pages' => $blaettern['pages'], 'base' => $adresse],
|
||||||
'navigation' => $this->navigation($path),
|
'navigation' => $navigation,
|
||||||
'postLists' => $postLists,
|
'postLists' => $postLists,
|
||||||
// Ohne Beiträge gibt es keinen Feed, auf den der Kopf verweisen könnte.
|
// Ohne Beiträge gibt es keinen Feed, auf den der Kopf verweisen könnte.
|
||||||
'hasPosts' => $this->posts->anyPublished(),
|
'hasPosts' => $this->posts->anyPublished(),
|
||||||
@@ -774,6 +781,50 @@ final class PageController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Whether any block carries values for the scrolled page (see StyleSchema::scrollable). */
|
/** Whether any block carries values for the scrolled page (see StyleSchema::scrollable). */
|
||||||
|
/**
|
||||||
|
* Die Menüplätze (header, footer …) der Menüs, die als Knopf aufklappen: Burger oder waagerecht
|
||||||
|
* mit Zusammenklappen.
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function collapsibleMenuSlots(array $documents): array
|
||||||
|
{
|
||||||
|
$slots = [];
|
||||||
|
$walk = function (array $blocks) use (&$walk, &$slots): void {
|
||||||
|
foreach ($blocks as $block) {
|
||||||
|
if ('menu' === ($block['type'] ?? null)) {
|
||||||
|
$layout = $block['props']['layout'] ?? 'horizontal';
|
||||||
|
if ('burger' === $layout || ('horizontal' === $layout && 'never' !== ($block['props']['collapse'] ?? 'tablet'))) {
|
||||||
|
$slots[] = (string) ($block['props']['slot'] ?? 'header');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$walk($block['children'] ?? []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
foreach ($documents as $document) {
|
||||||
|
$walk($document['blocks'] ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($slots));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ob ein Link des Menüs zu einem Sprungziel auf dieser Seite führt (#kontakt, /#kontakt auf der Startseite). @param list<string> $here */
|
||||||
|
private function jumpsWithin(array $links, array $here): bool
|
||||||
|
{
|
||||||
|
foreach ($links as $link) {
|
||||||
|
$href = (string) ($link['href'] ?? '');
|
||||||
|
$hash = strpos($href, '#');
|
||||||
|
if (false !== $hash && (0 === $hash || \in_array(rtrim(substr($href, 0, $hash), '/') ?: '/', $here, true))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ($this->jumpsWithin($link['children'] ?? [], $here)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private function hasScrolled(array $document): bool
|
private function hasScrolled(array $document): bool
|
||||||
{
|
{
|
||||||
$walk = function (array $blocks) use (&$walk): bool {
|
$walk = function (array $blocks) use (&$walk): bool {
|
||||||
|
|||||||
Reference in new issue
Block a user