Files
rspade_system/node_modules/@jqhtml/router/dist/jqhtml-router.esm.js
root 37bf37dc7b Update npm packages
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 08:27:11 +00:00

1064 lines
40 KiB
JavaScript
Executable File

/**
* JQHTML Router v2.2.175
* (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 = $('<div></div>');
$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