@jsenv/navi 0.16.49 → 0.16.51
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 +127 -26
- package/dist/jsenv_navi.js.map +5 -5
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -300,7 +300,9 @@ const getSignalType = (value) => {
|
|
|
300
300
|
*
|
|
301
301
|
* @param {any} a - First value to compare
|
|
302
302
|
* @param {any} b - Second value to compare
|
|
303
|
-
* @param {
|
|
303
|
+
* @param {Object} [options={}] - Comparison options
|
|
304
|
+
* @param {Function} [options.keyComparator] - Custom comparator function for object properties and array elements
|
|
305
|
+
* @param {boolean} [options.ignoreArrayOrder=false] - If true, arrays are considered equal regardless of element order
|
|
304
306
|
* @returns {boolean} true if values are deeply equal, false otherwise
|
|
305
307
|
*/
|
|
306
308
|
|
|
@@ -319,7 +321,11 @@ const getSignalType = (value) => {
|
|
|
319
321
|
*/
|
|
320
322
|
const SYMBOL_IDENTITY = Symbol.for("navi_object_identity");
|
|
321
323
|
|
|
322
|
-
const compareTwoJsValues = (
|
|
324
|
+
const compareTwoJsValues = (
|
|
325
|
+
rootA,
|
|
326
|
+
rootB,
|
|
327
|
+
{ keyComparator, ignoreArrayOrder = false } = {},
|
|
328
|
+
) => {
|
|
323
329
|
const seenSet = new Set();
|
|
324
330
|
const compare = (a, b) => {
|
|
325
331
|
if (a === b) {
|
|
@@ -370,6 +376,32 @@ const compareTwoJsValues = (rootA, rootB, { keyComparator } = {}) => {
|
|
|
370
376
|
if (a.length !== b.length) {
|
|
371
377
|
return false;
|
|
372
378
|
}
|
|
379
|
+
if (ignoreArrayOrder) {
|
|
380
|
+
// Unordered array comparison: each element in 'a' must have a match in 'b'
|
|
381
|
+
const usedIndices = new Set();
|
|
382
|
+
for (let i = 0; i < a.length; i++) {
|
|
383
|
+
const aValue = a[i];
|
|
384
|
+
let foundMatch = false;
|
|
385
|
+
|
|
386
|
+
for (let j = 0; j < b.length; j++) {
|
|
387
|
+
if (usedIndices.has(j)) continue; // Already matched with another element
|
|
388
|
+
|
|
389
|
+
const bValue = b[j];
|
|
390
|
+
const comparator = keyComparator || compare;
|
|
391
|
+
if (comparator(aValue, bValue, i, compare)) {
|
|
392
|
+
foundMatch = true;
|
|
393
|
+
usedIndices.add(j);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!foundMatch) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
// Ordered array comparison (original behavior)
|
|
373
405
|
let i = 0;
|
|
374
406
|
while (i < a.length) {
|
|
375
407
|
const aValue = a[i];
|
|
@@ -2393,11 +2425,17 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2393
2425
|
if (dynamicDefaultSignal) {
|
|
2394
2426
|
const dynamicValue = dynamicDefaultSignal.peek();
|
|
2395
2427
|
if (dynamicValue === undefined) {
|
|
2396
|
-
return value
|
|
2428
|
+
return !compareTwoJsValues(value, staticDefaultValue, {
|
|
2429
|
+
ignoreArrayOrder: true,
|
|
2430
|
+
});
|
|
2397
2431
|
}
|
|
2398
|
-
return value
|
|
2432
|
+
return !compareTwoJsValues(value, dynamicValue, {
|
|
2433
|
+
ignoreArrayOrder: true,
|
|
2434
|
+
});
|
|
2399
2435
|
}
|
|
2400
|
-
return value
|
|
2436
|
+
return !compareTwoJsValues(value, staticDefaultValue, {
|
|
2437
|
+
ignoreArrayOrder: true,
|
|
2438
|
+
});
|
|
2401
2439
|
};
|
|
2402
2440
|
|
|
2403
2441
|
// Create signal with initial value: use stored value, or undefined to indicate no explicit value
|
|
@@ -2553,6 +2591,21 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2553
2591
|
}
|
|
2554
2592
|
effect(() => {
|
|
2555
2593
|
const value = preactSignal.value;
|
|
2594
|
+
|
|
2595
|
+
if (dynamicDefaultSignal) {
|
|
2596
|
+
// With dynamic defaults: always persist to preserve user intent
|
|
2597
|
+
// even when value matches dynamic defaults that may change
|
|
2598
|
+
if (value !== undefined) {
|
|
2599
|
+
if (debug) {
|
|
2600
|
+
console.debug(
|
|
2601
|
+
`[stateSignal:${signalIdString}] dynamic default: writing to localStorage "${localStorageKey}"=${value}`,
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
writeIntoLocalStorage(value);
|
|
2605
|
+
}
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
// Static defaults: only persist custom values
|
|
2556
2609
|
if (isCustomValue(value)) {
|
|
2557
2610
|
if (debug) {
|
|
2558
2611
|
console.debug(
|
|
@@ -2630,6 +2683,7 @@ const localStorageTypeMap = {
|
|
|
2630
2683
|
date: "string",
|
|
2631
2684
|
time: "string",
|
|
2632
2685
|
email: "string",
|
|
2686
|
+
array: "object",
|
|
2633
2687
|
};
|
|
2634
2688
|
|
|
2635
2689
|
const getCallerInfo = (targetFunction = null, additionalOffset = 0) => {
|
|
@@ -7578,8 +7632,17 @@ const buildQueryString = (params) => {
|
|
|
7578
7632
|
if (value !== undefined && value !== null) {
|
|
7579
7633
|
const encodedKey = encodeURIComponent(key);
|
|
7580
7634
|
|
|
7635
|
+
// Handle array values - join with commas
|
|
7636
|
+
if (Array.isArray(value)) {
|
|
7637
|
+
if (value.length === 0) ; else {
|
|
7638
|
+
const encodedValue = value
|
|
7639
|
+
.map((item) => encodeURIComponent(String(item)))
|
|
7640
|
+
.join(",");
|
|
7641
|
+
searchParamPairs.push(`${encodedKey}=${encodedValue}`);
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7581
7644
|
// Handle boolean values - if true, just add the key without value
|
|
7582
|
-
if (value === true || value === "") {
|
|
7645
|
+
else if (value === true || value === "") {
|
|
7583
7646
|
searchParamPairs.push(encodedKey);
|
|
7584
7647
|
} else {
|
|
7585
7648
|
const encodedValue = encodeParamValue(value, false); // Search params encode slashes
|
|
@@ -7702,7 +7765,6 @@ const createRoutePattern = (pattern) => {
|
|
|
7702
7765
|
const signalSet = new Set();
|
|
7703
7766
|
for (const connection of connections) {
|
|
7704
7767
|
connectionMap.set(connection.paramName, connection);
|
|
7705
|
-
|
|
7706
7768
|
signalSet.add(connection.signal);
|
|
7707
7769
|
}
|
|
7708
7770
|
|
|
@@ -7717,7 +7779,7 @@ const createRoutePattern = (pattern) => {
|
|
|
7717
7779
|
const applyOn = (url) => {
|
|
7718
7780
|
const result = matchUrl(parsedPattern, url, {
|
|
7719
7781
|
baseUrl,
|
|
7720
|
-
|
|
7782
|
+
connectionMap,
|
|
7721
7783
|
patternObj: patternObject,
|
|
7722
7784
|
});
|
|
7723
7785
|
|
|
@@ -9531,7 +9593,7 @@ const checkIfLiteralCanBeOptionalWithPatternObj = (
|
|
|
9531
9593
|
const matchUrl = (
|
|
9532
9594
|
parsedPattern,
|
|
9533
9595
|
url,
|
|
9534
|
-
{ baseUrl,
|
|
9596
|
+
{ baseUrl, connectionMap, patternObj = null },
|
|
9535
9597
|
) => {
|
|
9536
9598
|
// Parse the URL
|
|
9537
9599
|
const urlObj = new URL(url, baseUrl);
|
|
@@ -9554,14 +9616,14 @@ const matchUrl = (
|
|
|
9554
9616
|
// OR when URL exactly matches baseUrl (treating baseUrl as root)
|
|
9555
9617
|
if (parsedPattern.segments.length === 0) {
|
|
9556
9618
|
if (pathname === "/" || pathname === "") {
|
|
9557
|
-
return extractSearchParams(urlObj,
|
|
9619
|
+
return extractSearchParams(urlObj, connectionMap);
|
|
9558
9620
|
}
|
|
9559
9621
|
|
|
9560
9622
|
// Special case: if URL exactly matches baseUrl, treat as root route
|
|
9561
9623
|
if (baseUrl) {
|
|
9562
9624
|
const baseUrlObj = new URL(baseUrl);
|
|
9563
9625
|
if (originalPathname === baseUrlObj.pathname) {
|
|
9564
|
-
return extractSearchParams(urlObj,
|
|
9626
|
+
return extractSearchParams(urlObj, connectionMap);
|
|
9565
9627
|
}
|
|
9566
9628
|
}
|
|
9567
9629
|
|
|
@@ -9651,7 +9713,7 @@ const matchUrl = (
|
|
|
9651
9713
|
// If pattern has trailing slash, wildcard, or children, allow extra segments
|
|
9652
9714
|
|
|
9653
9715
|
// Add search parameters
|
|
9654
|
-
const searchParams = extractSearchParams(urlObj,
|
|
9716
|
+
const searchParams = extractSearchParams(urlObj, connectionMap);
|
|
9655
9717
|
Object.assign(params, searchParams);
|
|
9656
9718
|
|
|
9657
9719
|
// Don't add defaults here - rawParams should only contain what's in the URL
|
|
@@ -9663,36 +9725,75 @@ const matchUrl = (
|
|
|
9663
9725
|
/**
|
|
9664
9726
|
* Extract search parameters from URL
|
|
9665
9727
|
*/
|
|
9666
|
-
const extractSearchParams = (urlObj,
|
|
9728
|
+
const extractSearchParams = (urlObj, connectionMap) => {
|
|
9667
9729
|
const params = {};
|
|
9668
9730
|
|
|
9669
|
-
//
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9731
|
+
// Parse the raw query string manually instead of using urlObj.searchParams
|
|
9732
|
+
// This is necessary for array parameters to handle encoded commas correctly.
|
|
9733
|
+
// urlObj.searchParams automatically decodes %2C to , which breaks our comma-based array splitting.
|
|
9734
|
+
//
|
|
9735
|
+
// Design choice: We use comma-separated values (colors=red,blue,green) instead of
|
|
9736
|
+
// the standard repeated parameters (colors=red&colors=blue&colors=green) because:
|
|
9737
|
+
// 1. More human-readable URLs
|
|
9738
|
+
// 2. Shorter URL length
|
|
9739
|
+
// 3. Easier to copy/paste and manually edit
|
|
9740
|
+
if (!urlObj.search) {
|
|
9741
|
+
return params;
|
|
9742
|
+
}
|
|
9743
|
+
|
|
9744
|
+
const rawQuery = urlObj.search.slice(1); // Remove leading ?
|
|
9745
|
+
const pairs = rawQuery.split("&");
|
|
9746
|
+
|
|
9747
|
+
for (const pair of pairs) {
|
|
9748
|
+
const eqIndex = pair.indexOf("=");
|
|
9749
|
+
let key;
|
|
9750
|
+
let rawValue;
|
|
9751
|
+
|
|
9752
|
+
if (eqIndex > -1) {
|
|
9753
|
+
key = decodeURIComponent(pair.slice(0, eqIndex));
|
|
9754
|
+
rawValue = pair.slice(eqIndex + 1); // Keep raw for array processing
|
|
9755
|
+
} else {
|
|
9756
|
+
key = decodeURIComponent(pair);
|
|
9757
|
+
rawValue = "";
|
|
9674
9758
|
}
|
|
9675
|
-
}
|
|
9676
9759
|
|
|
9677
|
-
|
|
9678
|
-
const signalType =
|
|
9760
|
+
const connection = connectionMap.get(key);
|
|
9761
|
+
const signalType = connection ? connection.type : null;
|
|
9679
9762
|
|
|
9680
9763
|
// Cast value based on signal type
|
|
9681
|
-
if (signalType === "
|
|
9682
|
-
|
|
9683
|
-
|
|
9764
|
+
if (signalType === "array") {
|
|
9765
|
+
// Handle array query parameters with proper comma encoding:
|
|
9766
|
+
// ?colors=red,blue,green → ["red", "blue", "green"]
|
|
9767
|
+
// ?colors=red,blue%2Cgreen → ["red", "blue,green"] (comma in value)
|
|
9768
|
+
// ?colors= → []
|
|
9769
|
+
// ?colors → []
|
|
9770
|
+
if (rawValue === "") {
|
|
9771
|
+
params[key] = [];
|
|
9772
|
+
} else {
|
|
9773
|
+
params[key] = rawValue
|
|
9774
|
+
.split(",")
|
|
9775
|
+
.map((item) => decodeURIComponent(item))
|
|
9776
|
+
.filter((item) => item.trim() !== "");
|
|
9777
|
+
}
|
|
9778
|
+
} else if (signalType === "number" || signalType === "float") {
|
|
9779
|
+
const decodedValue = decodeURIComponent(rawValue);
|
|
9780
|
+
const numberValue = Number(decodedValue);
|
|
9781
|
+
params[key] = isNaN(numberValue) ? decodedValue : numberValue;
|
|
9684
9782
|
} else if (signalType === "boolean") {
|
|
9783
|
+
const decodedValue = decodeURIComponent(rawValue);
|
|
9685
9784
|
// Handle boolean query parameters:
|
|
9686
9785
|
// ?walk=true → true
|
|
9687
9786
|
// ?walk=1 → true
|
|
9688
9787
|
// ?walk → true (parameter present without value)
|
|
9689
9788
|
// ?walk=false → false
|
|
9690
9789
|
// ?walk=0 → false
|
|
9691
|
-
params[key] =
|
|
9790
|
+
params[key] =
|
|
9791
|
+
decodedValue === "true" || decodedValue === "1" || decodedValue === "";
|
|
9692
9792
|
} else {
|
|
9693
|
-
params[key] =
|
|
9793
|
+
params[key] = decodeURIComponent(rawValue);
|
|
9694
9794
|
}
|
|
9695
9795
|
}
|
|
9796
|
+
|
|
9696
9797
|
return params;
|
|
9697
9798
|
};
|
|
9698
9799
|
|