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

16 KiB
Raw Blame History

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:

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(...):

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
$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):

    <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):

    <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.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
$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.