@jsenv/navi 0.15.10 → 0.16.1
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 +1448 -678
- package/dist/jsenv_navi.js.map +24 -30
- 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,950 @@ 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
|
|
7323
7525
|
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
let
|
|
7526
|
+
// Function to detect signals in route patterns and connect them
|
|
7527
|
+
const detectSignals = (routePattern) => {
|
|
7528
|
+
const signalConnections = [];
|
|
7529
|
+
let updatedPattern = routePattern;
|
|
7328
7530
|
|
|
7329
|
-
//
|
|
7330
|
-
|
|
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;
|
|
7331
7535
|
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
const optionalStartIndex = optionalPartMatch.index;
|
|
7335
|
-
result = result.substring(0, optionalStartIndex);
|
|
7536
|
+
while ((match = signalParamRegex.exec(routePattern)) !== null) {
|
|
7537
|
+
const [fullMatch, prefix, paramName, signalString] = match;
|
|
7336
7538
|
|
|
7337
|
-
//
|
|
7338
|
-
|
|
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;
|
|
7553
|
+
|
|
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;
|
|
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;
|
|
7734
|
+
|
|
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;
|
|
7791
|
+
}
|
|
7792
|
+
}
|
|
7424
7793
|
}
|
|
7794
|
+
}
|
|
7795
|
+
|
|
7796
|
+
// PARENT PARAMETER INHERITANCE: Inherit query parameters from parent patterns
|
|
7797
|
+
// This allows child routes like "/map/isochrone" to inherit "zoom=15" from parent "/map/?zoom=..."
|
|
7798
|
+
const parentPatterns = relationships?.parentPatterns || [];
|
|
7799
|
+
for (const parentPattern of parentPatterns) {
|
|
7800
|
+
const parentPatternData = getPatternData(parentPattern);
|
|
7801
|
+
if (!parentPatternData) continue;
|
|
7802
|
+
|
|
7803
|
+
// Check parent's signal connections for non-default values to inherit
|
|
7804
|
+
for (const parentConnection of parentPatternData.connections) {
|
|
7805
|
+
const { paramName, signal, options } = parentConnection;
|
|
7806
|
+
const defaultValue = options.defaultValue;
|
|
7807
|
+
|
|
7808
|
+
// If we don't already have this parameter and parent signal has non-default value
|
|
7809
|
+
if (
|
|
7810
|
+
!(paramName in finalParams) &&
|
|
7811
|
+
signal?.value !== undefined &&
|
|
7812
|
+
signal.value !== defaultValue
|
|
7813
|
+
) {
|
|
7814
|
+
// Check if this parameter corresponds to a literal segment in our path
|
|
7815
|
+
// E.g., don't inherit "section=analytics" if our path is "/admin/analytics"
|
|
7816
|
+
const shouldInherit = !isParameterRedundantWithLiteralSegments(
|
|
7817
|
+
parsedPattern,
|
|
7818
|
+
parentPatternData.parsedPattern,
|
|
7819
|
+
paramName,
|
|
7820
|
+
signal.value,
|
|
7821
|
+
);
|
|
7425
7822
|
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
// Replace parameter and remove optional marker if present
|
|
7431
|
-
relativeUrl = relativeUrl.replace(`:${key}?`, encodedValue);
|
|
7432
|
-
relativeUrl = relativeUrl.replace(`:${key}`, encodedValue);
|
|
7433
|
-
relativeUrl = relativeUrl.replace(`{${key}}?`, encodedValue);
|
|
7434
|
-
relativeUrl = relativeUrl.replace(`{${key}}`, encodedValue);
|
|
7435
|
-
|
|
7436
|
-
// If the URL did not change we'll maybe delete that param
|
|
7437
|
-
if (relativeUrl === beforeReplace) {
|
|
7438
|
-
extraParamMap.set(key, value);
|
|
7439
|
-
}
|
|
7440
|
-
}
|
|
7441
|
-
// Handle complex optional groups like {/time/:duration}?
|
|
7442
|
-
// Replace parameters inside optional groups and remove the optional marker
|
|
7443
|
-
relativeUrl = relativeUrl.replace(/\{([^}]*)\}\?/g, (match, group) => {
|
|
7444
|
-
let processedGroup = group;
|
|
7445
|
-
let hasReplacements = false;
|
|
7446
|
-
|
|
7447
|
-
// Check if any parameters in the group were provided
|
|
7448
|
-
for (const key of keys) {
|
|
7449
|
-
if (params[key] !== undefined) {
|
|
7450
|
-
const encodedValue = encodeParamValue(params[key], false); // Named parameters encode slashes
|
|
7451
|
-
const paramPattern = new RegExp(`:${key}\\b`);
|
|
7452
|
-
if (paramPattern.test(processedGroup)) {
|
|
7453
|
-
processedGroup = processedGroup.replace(paramPattern, encodedValue);
|
|
7454
|
-
hasReplacements = true;
|
|
7455
|
-
extraParamMap.delete(key);
|
|
7823
|
+
if (shouldInherit) {
|
|
7824
|
+
// Inherit the parent's signal value
|
|
7825
|
+
finalParams[paramName] = signal.value;
|
|
7456
7826
|
}
|
|
7457
7827
|
}
|
|
7458
7828
|
}
|
|
7829
|
+
}
|
|
7459
7830
|
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7831
|
+
if (!parsedPattern.segments) {
|
|
7832
|
+
return "/";
|
|
7833
|
+
}
|
|
7834
|
+
|
|
7835
|
+
// Filter out segments for parameters that are not provided (omitted defaults)
|
|
7836
|
+
const filteredPattern = {
|
|
7837
|
+
...parsedPattern,
|
|
7838
|
+
segments: parsedPattern.segments.filter((segment) => {
|
|
7839
|
+
if (segment.type === "param") {
|
|
7840
|
+
// Only keep parameter segments if we have a value for them
|
|
7841
|
+
return segment.name in finalParams;
|
|
7842
|
+
}
|
|
7843
|
+
// Always keep literal segments
|
|
7844
|
+
return true;
|
|
7845
|
+
}),
|
|
7846
|
+
};
|
|
7847
|
+
|
|
7848
|
+
// Remove trailing slash if we filtered out segments
|
|
7849
|
+
if (
|
|
7850
|
+
filteredPattern.segments.length < parsedPattern.segments.length &&
|
|
7851
|
+
parsedPattern.trailingSlash
|
|
7852
|
+
) {
|
|
7853
|
+
filteredPattern.trailingSlash = false;
|
|
7854
|
+
}
|
|
7855
|
+
|
|
7856
|
+
return buildUrlFromPattern(filteredPattern, finalParams);
|
|
7857
|
+
};
|
|
7858
|
+
|
|
7859
|
+
return {
|
|
7860
|
+
originalPattern: pattern, // Return the original pattern string
|
|
7861
|
+
pattern: parsedPattern,
|
|
7862
|
+
cleanPattern, // Return the clean pattern string
|
|
7863
|
+
connections, // Return signal connections along with pattern
|
|
7864
|
+
applyOn,
|
|
7865
|
+
buildUrl,
|
|
7866
|
+
buildMostPreciseUrl,
|
|
7867
|
+
resolveParams,
|
|
7868
|
+
};
|
|
7869
|
+
};
|
|
7870
|
+
|
|
7871
|
+
/**
|
|
7872
|
+
* Parse a route pattern string into structured segments
|
|
7873
|
+
*/
|
|
7874
|
+
const parsePattern = (pattern, parameterDefaults = new Map()) => {
|
|
7875
|
+
// Handle root route
|
|
7876
|
+
if (pattern === "/") {
|
|
7877
|
+
return {
|
|
7878
|
+
original: pattern,
|
|
7879
|
+
segments: [],
|
|
7880
|
+
trailingSlash: true,
|
|
7881
|
+
wildcard: false,
|
|
7882
|
+
queryParams: [],
|
|
7883
|
+
};
|
|
7884
|
+
}
|
|
7885
|
+
|
|
7886
|
+
// Separate path and query portions
|
|
7887
|
+
const [pathPortion, queryPortion] = pattern.split("?");
|
|
7888
|
+
|
|
7889
|
+
// Parse query parameters if present
|
|
7890
|
+
const queryParams = [];
|
|
7891
|
+
if (queryPortion) {
|
|
7892
|
+
// Split query parameters by & and parse each one
|
|
7893
|
+
const querySegments = queryPortion.split("&");
|
|
7894
|
+
for (const querySegment of querySegments) {
|
|
7895
|
+
if (querySegment.includes("=")) {
|
|
7896
|
+
// Parameter with potential value: tab=value or just tab
|
|
7897
|
+
const [paramName, paramValue] = querySegment.split("=", 2);
|
|
7898
|
+
queryParams.push({
|
|
7899
|
+
type: "query_param",
|
|
7900
|
+
name: paramName,
|
|
7901
|
+
hasDefaultValue: paramValue === undefined, // No value means it uses signal/default
|
|
7902
|
+
});
|
|
7903
|
+
} else {
|
|
7904
|
+
// Parameter without value: tab
|
|
7905
|
+
queryParams.push({
|
|
7906
|
+
type: "query_param",
|
|
7907
|
+
name: querySegment,
|
|
7908
|
+
hasDefaultValue: true,
|
|
7909
|
+
});
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
}
|
|
7913
|
+
|
|
7914
|
+
// Remove leading slash for processing the path portion
|
|
7915
|
+
let cleanPattern = pathPortion.startsWith("/")
|
|
7916
|
+
? pathPortion.slice(1)
|
|
7917
|
+
: pathPortion;
|
|
7918
|
+
|
|
7919
|
+
// Check for wildcard first
|
|
7920
|
+
const wildcard = cleanPattern.endsWith("*");
|
|
7921
|
+
if (wildcard) {
|
|
7922
|
+
cleanPattern = cleanPattern.slice(0, -1); // Remove *
|
|
7923
|
+
// Also remove the slash before * if present
|
|
7924
|
+
if (cleanPattern.endsWith("/")) {
|
|
7925
|
+
cleanPattern = cleanPattern.slice(0, -1);
|
|
7926
|
+
}
|
|
7927
|
+
}
|
|
7928
|
+
|
|
7929
|
+
// Check for trailing slash (after wildcard check)
|
|
7930
|
+
const trailingSlash = !wildcard && pathPortion.endsWith("/");
|
|
7931
|
+
if (trailingSlash) {
|
|
7932
|
+
cleanPattern = cleanPattern.slice(0, -1); // Remove trailing /
|
|
7933
|
+
}
|
|
7934
|
+
|
|
7935
|
+
// Split into segments (filter out empty segments)
|
|
7936
|
+
const segmentStrings = cleanPattern
|
|
7937
|
+
? cleanPattern.split("/").filter((s) => s !== "")
|
|
7938
|
+
: [];
|
|
7939
|
+
const segments = segmentStrings.map((seg, index) => {
|
|
7940
|
+
if (seg.startsWith(":")) {
|
|
7941
|
+
// Parameter segment
|
|
7942
|
+
const paramName = seg.slice(1).replace("?", ""); // Remove : and optional ?
|
|
7943
|
+
const isOptional = seg.endsWith("?") || parameterDefaults.has(paramName);
|
|
7944
|
+
|
|
7945
|
+
return {
|
|
7946
|
+
type: "param",
|
|
7947
|
+
name: paramName,
|
|
7948
|
+
optional: isOptional,
|
|
7949
|
+
index,
|
|
7950
|
+
};
|
|
7951
|
+
}
|
|
7952
|
+
// Literal segment
|
|
7953
|
+
return {
|
|
7954
|
+
type: "literal",
|
|
7955
|
+
value: seg,
|
|
7956
|
+
index,
|
|
7957
|
+
};
|
|
7958
|
+
});
|
|
7959
|
+
|
|
7960
|
+
return {
|
|
7961
|
+
original: pattern,
|
|
7962
|
+
segments,
|
|
7963
|
+
queryParams, // Add query parameters to the parsed pattern
|
|
7964
|
+
trailingSlash,
|
|
7965
|
+
wildcard,
|
|
7966
|
+
};
|
|
7967
|
+
};
|
|
7968
|
+
|
|
7969
|
+
/**
|
|
7970
|
+
* Check if a literal segment can be treated as optional based on parent route signal defaults
|
|
7971
|
+
*/
|
|
7972
|
+
const checkIfLiteralCanBeOptional = (literalValue, patternRegistry) => {
|
|
7973
|
+
// Look through all registered patterns for parent patterns that might have this literal as a default
|
|
7974
|
+
for (const [, patternData] of patternRegistry) {
|
|
7975
|
+
// Check if any signal connection has this literal value as default
|
|
7976
|
+
for (const connection of patternData.connections) {
|
|
7977
|
+
if (connection.options.defaultValue === literalValue) {
|
|
7978
|
+
return true; // This literal matches a signal default, so it can be optional
|
|
7979
|
+
}
|
|
7980
|
+
}
|
|
7981
|
+
}
|
|
7982
|
+
return false;
|
|
7983
|
+
};
|
|
7984
|
+
|
|
7985
|
+
/**
|
|
7986
|
+
* Match a URL against a parsed pattern
|
|
7987
|
+
*/
|
|
7988
|
+
const matchUrl = (parsedPattern, url, { parameterDefaults, baseUrl }) => {
|
|
7989
|
+
// Parse the URL
|
|
7990
|
+
const urlObj = new URL(url, baseUrl);
|
|
7991
|
+
let pathname = urlObj.pathname;
|
|
7992
|
+
const originalPathname = pathname; // Store original pathname before baseUrl processing
|
|
7993
|
+
|
|
7994
|
+
// If baseUrl is provided, calculate the pathname relative to the baseUrl's directory
|
|
7995
|
+
if (baseUrl) {
|
|
7996
|
+
const baseUrlObj = new URL(baseUrl);
|
|
7997
|
+
// if the base url is a file, we want to be relative to the directory containing that file
|
|
7998
|
+
const baseDir = baseUrlObj.pathname.endsWith("/")
|
|
7999
|
+
? baseUrlObj.pathname
|
|
8000
|
+
: baseUrlObj.pathname.substring(0, baseUrlObj.pathname.lastIndexOf("/"));
|
|
8001
|
+
if (pathname.startsWith(baseDir)) {
|
|
8002
|
+
pathname = pathname.slice(baseDir.length);
|
|
8003
|
+
}
|
|
8004
|
+
}
|
|
8005
|
+
|
|
8006
|
+
// Handle root route - only matches empty path or just "/"
|
|
8007
|
+
// OR when URL exactly matches baseUrl (treating baseUrl as root)
|
|
8008
|
+
if (parsedPattern.segments.length === 0) {
|
|
8009
|
+
if (pathname === "/" || pathname === "") {
|
|
8010
|
+
return extractSearchParams(urlObj);
|
|
8011
|
+
}
|
|
8012
|
+
|
|
8013
|
+
// Special case: if URL exactly matches baseUrl, treat as root route
|
|
8014
|
+
if (baseUrl) {
|
|
8015
|
+
const baseUrlObj = new URL(baseUrl);
|
|
8016
|
+
if (originalPathname === baseUrlObj.pathname) {
|
|
8017
|
+
return extractSearchParams(urlObj);
|
|
8018
|
+
}
|
|
8019
|
+
}
|
|
8020
|
+
|
|
8021
|
+
return null;
|
|
8022
|
+
}
|
|
8023
|
+
|
|
8024
|
+
// Remove leading slash and split into segments
|
|
8025
|
+
let urlSegments = pathname.startsWith("/")
|
|
8026
|
+
? pathname
|
|
8027
|
+
.slice(1)
|
|
8028
|
+
.split("/")
|
|
8029
|
+
.filter((s) => s !== "")
|
|
8030
|
+
: pathname.split("/").filter((s) => s !== "");
|
|
8031
|
+
|
|
8032
|
+
// Handle trailing slash flexibility: if pattern has trailing slash but URL doesn't (or vice versa)
|
|
8033
|
+
// and we're at the end of segments, allow the match
|
|
8034
|
+
const urlHasTrailingSlash = pathname.endsWith("/") && pathname !== "/";
|
|
8035
|
+
const patternHasTrailingSlash = parsedPattern.trailingSlash;
|
|
8036
|
+
|
|
8037
|
+
const params = {};
|
|
8038
|
+
let urlSegmentIndex = 0;
|
|
8039
|
+
|
|
8040
|
+
// Process each pattern segment
|
|
8041
|
+
for (let i = 0; i < parsedPattern.segments.length; i++) {
|
|
8042
|
+
const patternSeg = parsedPattern.segments[i];
|
|
8043
|
+
|
|
8044
|
+
if (patternSeg.type === "literal") {
|
|
8045
|
+
// Check if URL has this segment
|
|
8046
|
+
if (urlSegmentIndex >= urlSegments.length) {
|
|
8047
|
+
// URL is too short for this literal segment
|
|
8048
|
+
// Check if this literal segment can be treated as optional based on parent route defaults
|
|
8049
|
+
const canBeOptional = checkIfLiteralCanBeOptional(
|
|
8050
|
+
patternSeg.value,
|
|
8051
|
+
patternRegistry,
|
|
8052
|
+
);
|
|
8053
|
+
if (canBeOptional) {
|
|
8054
|
+
// Skip this literal segment, don't increment urlSegmentIndex
|
|
8055
|
+
continue;
|
|
8056
|
+
}
|
|
8057
|
+
return null; // URL too short and literal is not optional
|
|
8058
|
+
}
|
|
8059
|
+
|
|
8060
|
+
const urlSeg = urlSegments[urlSegmentIndex];
|
|
8061
|
+
if (urlSeg !== patternSeg.value) {
|
|
8062
|
+
// Literal mismatch - this route doesn't match this URL
|
|
8063
|
+
return null;
|
|
8064
|
+
}
|
|
8065
|
+
urlSegmentIndex++;
|
|
8066
|
+
} else if (patternSeg.type === "param") {
|
|
8067
|
+
// Parameter segment
|
|
8068
|
+
if (urlSegmentIndex >= urlSegments.length) {
|
|
8069
|
+
// No URL segment for this parameter
|
|
8070
|
+
if (patternSeg.optional) {
|
|
8071
|
+
// Optional parameter - use default if available
|
|
8072
|
+
const defaultValue = parameterDefaults.get(patternSeg.name);
|
|
8073
|
+
if (defaultValue !== undefined) {
|
|
8074
|
+
params[patternSeg.name] = defaultValue;
|
|
8075
|
+
}
|
|
8076
|
+
continue;
|
|
8077
|
+
}
|
|
8078
|
+
// Required parameter missing - but check if we can use trailing slash logic
|
|
8079
|
+
// If this is the last segment and we have a trailing slash difference, it might still match
|
|
8080
|
+
const isLastSegment = i === parsedPattern.segments.length - 1;
|
|
8081
|
+
if (isLastSegment && patternHasTrailingSlash && !urlHasTrailingSlash) {
|
|
8082
|
+
// Pattern expects trailing slash segment, URL doesn't have it
|
|
8083
|
+
const defaultValue = parameterDefaults.get(patternSeg.name);
|
|
8084
|
+
if (defaultValue !== undefined) {
|
|
8085
|
+
params[patternSeg.name] = defaultValue;
|
|
8086
|
+
continue;
|
|
7473
8087
|
}
|
|
7474
8088
|
}
|
|
8089
|
+
return null; // Required parameter missing
|
|
7475
8090
|
}
|
|
7476
8091
|
|
|
7477
|
-
//
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
8092
|
+
// Capture URL segment as parameter value
|
|
8093
|
+
const urlSeg = urlSegments[urlSegmentIndex];
|
|
8094
|
+
params[patternSeg.name] = decodeURIComponent(urlSeg);
|
|
8095
|
+
urlSegmentIndex++;
|
|
8096
|
+
}
|
|
7481
8097
|
}
|
|
7482
8098
|
|
|
7483
|
-
//
|
|
7484
|
-
|
|
8099
|
+
// Check for remaining URL segments
|
|
8100
|
+
// Patterns with trailing slashes can match additional URL segments (like wildcards)
|
|
8101
|
+
// Patterns without trailing slashes should match exactly (unless they're wildcards)
|
|
8102
|
+
if (
|
|
8103
|
+
!parsedPattern.wildcard &&
|
|
8104
|
+
!parsedPattern.trailingSlash &&
|
|
8105
|
+
urlSegmentIndex < urlSegments.length
|
|
8106
|
+
) {
|
|
8107
|
+
return null; // Pattern without trailing slash should not match extra segments
|
|
8108
|
+
}
|
|
8109
|
+
// If pattern has trailing slash or wildcard, allow extra segments (no additional check needed)
|
|
8110
|
+
|
|
8111
|
+
// Add search parameters
|
|
8112
|
+
const searchParams = extractSearchParams(urlObj);
|
|
8113
|
+
Object.assign(params, searchParams);
|
|
8114
|
+
|
|
8115
|
+
// Apply remaining parameter defaults for unmatched parameters
|
|
8116
|
+
for (const [paramName, defaultValue] of parameterDefaults) {
|
|
8117
|
+
if (!(paramName in params)) {
|
|
8118
|
+
params[paramName] = defaultValue;
|
|
8119
|
+
}
|
|
8120
|
+
}
|
|
8121
|
+
|
|
8122
|
+
return params;
|
|
8123
|
+
};
|
|
8124
|
+
|
|
8125
|
+
/**
|
|
8126
|
+
* Extract search parameters from URL
|
|
8127
|
+
*/
|
|
8128
|
+
const extractSearchParams = (urlObj) => {
|
|
8129
|
+
const params = {};
|
|
8130
|
+
for (const [key, value] of urlObj.searchParams) {
|
|
8131
|
+
params[key] = value;
|
|
8132
|
+
}
|
|
8133
|
+
return params;
|
|
8134
|
+
};
|
|
8135
|
+
|
|
8136
|
+
/**
|
|
8137
|
+
* Build a URL from a pattern and parameters
|
|
8138
|
+
*/
|
|
8139
|
+
const buildUrlFromPattern = (parsedPattern, params = {}) => {
|
|
8140
|
+
if (parsedPattern.segments.length === 0) {
|
|
8141
|
+
// Root route
|
|
8142
|
+
const searchParams = new URLSearchParams();
|
|
8143
|
+
for (const [key, value] of Object.entries(params)) {
|
|
8144
|
+
if (value !== undefined) {
|
|
8145
|
+
searchParams.set(key, value);
|
|
8146
|
+
}
|
|
8147
|
+
}
|
|
8148
|
+
const search = searchParams.toString();
|
|
8149
|
+
return `/${search ? `?${search}` : ""}`;
|
|
8150
|
+
}
|
|
7485
8151
|
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
8152
|
+
const segments = [];
|
|
8153
|
+
|
|
8154
|
+
for (const patternSeg of parsedPattern.segments) {
|
|
8155
|
+
if (patternSeg.type === "literal") {
|
|
8156
|
+
segments.push(patternSeg.value);
|
|
8157
|
+
} else if (patternSeg.type === "param") {
|
|
8158
|
+
const value = params[patternSeg.name];
|
|
8159
|
+
|
|
8160
|
+
// If value is provided, include it
|
|
8161
|
+
if (value !== undefined) {
|
|
8162
|
+
segments.push(encodeURIComponent(value));
|
|
8163
|
+
} else if (!patternSeg.optional) {
|
|
8164
|
+
// For required parameters without values, keep the placeholder
|
|
8165
|
+
segments.push(`:${patternSeg.name}`);
|
|
7496
8166
|
}
|
|
7497
|
-
|
|
7498
|
-
|
|
8167
|
+
// Optional parameters with undefined values are omitted
|
|
8168
|
+
}
|
|
8169
|
+
}
|
|
8170
|
+
|
|
8171
|
+
let path = `/${segments.join("/")}`;
|
|
8172
|
+
|
|
8173
|
+
// Handle trailing slash - only add if it serves a purpose
|
|
8174
|
+
if (parsedPattern.trailingSlash && !path.endsWith("/") && path !== "/") {
|
|
8175
|
+
// Only add trailing slash if the original pattern suggests there could be more content
|
|
8176
|
+
// For patterns like "/admin/:section/" where the slash is at the very end,
|
|
8177
|
+
// it's not needed in the generated URL if there are no more segments
|
|
8178
|
+
const lastSegment =
|
|
8179
|
+
parsedPattern.segments[parsedPattern.segments.length - 1];
|
|
8180
|
+
const hasMorePotentialContent =
|
|
8181
|
+
parsedPattern.wildcard || (lastSegment && lastSegment.type === "literal"); // Only add slash after literals, not parameters
|
|
8182
|
+
|
|
8183
|
+
if (hasMorePotentialContent) {
|
|
8184
|
+
path += "/";
|
|
8185
|
+
}
|
|
8186
|
+
} else if (
|
|
8187
|
+
!parsedPattern.trailingSlash &&
|
|
8188
|
+
path.endsWith("/") &&
|
|
8189
|
+
path !== "/"
|
|
8190
|
+
) {
|
|
8191
|
+
// Remove trailing slash for patterns without trailing slash
|
|
8192
|
+
path = path.slice(0, -1);
|
|
8193
|
+
}
|
|
8194
|
+
|
|
8195
|
+
// Check if we'll have query parameters to decide on trailing slash removal
|
|
8196
|
+
const willHaveQueryParams =
|
|
8197
|
+
parsedPattern.queryParams?.some((qp) => {
|
|
8198
|
+
const value = params[qp.name];
|
|
8199
|
+
return value !== undefined;
|
|
8200
|
+
}) ||
|
|
8201
|
+
Object.entries(params).some(([key, value]) => {
|
|
8202
|
+
const isPathParam = parsedPattern.segments.some(
|
|
8203
|
+
(s) => s.type === "param" && s.name === key,
|
|
8204
|
+
);
|
|
8205
|
+
const isQueryParam = parsedPattern.queryParams?.some(
|
|
8206
|
+
(qp) => qp.name === key,
|
|
8207
|
+
);
|
|
8208
|
+
return value !== undefined && !isPathParam && !isQueryParam;
|
|
7499
8209
|
});
|
|
8210
|
+
|
|
8211
|
+
// Remove trailing slash when we have query params for prettier URLs
|
|
8212
|
+
if (willHaveQueryParams && path.endsWith("/") && path !== "/") {
|
|
8213
|
+
path = path.slice(0, -1);
|
|
8214
|
+
}
|
|
8215
|
+
|
|
8216
|
+
// Add search parameters
|
|
8217
|
+
const pathParamNames = new Set(
|
|
8218
|
+
parsedPattern.segments.filter((s) => s.type === "param").map((s) => s.name),
|
|
8219
|
+
);
|
|
8220
|
+
|
|
8221
|
+
// Add query parameters defined in the pattern first
|
|
8222
|
+
const queryParamNames = new Set();
|
|
8223
|
+
const searchParams = new URLSearchParams();
|
|
8224
|
+
|
|
8225
|
+
// Handle pattern-defined query parameters (from ?tab, &lon, etc.)
|
|
8226
|
+
if (parsedPattern.queryParams) {
|
|
8227
|
+
for (const queryParam of parsedPattern.queryParams) {
|
|
8228
|
+
const paramName = queryParam.name;
|
|
8229
|
+
queryParamNames.add(paramName);
|
|
8230
|
+
|
|
8231
|
+
const value = params[paramName];
|
|
8232
|
+
if (value !== undefined) {
|
|
8233
|
+
searchParams.set(paramName, value);
|
|
8234
|
+
}
|
|
8235
|
+
// If no value provided, don't add the parameter to keep URLs clean
|
|
8236
|
+
}
|
|
8237
|
+
}
|
|
8238
|
+
|
|
8239
|
+
// Add remaining parameters as additional query parameters (excluding path and pattern query params)
|
|
8240
|
+
for (const [key, value] of Object.entries(params)) {
|
|
8241
|
+
if (
|
|
8242
|
+
!pathParamNames.has(key) &&
|
|
8243
|
+
!queryParamNames.has(key) &&
|
|
8244
|
+
value !== undefined
|
|
8245
|
+
) {
|
|
8246
|
+
searchParams.set(key, value);
|
|
8247
|
+
}
|
|
7500
8248
|
}
|
|
7501
8249
|
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
8250
|
+
const search = searchParams.toString();
|
|
8251
|
+
|
|
8252
|
+
// No longer handle trailing slash inheritance here
|
|
8253
|
+
|
|
8254
|
+
return path + (search ? `?${search}` : "");
|
|
8255
|
+
};
|
|
8256
|
+
|
|
8257
|
+
/**
|
|
8258
|
+
* Check if childPattern is a child route of parentPattern
|
|
8259
|
+
* E.g., "/admin/settings/:tab" is a child of "/admin/:section/"
|
|
8260
|
+
* Also, "/admin/?tab=something" is a child of "/admin/"
|
|
8261
|
+
*/
|
|
8262
|
+
const isChildPattern = (childPattern, parentPattern) => {
|
|
8263
|
+
// Split path and query parts
|
|
8264
|
+
const [childPath, childQuery] = childPattern.split("?");
|
|
8265
|
+
const [parentPath, parentQuery] = parentPattern.split("?");
|
|
8266
|
+
|
|
8267
|
+
// Remove trailing slashes for path comparison
|
|
8268
|
+
const cleanChild = childPath.replace(/\/$/, "");
|
|
8269
|
+
const cleanParent = parentPath.replace(/\/$/, "");
|
|
8270
|
+
|
|
8271
|
+
// CASE 1: Same path, child has query params, parent doesn't
|
|
8272
|
+
// E.g., "/admin/?tab=something" is child of "/admin/"
|
|
8273
|
+
if (cleanChild === cleanParent && childQuery && !parentQuery) {
|
|
8274
|
+
return true;
|
|
8275
|
+
}
|
|
8276
|
+
|
|
8277
|
+
// CASE 2: Traditional path-based child relationship
|
|
8278
|
+
// Convert patterns to comparable segments for proper comparison
|
|
8279
|
+
const childSegments = cleanChild.split("/").filter((s) => s);
|
|
8280
|
+
const parentSegments = cleanParent.split("/").filter((s) => s);
|
|
8281
|
+
|
|
8282
|
+
// Child must have at least as many segments as parent
|
|
8283
|
+
if (childSegments.length < parentSegments.length) {
|
|
8284
|
+
return false;
|
|
8285
|
+
}
|
|
8286
|
+
|
|
8287
|
+
let hasMoreSpecificSegment = false;
|
|
8288
|
+
|
|
8289
|
+
// Check if parent segments match child segments (allowing for parameters)
|
|
8290
|
+
for (let i = 0; i < parentSegments.length; i++) {
|
|
8291
|
+
const parentSeg = parentSegments[i];
|
|
8292
|
+
const childSeg = childSegments[i];
|
|
8293
|
+
|
|
8294
|
+
// If parent has parameter, child can have anything in that position
|
|
8295
|
+
if (parentSeg.startsWith(":")) {
|
|
8296
|
+
// Child is more specific if it has a literal value for a parent parameter
|
|
8297
|
+
// But if child also starts with ":", it's also a parameter (not more specific)
|
|
8298
|
+
if (!childSeg.startsWith(":")) {
|
|
8299
|
+
hasMoreSpecificSegment = true;
|
|
8300
|
+
}
|
|
8301
|
+
continue;
|
|
8302
|
+
}
|
|
8303
|
+
|
|
8304
|
+
// If parent has literal, child must match exactly
|
|
8305
|
+
if (parentSeg !== childSeg) {
|
|
8306
|
+
return false;
|
|
8307
|
+
}
|
|
8308
|
+
}
|
|
8309
|
+
|
|
8310
|
+
// Child must be more specific (more segments OR more specific segments)
|
|
8311
|
+
return childSegments.length > parentSegments.length || hasMoreSpecificSegment;
|
|
8312
|
+
};
|
|
8313
|
+
|
|
8314
|
+
/**
|
|
8315
|
+
* Check if a parameter is redundant because the child pattern already has it as a literal segment
|
|
8316
|
+
* E.g., parameter "section" is redundant for pattern "/admin/settings/:tab" because "settings" is literal
|
|
8317
|
+
*/
|
|
8318
|
+
const isParameterRedundantWithLiteralSegments = (
|
|
8319
|
+
childPattern,
|
|
8320
|
+
parentPattern,
|
|
8321
|
+
paramName,
|
|
8322
|
+
) => {
|
|
8323
|
+
// Find which segment position corresponds to this parameter in the parent
|
|
8324
|
+
let paramSegmentIndex = -1;
|
|
8325
|
+
for (let i = 0; i < parentPattern.segments.length; i++) {
|
|
8326
|
+
const segment = parentPattern.segments[i];
|
|
8327
|
+
if (segment.type === "param" && segment.name === paramName) {
|
|
8328
|
+
paramSegmentIndex = i;
|
|
8329
|
+
break;
|
|
8330
|
+
}
|
|
7508
8331
|
}
|
|
7509
8332
|
|
|
7510
|
-
//
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
relativeUrl = relativeUrl.slice(0, -1);
|
|
8333
|
+
// If parameter not found in parent segments, it's not redundant with path
|
|
8334
|
+
if (paramSegmentIndex === -1) {
|
|
8335
|
+
return false;
|
|
7514
8336
|
}
|
|
7515
8337
|
|
|
7516
|
-
//
|
|
7517
|
-
if (
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
// Handle boolean values - if true, just add the key without value
|
|
7524
|
-
if (value === true) {
|
|
7525
|
-
searchParamPairs.push(encodedKey);
|
|
7526
|
-
} else {
|
|
7527
|
-
const encodedValue = encodeParamValue(value, false); // Search params encode slashes
|
|
7528
|
-
searchParamPairs.push(`${encodedKey}=${encodedValue}`);
|
|
7529
|
-
}
|
|
7530
|
-
}
|
|
7531
|
-
}
|
|
7532
|
-
if (searchParamPairs.length > 0) {
|
|
7533
|
-
const searchString = searchParamPairs.join("&");
|
|
7534
|
-
relativeUrl += (relativeUrl.includes("?") ? "&" : "?") + searchString;
|
|
7535
|
-
}
|
|
7536
|
-
} else if (extraParamEffect === "warn") {
|
|
7537
|
-
console.warn(
|
|
7538
|
-
`Unknown parameters given to "${urlPatternInput}":`,
|
|
7539
|
-
Array.from(extraParamMap.keys()),
|
|
7540
|
-
);
|
|
8338
|
+
// Check if child has a literal segment at the same position
|
|
8339
|
+
if (childPattern.segments.length > paramSegmentIndex) {
|
|
8340
|
+
const childSegment = childPattern.segments[paramSegmentIndex];
|
|
8341
|
+
if (childSegment.type === "literal") {
|
|
8342
|
+
// Child has a literal segment where parent has parameter
|
|
8343
|
+
// This means the child is more specific and shouldn't inherit this parameter
|
|
8344
|
+
return true; // Redundant - child already specifies this position with a literal
|
|
7541
8345
|
}
|
|
7542
8346
|
}
|
|
7543
8347
|
|
|
7544
|
-
|
|
7545
|
-
if (stringQueryParams) {
|
|
7546
|
-
relativeUrl += (relativeUrl.includes("?") ? "&" : "?") + stringQueryParams;
|
|
7547
|
-
}
|
|
7548
|
-
|
|
7549
|
-
return {
|
|
7550
|
-
relativeUrl,
|
|
7551
|
-
hasRawUrlPartWithInvalidChars,
|
|
7552
|
-
};
|
|
8348
|
+
return false;
|
|
7553
8349
|
};
|
|
7554
8350
|
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
});
|
|
8351
|
+
/**
|
|
8352
|
+
* Register all patterns at once and build their relationships
|
|
8353
|
+
*/
|
|
8354
|
+
const setupPatterns = (patternDefinitions) => {
|
|
8355
|
+
// Clear existing patterns
|
|
8356
|
+
patternRegistry.clear();
|
|
8357
|
+
patternRelationships.clear();
|
|
7563
8358
|
|
|
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
|
-
});
|
|
8359
|
+
// Phase 1: Register all patterns
|
|
8360
|
+
for (const [key, urlPatternRaw] of Object.entries(patternDefinitions)) {
|
|
8361
|
+
const [cleanPattern, connections] = detectSignals(urlPatternRaw);
|
|
8362
|
+
const parsedPattern = parsePattern(cleanPattern);
|
|
7579
8363
|
|
|
7580
|
-
|
|
8364
|
+
const patternData = {
|
|
8365
|
+
key,
|
|
8366
|
+
urlPatternRaw,
|
|
8367
|
+
cleanPattern,
|
|
8368
|
+
connections,
|
|
8369
|
+
parsedPattern,
|
|
8370
|
+
childPatterns: [],
|
|
8371
|
+
parentPatterns: [],
|
|
8372
|
+
};
|
|
7581
8373
|
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
if (match) {
|
|
7585
|
-
return extractParams(match, url);
|
|
7586
|
-
}
|
|
8374
|
+
patternRegistry.set(urlPatternRaw, patternData);
|
|
8375
|
+
}
|
|
7587
8376
|
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
const pathname = urlObj.pathname;
|
|
8377
|
+
// Phase 2: Build relationships between all patterns
|
|
8378
|
+
const allPatterns = Array.from(patternRegistry.keys());
|
|
7591
8379
|
|
|
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
|
-
};
|
|
8380
|
+
for (const currentPattern of allPatterns) {
|
|
8381
|
+
const currentData = patternRegistry.get(currentPattern);
|
|
7614
8382
|
|
|
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;
|
|
8383
|
+
for (const otherPattern of allPatterns) {
|
|
8384
|
+
if (currentPattern === otherPattern) continue;
|
|
8385
|
+
|
|
8386
|
+
const otherData = patternRegistry.get(otherPattern);
|
|
8387
|
+
|
|
8388
|
+
// Check if current pattern is a child of other pattern using clean patterns
|
|
8389
|
+
if (isChildPattern(currentData.cleanPattern, otherData.cleanPattern)) {
|
|
8390
|
+
currentData.parentPatterns.push(otherPattern);
|
|
8391
|
+
otherData.childPatterns.push(currentPattern);
|
|
7653
8392
|
}
|
|
7654
8393
|
}
|
|
7655
|
-
return params;
|
|
7656
|
-
};
|
|
7657
8394
|
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
8395
|
+
// Store relationships for easy access
|
|
8396
|
+
patternRelationships.set(currentPattern, {
|
|
8397
|
+
pattern: currentData.parsedPattern,
|
|
8398
|
+
parsedPattern: currentData.parsedPattern,
|
|
8399
|
+
connections: currentData.connections,
|
|
8400
|
+
childPatterns: currentData.childPatterns, // Store child patterns
|
|
8401
|
+
parentPatterns: currentData.parentPatterns, // Store parent patterns
|
|
8402
|
+
originalPattern: currentPattern,
|
|
8403
|
+
});
|
|
8404
|
+
}
|
|
7662
8405
|
};
|
|
7663
8406
|
|
|
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
8407
|
/**
|
|
7675
|
-
*
|
|
7676
|
-
*
|
|
8408
|
+
* Get pattern data for a registered pattern
|
|
7677
8409
|
*/
|
|
8410
|
+
const getPatternData = (urlPatternRaw) => {
|
|
8411
|
+
return patternRegistry.get(urlPatternRaw);
|
|
8412
|
+
};
|
|
7678
8413
|
|
|
8414
|
+
/**
|
|
8415
|
+
* Clear all registered patterns
|
|
8416
|
+
*/
|
|
8417
|
+
const clearPatterns = () => {
|
|
8418
|
+
patternRegistry.clear();
|
|
8419
|
+
patternRelationships.clear();
|
|
8420
|
+
};
|
|
7679
8421
|
|
|
7680
|
-
|
|
7681
|
-
if (
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
}
|
|
8422
|
+
const resolveRouteUrl = (relativeUrl) => {
|
|
8423
|
+
if (relativeUrl[0] === "/") {
|
|
8424
|
+
// we remove the leading slash because we want to resolve against baseUrl which may
|
|
8425
|
+
// not be the root url
|
|
8426
|
+
relativeUrl = relativeUrl.slice(1);
|
|
8427
|
+
}
|
|
7686
8428
|
|
|
7687
|
-
|
|
7688
|
-
|
|
8429
|
+
// we don't use URL constructor on PURPOSE (in case the relativeUrl contains invalid url chars)
|
|
8430
|
+
// and we want to support use cases where people WANT to produce invalid urls (for example rawUrlPart with spaces)
|
|
8431
|
+
// because these urls will be handled by non standard clients (like a backend service allowing url like stuff)
|
|
8432
|
+
if (baseUrl.endsWith("/")) {
|
|
8433
|
+
return `${baseUrl}${relativeUrl}`;
|
|
8434
|
+
}
|
|
8435
|
+
return `${baseUrl}/${relativeUrl}`;
|
|
7689
8436
|
};
|
|
8437
|
+
|
|
8438
|
+
/**
|
|
8439
|
+
* Route management with pattern-first architecture
|
|
8440
|
+
* Routes work with relative URLs, patterns handle base URL resolution
|
|
8441
|
+
*/
|
|
8442
|
+
|
|
8443
|
+
|
|
7690
8444
|
// Controls what happens to actions when their route stops matching:
|
|
7691
8445
|
// 'abort' - Cancel the action immediately when route stops matching
|
|
7692
8446
|
// 'keep-loading' - Allow action to continue running after route stops matching
|
|
@@ -7695,19 +8449,23 @@ const setBaseUrl = (value) => {
|
|
|
7695
8449
|
// However, since route reactivation triggers action reload anyway, the old data won't be used
|
|
7696
8450
|
// so it's better to abort the action to avoid unnecessary resource usage.
|
|
7697
8451
|
const ROUTE_DEACTIVATION_STRATEGY = "abort"; // 'abort', 'keep-loading'
|
|
8452
|
+
const ROUTE_NOT_MATCHING_PARAMS = {};
|
|
7698
8453
|
|
|
7699
8454
|
const routeSet = new Set();
|
|
7700
8455
|
// Store previous route states to detect changes
|
|
8456
|
+
const routePrivatePropertiesMap = new Map();
|
|
8457
|
+
|
|
7701
8458
|
const routePreviousStateMap = new WeakMap();
|
|
7702
8459
|
// Store abort controllers per action to control their lifecycle based on route state
|
|
7703
8460
|
const actionAbortControllerWeakMap = new WeakMap();
|
|
8461
|
+
|
|
7704
8462
|
const updateRoutes = (
|
|
7705
8463
|
url,
|
|
7706
8464
|
{
|
|
8465
|
+
navigationType = "push",
|
|
8466
|
+
isVisited = () => false,
|
|
7707
8467
|
// state
|
|
7708
|
-
|
|
7709
|
-
isVisited,
|
|
7710
|
-
},
|
|
8468
|
+
} = {},
|
|
7711
8469
|
) => {
|
|
7712
8470
|
const routeMatchInfoSet = new Set();
|
|
7713
8471
|
for (const route of routeSet) {
|
|
@@ -7717,32 +8475,19 @@ const updateRoutes = (
|
|
|
7717
8475
|
// Get previous state
|
|
7718
8476
|
const previousState = routePreviousStateMap.get(route) || {
|
|
7719
8477
|
matching: false,
|
|
7720
|
-
|
|
7721
|
-
params: null,
|
|
8478
|
+
params: ROUTE_NOT_MATCHING_PARAMS,
|
|
7722
8479
|
};
|
|
7723
8480
|
const oldMatching = previousState.matching;
|
|
7724
|
-
const oldExactMatching = previousState.exactMatching;
|
|
7725
8481
|
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
|
-
}
|
|
8482
|
+
|
|
8483
|
+
// Use custom pattern matching - much simpler than URLPattern approach
|
|
8484
|
+
let extractedParams = routePattern.applyOn(url);
|
|
8485
|
+
let newMatching = Boolean(extractedParams);
|
|
8486
|
+
|
|
7744
8487
|
let newParams;
|
|
8488
|
+
|
|
7745
8489
|
if (extractedParams) {
|
|
8490
|
+
// No need for complex wildcard correction - custom system handles it properly
|
|
7746
8491
|
if (compareTwoJsValues(oldParams, extractedParams)) {
|
|
7747
8492
|
// No change in parameters, keep the old params
|
|
7748
8493
|
newParams = oldParams;
|
|
@@ -7750,7 +8495,7 @@ const updateRoutes = (
|
|
|
7750
8495
|
newParams = extractedParams;
|
|
7751
8496
|
}
|
|
7752
8497
|
} else {
|
|
7753
|
-
newParams =
|
|
8498
|
+
newParams = ROUTE_NOT_MATCHING_PARAMS;
|
|
7754
8499
|
}
|
|
7755
8500
|
|
|
7756
8501
|
const routeMatchInfo = {
|
|
@@ -7758,8 +8503,6 @@ const updateRoutes = (
|
|
|
7758
8503
|
routePrivateProperties,
|
|
7759
8504
|
oldMatching,
|
|
7760
8505
|
newMatching,
|
|
7761
|
-
oldExactMatching,
|
|
7762
|
-
newExactMatching,
|
|
7763
8506
|
oldParams,
|
|
7764
8507
|
newParams,
|
|
7765
8508
|
};
|
|
@@ -7767,7 +8510,6 @@ const updateRoutes = (
|
|
|
7767
8510
|
// Store current state for next comparison
|
|
7768
8511
|
routePreviousStateMap.set(route, {
|
|
7769
8512
|
matching: newMatching,
|
|
7770
|
-
exactMatching: newExactMatching,
|
|
7771
8513
|
params: newParams,
|
|
7772
8514
|
});
|
|
7773
8515
|
}
|
|
@@ -7779,14 +8521,12 @@ const updateRoutes = (
|
|
|
7779
8521
|
route,
|
|
7780
8522
|
routePrivateProperties,
|
|
7781
8523
|
newMatching,
|
|
7782
|
-
newExactMatching,
|
|
7783
8524
|
newParams,
|
|
7784
8525
|
} of routeMatchInfoSet) {
|
|
7785
8526
|
const { updateStatus } = routePrivateProperties;
|
|
7786
8527
|
const visited = isVisited(route.url);
|
|
7787
8528
|
updateStatus({
|
|
7788
8529
|
matching: newMatching,
|
|
7789
|
-
exactMatching: newExactMatching,
|
|
7790
8530
|
params: newParams,
|
|
7791
8531
|
visited,
|
|
7792
8532
|
});
|
|
@@ -7807,7 +8547,11 @@ const updateRoutes = (
|
|
|
7807
8547
|
const routeAction = route.action;
|
|
7808
8548
|
const currentAction = routeAction.getCurrentAction();
|
|
7809
8549
|
if (shouldLoad) {
|
|
7810
|
-
if (
|
|
8550
|
+
if (
|
|
8551
|
+
navigationType === "replace" ||
|
|
8552
|
+
currentAction.aborted ||
|
|
8553
|
+
currentAction.error
|
|
8554
|
+
) {
|
|
7811
8555
|
shouldLoad = false;
|
|
7812
8556
|
}
|
|
7813
8557
|
}
|
|
@@ -7885,12 +8629,15 @@ const updateRoutes = (
|
|
|
7885
8629
|
};
|
|
7886
8630
|
};
|
|
7887
8631
|
|
|
7888
|
-
const routePrivatePropertiesMap = new Map();
|
|
7889
8632
|
const getRoutePrivateProperties = (route) => {
|
|
7890
8633
|
return routePrivatePropertiesMap.get(route);
|
|
7891
8634
|
};
|
|
7892
8635
|
|
|
7893
|
-
const
|
|
8636
|
+
const registerRoute = (routePattern) => {
|
|
8637
|
+
const urlPatternRaw = routePattern.originalPattern;
|
|
8638
|
+
const patternData = getPatternData(urlPatternRaw);
|
|
8639
|
+
const { cleanPattern, connections } = patternData;
|
|
8640
|
+
|
|
7894
8641
|
const cleanupCallbackSet = new Set();
|
|
7895
8642
|
const cleanup = () => {
|
|
7896
8643
|
for (const cleanupCallback of cleanupCallbackSet) {
|
|
@@ -7898,14 +8645,14 @@ const createRoute = (urlPatternInput) => {
|
|
|
7898
8645
|
}
|
|
7899
8646
|
cleanupCallbackSet.clear();
|
|
7900
8647
|
};
|
|
7901
|
-
|
|
7902
8648
|
const [publishStatus, subscribeStatus] = createPubSub();
|
|
8649
|
+
|
|
7903
8650
|
const route = {
|
|
7904
|
-
urlPattern:
|
|
8651
|
+
urlPattern: cleanPattern,
|
|
8652
|
+
pattern: cleanPattern,
|
|
7905
8653
|
isRoute: true,
|
|
7906
8654
|
matching: false,
|
|
7907
|
-
|
|
7908
|
-
params: null,
|
|
8655
|
+
params: ROUTE_NOT_MATCHING_PARAMS,
|
|
7909
8656
|
buildUrl: null,
|
|
7910
8657
|
bindAction: null,
|
|
7911
8658
|
relativeUrl: null,
|
|
@@ -7913,167 +8660,178 @@ const createRoute = (urlPatternInput) => {
|
|
|
7913
8660
|
action: null,
|
|
7914
8661
|
cleanup,
|
|
7915
8662
|
toString: () => {
|
|
7916
|
-
return `route "${
|
|
8663
|
+
return `route "${cleanPattern}"`;
|
|
7917
8664
|
},
|
|
7918
8665
|
replaceParams: undefined,
|
|
7919
8666
|
subscribeStatus,
|
|
7920
8667
|
};
|
|
7921
8668
|
routeSet.add(route);
|
|
7922
|
-
|
|
7923
8669
|
const routePrivateProperties = {
|
|
7924
|
-
routePattern
|
|
8670
|
+
routePattern,
|
|
8671
|
+
originalPattern: urlPatternRaw,
|
|
8672
|
+
pattern: cleanPattern,
|
|
7925
8673
|
matchingSignal: null,
|
|
7926
|
-
exactMatchingSignal: null,
|
|
7927
8674
|
paramsSignal: null,
|
|
8675
|
+
rawParamsSignal: null,
|
|
7928
8676
|
visitedSignal: null,
|
|
7929
8677
|
relativeUrlSignal: null,
|
|
7930
8678
|
urlSignal: null,
|
|
7931
|
-
updateStatus: ({ matching,
|
|
8679
|
+
updateStatus: ({ matching, params, visited }) => {
|
|
7932
8680
|
let someChange = false;
|
|
7933
8681
|
matchingSignal.value = matching;
|
|
7934
|
-
|
|
7935
|
-
paramsSignal.value = params;
|
|
7936
|
-
visitedSignal.value = visited;
|
|
8682
|
+
|
|
7937
8683
|
if (route.matching !== matching) {
|
|
7938
8684
|
route.matching = matching;
|
|
7939
8685
|
someChange = true;
|
|
7940
8686
|
}
|
|
7941
|
-
|
|
7942
|
-
route.exactMatching = exactMatching;
|
|
7943
|
-
someChange = true;
|
|
7944
|
-
}
|
|
7945
|
-
if (route.params !== params) {
|
|
7946
|
-
route.params = params;
|
|
7947
|
-
someChange = true;
|
|
7948
|
-
}
|
|
8687
|
+
visitedSignal.value = visited;
|
|
7949
8688
|
if (route.visited !== visited) {
|
|
7950
8689
|
route.visited = visited;
|
|
7951
8690
|
someChange = true;
|
|
7952
8691
|
}
|
|
8692
|
+
// Store raw params (from URL) - paramsSignal will reactively compute merged params
|
|
8693
|
+
rawParamsSignal.value = params;
|
|
8694
|
+
// Get merged params for comparison (computed signal will handle the merging)
|
|
8695
|
+
const mergedParams = paramsSignal.value;
|
|
8696
|
+
if (route.params !== mergedParams) {
|
|
8697
|
+
route.params = mergedParams;
|
|
8698
|
+
someChange = true;
|
|
8699
|
+
}
|
|
7953
8700
|
if (someChange) {
|
|
7954
|
-
publishStatus({
|
|
8701
|
+
publishStatus({
|
|
8702
|
+
matching,
|
|
8703
|
+
params: mergedParams,
|
|
8704
|
+
visited,
|
|
8705
|
+
});
|
|
7955
8706
|
}
|
|
7956
8707
|
},
|
|
7957
8708
|
};
|
|
7958
8709
|
routePrivatePropertiesMap.set(route, routePrivateProperties);
|
|
7959
8710
|
|
|
7960
|
-
const
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
const
|
|
7964
|
-
|
|
7965
|
-
|
|
8711
|
+
const matchingSignal = signal(false);
|
|
8712
|
+
const rawParamsSignal = signal(ROUTE_NOT_MATCHING_PARAMS);
|
|
8713
|
+
const paramsSignal = computed(() => {
|
|
8714
|
+
const rawParams = rawParamsSignal.value;
|
|
8715
|
+
// Pattern system handles parameter defaults, routes just work with raw params
|
|
8716
|
+
return rawParams || {};
|
|
8717
|
+
});
|
|
8718
|
+
const visitedSignal = signal(false);
|
|
8719
|
+
for (const { signal: stateSignal, paramName, options = {} } of connections) {
|
|
8720
|
+
const { debug } = options;
|
|
7966
8721
|
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
8722
|
+
if (debug) {
|
|
8723
|
+
console.debug(
|
|
8724
|
+
`[route] connecting param "${paramName}" to signal`,
|
|
8725
|
+
stateSignal,
|
|
8726
|
+
);
|
|
8727
|
+
}
|
|
8728
|
+
|
|
8729
|
+
// URL -> Signal synchronization
|
|
8730
|
+
effect(() => {
|
|
8731
|
+
const matching = matchingSignal.value;
|
|
8732
|
+
const params = rawParamsSignal.value;
|
|
8733
|
+
const urlParamValue = params[paramName];
|
|
8734
|
+
|
|
8735
|
+
if (!matching) {
|
|
8736
|
+
return;
|
|
8737
|
+
}
|
|
8738
|
+
if (debug) {
|
|
8739
|
+
console.debug(
|
|
8740
|
+
`[stateSignal] URL -> Signal: ${paramName}=${urlParamValue}`,
|
|
8741
|
+
);
|
|
8742
|
+
}
|
|
8743
|
+
stateSignal.value = urlParamValue;
|
|
8744
|
+
});
|
|
8745
|
+
|
|
8746
|
+
// Signal -> URL synchronization
|
|
8747
|
+
effect(() => {
|
|
8748
|
+
const value = stateSignal.value;
|
|
8749
|
+
const params = rawParamsSignal.value;
|
|
8750
|
+
const urlParamValue = params[paramName];
|
|
8751
|
+
const matching = matchingSignal.value;
|
|
8752
|
+
|
|
8753
|
+
if (!matching || value === urlParamValue) {
|
|
8754
|
+
return;
|
|
8755
|
+
}
|
|
8756
|
+
|
|
8757
|
+
if (debug) {
|
|
8758
|
+
console.debug(`[stateSignal] Signal -> URL: ${paramName}=${value}`);
|
|
7975
8759
|
}
|
|
7976
|
-
|
|
8760
|
+
|
|
8761
|
+
route.replaceParams({ [paramName]: value });
|
|
8762
|
+
});
|
|
8763
|
+
}
|
|
8764
|
+
|
|
8765
|
+
route.navTo = (params) => {
|
|
8766
|
+
if (!browserIntegration$1) {
|
|
8767
|
+
return Promise.resolve();
|
|
7977
8768
|
}
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
if (
|
|
7982
|
-
return
|
|
8769
|
+
return browserIntegration$1.navTo(route.buildUrl(params));
|
|
8770
|
+
};
|
|
8771
|
+
route.redirectTo = (params) => {
|
|
8772
|
+
if (!browserIntegration$1) {
|
|
8773
|
+
return Promise.resolve();
|
|
7983
8774
|
}
|
|
7984
|
-
|
|
7985
|
-
|
|
8775
|
+
return browserIntegration$1.navTo(route.buildUrl(params), {
|
|
8776
|
+
replace: true,
|
|
8777
|
+
});
|
|
8778
|
+
};
|
|
8779
|
+
route.replaceParams = (newParams) => {
|
|
8780
|
+
const matching = matchingSignal.peek();
|
|
8781
|
+
if (!matching) {
|
|
8782
|
+
console.warn(
|
|
8783
|
+
`Cannot replace params on route ${route} because it is not matching the current URL.`,
|
|
8784
|
+
);
|
|
8785
|
+
return null;
|
|
7986
8786
|
}
|
|
7987
|
-
|
|
8787
|
+
if (route.action) {
|
|
8788
|
+
// For action: merge with resolved params (includes defaults) so action gets complete params
|
|
8789
|
+
const currentResolvedParams = routePattern.resolveParams();
|
|
8790
|
+
const updatedActionParams = { ...currentResolvedParams, ...newParams };
|
|
8791
|
+
route.action.replaceParams(updatedActionParams);
|
|
8792
|
+
}
|
|
8793
|
+
return route.redirectTo(newParams);
|
|
7988
8794
|
};
|
|
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;
|
|
8795
|
+
route.buildRelativeUrl = (params) => {
|
|
8796
|
+
// buildMostPreciseUrl now handles parameter resolution internally
|
|
8797
|
+
return routePattern.buildMostPreciseUrl(params);
|
|
8798
|
+
};
|
|
8799
|
+
route.buildUrl = (params) => {
|
|
8800
|
+
const routeRelativeUrl = route.buildRelativeUrl(params);
|
|
8801
|
+
const routeUrl = resolveRouteUrl(routeRelativeUrl);
|
|
8802
|
+
return routeUrl;
|
|
8803
|
+
};
|
|
8804
|
+
route.matchesParams = (providedParams) => {
|
|
8805
|
+
const currentParams = route.params;
|
|
8806
|
+
const resolvedParams = routePattern.resolveParams({
|
|
8807
|
+
...currentParams,
|
|
8808
|
+
...providedParams,
|
|
8809
|
+
});
|
|
8810
|
+
const same = compareTwoJsValues(currentParams, resolvedParams);
|
|
8811
|
+
return same;
|
|
8025
8812
|
};
|
|
8026
|
-
route.buildUrl = buildUrl;
|
|
8027
8813
|
|
|
8028
|
-
|
|
8029
|
-
const exactMatchingSignal = signal(false);
|
|
8030
|
-
const paramsSignal = signal(null);
|
|
8031
|
-
const visitedSignal = signal(false);
|
|
8814
|
+
// relativeUrl/url
|
|
8032
8815
|
const relativeUrlSignal = computed(() => {
|
|
8033
|
-
const
|
|
8034
|
-
const
|
|
8816
|
+
const rawParams = rawParamsSignal.value;
|
|
8817
|
+
const relativeUrl = route.buildRelativeUrl(rawParams);
|
|
8035
8818
|
return relativeUrl;
|
|
8036
8819
|
});
|
|
8820
|
+
const urlSignal = computed(() => {
|
|
8821
|
+
const routeUrl = route.buildUrl();
|
|
8822
|
+
return routeUrl;
|
|
8823
|
+
});
|
|
8037
8824
|
const disposeRelativeUrlEffect = effect(() => {
|
|
8038
8825
|
route.relativeUrl = relativeUrlSignal.value;
|
|
8039
8826
|
});
|
|
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
8827
|
const disposeUrlEffect = effect(() => {
|
|
8048
8828
|
route.url = urlSignal.value;
|
|
8049
8829
|
});
|
|
8830
|
+
cleanupCallbackSet.add(disposeRelativeUrlEffect);
|
|
8050
8831
|
cleanupCallbackSet.add(disposeUrlEffect);
|
|
8051
8832
|
|
|
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
|
-
|
|
8833
|
+
// action stuff (for later)
|
|
8834
|
+
route.bindAction = (action) => {
|
|
8077
8835
|
const { store } = action.meta;
|
|
8078
8836
|
if (store) {
|
|
8079
8837
|
const { mutableIdKeys } = store;
|
|
@@ -8100,75 +8858,40 @@ const createRoute = (urlPatternInput) => {
|
|
|
8100
8858
|
}
|
|
8101
8859
|
}
|
|
8102
8860
|
|
|
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
8861
|
const actionBoundToThisRoute = action.bindParams(paramsSignal);
|
|
8129
8862
|
route.action = actionBoundToThisRoute;
|
|
8130
8863
|
return actionBoundToThisRoute;
|
|
8131
8864
|
};
|
|
8132
|
-
route.bindAction = bindAction;
|
|
8133
8865
|
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
const routePattern = createRoutePattern(urlPatternInput, baseUrl);
|
|
8143
|
-
routePrivateProperties.routePattern = routePattern;
|
|
8144
|
-
}
|
|
8866
|
+
// Store private properties for internal access
|
|
8867
|
+
routePrivateProperties.matchingSignal = matchingSignal;
|
|
8868
|
+
routePrivateProperties.paramsSignal = paramsSignal;
|
|
8869
|
+
routePrivateProperties.rawParamsSignal = rawParamsSignal;
|
|
8870
|
+
routePrivateProperties.visitedSignal = visitedSignal;
|
|
8871
|
+
routePrivateProperties.relativeUrlSignal = relativeUrlSignal;
|
|
8872
|
+
routePrivateProperties.urlSignal = urlSignal;
|
|
8873
|
+
routePrivateProperties.cleanupCallbackSet = cleanupCallbackSet;
|
|
8145
8874
|
|
|
8146
8875
|
return route;
|
|
8147
8876
|
};
|
|
8877
|
+
|
|
8148
8878
|
const useRouteStatus = (route) => {
|
|
8149
8879
|
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
8150
8880
|
if (!routePrivateProperties) {
|
|
8151
8881
|
throw new Error(`Cannot find route private properties for ${route}`);
|
|
8152
8882
|
}
|
|
8153
8883
|
|
|
8154
|
-
const {
|
|
8155
|
-
|
|
8156
|
-
matchingSignal,
|
|
8157
|
-
exactMatchingSignal,
|
|
8158
|
-
paramsSignal,
|
|
8159
|
-
visitedSignal,
|
|
8160
|
-
} = routePrivateProperties;
|
|
8884
|
+
const { urlSignal, matchingSignal, paramsSignal, visitedSignal } =
|
|
8885
|
+
routePrivateProperties;
|
|
8161
8886
|
|
|
8162
8887
|
const url = urlSignal.value;
|
|
8163
8888
|
const matching = matchingSignal.value;
|
|
8164
|
-
const exactMatching = exactMatchingSignal.value;
|
|
8165
8889
|
const params = paramsSignal.value;
|
|
8166
8890
|
const visited = visitedSignal.value;
|
|
8167
8891
|
|
|
8168
8892
|
return {
|
|
8169
8893
|
url,
|
|
8170
8894
|
matching,
|
|
8171
|
-
exactMatching,
|
|
8172
8895
|
params,
|
|
8173
8896
|
visited,
|
|
8174
8897
|
};
|
|
@@ -8178,7 +8901,6 @@ let browserIntegration$1;
|
|
|
8178
8901
|
const setBrowserIntegration = (integration) => {
|
|
8179
8902
|
browserIntegration$1 = integration;
|
|
8180
8903
|
};
|
|
8181
|
-
|
|
8182
8904
|
let onRouteDefined = () => {};
|
|
8183
8905
|
const setOnRouteDefined = (v) => {
|
|
8184
8906
|
onRouteDefined = v;
|
|
@@ -8199,23 +8921,47 @@ const setOnRouteDefined = (v) => {
|
|
|
8199
8921
|
// at any given time (url can be shared, reloaded, etc..)
|
|
8200
8922
|
// Later I'll consider adding ability to have dynamic import into the mix
|
|
8201
8923
|
// (An async function returning an action)
|
|
8924
|
+
|
|
8202
8925
|
const setupRoutes = (routeDefinition) => {
|
|
8203
|
-
//
|
|
8204
|
-
|
|
8205
|
-
|
|
8926
|
+
// Prevent calling setupRoutes when routes already exist - enforce clean setup
|
|
8927
|
+
if (routeSet.size > 0) {
|
|
8928
|
+
throw new Error(
|
|
8929
|
+
"Routes already exist. Call clearAllRoutes() first to clean up existing routes before creating new ones. This prevents cross-test pollution and ensures clean state.",
|
|
8930
|
+
);
|
|
8206
8931
|
}
|
|
8207
|
-
|
|
8208
|
-
|
|
8932
|
+
// PHASE 1: Register all patterns and build their relationships
|
|
8933
|
+
setupPatterns(routeDefinition);
|
|
8934
|
+
// PHASE 2: Create route patterns with signal connections and parameter defaults
|
|
8935
|
+
const routePatterns = {};
|
|
8936
|
+
for (const key of Object.keys(routeDefinition)) {
|
|
8937
|
+
const urlPatternRaw = routeDefinition[key];
|
|
8938
|
+
routePatterns[key] = createRoutePattern(urlPatternRaw);
|
|
8939
|
+
}
|
|
8940
|
+
// PHASE 3: Create routes using pre-created patterns
|
|
8209
8941
|
const routes = {};
|
|
8210
8942
|
for (const key of Object.keys(routeDefinition)) {
|
|
8211
|
-
const
|
|
8212
|
-
const route =
|
|
8943
|
+
const routePattern = routePatterns[key];
|
|
8944
|
+
const route = registerRoute(routePattern);
|
|
8213
8945
|
routes[key] = route;
|
|
8214
8946
|
}
|
|
8215
8947
|
onRouteDefined();
|
|
8948
|
+
|
|
8216
8949
|
return routes;
|
|
8217
8950
|
};
|
|
8218
8951
|
|
|
8952
|
+
// for unit tests
|
|
8953
|
+
const clearAllRoutes = () => {
|
|
8954
|
+
for (const route of routeSet) {
|
|
8955
|
+
route.cleanup();
|
|
8956
|
+
}
|
|
8957
|
+
routeSet.clear();
|
|
8958
|
+
routePrivatePropertiesMap.clear();
|
|
8959
|
+
// Clear patterns as well
|
|
8960
|
+
clearPatterns();
|
|
8961
|
+
// Don't clear signal registry here - let tests manage it explicitly
|
|
8962
|
+
// This prevents clearing signals that are still being used across multiple route registrations
|
|
8963
|
+
};
|
|
8964
|
+
|
|
8219
8965
|
const arraySignal = (initialValue = []) => {
|
|
8220
8966
|
const theSignal = signal(initialValue);
|
|
8221
8967
|
|
|
@@ -8340,7 +9086,9 @@ computed(() => {
|
|
|
8340
9086
|
return reasonArray;
|
|
8341
9087
|
});
|
|
8342
9088
|
|
|
8343
|
-
const documentUrlSignal = signal(
|
|
9089
|
+
const documentUrlSignal = signal(
|
|
9090
|
+
typeof window === "undefined" ? "http://localhost" : window.location.href,
|
|
9091
|
+
);
|
|
8344
9092
|
const useDocumentUrl = () => {
|
|
8345
9093
|
return documentUrlSignal.value;
|
|
8346
9094
|
};
|
|
@@ -8477,11 +9225,10 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8477
9225
|
{ reason = "replaceDocumentState called" } = {},
|
|
8478
9226
|
) => {
|
|
8479
9227
|
const url = window.location.href;
|
|
8480
|
-
window.history.replaceState(newState, null, url);
|
|
8481
9228
|
handleRoutingTask(url, {
|
|
8482
|
-
replace: true,
|
|
8483
|
-
state: newState,
|
|
8484
9229
|
reason,
|
|
9230
|
+
navigationType: "replace",
|
|
9231
|
+
state: newState,
|
|
8485
9232
|
});
|
|
8486
9233
|
};
|
|
8487
9234
|
|
|
@@ -8504,42 +9251,52 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8504
9251
|
return;
|
|
8505
9252
|
}
|
|
8506
9253
|
visitedUrlSet.add(url);
|
|
8507
|
-
|
|
8508
|
-
// Increment signal to notify subscribers that visited URLs changed
|
|
8509
|
-
visitedUrlsSignal.value++;
|
|
9254
|
+
visitedUrlsSignal.value++; // Increment signal to notify subscribers that visited URLs changed
|
|
8510
9255
|
|
|
8511
9256
|
const historyState = getDocumentState() || {};
|
|
8512
|
-
const
|
|
9257
|
+
const historyStateWithVisitedUrls = {
|
|
8513
9258
|
...historyState,
|
|
8514
9259
|
jsenv_visited_urls: Array.from(visitedUrlSet),
|
|
8515
9260
|
};
|
|
8516
9261
|
window.history.replaceState(
|
|
8517
|
-
|
|
9262
|
+
historyStateWithVisitedUrls,
|
|
8518
9263
|
null,
|
|
8519
9264
|
window.location.href,
|
|
8520
9265
|
);
|
|
8521
|
-
updateDocumentState(
|
|
9266
|
+
updateDocumentState(historyStateWithVisitedUrls);
|
|
8522
9267
|
};
|
|
8523
9268
|
|
|
8524
9269
|
let abortController = null;
|
|
8525
|
-
const handleRoutingTask = (
|
|
8526
|
-
|
|
9270
|
+
const handleRoutingTask = (
|
|
9271
|
+
url,
|
|
9272
|
+
{
|
|
9273
|
+
reason,
|
|
9274
|
+
navigationType, // "push", "reload", "replace", "traverse"
|
|
9275
|
+
state,
|
|
9276
|
+
},
|
|
9277
|
+
) => {
|
|
9278
|
+
if (navigationType === "push") {
|
|
9279
|
+
window.history.pushState(state, null, url);
|
|
9280
|
+
} else if (navigationType === "replace") {
|
|
9281
|
+
window.history.replaceState(state, null, url);
|
|
9282
|
+
}
|
|
9283
|
+
|
|
8527
9284
|
updateDocumentUrl(url);
|
|
8528
9285
|
updateDocumentState(state);
|
|
9286
|
+
markUrlAsVisited(url);
|
|
8529
9287
|
if (abortController) {
|
|
8530
9288
|
abortController.abort(`navigating to ${url}`);
|
|
8531
9289
|
}
|
|
8532
9290
|
abortController = new AbortController();
|
|
8533
|
-
|
|
9291
|
+
const abortSignal = abortController.signal;
|
|
8534
9292
|
const { allResult, requestedResult } = applyRouting(url, {
|
|
8535
9293
|
globalAbortSignal: globalAbortController.signal,
|
|
8536
|
-
abortSignal
|
|
8537
|
-
state,
|
|
8538
|
-
replace,
|
|
8539
|
-
isVisited,
|
|
9294
|
+
abortSignal,
|
|
8540
9295
|
reason,
|
|
9296
|
+
navigationType,
|
|
9297
|
+
isVisited,
|
|
9298
|
+
state,
|
|
8541
9299
|
});
|
|
8542
|
-
|
|
8543
9300
|
executeWithCleanup(
|
|
8544
9301
|
() => allResult,
|
|
8545
9302
|
() => {
|
|
@@ -8583,11 +9340,10 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8583
9340
|
return;
|
|
8584
9341
|
}
|
|
8585
9342
|
e.preventDefault();
|
|
8586
|
-
const state = null;
|
|
8587
|
-
history.pushState(state, null, href);
|
|
8588
9343
|
handleRoutingTask(href, {
|
|
8589
|
-
state,
|
|
8590
9344
|
reason: `"click" on a[href="${href}"]`,
|
|
9345
|
+
navigationType: "push",
|
|
9346
|
+
state: null,
|
|
8591
9347
|
});
|
|
8592
9348
|
},
|
|
8593
9349
|
{ capture: true },
|
|
@@ -8596,7 +9352,8 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8596
9352
|
window.addEventListener(
|
|
8597
9353
|
"submit",
|
|
8598
9354
|
() => {
|
|
8599
|
-
//
|
|
9355
|
+
// Handle form submissions?
|
|
9356
|
+
// Not needed yet
|
|
8600
9357
|
},
|
|
8601
9358
|
{ capture: true },
|
|
8602
9359
|
);
|
|
@@ -8605,21 +9362,17 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8605
9362
|
const url = window.location.href;
|
|
8606
9363
|
const state = popstateEvent.state;
|
|
8607
9364
|
handleRoutingTask(url, {
|
|
8608
|
-
state,
|
|
8609
9365
|
reason: `"popstate" event for ${url}`,
|
|
9366
|
+
navigationType: "traverse",
|
|
9367
|
+
state,
|
|
8610
9368
|
});
|
|
8611
9369
|
});
|
|
8612
9370
|
|
|
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
|
-
}
|
|
9371
|
+
const navTo = async (url, { replace, state = null } = {}) => {
|
|
8619
9372
|
handleRoutingTask(url, {
|
|
8620
|
-
state,
|
|
8621
|
-
replace,
|
|
8622
9373
|
reason: `navTo called with "${url}"`,
|
|
9374
|
+
navigationType: replace ? "replace" : "push",
|
|
9375
|
+
state,
|
|
8623
9376
|
});
|
|
8624
9377
|
};
|
|
8625
9378
|
|
|
@@ -8631,6 +9384,8 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8631
9384
|
const url = window.location.href;
|
|
8632
9385
|
const state = history.state;
|
|
8633
9386
|
handleRoutingTask(url, {
|
|
9387
|
+
reason: "reload called",
|
|
9388
|
+
navigationType: "reload",
|
|
8634
9389
|
state,
|
|
8635
9390
|
});
|
|
8636
9391
|
};
|
|
@@ -8646,11 +9401,10 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
8646
9401
|
const init = () => {
|
|
8647
9402
|
const url = window.location.href;
|
|
8648
9403
|
const state = history.state;
|
|
8649
|
-
history.replaceState(state, null, url);
|
|
8650
9404
|
handleRoutingTask(url, {
|
|
8651
|
-
state,
|
|
8652
|
-
replace: true,
|
|
8653
9405
|
reason: "routing initialization",
|
|
9406
|
+
navigationType: "replace",
|
|
9407
|
+
state,
|
|
8654
9408
|
});
|
|
8655
9409
|
};
|
|
8656
9410
|
|
|
@@ -8686,7 +9440,7 @@ const applyRouting = (
|
|
|
8686
9440
|
globalAbortSignal,
|
|
8687
9441
|
abortSignal,
|
|
8688
9442
|
// state
|
|
8689
|
-
|
|
9443
|
+
navigationType,
|
|
8690
9444
|
isVisited,
|
|
8691
9445
|
reason,
|
|
8692
9446
|
},
|
|
@@ -8698,9 +9452,9 @@ const applyRouting = (
|
|
|
8698
9452
|
routeLoadRequestedMap,
|
|
8699
9453
|
activeRouteSet,
|
|
8700
9454
|
} = updateRoutes(url, {
|
|
8701
|
-
|
|
8702
|
-
// state,
|
|
9455
|
+
navigationType,
|
|
8703
9456
|
isVisited,
|
|
9457
|
+
// state,
|
|
8704
9458
|
});
|
|
8705
9459
|
if (loadSet.size === 0 && reloadSet.size === 0) {
|
|
8706
9460
|
return {
|
|
@@ -8937,7 +9691,8 @@ const Route = ({
|
|
|
8937
9691
|
index,
|
|
8938
9692
|
fallback,
|
|
8939
9693
|
meta,
|
|
8940
|
-
children
|
|
9694
|
+
children,
|
|
9695
|
+
routeParams
|
|
8941
9696
|
}) => {
|
|
8942
9697
|
const forceRender = useForceRender();
|
|
8943
9698
|
const hasDiscoveredRef = useRef(false);
|
|
@@ -8949,6 +9704,7 @@ const Route = ({
|
|
|
8949
9704
|
index: index,
|
|
8950
9705
|
fallback: fallback,
|
|
8951
9706
|
meta: meta,
|
|
9707
|
+
routeParams: routeParams,
|
|
8952
9708
|
onMatchingInfoChange: matchingInfo => {
|
|
8953
9709
|
hasDiscoveredRef.current = true;
|
|
8954
9710
|
matchingInfoRef.current = matchingInfo;
|
|
@@ -8977,6 +9733,7 @@ const MatchingRouteManager = ({
|
|
|
8977
9733
|
index,
|
|
8978
9734
|
fallback,
|
|
8979
9735
|
meta,
|
|
9736
|
+
routeParams,
|
|
8980
9737
|
onMatchingInfoChange,
|
|
8981
9738
|
children
|
|
8982
9739
|
}) => {
|
|
@@ -9018,6 +9775,7 @@ const MatchingRouteManager = ({
|
|
|
9018
9775
|
index,
|
|
9019
9776
|
fallback,
|
|
9020
9777
|
meta,
|
|
9778
|
+
routeParams,
|
|
9021
9779
|
indexCandidate,
|
|
9022
9780
|
fallbackCandidate,
|
|
9023
9781
|
candidateSet,
|
|
@@ -9036,6 +9794,7 @@ const initRouteObserver = ({
|
|
|
9036
9794
|
index,
|
|
9037
9795
|
fallback,
|
|
9038
9796
|
meta,
|
|
9797
|
+
routeParams,
|
|
9039
9798
|
indexCandidate,
|
|
9040
9799
|
fallbackCandidate,
|
|
9041
9800
|
candidateSet,
|
|
@@ -9092,6 +9851,12 @@ const initRouteObserver = ({
|
|
|
9092
9851
|
// we have a route and it does not match no need to go further
|
|
9093
9852
|
return null;
|
|
9094
9853
|
}
|
|
9854
|
+
|
|
9855
|
+
// Check if routeParams match current route parameters
|
|
9856
|
+
if (routeParams && !route.matchesParams(routeParams)) {
|
|
9857
|
+
return null; // routeParams don't match, don't render
|
|
9858
|
+
}
|
|
9859
|
+
|
|
9095
9860
|
// we have a route and it is matching
|
|
9096
9861
|
// we search the first matching child to put it in the slot
|
|
9097
9862
|
const matchingChildInfo = findMatchingChildInfo();
|
|
@@ -17322,26 +18087,23 @@ const RouteLink = ({
|
|
|
17322
18087
|
if (!route) {
|
|
17323
18088
|
throw new Error("route prop is required");
|
|
17324
18089
|
}
|
|
17325
|
-
|
|
18090
|
+
useRouteStatus(route);
|
|
17326
18091
|
const url = route.buildUrl(routeParams);
|
|
17327
|
-
let isCurrent;
|
|
17328
|
-
if (routeStatus.exactMatching) {
|
|
17329
|
-
isCurrent = true;
|
|
17330
|
-
} else if (routeStatus.matching) {
|
|
17331
|
-
isCurrent = routeParams ? route.matchesParams(routeParams) : false;
|
|
17332
|
-
} else {
|
|
17333
|
-
isCurrent = false;
|
|
17334
|
-
}
|
|
17335
18092
|
return jsx(Link, {
|
|
17336
18093
|
...rest,
|
|
17337
18094
|
href: url,
|
|
17338
|
-
pseudoState: {
|
|
17339
|
-
":-navi-href-current": isCurrent
|
|
17340
|
-
},
|
|
17341
18095
|
children: children || route.buildRelativeUrl(routeParams)
|
|
17342
18096
|
});
|
|
17343
18097
|
};
|
|
17344
18098
|
|
|
18099
|
+
const rawUrlPartSymbol = Symbol("raw_url_part");
|
|
18100
|
+
const rawUrlPart = (value) => {
|
|
18101
|
+
return {
|
|
18102
|
+
[rawUrlPartSymbol]: true,
|
|
18103
|
+
value,
|
|
18104
|
+
};
|
|
18105
|
+
};
|
|
18106
|
+
|
|
17345
18107
|
installImportMetaCss(import.meta);Object.assign(PSEUDO_CLASSES, {
|
|
17346
18108
|
":-navi-tab-selected": {
|
|
17347
18109
|
attribute: "data-tab-selected"
|
|
@@ -17649,6 +18411,10 @@ const TabRoute = ({
|
|
|
17649
18411
|
padding = 2,
|
|
17650
18412
|
paddingX,
|
|
17651
18413
|
paddingY,
|
|
18414
|
+
paddingLeft,
|
|
18415
|
+
paddingRight,
|
|
18416
|
+
paddingTop,
|
|
18417
|
+
paddingBottom,
|
|
17652
18418
|
alignX,
|
|
17653
18419
|
alignY,
|
|
17654
18420
|
...props
|
|
@@ -17675,6 +18441,10 @@ const TabRoute = ({
|
|
|
17675
18441
|
padding: padding,
|
|
17676
18442
|
paddingX: paddingX,
|
|
17677
18443
|
paddingY: paddingY,
|
|
18444
|
+
paddingLeft: paddingLeft,
|
|
18445
|
+
paddingRight: paddingRight,
|
|
18446
|
+
paddingTop: paddingTop,
|
|
18447
|
+
paddingBottom: paddingBottom,
|
|
17678
18448
|
alignX: alignX,
|
|
17679
18449
|
alignY: alignY,
|
|
17680
18450
|
children: children
|
|
@@ -25548,5 +26318,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
25548
26318
|
})
|
|
25549
26319
|
});
|
|
25550
26320
|
|
|
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 };
|
|
26321
|
+
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
26322
|
//# sourceMappingURL=jsenv_navi.js.map
|