@jsenv/navi 0.15.9 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/jsenv_navi.js +1349 -679
- package/dist/jsenv_navi.js.map +24 -31
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -2178,18 +2178,35 @@ const valueInLocalStorage = (
|
|
|
2178
2178
|
window.localStorage.removeItem(key);
|
|
2179
2179
|
return;
|
|
2180
2180
|
}
|
|
2181
|
-
|
|
2181
|
+
|
|
2182
|
+
let valueToSet = value;
|
|
2183
|
+
let validityMessage = getValidityMessage(valueToSet);
|
|
2184
|
+
|
|
2185
|
+
// If validation fails, try to convert the value
|
|
2186
|
+
if (validityMessage && converter) {
|
|
2187
|
+
const convertedValue = tryConvertValue(valueToSet, type);
|
|
2188
|
+
if (convertedValue !== valueToSet) {
|
|
2189
|
+
const convertedValidityMessage = getValidityMessage(convertedValue);
|
|
2190
|
+
if (!convertedValidityMessage) {
|
|
2191
|
+
// Conversion successful and valid
|
|
2192
|
+
valueToSet = convertedValue;
|
|
2193
|
+
validityMessage = "";
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2182
2198
|
if (validityMessage) {
|
|
2183
2199
|
console.warn(
|
|
2184
2200
|
`The value to set in localStorage "${key}" is invalid: ${validityMessage}`,
|
|
2185
2201
|
);
|
|
2186
2202
|
}
|
|
2203
|
+
|
|
2187
2204
|
if (converter && converter.encode) {
|
|
2188
|
-
const valueEncoded = converter.encode(
|
|
2205
|
+
const valueEncoded = converter.encode(valueToSet);
|
|
2189
2206
|
window.localStorage.setItem(key, valueEncoded);
|
|
2190
2207
|
return;
|
|
2191
2208
|
}
|
|
2192
|
-
window.localStorage.setItem(key,
|
|
2209
|
+
window.localStorage.setItem(key, valueToSet);
|
|
2193
2210
|
};
|
|
2194
2211
|
const remove = () => {
|
|
2195
2212
|
window.localStorage.removeItem(key);
|
|
@@ -2198,8 +2215,34 @@ const valueInLocalStorage = (
|
|
|
2198
2215
|
return [get, set, remove];
|
|
2199
2216
|
};
|
|
2200
2217
|
|
|
2218
|
+
const tryConvertValue = (value, type) => {
|
|
2219
|
+
const validator = typeConverters[type];
|
|
2220
|
+
if (!validator) {
|
|
2221
|
+
return value;
|
|
2222
|
+
}
|
|
2223
|
+
if (!validator.cast) {
|
|
2224
|
+
return value;
|
|
2225
|
+
}
|
|
2226
|
+
const fromType = typeof value;
|
|
2227
|
+
const castFunction = validator.cast[fromType];
|
|
2228
|
+
if (!castFunction) {
|
|
2229
|
+
return value;
|
|
2230
|
+
}
|
|
2231
|
+
const convertedValue = castFunction(value);
|
|
2232
|
+
return convertedValue;
|
|
2233
|
+
};
|
|
2234
|
+
|
|
2201
2235
|
const createNumberValidator = ({ min, max, step } = {}) => {
|
|
2202
2236
|
return {
|
|
2237
|
+
cast: {
|
|
2238
|
+
string: (value) => {
|
|
2239
|
+
const parsed = parseFloat(value);
|
|
2240
|
+
if (!isNaN(parsed) && isFinite(parsed)) {
|
|
2241
|
+
return parsed;
|
|
2242
|
+
}
|
|
2243
|
+
return value;
|
|
2244
|
+
},
|
|
2245
|
+
},
|
|
2203
2246
|
decode: (value) => {
|
|
2204
2247
|
const valueParsed = parseFloat(value);
|
|
2205
2248
|
return valueParsed;
|
|
@@ -2233,6 +2276,16 @@ const createNumberValidator = ({ min, max, step } = {}) => {
|
|
|
2233
2276
|
};
|
|
2234
2277
|
const typeConverters = {
|
|
2235
2278
|
boolean: {
|
|
2279
|
+
cast: {
|
|
2280
|
+
string: (value) => {
|
|
2281
|
+
if (value === "true") return true;
|
|
2282
|
+
if (value === "false") return false;
|
|
2283
|
+
return value;
|
|
2284
|
+
},
|
|
2285
|
+
number: (value) => {
|
|
2286
|
+
return Boolean(value);
|
|
2287
|
+
},
|
|
2288
|
+
},
|
|
2236
2289
|
checkValidity: (value) => {
|
|
2237
2290
|
if (typeof value !== "boolean") {
|
|
2238
2291
|
return `must be a boolean`;
|
|
@@ -2247,6 +2300,10 @@ const typeConverters = {
|
|
|
2247
2300
|
},
|
|
2248
2301
|
},
|
|
2249
2302
|
string: {
|
|
2303
|
+
cast: {
|
|
2304
|
+
number: String,
|
|
2305
|
+
boolean: String,
|
|
2306
|
+
},
|
|
2250
2307
|
checkValidity: (value) => {
|
|
2251
2308
|
if (typeof value !== "string") {
|
|
2252
2309
|
return `must be a string`;
|
|
@@ -2260,6 +2317,24 @@ const typeConverters = {
|
|
|
2260
2317
|
integer: createNumberValidator({ step: 1 }),
|
|
2261
2318
|
positive_integer: createNumberValidator({ min: 0, step: 1 }),
|
|
2262
2319
|
percentage: {
|
|
2320
|
+
cast: {
|
|
2321
|
+
number: (value) => {
|
|
2322
|
+
if (value >= 0 && value <= 100) {
|
|
2323
|
+
return `${value}%`;
|
|
2324
|
+
}
|
|
2325
|
+
return value;
|
|
2326
|
+
},
|
|
2327
|
+
string: (value) => {
|
|
2328
|
+
if (value.endsWith("%")) {
|
|
2329
|
+
return value;
|
|
2330
|
+
}
|
|
2331
|
+
const parsed = parseFloat(value);
|
|
2332
|
+
if (!isNaN(parsed) && parsed >= 0 && parsed <= 100) {
|
|
2333
|
+
return `${parsed}%`;
|
|
2334
|
+
}
|
|
2335
|
+
return value;
|
|
2336
|
+
},
|
|
2337
|
+
},
|
|
2263
2338
|
checkValidity: (value) => {
|
|
2264
2339
|
if (typeof value !== "string") {
|
|
2265
2340
|
return `must be a percentage`;
|
|
@@ -2279,6 +2354,16 @@ const typeConverters = {
|
|
|
2279
2354
|
},
|
|
2280
2355
|
},
|
|
2281
2356
|
object: {
|
|
2357
|
+
cast: {
|
|
2358
|
+
string: (value) => {
|
|
2359
|
+
try {
|
|
2360
|
+
return JSON.parse(value);
|
|
2361
|
+
} catch {
|
|
2362
|
+
// Invalid JSON, can't convert
|
|
2363
|
+
return value;
|
|
2364
|
+
}
|
|
2365
|
+
},
|
|
2366
|
+
},
|
|
2282
2367
|
decode: (value) => {
|
|
2283
2368
|
const valueParsed = JSON.parse(value);
|
|
2284
2369
|
return valueParsed;
|
|
@@ -2296,8 +2381,16 @@ const typeConverters = {
|
|
|
2296
2381
|
},
|
|
2297
2382
|
};
|
|
2298
2383
|
|
|
2384
|
+
// Global signal registry for route template detection
|
|
2385
|
+
const globalSignalRegistry = new Map();
|
|
2386
|
+
let signalIdCounter = 0;
|
|
2387
|
+
const generateSignalId = () => {
|
|
2388
|
+
const id = signalIdCounter++;
|
|
2389
|
+
return id;
|
|
2390
|
+
};
|
|
2391
|
+
|
|
2299
2392
|
/**
|
|
2300
|
-
* Creates an advanced signal with optional source signal synchronization
|
|
2393
|
+
* Creates an advanced signal with optional source signal synchronization, local storage persistence, and validation.
|
|
2301
2394
|
*
|
|
2302
2395
|
* The sourceSignal option creates a fallback mechanism where:
|
|
2303
2396
|
* 1. The signal initially takes the value from sourceSignal (if defined) or falls back to defaultValue
|
|
@@ -2311,16 +2404,37 @@ const typeConverters = {
|
|
|
2311
2404
|
*
|
|
2312
2405
|
* @param {any} defaultValue - The default value to use when no other value is available
|
|
2313
2406
|
* @param {Object} [options={}] - Configuration options
|
|
2407
|
+
* @param {string|number} [options.id] - Custom ID for the signal. If not provided, an auto-generated ID will be used. Used for localStorage key and route pattern detection.
|
|
2314
2408
|
* @param {import("@preact/signals").Signal} [options.sourceSignal] - Source signal to synchronize with. When the source signal changes, this signal will be updated
|
|
2315
|
-
* @param {
|
|
2409
|
+
* @param {boolean} [options.persists=false] - Whether to persist the signal value in localStorage using the signal ID as key
|
|
2316
2410
|
* @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
|
|
2317
|
-
* @
|
|
2411
|
+
* @param {Array} [options.oneOf] - Array of valid values for validation. Signal will be marked invalid if value is not in this array
|
|
2412
|
+
* @param {Function} [options.autoFix] - Function to call when validation fails to automatically fix the value
|
|
2413
|
+
* @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
|
|
2414
|
+
* @returns {import("@preact/signals").Signal} A signal that can be synchronized with a source signal and/or persisted in localStorage. The signal includes a `validity` property for validation state.
|
|
2318
2415
|
*
|
|
2319
2416
|
* @example
|
|
2320
2417
|
* // Basic signal with default value
|
|
2321
2418
|
* const count = stateSignal(0);
|
|
2322
2419
|
*
|
|
2323
2420
|
* @example
|
|
2421
|
+
* // Signal with custom ID and persistence
|
|
2422
|
+
* const theme = stateSignal("light", {
|
|
2423
|
+
* id: "user-theme",
|
|
2424
|
+
* persists: true,
|
|
2425
|
+
* type: "string"
|
|
2426
|
+
* });
|
|
2427
|
+
*
|
|
2428
|
+
* @example
|
|
2429
|
+
* // Signal with validation and auto-fix
|
|
2430
|
+
* const tab = stateSignal("overview", {
|
|
2431
|
+
* id: "current-tab",
|
|
2432
|
+
* oneOf: ["overview", "details", "settings"],
|
|
2433
|
+
* autoFix: () => "overview",
|
|
2434
|
+
* persists: true
|
|
2435
|
+
* });
|
|
2436
|
+
*
|
|
2437
|
+
* @example
|
|
2324
2438
|
* // Position that follows backend data but allows temporary overrides
|
|
2325
2439
|
* const backendPosition = signal({ x: 100, y: 50 });
|
|
2326
2440
|
* const currentPosition = stateSignal({ x: 0, y: 0 }, { sourceSignal: backendPosition });
|
|
@@ -2329,122 +2443,192 @@ const typeConverters = {
|
|
|
2329
2443
|
* // User drags: currentPosition.value = { x: 150, y: 80 } (manual override)
|
|
2330
2444
|
* // Backend updates: backendPosition.value = { x: 200, y: 60 }
|
|
2331
2445
|
* // Result: currentPosition.value = { x: 200, y: 60 } (reset to new backend value)
|
|
2332
|
-
*
|
|
2333
|
-
* @example
|
|
2334
|
-
* // Signal with localStorage persistence
|
|
2335
|
-
* const userPreference = stateSignal("light", {
|
|
2336
|
-
* localStorageKey: "theme",
|
|
2337
|
-
* type: "string"
|
|
2338
|
-
* });
|
|
2339
|
-
*
|
|
2340
|
-
* @example
|
|
2341
|
-
* // Combined: follows source with localStorage backup
|
|
2342
|
-
* const serverConfig = signal({ timeout: 5000 });
|
|
2343
|
-
* const appConfig = stateSignal({ timeout: 3000 }, {
|
|
2344
|
-
* sourceSignal: serverConfig,
|
|
2345
|
-
* localStorageKey: "app-config",
|
|
2346
|
-
* type: "object"
|
|
2347
|
-
* });
|
|
2348
2446
|
*/
|
|
2349
|
-
const
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2447
|
+
const NO_LOCAL_STORAGE = [() => undefined, () => {}, () => {}];
|
|
2448
|
+
const stateSignal = (defaultValue, options = {}) => {
|
|
2449
|
+
const {
|
|
2450
|
+
id,
|
|
2451
|
+
type = "string",
|
|
2452
|
+
oneOf,
|
|
2453
|
+
autoFix,
|
|
2454
|
+
sourceSignal,
|
|
2455
|
+
persists = false,
|
|
2456
|
+
debug,
|
|
2457
|
+
} = options;
|
|
2458
|
+
const signalId = id || generateSignalId();
|
|
2459
|
+
// Convert numeric IDs to strings for consistency
|
|
2460
|
+
const signalIdString = String(signalId);
|
|
2461
|
+
if (globalSignalRegistry.has(signalIdString)) {
|
|
2462
|
+
throw new Error(
|
|
2463
|
+
`Signal ID conflict: A signal with ID "${signalIdString}" already exists`,
|
|
2464
|
+
);
|
|
2361
2465
|
}
|
|
2362
|
-
return advancedSignal;
|
|
2363
|
-
};
|
|
2364
2466
|
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2467
|
+
// Determine localStorage key: use id if persists=true, or legacy localStorage option
|
|
2468
|
+
const localStorageKey = signalIdString;
|
|
2469
|
+
const [readFromLocalStorage, writeIntoLocalStorage, removeFromLocalStorage] =
|
|
2470
|
+
persists
|
|
2471
|
+
? valueInLocalStorage(localStorageKey, { type })
|
|
2472
|
+
: NO_LOCAL_STORAGE;
|
|
2473
|
+
const getFallbackValue = () => {
|
|
2474
|
+
const valueFromLocalStorage = readFromLocalStorage();
|
|
2475
|
+
if (valueFromLocalStorage !== undefined) {
|
|
2476
|
+
if (debug) {
|
|
2477
|
+
console.debug(
|
|
2478
|
+
`[stateSignal] using value from localStorage "${localStorageKey}"=${valueFromLocalStorage}`,
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
return valueFromLocalStorage;
|
|
2482
|
+
}
|
|
2483
|
+
if (sourceSignal) {
|
|
2484
|
+
const sourceValue = sourceSignal.peek();
|
|
2485
|
+
if (sourceValue !== undefined) {
|
|
2486
|
+
if (debug) {
|
|
2487
|
+
console.debug(
|
|
2488
|
+
`[stateSignal] using value from source signal=${sourceValue}`,
|
|
2489
|
+
);
|
|
2490
|
+
}
|
|
2491
|
+
return sourceValue;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
if (debug) {
|
|
2495
|
+
console.debug(`[stateSignal] using default value=${defaultValue}`);
|
|
2496
|
+
}
|
|
2497
|
+
return defaultValue;
|
|
2498
|
+
};
|
|
2499
|
+
|
|
2500
|
+
const advancedSignal = signal(getFallbackValue());
|
|
2501
|
+
|
|
2502
|
+
// Set signal ID and create meaningful string representation
|
|
2503
|
+
advancedSignal.__signalId = signalIdString;
|
|
2504
|
+
advancedSignal.toString = () => `{navi_state_signal:${signalIdString}}`;
|
|
2505
|
+
|
|
2506
|
+
// Store signal with its options for later route connection
|
|
2507
|
+
globalSignalRegistry.set(signalIdString, {
|
|
2508
|
+
signal: advancedSignal,
|
|
2509
|
+
options: {
|
|
2510
|
+
getFallbackValue,
|
|
2511
|
+
defaultValue,
|
|
2512
|
+
type,
|
|
2513
|
+
persists,
|
|
2514
|
+
localStorageKey,
|
|
2515
|
+
debug,
|
|
2516
|
+
...options,
|
|
2517
|
+
},
|
|
2518
|
+
});
|
|
2519
|
+
|
|
2520
|
+
const validity = { valid: true };
|
|
2521
|
+
advancedSignal.validity = validity;
|
|
2522
|
+
|
|
2523
|
+
// ensure current value always fallback to
|
|
2524
|
+
// 1. source signal
|
|
2525
|
+
// 2. local storage
|
|
2526
|
+
// 3. default value
|
|
2527
|
+
{
|
|
2528
|
+
let firstRun = true;
|
|
2529
|
+
effect(() => {
|
|
2530
|
+
const value = advancedSignal.value;
|
|
2531
|
+
if (sourceSignal) {
|
|
2532
|
+
// eslint-disable-next-line no-unused-expressions
|
|
2533
|
+
sourceSignal.value;
|
|
2534
|
+
}
|
|
2535
|
+
if (firstRun) {
|
|
2536
|
+
firstRun = true;
|
|
2380
2537
|
return;
|
|
2381
2538
|
}
|
|
2382
|
-
if (
|
|
2383
|
-
signal.value = fallbackValue;
|
|
2539
|
+
if (value !== undefined) {
|
|
2384
2540
|
return;
|
|
2385
2541
|
}
|
|
2386
|
-
|
|
2387
|
-
};
|
|
2388
|
-
applyFallback();
|
|
2389
|
-
return effect(() => {
|
|
2390
|
-
applyFallback();
|
|
2542
|
+
advancedSignal.value = getFallbackValue();
|
|
2391
2543
|
});
|
|
2392
2544
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
return;
|
|
2545
|
+
// When source signal value is updated, it overrides current signal value
|
|
2546
|
+
source_signal_override: {
|
|
2547
|
+
if (!sourceSignal) {
|
|
2548
|
+
break source_signal_override;
|
|
2398
2549
|
}
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2550
|
+
|
|
2551
|
+
let isFirstRun = true;
|
|
2552
|
+
let sourcePreviousValue;
|
|
2553
|
+
effect(() => {
|
|
2554
|
+
const sourceValue = sourceSignal.value;
|
|
2555
|
+
if (isFirstRun) {
|
|
2556
|
+
// first run
|
|
2557
|
+
isFirstRun = false;
|
|
2558
|
+
sourcePreviousValue = sourceValue;
|
|
2559
|
+
return;
|
|
2402
2560
|
}
|
|
2403
|
-
|
|
2404
|
-
|
|
2561
|
+
if (sourceValue === undefined) {
|
|
2562
|
+
// we don't have anything in the source signal, keep current value
|
|
2563
|
+
if (debug) {
|
|
2564
|
+
console.debug(
|
|
2565
|
+
`[stateSignal] source signal is undefined, keeping current value`,
|
|
2566
|
+
{
|
|
2567
|
+
sourcePreviousValue,
|
|
2568
|
+
sourceValue,
|
|
2569
|
+
},
|
|
2570
|
+
);
|
|
2571
|
+
}
|
|
2572
|
+
sourcePreviousValue = undefined;
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
// the case we want to support: source signal value changes -> override current value
|
|
2576
|
+
if (debug) {
|
|
2577
|
+
console.debug(`[stateSignal] source signal updated`, {
|
|
2578
|
+
sourcePreviousValue,
|
|
2579
|
+
sourceValue,
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
advancedSignal.value = sourceValue;
|
|
2583
|
+
sourcePreviousValue = sourceValue;
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
// Read/write into local storage when enabled
|
|
2587
|
+
persist_in_local_storage: {
|
|
2588
|
+
if (!localStorageKey) {
|
|
2589
|
+
break persist_in_local_storage;
|
|
2405
2590
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2591
|
+
effect(() => {
|
|
2592
|
+
const value = advancedSignal.value;
|
|
2593
|
+
if (value === undefined || value === null || value === defaultValue) {
|
|
2594
|
+
if (debug) {
|
|
2595
|
+
console.debug(
|
|
2596
|
+
`[stateSignal] removing "${localStorageKey}" from localStorage`,
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
removeFromLocalStorage();
|
|
2600
|
+
} else {
|
|
2601
|
+
if (debug) {
|
|
2602
|
+
console.debug(
|
|
2603
|
+
`[stateSignal] writing into localStorage "${localStorageKey}"=${value}`,
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
writeIntoLocalStorage(value);
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
// update validity object according to the advanced signal value
|
|
2611
|
+
{
|
|
2612
|
+
effect(() => {
|
|
2613
|
+
const value = advancedSignal.value;
|
|
2614
|
+
updateValidity({ oneOf }, validity, value);
|
|
2615
|
+
if (!validity.valid && autoFix) {
|
|
2616
|
+
advancedSignal.value = autoFix();
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
return advancedSignal;
|
|
2428
2623
|
};
|
|
2429
2624
|
|
|
2430
|
-
const
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
const [get, set, remove] = valueInLocalStorage(key, { type });
|
|
2436
|
-
const valueFromLocalStorage = get();
|
|
2437
|
-
if (valueFromLocalStorage !== undefined) {
|
|
2438
|
-
signal.value = valueFromLocalStorage;
|
|
2625
|
+
const updateValidity = (rules, validity, value) => {
|
|
2626
|
+
const { oneOf } = rules;
|
|
2627
|
+
if (oneOf && !oneOf.includes(value)) {
|
|
2628
|
+
validity.valid = false;
|
|
2629
|
+
return;
|
|
2439
2630
|
}
|
|
2440
|
-
|
|
2441
|
-
const value = signal.value;
|
|
2442
|
-
if (value === undefined || value === null) {
|
|
2443
|
-
remove();
|
|
2444
|
-
} else {
|
|
2445
|
-
set(value);
|
|
2446
|
-
}
|
|
2447
|
-
});
|
|
2631
|
+
validity.valid = true;
|
|
2448
2632
|
};
|
|
2449
2633
|
|
|
2450
2634
|
const getCallerInfo = (targetFunction = null, additionalOffset = 0) => {
|
|
@@ -7313,380 +7497,857 @@ const useUITransitionContentId = value => {
|
|
|
7313
7497
|
}, []);
|
|
7314
7498
|
};
|
|
7315
7499
|
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7500
|
+
/**
|
|
7501
|
+
* Custom route pattern matching system
|
|
7502
|
+
* Replaces URLPattern with a simpler, more predictable approach
|
|
7503
|
+
*/
|
|
7504
|
+
|
|
7505
|
+
|
|
7506
|
+
// Base URL management
|
|
7507
|
+
let baseFileUrl;
|
|
7508
|
+
let baseUrl;
|
|
7509
|
+
const setBaseUrl = (value) => {
|
|
7510
|
+
baseFileUrl = new URL(
|
|
7320
7511
|
value,
|
|
7321
|
-
|
|
7322
|
-
|
|
7512
|
+
typeof window === "undefined" ? "http://localhost/" : window.location,
|
|
7513
|
+
).href;
|
|
7514
|
+
baseUrl = new URL(".", baseFileUrl).href;
|
|
7515
|
+
};
|
|
7516
|
+
setBaseUrl(
|
|
7517
|
+
typeof window === "undefined"
|
|
7518
|
+
? "/"
|
|
7519
|
+
: window.location.origin,
|
|
7520
|
+
);
|
|
7521
|
+
|
|
7522
|
+
// Pattern registry for building relationships before routes are created
|
|
7523
|
+
const patternRegistry = new Map(); // pattern -> patternData
|
|
7524
|
+
const patternRelationships = new Map(); // pattern -> relationships
|
|
7525
|
+
|
|
7526
|
+
// Function to detect signals in route patterns and connect them
|
|
7527
|
+
const detectSignals = (routePattern) => {
|
|
7528
|
+
const signalConnections = [];
|
|
7529
|
+
let updatedPattern = routePattern;
|
|
7323
7530
|
|
|
7324
|
-
|
|
7325
|
-
//
|
|
7326
|
-
|
|
7327
|
-
let
|
|
7531
|
+
// Look for signals in the new syntax: :paramName={navi_state_signal:id} or ?paramName={navi_state_signal:id} or ¶mName={navi_state_signal:id}
|
|
7532
|
+
// Using curly braces to avoid conflicts with underscores in signal IDs
|
|
7533
|
+
const signalParamRegex = /([?:&])(\w+)=(\{navi_state_signal:[^}]+\})/g;
|
|
7534
|
+
let match;
|
|
7328
7535
|
|
|
7329
|
-
|
|
7330
|
-
|
|
7536
|
+
while ((match = signalParamRegex.exec(routePattern)) !== null) {
|
|
7537
|
+
const [fullMatch, prefix, paramName, signalString] = match;
|
|
7331
7538
|
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7539
|
+
// Extract the signal ID from the new format: {navi_state_signal:id}
|
|
7540
|
+
const signalIdMatch = signalString.match(/\{navi_state_signal:([^}]+)\}/);
|
|
7541
|
+
if (!signalIdMatch) {
|
|
7542
|
+
console.warn(
|
|
7543
|
+
`[detectSignals] Failed to extract signal ID from: ${signalString}`,
|
|
7544
|
+
);
|
|
7545
|
+
continue;
|
|
7546
|
+
}
|
|
7547
|
+
|
|
7548
|
+
const signalId = signalIdMatch[1];
|
|
7549
|
+
const signalData = globalSignalRegistry.get(signalId);
|
|
7550
|
+
|
|
7551
|
+
if (signalData) {
|
|
7552
|
+
const { signal, options } = signalData;
|
|
7336
7553
|
|
|
7337
|
-
|
|
7338
|
-
|
|
7554
|
+
let replacement;
|
|
7555
|
+
if (prefix === ":") {
|
|
7556
|
+
// Path parameter: :section=__jsenv_signal_1__ becomes :section
|
|
7557
|
+
replacement = `${prefix}${paramName}`;
|
|
7558
|
+
} else if (prefix === "?") {
|
|
7559
|
+
// First search parameter: ?city=__jsenv_signal_1__ becomes ?city
|
|
7560
|
+
replacement = `${prefix}${paramName}`;
|
|
7561
|
+
} else if (prefix === "&") {
|
|
7562
|
+
// Additional search parameter: &lon=__jsenv_signal_1__ becomes &lon
|
|
7563
|
+
replacement = `${prefix}${paramName}`;
|
|
7564
|
+
}
|
|
7565
|
+
updatedPattern = updatedPattern.replace(fullMatch, replacement);
|
|
7566
|
+
|
|
7567
|
+
signalConnections.push({
|
|
7568
|
+
signal,
|
|
7569
|
+
paramName,
|
|
7570
|
+
options,
|
|
7571
|
+
});
|
|
7572
|
+
} else {
|
|
7573
|
+
console.warn(
|
|
7574
|
+
`[detectSignals] Signal not found in registry for ID: "${signalId}"`,
|
|
7575
|
+
);
|
|
7576
|
+
console.warn(
|
|
7577
|
+
`[detectSignals] Available signal IDs in registry:`,
|
|
7578
|
+
Array.from(globalSignalRegistry.keys()),
|
|
7579
|
+
);
|
|
7580
|
+
console.warn(`[detectSignals] Full pattern: "${routePattern}"`);
|
|
7581
|
+
}
|
|
7339
7582
|
}
|
|
7340
7583
|
|
|
7341
|
-
return
|
|
7584
|
+
return [updatedPattern, signalConnections];
|
|
7342
7585
|
};
|
|
7343
7586
|
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
let hasRawUrlPartWithInvalidChars = false;
|
|
7351
|
-
let stringQueryParams = "";
|
|
7587
|
+
/**
|
|
7588
|
+
* Creates a custom route pattern matcher
|
|
7589
|
+
*/
|
|
7590
|
+
const createRoutePattern = (pattern) => {
|
|
7591
|
+
// Detect and process signals in the pattern first
|
|
7592
|
+
const [cleanPattern, connections] = detectSignals(pattern);
|
|
7352
7593
|
|
|
7353
|
-
//
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
if (
|
|
7358
|
-
|
|
7594
|
+
// Build parameter defaults from signal connections
|
|
7595
|
+
const parameterDefaults = new Map();
|
|
7596
|
+
for (const connection of connections) {
|
|
7597
|
+
const { paramName, options } = connection;
|
|
7598
|
+
if (options.defaultValue !== undefined) {
|
|
7599
|
+
parameterDefaults.set(paramName, options.defaultValue);
|
|
7359
7600
|
}
|
|
7360
|
-
// Set params to empty object so the rest of the function processes the URL pattern
|
|
7361
|
-
params = null;
|
|
7362
7601
|
}
|
|
7363
7602
|
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7603
|
+
const parsedPattern = parsePattern(cleanPattern, parameterDefaults);
|
|
7604
|
+
|
|
7605
|
+
const applyOn = (url) => {
|
|
7606
|
+
const result = matchUrl(parsedPattern, url, {
|
|
7607
|
+
parameterDefaults,
|
|
7608
|
+
baseUrl,
|
|
7609
|
+
});
|
|
7610
|
+
|
|
7611
|
+
return result;
|
|
7612
|
+
};
|
|
7613
|
+
|
|
7614
|
+
const buildUrl = (params = {}) => {
|
|
7615
|
+
return buildUrlFromPattern(parsedPattern, params);
|
|
7616
|
+
};
|
|
7617
|
+
|
|
7618
|
+
const resolveParams = (providedParams = {}) => {
|
|
7619
|
+
let resolvedParams = { ...providedParams };
|
|
7620
|
+
|
|
7621
|
+
// Process all connections for parameter resolution
|
|
7622
|
+
for (const connection of connections) {
|
|
7623
|
+
const { paramName, signal } = connection;
|
|
7624
|
+
|
|
7625
|
+
if (paramName in providedParams) ; else if (signal?.value !== undefined) {
|
|
7626
|
+
// Parameter was not provided, check signal value
|
|
7627
|
+
resolvedParams[paramName] = signal.value;
|
|
7628
|
+
}
|
|
7389
7629
|
}
|
|
7390
7630
|
|
|
7391
|
-
|
|
7392
|
-
return encodeURIComponent(value);
|
|
7631
|
+
return resolvedParams;
|
|
7393
7632
|
};
|
|
7394
|
-
const extraParamMap = new Map();
|
|
7395
|
-
let wildcardIndex = 0; // Declare wildcard index in the main scope
|
|
7396
|
-
|
|
7397
|
-
if (params) {
|
|
7398
|
-
const keys = Object.keys(params);
|
|
7399
|
-
|
|
7400
|
-
// First, handle special case: optional groups immediately followed by wildcards
|
|
7401
|
-
// This handles patterns like {/}?* where the optional part should be included when wildcard has content
|
|
7402
|
-
relativeUrl = relativeUrl.replace(/\{([^}]*)\}\?\*/g, (match, group) => {
|
|
7403
|
-
const paramKey = wildcardIndex.toString();
|
|
7404
|
-
const paramValue = params[paramKey];
|
|
7405
|
-
|
|
7406
|
-
if (paramValue) {
|
|
7407
|
-
// Don't add to extraParamMap since we're processing it here
|
|
7408
|
-
// For wildcards, preserve slashes as path separators
|
|
7409
|
-
const wildcardValue = encodeParamValue(paramValue, true);
|
|
7410
|
-
wildcardIndex++;
|
|
7411
|
-
// Include the optional group content when wildcard has value
|
|
7412
|
-
return group + wildcardValue;
|
|
7413
|
-
}
|
|
7414
|
-
wildcardIndex++;
|
|
7415
|
-
// Remove the optional group and wildcard when no value
|
|
7416
|
-
return "";
|
|
7417
|
-
});
|
|
7418
7633
|
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7634
|
+
/**
|
|
7635
|
+
* Build the most precise URL by using route relationships from pattern registry.
|
|
7636
|
+
* Each route is responsible for its own URL generation using its own signals.
|
|
7637
|
+
*/
|
|
7638
|
+
const buildMostPreciseUrl = (params = {}) => {
|
|
7639
|
+
// Handle parameter resolution internally to preserve user intent detection
|
|
7640
|
+
const resolvedParams = resolveParams(params);
|
|
7641
|
+
|
|
7642
|
+
// Start with resolved parameters
|
|
7643
|
+
let finalParams = { ...resolvedParams };
|
|
7644
|
+
|
|
7645
|
+
for (const connection of connections) {
|
|
7646
|
+
const { paramName, signal, options } = connection;
|
|
7647
|
+
const defaultValue = options.defaultValue;
|
|
7648
|
+
|
|
7649
|
+
if (paramName in finalParams) {
|
|
7650
|
+
// Parameter was explicitly provided - ALWAYS respect explicit values
|
|
7651
|
+
// If it equals the default value, remove it for shorter URLs
|
|
7652
|
+
if (finalParams[paramName] === defaultValue) {
|
|
7653
|
+
delete finalParams[paramName];
|
|
7654
|
+
}
|
|
7655
|
+
// Note: Don't fall through to signal logic - explicit params take precedence
|
|
7656
|
+
}
|
|
7657
|
+
// Parameter was NOT provided, check signal value
|
|
7658
|
+
else if (signal?.value !== undefined && signal.value !== defaultValue) {
|
|
7659
|
+
// Only include signal value if it's not the default
|
|
7660
|
+
finalParams[paramName] = signal.value;
|
|
7661
|
+
// If signal.value === defaultValue, omit the parameter for shorter URL
|
|
7662
|
+
}
|
|
7663
|
+
}
|
|
7664
|
+
|
|
7665
|
+
// DEEPEST URL GENERATION: Check if we should use a child route instead
|
|
7666
|
+
// This happens when:
|
|
7667
|
+
// 1. This route's parameters are all defaults (would be omitted)
|
|
7668
|
+
// 2. A child route has non-default parameters that should be included
|
|
7669
|
+
|
|
7670
|
+
// DEEPEST URL GENERATION: Only activate when NO explicit parameters provided
|
|
7671
|
+
// This prevents overriding explicit user intentions with signal-based "smart" routing
|
|
7672
|
+
// We need to distinguish between user-provided params and signal-derived params
|
|
7673
|
+
let hasUserProvidedParams = false;
|
|
7674
|
+
|
|
7675
|
+
// Check if provided params contain anything beyond what signals would provide
|
|
7676
|
+
const signalDerivedParams = {};
|
|
7677
|
+
for (const { paramName, signal, options } of connections) {
|
|
7678
|
+
if (signal?.value !== undefined) {
|
|
7679
|
+
const defaultValue = options.defaultValue;
|
|
7680
|
+
// Only include signal value if it's not the default (same logic as above)
|
|
7681
|
+
if (signal.value !== defaultValue) {
|
|
7682
|
+
signalDerivedParams[paramName] = signal.value;
|
|
7683
|
+
}
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
|
|
7687
|
+
// Check if original params (before resolution) contains anything that's not from signals
|
|
7688
|
+
// This preserves user intent detection for explicit parameters
|
|
7689
|
+
for (const [key, value] of Object.entries(params)) {
|
|
7690
|
+
if (signalDerivedParams[key] !== value) {
|
|
7691
|
+
hasUserProvidedParams = true;
|
|
7692
|
+
break;
|
|
7424
7693
|
}
|
|
7694
|
+
}
|
|
7695
|
+
|
|
7696
|
+
// Also check if original params has extra keys beyond what signals provide
|
|
7697
|
+
const providedKeys = new Set(Object.keys(params));
|
|
7698
|
+
const signalKeys = new Set(Object.keys(signalDerivedParams));
|
|
7699
|
+
for (const key of providedKeys) {
|
|
7700
|
+
if (!signalKeys.has(key)) {
|
|
7701
|
+
hasUserProvidedParams = true;
|
|
7702
|
+
break;
|
|
7703
|
+
}
|
|
7704
|
+
}
|
|
7705
|
+
|
|
7706
|
+
// ROOT ROUTE PROTECTION: Never apply deepest URL generation to root route "/"
|
|
7707
|
+
// Users must always be able to navigate to home page regardless of app state
|
|
7708
|
+
const isRootRoute = pattern === "/";
|
|
7709
|
+
|
|
7710
|
+
const relationships = patternRelationships.get(pattern);
|
|
7711
|
+
const childPatterns = relationships?.childPatterns || [];
|
|
7712
|
+
if (!hasUserProvidedParams && !isRootRoute && childPatterns.length) {
|
|
7713
|
+
// Try to find the most specific child pattern that has active signals
|
|
7714
|
+
for (const childPattern of childPatterns) {
|
|
7715
|
+
const childPatternData = getPatternData(childPattern);
|
|
7716
|
+
if (!childPatternData) continue;
|
|
7717
|
+
|
|
7718
|
+
// Check if any of this child's parameters have non-default signal values
|
|
7719
|
+
let hasActiveParams = false;
|
|
7720
|
+
const childParams = {};
|
|
7721
|
+
|
|
7722
|
+
// Include parent signal values for child pattern matching
|
|
7723
|
+
// But first check if they're compatible with the child pattern
|
|
7724
|
+
let parentSignalsCompatibleWithChild = true;
|
|
7725
|
+
for (const parentConnection of connections) {
|
|
7726
|
+
const { paramName, signal, options } = parentConnection;
|
|
7727
|
+
// Only include non-default parent signal values
|
|
7728
|
+
if (
|
|
7729
|
+
signal?.value !== undefined &&
|
|
7730
|
+
signal.value !== options.defaultValue
|
|
7731
|
+
) {
|
|
7732
|
+
// Check if child pattern has conflicting literal segments for this parameter
|
|
7733
|
+
const childParsedPattern = childPatternData.parsedPattern;
|
|
7425
7734
|
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7735
|
+
// Check if parent signal value matches a literal segment in child pattern
|
|
7736
|
+
const matchesChildLiteral = childParsedPattern.segments.some(
|
|
7737
|
+
(segment) =>
|
|
7738
|
+
segment.type === "literal" && segment.value === signal.value,
|
|
7739
|
+
);
|
|
7740
|
+
|
|
7741
|
+
// If parent signal matches a literal in child, don't add as parameter
|
|
7742
|
+
// (it's already represented in the child URL path)
|
|
7743
|
+
if (matchesChildLiteral) {
|
|
7744
|
+
// Compatible - signal value matches child literal, no need to add param
|
|
7745
|
+
continue;
|
|
7746
|
+
}
|
|
7747
|
+
|
|
7748
|
+
// For section parameter specifically, check if child has literal "settings"
|
|
7749
|
+
// but parent signal has different value (incompatible case)
|
|
7750
|
+
if (paramName === "section" && signal.value !== "settings") {
|
|
7751
|
+
const hasSettingsLiteral = childParsedPattern.segments.some(
|
|
7752
|
+
(segment) =>
|
|
7753
|
+
segment.type === "literal" && segment.value === "settings",
|
|
7754
|
+
);
|
|
7755
|
+
if (hasSettingsLiteral) {
|
|
7756
|
+
parentSignalsCompatibleWithChild = false;
|
|
7757
|
+
break;
|
|
7758
|
+
}
|
|
7759
|
+
}
|
|
7760
|
+
|
|
7761
|
+
// Only add parent signal as parameter if it doesn't match child literals
|
|
7762
|
+
childParams[paramName] = signal.value;
|
|
7763
|
+
}
|
|
7764
|
+
}
|
|
7765
|
+
|
|
7766
|
+
// Skip this child if parent signals are incompatible
|
|
7767
|
+
if (!parentSignalsCompatibleWithChild) {
|
|
7768
|
+
continue;
|
|
7769
|
+
}
|
|
7770
|
+
|
|
7771
|
+
// Check child connections and see if any have non-default values
|
|
7772
|
+
for (const connection of childPatternData.connections) {
|
|
7773
|
+
const { paramName, signal, options } = connection;
|
|
7774
|
+
const defaultValue = options.defaultValue;
|
|
7775
|
+
|
|
7776
|
+
if (signal?.value !== undefined) {
|
|
7777
|
+
childParams[paramName] = signal.value;
|
|
7778
|
+
if (signal.value !== defaultValue) {
|
|
7779
|
+
hasActiveParams = true;
|
|
7780
|
+
}
|
|
7781
|
+
}
|
|
7782
|
+
}
|
|
7783
|
+
|
|
7784
|
+
// If child has non-default parameters, use the child route
|
|
7785
|
+
if (hasActiveParams) {
|
|
7786
|
+
const childPatternObj = createRoutePattern(childPattern);
|
|
7787
|
+
// Use buildUrl (not buildMostPreciseUrl) to avoid infinite recursion
|
|
7788
|
+
const childUrl = childPatternObj.buildUrl(childParams);
|
|
7789
|
+
if (childUrl) {
|
|
7790
|
+
return childUrl;
|
|
7456
7791
|
}
|
|
7457
7792
|
}
|
|
7458
7793
|
}
|
|
7794
|
+
}
|
|
7459
7795
|
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7796
|
+
if (!parsedPattern.segments) {
|
|
7797
|
+
return "/";
|
|
7798
|
+
}
|
|
7799
|
+
|
|
7800
|
+
// Filter out segments for parameters that are not provided (omitted defaults)
|
|
7801
|
+
const filteredPattern = {
|
|
7802
|
+
...parsedPattern,
|
|
7803
|
+
segments: parsedPattern.segments.filter((segment) => {
|
|
7804
|
+
if (segment.type === "param") {
|
|
7805
|
+
// Only keep parameter segments if we have a value for them
|
|
7806
|
+
return segment.name in finalParams;
|
|
7807
|
+
}
|
|
7808
|
+
// Always keep literal segments
|
|
7809
|
+
return true;
|
|
7810
|
+
}),
|
|
7811
|
+
};
|
|
7812
|
+
|
|
7813
|
+
// Remove trailing slash if we filtered out segments
|
|
7814
|
+
if (
|
|
7815
|
+
filteredPattern.segments.length < parsedPattern.segments.length &&
|
|
7816
|
+
parsedPattern.trailingSlash
|
|
7817
|
+
) {
|
|
7818
|
+
filteredPattern.trailingSlash = false;
|
|
7819
|
+
}
|
|
7820
|
+
|
|
7821
|
+
return buildUrlFromPattern(filteredPattern, finalParams);
|
|
7822
|
+
};
|
|
7823
|
+
|
|
7824
|
+
return {
|
|
7825
|
+
originalPattern: pattern, // Return the original pattern string
|
|
7826
|
+
pattern: parsedPattern,
|
|
7827
|
+
cleanPattern, // Return the clean pattern string
|
|
7828
|
+
connections, // Return signal connections along with pattern
|
|
7829
|
+
applyOn,
|
|
7830
|
+
buildUrl,
|
|
7831
|
+
buildMostPreciseUrl,
|
|
7832
|
+
resolveParams,
|
|
7833
|
+
};
|
|
7834
|
+
};
|
|
7835
|
+
|
|
7836
|
+
/**
|
|
7837
|
+
* Parse a route pattern string into structured segments
|
|
7838
|
+
*/
|
|
7839
|
+
const parsePattern = (pattern, parameterDefaults = new Map()) => {
|
|
7840
|
+
// Handle root route
|
|
7841
|
+
if (pattern === "/") {
|
|
7842
|
+
return {
|
|
7843
|
+
original: pattern,
|
|
7844
|
+
segments: [],
|
|
7845
|
+
trailingSlash: true,
|
|
7846
|
+
wildcard: false,
|
|
7847
|
+
queryParams: [],
|
|
7848
|
+
};
|
|
7849
|
+
}
|
|
7850
|
+
|
|
7851
|
+
// Separate path and query portions
|
|
7852
|
+
const [pathPortion, queryPortion] = pattern.split("?");
|
|
7853
|
+
|
|
7854
|
+
// Parse query parameters if present
|
|
7855
|
+
const queryParams = [];
|
|
7856
|
+
if (queryPortion) {
|
|
7857
|
+
// Split query parameters by & and parse each one
|
|
7858
|
+
const querySegments = queryPortion.split("&");
|
|
7859
|
+
for (const querySegment of querySegments) {
|
|
7860
|
+
if (querySegment.includes("=")) {
|
|
7861
|
+
// Parameter with potential value: tab=value or just tab
|
|
7862
|
+
const [paramName, paramValue] = querySegment.split("=", 2);
|
|
7863
|
+
queryParams.push({
|
|
7864
|
+
type: "query_param",
|
|
7865
|
+
name: paramName,
|
|
7866
|
+
hasDefaultValue: paramValue === undefined, // No value means it uses signal/default
|
|
7867
|
+
});
|
|
7868
|
+
} else {
|
|
7869
|
+
// Parameter without value: tab
|
|
7870
|
+
queryParams.push({
|
|
7871
|
+
type: "query_param",
|
|
7872
|
+
name: querySegment,
|
|
7873
|
+
hasDefaultValue: true,
|
|
7874
|
+
});
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
}
|
|
7878
|
+
|
|
7879
|
+
// Remove leading slash for processing the path portion
|
|
7880
|
+
let cleanPattern = pathPortion.startsWith("/")
|
|
7881
|
+
? pathPortion.slice(1)
|
|
7882
|
+
: pathPortion;
|
|
7883
|
+
|
|
7884
|
+
// Check for wildcard first
|
|
7885
|
+
const wildcard = cleanPattern.endsWith("*");
|
|
7886
|
+
if (wildcard) {
|
|
7887
|
+
cleanPattern = cleanPattern.slice(0, -1); // Remove *
|
|
7888
|
+
// Also remove the slash before * if present
|
|
7889
|
+
if (cleanPattern.endsWith("/")) {
|
|
7890
|
+
cleanPattern = cleanPattern.slice(0, -1);
|
|
7891
|
+
}
|
|
7892
|
+
}
|
|
7893
|
+
|
|
7894
|
+
// Check for trailing slash (after wildcard check)
|
|
7895
|
+
const trailingSlash = !wildcard && pathPortion.endsWith("/");
|
|
7896
|
+
if (trailingSlash) {
|
|
7897
|
+
cleanPattern = cleanPattern.slice(0, -1); // Remove trailing /
|
|
7898
|
+
}
|
|
7899
|
+
|
|
7900
|
+
// Split into segments (filter out empty segments)
|
|
7901
|
+
const segmentStrings = cleanPattern
|
|
7902
|
+
? cleanPattern.split("/").filter((s) => s !== "")
|
|
7903
|
+
: [];
|
|
7904
|
+
const segments = segmentStrings.map((seg, index) => {
|
|
7905
|
+
if (seg.startsWith(":")) {
|
|
7906
|
+
// Parameter segment
|
|
7907
|
+
const paramName = seg.slice(1).replace("?", ""); // Remove : and optional ?
|
|
7908
|
+
const isOptional = seg.endsWith("?") || parameterDefaults.has(paramName);
|
|
7909
|
+
|
|
7910
|
+
return {
|
|
7911
|
+
type: "param",
|
|
7912
|
+
name: paramName,
|
|
7913
|
+
optional: isOptional,
|
|
7914
|
+
index,
|
|
7915
|
+
};
|
|
7916
|
+
}
|
|
7917
|
+
// Literal segment
|
|
7918
|
+
return {
|
|
7919
|
+
type: "literal",
|
|
7920
|
+
value: seg,
|
|
7921
|
+
index,
|
|
7922
|
+
};
|
|
7923
|
+
});
|
|
7924
|
+
|
|
7925
|
+
return {
|
|
7926
|
+
original: pattern,
|
|
7927
|
+
segments,
|
|
7928
|
+
queryParams, // Add query parameters to the parsed pattern
|
|
7929
|
+
trailingSlash,
|
|
7930
|
+
wildcard,
|
|
7931
|
+
};
|
|
7932
|
+
};
|
|
7933
|
+
|
|
7934
|
+
/**
|
|
7935
|
+
* Check if a literal segment can be treated as optional based on parent route signal defaults
|
|
7936
|
+
*/
|
|
7937
|
+
const checkIfLiteralCanBeOptional = (literalValue, patternRegistry) => {
|
|
7938
|
+
// Look through all registered patterns for parent patterns that might have this literal as a default
|
|
7939
|
+
for (const [, patternData] of patternRegistry) {
|
|
7940
|
+
// Check if any signal connection has this literal value as default
|
|
7941
|
+
for (const connection of patternData.connections) {
|
|
7942
|
+
if (connection.options.defaultValue === literalValue) {
|
|
7943
|
+
return true; // This literal matches a signal default, so it can be optional
|
|
7944
|
+
}
|
|
7945
|
+
}
|
|
7946
|
+
}
|
|
7947
|
+
return false;
|
|
7948
|
+
};
|
|
7949
|
+
|
|
7950
|
+
/**
|
|
7951
|
+
* Match a URL against a parsed pattern
|
|
7952
|
+
*/
|
|
7953
|
+
const matchUrl = (parsedPattern, url, { parameterDefaults, baseUrl }) => {
|
|
7954
|
+
// Parse the URL
|
|
7955
|
+
const urlObj = new URL(url, baseUrl);
|
|
7956
|
+
let pathname = urlObj.pathname;
|
|
7957
|
+
const originalPathname = pathname; // Store original pathname before baseUrl processing
|
|
7958
|
+
|
|
7959
|
+
// If baseUrl is provided, calculate the pathname relative to the baseUrl's directory
|
|
7960
|
+
if (baseUrl) {
|
|
7961
|
+
const baseUrlObj = new URL(baseUrl);
|
|
7962
|
+
// if the base url is a file, we want to be relative to the directory containing that file
|
|
7963
|
+
const baseDir = baseUrlObj.pathname.endsWith("/")
|
|
7964
|
+
? baseUrlObj.pathname
|
|
7965
|
+
: baseUrlObj.pathname.substring(0, baseUrlObj.pathname.lastIndexOf("/"));
|
|
7966
|
+
if (pathname.startsWith(baseDir)) {
|
|
7967
|
+
pathname = pathname.slice(baseDir.length);
|
|
7968
|
+
}
|
|
7969
|
+
}
|
|
7970
|
+
|
|
7971
|
+
// Handle root route - only matches empty path or just "/"
|
|
7972
|
+
// OR when URL exactly matches baseUrl (treating baseUrl as root)
|
|
7973
|
+
if (parsedPattern.segments.length === 0) {
|
|
7974
|
+
if (pathname === "/" || pathname === "") {
|
|
7975
|
+
return extractSearchParams(urlObj);
|
|
7976
|
+
}
|
|
7977
|
+
|
|
7978
|
+
// Special case: if URL exactly matches baseUrl, treat as root route
|
|
7979
|
+
if (baseUrl) {
|
|
7980
|
+
const baseUrlObj = new URL(baseUrl);
|
|
7981
|
+
if (originalPathname === baseUrlObj.pathname) {
|
|
7982
|
+
return extractSearchParams(urlObj);
|
|
7983
|
+
}
|
|
7984
|
+
}
|
|
7985
|
+
|
|
7986
|
+
return null;
|
|
7987
|
+
}
|
|
7988
|
+
|
|
7989
|
+
// Remove leading slash and split into segments
|
|
7990
|
+
let urlSegments = pathname.startsWith("/")
|
|
7991
|
+
? pathname
|
|
7992
|
+
.slice(1)
|
|
7993
|
+
.split("/")
|
|
7994
|
+
.filter((s) => s !== "")
|
|
7995
|
+
: pathname.split("/").filter((s) => s !== "");
|
|
7996
|
+
|
|
7997
|
+
// Handle trailing slash flexibility: if pattern has trailing slash but URL doesn't (or vice versa)
|
|
7998
|
+
// and we're at the end of segments, allow the match
|
|
7999
|
+
const urlHasTrailingSlash = pathname.endsWith("/") && pathname !== "/";
|
|
8000
|
+
const patternHasTrailingSlash = parsedPattern.trailingSlash;
|
|
8001
|
+
|
|
8002
|
+
const params = {};
|
|
8003
|
+
let urlSegmentIndex = 0;
|
|
8004
|
+
|
|
8005
|
+
// Process each pattern segment
|
|
8006
|
+
for (let i = 0; i < parsedPattern.segments.length; i++) {
|
|
8007
|
+
const patternSeg = parsedPattern.segments[i];
|
|
8008
|
+
|
|
8009
|
+
if (patternSeg.type === "literal") {
|
|
8010
|
+
// Check if URL has this segment
|
|
8011
|
+
if (urlSegmentIndex >= urlSegments.length) {
|
|
8012
|
+
// URL is too short for this literal segment
|
|
8013
|
+
// Check if this literal segment can be treated as optional based on parent route defaults
|
|
8014
|
+
const canBeOptional = checkIfLiteralCanBeOptional(
|
|
8015
|
+
patternSeg.value,
|
|
8016
|
+
patternRegistry,
|
|
8017
|
+
);
|
|
8018
|
+
if (canBeOptional) {
|
|
8019
|
+
// Skip this literal segment, don't increment urlSegmentIndex
|
|
8020
|
+
continue;
|
|
8021
|
+
}
|
|
8022
|
+
return null; // URL too short and literal is not optional
|
|
8023
|
+
}
|
|
8024
|
+
|
|
8025
|
+
const urlSeg = urlSegments[urlSegmentIndex];
|
|
8026
|
+
if (urlSeg !== patternSeg.value) {
|
|
8027
|
+
// Literal mismatch - this route doesn't match this URL
|
|
8028
|
+
return null;
|
|
8029
|
+
}
|
|
8030
|
+
urlSegmentIndex++;
|
|
8031
|
+
} else if (patternSeg.type === "param") {
|
|
8032
|
+
// Parameter segment
|
|
8033
|
+
if (urlSegmentIndex >= urlSegments.length) {
|
|
8034
|
+
// No URL segment for this parameter
|
|
8035
|
+
if (patternSeg.optional) {
|
|
8036
|
+
// Optional parameter - use default if available
|
|
8037
|
+
const defaultValue = parameterDefaults.get(patternSeg.name);
|
|
8038
|
+
if (defaultValue !== undefined) {
|
|
8039
|
+
params[patternSeg.name] = defaultValue;
|
|
8040
|
+
}
|
|
8041
|
+
continue;
|
|
8042
|
+
}
|
|
8043
|
+
// Required parameter missing - but check if we can use trailing slash logic
|
|
8044
|
+
// If this is the last segment and we have a trailing slash difference, it might still match
|
|
8045
|
+
const isLastSegment = i === parsedPattern.segments.length - 1;
|
|
8046
|
+
if (isLastSegment && patternHasTrailingSlash && !urlHasTrailingSlash) {
|
|
8047
|
+
// Pattern expects trailing slash segment, URL doesn't have it
|
|
8048
|
+
const defaultValue = parameterDefaults.get(patternSeg.name);
|
|
8049
|
+
if (defaultValue !== undefined) {
|
|
8050
|
+
params[patternSeg.name] = defaultValue;
|
|
8051
|
+
continue;
|
|
7473
8052
|
}
|
|
7474
8053
|
}
|
|
8054
|
+
return null; // Required parameter missing
|
|
7475
8055
|
}
|
|
7476
8056
|
|
|
7477
|
-
//
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
8057
|
+
// Capture URL segment as parameter value
|
|
8058
|
+
const urlSeg = urlSegments[urlSegmentIndex];
|
|
8059
|
+
params[patternSeg.name] = decodeURIComponent(urlSeg);
|
|
8060
|
+
urlSegmentIndex++;
|
|
8061
|
+
}
|
|
8062
|
+
}
|
|
8063
|
+
|
|
8064
|
+
// Check for remaining URL segments
|
|
8065
|
+
// Patterns with trailing slashes can match additional URL segments (like wildcards)
|
|
8066
|
+
// Patterns without trailing slashes should match exactly (unless they're wildcards)
|
|
8067
|
+
if (
|
|
8068
|
+
!parsedPattern.wildcard &&
|
|
8069
|
+
!parsedPattern.trailingSlash &&
|
|
8070
|
+
urlSegmentIndex < urlSegments.length
|
|
8071
|
+
) {
|
|
8072
|
+
return null; // Pattern without trailing slash should not match extra segments
|
|
8073
|
+
}
|
|
8074
|
+
// If pattern has trailing slash or wildcard, allow extra segments (no additional check needed)
|
|
8075
|
+
|
|
8076
|
+
// Add search parameters
|
|
8077
|
+
const searchParams = extractSearchParams(urlObj);
|
|
8078
|
+
Object.assign(params, searchParams);
|
|
8079
|
+
|
|
8080
|
+
// Apply remaining parameter defaults for unmatched parameters
|
|
8081
|
+
for (const [paramName, defaultValue] of parameterDefaults) {
|
|
8082
|
+
if (!(paramName in params)) {
|
|
8083
|
+
params[paramName] = defaultValue;
|
|
8084
|
+
}
|
|
8085
|
+
}
|
|
8086
|
+
|
|
8087
|
+
return params;
|
|
8088
|
+
};
|
|
8089
|
+
|
|
8090
|
+
/**
|
|
8091
|
+
* Extract search parameters from URL
|
|
8092
|
+
*/
|
|
8093
|
+
const extractSearchParams = (urlObj) => {
|
|
8094
|
+
const params = {};
|
|
8095
|
+
for (const [key, value] of urlObj.searchParams) {
|
|
8096
|
+
params[key] = value;
|
|
8097
|
+
}
|
|
8098
|
+
return params;
|
|
8099
|
+
};
|
|
8100
|
+
|
|
8101
|
+
/**
|
|
8102
|
+
* Build a URL from a pattern and parameters
|
|
8103
|
+
*/
|
|
8104
|
+
const buildUrlFromPattern = (parsedPattern, params = {}) => {
|
|
8105
|
+
if (parsedPattern.segments.length === 0) {
|
|
8106
|
+
// Root route
|
|
8107
|
+
const searchParams = new URLSearchParams();
|
|
8108
|
+
for (const [key, value] of Object.entries(params)) {
|
|
8109
|
+
if (value !== undefined) {
|
|
8110
|
+
searchParams.set(key, value);
|
|
8111
|
+
}
|
|
8112
|
+
}
|
|
8113
|
+
const search = searchParams.toString();
|
|
8114
|
+
return `/${search ? `?${search}` : ""}`;
|
|
8115
|
+
}
|
|
8116
|
+
|
|
8117
|
+
const segments = [];
|
|
8118
|
+
|
|
8119
|
+
for (const patternSeg of parsedPattern.segments) {
|
|
8120
|
+
if (patternSeg.type === "literal") {
|
|
8121
|
+
segments.push(patternSeg.value);
|
|
8122
|
+
} else if (patternSeg.type === "param") {
|
|
8123
|
+
const value = params[patternSeg.name];
|
|
8124
|
+
|
|
8125
|
+
// If value is provided, include it
|
|
8126
|
+
if (value !== undefined) {
|
|
8127
|
+
segments.push(encodeURIComponent(value));
|
|
8128
|
+
} else if (!patternSeg.optional) {
|
|
8129
|
+
// For required parameters without values, keep the placeholder
|
|
8130
|
+
segments.push(`:${patternSeg.name}`);
|
|
8131
|
+
}
|
|
8132
|
+
// Optional parameters with undefined values are omitted
|
|
8133
|
+
}
|
|
8134
|
+
}
|
|
8135
|
+
|
|
8136
|
+
let path = `/${segments.join("/")}`;
|
|
8137
|
+
|
|
8138
|
+
// Handle trailing slash - only add if it serves a purpose
|
|
8139
|
+
if (parsedPattern.trailingSlash && !path.endsWith("/") && path !== "/") {
|
|
8140
|
+
// Only add trailing slash if the original pattern suggests there could be more content
|
|
8141
|
+
// For patterns like "/admin/:section/" where the slash is at the very end,
|
|
8142
|
+
// it's not needed in the generated URL if there are no more segments
|
|
8143
|
+
const lastSegment =
|
|
8144
|
+
parsedPattern.segments[parsedPattern.segments.length - 1];
|
|
8145
|
+
const hasMorePotentialContent =
|
|
8146
|
+
parsedPattern.wildcard || (lastSegment && lastSegment.type === "literal"); // Only add slash after literals, not parameters
|
|
8147
|
+
|
|
8148
|
+
if (hasMorePotentialContent) {
|
|
8149
|
+
path += "/";
|
|
8150
|
+
}
|
|
8151
|
+
} else if (
|
|
8152
|
+
!parsedPattern.trailingSlash &&
|
|
8153
|
+
path.endsWith("/") &&
|
|
8154
|
+
path !== "/"
|
|
8155
|
+
) {
|
|
8156
|
+
// Remove trailing slash for patterns without trailing slash
|
|
8157
|
+
path = path.slice(0, -1);
|
|
8158
|
+
}
|
|
8159
|
+
|
|
8160
|
+
// Add search parameters
|
|
8161
|
+
const pathParamNames = new Set(
|
|
8162
|
+
parsedPattern.segments.filter((s) => s.type === "param").map((s) => s.name),
|
|
8163
|
+
);
|
|
8164
|
+
|
|
8165
|
+
// Add query parameters defined in the pattern first
|
|
8166
|
+
const queryParamNames = new Set();
|
|
8167
|
+
const searchParams = new URLSearchParams();
|
|
8168
|
+
|
|
8169
|
+
// Handle pattern-defined query parameters (from ?tab, &lon, etc.)
|
|
8170
|
+
if (parsedPattern.queryParams) {
|
|
8171
|
+
for (const queryParam of parsedPattern.queryParams) {
|
|
8172
|
+
const paramName = queryParam.name;
|
|
8173
|
+
queryParamNames.add(paramName);
|
|
8174
|
+
|
|
8175
|
+
const value = params[paramName];
|
|
8176
|
+
if (value !== undefined) {
|
|
8177
|
+
searchParams.set(paramName, value);
|
|
8178
|
+
}
|
|
8179
|
+
// If no value provided, don't add the parameter to keep URLs clean
|
|
8180
|
+
}
|
|
8181
|
+
}
|
|
8182
|
+
|
|
8183
|
+
// Add remaining parameters as additional query parameters (excluding path and pattern query params)
|
|
8184
|
+
for (const [key, value] of Object.entries(params)) {
|
|
8185
|
+
if (
|
|
8186
|
+
!pathParamNames.has(key) &&
|
|
8187
|
+
!queryParamNames.has(key) &&
|
|
8188
|
+
value !== undefined
|
|
8189
|
+
) {
|
|
8190
|
+
searchParams.set(key, value);
|
|
8191
|
+
}
|
|
7481
8192
|
}
|
|
7482
8193
|
|
|
7483
|
-
|
|
7484
|
-
relativeUrl = relativeUrl.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
8194
|
+
const search = searchParams.toString();
|
|
7485
8195
|
|
|
7486
|
-
//
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
const paramValue = params[paramKey];
|
|
7491
|
-
if (paramValue) {
|
|
7492
|
-
extraParamMap.delete(paramKey);
|
|
7493
|
-
const replacement = encodeParamValue(paramValue, true); // Wildcards preserve slashes
|
|
7494
|
-
wildcardIndex++;
|
|
7495
|
-
return replacement;
|
|
7496
|
-
}
|
|
7497
|
-
wildcardIndex++;
|
|
7498
|
-
return "*";
|
|
7499
|
-
});
|
|
7500
|
-
}
|
|
8196
|
+
// No longer handle trailing slash inheritance here
|
|
8197
|
+
|
|
8198
|
+
return path + (search ? `?${search}` : "");
|
|
8199
|
+
};
|
|
7501
8200
|
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
8201
|
+
/**
|
|
8202
|
+
* Check if childPattern is a child route of parentPattern
|
|
8203
|
+
* E.g., "/admin/settings/:tab" is a child of "/admin/:section/"
|
|
8204
|
+
* Also, "/admin/?tab=something" is a child of "/admin/"
|
|
8205
|
+
*/
|
|
8206
|
+
const isChildPattern = (childPattern, parentPattern) => {
|
|
8207
|
+
// Split path and query parts
|
|
8208
|
+
const [childPath, childQuery] = childPattern.split("?");
|
|
8209
|
+
const [parentPath, parentQuery] = parentPattern.split("?");
|
|
8210
|
+
|
|
8211
|
+
// Remove trailing slashes for path comparison
|
|
8212
|
+
const cleanChild = childPath.replace(/\/$/, "");
|
|
8213
|
+
const cleanParent = parentPath.replace(/\/$/, "");
|
|
8214
|
+
|
|
8215
|
+
// CASE 1: Same path, child has query params, parent doesn't
|
|
8216
|
+
// E.g., "/admin/?tab=something" is child of "/admin/"
|
|
8217
|
+
if (cleanChild === cleanParent && childQuery && !parentQuery) {
|
|
8218
|
+
return true;
|
|
7508
8219
|
}
|
|
7509
8220
|
|
|
7510
|
-
//
|
|
7511
|
-
//
|
|
7512
|
-
|
|
7513
|
-
|
|
8221
|
+
// CASE 2: Traditional path-based child relationship
|
|
8222
|
+
// Convert patterns to comparable segments for proper comparison
|
|
8223
|
+
const childSegments = cleanChild.split("/").filter((s) => s);
|
|
8224
|
+
const parentSegments = cleanParent.split("/").filter((s) => s);
|
|
8225
|
+
|
|
8226
|
+
// Child must have at least as many segments as parent
|
|
8227
|
+
if (childSegments.length < parentSegments.length) {
|
|
8228
|
+
return false;
|
|
7514
8229
|
}
|
|
7515
8230
|
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
}
|
|
7530
|
-
}
|
|
7531
|
-
}
|
|
7532
|
-
if (searchParamPairs.length > 0) {
|
|
7533
|
-
const searchString = searchParamPairs.join("&");
|
|
7534
|
-
relativeUrl += (relativeUrl.includes("?") ? "&" : "?") + searchString;
|
|
8231
|
+
let hasMoreSpecificSegment = false;
|
|
8232
|
+
|
|
8233
|
+
// Check if parent segments match child segments (allowing for parameters)
|
|
8234
|
+
for (let i = 0; i < parentSegments.length; i++) {
|
|
8235
|
+
const parentSeg = parentSegments[i];
|
|
8236
|
+
const childSeg = childSegments[i];
|
|
8237
|
+
|
|
8238
|
+
// If parent has parameter, child can have anything in that position
|
|
8239
|
+
if (parentSeg.startsWith(":")) {
|
|
8240
|
+
// Child is more specific if it has a literal value for a parent parameter
|
|
8241
|
+
// But if child also starts with ":", it's also a parameter (not more specific)
|
|
8242
|
+
if (!childSeg.startsWith(":")) {
|
|
8243
|
+
hasMoreSpecificSegment = true;
|
|
7535
8244
|
}
|
|
7536
|
-
|
|
7537
|
-
console.warn(
|
|
7538
|
-
`Unknown parameters given to "${urlPatternInput}":`,
|
|
7539
|
-
Array.from(extraParamMap.keys()),
|
|
7540
|
-
);
|
|
8245
|
+
continue;
|
|
7541
8246
|
}
|
|
7542
|
-
}
|
|
7543
8247
|
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
8248
|
+
// If parent has literal, child must match exactly
|
|
8249
|
+
if (parentSeg !== childSeg) {
|
|
8250
|
+
return false;
|
|
8251
|
+
}
|
|
7547
8252
|
}
|
|
7548
8253
|
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
hasRawUrlPartWithInvalidChars,
|
|
7552
|
-
};
|
|
8254
|
+
// Child must be more specific (more segments OR more specific segments)
|
|
8255
|
+
return childSegments.length > parentSegments.length || hasMoreSpecificSegment;
|
|
7553
8256
|
};
|
|
7554
8257
|
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
});
|
|
8258
|
+
/**
|
|
8259
|
+
* Register all patterns at once and build their relationships
|
|
8260
|
+
*/
|
|
8261
|
+
const setupPatterns = (patternDefinitions) => {
|
|
8262
|
+
// Clear existing patterns
|
|
8263
|
+
patternRegistry.clear();
|
|
8264
|
+
patternRelationships.clear();
|
|
7563
8265
|
|
|
7564
|
-
//
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
optionalParamKeySet.add(name);
|
|
7569
|
-
return "";
|
|
7570
|
-
});
|
|
7571
|
-
let wildcardIndex = 0;
|
|
7572
|
-
normalizedUrlPattern.replace(/\*(\?)?/g, (_m, opt) => {
|
|
7573
|
-
if (opt === "?") {
|
|
7574
|
-
optionalParamKeySet.add(String(wildcardIndex));
|
|
7575
|
-
}
|
|
7576
|
-
wildcardIndex++;
|
|
7577
|
-
return "";
|
|
7578
|
-
});
|
|
8266
|
+
// Phase 1: Register all patterns
|
|
8267
|
+
for (const [key, urlPatternRaw] of Object.entries(patternDefinitions)) {
|
|
8268
|
+
const [cleanPattern, connections] = detectSignals(urlPatternRaw);
|
|
8269
|
+
const parsedPattern = parsePattern(cleanPattern);
|
|
7579
8270
|
|
|
7580
|
-
|
|
8271
|
+
const patternData = {
|
|
8272
|
+
key,
|
|
8273
|
+
urlPatternRaw,
|
|
8274
|
+
cleanPattern,
|
|
8275
|
+
connections,
|
|
8276
|
+
parsedPattern,
|
|
8277
|
+
childPatterns: [],
|
|
8278
|
+
parentPatterns: [],
|
|
8279
|
+
};
|
|
7581
8280
|
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
if (match) {
|
|
7585
|
-
return extractParams(match, url);
|
|
7586
|
-
}
|
|
8281
|
+
patternRegistry.set(urlPatternRaw, patternData);
|
|
8282
|
+
}
|
|
7587
8283
|
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
const pathname = urlObj.pathname;
|
|
8284
|
+
// Phase 2: Build relationships between all patterns
|
|
8285
|
+
const allPatterns = Array.from(patternRegistry.keys());
|
|
7591
8286
|
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
const pathnameWithoutSlash = pathname.slice(0, -1);
|
|
7595
|
-
urlObj.pathname = pathnameWithoutSlash;
|
|
7596
|
-
const normalizedUrl = urlObj.href;
|
|
7597
|
-
const matchWithoutTrailingSlash = urlPattern.exec(normalizedUrl);
|
|
7598
|
-
if (matchWithoutTrailingSlash) {
|
|
7599
|
-
return extractParams(matchWithoutTrailingSlash, url);
|
|
7600
|
-
}
|
|
7601
|
-
}
|
|
7602
|
-
// Try adding trailing slash to pathname
|
|
7603
|
-
else if (!pathname.endsWith("/")) {
|
|
7604
|
-
const pathnameWithSlash = `${pathname}/`;
|
|
7605
|
-
urlObj.pathname = pathnameWithSlash;
|
|
7606
|
-
const normalizedUrl = urlObj.href;
|
|
7607
|
-
const matchWithTrailingSlash = urlPattern.exec(normalizedUrl);
|
|
7608
|
-
if (matchWithTrailingSlash) {
|
|
7609
|
-
return extractParams(matchWithTrailingSlash, url);
|
|
7610
|
-
}
|
|
7611
|
-
}
|
|
7612
|
-
return null;
|
|
7613
|
-
};
|
|
8287
|
+
for (const currentPattern of allPatterns) {
|
|
8288
|
+
const currentData = patternRegistry.get(currentPattern);
|
|
7614
8289
|
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
// Collect all parameters from URLPattern groups, handling both named and numbered groups
|
|
7625
|
-
let wildcardOffset = 0;
|
|
7626
|
-
for (const property of URL_PATTERN_PROPERTIES_WITH_GROUP_SET) {
|
|
7627
|
-
const urlPartMatch = match[property];
|
|
7628
|
-
if (urlPartMatch && urlPartMatch.groups) {
|
|
7629
|
-
let localWildcardCount = 0;
|
|
7630
|
-
for (const key of Object.keys(urlPartMatch.groups)) {
|
|
7631
|
-
const value = urlPartMatch.groups[key];
|
|
7632
|
-
const keyAsNumber = parseInt(key, 10);
|
|
7633
|
-
if (!isNaN(keyAsNumber)) {
|
|
7634
|
-
// Skip group "0" from search params as it captures the entire search string
|
|
7635
|
-
if (property === "search" && key === "0") {
|
|
7636
|
-
continue;
|
|
7637
|
-
}
|
|
7638
|
-
if (value) {
|
|
7639
|
-
// Only include non-empty values and non-ignored wildcard indices
|
|
7640
|
-
const wildcardKey = String(wildcardOffset + keyAsNumber);
|
|
7641
|
-
if (!optionalParamKeySet.has(wildcardKey)) {
|
|
7642
|
-
params[wildcardKey] = decodeURIComponent(value);
|
|
7643
|
-
}
|
|
7644
|
-
localWildcardCount++;
|
|
7645
|
-
}
|
|
7646
|
-
} else if (!optionalParamKeySet.has(key)) {
|
|
7647
|
-
// Named group (:param or {param}) - only include if not ignored
|
|
7648
|
-
params[key] = decodeURIComponent(value);
|
|
7649
|
-
}
|
|
7650
|
-
}
|
|
7651
|
-
// Update wildcard offset for next URL part
|
|
7652
|
-
wildcardOffset += localWildcardCount;
|
|
8290
|
+
for (const otherPattern of allPatterns) {
|
|
8291
|
+
if (currentPattern === otherPattern) continue;
|
|
8292
|
+
|
|
8293
|
+
const otherData = patternRegistry.get(otherPattern);
|
|
8294
|
+
|
|
8295
|
+
// Check if current pattern is a child of other pattern using clean patterns
|
|
8296
|
+
if (isChildPattern(currentData.cleanPattern, otherData.cleanPattern)) {
|
|
8297
|
+
currentData.parentPatterns.push(otherPattern);
|
|
8298
|
+
otherData.childPatterns.push(currentPattern);
|
|
7653
8299
|
}
|
|
7654
8300
|
}
|
|
7655
|
-
return params;
|
|
7656
|
-
};
|
|
7657
8301
|
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
8302
|
+
// Store relationships for easy access
|
|
8303
|
+
patternRelationships.set(currentPattern, {
|
|
8304
|
+
pattern: currentData.parsedPattern,
|
|
8305
|
+
parsedPattern: currentData.parsedPattern,
|
|
8306
|
+
connections: currentData.connections,
|
|
8307
|
+
childPatterns: currentData.childPatterns, // Store child patterns
|
|
8308
|
+
parentPatterns: currentData.parentPatterns, // Store parent patterns
|
|
8309
|
+
originalPattern: currentPattern,
|
|
8310
|
+
});
|
|
8311
|
+
}
|
|
7662
8312
|
};
|
|
7663
8313
|
|
|
7664
|
-
const URL_PATTERN_PROPERTIES_WITH_GROUP_SET = new Set([
|
|
7665
|
-
"protocol",
|
|
7666
|
-
"username",
|
|
7667
|
-
"password",
|
|
7668
|
-
"hostname",
|
|
7669
|
-
"pathname",
|
|
7670
|
-
"search",
|
|
7671
|
-
"hash",
|
|
7672
|
-
]);
|
|
7673
|
-
|
|
7674
8314
|
/**
|
|
7675
|
-
*
|
|
7676
|
-
*
|
|
8315
|
+
* Get pattern data for a registered pattern
|
|
7677
8316
|
*/
|
|
8317
|
+
const getPatternData = (urlPatternRaw) => {
|
|
8318
|
+
return patternRegistry.get(urlPatternRaw);
|
|
8319
|
+
};
|
|
7678
8320
|
|
|
8321
|
+
/**
|
|
8322
|
+
* Clear all registered patterns
|
|
8323
|
+
*/
|
|
8324
|
+
const clearPatterns = () => {
|
|
8325
|
+
patternRegistry.clear();
|
|
8326
|
+
patternRelationships.clear();
|
|
8327
|
+
};
|
|
7679
8328
|
|
|
7680
|
-
|
|
7681
|
-
if (
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
}
|
|
8329
|
+
const resolveRouteUrl = (relativeUrl) => {
|
|
8330
|
+
if (relativeUrl[0] === "/") {
|
|
8331
|
+
// we remove the leading slash because we want to resolve against baseUrl which may
|
|
8332
|
+
// not be the root url
|
|
8333
|
+
relativeUrl = relativeUrl.slice(1);
|
|
8334
|
+
}
|
|
7686
8335
|
|
|
7687
|
-
|
|
7688
|
-
|
|
8336
|
+
// we don't use URL constructor on PURPOSE (in case the relativeUrl contains invalid url chars)
|
|
8337
|
+
// and we want to support use cases where people WANT to produce invalid urls (for example rawUrlPart with spaces)
|
|
8338
|
+
// because these urls will be handled by non standard clients (like a backend service allowing url like stuff)
|
|
8339
|
+
if (baseUrl.endsWith("/")) {
|
|
8340
|
+
return `${baseUrl}${relativeUrl}`;
|
|
8341
|
+
}
|
|
8342
|
+
return `${baseUrl}/${relativeUrl}`;
|
|
7689
8343
|
};
|
|
8344
|
+
|
|
8345
|
+
/**
|
|
8346
|
+
* Route management with pattern-first architecture
|
|
8347
|
+
* Routes work with relative URLs, patterns handle base URL resolution
|
|
8348
|
+
*/
|
|
8349
|
+
|
|
8350
|
+
|
|
7690
8351
|
// Controls what happens to actions when their route stops matching:
|
|
7691
8352
|
// 'abort' - Cancel the action immediately when route stops matching
|
|
7692
8353
|
// 'keep-loading' - Allow action to continue running after route stops matching
|
|
@@ -7695,19 +8356,23 @@ const setBaseUrl = (value) => {
|
|
|
7695
8356
|
// However, since route reactivation triggers action reload anyway, the old data won't be used
|
|
7696
8357
|
// so it's better to abort the action to avoid unnecessary resource usage.
|
|
7697
8358
|
const ROUTE_DEACTIVATION_STRATEGY = "abort"; // 'abort', 'keep-loading'
|
|
8359
|
+
const ROUTE_NOT_MATCHING_PARAMS = {};
|
|
7698
8360
|
|
|
7699
8361
|
const routeSet = new Set();
|
|
7700
8362
|
// Store previous route states to detect changes
|
|
8363
|
+
const routePrivatePropertiesMap = new Map();
|
|
8364
|
+
|
|
7701
8365
|
const routePreviousStateMap = new WeakMap();
|
|
7702
8366
|
// Store abort controllers per action to control their lifecycle based on route state
|
|
7703
8367
|
const actionAbortControllerWeakMap = new WeakMap();
|
|
8368
|
+
|
|
7704
8369
|
const updateRoutes = (
|
|
7705
8370
|
url,
|
|
7706
8371
|
{
|
|
8372
|
+
navigationType = "push",
|
|
8373
|
+
isVisited = () => false,
|
|
7707
8374
|
// state
|
|
7708
|
-
|
|
7709
|
-
isVisited,
|
|
7710
|
-
},
|
|
8375
|
+
} = {},
|
|
7711
8376
|
) => {
|
|
7712
8377
|
const routeMatchInfoSet = new Set();
|
|
7713
8378
|
for (const route of routeSet) {
|
|
@@ -7717,32 +8382,19 @@ const updateRoutes = (
|
|
|
7717
8382
|
// Get previous state
|
|
7718
8383
|
const previousState = routePreviousStateMap.get(route) || {
|
|
7719
8384
|
matching: false,
|
|
7720
|
-
|
|
7721
|
-
params: null,
|
|
8385
|
+
params: ROUTE_NOT_MATCHING_PARAMS,
|
|
7722
8386
|
};
|
|
7723
8387
|
const oldMatching = previousState.matching;
|
|
7724
|
-
const oldExactMatching = previousState.exactMatching;
|
|
7725
8388
|
const oldParams = previousState.params;
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
if (newMatching && extractedParams) {
|
|
7732
|
-
// Check if any wildcard parameters (numeric keys) have meaningful content
|
|
7733
|
-
const hasWildcardContent = Object.keys(extractedParams).some((key) => {
|
|
7734
|
-
const keyAsNumber = parseInt(key, 10);
|
|
7735
|
-
if (!isNaN(keyAsNumber)) {
|
|
7736
|
-
// This is a wildcard parameter (numeric key)
|
|
7737
|
-
const value = extractedParams[key];
|
|
7738
|
-
return value && value.trim() !== "";
|
|
7739
|
-
}
|
|
7740
|
-
return false;
|
|
7741
|
-
});
|
|
7742
|
-
newExactMatching = !hasWildcardContent;
|
|
7743
|
-
}
|
|
8389
|
+
|
|
8390
|
+
// Use custom pattern matching - much simpler than URLPattern approach
|
|
8391
|
+
let extractedParams = routePattern.applyOn(url);
|
|
8392
|
+
let newMatching = Boolean(extractedParams);
|
|
8393
|
+
|
|
7744
8394
|
let newParams;
|
|
8395
|
+
|
|
7745
8396
|
if (extractedParams) {
|
|
8397
|
+
// No need for complex wildcard correction - custom system handles it properly
|
|
7746
8398
|
if (compareTwoJsValues(oldParams, extractedParams)) {
|
|
7747
8399
|
// No change in parameters, keep the old params
|
|
7748
8400
|
newParams = oldParams;
|
|
@@ -7750,7 +8402,7 @@ const updateRoutes = (
|
|
|
7750
8402
|
newParams = extractedParams;
|
|
7751
8403
|
}
|
|
7752
8404
|
} else {
|
|
7753
|
-
newParams =
|
|
8405
|
+
newParams = ROUTE_NOT_MATCHING_PARAMS;
|
|
7754
8406
|
}
|
|
7755
8407
|
|
|
7756
8408
|
const routeMatchInfo = {
|
|
@@ -7758,8 +8410,6 @@ const updateRoutes = (
|
|
|
7758
8410
|
routePrivateProperties,
|
|
7759
8411
|
oldMatching,
|
|
7760
8412
|
newMatching,
|
|
7761
|
-
oldExactMatching,
|
|
7762
|
-
newExactMatching,
|
|
7763
8413
|
oldParams,
|
|
7764
8414
|
newParams,
|
|
7765
8415
|
};
|
|
@@ -7767,7 +8417,6 @@ const updateRoutes = (
|
|
|
7767
8417
|
// Store current state for next comparison
|
|
7768
8418
|
routePreviousStateMap.set(route, {
|
|
7769
8419
|
matching: newMatching,
|
|
7770
|
-
exactMatching: newExactMatching,
|
|
7771
8420
|
params: newParams,
|
|
7772
8421
|
});
|
|
7773
8422
|
}
|
|
@@ -7779,14 +8428,12 @@ const updateRoutes = (
|
|
|
7779
8428
|
route,
|
|
7780
8429
|
routePrivateProperties,
|
|
7781
8430
|
newMatching,
|
|
7782
|
-
newExactMatching,
|
|
7783
8431
|
newParams,
|
|
7784
8432
|
} of routeMatchInfoSet) {
|
|
7785
8433
|
const { updateStatus } = routePrivateProperties;
|
|
7786
8434
|
const visited = isVisited(route.url);
|
|
7787
8435
|
updateStatus({
|
|
7788
8436
|
matching: newMatching,
|
|
7789
|
-
exactMatching: newExactMatching,
|
|
7790
8437
|
params: newParams,
|
|
7791
8438
|
visited,
|
|
7792
8439
|
});
|
|
@@ -7807,7 +8454,11 @@ const updateRoutes = (
|
|
|
7807
8454
|
const routeAction = route.action;
|
|
7808
8455
|
const currentAction = routeAction.getCurrentAction();
|
|
7809
8456
|
if (shouldLoad) {
|
|
7810
|
-
if (
|
|
8457
|
+
if (
|
|
8458
|
+
navigationType === "replace" ||
|
|
8459
|
+
currentAction.aborted ||
|
|
8460
|
+
currentAction.error
|
|
8461
|
+
) {
|
|
7811
8462
|
shouldLoad = false;
|
|
7812
8463
|
}
|
|
7813
8464
|
}
|
|
@@ -7885,12 +8536,15 @@ const updateRoutes = (
|
|
|
7885
8536
|
};
|
|
7886
8537
|
};
|
|
7887
8538
|
|
|
7888
|
-
const routePrivatePropertiesMap = new Map();
|
|
7889
8539
|
const getRoutePrivateProperties = (route) => {
|
|
7890
8540
|
return routePrivatePropertiesMap.get(route);
|
|
7891
8541
|
};
|
|
7892
8542
|
|
|
7893
|
-
const
|
|
8543
|
+
const registerRoute = (routePattern) => {
|
|
8544
|
+
const urlPatternRaw = routePattern.originalPattern;
|
|
8545
|
+
const patternData = getPatternData(urlPatternRaw);
|
|
8546
|
+
const { cleanPattern, connections } = patternData;
|
|
8547
|
+
|
|
7894
8548
|
const cleanupCallbackSet = new Set();
|
|
7895
8549
|
const cleanup = () => {
|
|
7896
8550
|
for (const cleanupCallback of cleanupCallbackSet) {
|
|
@@ -7898,14 +8552,14 @@ const createRoute = (urlPatternInput) => {
|
|
|
7898
8552
|
}
|
|
7899
8553
|
cleanupCallbackSet.clear();
|
|
7900
8554
|
};
|
|
7901
|
-
|
|
7902
8555
|
const [publishStatus, subscribeStatus] = createPubSub();
|
|
8556
|
+
|
|
7903
8557
|
const route = {
|
|
7904
|
-
urlPattern:
|
|
8558
|
+
urlPattern: cleanPattern,
|
|
8559
|
+
pattern: cleanPattern,
|
|
7905
8560
|
isRoute: true,
|
|
7906
8561
|
matching: false,
|
|
7907
|
-
|
|
7908
|
-
params: null,
|
|
8562
|
+
params: ROUTE_NOT_MATCHING_PARAMS,
|
|
7909
8563
|
buildUrl: null,
|
|
7910
8564
|
bindAction: null,
|
|
7911
8565
|
relativeUrl: null,
|
|
@@ -7913,167 +8567,171 @@ const createRoute = (urlPatternInput) => {
|
|
|
7913
8567
|
action: null,
|
|
7914
8568
|
cleanup,
|
|
7915
8569
|
toString: () => {
|
|
7916
|
-
return `route "${
|
|
8570
|
+
return `route "${cleanPattern}"`;
|
|
7917
8571
|
},
|
|
7918
8572
|
replaceParams: undefined,
|
|
7919
8573
|
subscribeStatus,
|
|
7920
8574
|
};
|
|
7921
8575
|
routeSet.add(route);
|
|
7922
|
-
|
|
7923
8576
|
const routePrivateProperties = {
|
|
7924
|
-
routePattern
|
|
8577
|
+
routePattern,
|
|
8578
|
+
originalPattern: urlPatternRaw,
|
|
8579
|
+
pattern: cleanPattern,
|
|
7925
8580
|
matchingSignal: null,
|
|
7926
|
-
exactMatchingSignal: null,
|
|
7927
8581
|
paramsSignal: null,
|
|
8582
|
+
rawParamsSignal: null,
|
|
7928
8583
|
visitedSignal: null,
|
|
7929
8584
|
relativeUrlSignal: null,
|
|
7930
8585
|
urlSignal: null,
|
|
7931
|
-
updateStatus: ({ matching,
|
|
8586
|
+
updateStatus: ({ matching, params, visited }) => {
|
|
7932
8587
|
let someChange = false;
|
|
7933
8588
|
matchingSignal.value = matching;
|
|
7934
|
-
|
|
7935
|
-
paramsSignal.value = params;
|
|
7936
|
-
visitedSignal.value = visited;
|
|
8589
|
+
|
|
7937
8590
|
if (route.matching !== matching) {
|
|
7938
8591
|
route.matching = matching;
|
|
7939
8592
|
someChange = true;
|
|
7940
8593
|
}
|
|
7941
|
-
|
|
7942
|
-
route.exactMatching = exactMatching;
|
|
7943
|
-
someChange = true;
|
|
7944
|
-
}
|
|
7945
|
-
if (route.params !== params) {
|
|
7946
|
-
route.params = params;
|
|
7947
|
-
someChange = true;
|
|
7948
|
-
}
|
|
8594
|
+
visitedSignal.value = visited;
|
|
7949
8595
|
if (route.visited !== visited) {
|
|
7950
8596
|
route.visited = visited;
|
|
7951
8597
|
someChange = true;
|
|
7952
8598
|
}
|
|
8599
|
+
// Store raw params (from URL) - paramsSignal will reactively compute merged params
|
|
8600
|
+
rawParamsSignal.value = params;
|
|
8601
|
+
// Get merged params for comparison (computed signal will handle the merging)
|
|
8602
|
+
const mergedParams = paramsSignal.value;
|
|
8603
|
+
if (route.params !== mergedParams) {
|
|
8604
|
+
route.params = mergedParams;
|
|
8605
|
+
someChange = true;
|
|
8606
|
+
}
|
|
7953
8607
|
if (someChange) {
|
|
7954
|
-
publishStatus({
|
|
8608
|
+
publishStatus({
|
|
8609
|
+
matching,
|
|
8610
|
+
params: mergedParams,
|
|
8611
|
+
visited,
|
|
8612
|
+
});
|
|
7955
8613
|
}
|
|
7956
8614
|
},
|
|
7957
8615
|
};
|
|
7958
8616
|
routePrivatePropertiesMap.set(route, routePrivateProperties);
|
|
7959
8617
|
|
|
7960
|
-
const
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
const
|
|
7964
|
-
|
|
7965
|
-
|
|
8618
|
+
const matchingSignal = signal(false);
|
|
8619
|
+
const rawParamsSignal = signal(ROUTE_NOT_MATCHING_PARAMS);
|
|
8620
|
+
const paramsSignal = computed(() => {
|
|
8621
|
+
const rawParams = rawParamsSignal.value;
|
|
8622
|
+
// Pattern system handles parameter defaults, routes just work with raw params
|
|
8623
|
+
return rawParams || {};
|
|
8624
|
+
});
|
|
8625
|
+
const visitedSignal = signal(false);
|
|
8626
|
+
for (const { signal: stateSignal, paramName, options = {} } of connections) {
|
|
8627
|
+
const { debug } = options;
|
|
7966
8628
|
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
const
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
8629
|
+
// URL -> Signal synchronization
|
|
8630
|
+
effect(() => {
|
|
8631
|
+
const matching = matchingSignal.value;
|
|
8632
|
+
const params = rawParamsSignal.value;
|
|
8633
|
+
const urlParamValue = params[paramName];
|
|
8634
|
+
|
|
8635
|
+
if (!matching) {
|
|
8636
|
+
return;
|
|
8637
|
+
}
|
|
8638
|
+
if (debug) {
|
|
8639
|
+
console.debug(
|
|
8640
|
+
`[stateSignal] URL -> Signal: ${paramName}=${urlParamValue}`,
|
|
8641
|
+
);
|
|
8642
|
+
}
|
|
8643
|
+
stateSignal.value = urlParamValue;
|
|
8644
|
+
});
|
|
8645
|
+
|
|
8646
|
+
// Signal -> URL synchronization
|
|
8647
|
+
effect(() => {
|
|
8648
|
+
const value = stateSignal.value;
|
|
8649
|
+
const params = rawParamsSignal.value;
|
|
8650
|
+
const urlParamValue = params[paramName];
|
|
8651
|
+
const matching = matchingSignal.value;
|
|
8652
|
+
|
|
8653
|
+
if (!matching || value === urlParamValue) {
|
|
8654
|
+
return;
|
|
8655
|
+
}
|
|
8656
|
+
|
|
8657
|
+
if (debug) {
|
|
8658
|
+
console.debug(`[stateSignal] Signal -> URL: ${paramName}=${value}`);
|
|
7975
8659
|
}
|
|
7976
|
-
|
|
8660
|
+
|
|
8661
|
+
route.replaceParams({ [paramName]: value });
|
|
8662
|
+
});
|
|
8663
|
+
}
|
|
8664
|
+
|
|
8665
|
+
route.navTo = (params) => {
|
|
8666
|
+
if (!browserIntegration$1) {
|
|
8667
|
+
return Promise.resolve();
|
|
7977
8668
|
}
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
if (
|
|
7982
|
-
return
|
|
8669
|
+
return browserIntegration$1.navTo(route.buildUrl(params));
|
|
8670
|
+
};
|
|
8671
|
+
route.redirectTo = (params) => {
|
|
8672
|
+
if (!browserIntegration$1) {
|
|
8673
|
+
return Promise.resolve();
|
|
7983
8674
|
}
|
|
7984
|
-
|
|
7985
|
-
|
|
8675
|
+
return browserIntegration$1.navTo(route.buildUrl(params), {
|
|
8676
|
+
replace: true,
|
|
8677
|
+
});
|
|
8678
|
+
};
|
|
8679
|
+
route.replaceParams = (newParams) => {
|
|
8680
|
+
const matching = matchingSignal.peek();
|
|
8681
|
+
if (!matching) {
|
|
8682
|
+
console.warn(
|
|
8683
|
+
`Cannot replace params on route ${route} because it is not matching the current URL.`,
|
|
8684
|
+
);
|
|
8685
|
+
return null;
|
|
7986
8686
|
}
|
|
7987
|
-
|
|
8687
|
+
if (route.action) {
|
|
8688
|
+
// For action: merge with resolved params (includes defaults) so action gets complete params
|
|
8689
|
+
const currentResolvedParams = routePattern.resolveParams();
|
|
8690
|
+
const updatedActionParams = { ...currentResolvedParams, ...newParams };
|
|
8691
|
+
route.action.replaceParams(updatedActionParams);
|
|
8692
|
+
}
|
|
8693
|
+
return route.redirectTo(newParams);
|
|
7988
8694
|
};
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
*
|
|
8007
|
-
*/
|
|
8008
|
-
const buildUrl = (params = {}) => {
|
|
8009
|
-
const { relativeUrl, hasRawUrlPartWithInvalidChars } =
|
|
8010
|
-
buildRelativeUrl(params);
|
|
8011
|
-
let processedRelativeUrl = relativeUrl;
|
|
8012
|
-
if (processedRelativeUrl[0] === "/") {
|
|
8013
|
-
// we remove the leading slash because we want to resolve against baseUrl which may
|
|
8014
|
-
// not be the root url
|
|
8015
|
-
processedRelativeUrl = processedRelativeUrl.slice(1);
|
|
8016
|
-
}
|
|
8017
|
-
if (hasRawUrlPartWithInvalidChars) {
|
|
8018
|
-
if (!baseUrl.endsWith("/")) {
|
|
8019
|
-
return `${baseUrl}/${processedRelativeUrl}`;
|
|
8020
|
-
}
|
|
8021
|
-
return `${baseUrl}${processedRelativeUrl}`;
|
|
8022
|
-
}
|
|
8023
|
-
const url = new URL(processedRelativeUrl, baseUrl).href;
|
|
8024
|
-
return url;
|
|
8695
|
+
route.buildRelativeUrl = (params) => {
|
|
8696
|
+
// buildMostPreciseUrl now handles parameter resolution internally
|
|
8697
|
+
return routePattern.buildMostPreciseUrl(params);
|
|
8698
|
+
};
|
|
8699
|
+
route.buildUrl = (params) => {
|
|
8700
|
+
const routeRelativeUrl = route.buildRelativeUrl(params);
|
|
8701
|
+
const routeUrl = resolveRouteUrl(routeRelativeUrl);
|
|
8702
|
+
return routeUrl;
|
|
8703
|
+
};
|
|
8704
|
+
route.matchesParams = (providedParams) => {
|
|
8705
|
+
const currentParams = route.params;
|
|
8706
|
+
const resolvedParams = routePattern.resolveParams({
|
|
8707
|
+
...currentParams,
|
|
8708
|
+
...providedParams,
|
|
8709
|
+
});
|
|
8710
|
+
const same = compareTwoJsValues(currentParams, resolvedParams);
|
|
8711
|
+
return same;
|
|
8025
8712
|
};
|
|
8026
|
-
route.buildUrl = buildUrl;
|
|
8027
8713
|
|
|
8028
|
-
|
|
8029
|
-
const exactMatchingSignal = signal(false);
|
|
8030
|
-
const paramsSignal = signal(null);
|
|
8031
|
-
const visitedSignal = signal(false);
|
|
8714
|
+
// relativeUrl/url
|
|
8032
8715
|
const relativeUrlSignal = computed(() => {
|
|
8033
|
-
const
|
|
8034
|
-
const
|
|
8716
|
+
const rawParams = rawParamsSignal.value;
|
|
8717
|
+
const relativeUrl = route.buildRelativeUrl(rawParams);
|
|
8035
8718
|
return relativeUrl;
|
|
8036
8719
|
});
|
|
8720
|
+
const urlSignal = computed(() => {
|
|
8721
|
+
const routeUrl = route.buildUrl();
|
|
8722
|
+
return routeUrl;
|
|
8723
|
+
});
|
|
8037
8724
|
const disposeRelativeUrlEffect = effect(() => {
|
|
8038
8725
|
route.relativeUrl = relativeUrlSignal.value;
|
|
8039
8726
|
});
|
|
8040
|
-
cleanupCallbackSet.add(disposeRelativeUrlEffect);
|
|
8041
|
-
|
|
8042
|
-
const urlSignal = computed(() => {
|
|
8043
|
-
const relativeUrl = relativeUrlSignal.value;
|
|
8044
|
-
const url = new URL(relativeUrl, baseUrl).href;
|
|
8045
|
-
return url;
|
|
8046
|
-
});
|
|
8047
8727
|
const disposeUrlEffect = effect(() => {
|
|
8048
8728
|
route.url = urlSignal.value;
|
|
8049
8729
|
});
|
|
8730
|
+
cleanupCallbackSet.add(disposeRelativeUrlEffect);
|
|
8050
8731
|
cleanupCallbackSet.add(disposeUrlEffect);
|
|
8051
8732
|
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
const updatedParams = { ...currentParams, ...newParams };
|
|
8055
|
-
const updatedUrl = route.buildUrl(updatedParams);
|
|
8056
|
-
if (route.action) {
|
|
8057
|
-
route.action.replaceParams(updatedParams);
|
|
8058
|
-
}
|
|
8059
|
-
browserIntegration$1.navTo(updatedUrl, { replace: true });
|
|
8060
|
-
};
|
|
8061
|
-
route.replaceParams = replaceParams;
|
|
8062
|
-
|
|
8063
|
-
const bindAction = (action) => {
|
|
8064
|
-
/*
|
|
8065
|
-
*
|
|
8066
|
-
* here I need to check the store for that action (if any)
|
|
8067
|
-
* and listen store changes to do this:
|
|
8068
|
-
*
|
|
8069
|
-
* When we detect changes we want to update the route params
|
|
8070
|
-
* so we'll need to use navTo(buildUrl(params), { replace: true })
|
|
8071
|
-
*
|
|
8072
|
-
* reinserted is useful because the item id might have changed
|
|
8073
|
-
* but not the mutable key
|
|
8074
|
-
*
|
|
8075
|
-
*/
|
|
8076
|
-
|
|
8733
|
+
// action stuff (for later)
|
|
8734
|
+
route.bindAction = (action) => {
|
|
8077
8735
|
const { store } = action.meta;
|
|
8078
8736
|
if (store) {
|
|
8079
8737
|
const { mutableIdKeys } = store;
|
|
@@ -8100,75 +8758,40 @@ const createRoute = (urlPatternInput) => {
|
|
|
8100
8758
|
}
|
|
8101
8759
|
}
|
|
8102
8760
|
|
|
8103
|
-
/*
|
|
8104
|
-
store.registerPropertyLifecycle(activeItemSignal, key, {
|
|
8105
|
-
changed: (value) => {
|
|
8106
|
-
route.replaceParams({
|
|
8107
|
-
[key]: value,
|
|
8108
|
-
});
|
|
8109
|
-
},
|
|
8110
|
-
dropped: () => {
|
|
8111
|
-
route.reload();
|
|
8112
|
-
},
|
|
8113
|
-
reinserted: () => {
|
|
8114
|
-
// this will reload all routes which works but
|
|
8115
|
-
// - most of the time only "route" is impacted, any other route could stay as is
|
|
8116
|
-
// - we already have the data, reloading the route will refetch the backend which is unnecessary
|
|
8117
|
-
// we could just remove routing error (which is cause by 404 likely)
|
|
8118
|
-
// to actually let the data be displayed
|
|
8119
|
-
// because they are available, but in reality the route has no data
|
|
8120
|
-
// because the fetch failed
|
|
8121
|
-
// so conceptually reloading is fine,
|
|
8122
|
-
// the only thing that bothers me a little is that it reloads all routes
|
|
8123
|
-
route.reload();
|
|
8124
|
-
},
|
|
8125
|
-
});
|
|
8126
|
-
*/
|
|
8127
|
-
|
|
8128
8761
|
const actionBoundToThisRoute = action.bindParams(paramsSignal);
|
|
8129
8762
|
route.action = actionBoundToThisRoute;
|
|
8130
8763
|
return actionBoundToThisRoute;
|
|
8131
8764
|
};
|
|
8132
|
-
route.bindAction = bindAction;
|
|
8133
8765
|
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
const routePattern = createRoutePattern(urlPatternInput, baseUrl);
|
|
8143
|
-
routePrivateProperties.routePattern = routePattern;
|
|
8144
|
-
}
|
|
8766
|
+
// Store private properties for internal access
|
|
8767
|
+
routePrivateProperties.matchingSignal = matchingSignal;
|
|
8768
|
+
routePrivateProperties.paramsSignal = paramsSignal;
|
|
8769
|
+
routePrivateProperties.rawParamsSignal = rawParamsSignal;
|
|
8770
|
+
routePrivateProperties.visitedSignal = visitedSignal;
|
|
8771
|
+
routePrivateProperties.relativeUrlSignal = relativeUrlSignal;
|
|
8772
|
+
routePrivateProperties.urlSignal = urlSignal;
|
|
8773
|
+
routePrivateProperties.cleanupCallbackSet = cleanupCallbackSet;
|
|
8145
8774
|
|
|
8146
8775
|
return route;
|
|
8147
8776
|
};
|
|
8777
|
+
|
|
8148
8778
|
const useRouteStatus = (route) => {
|
|
8149
8779
|
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
8150
8780
|
if (!routePrivateProperties) {
|
|
8151
8781
|
throw new Error(`Cannot find route private properties for ${route}`);
|
|
8152
8782
|
}
|
|
8153
8783
|
|
|
8154
|
-
const {
|
|
8155
|
-
|
|
8156
|
-
matchingSignal,
|
|
8157
|
-
exactMatchingSignal,
|
|
8158
|
-
paramsSignal,
|
|
8159
|
-
visitedSignal,
|
|
8160
|
-
} = routePrivateProperties;
|
|
8784
|
+
const { urlSignal, matchingSignal, paramsSignal, visitedSignal } =
|
|
8785
|
+
routePrivateProperties;
|
|
8161
8786
|
|
|
8162
8787
|
const url = urlSignal.value;
|
|
8163
8788
|
const matching = matchingSignal.value;
|
|
8164
|
-
const exactMatching = exactMatchingSignal.value;
|
|
8165
8789
|
const params = paramsSignal.value;
|
|
8166
8790
|
const visited = visitedSignal.value;
|
|
8167
8791
|
|
|
8168
8792
|
return {
|
|
8169
8793
|
url,
|
|
8170
8794
|
matching,
|
|
8171
|
-
exactMatching,
|
|
8172
8795
|
params,
|
|
8173
8796
|
visited,
|
|
8174
8797
|
};
|
|
@@ -8178,7 +8801,6 @@ let browserIntegration$1;
|
|
|
8178
8801
|
const setBrowserIntegration = (integration) => {
|
|
8179
8802
|
browserIntegration$1 = integration;
|
|
8180
8803
|
};
|
|
8181
|
-
|
|
8182
8804
|
let onRouteDefined = () => {};
|
|
8183
8805
|
const setOnRouteDefined = (v) => {
|
|
8184
8806
|
onRouteDefined = v;
|
|
@@ -8199,23 +8821,47 @@ const setOnRouteDefined = (v) => {
|
|
|
8199
8821
|
// at any given time (url can be shared, reloaded, etc..)
|
|
8200
8822
|
// Later I'll consider adding ability to have dynamic import into the mix
|
|
8201
8823
|
// (An async function returning an action)
|
|
8824
|
+
|
|
8202
8825
|
const setupRoutes = (routeDefinition) => {
|
|
8203
|
-
//
|
|
8204
|
-
|
|
8205
|
-
|
|
8826
|
+
// Prevent calling setupRoutes when routes already exist - enforce clean setup
|
|
8827
|
+
if (routeSet.size > 0) {
|
|
8828
|
+
throw new Error(
|
|
8829
|
+
"Routes already exist. Call clearAllRoutes() first to clean up existing routes before creating new ones. This prevents cross-test pollution and ensures clean state.",
|
|
8830
|
+
);
|
|
8206
8831
|
}
|
|
8207
|
-
|
|
8208
|
-
|
|
8832
|
+
// PHASE 1: Register all patterns and build their relationships
|
|
8833
|
+
setupPatterns(routeDefinition);
|
|
8834
|
+
// PHASE 2: Create route patterns with signal connections and parameter defaults
|
|
8835
|
+
const routePatterns = {};
|
|
8836
|
+
for (const key of Object.keys(routeDefinition)) {
|
|
8837
|
+
const urlPatternRaw = routeDefinition[key];
|
|
8838
|
+
routePatterns[key] = createRoutePattern(urlPatternRaw);
|
|
8839
|
+
}
|
|
8840
|
+
// PHASE 3: Create routes using pre-created patterns
|
|
8209
8841
|
const routes = {};
|
|
8210
8842
|
for (const key of Object.keys(routeDefinition)) {
|
|
8211
|
-
const
|
|
8212
|
-
const route =
|
|
8843
|
+
const routePattern = routePatterns[key];
|
|
8844
|
+
const route = registerRoute(routePattern);
|
|
8213
8845
|
routes[key] = route;
|
|
8214
8846
|
}
|
|
8215
8847
|
onRouteDefined();
|
|
8848
|
+
|
|
8216
8849
|
return routes;
|
|
8217
8850
|
};
|
|
8218
8851
|
|
|
8852
|
+
// for unit tests
|
|
8853
|
+
const clearAllRoutes = () => {
|
|
8854
|
+
for (const route of routeSet) {
|
|
8855
|
+
route.cleanup();
|
|
8856
|
+
}
|
|
8857
|
+
routeSet.clear();
|
|
8858
|
+
routePrivatePropertiesMap.clear();
|
|
8859
|
+
// Clear patterns as well
|
|
8860
|
+
clearPatterns();
|
|
8861
|
+
// Don't clear signal registry here - let tests manage it explicitly
|
|
8862
|
+
// This prevents clearing signals that are still being used across multiple route registrations
|
|
8863
|
+
};
|
|
8864
|
+
|
|
8219
8865
|
const arraySignal = (initialValue = []) => {
|
|
8220
8866
|
const theSignal = signal(initialValue);
|
|
8221
8867
|
|
|
@@ -8340,7 +8986,9 @@ computed(() => {
|
|
|
8340
8986
|
return reasonArray;
|
|
8341
8987
|
});
|
|
8342
8988
|
|
|
8343
|
-
const documentUrlSignal = signal(
|
|
8989
|
+
const documentUrlSignal = signal(
|
|
8990
|
+
typeof window === "undefined" ? "http://localhost" : window.location.href,
|
|
8991
|
+
);
|
|
8344
8992
|
const useDocumentUrl = () => {
|
|
8345
8993
|
return documentUrlSignal.value;
|
|
8346
8994
|
};
|
|
@@ -8477,11 +9125,10 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8477
9125
|
{ reason = "replaceDocumentState called" } = {},
|
|
8478
9126
|
) => {
|
|
8479
9127
|
const url = window.location.href;
|
|
8480
|
-
window.history.replaceState(newState, null, url);
|
|
8481
9128
|
handleRoutingTask(url, {
|
|
8482
|
-
replace: true,
|
|
8483
|
-
state: newState,
|
|
8484
9129
|
reason,
|
|
9130
|
+
navigationType: "replace",
|
|
9131
|
+
state: newState,
|
|
8485
9132
|
});
|
|
8486
9133
|
};
|
|
8487
9134
|
|
|
@@ -8504,42 +9151,52 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8504
9151
|
return;
|
|
8505
9152
|
}
|
|
8506
9153
|
visitedUrlSet.add(url);
|
|
8507
|
-
|
|
8508
|
-
// Increment signal to notify subscribers that visited URLs changed
|
|
8509
|
-
visitedUrlsSignal.value++;
|
|
9154
|
+
visitedUrlsSignal.value++; // Increment signal to notify subscribers that visited URLs changed
|
|
8510
9155
|
|
|
8511
9156
|
const historyState = getDocumentState() || {};
|
|
8512
|
-
const
|
|
9157
|
+
const historyStateWithVisitedUrls = {
|
|
8513
9158
|
...historyState,
|
|
8514
9159
|
jsenv_visited_urls: Array.from(visitedUrlSet),
|
|
8515
9160
|
};
|
|
8516
9161
|
window.history.replaceState(
|
|
8517
|
-
|
|
9162
|
+
historyStateWithVisitedUrls,
|
|
8518
9163
|
null,
|
|
8519
9164
|
window.location.href,
|
|
8520
9165
|
);
|
|
8521
|
-
updateDocumentState(
|
|
9166
|
+
updateDocumentState(historyStateWithVisitedUrls);
|
|
8522
9167
|
};
|
|
8523
9168
|
|
|
8524
9169
|
let abortController = null;
|
|
8525
|
-
const handleRoutingTask = (
|
|
8526
|
-
|
|
9170
|
+
const handleRoutingTask = (
|
|
9171
|
+
url,
|
|
9172
|
+
{
|
|
9173
|
+
reason,
|
|
9174
|
+
navigationType, // "push", "reload", "replace", "traverse"
|
|
9175
|
+
state,
|
|
9176
|
+
},
|
|
9177
|
+
) => {
|
|
9178
|
+
if (navigationType === "push") {
|
|
9179
|
+
window.history.pushState(state, null, url);
|
|
9180
|
+
} else if (navigationType === "replace") {
|
|
9181
|
+
window.history.replaceState(state, null, url);
|
|
9182
|
+
}
|
|
9183
|
+
|
|
8527
9184
|
updateDocumentUrl(url);
|
|
8528
9185
|
updateDocumentState(state);
|
|
9186
|
+
markUrlAsVisited(url);
|
|
8529
9187
|
if (abortController) {
|
|
8530
9188
|
abortController.abort(`navigating to ${url}`);
|
|
8531
9189
|
}
|
|
8532
9190
|
abortController = new AbortController();
|
|
8533
|
-
|
|
9191
|
+
const abortSignal = abortController.signal;
|
|
8534
9192
|
const { allResult, requestedResult } = applyRouting(url, {
|
|
8535
9193
|
globalAbortSignal: globalAbortController.signal,
|
|
8536
|
-
abortSignal
|
|
8537
|
-
state,
|
|
8538
|
-
replace,
|
|
8539
|
-
isVisited,
|
|
9194
|
+
abortSignal,
|
|
8540
9195
|
reason,
|
|
9196
|
+
navigationType,
|
|
9197
|
+
isVisited,
|
|
9198
|
+
state,
|
|
8541
9199
|
});
|
|
8542
|
-
|
|
8543
9200
|
executeWithCleanup(
|
|
8544
9201
|
() => allResult,
|
|
8545
9202
|
() => {
|
|
@@ -8583,11 +9240,10 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8583
9240
|
return;
|
|
8584
9241
|
}
|
|
8585
9242
|
e.preventDefault();
|
|
8586
|
-
const state = null;
|
|
8587
|
-
history.pushState(state, null, href);
|
|
8588
9243
|
handleRoutingTask(href, {
|
|
8589
|
-
state,
|
|
8590
9244
|
reason: `"click" on a[href="${href}"]`,
|
|
9245
|
+
navigationType: "push",
|
|
9246
|
+
state: null,
|
|
8591
9247
|
});
|
|
8592
9248
|
},
|
|
8593
9249
|
{ capture: true },
|
|
@@ -8596,7 +9252,8 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8596
9252
|
window.addEventListener(
|
|
8597
9253
|
"submit",
|
|
8598
9254
|
() => {
|
|
8599
|
-
//
|
|
9255
|
+
// Handle form submissions?
|
|
9256
|
+
// Not needed yet
|
|
8600
9257
|
},
|
|
8601
9258
|
{ capture: true },
|
|
8602
9259
|
);
|
|
@@ -8605,21 +9262,17 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8605
9262
|
const url = window.location.href;
|
|
8606
9263
|
const state = popstateEvent.state;
|
|
8607
9264
|
handleRoutingTask(url, {
|
|
8608
|
-
state,
|
|
8609
9265
|
reason: `"popstate" event for ${url}`,
|
|
9266
|
+
navigationType: "traverse",
|
|
9267
|
+
state,
|
|
8610
9268
|
});
|
|
8611
9269
|
});
|
|
8612
9270
|
|
|
8613
|
-
const navTo = async (url, { state = null
|
|
8614
|
-
if (replace) {
|
|
8615
|
-
window.history.replaceState(state, null, url);
|
|
8616
|
-
} else {
|
|
8617
|
-
window.history.pushState(state, null, url);
|
|
8618
|
-
}
|
|
9271
|
+
const navTo = async (url, { replace, state = null } = {}) => {
|
|
8619
9272
|
handleRoutingTask(url, {
|
|
8620
|
-
state,
|
|
8621
|
-
replace,
|
|
8622
9273
|
reason: `navTo called with "${url}"`,
|
|
9274
|
+
navigationType: replace ? "replace" : "push",
|
|
9275
|
+
state,
|
|
8623
9276
|
});
|
|
8624
9277
|
};
|
|
8625
9278
|
|
|
@@ -8631,6 +9284,8 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8631
9284
|
const url = window.location.href;
|
|
8632
9285
|
const state = history.state;
|
|
8633
9286
|
handleRoutingTask(url, {
|
|
9287
|
+
reason: "reload called",
|
|
9288
|
+
navigationType: "reload",
|
|
8634
9289
|
state,
|
|
8635
9290
|
});
|
|
8636
9291
|
};
|
|
@@ -8646,11 +9301,10 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8646
9301
|
const init = () => {
|
|
8647
9302
|
const url = window.location.href;
|
|
8648
9303
|
const state = history.state;
|
|
8649
|
-
history.replaceState(state, null, url);
|
|
8650
9304
|
handleRoutingTask(url, {
|
|
8651
|
-
state,
|
|
8652
|
-
replace: true,
|
|
8653
9305
|
reason: "routing initialization",
|
|
9306
|
+
navigationType: "replace",
|
|
9307
|
+
state,
|
|
8654
9308
|
});
|
|
8655
9309
|
};
|
|
8656
9310
|
|
|
@@ -8686,7 +9340,7 @@ const applyRouting = (
|
|
|
8686
9340
|
globalAbortSignal,
|
|
8687
9341
|
abortSignal,
|
|
8688
9342
|
// state
|
|
8689
|
-
|
|
9343
|
+
navigationType,
|
|
8690
9344
|
isVisited,
|
|
8691
9345
|
reason,
|
|
8692
9346
|
},
|
|
@@ -8698,9 +9352,9 @@ const applyRouting = (
|
|
|
8698
9352
|
routeLoadRequestedMap,
|
|
8699
9353
|
activeRouteSet,
|
|
8700
9354
|
} = updateRoutes(url, {
|
|
8701
|
-
|
|
8702
|
-
// state,
|
|
9355
|
+
navigationType,
|
|
8703
9356
|
isVisited,
|
|
9357
|
+
// state,
|
|
8704
9358
|
});
|
|
8705
9359
|
if (loadSet.size === 0 && reloadSet.size === 0) {
|
|
8706
9360
|
return {
|
|
@@ -8937,7 +9591,8 @@ const Route = ({
|
|
|
8937
9591
|
index,
|
|
8938
9592
|
fallback,
|
|
8939
9593
|
meta,
|
|
8940
|
-
children
|
|
9594
|
+
children,
|
|
9595
|
+
routeParams
|
|
8941
9596
|
}) => {
|
|
8942
9597
|
const forceRender = useForceRender();
|
|
8943
9598
|
const hasDiscoveredRef = useRef(false);
|
|
@@ -8949,6 +9604,7 @@ const Route = ({
|
|
|
8949
9604
|
index: index,
|
|
8950
9605
|
fallback: fallback,
|
|
8951
9606
|
meta: meta,
|
|
9607
|
+
routeParams: routeParams,
|
|
8952
9608
|
onMatchingInfoChange: matchingInfo => {
|
|
8953
9609
|
hasDiscoveredRef.current = true;
|
|
8954
9610
|
matchingInfoRef.current = matchingInfo;
|
|
@@ -8977,6 +9633,7 @@ const MatchingRouteManager = ({
|
|
|
8977
9633
|
index,
|
|
8978
9634
|
fallback,
|
|
8979
9635
|
meta,
|
|
9636
|
+
routeParams,
|
|
8980
9637
|
onMatchingInfoChange,
|
|
8981
9638
|
children
|
|
8982
9639
|
}) => {
|
|
@@ -9018,6 +9675,7 @@ const MatchingRouteManager = ({
|
|
|
9018
9675
|
index,
|
|
9019
9676
|
fallback,
|
|
9020
9677
|
meta,
|
|
9678
|
+
routeParams,
|
|
9021
9679
|
indexCandidate,
|
|
9022
9680
|
fallbackCandidate,
|
|
9023
9681
|
candidateSet,
|
|
@@ -9036,6 +9694,7 @@ const initRouteObserver = ({
|
|
|
9036
9694
|
index,
|
|
9037
9695
|
fallback,
|
|
9038
9696
|
meta,
|
|
9697
|
+
routeParams,
|
|
9039
9698
|
indexCandidate,
|
|
9040
9699
|
fallbackCandidate,
|
|
9041
9700
|
candidateSet,
|
|
@@ -9092,6 +9751,12 @@ const initRouteObserver = ({
|
|
|
9092
9751
|
// we have a route and it does not match no need to go further
|
|
9093
9752
|
return null;
|
|
9094
9753
|
}
|
|
9754
|
+
|
|
9755
|
+
// Check if routeParams match current route parameters
|
|
9756
|
+
if (routeParams && !route.matchesParams(routeParams)) {
|
|
9757
|
+
return null; // routeParams don't match, don't render
|
|
9758
|
+
}
|
|
9759
|
+
|
|
9095
9760
|
// we have a route and it is matching
|
|
9096
9761
|
// we search the first matching child to put it in the slot
|
|
9097
9762
|
const matchingChildInfo = findMatchingChildInfo();
|
|
@@ -17322,26 +17987,23 @@ const RouteLink = ({
|
|
|
17322
17987
|
if (!route) {
|
|
17323
17988
|
throw new Error("route prop is required");
|
|
17324
17989
|
}
|
|
17325
|
-
|
|
17990
|
+
useRouteStatus(route);
|
|
17326
17991
|
const url = route.buildUrl(routeParams);
|
|
17327
|
-
let isCurrent;
|
|
17328
|
-
if (routeStatus.exactMatching) {
|
|
17329
|
-
isCurrent = true;
|
|
17330
|
-
} else if (routeStatus.matching) {
|
|
17331
|
-
isCurrent = route.matchesParams(routeParams);
|
|
17332
|
-
} else {
|
|
17333
|
-
isCurrent = false;
|
|
17334
|
-
}
|
|
17335
17992
|
return jsx(Link, {
|
|
17336
17993
|
...rest,
|
|
17337
17994
|
href: url,
|
|
17338
|
-
pseudoState: {
|
|
17339
|
-
":-navi-href-current": isCurrent
|
|
17340
|
-
},
|
|
17341
17995
|
children: children || route.buildRelativeUrl(routeParams)
|
|
17342
17996
|
});
|
|
17343
17997
|
};
|
|
17344
17998
|
|
|
17999
|
+
const rawUrlPartSymbol = Symbol("raw_url_part");
|
|
18000
|
+
const rawUrlPart = (value) => {
|
|
18001
|
+
return {
|
|
18002
|
+
[rawUrlPartSymbol]: true,
|
|
18003
|
+
value,
|
|
18004
|
+
};
|
|
18005
|
+
};
|
|
18006
|
+
|
|
17345
18007
|
installImportMetaCss(import.meta);Object.assign(PSEUDO_CLASSES, {
|
|
17346
18008
|
":-navi-tab-selected": {
|
|
17347
18009
|
attribute: "data-tab-selected"
|
|
@@ -17649,6 +18311,10 @@ const TabRoute = ({
|
|
|
17649
18311
|
padding = 2,
|
|
17650
18312
|
paddingX,
|
|
17651
18313
|
paddingY,
|
|
18314
|
+
paddingLeft,
|
|
18315
|
+
paddingRight,
|
|
18316
|
+
paddingTop,
|
|
18317
|
+
paddingBottom,
|
|
17652
18318
|
alignX,
|
|
17653
18319
|
alignY,
|
|
17654
18320
|
...props
|
|
@@ -17675,6 +18341,10 @@ const TabRoute = ({
|
|
|
17675
18341
|
padding: padding,
|
|
17676
18342
|
paddingX: paddingX,
|
|
17677
18343
|
paddingY: paddingY,
|
|
18344
|
+
paddingLeft: paddingLeft,
|
|
18345
|
+
paddingRight: paddingRight,
|
|
18346
|
+
paddingTop: paddingTop,
|
|
18347
|
+
paddingBottom: paddingBottom,
|
|
17678
18348
|
alignX: alignX,
|
|
17679
18349
|
alignY: alignY,
|
|
17680
18350
|
children: children
|
|
@@ -25548,5 +26218,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
25548
26218
|
})
|
|
25549
26219
|
});
|
|
25550
26220
|
|
|
25551
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, Code, Col, Colgroup, ConstructionSvg, Details, DialogLayout, Editable, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, MessageBox, Paragraph, Radio, RadioList, Route, RouteLink, Routes, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, SearchSvg, Select, SelectionContext, Separator, SettingsSvg, StarSvg, SummaryMarker, Svg, Tab, TabList, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, addCustomMessage, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, updateActions, useActionData, useActionStatus, useCalloutClose, useCellsAndColumns, useConstraintValidityState, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useMatchingRouteInfo, useNavState$1 as useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage };
|
|
26221
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, Code, Col, Colgroup, ConstructionSvg, Details, DialogLayout, Editable, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, MessageBox, Paragraph, Radio, RadioList, Route, RouteLink, Routes, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, SearchSvg, Select, SelectionContext, Separator, SettingsSvg, StarSvg, SummaryMarker, Svg, Tab, TabList, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, addCustomMessage, clearAllRoutes, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, updateActions, useActionData, useActionStatus, useCalloutClose, useCellsAndColumns, useConstraintValidityState, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useMatchingRouteInfo, useNavState$1 as useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage };
|
|
25552
26222
|
//# sourceMappingURL=jsenv_navi.js.map
|