/** * JQHTML Router v2.2.142 * (c) 2025 JQHTML Team * Released under the MIT License */ import { logDispatch, get_component_class, Jqhtml_Component } from '@jqhtml/core'; /** * Core Router implementation for JQHTML v2 */ class Jqhtml_Router { /** * Register a route */ static register_route(path, component_name, component_class) { // console.log(`[Router] Registering route: ${path} => ${component_name}`) // Normalize path - remove trailing /index if (path.endsWith('/index')) { path = path.slice(0, -6) || '/'; } // Check for duplicates in development if (this.is_dev()) { const existing = this.routes.find(r => r.url === path); if (existing) { console.error(`Duplicate routes detected for '${path}' - ${component_name} and ${existing.component_name}`); } } this.routes.push({ url: path, component_name, component_class, layout: component_class.layout, meta: component_class.meta || {} }); // console.log(`[Router] Total routes registered:`, this.routes.length) } /** * Register a layout */ static register_layout(name, component_class) { this.layouts.set(name, component_class); } /** * Parse a URL into components */ static parse_url(url, base) { let parsed; try { if (url.startsWith('http://') || url.startsWith('https://')) { parsed = new URL(url); } else if (base) { parsed = new URL(url, base); } else { // Use appropriate base URL depending on context const defaultBase = typeof window !== 'undefined' && window.location ? window.location.href : 'http://localhost:3000'; parsed = new URL(url, defaultBase); } } catch (e) { // Fallback for malformed URLs const fallbackBase = typeof window !== 'undefined' && window.location ? window.location.href : 'http://localhost:3000'; parsed = new URL(fallbackBase); } const port = parsed.port ? `:${parsed.port}` : ''; const host = `${parsed.protocol}//${parsed.hostname}${port}`; let path = parsed.pathname; // Handle file:// URLs - remove drive letter prefix if (parsed.protocol === 'file:' && path.match(/^\/[A-Z]:/)) { path = path.substring(3); // Remove "/Z:" prefix } // Normalize path - no trailing slash except for root if (path !== '/' && path.endsWith('/')) { path = path.slice(0, -1); } return { host, path, search: parsed.search, hash: parsed.hash, href: parsed.href, host_path: host + path, host_path_search: host + path + parsed.search, path_search: path + parsed.search, path_search_hash: path + parsed.search + parsed.hash }; } /** * Match a URL to a route and extract parameters */ static match_url_to_route(url) { // console.log('[Router] match_url_to_route called with:', url) // console.log('[Router] Registered routes:', this.routes.map(r => r.url)) let parsed = this.parse_url(url); let path = parsed.path; // For hash routing, extract path from hash if (this.use_hash_routing && url.includes('#')) { const hashIndex = url.indexOf('#'); const hashPart = url.substring(hashIndex + 1); if (hashPart.startsWith('/')) { // Parse the hash part as the actual path const hashUrl = hashPart.split('?')[0]; path = hashUrl; parsed = { ...parsed, path: hashUrl, search: hashPart.includes('?') ? '?' + hashPart.split('?')[1] : '', hash: '' }; } } // console.log('[Router] Parsed path:', path) path = path.substring(1); // Remove leading / // Normalize path - remove /index suffix if (path === 'index' || path.endsWith('/index')) { path = path.slice(0, -5) || ''; } // Handle root path specially const path_parts = path === '' ? [''] : path.split('/'); let matched_route = null; let matched_params = {}; // Try to match against registered routes for (const route of this.routes) { const route_path = route.url.startsWith('/') ? route.url.substring(1) : route.url; const route_parts = route_path === '' ? [''] : route_path.split('/'); // Skip if different number of segments if (route_parts.length !== path_parts.length) { continue; } let is_match = true; const params = {}; // Check each segment for (let i = 0; i < route_parts.length; i++) { const route_part = route_parts[i]; const path_part = path_parts[i]; if (route_part.startsWith(':')) { // Parameter segment - extract value const param_name = route_part.substring(1); params[param_name] = decodeURIComponent(path_part); } else if (route_part !== path_part) { // Static segment mismatch is_match = false; break; } } if (is_match) { matched_route = route; matched_params = params; // In dev mode, continue checking for duplicates if (!this.is_dev()) { break; } } } if (!matched_route) { // console.log('[Router] No route matched for path:', path) return null; } // console.log('[Router] Route matched:', matched_route.component_name, 'for path:', path) // Parse query parameters const query_params = this.deserialize(parsed.search); // Combine all parameters const args = { ...matched_params, ...query_params }; // Add hash if present if (parsed.hash) { args.hash = parsed.hash.substring(1); // Remove leading # } return { url: '/' + path, // Use the normalized path for consistency path: '/' + path, args, hash: parsed.hash.substring(1), meta: matched_route.meta || {}, component_name: matched_route.component_name, component_class: matched_route.component_class, layout: matched_route.layout }; } /** * Convert object to query string */ static serialize(params) { const parts = []; for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); } } return parts.length > 0 ? `?${parts.join('&')}` : ''; } /** * Parse query string to object */ static deserialize(search) { const params = {}; if (!search || search === '?') { return params; } const query = search.startsWith('?') ? search.substring(1) : search; const parts = query.split('&'); for (const part of parts) { const [key, value] = part.split('='); if (key) { params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : ''; } } return params; } /** * Generate URL for a route with parameters */ static url(route_name, params) { // Find route by name const route = this.routes.find(r => { var _a; return ((_a = r.meta) === null || _a === void 0 ? void 0 : _a.name) === route_name; }); if (!route) { console.error(`Route not found: ${route_name}`); return '#'; } return this.build_url(route.url, params || {}); } /** * Build URL from pattern and parameters */ static build_url(pattern, params) { const used_params = new Set(); // Replace :param placeholders let url = pattern.replace(/:([^/]+)/g, (match, param_name) => { if (params.hasOwnProperty(param_name)) { used_params.add(param_name); return encodeURIComponent(params[param_name]); } return ''; }); // Collect unused parameters for query string const query_params = {}; for (const [key, value] of Object.entries(params)) { if (!used_params.has(key) && key !== 'hash') { query_params[key] = value; } } // Add query string if needed if (Object.keys(query_params).length > 0) { url += this.serialize(query_params); } // Add hash if provided (only for non-hash routing) if (!this.use_hash_routing && params.hash) { url += `#${params.hash}`; } // Remove /index suffix if (url.includes('/index')) { url = url.replace('/index', ''); } url = url || '/'; // For hash routing, prepend # to all URLs if (this.use_hash_routing && !url.startsWith('#')) { url = '#' + url; } return url; } /** * Get current URL */ static get_current_url() { return this.current_url; } /** * Get current hash */ static get_current_hash() { return this.current_hash; } /** * Check if in development mode */ static is_dev() { return typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production'; } /** * Get all registered routes (for debugging) */ static get_routes() { return this.routes.map(r => ({ path: r.url, component_name: r.component_name })); } /** * Set the default layout name */ static set_default_layout(layout_name) { this.default_layout = layout_name; } /** * Set the SPA app instance */ static set_app(app) { this.app = app; } /** * Initialize the router * Sets up browser event handlers */ static async init() { if (this.initialized) { return; } this.initialized = true; // Detect if we need hash routing (file:// protocol) this.use_hash_routing = window.location.protocol === 'file:'; if (this.use_hash_routing) ; this.first_url = window.location.href; this.current_url = window.location.href; this.current_hash = window.location.hash.substring(1); // Set up browser event handlers this.setup_browser_integration(); // Set 30-minute expiry setTimeout(() => { this.is_expired = true; }, 30 * 60 * 1000); } /** * Setup browser integration for link interception and history */ static setup_browser_integration() { // Handle browser back/forward buttons if (this.use_hash_routing) { // For hash routing, listen to hashchange window.addEventListener('hashchange', () => { // Ignore hash changes that we triggered ourselves if (this.ignore_next_hashchange) { // console.log('Router: ignoring self-triggered hash change') return; } // Ignore hash changes while we're already dispatching if (this.is_dispatching) { // console.log('Router: ignoring hash change during dispatch') return; } // console.log('Router: hash change detected') const hashPath = window.location.hash.substring(1) || '/'; this.dispatch(hashPath, { modify_history: false }); }); } else { // For normal routing, listen to popstate window.addEventListener('popstate', () => { const current_parsed = this.parse_url(this.current_url); const target_parsed = this.parse_url(window.location.href); if (current_parsed.host_path_search !== target_parsed.host_path_search) { // console.log('Router: browser navigation detected') this.dispatch(window.location.href, { modify_history: false }); } }); } // Track ctrl key for link handling document.addEventListener('keydown', (e) => { if (e.key === 'Control' || e.metaKey) { this.ctrl_pressed = true; } }); document.addEventListener('keyup', (e) => { if (e.key === 'Control' || !e.metaKey) { this.ctrl_pressed = false; } }); // Intercept link clicks document.addEventListener('click', (e) => { // Check if expired if (this.is_expired) { return; } // Find the link element let link = e.target; while (link && link.tagName !== 'A') { link = link.parentElement; } if (!link || link.tagName !== 'A') { return; } const href = link.getAttribute('href'); // Ignore if: // - No href // - Ctrl/Cmd is pressed (open in new tab) // - Has target attribute // - Not left click // - Starts with # only (unless we're in hash routing mode) if (!href || this.ctrl_pressed || link.getAttribute('target') || e.button !== 0) { return; } // Special handling for hash routing if (this.use_hash_routing) { // Convert regular paths to hash paths if (href.startsWith('/') && !href.startsWith('//')) { e.preventDefault(); this.dispatch(href); return; } // Handle hash links if (href.startsWith('#/')) { e.preventDefault(); this.dispatch(href.substring(1)); return; } return; } // Normal routing - ignore pure hash links if (href === '#' || (href.startsWith('#') && !href.substring(1))) { return; } const current_parsed = this.parse_url(this.current_url); const target_parsed = this.parse_url(href); // Only intercept same-domain navigation if (current_parsed.host === target_parsed.host) { e.preventDefault(); this.dispatch(target_parsed.href); } }); } /** * Main dispatch method - handles navigation */ static async dispatch(url, options = {}) { // console.log('[Router] dispatch called with:', url) // Set dispatching flag to prevent re-entrant dispatches this.is_dispatching = true; // Default options const opts = { modify_history: true, trigger_events: true, scroll_top: true, ...options }; // For hash routing, handle the URL differently let routeUrl = url; let target_parsed; let current_parsed; if (this.use_hash_routing) { // For hash routing, don't parse the URL as absolute // Just ensure it starts with / if (!url.startsWith('/') && !url.startsWith('#')) { routeUrl = '/' + url; } else if (url.startsWith('#')) { routeUrl = url.substring(1); } // Parse current state for comparison current_parsed = this.parse_url(this.current_url); // For target, we just need a simple structure target_parsed = { ...current_parsed, path: routeUrl, href: window.location.href.split('#')[0] + '#' + routeUrl, path_search: routeUrl, path_search_hash: routeUrl }; } else { // Normal routing - parse URLs normally target_parsed = this.parse_url(url); current_parsed = this.parse_url(this.current_url); routeUrl = url; } // Update current URL state this.current_url = target_parsed.href; this.current_hash = target_parsed.hash.substring(1); // Update browser history if (opts.modify_history) { if (this.use_hash_routing) { // For hash routing, update the hash const hashPath = routeUrl.startsWith('#') ? routeUrl : '#' + routeUrl; if (window.location.hash !== hashPath) { // We're about to change the hash, which will trigger a hashchange event // Mark that we should ignore it since this dispatch is already handling it this.ignore_next_hashchange = true; window.location.hash = hashPath; // Clear the flag after the event loop to ensure hashchange has fired setTimeout(() => { this.ignore_next_hashchange = false; }, 0); } } else { // Normal routing with pushState if (current_parsed.host_path_search !== target_parsed.host_path_search) { // Different page - push state window.history.pushState({}, '', target_parsed.path_search_hash); } else if (current_parsed.hash !== target_parsed.hash) { // Same page, different hash - replace state window.history.replaceState({}, '', target_parsed.path_search_hash); } } } // Match URL to route (use the normalized routeUrl for hash routing) const route_info = this.match_url_to_route(this.use_hash_routing ? routeUrl : url); // console.log('[Router] Route info:', route_info) // Debug logging if (route_info) { logDispatch(url, route_info, route_info.args, false); } if (!route_info) { // No route matched - navigate to server if (target_parsed.host_path_search !== this.parse_url(this.first_url).host_path_search) { window.location.href = target_parsed.href; this.is_dispatching = false; return; } console.error('No route matched and on same domain:', url); this.is_dispatching = false; return; } // Store current route info const old_route_info = this.current_route_info; this.current_route_info = route_info; try { // Execute pre-dispatch hooks if (opts.trigger_events && this.app) { // App pre_dispatch const app_result = await this.app.pre_dispatch(route_info); if (app_result === false) { // Navigation cancelled this.current_route_info = old_route_info; this.is_dispatching = false; return; } else if (typeof app_result === 'string') { // Redirect requested this.is_dispatching = false; await this.dispatch(app_result); return; } } // Get or create layout const layout_name = route_info.layout || this.default_layout; let layout_component = this.current_layout; // Check if we need a new layout if (!layout_component || layout_component.constructor.name !== layout_name) { // console.log(`[Router] Need to create layout: ${layout_name}, current: ${layout_component?.constructor.name || 'none'}`) // Destroy old layout if exists if (layout_component) { // console.log('[Router] Destroying old layout:', layout_component.constructor.name) layout_component.destroy(); } // Get layout class const LayoutClass = get_component_class(layout_name); if (!LayoutClass) { throw new Error(`Layout not found: ${layout_name}`); } // Find the container for layouts // If we have an app (SPA), render inside it // Otherwise render directly in #app let $container; if (this.app && this.app.$) { // Render layout inside the SPA component // console.log('[Router] Rendering layout inside SPA component') $container = this.app.$; } else { // No SPA, render directly in #app // console.log('[Router] Rendering layout directly in #app') $container = $('#app'); if (!$container.length) { throw new Error('No #app element found in document'); } } // Empty the container (either SPA content or #app) $container.empty(); // Create layout component // console.log('[Router] Creating new layout component:', layout_name) layout_component = new LayoutClass({}); $container.append(layout_component.$); // console.log('[Router] Layout DOM appended, waiting for ready state') // Wait for layout to be ready before we can find elements in it // The component registers itself with LifecycleManager automatically await new Promise((resolve) => { const checkReady = () => { if (layout_component._ready_state >= 4) { resolve(); } else { setTimeout(checkReady, 10); } }; checkReady(); }); this.current_layout = layout_component; this.state.layout = layout_name; // console.log('[Router] Layout ready and stored as current_layout') } else { // console.log('[Router] Reusing existing layout:', layout_name) } // Layout pre_dispatch if (opts.trigger_events && layout_component.pre_dispatch) { const layout_result = await layout_component.pre_dispatch(route_info); if (layout_result === false) { // Navigation cancelled this.current_route_info = old_route_info; this.is_dispatching = false; return; } else if (typeof layout_result === 'string') { // Redirect requested this.is_dispatching = false; await this.dispatch(layout_result); return; } } // Get route component class const RouteClass = route_info.component_class; if (!RouteClass) { throw new Error(`Route component class not found for: ${route_info.component_name}`); } // Create route component with args const route_component = new RouteClass({ args: route_info.args }); // Route pre_dispatch if (opts.trigger_events && route_component.pre_dispatch) { const route_result = await route_component.pre_dispatch(); if (route_result === false) { // Navigation cancelled route_component.destroy(); this.current_route_info = old_route_info; this.is_dispatching = false; return; } else if (typeof route_result === 'string') { // Redirect requested route_component.destroy(); this.is_dispatching = false; await this.dispatch(route_result); return; } } // Destroy old route if exists if (this.current_route) { this.current_route.destroy(); } // Render route into layout await layout_component._render_route(route_component); this.current_route = route_component; // Wait for route to be ready before continuing // This ensures the route's lifecycle completes before we finish dispatch await new Promise((resolve) => { const checkReady = () => { if (route_component._ready_state >= 4) { resolve(); } else { setTimeout(checkReady, 10); } }; checkReady(); }); // Update state this.state.route = route_info.component_name; this.state.url = route_info.url; this.state.args = route_info.args; this.state.hash = route_info.hash; // Notify layout of route change if (layout_component.on_route_change) { await layout_component.on_route_change(old_route_info, route_info); } // Execute post-dispatch hooks in reverse order if (opts.trigger_events) { // Route post_dispatch if (route_component.post_dispatch) { await route_component.post_dispatch(); } // Layout post_dispatch if (layout_component.post_dispatch) { await layout_component.post_dispatch(route_info); } // App post_dispatch if (this.app && this.app.post_dispatch) { await this.app.post_dispatch(route_info); } } // Scroll to top if requested if (opts.scroll_top) { window.scrollTo(0, 0); } // console.log('Route dispatched successfully:', route_info.component_name) // Clear dispatching flag this.is_dispatching = false; } catch (error) { console.error('Dispatch error:', error); // Restore previous route info on error this.current_route_info = old_route_info; // Clear dispatching flag even on error this.is_dispatching = false; throw error; } } /** * Navigate and replace current history entry */ static redirect(url) { this.dispatch(url, { modify_history: true }); } /** * Navigate and replace current history entry */ static replace(url) { if (this.use_hash_routing) { // For hash routing, just update the hash const hashPath = url.startsWith('#') ? url : '#' + url; window.location.hash = hashPath; this.dispatch(url, { modify_history: false }); } else { const parsed = this.parse_url(url); window.history.replaceState({}, '', parsed.path_search_hash); this.dispatch(url, { modify_history: false }); } } } // Router state Jqhtml_Router.state = { route: null, layout: null, args: {}, url: '', hash: '' }; // Router configuration Jqhtml_Router.routes = []; Jqhtml_Router.layouts = new Map(); Jqhtml_Router.initialized = false; Jqhtml_Router.ctrl_pressed = false; Jqhtml_Router.is_expired = false; Jqhtml_Router.first_url = ''; Jqhtml_Router.current_url = ''; Jqhtml_Router.current_hash = ''; Jqhtml_Router.default_layout = 'Default_Layout'; Jqhtml_Router.app = null; Jqhtml_Router.current_route_info = null; Jqhtml_Router.current_layout = null; Jqhtml_Router.current_route = null; Jqhtml_Router.use_hash_routing = false; Jqhtml_Router.is_dispatching = false; Jqhtml_Router.ignore_next_hashchange = false; /** * Base class for route components in JQHTML Router v2 */ class Jqhtml_Route extends Jqhtml_Component { constructor(options) { super(options); // Mark this as a route component this._is_route = true; // Route parameters from URL this.args = {}; // Extract args from options if provided if (options && options.args) { this.args = options.args; } } /** * Called during app initialization to register routes * This is where routes call register_route for each path they handle */ static init() { // console.log(`[Route] Initializing ${this.name} with routes:`, this.routes) // Register each route path with the router for (const route of this.routes) { Jqhtml_Router.register_route(route, this.name, this); } } /** * Generate URL for this route with given parameters * Static version for generating URLs without an instance */ static url(params) { // If this route has a name in meta, use it if (this.meta.name) { return Jqhtml_Router.url(this.meta.name, params); } // Otherwise use the first route pattern if (this.routes.length > 0) { return Jqhtml_Router.build_url(this.routes[0], params || {}); } console.error(`Route ${this.name} has no routes defined`); return '#'; } /** * Generate URL for this route with current args merged with new params * Instance method that includes current route parameters */ url(params) { // Merge current args with new params const merged_params = { ...this.args, ...params }; return this.constructor.url(merged_params); } /** * Navigate to this route with given parameters * Static version for programmatic navigation */ static dispatch(params) { const url = this.url(params); Jqhtml_Router.dispatch(url); } /** * Navigate to this route with current args merged with new params * Instance method for navigation from within a route */ dispatch(params) { const url = this.url(params); Jqhtml_Router.dispatch(url); } /** * Called before this route is activated * Can be used for route-specific guards */ async pre_dispatch() { // Default allows activation return true; } /** * Called after this route has fully loaded * Can be used for analytics, etc. */ async post_dispatch() { // Default implementation does nothing } /** * Override on_render for custom route rendering * By default does nothing - routes should override this or use templates */ async on_render() { // Default implementation does nothing // Routes should override this method or use templates } } // Route configuration Jqhtml_Route.routes = []; Jqhtml_Route.layout = 'Default_Layout'; Jqhtml_Route.meta = {}; /** * Base class for layout components in JQHTML Router v2 */ class Jqhtml_Layout extends Jqhtml_Component { constructor(options) { super(options); // Mark this as a layout component this._is_layout = true; } /** * Called when the route changes within the same layout * Override this to update layout state (e.g., active navigation items) */ async on_route_change(old_route, new_route) { // Default implementation does nothing // Subclasses can override to handle route changes } /** * Called before dispatching to a new route * Can cancel navigation by returning false or redirect by returning a URL */ async pre_dispatch(route_info) { // Default allows all navigation return true; } /** * Called after a route has fully loaded * Can trigger redirects for post-load logic */ async post_dispatch(route_info) { // Default implementation does nothing } /** * Get the content container where routes render * Must contain an element with $id="content" */ $content() { const $content = this.$id('content'); if (!$content.length) { throw new Error(`Layout ${this.constructor.name} must have an element with $id="content"`); } return $content; } /** * Internal method to render a route into this layout * Called by the router during dispatch */ async _render_route(route_component) { const $content = this.$content(); // Clear existing content $content.empty(); // Create container for route const $route_container = $('
'); $content.append($route_container); // Mount the route component // The route component will be created by the dispatch system // and passed here already initialized $route_container.append(route_component.$); } /** * Override on_render for custom layout rendering * By default does nothing - layouts should override this or use templates */ async on_render() { // Default implementation does nothing // Layouts should override this method or use templates } // Layouts should use templates or override on_render(), not render() /** * Layouts should never re-render after initial load * They persist across route changes */ should_rerender() { return false; } } /** * Base application class for JQHTML SPAs */ class Jqhtml_SPA extends Jqhtml_Component { constructor() { super(...arguments); this.initialized = false; this._initial_url = null; } /** * Initialize the router and set up event handlers */ async init_router(config = {}) { // console.log('[SPA] Initializing router with config:', config); try { // Configure router if (config.default_layout) { // console.log('[SPA] Setting default layout:', config.default_layout); Jqhtml_Router.set_default_layout(config.default_layout); } // TODO: Add not_found_component support to router if (config.not_found_component) { // console.log('[SPA] Warning: not_found_component not yet implemented in router'); } // Set this SPA as the app instance // console.log('[SPA] Setting SPA as router app'); Jqhtml_Router.set_app(this); // Initialize the router // console.log('[SPA] Initializing router...'); await Jqhtml_Router.init(); // Determine initial URL based on routing mode let initial_url; // Check if router is using hash routing const use_hash_routing = Jqhtml_Router.use_hash_routing; if (use_hash_routing) { // For hash routing, always use the hash part if (window.location.hash && window.location.hash.length > 1) { initial_url = window.location.hash.substring(1); // console.log('[SPA] Using hash for initial URL:', initial_url); } else { initial_url = "/"; // console.log('[SPA] No hash found, using default root path'); } } else { // For normal routing, use pathname + search initial_url = window.location.pathname + window.location.search; // console.log('[SPA] Initial URL from location:', initial_url); // Handle direct .html access const parsed = Jqhtml_Router.parse_url(initial_url); // console.log('[SPA] Parsed URL:', parsed); if (parsed.path === "/" || parsed.path.endsWith(".html")) { // Check for hash-based routing fallback if (window.location.hash && window.location.hash.length > 1 && window.location.hash.startsWith('#/')) { initial_url = window.location.hash.substring(1); // console.log('[SPA] Using hash fallback for initial URL:', initial_url); } else { initial_url = "/"; // console.log('[SPA] Using default root path'); } } } // Store initial URL for dispatching in on_ready this._initial_url = initial_url; // console.log('[SPA] Stored initial URL, will dispatch in on_ready phase'); // console.log('[SPA] Router initialization complete'); } catch (error) { console.error('[SPA] Failed to initialize router:', error); throw error; } } /** * Called when the SPA component is fully ready * This is where we initialize the router and dispatch the initial route */ async on_ready() { // console.log('[SPA] on_ready() called'); if (this.initialized) { console.warn('[SPA] Already initialized'); return; } this.initialized = true; // Let subclass do any setup first await super.on_ready(); // Now dispatch to the initial URL if we have one if (this._initial_url) { // console.log('[SPA] Dispatching to initial URL:', this._initial_url); await Jqhtml_Router.dispatch(this._initial_url, { modify_history: false, // Don't modify history for initial load trigger_events: true, scroll_top: false // Preserve scroll position on initial load }); } // console.log('[SPA] Component ready, router initialized and initial route dispatched'); } /** * Called before dispatching to a new route * Can cancel navigation by returning false or redirect by returning a URL * This runs before layout.pre_dispatch() */ async pre_dispatch(route_info) { // Default allows all navigation return true; } /** * Called after a route has fully loaded * This runs after layout.post_dispatch() * Can trigger redirects for post-load logic */ async post_dispatch(route_info) { // Default implementation does nothing } /** * Set the default layout name * Call this in on_create() if you want a different default than 'Default_Layout' */ static set_default_layout(layout_name) { Jqhtml_Router.set_default_layout(layout_name); } /** * Get current router state */ static get state() { return Jqhtml_Router.state; } /** * Get current route info */ static get current_route() { return Jqhtml_Router.current_route_info || null; } } export { Jqhtml_Layout, Jqhtml_Route, Jqhtml_Router, Jqhtml_SPA }; //# sourceMappingURL=jqhtml-router.esm.js.map