Files
breadcrumb-the-shire/web/js/core/app-fragment-init.js
fs 811588290c feat(core): detail drawer + address book list redesign
Introduces a reusable core detail-drawer primitive that slides in from the
right and loads any view via a `*-fragment(none).phtml` endpoint. Bundles the
address book list overhaul that is its first consumer.

Core additions:
- `app-detail-drawer.js` — generic drawer with stepper, focus trap, body
  scroll-lock, URL-hash deep-linking, session-expiry detection
- `app-fragment-init.js` — auto-wires tabs/lookups/confirm/file-upload/
  fslightbox inside injected HTML; consumers do not re-initialize components
- `app-focus-trap.js` — shared focus-trap + refcounted scroll-lock, used by
  both filter-drawer and detail-drawer
- `getHtml()` in `app-http.js` + `SessionExpiredError`; drawer reloads the
  page on auth redirect instead of rendering the login form in the panel
- `DetailDrawerFragmentContractTest` enforces that every `initDetailDrawer`
  consumer ships matching `*-fragment($id).php` + `*-fragment(none).phtml`

Address book list:
- Grid collapses from 9 columns to 4 (identity / context / phone / actions)
  with a two-line identity cell (avatar + name + email)
- Tenant register tabs above the grid using the `app-list-tabs` partial;
  tenant filter wired via hidden toolbar field so grid.js forwards it on
  every data fetch
- Profile body extracted to a shared partial so the full-page view and the
  new drawer fragment share the same markup
- New i18n keys for the drawer/list labels

Also refactors `app-filter-drawer` to reuse the shared focus-trap and
scroll-lock instead of maintaining its own copy, and documents the
detail-drawer convention in CLAUDE.md.

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

83 lines
2.3 KiB
JavaScript

/**
* Initializes standard UI components inside a dynamically-loaded HTML fragment
* (e.g. detail drawer body). Mirrors the subset of app-init.js components that
* make sense inside content fragments — layout/topbar/global components are
* intentionally excluded since those live in the shell, not the fragment.
*
* Consumers:
* import { initFragmentContent } from '/js/core/app-fragment-init.js';
* initFragmentContent(contentEl);
*
* Each initializer is best-effort: failures are logged but do not abort the
* remaining initializers so one broken component cannot silently break all
* interactive elements in the fragment.
*/
import { initTabs } from '../components/app-tabs.js';
import { initConfirmActions } from '../components/app-confirm-actions.js';
import { initLookupFields } from '../components/app-lookup-field.js';
import { initAutoSubmit } from '../components/app-auto-submit.js';
import { initFileUpload } from '../components/app-file-upload.js';
const INITIALIZERS = [
{
name: 'tabs',
selector: '[data-app-component="tabs"]',
run: (root) => initTabs(root),
},
{
name: 'confirm-actions',
selector: '[data-confirm]',
run: (root) => initConfirmActions(root),
},
{
name: 'lookup-field',
selector: '[data-lookup-field]',
run: (root) => initLookupFields(root),
},
{
name: 'auto-submit',
selector: '[data-auto-submit]',
run: (root) => initAutoSubmit(root),
},
{
name: 'file-upload',
selector: '[data-app-component="file-upload"]',
run: (root) => initFileUpload(root),
},
];
const refreshFsLightboxSafe = () => {
if (typeof window !== 'undefined' && typeof window.refreshFsLightbox === 'function') {
try {
window.refreshFsLightbox();
} catch (err) {
console.warn('[fragment-init] refreshFsLightbox failed', err);
}
}
};
/**
* Initialize all known components within the given fragment root.
*
* @param {HTMLElement} contentEl — the container holding freshly-injected HTML
*/
export function initFragmentContent(contentEl) {
if (!(contentEl instanceof HTMLElement)) {
return;
}
for (const { name, selector, run } of INITIALIZERS) {
if (!contentEl.querySelector(selector)) {
continue;
}
try {
run(contentEl);
} catch (err) {
console.warn(`[fragment-init] ${name} failed`, err);
}
}
refreshFsLightboxSafe();
}