Most German docs were written with `ue`/`ae`/`oe`/`ss` ASCII fallbacks (`fuer`, `aenderung`, `koennen`, `gemaess`) — relic from older toolchain constraints. Replaced 237 occurrences across 38 .md files with proper umlauts and ß so the docs read naturally. Substitutions are constrained to plain prose: fenced code blocks (```...```), inline code spans (`...`), markdown link targets `[..](..)`, and HTML comments are preserved verbatim. Path references like `docs/howto-erste-aenderung.md` keep their original (umlaut-replaced) filenames — only the surrounding prose is normalised. Tool: bin/fix-md-umlauts.py (idempotent — no-op on already-clean files). Re-runnable if ASCII fallbacks creep back in via copy-paste. Doc-link-check + doc-drift-check stay green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16 KiB
Frontend JavaScript
Letzte Aktualisierung: 2026-03-18
Ordnerstruktur
/web/js/core- technische Basismodule (DOM/Factories)
/web/js/components- wiederverwendbare UI-Features
/web/js/pages- seitenspezifische Hilfen (z. B. Listeninitialisierung)
Initialisierung
/web/js/app-boot.js- wird im
<head>geladen - setzt früh UI-States (no-js/js, Sidebar-/Contrast-LocalStorage)
- wird im
/web/js/app-init.js- Modul-Entrypoint für Standardseiten
- initialisiert Runtime + globale Komponenten zentral
/web/js/app-login-init.js- reduzierter Entrypoint für Loginseiten
Modulkonvention (components/pages)
Empfohlenes Muster:
init...()Funktion exportieren- Früher Guard auf fehlende Elemente
- Keine stillen Fehler verschlucken
- Nur DOM/UI-Verhalten, keine Fachlogik
- Lifecycle-Contract für Runtime-Komponenten:
init(root, config)gibt API mitdestroy()zurück - Runtime-Komponenten dürfen keinen eigenen Auto-Init via
DOMContentLoaded/Sideeffect enthalten
Component Runtime (Restmigration)
- Zentrale Runtime:
web/js/core/app-component-runtime.js. - Runtime-Page-Configs:
- Standardseiten:
<script type="application/json" id="page-config-app-components">...</script> - Loginseiten:
<script type="application/json" id="page-config-login-components">...</script>
- Standardseiten:
- Runtime liest Config über
readPageConfig(...)und mountet registrierte Komponenten pro Entrypoint:web/js/app-init.js(default)web/js/app-login-init.js(login)
- Komponenteninterner Contract:
- Init-Signatur:
initX(root, config) - Rueckgabe:
{ destroy() } destroy()muss alle Listener/Timer/Observer abbauen.
- Init-Signatur:
- Hybrid Host-Policy:
- Host-gebundene UI-Komponenten nutzen explizite
data-app-component-Hosts im Markup. - Tabs sind host-gebunden und laufen ausschliesslich über
data-app-component="tabs". - Globale Utilities ohne natürliches Host-Element werden runtime-global (
scope: 'global') registriert, aber mit demselben Lifecycle-Contract. - Root-Strict gilt für host-gebundene Komponenten: keine
document.querySelector-Fallback-Pfade. - Projektseitige Custom-Globals auf
window.*sind verboten; Komponenten kommunizieren über Core-Channels.
- Host-gebundene UI-Komponenten nutzen explizite
UI Storage Contract
- Persistente UI-States laufen für Runtime-Komponenten über
web/js/core/app-ui-storage.js. - Storage-Keys sind namespaced (
namespace:version:scope:*) statt ungeordnet verteilt. - Hard-Cut-Policy: keine Legacy-Key-Fallbacks und kein Legacy-Key-Sync in migrierten Runtime-Komponenten/Core-Modulen.
Template für neue Komponenten:
- bestehende Komponenten als Referenz verwenden (
/web/js/components/*.js)
Page Module Contract (Hard Cut)
Für seitenbezogene Initialisierung gilt verbindlich:
- Kein inline
<script type="module">inpages/**odertemplates/**. - Pro Seite genau ein externes Modul unter
web/js/pages/*laden. - Serverseitige Daten für dieses Modul über
<script type="application/json" id="page-config-...">...</script>bereitstellen. - Page-Module lesen Config zentral über
readPageConfig(...)ausweb/js/core/app-page-config.js. - JS-
srcinpages/**undtemplates/**nur überassetVersion('...js...').
Defensive DOM-Nutzung
Nicht jede Seite hat jedes Element.
- Vor Zugriff immer Element-Existenz prüfen.
- Optional zentrale Helper (
app-dom.js) verwenden. - Module sollen auf nicht-passenden Seiten sauber no-op sein.
Grid.js-Integration
- Gemeinsame Initialisierung über Core/Page-Utilities.
- Vendor-spezifische CSS-Anpassungen in:
/web/css/vendor-overrides/gridjs.css
- Bei dynamischen Rows mit Lightbox/Tooltips: nach Render Refresh triggern.
Grid URL State Sync
createServerGrid(...) synchronisiert bei urlSync: true den Grid-Zustand zentral in die URL.
- Search + Filter (bestehendes Verhalten)
- Sortierung über
order+dir - Pagination über
page(1-basiert)
Optionale Konfiguration:
createServerGrid({
// ...
urlSync: true,
urlState: {
pageParam: 'page',
sortOrderParam: 'order',
sortDirParam: 'dir',
cleanFirstPage: true,
syncPage: true,
syncSort: true
}
});
Hinweise:
page=1wird beicleanFirstPage: truenicht in der URL gehalten.- Übernommene URL-Werte werden validiert:
page >= 1dirnurasc|descordernur bekannte Sort-Key-Spalten
- Bei Search-/Filter-Änderung wird auf Seite 1 zurückgesetzt.
Row Interaction (Globaler Standard)
Listen mit rowDblClick.getUrl(...) nutzen standardmäßig eine zugängliche Row-Interaktion:
- sichtbare Action-Spalte rechts (
Open/Edit) - Doppelklick bleibt als Fallback aktiv
Enterauf fokussierter Zeile navigiert zur selben URL- Action-Header/Tooltip zeigt den Enter-Hinweis
Optionale Konfiguration über createServerGrid(...):
createServerGrid({
// ...
rowDblClick: {
getUrl: (rowData) => new URL(`admin/users/edit/${rowData?.cells?.[1]?.data}`, appBase).toString(),
},
rowInteraction: {
enabled: true,
showActionButton: true,
keepDblClick: true,
enableEnterOnRow: true,
actionColumnLabel: 'Actions',
resolveActionLabel: (url) => (url.includes('/edit/') ? 'Edit' : 'Open'),
}
});
Defaults:
rowInteraction.enabled:true, wennrowDblClick.getUrlvorhanden istshowActionButton:truekeepDblClick:trueenableEnterOnRow:trueactionColumnLabel:language.actions.columnoder'Actions'
Hinweis:
- Falls eine Seite
actions.enabled: trueexplizit setzt, wird keine zusätzliche automatische Row-Action-Spalte injiziert.
Grid Error Recovery
Bei Grid-Fehlern rendert die Factory automatisch einen Retry-CTA im Error-State.
- Label kommt aus
language.errorRetry(Fallback:Retry). - Klick auf Retry lädt den Grid-Server-Request erneut.
Filter UX 2.0 (Globaler Standard)
Für Listen gilt standardmäßig:
- Quick Search sichtbar außerhalb des Drawers.
- Restliche Filter im rechten Drawer (
data-filter-drawer-*). - Drawer arbeitet
apply-first(Draft bisApply). - Aktive Filter werden als Chips angezeigt (
data-active-filter-chips). - Filter-Reset erfolgt über Chips (
Clear all filters), nicht im Drawer.
JS-Verkabelung:
- Standard-Entrypoint ist
initStandardListPage(...)ausweb/js/core/app-grid-factory.js. - Der Wrapper kapselt intern
createServerGrid(...)+ (beimode: 'drawer')initListFilterExperience(...). - Bei Apply/Clear wird URL mit
pushStatefortgeschrieben. - Save-Filter-Usecases lesen den angewandten Zustand über
gridConfig.baseUrl(). - Chip-State-Logik ist zentral im Wrapper verdrahtet (Default:
app-list-filter-state.js).
Search-only-Ausnahme:
- Listen ohne zusätzliche Filter initialisieren über denselben Wrapper mit
mode: 'search-only'(ohne Drawer/Chips).
Standard-List-API
Für Listen-Templates gilt:
initStandardListPage({ grid, filters, hooks })verwenden.grid.filterSchemaserverseitig ausfilter-schema.php(gridSchemaClientFilters(...)) übergeben.- Optional zusätzliche Spezialfilter über
grid.extraFilters. - Bei Drawer-Listen
filters.mode: 'drawer', bei Search-onlyfilters.mode: 'search-only'.
Regel:
- keine direkten Aufrufe von
gridFiltersFromSchema(...)in Listen-Templates - keine direkten Aufrufe von
initListFilterExperience(...)in Listen-Templates - keine inline
filters: [...]-Arrays in Listen-Templates - MultiSelect-Filter immer als CSV-Key (
*_ids,actions,event_typesetc.)
Serverseitig passendes Template-Markup:
- normale Select-Filter über
selectFilter(...) - MultiSelect-Filter über
multiSelectFilter(...)
List Titlebar Standard
Für Listen-Titlebars gilt:
- Titlebar über
templates/partials/app-list-titlebar.phtmlrendern. - Titel über
$listTitlesetzen. - Aktionen über
$listTitleActionsHtmlals Slot (captured HTML) übergeben.
Beispiel:
<?php
$listTitle = t('Users');
ob_start();
?>
<a role="button" class="primary" href="admin/users/create"><?php e(t('Create user')); ?></a>
<?php
$listTitleActionsHtml = ob_get_clean();
require templatePath('partials/app-list-titlebar.phtml');
?>
List Tabs Standard
Für Tenant- oder Kontext-Tabs in Listen gilt:
- Tabs über
templates/partials/app-list-tabs.phtmlrendern. - Tab-Items über
$listTabsItemsmithref,label,activeübergeben. - Optional
idüber$listTabsIdsetzen.
List Purge Action Standard
Für Purge-Buttons in Titlebar-Actions gilt:
- Purge-Markup über
templates/partials/app-list-purge-action.phtmlrendern. - Formular/CSRF/Button/Confirm werden zentral übergeben via:
$listPurgeEnabled$listPurgeFormId$listPurgeAction$listPurgeConfirmMessage$listPurgeButtonLabel
Aside Grouped Navigation + Persisted Details State
Für gruppierte Sidebar-Navigation mit einklappbaren details gilt:
- Zentraler Storage-Helper:
web/js/core/app-details-open-state.js(initPersistedDetailsGroup(...)). - Standardgruppen über
details[data-details-key]. - Persistenz-Aktivierung über:
data-details-storage="..."(allgemein, z. B. Detailseiten)data-aside-details-storage="..."(Aside-Panels)
- Optionales Aktiv-Verhalten im Aside über
data-aside-details-open-active="1":- aktive Gruppe bleibt immer offen
- effektive Open-Keys =
persistedOpenKeys ∪ activeOpenKeys
Admin-Aside-Standard:
- Storage-Key:
aside-admin-sections-v1 - Gruppenkeys:
admin-organizationadmin-roles-permissionsadmin-automationadmin-monitoringadmin-logsadmin-system
Detail Page Standard
Für Create/Edit-Detailseiten gilt:
- Hauptformular über
data-standard-detail-form="1"explizit opt-in markieren. - Initialisierung läuft zentral über
initStandardDetailPage(...)ausweb/js/core/app-detail-page-factory.js. - Runtime-Init erfolgt über
web/js/app-init.js(Komponentestandard-detail-page).
Standardverhalten:
- Dirty-State über serialisierte
FormData-Diffs (initial vs. aktuell). beforeunload-Warnung nur bei ungespeicherten Änderungen.- Back/Cancel-Link in
.app-details-titlebar h1 afragt bei Dirty-State per Confirm. Cmd/Ctrl+Striggert primären Save/Create-Button (data-detail-save-primary="1").- Submit-Lock/Confirm laufen zentral über die Detail-Action-Policy.
- Bei Dirty-State wird in der Titlebar automatisch ein kleiner Hinweis mit Feldanzahl gezeigt.
Detail Action Policy
Für standardisierte Detailseiten gilt zusätzlich:
initStandardDetailPage(...)bindet interninitDetailActionPolicy(...)ausweb/js/core/app-details-action-policy.js.- Aktivierung pro Seite über
data-detail-action-policy="1"auf.app-details-titlebar(Default im Shared-Partial aktiv). - Confirm nicht mehr inline per
onclick/onsubmit, sondern überdata-detail-confirm-message. - Aktionssemantik über
data-detail-action-kind(z. B.save,save-close,delete,danger). - Optionaler Lock-Override über
data-detail-submit-lock="none"(Default:form).
Copy-to-Clipboard Standard
Für kopierbare Inhalte (Werte, Feld-Inhalte, Badges) gilt eine einzige
Component: web/js/components/app-copy-field.js — ein Button mit
data-copy-button bekommt automatisch einen Click-Handler, der in die
Zwischenablage schreibt und 1,2 s lang ein Check-Icon statt des Copy-Icons
anzeigt (Klasse is-copied wird am Button getoggelt).
Zwei Quell-Modi:
-
Statischer Wert (fix im Markup):
<button type="button" data-copy-button data-copy-value="max@acme.com" aria-label="Copy email"> <i class="bi bi-copy" data-copy-icon-idle></i> <i class="bi bi-check-lg" data-copy-icon-done></i> </button> -
Live Input-Wert (liest
input.valuebeim Klick):<input id="privacy_url" …> <button type="button" data-copy-button data-copy-target="privacy_url" …>…</button>Der Live-Modus ist vorzuziehen, wenn der User das Feld editieren kann — der Button kopiert dann immer den aktuellen Wert, nicht den beim ersten Render gesetzten.
Copyable Input Standard
Für Text-Inputs, bei denen ein Copy-Button im Feld sitzen soll (z. B. URLs,
IDs, Tokens) existiert die Core-Partial templates/partials/app-input-copy.phtml.
Verwendung im Form:
<?php
$name = 'privacy_url';
$label = t('Privacy URL');
$type = 'url';
$value = (string) ($values['privacy_url'] ?? '');
$readonly = $isReadOnly;
require templatePath('partials/app-input-copy.phtml');
?>
Optionale Vars: $id (Default = $name), $placeholder, $required,
$disabled. Die Partial rendert:
<label for="privacy_url">
<span>Privacy URL</span>
<div class="app-input-copy">
<input type="url" name="privacy_url" id="privacy_url" value="…">
<button class="app-input-copy-button" data-copy-button
data-copy-target="privacy_url" …>
<i class="bi bi-clipboard" data-copy-icon-idle></i>
<i class="bi bi-check-lg" data-copy-icon-done></i>
</button>
</div>
</label>
CSS-Kontrakt (web/css/components/app-input-copy.css):
.app-input-copy— Positioning-Wrapper,position: relative.app-input-copy > input— Native Form-Styling,padding-right: 2.5remfür den Overlay-Button.app-input-copy > .app-input-copy-button— absolute positioniert rechts, vertikal zentriert viainset 0/auto + margin-block: auto. Spezifität(0,2,0)beim Descendant-Selector nötig, damit die globale[data-tooltip] { position: relative }-Regel nicht gewinnt..is-copied(wird vom Copy-Field-Component gesetzt) — tauscht Copy-Icon gegen Check, färbt Button in--app-action-success-border.
Nicht selbst die Markup-Struktur nachbauen — immer die Partial verwenden,
damit Spacing, Vertical-Centering und A11y (aria-label, Tooltip) konsistent
bleiben.
Global Confirm Actions
Für allgemeine Form-/Button-Confirms außerhalb der Detail-Policy gilt:
- zentral über
web/js/components/app-confirm-actions.js - Dialog-Engine zentral über
web/js/core/app-confirm-dialog.js(confirmDialog.confirm(...)) - Confirm-Text über
data-confirm-messagebzw. auf Detail-Aktionendata-detail-confirm-message - optionale Dialog-Meta über
data-confirm-title,data-confirm-confirm-label,data-confirm-cancel-label,data-confirm-variant,data-confirm-focus- analog für Detail-Aktionen:
data-detail-confirm-*
- kein direktes
window.confirm(...)in App-JS - keine inline
onclick="return confirm(...)"/onsubmit="return confirm(...)"verwenden
Frontend Telemetry Standard
Für UX-/Fehler-Telemetrie im Frontend gilt:
- zentral über
web/js/core/app-telemetry.js(telemetry.capture(...)) warnOnce(...)sendet standardisiertfrontend.warn_once- AJAX-/Fetch-Fehler laufen zentral über den globalen Fetch-Hook als
frontend.ajax_error - keine neuen Console-only-Warnpfade für relevante UX-Probleme ohne Telemetrie
- Payloads nur mit Whitelist-Feldern und ohne sensitive Rohdaten (keine Tokens/Query-Parameter/Stacktraces)
Detail Validation Summary Standard
Für serverseitige Formularfehler in Create/Edit-Templates gilt:
- Fehlerausgabe über
templates/partials/app-details-validation-summary.phtml. - Keine inline
<div class="notice" data-variant="error">-Blöcke mitforeach ($errors as $error)mehr. - Standardtitel kommt aus
t('Please review the following errors'). - In Action-Handlern strukturierte Feld-Errors als
$validationSummaryErrors = $errorBag->toArray();bereitstellen.
Template-Beispiel:
<?php
$validationSummaryErrors = $validationSummaryErrors ?? ($errors ?? []);
require templatePath('partials/app-details-validation-summary.phtml');
?>
Optional strukturierte Einträge:
['message' => '...', 'fieldName' => 'email']['message' => '...', 'fieldId' => 'email']['email' => ['Required']](FormErrors-ähnliche Map)
JS-Verhalten über initStandardDetailPage(...):
- Klick auf Validation-Link fokussiert das passende Feld.
- Bei vorhandener Summary wird initial das erste betroffene Feld (oder Summary) fokussiert.
- Betroffene Felder werden zentral mit
aria-invalid="true"undaria-describedby(Summary-ID) markiert.
Laufzeit-Check (ohne Node)
Prüfen im Browser:
- DevTools öffnen (Console, optional Preserve log aktivieren).
- Kernseiten laden (
/admin/users,/address-book, Audit-Listen). - Filter setzen/zurücksetzen, Row-Action-Button + Enter + Doppelklick sowie Bulk-Aktionen testen.
- Erwartung: keine JavaScript-Errors und keine Unhandled Promise Rejections.