Files
familienarchiv/frontend/src/lib/shared/services/confirm.svelte.ts
2026-05-05 14:35:15 +02:00

101 lines
3.0 KiB
TypeScript

/**
* Context-based confirmation service. Provides an async `confirm()` function
* that any component can call without managing its own modal state.
*
* ## Setup
* Mount `<ConfirmDialog>` once in the root `+layout.svelte` — it sets up the context
* automatically. Then call `getConfirmService()` from any descendant component.
*
* ## Usage in event handlers
* ```typescript
* import { getConfirmService } from '$lib/shared/services/confirm.svelte.js';
* const { confirm } = getConfirmService();
*
* async function handleDelete() {
* const ok = await confirm({ title: m.confirm_delete_title(), destructive: true });
* if (ok) doDelete();
* }
* ```
*
* ## Usage with use:enhance
* ```svelte
* <form use:enhance={async ({ cancel }) => {
* const ok = await confirm({ title: m.confirm_delete_title(), destructive: true });
* if (!ok) cancel();
* }}>
* ```
*/
import { getContext, setContext } from 'svelte';
import { browser } from '$app/environment';
export const CONFIRM_KEY = Symbol('confirm');
export interface ConfirmOptions {
title: string;
body?: string;
/** Defaults to m.btn_confirm() ("Bestätigen") */
confirmLabel?: string;
/** Defaults to m.btn_cancel() ("Abbrechen") */
cancelLabel?: string;
/** Uses danger color for confirm button. Defaults to false. */
destructive?: boolean;
/** Close when clicking outside the dialog. Defaults to !destructive. */
closeOnBackdrop?: boolean;
}
export interface ConfirmService {
confirm(opts: ConfirmOptions): Promise<boolean>;
/** Read by ConfirmDialog to render the current dialog. Internal use only. */
readonly options: ConfirmOptions | null;
/** Called by ConfirmDialog when the user makes a choice. Internal use only. */
settle(value: boolean): void;
}
export function createConfirmService(): ConfirmService {
let resolveRef: ((value: boolean) => void) | null = null;
let options: ConfirmOptions | null = $state(null);
return {
confirm(opts: ConfirmOptions): Promise<boolean> {
if (!browser) return Promise.resolve(false);
// Concurrent call while dialog is already open — reject immediately.
if (resolveRef !== null) return Promise.resolve(false);
options = opts;
return new Promise((r) => {
resolveRef = r;
});
},
get options() {
return options;
},
settle(value: boolean): void {
options = null;
const r = resolveRef;
resolveRef = null;
r?.(value);
}
};
}
export function provideConfirmService(): ConfirmService {
const service = createConfirmService();
setContext(CONFIRM_KEY, service);
return service;
}
export function getConfirmService(): ConfirmService {
// Outside component init, getContext either returns undefined or throws a Svelte error.
// Either way, map it to our descriptive developer error.
let service: ConfirmService | undefined;
try {
service = getContext<ConfirmService>(CONFIRM_KEY);
} catch {
throw new Error('ConfirmService not found — mount <ConfirmDialog> in +layout.svelte');
}
if (!service)
throw new Error('ConfirmService not found — mount <ConfirmDialog> in +layout.svelte');
return service;
}