Files
breadcrumb-the-shire/docs/reference-frontend-javascript.md
fs fa86009e3f feat(ui): copyable input partial + dynamic copy-target
- New core partial templates/partials/app-input-copy.phtml renders a
  standard label + input with a compact copy button overlaid on the
  right edge of the input. Used for privacy/imprint URLs on the tenant
  form; reusable for any field where a one-click copy is helpful.
- Extend the shared copy-field component (web/js/components/app-copy-field.js)
  with data-copy-target support — the button now reads the target input's
  .value at click-time, so users can edit a field and still copy the
  current value. Static data-copy-value keeps working unchanged.
- New component CSS web/css/components/app-input-copy.css positions the
  button absolute/inset + margin-block:auto (robust vertical centering
  regardless of input height) and uses a descendant selector (0,2,0)
  so the button wins over the global [data-tooltip] position:relative.
- Also register app-input-copy.css + app-tenant-logo.css in core.css
  @import list — they were only in the shared asset group before, which
  default-template admin pages don't load, so tenant-logo styles were
  effectively missing on admin tenant edit (topbar size etc.).
- Document the Copy-to-Clipboard + Copyable Input standards in
  docs/reference-frontend-javascript.md so future consumers don't
  re-invent the markup/selectors.
- File-upload preview: pending-file block restructured to a compact
  list-item row (small square thumb + filename/size stack + clear X)
  and removed the transparency checker pattern on the current-image
  preview in favour of a flat --app-preview-bg surface.
- Tenant edit page title + breadcrumb now show the tenant description
  ("Acme GmbH") instead of the generic "Mandant bearbeiten" when one
  is present.
- i18n: add "Copy to clipboard" / "In Zwischenablage kopieren" across
  both locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:26:11 +02:00

449 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
- `/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: `<script type="application/json" id="page-config-app-components">...</script>`
- Loginseiten: `<script type="application/json" id="page-config-login-components">...</script>`
- 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 ueber `data-app-component="tabs"`.
- Globale Utilities ohne natuerliches Host-Element werden runtime-global (`scope: 'global'`) registriert, aber mit demselben Lifecycle-Contract.
- Root-Strict gilt fuer host-gebundene Komponenten: keine `document.querySelector`-Fallback-Pfade.
- Projektseitige Custom-Globals auf `window.*` sind verboten; Komponenten kommunizieren ueber Core-Channels.
## UI Storage Contract
- Persistente UI-States laufen fuer Runtime-Komponenten ueber `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">` 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`.
- 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
<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.value` beim Klick):
```html
<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
<?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:
```html
<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.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 `<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.