import { BrowserEventDispatcher } from '@wlindabla/event_dispatcher/browser'; import { EventSubscriberInterface } from '@wlindabla/event_dispatcher'; import { RouteResolverInterface, HistoryManagerInterface, SpaRouterInterface, BindingManagerInterface, SpaKernelExtensionInterface } from './contracts.cjs'; export { RequestMatcherInterface, SpaExtensionContextInterface, SpaSubscriberInterface, SwapStrategyInterface } from './contracts.cjs'; import { CRUDPageType, RouteMatch, SpaRouterOptions, APP_ENV, SpaRequest } from './types.cjs'; export { BatchConfirmData, CRUDSuffixURL, FetchConfirmDeleteOptions, SpaRedirectType, SpaResponse, SwapContext } from './types.cjs'; export { SpaExtensionContext } from './extension.cjs'; export { SonataSpaLogger } from './logger.cjs'; export { SonataSubmitButton, SonataSubmitButtonName, SpaBatchConfirmRequestedEvent, SpaBatchFailedEvent, SpaBatchProcessingEvent, SpaBatchSucceededEvent, SpaCrudEvent, SpaDeleteConfirmRequestedEvent, SpaDeleteFailedEvent, SpaDeleteProcessingEvent, SpaDeleteSucceededEvent, SpaDomReadyEvent, SpaEvents, SpaFetchErrorEvent, SpaFormSubmitEvent, SpaNavigateCompletedEvent, SpaRequestEvent, SpaResponseEvent, SpaRouteResolvedEvent, SpaServerRedirectEvent, SpaSwapAfterEvent, SpaSwapEvent } from './events.cjs'; export { A as AbstractCRUDPageSubscriber, D as DefaultBatchSubscriber, a as DefaultDeletionOperationSubscriber } from './AbstractCRUDPageSubscriber-oJkKwpCi.cjs'; export { D as DomSwapManager } from './DomSwapManager-CBjWYRG4.cjs'; export { SpaRedirectResponse } from './http.cjs'; import '@wlindabla/http_client'; import '@wlindabla/form_validator/utils'; import '@wlindabla/http_client/contracts'; /** * @wlindabla/sonata_spa — RouteResolver * Parses SonataAdmin URLs and resolves them to a RouteMatch. * Frontend equivalent of Symfony's Router component. * @author AGBOKOUDJO Franck */ /** * Resolves SonataAdmin URLs to a RouteMatch. * * SonataAdmin URL patterns: * /admin/{prefix}/{resource}/list * /admin/{prefix}/{resource}/{token}/show * /admin/{prefix}/{resource}/{token}/edit * /admin/{prefix}/{resource}/create * /admin/{prefix}/{resource}/{token}/delete * /admin/dashboard * * The resolver extracts: * - pageType → detected from the URL suffix * - resource → the Sonata resource name (e.g. "user", "product") * - token → the unique object token for show/edit/delete * - url → the full resolved URL * @author AGBOKOUDJO Franck */ declare class RouteResolver implements RouteResolverInterface { private static _instance; /** * Map of URL suffix patterns to CRUDPageType. * Order matters — more specific patterns first. */ private static readonly SUFFIX_MAP; /** * Instance-level suffix map — extensible via addPattern(). * Initialized from the static SUFFIX_MAP defaults. * Extensions can prepend custom patterns via addPattern(). */ private readonly suffixMap; private constructor(); /** * Returns the unique RouteResolver instance. * Creates it on first call — returns the existing one on subsequent calls. * Ensures a single source of truth for URL resolution across the SPA lifetime. * * @internal — called by SpaKernel only. */ static create(): RouteResolver; /** * Reset the singleton instance. * @internal — for testing purposes only. */ static reset(): void; /** * Add a custom URL pattern to the RouteResolver. * The pattern is prepended so it takes precedence over built-in patterns. * Called by SpaExtensionContext.addRoutePattern(). * * @param pattern - The RegExp to match against the URL pathname * @param pageType - The CRUDPageType to assign when the pattern matches * * @example * ```typescript * resolver.addPattern(/\/approval(\/)?(\?.*)?$/, 'approval' as CRUDPageType); * ``` */ addPattern(pattern: RegExp, pageType: CRUDPageType): void; /** * Resolve a SonataAdmin URL to a RouteMatch. * * @param url - The full URL to resolve (absolute or relative) * @returns RouteMatch with pageType, resource, token and url * * @example * resolver.resolve('/admin/app/user/list') * // → { pageType: 'list', resource: 'user', token: undefined, url: '/admin/app/user/list' } * * resolver.resolve('/admin/app/user/s69db38c053269/show') * // → { pageType: 'show', resource: 'user', token: 's69db38c053269', url: '...' } */ resolve(url: string): RouteMatch; /** * Extract the pathname from a full URL or relative path. * Handles both absolute URLs (https://...) and relative paths (/admin/...). */ private extractPathname; /** * Detect the CRUDPageType from the URL pathname. */ private detectPageType; /** * Extract the resource name and optional token from the URL path segments. * * Sonata URL structure: * /admin/{adminPrefix}/{resource}/list * /admin/{adminPrefix}/{resource}/{token}/show * * The resource is the segment before the action suffix. * The token is present for show/edit/delete — it sits between resource and suffix. */ private extractResourceAndToken; /** * Check if a URL segment is a known Sonata action suffix. */ private isKnownSuffix; /** * Check if the URL pathname points to the dashboard. */ private isDashboardUrl; /** * Check if a URL points to a list page. */ static isListUrl(url: string): boolean; /** * Check if a URL points to a show page. */ static isShowUrl(url: string): boolean; /** * Check if a URL points to a dashboard page. */ static isDashboardUrl(url: string): boolean; /** * Check if a URL points to a delete page. */ static isDeleteUrl(url: string): boolean; static isBatchUrl(url: string): boolean; static needsFullPage(currentUrl: string): boolean; /** * Check if two URLs point to the same Sonata resource (same pathname base). * Used to decide between fetchFragment (same resource) and fetchFullPage (different resource). * * Example: * /admin/admin_user/list?page=1 vs /admin/admin_user/list?page=3 → true → fetchFragment * /admin/book/list?page=1 vs /admin/admin_user/list?page=1 → false → fetchFullPage */ static isSameResource(currentUrl: string, targetUrl: string): boolean; } /** * @wlindabla/sonata_spa — HistoryManager * Manages browser history via the History API (pushState / popstate). * Called by Page Subscribers after each successful navigation. * @author AGBOKOUDJO Franck */ /** * Manages browser history for the SPA router. * * Responsibilities: * 1. push(url, routeMatch) — pushes a new history entry after navigation * 2. listen() — subscribes to popstate (back/forward buttons) * and calls the kernel navigate callback with the stored RouteMatch * * The RouteMatch is stored in the history state to avoid re-running * RouteResolver on popstate — we already know the page type. * * Pipeline position (called by Page Subscribers after DOM swap): * DomSwapper.swap() → HistoryManager.push() ← HERE * → DomManager.reinitialize() * → dispatch spa:navigate:completed */ declare class HistoryManager implements HistoryManagerInterface { private static _instance; /** Callback invoked on popstate — provided by SpaKernel */ private onPopState; /** Whether listen() has already been called */ private isListening; /** * @param navigateCallback - The SpaKernel.handle() callback to invoke on popstate. * Injected by the kernel during boot() to avoid circular dependencies. */ private constructor(); /** * Returns the unique HistoryManager instance. * On first call, creates the instance with the provided callback. * On subsequent calls, returns the existing instance — callback argument is ignored. * * @param navigateCallback - The SpaKernel navigate callback (first call only) * @internal — called by SpaKernel only. */ static create(navigateCallback?: (url: string, routeMatch: RouteMatch) => Promise): HistoryManager; /** * Reset the singleton instance. * @internal — for testing purposes only. */ static reset(): void; /** * Set the navigate callback after construction. * Called by SpaKernel.boot() to inject the handle() method. * * @param callback - The callback to invoke on popstate */ setNavigateCallback(callback: (url: string, routeMatch: RouteMatch) => Promise): void; /** * Push a new entry to the browser history. * Stores the RouteMatch in the history state for popstate retrieval. * * Called by Page Subscribers after a successful DOM swap. * * @param url - The URL to push into history * @param routeMatch - The resolved RouteMatch to store in history state * * @example * // After a successful list navigation: * historyManager.push('/admin/app/user/list', routeMatch); * // window.history now has state: { _spa: true, url, routeMatch, pushedAt } */ push(url: string, routeMatch: RouteMatch): void; /** * Replace the current history entry without pushing a new one. * Used for the initial page load to mark the current state as SPA-managed. * * @param url - The URL to set (defaults to current URL) * @param routeMatch - The RouteMatch for the current page */ replace(url: string, routeMatch: RouteMatch): void; /** * Start listening to popstate events (back/forward browser buttons). * Must be called once during SpaKernel.boot(). * * On popstate: * 1. Checks if the state was pushed by @wlindabla/sonata_spa * 2. If yes — uses the stored RouteMatch directly (no re-resolution) * 3. If no — falls back to the current pathname * * @throws Error if listen() is called before setNavigateCallback() */ listen(): void; /** * Handle a popstate event from the browser. * Extracts the URL and RouteMatch from the history state and * calls the navigate callback. */ private handlePopState; /** * Build a minimal fallback RouteMatch when the state is not SPA-managed. * The SpaKernel will re-resolve it properly via RouteResolver. */ private buildFallbackRouteMatch; /** * Check if the current history state was pushed by @wlindabla/sonata_spa. */ isCurrentStateSpaManaged(): boolean; /** * Get the RouteMatch stored in the current history state. * Returns null if the current state was not pushed by the SPA. */ getCurrentRouteMatch(): RouteMatch | null; /** * Whether the HistoryManager is currently listening to popstate events. */ get listening(): boolean; } /** * @wlindabla/sonata_spa — SpaKernel * Central orchestrator of the SPA system. * Frontend equivalent of Symfony's HttpKernel. * @author AGBOKOUDJO Franck */ /** * The SpaKernel is the heart of @wlindabla/sonata_spa. * Frontend equivalent of Symfony's HttpKernel. * * It manages the full navigation pipeline: * * User click * → BindingManager builds SpaRequest * → SpaKernel.handle(SpaRequest) * 1. dispatch SpaRequestEvent (STOPPABLE — cancel navigation) * 2. RequestMatcher.isServerManaged? (YES → window.location.href) * 3. RouteResolver.resolve(url) → RouteMatch * 4. dispatch SpaRouteResolvedEvent (STOPPABLE — dev takes control) * 5. dispatch crud:list|show|delete|dashboard * → Page Subscriber handles the rest: * fetch → swap → history → dom:ready → navigate:completed * * The kernel itself does NOT fetch, does NOT swap the DOM. * It only orchestrates and dispatches events. * Page Subscribers do the actual work. * * @example * ```typescript * import { SpaKernel } from '@wlindabla/sonata_spa/kernel'; * * const spa =SpaKernel.create({ * router: { * sidebarSelector: '#sonata-admin-sidebar', * mainSelector: '#app-main', * mainContentAreaSelector: '#app-content', * mainContentHeaderSelector: '#app-content-header', * }, * serverManagedUrlOptions: [ * /\/edit(\?.*)?$/, * /\/create(\?.*)?$/ * ], * }, * env:APP_ENV * new BrowserEventDispatcher(window,{ bubbles: true })); * * // Optionally add custom subscribers * spa.addSubscriber(new MyCustomSubscriber()); *.addKernelExtension(new MyExtension(), new AnotherExtension()) * // Boot — registers all built-in subscribers and binding managers * spa.boot(); * ``` * @author AGBOKOUDJO Franck */ declare class SpaKernel implements SpaRouterInterface { private readonly _options; private readonly env; private static _instance; private readonly dispatcher; private readonly routeResolver; private readonly requestMatcher; private readonly historyManager; private sidebar; private mainContainer; private mainContentArea; private mainContentHeader; private domSwapManager; private domManager; private pageFetcher; private deleteFetcher; private delegateFetcher; private batchFetcher; /** Whether boot() has already been called */ private booted; /** Whether a navigation is currently in progress */ private isNavigating; /** Custom subscribers registered by the developer before boot() */ private readonly pendingSubscribers; /** Binding managers registered during boot() */ private readonly bindingManagers; /** The current URL being navigated to */ private currentUrl; /** Extensions registered via addKernelExtension() — sorted by priority desc */ private readonly kernelExtensions; /** Mutable CRUD event map — extensible by extensions */ private readonly crudEventMap; /** * Private constructor — SpaKernel is sealed and cannot be extended. * * Use {@link SpaKernel.create} to instantiate. * To extend kernel behavior, implement {@link SpaKernelExtensionInterface} * and register via {@link SpaKernel.create} options or {@link addKernelExtension}. * * @example * ```typescript * //correct * const spa = SpaKernel.create({ router: { ... } }, 'dev', dispatcher); * * // TypeScript error — constructor is private * class MySpa extends SpaKernel { } * ``` */ private constructor(); /** * Factory method — the only way to instantiate SpaKernel. * * SpaKernel is sealed — it cannot be extended via inheritance. * Use {@link SpaKernelExtensionInterface} to add custom behavior. * * @param options - The SPA router configuration options * @param env - The application environment (default: 'prod') * @param dispatcher - Optional shared BrowserEventDispatcher instance. * Pass the instance from @wlindabla/form_validator * (window.eventDispatcherBrowser) to share the same * event bus across all libraries. * @returns A fully constructed SpaKernel instance ready for boot() * * @example * ```typescript * import { BrowserEventDispatcher } from '@wlindabla/form_validator'; * * const spa = SpaKernel.create( * { * router: { * sidebarSelector: '.app-sidebar', * mainContentAreaSelector: '#app-content', * mainContentHeaderSelector: '#app-content-header', * }, * }, * 'dev', * new BrowserEventDispatcher(window, { bubbles: true }) * ); * * spa * .addKernelExtension(new MyExtension(),new AnotherExtension()) * .addSubscriber(new MySubscriber()) * .boot(); * ``` */ static create(options: SpaRouterOptions, env?: APP_ENV, dispatcher?: BrowserEventDispatcher): SpaKernel; /** * Boot the SPA kernel. * Must be called once after instantiation and after addSubscriber() calls. * * Execution order: * 1. Register all pending custom subscribers * 2. Register all built-in Page Subscribers * 3. Register all built-in Binding Managers * 4. Start listening to popstate (HistoryManager) * 5. Mark current URL in history state * 6. Mark as booted * * @throws Error if boot() is called more than once */ boot(): void; /** * Handle a SPA navigation request. * Frontend equivalent of Symfony's HttpKernel.handle(). * * Pipeline: * 1. Guard — prevent concurrent navigation * 2. dispatch SpaRequestEvent (STOPPABLE) * 3. RequestMatcher — server-managed check * 4. RouteResolver — resolve URL to RouteMatch * 5. dispatch SpaRouteResolvedEvent (STOPPABLE) * 6. dispatch crud:* event → Page Subscriber takes over * * @param request - The SPA navigation request to handle */ handle(request: SpaRequest): Promise; /** * Programmatically navigate to a URL. * Creates a SpaRequest with trigger 'programmatic' and calls handle(). * * @param url - The destination URL */ navigate(url: string): Promise; /** * Register a custom subscriber on the event dispatcher. * If called before boot(), the subscriber is queued and registered during boot(). * If called after boot(), the subscriber is registered immediately. * * @param subscriber - The subscriber to register * @returns this — for method chaining * * @example * ```typescript * spa * .addSubscriber(new MyAnalyticsSubscriber()) * .addSubscriber(new MyConfirmDeleteSubscriber()) * .boot(); * ``` */ addSubscriber(subscriber: EventSubscriberInterface): this; /** * Get the event dispatcher instance. * Allows the developer to add raw listeners outside subscribers. * * @example * ```typescript * spa.getDispatcher().addListener(SpaEvents.DOM_READY, (event) => { * myLibrary.init(event.container); * }); * ``` */ getDispatcher(): BrowserEventDispatcher; /** * Get the current SpaRouterOptions. */ get options(): SpaRouterOptions; /** * Get the current URL being navigated. */ get currentNavigationUrl(): string; /** * Get the HistoryManager instance. * Used by Page Subscribers to push history entries. */ getHistoryManager(): HistoryManager; /** * Get the RouteResolver instance. * Used by Page Subscribers and FormSubscriber. */ getRouteResolver(): RouteResolver; /** * Dispatch the appropriate crud:* event based on the resolved RouteMatch. * Page Subscribers listen to these events and handle the full pipeline. * * @param request - The original SPA request * @param routeMatch - The resolved RouteMatch */ private dispatchCrudEvent; /** * Map a RouteMatch pageType to the corresponding event name. * Now reads from the mutable crudEventMap so extensions can add entries. */ private getCrudEventName; /** * Redirect to the Symfony server via full page reload. * Used for server-managed URLs and error fallbacks. * * @param url - The URL to redirect to * @param reason - The reason for the server redirect */ private redirectToServer; /** * Rebind all registered Binding Managers after a DOM swap. * Called on each spa:dom:ready event. * * @param container - The swapped container element */ private rebindManagers; /** * Register a binding manager and immediately call bind(). * * @param manager - The binding manager to register */ protected registerBindingManager(manager: BindingManagerInterface): void; /** * Resolve all required DOM element references from selectors. * Called at the start of boot(). * Throws if required elements are not found. */ private resolveDomReferences; /** * Instantiate all shared services that depend on DOM references. * Called after resolveDomReferences(). */ private instantiateServices; /** * Register all built-in Page Subscribers. * * ListPageSubscriber → crud:list * ShowPageSubscriber → crud:show * DeletePageSubscriber → crud:delete * DashboardSubscriber → spa:dashboard * FormSubscriber → spa:form:submit */ private registerBuiltInSubscribers; /** * Register and bind all built-in Binding Managers. * * SidebarBindingManager → sidebar link clicks * ActionBindingManager → show/delete action links * PaginationBindingManager → pagination links * FilterBindingManager → filter form submit/reset + sort links * FormBindingManager → Sonata form submit + form_validator */ private registerBindingManagers; /** * Register one or more kernel extensions. * Extensions are sorted by priority (highest first) before boot(). * Must be called before boot(). * * Inspired by Sonata's AdminExtension system — the developer creates * a class that implements {@link SpaKernelExtensionInterface} without * inheriting from SpaKernel. * * @param extensions - One or more extension instances to register * @returns this — for method chaining * * @example * ```typescript * spa * .addKernelExtension(new MyExtension(), new AnotherExtension()) * .addSubscriber(new MySubscriber()) * .boot(); * ``` */ addKernelExtension(...extensions: SpaKernelExtensionInterface[]): this; /** * Build the SpaExtensionContext passed to all extensions during boot(). * Creates a single shared context instance — all extensions share the same * references (dispatcher, routeResolver, etc.). */ private buildExtensionContext; /** * Reset all singleton instances. * @internal — for testing purposes only. */ static reset(): void; } declare const KERNEL_WRITE_TOKEN: unique symbol; /** * @wlindabla/sonata_spa — SpaParameterBag * Centralized read-only access to SPA runtime parameters. * * Write access is restricted to SpaKernel via a private Symbol token * that is never exported — equivalent to C++ `friend class` pattern. * * Consumers (Fetchers, Subscribers, BindingManagers) can read parameters * via static getters. Only SpaKernel can write via the unexported token. * * @example * ```typescript * // In any Fetcher, Subscriber or BindingManager * if (SpaParameterBag.isDebug()) { * SonataSpaLogger.info('[MyService] debug info'); * } * const env = SpaParameterBag.getEnv(); // 'prod' | 'dev' | 'test' * ``` */ declare class SpaParameterBag { private static _env; private static _debug; private static _version; private static _booted; /** * Initialize all parameters at once. * Can only be called ONCE — subsequent calls are ignored. * The token parameter ensures only SpaKernel (which holds the Symbol) can call this. * * @internal SpaKernel only */ static initialize(token: typeof KERNEL_WRITE_TOKEN, params: { env: APP_ENV; debug?: boolean; version?: string; }): void; /** * Returns the current application environment. * @returns 'prod' | 'dev' | 'test' */ static getEnv(): APP_ENV; /** * Returns true when the application runs in debug mode. * Automatically true in 'dev' and 'test' environments. */ static isDebug(): boolean; /** * Returns the current SPA version string. */ static getVersion(): string; /** * Returns true if the ParameterBag has been initialized by SpaKernel. * Useful for guards in services that need env at construction time. */ static isBooted(): boolean; /** * Throws if the provided token does not match the private write token. * This is the enforcement mechanism — no token = no write access. */ private static guardToken; } export { APP_ENV, BindingManagerInterface, CRUDPageType, HistoryManagerInterface, RouteMatch, RouteResolverInterface, SpaKernel, SpaKernelExtensionInterface, SpaParameterBag, SpaRequest, SpaRouterInterface, SpaRouterOptions };