# Frontend JavaScript

Letzte Aktualisierung: 2026-03-04

## 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)
- `/web/js/app-init.js`
  - Modul-Entrypoint für Standardseiten
  - importiert Komponenten zentral
- `/web/js/app-login-init.js`
  - reduzierter Entrypoint für Loginseiten

## Modulkonvention (components/pages)

Empfohlenes Muster:

1. `init...()` Funktion exportieren
2. Früher Guard auf fehlende Elemente
3. Keine stillen Fehler verschlucken
4. Nur DOM/UI-Verhalten, keine Fachlogik

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">` in `pages/**` oder `templates/**`.
- 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(...)` aus `web/js/core/app-page-config.js`.
- JS-`src` in `pages/**` und `templates/**` nur über `assetVersion('...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:

```js
createServerGrid({
  // ...
  urlSync: true,
  urlState: {
    pageParam: 'page',
    sortOrderParam: 'order',
    sortDirParam: 'dir',
    cleanFirstPage: true,
    syncPage: true,
    syncSort: true
  }
});
```

Hinweise:

- `page=1` wird bei `cleanFirstPage: true` nicht in der URL gehalten.
- Übernommene URL-Werte werden validiert:
  - `page >= 1`
  - `dir` nur `asc|desc`
  - `order` nur 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
- `Enter` auf fokussierter Zeile navigiert zur selben URL
- Action-Header/Tooltip zeigt den Enter-Hinweis

Optionale Konfiguration über `createServerGrid(...)`:

```js
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`, wenn `rowDblClick.getUrl` vorhanden ist
- `showActionButton`: `true`
- `keepDblClick`: `true`
- `enableEnterOnRow`: `true`
- `actionColumnLabel`: `language.actions.column` oder `'Actions'`

Hinweis:

- Falls eine Seite `actions.enabled: true` explizit 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:

1. Quick Search sichtbar außerhalb des Drawers.
2. Restliche Filter im rechten Drawer (`data-filter-drawer-*`).
3. Drawer arbeitet `apply-first` (Draft bis `Apply`).
4. Aktive Filter werden als Chips angezeigt (`data-active-filter-chips`).
5. Filter-Reset erfolgt über Chips (`Clear all filters`), nicht im Drawer.

JS-Verkabelung:

- Standard-Entrypoint ist `initStandardListPage(...)` aus `web/js/core/app-grid-factory.js`.
- Der Wrapper kapselt intern `createServerGrid(...)` + (bei `mode: 'drawer'`) `initListFilterExperience(...)`.
- Bei Apply/Clear wird URL mit `pushState` fortgeschrieben.
- 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.filterSchema` serverseitig aus `filter-schema.php` (`gridSchemaClientFilters(...)`) übergeben.
- Optional zusätzliche Spezialfilter über `grid.extraFilters`.
- Bei Drawer-Listen `filters.mode: 'drawer'`, bei Search-only `filters.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_types` etc.)

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.phtml` rendern.
- Titel über `$listTitle` setzen.
- Aktionen über `$listTitleActionsHtml` als Slot (captured HTML) übergeben.

Beispiel:

```php
<?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.phtml` rendern.
- Tab-Items über `$listTabsItems` mit `href`, `label`, `active` übergeben.
- Optional `id` über `$listTabsId` setzen.

### List Purge Action Standard

Für Purge-Buttons in Titlebar-Actions gilt:

- Purge-Markup über `templates/partials/app-list-purge-action.phtml` rendern.
- 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-organization`
  - `admin-roles-permissions`
  - `admin-automation`
  - `admin-monitoring`
  - `admin-logs`
  - `admin-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(...)` aus `web/js/core/app-detail-page-factory.js`.
- Auto-Init erfolgt über `web/js/components/app-standard-detail-page.js` (in `app-init.js` eingebunden).

Standardverhalten:

- Dirty-State über serialisierte `FormData`-Diffs (initial vs. aktuell).
- `beforeunload`-Warnung nur bei ungespeicherten Änderungen.
- Back/Cancel-Link in `.app-details-titlebar h1 a` fragt bei Dirty-State per Confirm.
- `Cmd/Ctrl+S` triggert 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 intern `initDetailActionPolicy(...)` aus `web/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 über `data-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`).

### Global Confirm Actions

Für allgemeine Form-/Button-Confirms außerhalb der Detail-Policy gilt:

- zentral über `web/js/components/app-confirm-actions.js`
- Confirm-Text über `data-confirm-message`
- keine inline `onclick="return confirm(...)"` / `onsubmit="return confirm(...)"` verwenden

### 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 mit `foreach ($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
<?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"` und `aria-describedby` (Summary-ID) markiert.

## Laufzeit-Check (ohne Node)

Prüfen im Browser:

1. DevTools öffnen (Console, optional Preserve log aktivieren).
2. Kernseiten laden (`/admin/users`, `/address-book`, Audit-Listen).
3. Filter setzen/zurücksetzen, Row-Action-Button + Enter + Doppelklick sowie Bulk-Aktionen testen.
4. Erwartung: keine JavaScript-Errors und keine Unhandled Promise Rejections.
