@jsenv/navi 0.16.25 → 0.16.27
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 +226 -184
- package/dist/jsenv_navi.js.map +5 -5
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -2390,22 +2390,26 @@ const generateSignalId = () => {
|
|
|
2390
2390
|
};
|
|
2391
2391
|
|
|
2392
2392
|
/**
|
|
2393
|
-
* Creates an advanced signal with
|
|
2393
|
+
* Creates an advanced signal with dynamic default value, local storage persistence, and validation.
|
|
2394
2394
|
*
|
|
2395
|
-
* The
|
|
2396
|
-
*
|
|
2397
|
-
*
|
|
2398
|
-
* 3. When sourceSignal changes, it will override the current value again
|
|
2395
|
+
* The first parameter can be either a static value or a signal acting as a "dynamic default":
|
|
2396
|
+
* - If a static value: traditional default behavior
|
|
2397
|
+
* - If a signal: acts as a dynamic default that updates the signal ONLY when no explicit value has been set
|
|
2399
2398
|
*
|
|
2400
|
-
*
|
|
2401
|
-
*
|
|
2402
|
-
*
|
|
2403
|
-
*
|
|
2399
|
+
* Dynamic default behavior (when first param is a signal):
|
|
2400
|
+
* 1. Initially takes value from the default signal
|
|
2401
|
+
* 2. When explicitly set (programmatically or via localStorage), the explicit value takes precedence
|
|
2402
|
+
* 3. When default signal changes, it only updates if no explicit value was ever set
|
|
2403
|
+
* 4. Calling reset() or setting to undefined makes the signal use the dynamic default again
|
|
2404
2404
|
*
|
|
2405
|
-
*
|
|
2405
|
+
* This is useful for:
|
|
2406
|
+
* - Backend data that can change but shouldn't override user preferences
|
|
2407
|
+
* - Route parameters with dynamic defaults based on other state
|
|
2408
|
+
* - Cascading configuration where defaults can be updated without losing user customizations
|
|
2409
|
+
*
|
|
2410
|
+
* @param {any|import("@preact/signals").Signal} defaultValue - Static default value OR signal for dynamic default behavior
|
|
2406
2411
|
* @param {Object} [options={}] - Configuration options
|
|
2407
2412
|
* @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.
|
|
2408
|
-
* @param {import("@preact/signals").Signal} [options.sourceSignal] - Source signal to synchronize with. When the source signal changes, this signal will be updated
|
|
2409
2413
|
* @param {boolean} [options.persists=false] - Whether to persist the signal value in localStorage using the signal ID as key
|
|
2410
2414
|
* @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
|
|
2411
2415
|
* @param {Array} [options.oneOf] - Array of valid values for validation. Signal will be marked invalid if value is not in this array
|
|
@@ -2435,14 +2439,21 @@ const generateSignalId = () => {
|
|
|
2435
2439
|
* });
|
|
2436
2440
|
*
|
|
2437
2441
|
* @example
|
|
2438
|
-
* //
|
|
2439
|
-
* const
|
|
2440
|
-
* const
|
|
2442
|
+
* // Dynamic default that doesn't override user choices
|
|
2443
|
+
* const backendTheme = signal("light");
|
|
2444
|
+
* const userTheme = stateSignal(backendTheme, { persists: true });
|
|
2445
|
+
*
|
|
2446
|
+
* // Initially: userTheme.value = "light" (from dynamic default)
|
|
2447
|
+
* // User sets: userTheme.value = "dark" (explicit choice, persisted)
|
|
2448
|
+
* // Backend changes: backendTheme.value = "blue"
|
|
2449
|
+
* // Result: userTheme.value = "dark" (user choice preserved)
|
|
2450
|
+
* // Reset: userTheme.value = undefined; // Now follows dynamic default again
|
|
2441
2451
|
*
|
|
2442
|
-
*
|
|
2443
|
-
* //
|
|
2444
|
-
*
|
|
2445
|
-
*
|
|
2452
|
+
* @example
|
|
2453
|
+
* // Route parameter with dynamic default from parent route
|
|
2454
|
+
* const parentTab = signal("overview");
|
|
2455
|
+
* const childTab = stateSignal(parentTab);
|
|
2456
|
+
* // childTab follows parentTab changes unless explicitly set
|
|
2446
2457
|
*/
|
|
2447
2458
|
const NO_LOCAL_STORAGE = [() => undefined, () => {}, () => {}];
|
|
2448
2459
|
const stateSignal = (defaultValue, options = {}) => {
|
|
@@ -2451,10 +2462,18 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2451
2462
|
type = "string",
|
|
2452
2463
|
oneOf,
|
|
2453
2464
|
autoFix,
|
|
2454
|
-
sourceSignal,
|
|
2455
2465
|
persists = false,
|
|
2456
2466
|
debug,
|
|
2457
2467
|
} = options;
|
|
2468
|
+
|
|
2469
|
+
// Check if defaultValue is a signal (dynamic default) or static value
|
|
2470
|
+
const isDynamicDefault =
|
|
2471
|
+
defaultValue &&
|
|
2472
|
+
typeof defaultValue === "object" &&
|
|
2473
|
+
"value" in defaultValue &&
|
|
2474
|
+
"peek" in defaultValue;
|
|
2475
|
+
const dynamicDefaultSignal = isDynamicDefault ? defaultValue : null;
|
|
2476
|
+
const staticDefaultValue = isDynamicDefault ? undefined : defaultValue;
|
|
2458
2477
|
const signalId = id || generateSignalId();
|
|
2459
2478
|
// Convert numeric IDs to strings for consistency
|
|
2460
2479
|
const signalIdString = String(signalId);
|
|
@@ -2471,202 +2490,200 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2471
2490
|
persists
|
|
2472
2491
|
? valueInLocalStorage(localStorageKey, { type })
|
|
2473
2492
|
: NO_LOCAL_STORAGE;
|
|
2474
|
-
const
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
if (
|
|
2478
|
-
console.debug(
|
|
2479
|
-
`[stateSignal:${signalIdString}] using value from localStorage "${localStorageKey}"=${valueFromLocalStorage}`,
|
|
2480
|
-
);
|
|
2481
|
-
}
|
|
2482
|
-
return valueFromLocalStorage;
|
|
2483
|
-
}
|
|
2484
|
-
if (sourceSignal) {
|
|
2485
|
-
const sourceValue = sourceSignal.peek();
|
|
2486
|
-
if (sourceValue !== undefined) {
|
|
2493
|
+
const getDefaultValue = () => {
|
|
2494
|
+
if (persists) {
|
|
2495
|
+
const valueFromLocalStorage = readFromLocalStorage();
|
|
2496
|
+
if (valueFromLocalStorage !== undefined) {
|
|
2487
2497
|
if (debug) {
|
|
2488
2498
|
console.debug(
|
|
2489
|
-
`[stateSignal:${signalIdString}] using value from
|
|
2499
|
+
`[stateSignal:${signalIdString}] using value from localStorage "${localStorageKey}"=${valueFromLocalStorage}`,
|
|
2490
2500
|
);
|
|
2491
2501
|
}
|
|
2492
|
-
return
|
|
2502
|
+
return valueFromLocalStorage;
|
|
2493
2503
|
}
|
|
2494
2504
|
}
|
|
2505
|
+
if (dynamicDefaultSignal) {
|
|
2506
|
+
const dynamicValue = dynamicDefaultSignal.peek();
|
|
2507
|
+
if (dynamicValue === undefined) {
|
|
2508
|
+
return undefined;
|
|
2509
|
+
}
|
|
2510
|
+
if (debug) {
|
|
2511
|
+
console.debug(
|
|
2512
|
+
`[stateSignal:${signalIdString}] using value from dynamic default signal=${dynamicValue}`,
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
return dynamicValue;
|
|
2516
|
+
}
|
|
2495
2517
|
if (debug) {
|
|
2496
2518
|
console.debug(
|
|
2497
|
-
`[stateSignal:${signalIdString}] using default value=${
|
|
2519
|
+
`[stateSignal:${signalIdString}] using static default value=${staticDefaultValue}`,
|
|
2498
2520
|
);
|
|
2499
2521
|
}
|
|
2500
|
-
return
|
|
2522
|
+
return staticDefaultValue;
|
|
2523
|
+
};
|
|
2524
|
+
const isCustomValue = (value) => {
|
|
2525
|
+
if (value === undefined) {
|
|
2526
|
+
return false;
|
|
2527
|
+
}
|
|
2528
|
+
if (dynamicDefaultSignal) {
|
|
2529
|
+
return value !== dynamicDefaultSignal.peek();
|
|
2530
|
+
}
|
|
2531
|
+
return value !== staticDefaultValue;
|
|
2501
2532
|
};
|
|
2502
2533
|
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
if (debug) {
|
|
2506
|
-
console.debug(
|
|
2507
|
-
`[stateSignal:${signalIdString}] created with initial value=${advancedSignal.peek()}`,
|
|
2508
|
-
{
|
|
2509
|
-
defaultValue,
|
|
2510
|
-
hasSourceSignal: Boolean(sourceSignal),
|
|
2511
|
-
persists,
|
|
2512
|
-
localStorageKey: persists ? localStorageKey : undefined,
|
|
2513
|
-
},
|
|
2514
|
-
);
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
// Set signal ID and create meaningful string representation
|
|
2518
|
-
advancedSignal.__signalId = signalIdString;
|
|
2519
|
-
advancedSignal.toString = () => `{navi_state_signal:${signalIdString}}`;
|
|
2520
|
-
|
|
2521
|
-
// Store signal with its options for later route connection
|
|
2522
|
-
globalSignalRegistry.set(signalIdString, {
|
|
2523
|
-
signal: advancedSignal,
|
|
2524
|
-
options: {
|
|
2525
|
-
getFallbackValue,
|
|
2526
|
-
defaultValue,
|
|
2527
|
-
type,
|
|
2528
|
-
persists,
|
|
2529
|
-
localStorageKey,
|
|
2530
|
-
debug,
|
|
2531
|
-
...options,
|
|
2532
|
-
},
|
|
2533
|
-
});
|
|
2534
|
-
|
|
2534
|
+
// Create signal with initial value: use stored value, or undefined to indicate no explicit value
|
|
2535
|
+
const advancedSignal = signal(getDefaultValue());
|
|
2535
2536
|
const validity = { valid: true };
|
|
2536
2537
|
advancedSignal.validity = validity;
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
// 1.
|
|
2540
|
-
// 2.
|
|
2541
|
-
// 3. default value
|
|
2538
|
+
advancedSignal.__signalId = signalIdString;
|
|
2539
|
+
advancedSignal.toString = () => `{navi_state_signal:${signalIdString}}`;
|
|
2540
|
+
// 1. when signal value changes to undefined, it needs to fallback to default value
|
|
2541
|
+
// 2. when dynamic default changes and signal value is not custom, it needs to update
|
|
2542
2542
|
{
|
|
2543
|
-
let
|
|
2543
|
+
let isFirstRun = true;
|
|
2544
2544
|
effect(() => {
|
|
2545
2545
|
const value = advancedSignal.value;
|
|
2546
|
-
if (
|
|
2547
|
-
|
|
2548
|
-
sourceSignal.value;
|
|
2549
|
-
}
|
|
2550
|
-
if (firstRun) {
|
|
2551
|
-
firstRun = true;
|
|
2546
|
+
if (isFirstRun) {
|
|
2547
|
+
isFirstRun = false;
|
|
2552
2548
|
return;
|
|
2553
2549
|
}
|
|
2554
2550
|
if (value !== undefined) {
|
|
2555
2551
|
return;
|
|
2556
2552
|
}
|
|
2557
|
-
|
|
2553
|
+
const defaultValue = getDefaultValue();
|
|
2554
|
+
if (defaultValue === value) {
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
if (debug) {
|
|
2558
|
+
console.debug(
|
|
2559
|
+
`[stateSignal:${signalIdString}] becomes undefined, reset to ${defaultValue}`,
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
advancedSignal.value = defaultValue;
|
|
2558
2563
|
});
|
|
2559
2564
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
break source_signal_override;
|
|
2565
|
+
dynamic_signal_effect: {
|
|
2566
|
+
if (!dynamicDefaultSignal) {
|
|
2567
|
+
break dynamic_signal_effect;
|
|
2564
2568
|
}
|
|
2565
|
-
|
|
2569
|
+
// here we listen only on the dynamic default signal
|
|
2566
2570
|
let isFirstRun = true;
|
|
2567
|
-
let
|
|
2571
|
+
let dynamicDefaultPreviousValue;
|
|
2568
2572
|
effect(() => {
|
|
2569
|
-
const
|
|
2573
|
+
const value = advancedSignal.peek();
|
|
2574
|
+
const dynamicDefaultValue = dynamicDefaultSignal.value;
|
|
2570
2575
|
if (isFirstRun) {
|
|
2571
|
-
// first run
|
|
2572
2576
|
isFirstRun = false;
|
|
2573
|
-
|
|
2577
|
+
dynamicDefaultPreviousValue = dynamicDefaultValue;
|
|
2574
2578
|
return;
|
|
2575
2579
|
}
|
|
2576
|
-
if (
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
sourceValue,
|
|
2584
|
-
currentValue: advancedSignal.peek(),
|
|
2585
|
-
},
|
|
2586
|
-
);
|
|
2587
|
-
}
|
|
2588
|
-
sourcePreviousValue = undefined;
|
|
2580
|
+
if (value !== dynamicDefaultPreviousValue) {
|
|
2581
|
+
dynamicDefaultPreviousValue = dynamicDefaultValue;
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2584
|
+
dynamicDefaultPreviousValue = dynamicDefaultValue;
|
|
2585
|
+
const defaultValue = getDefaultValue();
|
|
2586
|
+
if (defaultValue === value) {
|
|
2589
2587
|
return;
|
|
2590
2588
|
}
|
|
2591
|
-
// the case we want to support: source signal value changes -> override current value
|
|
2592
2589
|
if (debug) {
|
|
2593
2590
|
console.debug(
|
|
2594
|
-
`[stateSignal:${signalIdString}]
|
|
2595
|
-
{
|
|
2596
|
-
sourcePreviousValue,
|
|
2597
|
-
sourceValue,
|
|
2598
|
-
previousValue: advancedSignal.peek(),
|
|
2599
|
-
},
|
|
2591
|
+
`[stateSignal:${signalIdString}] dynamic default updated, update to ${defaultValue}`,
|
|
2600
2592
|
);
|
|
2601
2593
|
}
|
|
2602
|
-
advancedSignal.value =
|
|
2603
|
-
sourcePreviousValue = sourceValue;
|
|
2594
|
+
advancedSignal.value = defaultValue;
|
|
2604
2595
|
});
|
|
2605
2596
|
}
|
|
2606
|
-
// Read/write into local storage when enabled
|
|
2607
2597
|
persist_in_local_storage: {
|
|
2608
2598
|
if (!localStorageKey) {
|
|
2609
2599
|
break persist_in_local_storage;
|
|
2610
2600
|
}
|
|
2611
2601
|
effect(() => {
|
|
2612
2602
|
const value = advancedSignal.value;
|
|
2613
|
-
if (value
|
|
2603
|
+
if (isCustomValue(value)) {
|
|
2614
2604
|
if (debug) {
|
|
2615
2605
|
console.debug(
|
|
2616
|
-
`[stateSignal:${signalIdString}]
|
|
2606
|
+
`[stateSignal:${signalIdString}] writing into localStorage "${localStorageKey}"=${value}`,
|
|
2617
2607
|
);
|
|
2618
2608
|
}
|
|
2619
|
-
|
|
2609
|
+
writeIntoLocalStorage(value);
|
|
2620
2610
|
} else {
|
|
2621
2611
|
if (debug) {
|
|
2622
2612
|
console.debug(
|
|
2623
|
-
`[stateSignal:${signalIdString}]
|
|
2613
|
+
`[stateSignal:${signalIdString}] removing "${localStorageKey}" from localStorage (value=${value})`,
|
|
2624
2614
|
);
|
|
2625
2615
|
}
|
|
2626
|
-
|
|
2616
|
+
removeFromLocalStorage();
|
|
2627
2617
|
}
|
|
2628
2618
|
});
|
|
2629
2619
|
}
|
|
2630
|
-
// update validity object according to the
|
|
2620
|
+
// update validity object according to the signal value
|
|
2631
2621
|
{
|
|
2632
2622
|
effect(() => {
|
|
2633
|
-
const value = advancedSignal.value;
|
|
2634
2623
|
const wasValid = validity.valid;
|
|
2624
|
+
const value = advancedSignal.value;
|
|
2635
2625
|
updateValidity({ oneOf }, validity, value);
|
|
2636
|
-
if (
|
|
2637
|
-
if (
|
|
2638
|
-
console.debug(`[stateSignal:${signalIdString}] validation failed`, {
|
|
2639
|
-
value,
|
|
2640
|
-
oneOf,
|
|
2641
|
-
hasAutoFix: Boolean(autoFix),
|
|
2642
|
-
});
|
|
2643
|
-
}
|
|
2644
|
-
if (autoFix) {
|
|
2645
|
-
const fixedValue = autoFix();
|
|
2626
|
+
if (validity.valid) {
|
|
2627
|
+
if (!wasValid) {
|
|
2646
2628
|
if (debug) {
|
|
2647
2629
|
console.debug(
|
|
2648
|
-
`[stateSignal:${signalIdString}]
|
|
2649
|
-
{
|
|
2650
|
-
invalidValue: value,
|
|
2651
|
-
fixedValue,
|
|
2652
|
-
},
|
|
2630
|
+
`[stateSignal:${signalIdString}] validation now passes`,
|
|
2631
|
+
{ value },
|
|
2653
2632
|
);
|
|
2654
2633
|
}
|
|
2655
|
-
advancedSignal.value = fixedValue;
|
|
2656
|
-
return;
|
|
2657
2634
|
}
|
|
2658
|
-
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
if (debug) {
|
|
2638
|
+
console.debug(`[stateSignal:${signalIdString}] validation failed`, {
|
|
2639
|
+
value,
|
|
2640
|
+
oneOf,
|
|
2641
|
+
hasAutoFix: Boolean(autoFix),
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
if (autoFix) {
|
|
2645
|
+
const fixedValue = autoFix(value);
|
|
2659
2646
|
if (debug) {
|
|
2660
2647
|
console.debug(
|
|
2661
|
-
`[stateSignal:${signalIdString}]
|
|
2648
|
+
`[stateSignal:${signalIdString}] autoFix applied: ${value} → ${fixedValue}`,
|
|
2662
2649
|
{
|
|
2663
2650
|
value,
|
|
2651
|
+
fixedValue,
|
|
2664
2652
|
},
|
|
2665
2653
|
);
|
|
2666
2654
|
}
|
|
2655
|
+
advancedSignal.value = fixedValue;
|
|
2656
|
+
return;
|
|
2667
2657
|
}
|
|
2668
2658
|
});
|
|
2669
2659
|
}
|
|
2660
|
+
// Store signal with its options (used by route_pattern.js)
|
|
2661
|
+
globalSignalRegistry.set(signalIdString, {
|
|
2662
|
+
signal: advancedSignal,
|
|
2663
|
+
options: {
|
|
2664
|
+
getDefaultValue,
|
|
2665
|
+
defaultValue: staticDefaultValue,
|
|
2666
|
+
dynamicDefaultSignal,
|
|
2667
|
+
isCustomValue,
|
|
2668
|
+
type,
|
|
2669
|
+
persists,
|
|
2670
|
+
localStorageKey,
|
|
2671
|
+
debug,
|
|
2672
|
+
...options,
|
|
2673
|
+
},
|
|
2674
|
+
});
|
|
2675
|
+
if (debug) {
|
|
2676
|
+
console.debug(
|
|
2677
|
+
`[stateSignal:${signalIdString}] created with initial value=${advancedSignal.value}`,
|
|
2678
|
+
{
|
|
2679
|
+
staticDefaultValue,
|
|
2680
|
+
hasDynamicDefault: Boolean(dynamicDefaultSignal),
|
|
2681
|
+
hasStoredValue: persists && readFromLocalStorage() !== undefined,
|
|
2682
|
+
persists,
|
|
2683
|
+
localStorageKey: persists ? localStorageKey : undefined,
|
|
2684
|
+
},
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2670
2687
|
|
|
2671
2688
|
return advancedSignal;
|
|
2672
2689
|
};
|
|
@@ -7831,21 +7848,26 @@ const createRoutePattern = (pattern) => {
|
|
|
7831
7848
|
|
|
7832
7849
|
/**
|
|
7833
7850
|
* Helper: Filter out default values from parameters for cleaner URLs
|
|
7851
|
+
*
|
|
7852
|
+
* This function removes parameters that match their default values (static or dynamic)
|
|
7853
|
+
* while preserving custom values and inherited parameters from ancestor routes.
|
|
7854
|
+
* Parameter inheritance from parent routes is intentional - only default values
|
|
7855
|
+
* for the current route's own parameters are filtered out.
|
|
7834
7856
|
*/
|
|
7835
7857
|
const removeDefaultValues = (params) => {
|
|
7836
7858
|
const filtered = { ...params };
|
|
7837
7859
|
|
|
7838
7860
|
for (const connection of connections) {
|
|
7839
|
-
const { paramName, signal } = connection;
|
|
7840
|
-
const defaultValue = parameterDefaults.get(paramName);
|
|
7861
|
+
const { paramName, signal, options } = connection;
|
|
7841
7862
|
|
|
7842
|
-
if (paramName in filtered
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
) {
|
|
7863
|
+
if (paramName in filtered) {
|
|
7864
|
+
// Parameter is explicitly provided - check if we should remove it
|
|
7865
|
+
if (!options.isCustomValue?.(filtered[paramName])) {
|
|
7866
|
+
// Parameter value is not custom (matches default) - remove it
|
|
7867
|
+
delete filtered[paramName];
|
|
7868
|
+
}
|
|
7869
|
+
} else if (options.isCustomValue?.(signal.value)) {
|
|
7870
|
+
// Parameter not provided but signal has custom value - add it
|
|
7849
7871
|
filtered[paramName] = signal.value;
|
|
7850
7872
|
}
|
|
7851
7873
|
}
|
|
@@ -7864,7 +7886,7 @@ const createRoutePattern = (pattern) => {
|
|
|
7864
7886
|
const effectiveValue = userValue !== undefined ? userValue : signalValue;
|
|
7865
7887
|
return (
|
|
7866
7888
|
effectiveValue === literalValue &&
|
|
7867
|
-
|
|
7889
|
+
conn.options.isCustomValue?.(effectiveValue)
|
|
7868
7890
|
);
|
|
7869
7891
|
});
|
|
7870
7892
|
|
|
@@ -7902,7 +7924,7 @@ const createRoutePattern = (pattern) => {
|
|
|
7902
7924
|
const signalValue = conn.signal?.value;
|
|
7903
7925
|
return (
|
|
7904
7926
|
signalValue === literalValue &&
|
|
7905
|
-
|
|
7927
|
+
conn.options.isCustomValue?.(signalValue)
|
|
7906
7928
|
);
|
|
7907
7929
|
});
|
|
7908
7930
|
|
|
@@ -8045,10 +8067,10 @@ const createRoutePattern = (pattern) => {
|
|
|
8045
8067
|
} else {
|
|
8046
8068
|
const { paramName: name, signal, options } = item;
|
|
8047
8069
|
paramName = name;
|
|
8048
|
-
// Only include
|
|
8070
|
+
// Only include custom parent signal values (not using defaults)
|
|
8049
8071
|
if (
|
|
8050
8072
|
signal?.value === undefined ||
|
|
8051
|
-
signal.value
|
|
8073
|
+
!options.isCustomValue?.(signal.value)
|
|
8052
8074
|
) {
|
|
8053
8075
|
return { isCompatible: true, shouldInclude: false };
|
|
8054
8076
|
}
|
|
@@ -8198,7 +8220,6 @@ const createRoutePattern = (pattern) => {
|
|
|
8198
8220
|
|
|
8199
8221
|
for (const connection of childPatternObj.connections) {
|
|
8200
8222
|
const { paramName, signal, options } = connection;
|
|
8201
|
-
const defaultValue = options.defaultValue;
|
|
8202
8223
|
|
|
8203
8224
|
// Check if parameter was explicitly provided by user
|
|
8204
8225
|
const hasExplicitParam = paramName in params;
|
|
@@ -8207,13 +8228,16 @@ const createRoutePattern = (pattern) => {
|
|
|
8207
8228
|
if (hasExplicitParam) {
|
|
8208
8229
|
// User explicitly provided this parameter - use their value
|
|
8209
8230
|
childParams[paramName] = explicitValue;
|
|
8210
|
-
if (
|
|
8231
|
+
if (
|
|
8232
|
+
explicitValue !== undefined &&
|
|
8233
|
+
options.isCustomValue?.(explicitValue)
|
|
8234
|
+
) {
|
|
8211
8235
|
hasActiveParams = true;
|
|
8212
8236
|
}
|
|
8213
8237
|
} else if (signal?.value !== undefined) {
|
|
8214
8238
|
// No explicit override - use signal value
|
|
8215
8239
|
childParams[paramName] = signal.value;
|
|
8216
|
-
if (signal.value
|
|
8240
|
+
if (options.isCustomValue?.(signal.value)) {
|
|
8217
8241
|
hasActiveParams = true;
|
|
8218
8242
|
}
|
|
8219
8243
|
}
|
|
@@ -8293,7 +8317,7 @@ const createRoutePattern = (pattern) => {
|
|
|
8293
8317
|
// Check if parameters that determine child selection are non-default
|
|
8294
8318
|
// OR if any descendant parameters indicate explicit navigation
|
|
8295
8319
|
for (const connection of connections) {
|
|
8296
|
-
const { paramName } = connection;
|
|
8320
|
+
const { paramName, options } = connection;
|
|
8297
8321
|
const defaultValue = parameterDefaults.get(paramName);
|
|
8298
8322
|
const resolvedValue = resolvedParams[paramName];
|
|
8299
8323
|
const userProvidedParam = paramName in params;
|
|
@@ -8302,9 +8326,10 @@ const createRoutePattern = (pattern) => {
|
|
|
8302
8326
|
// This literal corresponds to a parameter in the parent
|
|
8303
8327
|
if (
|
|
8304
8328
|
userProvidedParam ||
|
|
8305
|
-
(resolvedValue !== undefined &&
|
|
8329
|
+
(resolvedValue !== undefined &&
|
|
8330
|
+
options.isCustomValue?.(resolvedValue))
|
|
8306
8331
|
) {
|
|
8307
|
-
// Parameter was explicitly provided or has
|
|
8332
|
+
// Parameter was explicitly provided or has custom value - child is needed
|
|
8308
8333
|
childSpecificParamsAreDefaults = false;
|
|
8309
8334
|
break;
|
|
8310
8335
|
}
|
|
@@ -8405,7 +8430,7 @@ const createRoutePattern = (pattern) => {
|
|
|
8405
8430
|
// If explicitly undefined, don't include it (which means don't use child route)
|
|
8406
8431
|
} else if (
|
|
8407
8432
|
signal?.value !== undefined &&
|
|
8408
|
-
signal.value
|
|
8433
|
+
options.isCustomValue?.(signal.value)
|
|
8409
8434
|
) {
|
|
8410
8435
|
// No explicit override - use signal value if non-default
|
|
8411
8436
|
baseParams[paramName] = signal.value;
|
|
@@ -8423,7 +8448,6 @@ const createRoutePattern = (pattern) => {
|
|
|
8423
8448
|
// Add parent's signal parameters
|
|
8424
8449
|
for (const connection of parentPatternObj.connections) {
|
|
8425
8450
|
const { paramName, signal, options } = connection;
|
|
8426
|
-
const defaultValue = options.defaultValue;
|
|
8427
8451
|
|
|
8428
8452
|
// Skip if child route already handles this parameter
|
|
8429
8453
|
const childConnection = childPatternObj.connections.find(
|
|
@@ -8438,8 +8462,11 @@ const createRoutePattern = (pattern) => {
|
|
|
8438
8462
|
continue; // Already have this parameter
|
|
8439
8463
|
}
|
|
8440
8464
|
|
|
8441
|
-
// Only include
|
|
8442
|
-
if (
|
|
8465
|
+
// Only include custom signal values (not using defaults)
|
|
8466
|
+
if (
|
|
8467
|
+
signal?.value !== undefined &&
|
|
8468
|
+
options.isCustomValue?.(signal.value)
|
|
8469
|
+
) {
|
|
8443
8470
|
// Skip if parameter is consumed by child's literal path segments
|
|
8444
8471
|
const isConsumedByChildPath = childPatternObj.pattern.segments.some(
|
|
8445
8472
|
(segment) =>
|
|
@@ -8505,10 +8532,9 @@ const createRoutePattern = (pattern) => {
|
|
|
8505
8532
|
|
|
8506
8533
|
if (childConnection) {
|
|
8507
8534
|
const { options } = childConnection;
|
|
8508
|
-
const defaultValue = options.defaultValue;
|
|
8509
8535
|
|
|
8510
|
-
// Only include if it's
|
|
8511
|
-
if (userValue
|
|
8536
|
+
// Only include if it's a custom value (not default)
|
|
8537
|
+
if (options.isCustomValue?.(userValue)) {
|
|
8512
8538
|
baseParams[paramName] = userValue;
|
|
8513
8539
|
} else {
|
|
8514
8540
|
// User provided the default value - complete omission
|
|
@@ -8568,7 +8594,7 @@ const createRoutePattern = (pattern) => {
|
|
|
8568
8594
|
const hasNonDefaultChildParams = (childPatternObj.connections || []).some(
|
|
8569
8595
|
(childConnection) => {
|
|
8570
8596
|
const { signal, options } = childConnection;
|
|
8571
|
-
return signal?.value
|
|
8597
|
+
return options.isCustomValue?.(signal?.value);
|
|
8572
8598
|
},
|
|
8573
8599
|
);
|
|
8574
8600
|
|
|
@@ -8778,13 +8804,15 @@ const createRoutePattern = (pattern) => {
|
|
|
8778
8804
|
// 2. The source route has only query parameters that are non-default
|
|
8779
8805
|
const hasNonDefaultPathParams = connections.some((connection) => {
|
|
8780
8806
|
const resolvedValue = resolvedParams[connection.paramName];
|
|
8781
|
-
|
|
8807
|
+
|
|
8782
8808
|
// Check if this is a query parameter (not in the pattern path)
|
|
8783
8809
|
const isQueryParam = parsedPattern.queryParams.some(
|
|
8784
8810
|
(qp) => qp.name === connection.paramName,
|
|
8785
8811
|
);
|
|
8786
8812
|
// Allow non-default query parameters, but not path parameters
|
|
8787
|
-
return
|
|
8813
|
+
return (
|
|
8814
|
+
!isQueryParam && connection.options.isCustomValue?.(resolvedValue)
|
|
8815
|
+
);
|
|
8788
8816
|
});
|
|
8789
8817
|
|
|
8790
8818
|
if (hasNonDefaultPathParams) {
|
|
@@ -8814,8 +8842,7 @@ const createRoutePattern = (pattern) => {
|
|
|
8814
8842
|
// For non-immediate parents, only allow optimization if all resolved parameters have default values
|
|
8815
8843
|
const hasNonDefaultParameters = connections.some((connection) => {
|
|
8816
8844
|
const resolvedValue = resolvedParams[connection.paramName];
|
|
8817
|
-
|
|
8818
|
-
return resolvedValue !== defaultValue;
|
|
8845
|
+
return connection.options.isCustomValue?.(resolvedValue);
|
|
8819
8846
|
});
|
|
8820
8847
|
|
|
8821
8848
|
if (hasNonDefaultParameters) {
|
|
@@ -9084,13 +9111,12 @@ const createRoutePattern = (pattern) => {
|
|
|
9084
9111
|
// Also check target ancestor's own signal values for parameters not in resolvedParams
|
|
9085
9112
|
for (const connection of targetAncestor.connections) {
|
|
9086
9113
|
const { paramName, signal, options } = connection;
|
|
9087
|
-
const defaultValue = options.defaultValue;
|
|
9088
9114
|
|
|
9089
|
-
// Only include if not already processed and has
|
|
9115
|
+
// Only include if not already processed and has custom value (not default)
|
|
9090
9116
|
if (
|
|
9091
9117
|
!(paramName in ancestorParams) &&
|
|
9092
9118
|
signal?.value !== undefined &&
|
|
9093
|
-
signal.value
|
|
9119
|
+
options.isCustomValue?.(signal.value)
|
|
9094
9120
|
) {
|
|
9095
9121
|
// Don't include path parameters that correspond to literal segments we're optimizing away
|
|
9096
9122
|
const targetParam = targetParams.find((p) => p.name === paramName);
|
|
@@ -9120,13 +9146,12 @@ const createRoutePattern = (pattern) => {
|
|
|
9120
9146
|
while (currentParent) {
|
|
9121
9147
|
for (const connection of currentParent.connections) {
|
|
9122
9148
|
const { paramName, signal, options } = connection;
|
|
9123
|
-
const defaultValue = options.defaultValue;
|
|
9124
9149
|
|
|
9125
|
-
// Only inherit
|
|
9150
|
+
// Only inherit custom values (not defaults) that we don't already have
|
|
9126
9151
|
if (
|
|
9127
9152
|
!(paramName in ancestorParams) &&
|
|
9128
9153
|
signal?.value !== undefined &&
|
|
9129
|
-
signal.value
|
|
9154
|
+
options.isCustomValue?.(signal.value)
|
|
9130
9155
|
) {
|
|
9131
9156
|
// Check if this parameter would be redundant with target ancestor's literal segments
|
|
9132
9157
|
const isRedundant = isParameterRedundantWithLiteralSegments(
|
|
@@ -9201,13 +9226,12 @@ const createRoutePattern = (pattern) => {
|
|
|
9201
9226
|
// Check parent's signal connections for non-default values to inherit
|
|
9202
9227
|
for (const parentConnection of currentParent.connections) {
|
|
9203
9228
|
const { paramName, signal, options } = parentConnection;
|
|
9204
|
-
const defaultValue = options.defaultValue;
|
|
9205
9229
|
|
|
9206
|
-
// Only inherit if we don't have this param and parent has
|
|
9230
|
+
// Only inherit if we don't have this param and parent has custom value (not default)
|
|
9207
9231
|
if (
|
|
9208
9232
|
!(paramName in finalParams) &&
|
|
9209
9233
|
signal?.value !== undefined &&
|
|
9210
|
-
signal.value
|
|
9234
|
+
options.isCustomValue?.(signal.value)
|
|
9211
9235
|
) {
|
|
9212
9236
|
// Don't inherit if parameter corresponds to a literal in our path
|
|
9213
9237
|
const shouldInherit = !isParameterRedundantWithLiteralSegments(
|
|
@@ -9691,6 +9715,21 @@ const extractSearchParams = (urlObj, connections = []) => {
|
|
|
9691
9715
|
/**
|
|
9692
9716
|
* Build query parameters respecting hierarchical order from ancestor patterns
|
|
9693
9717
|
*/
|
|
9718
|
+
/**
|
|
9719
|
+
* Build hierarchical query parameters from pattern hierarchy
|
|
9720
|
+
*
|
|
9721
|
+
* IMPORTANT: This function implements parameter inheritance - child routes inherit
|
|
9722
|
+
* query parameters from their ancestor routes. This is intentional behavior that
|
|
9723
|
+
* allows child routes to preserve context from parent routes.
|
|
9724
|
+
*
|
|
9725
|
+
* For example:
|
|
9726
|
+
* - Parent route: /map/?lon=123
|
|
9727
|
+
* - Child route: /map/isochrone?iso_lon=456
|
|
9728
|
+
* - Final URL: /map/isochrone?lon=123&iso_lon=456
|
|
9729
|
+
*
|
|
9730
|
+
* The child route inherits 'lon' from its parent, maintaining navigation context.
|
|
9731
|
+
* Only parameters that match their defaults (static or dynamic) are omitted.
|
|
9732
|
+
*/
|
|
9694
9733
|
const buildHierarchicalQueryParams = (
|
|
9695
9734
|
parsedPattern,
|
|
9696
9735
|
params,
|
|
@@ -10287,12 +10326,15 @@ const updateRoutes = (
|
|
|
10287
10326
|
if (newMatching) {
|
|
10288
10327
|
// When route matches, sync signal with URL parameter value
|
|
10289
10328
|
// This ensures URL is the source of truth
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
|
|
10329
|
+
const currentValue = stateSignal.peek();
|
|
10330
|
+
if (currentValue !== urlParamValue) {
|
|
10331
|
+
if (debug) {
|
|
10332
|
+
console.debug(
|
|
10333
|
+
`[route] Route matching: setting ${paramName} signal to URL value: ${urlParamValue}`,
|
|
10334
|
+
);
|
|
10335
|
+
}
|
|
10336
|
+
stateSignal.value = urlParamValue;
|
|
10294
10337
|
}
|
|
10295
|
-
stateSignal.value = urlParamValue;
|
|
10296
10338
|
} else {
|
|
10297
10339
|
// Route doesn't match - check if any matching route extracts this parameter
|
|
10298
10340
|
let parameterExtractedByMatchingRoute = false;
|