# 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 `` geladen - setzt früh UI-States (no-js/js, Sidebar-/Contrast-LocalStorage) - `/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: 1. `init...()` Funktion exportieren 2. Früher Guard auf fehlende Elemente 3. Keine stillen Fehler verschlucken 4. Nur DOM/UI-Verhalten, keine Fachlogik 5. Lifecycle-Contract für Runtime-Komponenten: `init(root, config)` gibt API mit `destroy()` zurück 6. 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: `` - Loginseiten: `` - 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. - 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. ## 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 `` 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 ``` ### 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`. - Runtime-Init erfolgt über `web/js/app-init.js` (Komponente `standard-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 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`). ### 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): ```html ``` - **Live Input-Wert** (liest `input.value` beim Klick): ```html ``` 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 ``` Optionale Vars: `$id` (Default = `$name`), `$placeholder`, `$required`, `$disabled`. Die Partial rendert: ```html ``` 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.5rem` für den Overlay-Button - `.app-input-copy > .app-input-copy-button` — absolute positioniert rechts, vertikal zentriert via `inset 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-message` bzw. auf Detail-Aktionen `data-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 standardisiert `frontend.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 `
`-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 ``` 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.