@jsenv/navi 0.29.87 → 0.29.88
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 +890 -2235
- package/dist/jsenv_navi.js.map +11 -10
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -2777,6 +2777,11 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2777
2777
|
|
|
2778
2778
|
const DEBUG$2 =
|
|
2779
2779
|
typeof process === "object" ? process.env.DEBUG === "true" : false;
|
|
2780
|
+
const debug$3 = (...args) => {
|
|
2781
|
+
if (DEBUG$2) {
|
|
2782
|
+
console.debug(...args);
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2780
2785
|
|
|
2781
2786
|
// Base URL management
|
|
2782
2787
|
let baseFileUrl;
|
|
@@ -2820,10 +2825,8 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2820
2825
|
|
|
2821
2826
|
// Build pathConnectionMap from path signals
|
|
2822
2827
|
const pathConnectionMap = new Map();
|
|
2823
|
-
const signalSet = new Set();
|
|
2824
2828
|
for (const connection of pathConnections) {
|
|
2825
2829
|
pathConnectionMap.set(connection.paramName, connection);
|
|
2826
|
-
signalSet.add(connection.signal);
|
|
2827
2830
|
}
|
|
2828
2831
|
|
|
2829
2832
|
// Build queryConnectionMap directly from searchParams
|
|
@@ -2835,7 +2838,6 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2835
2838
|
const { signal, options } = registryEntry;
|
|
2836
2839
|
const connection = { paramName, signal, paramType: "query", ...options };
|
|
2837
2840
|
queryConnectionMap.set(paramName, connection);
|
|
2838
|
-
signalSet.add(signal);
|
|
2839
2841
|
}
|
|
2840
2842
|
}
|
|
2841
2843
|
|
|
@@ -2847,16 +2849,10 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2847
2849
|
queryConnectionMap,
|
|
2848
2850
|
});
|
|
2849
2851
|
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
console.debug(
|
|
2855
|
-
`[CustomPattern] Query connections:`,
|
|
2856
|
-
queryConnectionMap.size,
|
|
2857
|
-
);
|
|
2858
|
-
console.debug(`[CustomPattern] SignalSet size:`, signalSet.size);
|
|
2859
|
-
}
|
|
2852
|
+
debug$3(`[CustomPattern] Created pattern:`, parsedPattern);
|
|
2853
|
+
debug$3(`[CustomPattern] Signal connections:`, connections);
|
|
2854
|
+
debug$3(`[CustomPattern] Path connections:`, pathConnectionMap.size);
|
|
2855
|
+
debug$3(`[CustomPattern] Query connections:`, queryConnectionMap.size);
|
|
2860
2856
|
|
|
2861
2857
|
const applyOn = (url) => {
|
|
2862
2858
|
const result = matchUrl(parsedPattern, url, {
|
|
@@ -2866,58 +2862,33 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2866
2862
|
patternObj: patternObject,
|
|
2867
2863
|
});
|
|
2868
2864
|
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
);
|
|
2874
|
-
}
|
|
2865
|
+
debug$3(
|
|
2866
|
+
`[CustomPattern] Matching "${url}" against "${cleanPattern}":`,
|
|
2867
|
+
result,
|
|
2868
|
+
);
|
|
2875
2869
|
|
|
2876
2870
|
return result;
|
|
2877
2871
|
};
|
|
2878
2872
|
|
|
2879
2873
|
const resolveParams = (providedParams = {}) => {
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
// Process path connections for parameter resolution
|
|
2883
|
-
for (const [paramName, connection] of pathConnectionMap) {
|
|
2884
|
-
if (paramName in providedParams) {
|
|
2885
|
-
// Parameter was explicitly provided - always respect explicit parameters
|
|
2886
|
-
continue;
|
|
2887
|
-
}
|
|
2888
|
-
const signalValue = readSignalForUrlBuild(connection);
|
|
2889
|
-
if (signalValue !== undefined) {
|
|
2890
|
-
// Parameter was not provided, check signal value
|
|
2891
|
-
resolvedParams[paramName] = signalValue;
|
|
2892
|
-
}
|
|
2893
|
-
}
|
|
2874
|
+
const resolvedParams = { ...providedParams };
|
|
2894
2875
|
|
|
2895
|
-
//
|
|
2896
|
-
for (const
|
|
2876
|
+
// Signal values for parameters that were not explicitly provided
|
|
2877
|
+
for (const connection of connections) {
|
|
2878
|
+
const { paramName } = connection;
|
|
2897
2879
|
if (paramName in providedParams) {
|
|
2898
2880
|
// Parameter was explicitly provided - always respect explicit parameters
|
|
2899
2881
|
continue;
|
|
2900
2882
|
}
|
|
2901
2883
|
const signalValue = readSignalForUrlBuild(connection);
|
|
2902
2884
|
if (signalValue !== undefined) {
|
|
2903
|
-
// Parameter was not provided, check signal value
|
|
2904
2885
|
resolvedParams[paramName] = signalValue;
|
|
2905
2886
|
}
|
|
2906
2887
|
}
|
|
2907
2888
|
|
|
2908
|
-
//
|
|
2909
|
-
for (const
|
|
2910
|
-
|
|
2911
|
-
continue;
|
|
2912
|
-
}
|
|
2913
|
-
const currentDefault = connection.getDefaultValue();
|
|
2914
|
-
if (currentDefault !== undefined) {
|
|
2915
|
-
resolvedParams[paramName] = currentDefault;
|
|
2916
|
-
}
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
|
-
// Add defaults for query parameters that are still missing
|
|
2920
|
-
for (const [paramName, connection] of queryConnectionMap) {
|
|
2889
|
+
// Defaults for parameters that are still missing
|
|
2890
|
+
for (const connection of connections) {
|
|
2891
|
+
const { paramName } = connection;
|
|
2921
2892
|
if (paramName in resolvedParams) {
|
|
2922
2893
|
continue;
|
|
2923
2894
|
}
|
|
@@ -3035,1754 +3006,623 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3035
3006
|
};
|
|
3036
3007
|
|
|
3037
3008
|
/**
|
|
3038
|
-
*
|
|
3039
|
-
*
|
|
3009
|
+
* Generate-and-verify url building.
|
|
3010
|
+
*
|
|
3011
|
+
* One hard rule: a built url must ROUND-TRIP. Matched back against the
|
|
3012
|
+
* route family and re-resolved (defaults included), it must reproduce
|
|
3013
|
+
* exactly the state it was built from — and it must still match this
|
|
3014
|
+
* route. The verifier makes a wrong url impossible; the candidate order
|
|
3015
|
+
* then decides which of several faithful urls is canonical:
|
|
3016
|
+
*
|
|
3017
|
+
* - highest ancestor first: when everything this route adds over an
|
|
3018
|
+
* ancestor is default values, the ancestor's shorter url is the same
|
|
3019
|
+
* place ("/admin/settings" with default tab IS "/admin")
|
|
3020
|
+
* - the route itself
|
|
3021
|
+
* - descendants, deepest first: a descendant url is used when it carries
|
|
3022
|
+
* custom state that this route's own url cannot encode (a tab the user
|
|
3023
|
+
* selected must survive "/admin" being rebuilt)
|
|
3040
3024
|
*/
|
|
3041
3025
|
|
|
3042
3026
|
/**
|
|
3043
|
-
*
|
|
3027
|
+
* The state a url built by this route must encode, as a Map of
|
|
3028
|
+
* paramName -> { value, connection, segmentIndex, owner, explicit }.
|
|
3029
|
+
* owner is relative to this route: "own" | "ancestor" | "descendant" | "extra".
|
|
3044
3030
|
*
|
|
3045
|
-
*
|
|
3046
|
-
*
|
|
3047
|
-
*
|
|
3048
|
-
*
|
|
3031
|
+
* Sources, in priority order (first write wins):
|
|
3032
|
+
* - explicit params (an explicit undefined pins the param to its default)
|
|
3033
|
+
* - own connection signals (path + query, inherited query included)
|
|
3034
|
+
* - ancestor path params pinned by this pattern's own literals: being on
|
|
3035
|
+
* "/admin/settings" MEANS section=settings, whatever the signal says
|
|
3036
|
+
* - ancestor query signals holding custom values
|
|
3037
|
+
* - reachable descendants' connection signals. A descendant is reachable
|
|
3038
|
+
* when its literals agree with the state collected so far (a tab value
|
|
3039
|
+
* is unreachable while section conflicts with the tab route's literals)
|
|
3040
|
+
* and its extra literals are justified by a param value (building "/"
|
|
3041
|
+
* must not teleport to "/admin").
|
|
3042
|
+
*
|
|
3043
|
+
* Every family signal is read (not only the stored ones) so that a url
|
|
3044
|
+
* computed inside a preact computed() subscribes to all of them.
|
|
3049
3045
|
*/
|
|
3050
|
-
const
|
|
3051
|
-
const
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
const signalValue = readSignalForUrlBuild(connection);
|
|
3065
|
-
if (connection.isCustomValue(signalValue)) {
|
|
3066
|
-
// Only include custom values
|
|
3067
|
-
filtered[paramName] = signalValue;
|
|
3068
|
-
}
|
|
3046
|
+
const buildIntendedState = (explicitParams) => {
|
|
3047
|
+
const intended = new Map();
|
|
3048
|
+
const reachableDescendants = new Set();
|
|
3049
|
+
const setEntry = (
|
|
3050
|
+
name,
|
|
3051
|
+
value,
|
|
3052
|
+
connection,
|
|
3053
|
+
segmentIndex,
|
|
3054
|
+
owner,
|
|
3055
|
+
explicit,
|
|
3056
|
+
pageNaming = false,
|
|
3057
|
+
) => {
|
|
3058
|
+
if (intended.has(name)) {
|
|
3059
|
+
return;
|
|
3069
3060
|
}
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3061
|
+
intended.set(name, {
|
|
3062
|
+
value,
|
|
3063
|
+
connection,
|
|
3064
|
+
segmentIndex,
|
|
3065
|
+
owner,
|
|
3066
|
+
explicit,
|
|
3067
|
+
pageNaming,
|
|
3068
|
+
});
|
|
3069
|
+
};
|
|
3070
|
+
const paramSegmentIndex = (patternObj, name) => {
|
|
3071
|
+
const seg = patternObj.pattern.segments.find(
|
|
3072
|
+
(s) => s.type === "param" && s.name === name,
|
|
3073
|
+
);
|
|
3074
|
+
return seg ? seg.index : undefined;
|
|
3075
|
+
};
|
|
3076
|
+
const findFamilyConnection = (name) => {
|
|
3077
|
+
const own = pathConnectionMap.get(name) || queryConnectionMap.get(name);
|
|
3078
|
+
if (own) {
|
|
3079
|
+
return {
|
|
3080
|
+
connection: own,
|
|
3081
|
+
segmentIndex: paramSegmentIndex(patternObject, name),
|
|
3082
|
+
owner: "own",
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
let ancestor = patternObject.parent;
|
|
3086
|
+
while (ancestor) {
|
|
3087
|
+
const conn =
|
|
3088
|
+
ancestor.pathConnectionMap.get(name) ||
|
|
3089
|
+
ancestor.queryConnectionMap.get(name);
|
|
3090
|
+
if (conn) {
|
|
3091
|
+
return {
|
|
3092
|
+
connection: conn,
|
|
3093
|
+
segmentIndex: paramSegmentIndex(ancestor, name),
|
|
3094
|
+
owner: "ancestor",
|
|
3095
|
+
};
|
|
3087
3096
|
}
|
|
3097
|
+
ancestor = ancestor.parent;
|
|
3088
3098
|
}
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3099
|
+
let found = null;
|
|
3100
|
+
const visit = (patternObj) => {
|
|
3101
|
+
for (const child of patternObj.children) {
|
|
3102
|
+
if (found) {
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
const conn =
|
|
3106
|
+
child.pathConnectionMap.get(name) ||
|
|
3107
|
+
child.queryConnectionMap.get(name);
|
|
3108
|
+
if (conn) {
|
|
3109
|
+
found = {
|
|
3110
|
+
connection: conn,
|
|
3111
|
+
segmentIndex: paramSegmentIndex(child, name),
|
|
3112
|
+
owner: "descendant",
|
|
3113
|
+
};
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
visit(child);
|
|
3117
|
+
}
|
|
3118
|
+
};
|
|
3119
|
+
visit(patternObject);
|
|
3103
3120
|
return (
|
|
3104
|
-
|
|
3121
|
+
found || {
|
|
3122
|
+
connection: undefined,
|
|
3123
|
+
segmentIndex: undefined,
|
|
3124
|
+
owner: "extra",
|
|
3125
|
+
}
|
|
3105
3126
|
);
|
|
3106
|
-
}
|
|
3107
|
-
if (parentCanProvide) {
|
|
3108
|
-
return true;
|
|
3109
|
-
}
|
|
3127
|
+
};
|
|
3110
3128
|
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
(
|
|
3114
|
-
);
|
|
3115
|
-
if (userCanProvide) {
|
|
3116
|
-
return true;
|
|
3129
|
+
for (const [name, value] of Object.entries(explicitParams)) {
|
|
3130
|
+
const { connection, segmentIndex, owner } = findFamilyConnection(name);
|
|
3131
|
+
setEntry(name, value, connection, segmentIndex, owner, true);
|
|
3117
3132
|
}
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3133
|
+
for (const connection of connections) {
|
|
3134
|
+
const { paramName } = connection;
|
|
3135
|
+
const value = readSignalForUrlBuild(connection);
|
|
3136
|
+
if (value !== undefined) {
|
|
3137
|
+
setEntry(
|
|
3138
|
+
paramName,
|
|
3139
|
+
value,
|
|
3140
|
+
connection,
|
|
3141
|
+
paramSegmentIndex(patternObject, paramName),
|
|
3142
|
+
"own",
|
|
3143
|
+
false,
|
|
3144
|
+
);
|
|
3145
|
+
}
|
|
3127
3146
|
}
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
// CRITICAL: Check if parent route can reach all child route's literal segments
|
|
3138
|
-
// A route can only optimize to a descendant if there's a viable path through parameters
|
|
3139
|
-
// to reach all the descendant's literal segments (e.g., "/" cannot reach "/admin"
|
|
3140
|
-
// without a parameter that produces "admin")
|
|
3141
|
-
const childLiterals = childPatternObj.pattern.segments.filter(
|
|
3142
|
-
(segment) => segment.type === "literal",
|
|
3143
|
-
);
|
|
3144
|
-
// Check each child literal segment
|
|
3145
|
-
for (let i = 0; i < childLiterals.length; i++) {
|
|
3146
|
-
const childLiteral = childLiterals[i];
|
|
3147
|
-
const childPosition = childLiteral.index;
|
|
3148
|
-
const literalValue = childLiteral.value;
|
|
3149
|
-
|
|
3150
|
-
// Check what the parent has at this position
|
|
3151
|
-
const parentSegmentAtPosition = parsedPattern.segments.find(
|
|
3152
|
-
(segment) => segment.index === childPosition,
|
|
3153
|
-
);
|
|
3154
|
-
|
|
3155
|
-
if (parentSegmentAtPosition) {
|
|
3156
|
-
if (parentSegmentAtPosition.type === "literal") {
|
|
3157
|
-
// Parent has a literal at this position
|
|
3158
|
-
if (parentSegmentAtPosition.value === literalValue) {
|
|
3159
|
-
// Same literal - no problem
|
|
3160
|
-
continue;
|
|
3161
|
-
}
|
|
3162
|
-
// Different literal - incompatible
|
|
3163
|
-
if (DEBUG$2) {
|
|
3164
|
-
console.debug(
|
|
3165
|
-
`[${pattern}] INCOMPATIBLE with ${childPatternObj.originalPattern}: conflicting literal "${parentSegmentAtPosition.value}" vs "${literalValue}" at position ${childPosition}`,
|
|
3166
|
-
);
|
|
3167
|
-
}
|
|
3168
|
-
return { isCompatible: false, childParams: {} };
|
|
3169
|
-
}
|
|
3170
|
-
if (parentSegmentAtPosition.type === "param") {
|
|
3171
|
-
// Parent has a parameter at this position - child literal can satisfy this parameter
|
|
3172
|
-
// BUT we need to check if the parent's parameter value matches the child's literal
|
|
3173
|
-
|
|
3174
|
-
// Find the parent's parameter value from signals or params
|
|
3175
|
-
const paramName = parentSegmentAtPosition.name;
|
|
3176
|
-
let parentParamValue = params[paramName];
|
|
3177
|
-
|
|
3178
|
-
// If not in params, check signals
|
|
3179
|
-
if (parentParamValue === undefined) {
|
|
3180
|
-
const parentConnection =
|
|
3181
|
-
pathConnectionMap.get(paramName) ||
|
|
3182
|
-
queryConnectionMap.get(paramName);
|
|
3183
|
-
if (parentConnection) {
|
|
3184
|
-
parentParamValue = readSignalForUrlBuild(parentConnection);
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
|
-
|
|
3188
|
-
// If parent has a specific value for this parameter, it must match the child literal
|
|
3189
|
-
if (
|
|
3190
|
-
parentParamValue !== undefined &&
|
|
3191
|
-
parentParamValue !== literalValue
|
|
3192
|
-
) {
|
|
3193
|
-
return { isCompatible: false, childParams: {} };
|
|
3194
|
-
}
|
|
3195
|
-
|
|
3147
|
+
let ancestor = patternObject.parent;
|
|
3148
|
+
while (ancestor) {
|
|
3149
|
+
for (const seg of ancestor.pattern.segments) {
|
|
3150
|
+
if (seg.type !== "param" || intended.has(seg.name)) {
|
|
3151
|
+
continue;
|
|
3152
|
+
}
|
|
3153
|
+
const conn = ancestor.pathConnectionMap.get(seg.name);
|
|
3154
|
+
if (!conn) {
|
|
3196
3155
|
continue;
|
|
3197
3156
|
}
|
|
3157
|
+
const selfSeg = parsedPattern.segments[seg.index];
|
|
3158
|
+
if (selfSeg && selfSeg.type === "literal") {
|
|
3159
|
+
setEntry(seg.name, selfSeg.value, conn, seg.index, "ancestor", false);
|
|
3160
|
+
}
|
|
3198
3161
|
}
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
);
|
|
3162
|
+
for (const [name, conn] of ancestor.queryConnectionMap) {
|
|
3163
|
+
if (intended.has(name)) {
|
|
3164
|
+
continue;
|
|
3165
|
+
}
|
|
3166
|
+
const value = readSignalForUrlBuild(conn);
|
|
3167
|
+
if (value !== undefined && conn.isCustomValue(value)) {
|
|
3168
|
+
setEntry(name, value, conn, undefined, "ancestor", false);
|
|
3206
3169
|
}
|
|
3207
|
-
return { isCompatible: false, childParams: {} };
|
|
3208
3170
|
}
|
|
3171
|
+
ancestor = ancestor.parent;
|
|
3209
3172
|
}
|
|
3210
3173
|
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3174
|
+
const intendedValueAt = (name) => {
|
|
3175
|
+
const entry = intended.get(name);
|
|
3176
|
+
return entry ? entry.value : undefined;
|
|
3177
|
+
};
|
|
3178
|
+
const isDescendantReachable = (descendant) => {
|
|
3179
|
+
const selfSegments = parsedPattern.segments;
|
|
3180
|
+
for (const dSeg of descendant.pattern.segments) {
|
|
3181
|
+
if (dSeg.type !== "literal") {
|
|
3182
|
+
continue;
|
|
3183
|
+
}
|
|
3184
|
+
const selfSeg = selfSegments[dSeg.index];
|
|
3185
|
+
if (selfSeg) {
|
|
3186
|
+
if (selfSeg.type === "literal") {
|
|
3187
|
+
if (selfSeg.value !== dSeg.value) {
|
|
3188
|
+
return false;
|
|
3189
|
+
}
|
|
3190
|
+
continue;
|
|
3191
|
+
}
|
|
3192
|
+
const value = intendedValueAt(selfSeg.name);
|
|
3193
|
+
if (value === undefined || String(value) !== dSeg.value) {
|
|
3194
|
+
return false;
|
|
3195
|
+
}
|
|
3196
|
+
continue;
|
|
3197
|
+
}
|
|
3198
|
+
// beyond this route's own segments: the literal must be justified by
|
|
3199
|
+
// a custom param value at the same position, or by an explicit value
|
|
3200
|
+
const connsAtPosition =
|
|
3201
|
+
patternObject.descendantPathSignals.get(dSeg.index) || [];
|
|
3202
|
+
const justifiedBySignal = connsAtPosition.some((conn) => {
|
|
3203
|
+
const entry = intended.get(conn.paramName);
|
|
3204
|
+
return (
|
|
3205
|
+
entry &&
|
|
3206
|
+
entry.value !== undefined &&
|
|
3207
|
+
conn.isCustomValue(entry.value) &&
|
|
3208
|
+
String(entry.value) === dSeg.value
|
|
3238
3209
|
);
|
|
3210
|
+
});
|
|
3211
|
+
if (justifiedBySignal) {
|
|
3212
|
+
continue;
|
|
3239
3213
|
}
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
if (result.shouldInclude) {
|
|
3244
|
-
childParams[result.paramName] = result.paramValue;
|
|
3245
|
-
}
|
|
3246
|
-
}
|
|
3247
|
-
|
|
3248
|
-
if (DEBUG$2) {
|
|
3249
|
-
console.debug(
|
|
3250
|
-
`[${pattern}] Final compatibility result for ${childPatternObj.originalPattern}: ${isCompatible}`,
|
|
3251
|
-
);
|
|
3252
|
-
}
|
|
3253
|
-
|
|
3254
|
-
return { isCompatible, childParams };
|
|
3255
|
-
};
|
|
3256
|
-
|
|
3257
|
-
/**
|
|
3258
|
-
* Helper: Process a single parameter for child route compatibility
|
|
3259
|
-
*/
|
|
3260
|
-
const processParameterForChildRoute = (item, childParsedPattern) => {
|
|
3261
|
-
let paramName;
|
|
3262
|
-
let paramValue;
|
|
3263
|
-
|
|
3264
|
-
if (item.isUserProvided) {
|
|
3265
|
-
paramName = item.paramName;
|
|
3266
|
-
paramValue = item.userValue;
|
|
3267
|
-
} else {
|
|
3268
|
-
paramName = item.paramName;
|
|
3269
|
-
paramValue = readSignalForUrlBuild(item);
|
|
3270
|
-
// Only include custom parent signal values (not using defaults)
|
|
3271
|
-
if (paramValue === undefined || !item.isCustomValue(paramValue)) {
|
|
3272
|
-
return { isCompatible: true, shouldInclude: false };
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
|
|
3276
|
-
// Check if parameter value matches a literal segment in child pattern
|
|
3277
|
-
const matchesChildLiteral = paramMatchesChildLiteral(
|
|
3278
|
-
paramValue,
|
|
3279
|
-
childParsedPattern,
|
|
3280
|
-
);
|
|
3281
|
-
if (matchesChildLiteral) {
|
|
3282
|
-
// Compatible - parameter value matches child literal
|
|
3283
|
-
return {
|
|
3284
|
-
isCompatible: true,
|
|
3285
|
-
shouldInclude: !item.isUserProvided,
|
|
3286
|
-
paramName,
|
|
3287
|
-
paramValue,
|
|
3288
|
-
};
|
|
3289
|
-
}
|
|
3290
|
-
|
|
3291
|
-
// ROBUST FIX: For path parameters, check semantic compatibility by verifying
|
|
3292
|
-
// that parent parameter values can actually produce the child route structure
|
|
3293
|
-
const isParentPathParam = pathConnectionMap.has(paramName);
|
|
3294
|
-
if (isParentPathParam) {
|
|
3295
|
-
// Check if parent parameter value matches any child literal where it should
|
|
3296
|
-
// The key insight: if parent has a specific parameter value, child route must
|
|
3297
|
-
// be reachable with that value or they're incompatible
|
|
3298
|
-
const parameterCanReachChild = canParameterReachChildRoute(
|
|
3299
|
-
paramName,
|
|
3300
|
-
paramValue,
|
|
3301
|
-
parsedPattern,
|
|
3302
|
-
childParsedPattern,
|
|
3303
|
-
);
|
|
3304
|
-
|
|
3305
|
-
if (!parameterCanReachChild) {
|
|
3306
|
-
return { isCompatible: false };
|
|
3307
|
-
}
|
|
3308
|
-
}
|
|
3309
|
-
|
|
3310
|
-
// Check if this is a query parameter in the parent pattern
|
|
3311
|
-
const isParentQueryParam = queryConnectionMap.has(paramName);
|
|
3312
|
-
if (isParentQueryParam) {
|
|
3313
|
-
// Query parameters are always compatible and can be inherited by child routes
|
|
3314
|
-
return {
|
|
3315
|
-
isCompatible: true,
|
|
3316
|
-
shouldInclude: !item.isUserProvided && !matchesChildLiteral,
|
|
3317
|
-
paramName,
|
|
3318
|
-
paramValue,
|
|
3319
|
-
};
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
// Check for generic parameter-literal conflicts (only for path parameters)
|
|
3323
|
-
if (!matchesChildLiteral) {
|
|
3324
|
-
// Check if this is a path parameter from parent pattern
|
|
3325
|
-
const isParentPathParam = pathConnectionMap.has(paramName);
|
|
3326
|
-
if (isParentPathParam) {
|
|
3327
|
-
// Parameter value (from user or signal) doesn't match this child's literals
|
|
3328
|
-
// Check if child has any literal segments that would conflict with this parameter
|
|
3329
|
-
const hasConflictingLiteral = childParsedPattern.segments.some(
|
|
3330
|
-
(segment) =>
|
|
3331
|
-
segment.type === "literal" && segment.value !== paramValue,
|
|
3214
|
+
const justifiedByExplicit = [...intended.values()].some(
|
|
3215
|
+
(entry) => entry.explicit && String(entry.value) === dSeg.value,
|
|
3332
3216
|
);
|
|
3333
|
-
if (
|
|
3334
|
-
return
|
|
3217
|
+
if (!justifiedByExplicit) {
|
|
3218
|
+
return false;
|
|
3335
3219
|
}
|
|
3336
3220
|
}
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
// Compatible but should only include if from signal (not user-provided)
|
|
3340
|
-
return {
|
|
3341
|
-
isCompatible: true,
|
|
3342
|
-
shouldInclude: !item.isUserProvided && !matchesChildLiteral,
|
|
3343
|
-
paramName,
|
|
3344
|
-
paramValue,
|
|
3221
|
+
return true;
|
|
3345
3222
|
};
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
compatibility,
|
|
3355
|
-
resolvedParams,
|
|
3356
|
-
) => {
|
|
3357
|
-
// CRITICAL: Check if user explicitly passed undefined for parameters that would
|
|
3358
|
-
// normally be used to select this child route via sibling route relationships
|
|
3359
|
-
for (const [paramName, paramValue] of Object.entries(params)) {
|
|
3360
|
-
if (paramValue !== undefined) {
|
|
3361
|
-
continue;
|
|
3362
|
-
}
|
|
3363
|
-
|
|
3364
|
-
// Look for sibling routes (other children of the same parent) that use this parameter
|
|
3365
|
-
const siblingPatternObjs = patternObject.children;
|
|
3366
|
-
for (const siblingPatternObj of siblingPatternObjs) {
|
|
3367
|
-
if (siblingPatternObj === childPatternObj) continue; // Skip self
|
|
3368
|
-
|
|
3369
|
-
// Check if sibling route uses this parameter and get the connection
|
|
3370
|
-
const siblingConnection =
|
|
3371
|
-
siblingPatternObj.pathConnectionMap.get(paramName) ||
|
|
3372
|
-
siblingPatternObj.queryConnectionMap.get(paramName);
|
|
3373
|
-
if (!siblingConnection) {
|
|
3223
|
+
// Query values are inherited under a LOOSER rule than path descent: the
|
|
3224
|
+
// descendant's extra literals only need a param position to exist there,
|
|
3225
|
+
// not a param value naming them. Leaving a sub screen keeps its query
|
|
3226
|
+
// prefs in the url; entering it by literal requires a value that says so.
|
|
3227
|
+
const isDescendantQueryReachable = (descendant) => {
|
|
3228
|
+
const selfSegments = parsedPattern.segments;
|
|
3229
|
+
for (const dSeg of descendant.pattern.segments) {
|
|
3230
|
+
if (dSeg.type !== "literal") {
|
|
3374
3231
|
continue;
|
|
3375
3232
|
}
|
|
3376
|
-
const
|
|
3377
|
-
if (
|
|
3233
|
+
const selfSeg = selfSegments[dSeg.index];
|
|
3234
|
+
if (selfSeg) {
|
|
3235
|
+
if (selfSeg.type === "literal") {
|
|
3236
|
+
if (selfSeg.value !== dSeg.value) {
|
|
3237
|
+
return false;
|
|
3238
|
+
}
|
|
3239
|
+
continue;
|
|
3240
|
+
}
|
|
3241
|
+
const entry = intended.get(selfSeg.name);
|
|
3242
|
+
const conn = pathConnectionMap.get(selfSeg.name);
|
|
3243
|
+
const value =
|
|
3244
|
+
entry && entry.value !== undefined
|
|
3245
|
+
? entry.value
|
|
3246
|
+
: conn
|
|
3247
|
+
? conn.getDefaultValue()
|
|
3248
|
+
: undefined;
|
|
3249
|
+
if (String(value) !== dSeg.value) {
|
|
3250
|
+
return false;
|
|
3251
|
+
}
|
|
3378
3252
|
continue;
|
|
3379
3253
|
}
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
segment.type === "literal" &&
|
|
3385
|
-
segment.value === siblingSignalValue,
|
|
3386
|
-
);
|
|
3387
|
-
if (signalMatchesThisChildLiteral) {
|
|
3388
|
-
// This child route's literal matches the sibling's signal value
|
|
3389
|
-
// User passed undefined to override that signal - don't use this child route
|
|
3390
|
-
if (DEBUG$2) {
|
|
3391
|
-
console.debug(
|
|
3392
|
-
`[${pattern}] Blocking child route ${childPatternObj.originalPattern} because ${paramName}:undefined overrides sibling signal value "${siblingSignalValue}"`,
|
|
3393
|
-
);
|
|
3394
|
-
}
|
|
3254
|
+
const connsAtPosition = patternObject.descendantPathSignals.get(
|
|
3255
|
+
dSeg.index,
|
|
3256
|
+
);
|
|
3257
|
+
if (!connsAtPosition || connsAtPosition.length === 0) {
|
|
3395
3258
|
return false;
|
|
3396
3259
|
}
|
|
3397
3260
|
}
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3261
|
+
return true;
|
|
3262
|
+
};
|
|
3263
|
+
const visitDescendants = (patternObj) => {
|
|
3264
|
+
for (const child of patternObj.children) {
|
|
3265
|
+
const reachable = isDescendantReachable(child);
|
|
3266
|
+
if (reachable) {
|
|
3267
|
+
reachableDescendants.add(child);
|
|
3268
|
+
}
|
|
3269
|
+
const queryReachable = reachable || isDescendantQueryReachable(child);
|
|
3270
|
+
for (const conn of child.connections) {
|
|
3271
|
+
if (conn.inherited) {
|
|
3272
|
+
continue;
|
|
3273
|
+
}
|
|
3274
|
+
const { paramName } = conn;
|
|
3275
|
+
// read even when the value is not kept: subscribes the caller
|
|
3276
|
+
const value = readSignalForUrlBuild(conn);
|
|
3277
|
+
if (intended.has(paramName)) {
|
|
3278
|
+
continue;
|
|
3279
|
+
}
|
|
3280
|
+
if (conn.paramType === "path") {
|
|
3281
|
+
if (!reachable || value === undefined) {
|
|
3282
|
+
continue;
|
|
3283
|
+
}
|
|
3284
|
+
// A param whose other values are declared as literal routes and
|
|
3285
|
+
// which has a default NAMES pages: this route's url stays the
|
|
3286
|
+
// url of the default value, it does not follow the signal. The
|
|
3287
|
+
// value still travels along when something else picks the page
|
|
3288
|
+
// (pageNaming entries build urls but neither trigger descent nor
|
|
3289
|
+
// fail verification).
|
|
3290
|
+
const pageNaming =
|
|
3291
|
+
conn.namedByLiteralRoutes &&
|
|
3292
|
+
conn.getDefaultValue() !== undefined &&
|
|
3293
|
+
!pathConnectionMap.has(paramName);
|
|
3294
|
+
setEntry(
|
|
3295
|
+
paramName,
|
|
3296
|
+
value,
|
|
3297
|
+
conn,
|
|
3298
|
+
paramSegmentIndex(child, paramName),
|
|
3299
|
+
"descendant",
|
|
3300
|
+
false,
|
|
3301
|
+
pageNaming,
|
|
3431
3302
|
);
|
|
3303
|
+
continue;
|
|
3432
3304
|
}
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
if (explicitValue === childSegment.value && connection) {
|
|
3439
|
-
const defaultValue = connection.getDefaultValue();
|
|
3440
|
-
if (explicitValue !== defaultValue) {
|
|
3441
|
-
hasMatchingNonDefaultLiterals = true;
|
|
3305
|
+
if (!queryReachable || value === undefined) {
|
|
3306
|
+
continue;
|
|
3307
|
+
}
|
|
3308
|
+
if (conn.isCustomValue(value)) {
|
|
3309
|
+
setEntry(paramName, value, conn, undefined, "descendant", false);
|
|
3442
3310
|
}
|
|
3443
3311
|
}
|
|
3312
|
+
visitDescendants(child);
|
|
3444
3313
|
}
|
|
3445
|
-
}
|
|
3314
|
+
};
|
|
3315
|
+
visitDescendants(patternObject);
|
|
3446
3316
|
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
return false;
|
|
3450
|
-
}
|
|
3317
|
+
return { intended, reachableDescendants };
|
|
3318
|
+
};
|
|
3451
3319
|
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
const
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
return false; // we carry that param ourselves, we are not its default
|
|
3320
|
+
/**
|
|
3321
|
+
* The natural url of one candidate route for the intended state.
|
|
3322
|
+
* Values are placed where they belong: path params in the path, query
|
|
3323
|
+
* connections and explicit extras in the query string. A meaningful path
|
|
3324
|
+
* value the candidate can express neither as a param nor as a matching
|
|
3325
|
+
* literal disqualifies it — except an explicit value for an ancestor path
|
|
3326
|
+
* param, which travels as a search param ("/admin/settings?section=toto").
|
|
3327
|
+
*/
|
|
3328
|
+
const buildCandidateUrl = (
|
|
3329
|
+
candidate,
|
|
3330
|
+
intended,
|
|
3331
|
+
{ dropMissing, lenient } = {},
|
|
3332
|
+
) => {
|
|
3333
|
+
const urlParams = {};
|
|
3334
|
+
const candidateSegments = candidate.pattern.segments;
|
|
3335
|
+
for (const seg of candidateSegments) {
|
|
3336
|
+
if (seg.type !== "param") {
|
|
3337
|
+
continue;
|
|
3471
3338
|
}
|
|
3472
|
-
|
|
3473
|
-
|
|
3339
|
+
const entry = intended.get(seg.name);
|
|
3340
|
+
if (!entry || entry.value === undefined) {
|
|
3341
|
+
continue;
|
|
3474
3342
|
}
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
const childParams = { ...compatibility.childParams };
|
|
3481
|
-
|
|
3482
|
-
for (const [paramName, connection] of new Map([
|
|
3483
|
-
...childPatternObj.pathConnectionMap,
|
|
3484
|
-
...childPatternObj.queryConnectionMap,
|
|
3485
|
-
])) {
|
|
3486
|
-
// Check if parameter was explicitly provided by user
|
|
3487
|
-
const hasExplicitParam = paramName in params;
|
|
3488
|
-
const explicitValue = params[paramName];
|
|
3489
|
-
|
|
3490
|
-
if (hasExplicitParam) {
|
|
3491
|
-
// User explicitly provided this parameter - use their value
|
|
3492
|
-
childParams[paramName] = explicitValue;
|
|
3493
|
-
if (
|
|
3494
|
-
explicitValue !== undefined &&
|
|
3495
|
-
connection.isCustomValue(explicitValue)
|
|
3496
|
-
) {
|
|
3497
|
-
hasActiveParams = true;
|
|
3498
|
-
}
|
|
3499
|
-
} else {
|
|
3500
|
-
const signalValue = readSignalForUrlBuild(connection);
|
|
3501
|
-
if (signalValue !== undefined) {
|
|
3502
|
-
// No explicit override - use signal value
|
|
3503
|
-
childParams[paramName] = signalValue;
|
|
3504
|
-
if (
|
|
3505
|
-
connection.isCustomValue(signalValue) &&
|
|
3506
|
-
!thisUrlAlreadyMeansAParamValue(connection)
|
|
3507
|
-
) {
|
|
3508
|
-
hasActiveParams = true;
|
|
3509
|
-
}
|
|
3510
|
-
}
|
|
3343
|
+
const meaningful = entry.connection
|
|
3344
|
+
? entry.connection.isCustomValue(entry.value)
|
|
3345
|
+
: true;
|
|
3346
|
+
if (meaningful) {
|
|
3347
|
+
urlParams[seg.name] = entry.value;
|
|
3511
3348
|
}
|
|
3512
3349
|
}
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
(
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3350
|
+
for (const [name, entry] of intended) {
|
|
3351
|
+
if (name in urlParams) {
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
3354
|
+
if (entry.value === undefined) {
|
|
3355
|
+
continue;
|
|
3356
|
+
}
|
|
3357
|
+
const conn = entry.connection;
|
|
3358
|
+
if (!conn) {
|
|
3359
|
+
if (entry.explicit) {
|
|
3360
|
+
urlParams[name] = entry.value;
|
|
3523
3361
|
}
|
|
3524
|
-
|
|
3525
|
-
}
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
const childConnection =
|
|
3535
|
-
childPatternObj.pathConnectionMap.get(paramName) ||
|
|
3536
|
-
childPatternObj.queryConnectionMap.get(paramName);
|
|
3537
|
-
if (childConnection) {
|
|
3538
|
-
const childDefault = childConnection.getDefaultValue();
|
|
3539
|
-
return value !== childDefault;
|
|
3540
|
-
}
|
|
3541
|
-
|
|
3542
|
-
// Check if this parameter has a default value in parent's connections (current pattern)
|
|
3543
|
-
const parentConnection =
|
|
3544
|
-
pathConnectionMap.get(paramName) || queryConnectionMap.get(paramName);
|
|
3545
|
-
if (parentConnection) {
|
|
3546
|
-
const parentDefault = parentConnection.getDefaultValue();
|
|
3547
|
-
return value !== parentDefault;
|
|
3548
|
-
}
|
|
3549
|
-
|
|
3550
|
-
return true; // Non-connection parameters are considered non-default
|
|
3551
|
-
},
|
|
3552
|
-
);
|
|
3553
|
-
|
|
3554
|
-
const hasNonDefaultProvidedParams = nonDefaultParams.length > 0;
|
|
3555
|
-
|
|
3556
|
-
// Use child route if:
|
|
3557
|
-
// 1. Child has active non-default parameters, OR
|
|
3558
|
-
// 2. User provided non-default params AND child can be built completely, OR
|
|
3559
|
-
// 3. User provided params that match child literal segments AND are non-default values
|
|
3560
|
-
// EXCEPT: Don't use child if parent can produce cleaner URL by omitting defaults
|
|
3561
|
-
let shouldUse =
|
|
3562
|
-
hasActiveParams ||
|
|
3563
|
-
(hasNonDefaultProvidedParams && canBuildChildCompletely) ||
|
|
3564
|
-
(hasMatchingNonDefaultLiterals && canBuildChildCompletely);
|
|
3565
|
-
|
|
3566
|
-
if (DEBUG$2) {
|
|
3567
|
-
console.debug(
|
|
3568
|
-
`[${pattern}] shouldUseChildRoute decision for ${childPatternObj.originalPattern}:`,
|
|
3569
|
-
{
|
|
3570
|
-
hasActiveParams,
|
|
3571
|
-
hasNonDefaultProvidedParams,
|
|
3572
|
-
canBuildChildCompletely,
|
|
3573
|
-
shouldUse,
|
|
3574
|
-
},
|
|
3575
|
-
);
|
|
3576
|
-
}
|
|
3577
|
-
|
|
3578
|
-
// Optimization: Check if child would include literal segments that represent default values
|
|
3579
|
-
if (shouldUse) {
|
|
3580
|
-
// Check if child pattern has literal segments that correspond to default parameter values
|
|
3581
|
-
const childLiterals = childPatternObj.pattern.segments
|
|
3582
|
-
.filter((seg) => seg.type === "literal")
|
|
3583
|
-
.map((seg) => seg.value);
|
|
3584
|
-
|
|
3585
|
-
const parentLiterals = parsedPattern.segments
|
|
3586
|
-
.filter((seg) => seg.type === "literal")
|
|
3587
|
-
.map((seg) => seg.value);
|
|
3588
|
-
|
|
3589
|
-
// If child has more literal segments than parent, check if the extra ones are defaults
|
|
3590
|
-
if (childLiterals.length > parentLiterals.length) {
|
|
3591
|
-
const extraLiterals = childLiterals.slice(parentLiterals.length);
|
|
3592
|
-
|
|
3593
|
-
// Check if any extra literal matches a default parameter value
|
|
3594
|
-
// BUT only skip if user didn't explicitly provide that parameter AND
|
|
3595
|
-
// both conditions are true:
|
|
3596
|
-
// 1. The parameters that would cause us to use this child route are defaults
|
|
3597
|
-
// 2. The child route doesn't have non-default parameters that would be lost
|
|
3598
|
-
let childSpecificParamsAreDefaults = true;
|
|
3599
|
-
|
|
3600
|
-
// Check if parameters that determine child selection are non-default
|
|
3601
|
-
// OR if any descendant parameters indicate explicit navigation
|
|
3602
|
-
for (const [paramName, connection] of new Map([
|
|
3603
|
-
...pathConnectionMap,
|
|
3604
|
-
...queryConnectionMap,
|
|
3605
|
-
])) {
|
|
3606
|
-
const currentDefault = connection.getDefaultValue(); // Use current dynamic default
|
|
3607
|
-
const resolvedValue = resolvedParams[paramName];
|
|
3608
|
-
const userProvidedParam = paramName in params;
|
|
3609
|
-
|
|
3610
|
-
if (extraLiterals.includes(currentDefault)) {
|
|
3611
|
-
// This literal corresponds to a parameter in the parent
|
|
3612
|
-
if (
|
|
3613
|
-
userProvidedParam ||
|
|
3614
|
-
(resolvedValue !== undefined &&
|
|
3615
|
-
connection.isCustomValue(resolvedValue))
|
|
3616
|
-
) {
|
|
3617
|
-
// Parameter was explicitly provided or has custom value - child is needed
|
|
3618
|
-
childSpecificParamsAreDefaults = false;
|
|
3619
|
-
break;
|
|
3620
|
-
}
|
|
3621
|
-
}
|
|
3622
|
-
}
|
|
3623
|
-
|
|
3624
|
-
// Additional check: if child route has path parameters that are non-default,
|
|
3625
|
-
// this indicates explicit navigation even if structural parameters happen to be default
|
|
3626
|
-
// (Query parameters don't count as they don't indicate structural navigation)
|
|
3627
|
-
if (childSpecificParamsAreDefaults) {
|
|
3628
|
-
for (const childConnection of childPatternObj.connections) {
|
|
3629
|
-
const childParamName = childConnection.paramName;
|
|
3630
|
-
const childDefaultValue = childConnection.getDefaultValue();
|
|
3631
|
-
const childResolvedValue = resolvedParams[childParamName];
|
|
3632
|
-
|
|
3633
|
-
// Only consider path parameters, not query parameters
|
|
3634
|
-
const isPathParam = childPatternObj.pattern.segments.some(
|
|
3635
|
-
(seg) => seg.type === "param" && seg.name === childParamName,
|
|
3636
|
-
);
|
|
3637
|
-
|
|
3638
|
-
if (
|
|
3639
|
-
isPathParam &&
|
|
3640
|
-
childResolvedValue !== undefined &&
|
|
3641
|
-
childResolvedValue !== childDefaultValue
|
|
3642
|
-
) {
|
|
3643
|
-
// Child has non-default path parameters, indicating explicit navigation
|
|
3644
|
-
childSpecificParamsAreDefaults = false;
|
|
3645
|
-
if (DEBUG$2) {
|
|
3646
|
-
console.debug(
|
|
3647
|
-
`[${pattern}] Child has non-default path parameter '${childParamName}=${childResolvedValue}' (default: ${childDefaultValue}) - indicates explicit navigation`,
|
|
3648
|
-
);
|
|
3649
|
-
}
|
|
3650
|
-
break;
|
|
3651
|
-
}
|
|
3652
|
-
}
|
|
3653
|
-
}
|
|
3654
|
-
|
|
3655
|
-
// When structural parameters (those that determine child selection) are defaults,
|
|
3656
|
-
// prefer parent route ONLY if child doesn't have any non-default parameters
|
|
3657
|
-
if (childSpecificParamsAreDefaults && !hasActiveParams) {
|
|
3658
|
-
for (const [paramName, connection] of new Map([
|
|
3659
|
-
...pathConnectionMap,
|
|
3660
|
-
...queryConnectionMap,
|
|
3661
|
-
])) {
|
|
3662
|
-
const currentDefault = connection.getDefaultValue(); // Use current dynamic default
|
|
3663
|
-
const userProvidedParam = paramName in params;
|
|
3664
|
-
|
|
3665
|
-
if (extraLiterals.includes(currentDefault) && !userProvidedParam) {
|
|
3666
|
-
// This child includes a literal that represents a default value
|
|
3667
|
-
// AND user didn't explicitly provide this parameter
|
|
3668
|
-
// When structural parameters are defaults, prefer parent for cleaner URL
|
|
3669
|
-
shouldUse = false;
|
|
3670
|
-
if (DEBUG$2) {
|
|
3671
|
-
console.debug(
|
|
3672
|
-
`[${pattern}] Preferring parent over child - child includes default literal '${currentDefault}' for param '${paramName}' (structural parameter is default and no active params)`,
|
|
3673
|
-
);
|
|
3674
|
-
}
|
|
3675
|
-
break;
|
|
3676
|
-
}
|
|
3677
|
-
}
|
|
3678
|
-
} else if (DEBUG$2) {
|
|
3679
|
-
console.debug(
|
|
3680
|
-
`[${pattern}] Using child route - parameters that determine child selection are non-default or child has active params`,
|
|
3681
|
-
);
|
|
3682
|
-
}
|
|
3683
|
-
}
|
|
3684
|
-
}
|
|
3685
|
-
|
|
3686
|
-
if (DEBUG$2 && shouldUse) {
|
|
3687
|
-
console.debug(
|
|
3688
|
-
`[${pattern}] Will use child route ${childPatternObj.originalPattern}`,
|
|
3689
|
-
);
|
|
3690
|
-
}
|
|
3691
|
-
|
|
3692
|
-
return shouldUse;
|
|
3693
|
-
};
|
|
3694
|
-
|
|
3695
|
-
/**
|
|
3696
|
-
* Helper: Build URL for selected child route with proper parameter filtering
|
|
3697
|
-
*/
|
|
3698
|
-
const buildChildRouteUrl = (
|
|
3699
|
-
childPatternObj,
|
|
3700
|
-
params,
|
|
3701
|
-
parentResolvedParams = {},
|
|
3702
|
-
) => {
|
|
3703
|
-
// Start with child signal values
|
|
3704
|
-
const baseParams = {};
|
|
3705
|
-
for (const [paramName, connection] of new Map([
|
|
3706
|
-
...childPatternObj.pathConnectionMap,
|
|
3707
|
-
...childPatternObj.queryConnectionMap,
|
|
3708
|
-
])) {
|
|
3709
|
-
// Check if parameter was explicitly provided by user
|
|
3710
|
-
const hasExplicitParam = paramName in params;
|
|
3711
|
-
const explicitValue = params[paramName];
|
|
3712
|
-
|
|
3713
|
-
if (hasExplicitParam) {
|
|
3714
|
-
// User explicitly provided this parameter - use their value (even if undefined)
|
|
3715
|
-
if (explicitValue !== undefined) {
|
|
3716
|
-
baseParams[paramName] = explicitValue;
|
|
3717
|
-
}
|
|
3718
|
-
// If explicitly undefined, don't include it (which means don't use child route)
|
|
3719
|
-
} else {
|
|
3720
|
-
const signalValue = readSignalForUrlBuild(connection);
|
|
3721
|
-
if (
|
|
3722
|
-
signalValue !== undefined &&
|
|
3723
|
-
connection.isCustomValue(signalValue)
|
|
3724
|
-
) {
|
|
3725
|
-
// No explicit override - use signal value if non-default
|
|
3726
|
-
baseParams[paramName] = signalValue;
|
|
3727
|
-
}
|
|
3728
|
-
}
|
|
3729
|
-
}
|
|
3730
|
-
|
|
3731
|
-
// Collect parameters from ALL ancestor routes in the hierarchy (not just immediate parent)
|
|
3732
|
-
const collectAncestorParameters = (currentPatternObj) => {
|
|
3733
|
-
if (!currentPatternObj?.parent) {
|
|
3734
|
-
return; // No more ancestors
|
|
3735
|
-
}
|
|
3736
|
-
|
|
3737
|
-
const parentPatternObj = currentPatternObj.parent;
|
|
3738
|
-
|
|
3739
|
-
// Add parent's signal parameters (query params only, not path params)
|
|
3740
|
-
// Path params from ancestors are structural path segments, not inheritable
|
|
3741
|
-
for (const connection of parentPatternObj.connections) {
|
|
3742
|
-
if (connection.paramType === "path") {
|
|
3743
|
-
continue;
|
|
3744
|
-
}
|
|
3745
|
-
const { paramName } = connection;
|
|
3746
|
-
|
|
3747
|
-
// Skip if child route already handles this parameter
|
|
3748
|
-
if (
|
|
3749
|
-
childPatternObj.pathConnectionMap.has(paramName) ||
|
|
3750
|
-
childPatternObj.queryConnectionMap.has(paramName)
|
|
3751
|
-
) {
|
|
3752
|
-
continue; // Child route handles this parameter directly
|
|
3753
|
-
}
|
|
3754
|
-
|
|
3755
|
-
// Skip if parameter is already collected
|
|
3756
|
-
if (paramName in baseParams) {
|
|
3757
|
-
continue; // Already have this parameter
|
|
3758
|
-
}
|
|
3759
|
-
|
|
3760
|
-
const signalValue = readSignalForUrlBuild(connection);
|
|
3761
|
-
// Only include custom signal values (not using defaults)
|
|
3762
|
-
if (
|
|
3763
|
-
signalValue !== undefined &&
|
|
3764
|
-
connection.isCustomValue(signalValue)
|
|
3765
|
-
) {
|
|
3766
|
-
// Skip if parameter is consumed by child's literal path segments
|
|
3767
|
-
const isConsumedByChildPath = childPatternObj.pattern.segments.some(
|
|
3768
|
-
(segment) =>
|
|
3769
|
-
segment.type === "literal" && segment.value === signalValue,
|
|
3770
|
-
);
|
|
3771
|
-
if (!isConsumedByChildPath) {
|
|
3772
|
-
baseParams[paramName] = signalValue;
|
|
3773
|
-
}
|
|
3774
|
-
}
|
|
3775
|
-
}
|
|
3776
|
-
|
|
3777
|
-
// Recursively collect from higher ancestors
|
|
3778
|
-
collectAncestorParameters(parentPatternObj);
|
|
3779
|
-
};
|
|
3780
|
-
|
|
3781
|
-
// Start collecting from the child's parent
|
|
3782
|
-
collectAncestorParameters(childPatternObj);
|
|
3783
|
-
|
|
3784
|
-
// Add parent parameters from the immediate calling context
|
|
3785
|
-
for (const [paramName, parentValue] of Object.entries(
|
|
3786
|
-
parentResolvedParams,
|
|
3787
|
-
)) {
|
|
3788
|
-
// Skip if already collected from ancestors or child handles it
|
|
3789
|
-
if (paramName in baseParams) {
|
|
3790
|
-
continue;
|
|
3791
|
-
}
|
|
3792
|
-
|
|
3793
|
-
// Skip if child route already handles this parameter
|
|
3794
|
-
if (
|
|
3795
|
-
childPatternObj.pathConnectionMap.has(paramName) ||
|
|
3796
|
-
childPatternObj.queryConnectionMap.has(paramName)
|
|
3797
|
-
) {
|
|
3798
|
-
continue; // Child route handles this parameter directly
|
|
3799
|
-
}
|
|
3800
|
-
|
|
3801
|
-
// Skip if parameter is consumed by child's literal path segments
|
|
3802
|
-
const isConsumedByChildPath = childPatternObj.pattern.segments.some(
|
|
3803
|
-
(segment) =>
|
|
3804
|
-
segment.type === "literal" && segment.value === parentValue,
|
|
3805
|
-
);
|
|
3806
|
-
if (isConsumedByChildPath) {
|
|
3807
|
-
continue; // Parameter is consumed by child's literal path
|
|
3808
|
-
}
|
|
3809
|
-
|
|
3810
|
-
// Check if parent parameter is at default value
|
|
3811
|
-
const parentConnection =
|
|
3812
|
-
pathConnectionMap.get(paramName) || queryConnectionMap.get(paramName);
|
|
3813
|
-
const parentDefault = parentConnection
|
|
3814
|
-
? parentConnection.getDefaultValue()
|
|
3815
|
-
: undefined;
|
|
3816
|
-
if (parentValue === parentDefault) {
|
|
3817
|
-
continue; // Don't inherit default values
|
|
3818
|
-
}
|
|
3819
|
-
|
|
3820
|
-
// Inherit this parameter as it's not handled by child and not at default
|
|
3821
|
-
baseParams[paramName] = parentValue;
|
|
3822
|
-
}
|
|
3823
|
-
|
|
3824
|
-
// Apply user params with filtering logic
|
|
3825
|
-
for (const [paramName, userValue] of Object.entries(params)) {
|
|
3826
|
-
const childConnection =
|
|
3827
|
-
childPatternObj.pathConnectionMap.get(paramName) ||
|
|
3828
|
-
childPatternObj.queryConnectionMap.get(paramName);
|
|
3829
|
-
|
|
3830
|
-
if (childConnection) {
|
|
3831
|
-
// Only include if it's a custom value (not default)
|
|
3832
|
-
if (childConnection.isCustomValue(userValue)) {
|
|
3833
|
-
baseParams[paramName] = userValue;
|
|
3834
|
-
} else {
|
|
3835
|
-
// User provided the default value - complete omission
|
|
3836
|
-
delete baseParams[paramName];
|
|
3837
|
-
}
|
|
3838
|
-
} else {
|
|
3839
|
-
// Check if param corresponds to a literal segment in child pattern
|
|
3840
|
-
const isConsumedByChildPath = childPatternObj.pattern.segments.some(
|
|
3841
|
-
(segment) =>
|
|
3842
|
-
segment.type === "literal" && segment.value === userValue,
|
|
3843
|
-
);
|
|
3844
|
-
|
|
3845
|
-
if (!isConsumedByChildPath) {
|
|
3846
|
-
// Not consumed by child path, keep it as query param
|
|
3847
|
-
baseParams[paramName] = userValue;
|
|
3848
|
-
}
|
|
3849
|
-
}
|
|
3850
|
-
}
|
|
3851
|
-
|
|
3852
|
-
// Build child URL using buildUrl (not buildMostPreciseUrl) to prevent recursion
|
|
3853
|
-
const childUrl = buildUrlFromPattern(
|
|
3854
|
-
childPatternObj.pattern,
|
|
3855
|
-
baseParams,
|
|
3856
|
-
childPatternObj.originalPattern,
|
|
3857
|
-
childPatternObj,
|
|
3858
|
-
);
|
|
3859
|
-
|
|
3860
|
-
if (childUrl && !childUrl.includes(":")) {
|
|
3861
|
-
// Check for parent optimization before returning
|
|
3862
|
-
const optimizedUrl = checkChildParentOptimization(
|
|
3863
|
-
childPatternObj,
|
|
3864
|
-
childUrl,
|
|
3865
|
-
baseParams,
|
|
3866
|
-
);
|
|
3867
|
-
return optimizedUrl || childUrl;
|
|
3868
|
-
}
|
|
3869
|
-
|
|
3870
|
-
return null;
|
|
3871
|
-
};
|
|
3872
|
-
|
|
3873
|
-
/**
|
|
3874
|
-
* Helper: Check if parent route optimization applies to child route
|
|
3875
|
-
*/
|
|
3876
|
-
const checkChildParentOptimization = (
|
|
3877
|
-
childPatternObj,
|
|
3878
|
-
childUrl,
|
|
3879
|
-
baseParams,
|
|
3880
|
-
) => {
|
|
3881
|
-
const childParent = childPatternObj.parent;
|
|
3882
|
-
|
|
3883
|
-
if (childParent && childParent.originalPattern === pattern) {
|
|
3884
|
-
// Check if child path segments correspond to parent's default path parameters
|
|
3885
|
-
// If so, we can optimize to use parent's path but preserve child's query parameters
|
|
3886
|
-
|
|
3887
|
-
let canOptimizeToParent = true;
|
|
3888
|
-
const parentPathDefaults = {};
|
|
3889
|
-
|
|
3890
|
-
// Check each segment in child vs parent to see if child literals match parent defaults
|
|
3891
|
-
for (
|
|
3892
|
-
let i = 0;
|
|
3893
|
-
i < childPatternObj.pattern.segments.length &&
|
|
3894
|
-
i < parsedPattern.segments.length;
|
|
3895
|
-
i++
|
|
3896
|
-
) {
|
|
3897
|
-
const childSegment = childPatternObj.pattern.segments[i];
|
|
3898
|
-
const parentSegment = parsedPattern.segments[i];
|
|
3899
|
-
|
|
3900
|
-
if (
|
|
3901
|
-
childSegment.type === "literal" &&
|
|
3902
|
-
parentSegment &&
|
|
3903
|
-
parentSegment.type === "param"
|
|
3904
|
-
) {
|
|
3905
|
-
// Child has literal where parent has parameter - check if literal matches default
|
|
3906
|
-
const paramName = parentSegment.name;
|
|
3907
|
-
const connection =
|
|
3908
|
-
pathConnectionMap.get(paramName) ||
|
|
3909
|
-
queryConnectionMap.get(paramName);
|
|
3910
|
-
|
|
3911
|
-
if (connection) {
|
|
3912
|
-
const defaultValue = connection.getDefaultValue();
|
|
3913
|
-
if (childSegment.value === defaultValue) {
|
|
3914
|
-
// Child literal matches parent default - this is optimizable
|
|
3915
|
-
parentPathDefaults[paramName] = defaultValue;
|
|
3916
|
-
} else {
|
|
3917
|
-
// Child literal doesn't match parent default - can't optimize
|
|
3918
|
-
canOptimizeToParent = false;
|
|
3919
|
-
break;
|
|
3920
|
-
}
|
|
3921
|
-
} else {
|
|
3922
|
-
canOptimizeToParent = false;
|
|
3923
|
-
break;
|
|
3924
|
-
}
|
|
3925
|
-
}
|
|
3926
|
-
}
|
|
3927
|
-
|
|
3928
|
-
if (canOptimizeToParent && Object.keys(parentPathDefaults).length > 0) {
|
|
3929
|
-
if (DEBUG$2) {
|
|
3930
|
-
console.debug(
|
|
3931
|
-
`[${pattern}] checkChildParentOptimization: checking child ${childPatternObj.originalPattern}`,
|
|
3932
|
-
{ parentPathDefaults, canOptimizeToParent },
|
|
3933
|
-
);
|
|
3934
|
-
}
|
|
3935
|
-
|
|
3936
|
-
// CRITICAL: Check if child route has non-default path parameters
|
|
3937
|
-
// If it does, don't optimize away the child route structure
|
|
3938
|
-
for (const [
|
|
3939
|
-
paramName,
|
|
3940
|
-
connection,
|
|
3941
|
-
] of childPatternObj.pathConnectionMap) {
|
|
3942
|
-
const signalValue = readSignalForUrlBuild(connection);
|
|
3943
|
-
if (
|
|
3944
|
-
signalValue !== undefined &&
|
|
3945
|
-
connection.isCustomValue(signalValue)
|
|
3946
|
-
) {
|
|
3947
|
-
// Child has non-default path parameters - don't optimize away the structure
|
|
3948
|
-
if (DEBUG$2) {
|
|
3949
|
-
console.debug(
|
|
3950
|
-
`[${pattern}] Not optimizing child route because it has non-default path parameter '${paramName}=${signalValue}'`,
|
|
3951
|
-
);
|
|
3952
|
-
}
|
|
3953
|
-
return null;
|
|
3954
|
-
}
|
|
3955
|
-
}
|
|
3956
|
-
|
|
3957
|
-
// Check if child has non-default query parameters that should be preserved
|
|
3958
|
-
const nonDefaultQueryParams = {};
|
|
3959
|
-
|
|
3960
|
-
for (const [
|
|
3961
|
-
paramName,
|
|
3962
|
-
connection,
|
|
3963
|
-
] of childPatternObj.queryConnectionMap) {
|
|
3964
|
-
const signalValue = readSignalForUrlBuild(connection);
|
|
3965
|
-
if (
|
|
3966
|
-
signalValue !== undefined &&
|
|
3967
|
-
connection.isCustomValue(signalValue)
|
|
3968
|
-
) {
|
|
3969
|
-
nonDefaultQueryParams[paramName] = signalValue;
|
|
3970
|
-
}
|
|
3971
|
-
}
|
|
3972
|
-
|
|
3973
|
-
// Also include any query parameters from baseParams
|
|
3974
|
-
for (const [paramName, paramValue] of Object.entries(baseParams)) {
|
|
3975
|
-
// Check if this parameter is not a path parameter that we're optimizing away
|
|
3976
|
-
if (!(paramName in parentPathDefaults)) {
|
|
3977
|
-
nonDefaultQueryParams[paramName] = paramValue;
|
|
3978
|
-
}
|
|
3979
|
-
}
|
|
3980
|
-
|
|
3981
|
-
// Build optimized URL using parent path but child's query parameters
|
|
3982
|
-
// Always optimize when we can, even if there are no query parameters
|
|
3983
|
-
const parentParams = { ...nonDefaultQueryParams };
|
|
3984
|
-
|
|
3985
|
-
// Remove default path parameters to get clean parent URL
|
|
3986
|
-
for (const defaultParam of Object.keys(parentPathDefaults)) {
|
|
3987
|
-
delete parentParams[defaultParam];
|
|
3988
|
-
}
|
|
3989
|
-
|
|
3990
|
-
const optimizedUrl = buildUrlFromPattern(
|
|
3991
|
-
parsedPattern,
|
|
3992
|
-
parentParams,
|
|
3993
|
-
pattern,
|
|
3994
|
-
patternObject,
|
|
3995
|
-
);
|
|
3996
|
-
|
|
3997
|
-
if (DEBUG$2) {
|
|
3998
|
-
console.debug(
|
|
3999
|
-
`[${pattern}] Optimizing child route ${childPatternObj.originalPattern} to parent with query params:`,
|
|
4000
|
-
{ parentPathDefaults, nonDefaultQueryParams, optimizedUrl },
|
|
4001
|
-
);
|
|
4002
|
-
}
|
|
4003
|
-
|
|
4004
|
-
return optimizedUrl;
|
|
4005
|
-
}
|
|
4006
|
-
}
|
|
4007
|
-
|
|
4008
|
-
return null;
|
|
4009
|
-
};
|
|
4010
|
-
|
|
4011
|
-
const buildMostPreciseUrl = (params = {}) => {
|
|
4012
|
-
if (DEBUG$2) {
|
|
4013
|
-
console.debug(`[${pattern}] buildMostPreciseUrl called`);
|
|
4014
|
-
}
|
|
4015
|
-
|
|
4016
|
-
// Use the pattern object's signalSet (updated by setupPatterns)
|
|
4017
|
-
const effectiveSignalSet = patternObject.signalSet;
|
|
4018
|
-
|
|
4019
|
-
// Access signal.value to trigger dependency tracking
|
|
4020
|
-
if (DEBUG$2) {
|
|
4021
|
-
console.debug(
|
|
4022
|
-
`[${pattern}] Reading ${effectiveSignalSet.size} signals for reactive dependencies`,
|
|
4023
|
-
);
|
|
4024
|
-
}
|
|
4025
|
-
// for (const signal of effectiveSignalSet) {
|
|
4026
|
-
// // Access signal.value to trigger dependency tracking
|
|
4027
|
-
// // eslint-disable-next-line no-unused-expressions
|
|
4028
|
-
// signal.value; // This line is critical for signal reactivity - when commented out, routes may not update properly
|
|
4029
|
-
// }
|
|
4030
|
-
|
|
4031
|
-
// Step 1: Resolve and clean parameters
|
|
4032
|
-
const resolvedParams = resolveParams(params);
|
|
4033
|
-
|
|
4034
|
-
// Step 2: Try ancestors first - find the highest ancestor that works
|
|
4035
|
-
const parentPattern = patternObject.parent;
|
|
4036
|
-
|
|
4037
|
-
if (DEBUG$2 && parentPattern) {
|
|
4038
|
-
console.debug(
|
|
4039
|
-
`[${pattern}] Available ancestor:`,
|
|
4040
|
-
parentPattern.originalPattern,
|
|
4041
|
-
);
|
|
4042
|
-
}
|
|
4043
|
-
|
|
4044
|
-
let bestAncestorUrl = null;
|
|
4045
|
-
if (parentPattern) {
|
|
4046
|
-
// Skip root route - never use as optimization target
|
|
4047
|
-
if (parentPattern.originalPattern !== "/") {
|
|
4048
|
-
// Try to use this ancestor and traverse up to find the highest possible
|
|
4049
|
-
const highestAncestorUrl = findHighestAncestor(
|
|
4050
|
-
parentPattern,
|
|
4051
|
-
resolvedParams,
|
|
4052
|
-
);
|
|
4053
|
-
if (DEBUG$2) {
|
|
4054
|
-
console.debug(
|
|
4055
|
-
`[${pattern}] Highest ancestor from ${parentPattern.originalPattern}:`,
|
|
4056
|
-
highestAncestorUrl,
|
|
4057
|
-
);
|
|
4058
|
-
}
|
|
4059
|
-
|
|
4060
|
-
if (highestAncestorUrl) {
|
|
4061
|
-
bestAncestorUrl = highestAncestorUrl;
|
|
4062
|
-
}
|
|
4063
|
-
}
|
|
4064
|
-
}
|
|
4065
|
-
|
|
4066
|
-
if (bestAncestorUrl) {
|
|
4067
|
-
if (DEBUG$2) {
|
|
4068
|
-
console.debug(`[${pattern}] Using ancestor optimization`);
|
|
4069
|
-
}
|
|
4070
|
-
return bestAncestorUrl;
|
|
4071
|
-
}
|
|
4072
|
-
|
|
4073
|
-
// Step 3: Remove default values for normal URL building
|
|
4074
|
-
let finalParams = removeDefaultValues(resolvedParams);
|
|
4075
|
-
|
|
4076
|
-
// Step 4: Try descendants - find the deepest descendant that works
|
|
4077
|
-
const childPatternObjs = patternObject.children;
|
|
4078
|
-
|
|
4079
|
-
let bestDescendantUrl = null;
|
|
4080
|
-
for (const childPatternObj of childPatternObjs) {
|
|
4081
|
-
const deepestDescendantUrl = findDeepestDescendant(
|
|
4082
|
-
childPatternObj,
|
|
4083
|
-
params,
|
|
4084
|
-
resolvedParams,
|
|
4085
|
-
);
|
|
4086
|
-
if (deepestDescendantUrl) {
|
|
4087
|
-
// Take the first valid deepest descendant we find (or keep deepest among multiple)
|
|
4088
|
-
if (!bestDescendantUrl) {
|
|
4089
|
-
bestDescendantUrl = deepestDescendantUrl;
|
|
4090
|
-
}
|
|
4091
|
-
}
|
|
4092
|
-
}
|
|
4093
|
-
|
|
4094
|
-
if (bestDescendantUrl) {
|
|
4095
|
-
if (DEBUG$2) {
|
|
4096
|
-
console.debug(`[${pattern}] Using descendant optimization`);
|
|
4097
|
-
}
|
|
4098
|
-
return bestDescendantUrl;
|
|
4099
|
-
}
|
|
4100
|
-
if (DEBUG$2) {
|
|
4101
|
-
console.debug(`[${pattern}] No suitable child route found`);
|
|
4102
|
-
}
|
|
4103
|
-
|
|
4104
|
-
// Step 5: Inherit parameters from parent routes
|
|
4105
|
-
inheritParentParameters(finalParams);
|
|
4106
|
-
|
|
4107
|
-
// Step 6: Build the current route URL
|
|
4108
|
-
const generatedUrl = buildCurrentRouteUrl(finalParams);
|
|
4109
|
-
|
|
4110
|
-
return generatedUrl;
|
|
4111
|
-
};
|
|
4112
|
-
|
|
4113
|
-
/**
|
|
4114
|
-
* Helper: Find the highest ancestor by traversing parent chain recursively
|
|
4115
|
-
*/
|
|
4116
|
-
const findHighestAncestor = (startAncestor, resolvedParams) => {
|
|
4117
|
-
// Check if we can use this ancestor directly
|
|
4118
|
-
const directUrl = tryUseAncestor(startAncestor, resolvedParams);
|
|
4119
|
-
if (!directUrl) {
|
|
4120
|
-
return null;
|
|
4121
|
-
}
|
|
4122
|
-
|
|
4123
|
-
// Look for an even higher ancestor by checking the ancestor's parent
|
|
4124
|
-
if (startAncestor.parent) {
|
|
4125
|
-
const higherAncestor = startAncestor.parent;
|
|
4126
|
-
|
|
4127
|
-
// Skip root pattern
|
|
4128
|
-
if (higherAncestor.originalPattern === "/") {
|
|
4129
|
-
return directUrl;
|
|
4130
|
-
}
|
|
4131
|
-
|
|
4132
|
-
// Recursively check if we can optimize to an even higher ancestor
|
|
4133
|
-
const higherUrl = findHighestAncestor(higherAncestor, resolvedParams);
|
|
4134
|
-
if (higherUrl) {
|
|
4135
|
-
return higherUrl; // Found a higher ancestor, return that
|
|
4136
|
-
}
|
|
4137
|
-
}
|
|
4138
|
-
|
|
4139
|
-
// No higher ancestor found, return the direct optimization
|
|
4140
|
-
return directUrl;
|
|
4141
|
-
};
|
|
4142
|
-
|
|
4143
|
-
/**
|
|
4144
|
-
* Helper: Find the deepest descendant that can be used for this route
|
|
4145
|
-
*/
|
|
4146
|
-
const findDeepestDescendant = (startChild, params, resolvedParams) => {
|
|
4147
|
-
// Check if we can use this child directly
|
|
4148
|
-
const directUrl = tryUseDescendant(startChild, params, resolvedParams);
|
|
4149
|
-
if (!directUrl) {
|
|
4150
|
-
return null;
|
|
4151
|
-
}
|
|
4152
|
-
|
|
4153
|
-
// Now traverse down the child chain to find the deepest possible descendant
|
|
4154
|
-
let currentChild = startChild;
|
|
4155
|
-
let deepestUrl = directUrl;
|
|
4156
|
-
|
|
4157
|
-
while (true) {
|
|
4158
|
-
const childChildren = currentChild.children || [];
|
|
4159
|
-
|
|
4160
|
-
let foundDeeper = false;
|
|
4161
|
-
for (const deeperChild of childChildren) {
|
|
4162
|
-
const deeperUrl = tryUseDescendant(deeperChild, params, resolvedParams);
|
|
4163
|
-
if (deeperUrl) {
|
|
4164
|
-
// Found a deeper descendant that works
|
|
4165
|
-
deepestUrl = deeperUrl;
|
|
4166
|
-
currentChild = deeperChild;
|
|
4167
|
-
foundDeeper = true;
|
|
4168
|
-
break;
|
|
4169
|
-
}
|
|
4170
|
-
}
|
|
4171
|
-
|
|
4172
|
-
if (!foundDeeper) {
|
|
4173
|
-
break; // No deeper descendant found, we're at the bottom
|
|
4174
|
-
}
|
|
4175
|
-
}
|
|
4176
|
-
|
|
4177
|
-
return deepestUrl;
|
|
4178
|
-
};
|
|
4179
|
-
|
|
4180
|
-
/**
|
|
4181
|
-
* Helper: Check if child route can optimize to parent based on path segment matching
|
|
4182
|
-
*/
|
|
4183
|
-
const canChildOptimizeToParentPath = (
|
|
4184
|
-
childPattern,
|
|
4185
|
-
parentPattern,
|
|
4186
|
-
parentConnections,
|
|
4187
|
-
) => {
|
|
4188
|
-
if (!childPattern || !parentPattern) {
|
|
4189
|
-
return false;
|
|
4190
|
-
}
|
|
4191
|
-
|
|
4192
|
-
// Check each segment in child vs parent to see if child literals match parent defaults
|
|
4193
|
-
let hasMatchingPathOptimization = false;
|
|
4194
|
-
for (
|
|
4195
|
-
let i = 0;
|
|
4196
|
-
i < childPattern.segments.length && i < parentPattern.segments.length;
|
|
4197
|
-
i++
|
|
4198
|
-
) {
|
|
4199
|
-
const childSegment = childPattern.segments[i];
|
|
4200
|
-
const parentSegment = parentPattern.segments[i];
|
|
4201
|
-
|
|
4202
|
-
if (
|
|
4203
|
-
childSegment.type === "literal" &&
|
|
4204
|
-
parentSegment &&
|
|
4205
|
-
parentSegment.type === "param"
|
|
4206
|
-
) {
|
|
4207
|
-
// Child has literal where parent has parameter - check if literal matches default
|
|
4208
|
-
const paramName = parentSegment.name;
|
|
4209
|
-
const connection = parentConnections.find(
|
|
4210
|
-
(conn) => conn.paramName === paramName,
|
|
4211
|
-
);
|
|
4212
|
-
|
|
4213
|
-
if (connection) {
|
|
4214
|
-
const defaultValue = connection.getDefaultValue();
|
|
4215
|
-
if (childSegment.value === defaultValue) {
|
|
4216
|
-
// Child literal matches parent default - this enables path-based optimization
|
|
4217
|
-
hasMatchingPathOptimization = true;
|
|
4218
|
-
} else {
|
|
4219
|
-
// Child literal doesn't match parent default - can't optimize
|
|
4220
|
-
return false;
|
|
4221
|
-
}
|
|
4222
|
-
} else {
|
|
4223
|
-
return false;
|
|
4224
|
-
}
|
|
4225
|
-
}
|
|
4226
|
-
}
|
|
4227
|
-
|
|
4228
|
-
return hasMatchingPathOptimization;
|
|
4229
|
-
};
|
|
4230
|
-
|
|
4231
|
-
/**
|
|
4232
|
-
* Helper: Try to use an ancestor route (only immediate parent for parameter optimization)
|
|
4233
|
-
*/
|
|
4234
|
-
const tryUseAncestor = (ancestorPatternObj, resolvedParams) => {
|
|
4235
|
-
// Check if this ancestor is the immediate parent (for parameter optimization safety)
|
|
4236
|
-
const immediateParent = patternObject.parent;
|
|
4237
|
-
|
|
4238
|
-
if (
|
|
4239
|
-
immediateParent &&
|
|
4240
|
-
immediateParent.originalPattern === ancestorPatternObj.originalPattern
|
|
4241
|
-
) {
|
|
4242
|
-
// This is the immediate parent - check if we can optimize
|
|
4243
|
-
if (DEBUG$2) {
|
|
4244
|
-
console.debug(
|
|
4245
|
-
`[${pattern}] tryUseAncestor: Trying immediate parent ${ancestorPatternObj.originalPattern}`,
|
|
4246
|
-
);
|
|
4247
|
-
}
|
|
4248
|
-
|
|
4249
|
-
// For immediate parent optimization, check if we can optimize based on path segments
|
|
4250
|
-
// Even if query parameters are non-default, we should still optimize if the child's
|
|
4251
|
-
// literal path segments correspond to the parent's default path parameter values
|
|
4252
|
-
const canOptimizeBasedOnPath = canChildOptimizeToParentPath(
|
|
4253
|
-
parsedPattern,
|
|
4254
|
-
ancestorPatternObj.pattern,
|
|
4255
|
-
ancestorPatternObj.connections,
|
|
4256
|
-
);
|
|
4257
|
-
|
|
4258
|
-
if (canOptimizeBasedOnPath) {
|
|
4259
|
-
// Path-based optimization is possible, but ALSO check if current PATH parameters are defaults
|
|
4260
|
-
// Query parameters should not block path-based optimization, only path parameters should
|
|
4261
|
-
const hasNonDefaultPathParameters = connections.some((connection) => {
|
|
4262
|
-
const resolvedValue = resolvedParams[connection.paramName];
|
|
4263
|
-
|
|
4264
|
-
// Only check path parameters, not query parameters
|
|
4265
|
-
const isPathParameter = parsedPattern.segments.some(
|
|
4266
|
-
(segment) =>
|
|
4267
|
-
segment.type === "param" && segment.name === connection.paramName,
|
|
4268
|
-
);
|
|
4269
|
-
|
|
4270
|
-
return isPathParameter && connection.isCustomValue(resolvedValue);
|
|
4271
|
-
});
|
|
4272
|
-
|
|
4273
|
-
if (!hasNonDefaultPathParameters) {
|
|
4274
|
-
// Child has no non-default path parameters - proceed with optimization
|
|
4275
|
-
// Query parameters can be moved to the parent route
|
|
4276
|
-
const result = tryDirectOptimization(
|
|
4277
|
-
parsedPattern,
|
|
4278
|
-
connections,
|
|
4279
|
-
ancestorPatternObj,
|
|
4280
|
-
resolvedParams,
|
|
4281
|
-
);
|
|
4282
|
-
if (DEBUG$2) {
|
|
4283
|
-
console.debug(
|
|
4284
|
-
`[${pattern}] tryUseAncestor: Path-based optimization result:`,
|
|
4285
|
-
result,
|
|
4286
|
-
);
|
|
4287
|
-
}
|
|
4288
|
-
return result;
|
|
4289
|
-
}
|
|
4290
|
-
|
|
4291
|
-
if (DEBUG$2) {
|
|
4292
|
-
console.debug(
|
|
4293
|
-
`[${pattern}] tryUseAncestor: Path-based optimization blocked - child has non-default path parameters`,
|
|
4294
|
-
);
|
|
4295
|
-
}
|
|
4296
|
-
}
|
|
4297
|
-
|
|
4298
|
-
// For other cases, check if current route's OWN parameters have non-default values
|
|
4299
|
-
// Only check parameters that belong to this route, not inherited ones
|
|
4300
|
-
const hasNonDefaultOwnParameters = connections.some((connection) => {
|
|
4301
|
-
// Skip inherited connections - they shouldn't block optimization
|
|
4302
|
-
if (connection.inherited) {
|
|
4303
|
-
return false;
|
|
4304
|
-
}
|
|
4305
|
-
const resolvedValue = resolvedParams[connection.paramName];
|
|
4306
|
-
return connection.isCustomValue(resolvedValue);
|
|
4307
|
-
});
|
|
4308
|
-
|
|
4309
|
-
if (hasNonDefaultOwnParameters) {
|
|
4310
|
-
if (DEBUG$2) {
|
|
4311
|
-
console.debug(
|
|
4312
|
-
`[${pattern}] tryUseAncestor: Has non-default own parameters, skipping immediate parent optimization`,
|
|
4313
|
-
);
|
|
4314
|
-
}
|
|
4315
|
-
return null;
|
|
4316
|
-
}
|
|
4317
|
-
|
|
4318
|
-
// All own parameters have default values - proceed with optimization
|
|
4319
|
-
const result = tryDirectOptimization(
|
|
4320
|
-
parsedPattern,
|
|
4321
|
-
connections,
|
|
4322
|
-
ancestorPatternObj,
|
|
4323
|
-
resolvedParams,
|
|
4324
|
-
);
|
|
4325
|
-
if (DEBUG$2) {
|
|
4326
|
-
console.debug(
|
|
4327
|
-
`[${pattern}] tryUseAncestor: tryDirectOptimization result:`,
|
|
4328
|
-
result,
|
|
4329
|
-
);
|
|
4330
|
-
}
|
|
4331
|
-
return result;
|
|
4332
|
-
}
|
|
4333
|
-
|
|
4334
|
-
// For non-immediate parents, only allow optimization if all own parameters have default values
|
|
4335
|
-
const hasNonDefaultOwnParameters = connections.some((connection) => {
|
|
4336
|
-
// Skip inherited connections - they shouldn't block optimization
|
|
4337
|
-
if (connection.inherited) {
|
|
4338
|
-
return false;
|
|
4339
|
-
}
|
|
4340
|
-
const resolvedValue = resolvedParams[connection.paramName];
|
|
4341
|
-
return connection.isCustomValue(resolvedValue);
|
|
4342
|
-
});
|
|
4343
|
-
|
|
4344
|
-
if (hasNonDefaultOwnParameters) {
|
|
4345
|
-
if (DEBUG$2) {
|
|
4346
|
-
console.debug(
|
|
4347
|
-
`[${pattern}] tryUseAncestor: Non-immediate parent with non-default own parameters, skipping`,
|
|
4348
|
-
);
|
|
4349
|
-
}
|
|
4350
|
-
return null;
|
|
4351
|
-
}
|
|
4352
|
-
|
|
4353
|
-
// This is not the immediate parent - only allow literal-only optimization
|
|
4354
|
-
const hasParameters =
|
|
4355
|
-
connections.length > 0 ||
|
|
4356
|
-
parsedPattern.segments.some((seg) => seg.type === "param");
|
|
4357
|
-
|
|
4358
|
-
if (hasParameters) {
|
|
4359
|
-
if (DEBUG$2) {
|
|
4360
|
-
console.debug(
|
|
4361
|
-
`[${pattern}] tryUseAncestor: Non-immediate parent with parameters, skipping`,
|
|
4362
|
-
);
|
|
4363
|
-
}
|
|
4364
|
-
return null;
|
|
4365
|
-
}
|
|
4366
|
-
|
|
4367
|
-
// Pure literal route optimization
|
|
4368
|
-
// Allow literal routes to optimize to parametric ancestors if literal segments match parameter defaults
|
|
4369
|
-
if (DEBUG$2) {
|
|
4370
|
-
console.debug(
|
|
4371
|
-
`[${pattern}] tryUseAncestor: Trying optimization to ${ancestorPatternObj.originalPattern}`,
|
|
4372
|
-
);
|
|
4373
|
-
}
|
|
4374
|
-
|
|
4375
|
-
const result = tryDirectOptimization(
|
|
4376
|
-
parsedPattern,
|
|
4377
|
-
connections,
|
|
4378
|
-
ancestorPatternObj,
|
|
4379
|
-
resolvedParams,
|
|
4380
|
-
);
|
|
4381
|
-
if (DEBUG$2) {
|
|
4382
|
-
console.debug(
|
|
4383
|
-
`[${pattern}] tryUseAncestor: tryDirectOptimization result:`,
|
|
4384
|
-
result,
|
|
4385
|
-
);
|
|
4386
|
-
}
|
|
4387
|
-
return result;
|
|
4388
|
-
};
|
|
4389
|
-
|
|
4390
|
-
/**
|
|
4391
|
-
* Helper: Check if current literal route can be optimized to target ancestor
|
|
4392
|
-
*/
|
|
4393
|
-
const tryDirectOptimization = (
|
|
4394
|
-
sourcePattern,
|
|
4395
|
-
sourceConnections,
|
|
4396
|
-
targetAncestor,
|
|
4397
|
-
resolvedParams,
|
|
4398
|
-
) => {
|
|
4399
|
-
const sourceLiterals = sourcePattern.segments
|
|
4400
|
-
.filter((seg) => seg.type === "literal")
|
|
4401
|
-
.map((seg) => seg.value);
|
|
4402
|
-
|
|
4403
|
-
const targetLiterals = targetAncestor.pattern.segments
|
|
4404
|
-
.filter((seg) => seg.type === "literal")
|
|
4405
|
-
.map((seg) => seg.value);
|
|
4406
|
-
|
|
4407
|
-
const targetParams = targetAncestor.pattern.segments.filter(
|
|
4408
|
-
(seg) => seg.type === "param",
|
|
4409
|
-
);
|
|
4410
|
-
|
|
4411
|
-
if (DEBUG$2) {
|
|
4412
|
-
console.debug(
|
|
4413
|
-
`[${pattern}] tryDirectOptimization: sourceLiterals:`,
|
|
4414
|
-
sourceLiterals,
|
|
4415
|
-
);
|
|
4416
|
-
console.debug(
|
|
4417
|
-
`[${pattern}] tryDirectOptimization: targetLiterals:`,
|
|
4418
|
-
targetLiterals,
|
|
4419
|
-
);
|
|
4420
|
-
console.debug(
|
|
4421
|
-
`[${pattern}] tryDirectOptimization: targetParams:`,
|
|
4422
|
-
targetParams,
|
|
4423
|
-
);
|
|
4424
|
-
}
|
|
4425
|
-
|
|
4426
|
-
// Source must extend target's literal path
|
|
4427
|
-
if (sourceLiterals.length <= targetLiterals.length) {
|
|
4428
|
-
if (DEBUG$2) {
|
|
4429
|
-
console.debug(`[${pattern}] tryDirectOptimization: Source too short`);
|
|
4430
|
-
}
|
|
4431
|
-
return null;
|
|
4432
|
-
}
|
|
4433
|
-
|
|
4434
|
-
// Source must start with same literals as target
|
|
4435
|
-
for (let i = 0; i < targetLiterals.length; i++) {
|
|
4436
|
-
if (sourceLiterals[i] !== targetLiterals[i]) {
|
|
4437
|
-
if (DEBUG$2) {
|
|
4438
|
-
console.debug(
|
|
4439
|
-
`[${pattern}] tryDirectOptimization: Literal mismatch at ${i}`,
|
|
4440
|
-
);
|
|
4441
|
-
}
|
|
4442
|
-
return null;
|
|
4443
|
-
}
|
|
4444
|
-
}
|
|
4445
|
-
|
|
4446
|
-
// For literal-only optimization: if both source and target have only literals AND no parameters,
|
|
4447
|
-
// and source extends target, we can optimize directly
|
|
4448
|
-
const sourceHasOnlyLiterals =
|
|
4449
|
-
sourcePattern.segments.every((seg) => seg.type === "literal") &&
|
|
4450
|
-
sourceConnections.length === 0;
|
|
4451
|
-
|
|
4452
|
-
const targetHasOnlyLiterals =
|
|
4453
|
-
targetAncestor.pattern.segments.every((seg) => seg.type === "literal") &&
|
|
4454
|
-
targetAncestor.connections.length === 0;
|
|
4455
|
-
|
|
4456
|
-
if (sourceHasOnlyLiterals && targetHasOnlyLiterals) {
|
|
4457
|
-
// Two pure literal routes have no parametric relationship — nothing to optimize.
|
|
4458
|
-
// /dashboard/section must never collapse to /dashboard.
|
|
4459
|
-
if (DEBUG$2) {
|
|
4460
|
-
console.debug(
|
|
4461
|
-
`[${pattern}] tryDirectOptimization: Both are pure literal-only routes, no optimization possible`,
|
|
4462
|
-
);
|
|
4463
|
-
}
|
|
4464
|
-
return null;
|
|
4465
|
-
}
|
|
4466
|
-
|
|
4467
|
-
// For parametric optimization: remaining segments must match target's parameter defaults
|
|
4468
|
-
const extraSegments = sourceLiterals.slice(targetLiterals.length);
|
|
4469
|
-
if (extraSegments.length !== targetParams.length) {
|
|
4470
|
-
if (DEBUG$2) {
|
|
4471
|
-
console.debug(
|
|
4472
|
-
`[${pattern}] tryDirectOptimization: Extra segments ${extraSegments.length} != target params ${targetParams.length}`,
|
|
4473
|
-
);
|
|
4474
|
-
}
|
|
4475
|
-
return null;
|
|
4476
|
-
}
|
|
4477
|
-
|
|
4478
|
-
for (let i = 0; i < extraSegments.length; i++) {
|
|
4479
|
-
const segment = extraSegments[i];
|
|
4480
|
-
const param = targetParams[i];
|
|
4481
|
-
const connection = targetAncestor.connections.find(
|
|
4482
|
-
(conn) => conn.paramName === param.name,
|
|
4483
|
-
);
|
|
4484
|
-
if (!connection || connection.getDefaultValue() !== segment) {
|
|
4485
|
-
if (DEBUG$2) {
|
|
4486
|
-
console.debug(
|
|
4487
|
-
`[${pattern}] tryDirectOptimization: Parameter default mismatch for ${param.name}`,
|
|
4488
|
-
);
|
|
4489
|
-
}
|
|
4490
|
-
return null;
|
|
4491
|
-
}
|
|
4492
|
-
}
|
|
4493
|
-
|
|
4494
|
-
if (DEBUG$2) {
|
|
4495
|
-
console.debug(
|
|
4496
|
-
`[${pattern}] tryDirectOptimization: SUCCESS! Returning ancestor URL`,
|
|
4497
|
-
);
|
|
4498
|
-
console.debug(
|
|
4499
|
-
`[${pattern}] tryDirectOptimization: resolvedParams:`,
|
|
4500
|
-
resolvedParams,
|
|
4501
|
-
);
|
|
4502
|
-
}
|
|
4503
|
-
|
|
4504
|
-
// Build ancestor URL with inherited parameters that don't conflict with optimization
|
|
4505
|
-
const ancestorParams = {};
|
|
4506
|
-
|
|
4507
|
-
// First, add extra parameters from the original resolvedParams
|
|
4508
|
-
// These are parameters that don't correspond to any pattern segments or query params
|
|
4509
|
-
const sourcePatternParamNames = new Set(
|
|
4510
|
-
sourceConnections.map((conn) => conn.paramName),
|
|
4511
|
-
);
|
|
4512
|
-
const sourceQueryParamNames = new Set(
|
|
4513
|
-
sourcePattern.queryParams.map((qp) => qp.name),
|
|
4514
|
-
);
|
|
4515
|
-
const targetPatternParamNames = new Set(
|
|
4516
|
-
targetAncestor.connections.map((conn) => conn.paramName),
|
|
4517
|
-
);
|
|
4518
|
-
const targetQueryParamNames = new Set(
|
|
4519
|
-
targetAncestor.pattern.queryParams.map((qp) => qp.name),
|
|
4520
|
-
);
|
|
4521
|
-
|
|
4522
|
-
for (const [paramName, value] of Object.entries(resolvedParams)) {
|
|
4523
|
-
if (DEBUG$2) {
|
|
4524
|
-
console.debug(
|
|
4525
|
-
`[${pattern}] tryDirectOptimization: Considering param ${paramName}=${value}`,
|
|
4526
|
-
);
|
|
4527
|
-
}
|
|
4528
|
-
// Include parameters that target pattern specifically needs
|
|
4529
|
-
if (targetQueryParamNames.has(paramName)) {
|
|
4530
|
-
// Only include if the value is not the default value
|
|
4531
|
-
const connection =
|
|
4532
|
-
targetAncestor.pathConnectionMap.get(paramName) ||
|
|
4533
|
-
targetAncestor.queryConnectionMap.get(paramName);
|
|
4534
|
-
if (connection && connection.getDefaultValue() !== value) {
|
|
4535
|
-
ancestorParams[paramName] = value;
|
|
4536
|
-
if (DEBUG$2) {
|
|
4537
|
-
console.debug(
|
|
4538
|
-
`[${pattern}] tryDirectOptimization: Added target param ${paramName}=${value}`,
|
|
4539
|
-
);
|
|
4540
|
-
}
|
|
4541
|
-
}
|
|
4542
|
-
}
|
|
4543
|
-
// Include source query parameters (these should be inherited during ancestor optimization)
|
|
4544
|
-
else if (sourceQueryParamNames.has(paramName)) {
|
|
4545
|
-
// Only include source parameters if they're not default values
|
|
4546
|
-
// Default values should still be omitted from URLs to keep them clean
|
|
4547
|
-
const connection = sourceConnections.find(
|
|
4548
|
-
(conn) => conn.paramName === paramName,
|
|
4549
|
-
);
|
|
4550
|
-
if (
|
|
4551
|
-
connection &&
|
|
4552
|
-
value !== undefined &&
|
|
4553
|
-
connection.getDefaultValue() !== value
|
|
4554
|
-
) {
|
|
4555
|
-
ancestorParams[paramName] = value;
|
|
4556
|
-
if (DEBUG$2) {
|
|
4557
|
-
console.debug(
|
|
4558
|
-
`[${pattern}] tryDirectOptimization: Added source param ${paramName}=${value}`,
|
|
4559
|
-
);
|
|
4560
|
-
}
|
|
4561
|
-
}
|
|
4562
|
-
}
|
|
4563
|
-
// Include extra parameters that are not part of either pattern (true extra parameters)
|
|
4564
|
-
else if (
|
|
4565
|
-
!sourcePatternParamNames.has(paramName) &&
|
|
4566
|
-
!targetPatternParamNames.has(paramName)
|
|
4567
|
-
) {
|
|
4568
|
-
ancestorParams[paramName] = value;
|
|
4569
|
-
if (DEBUG$2) {
|
|
4570
|
-
console.debug(
|
|
4571
|
-
`[${pattern}] tryDirectOptimization: Added extra param ${paramName}=${value}`,
|
|
4572
|
-
);
|
|
4573
|
-
}
|
|
4574
|
-
}
|
|
4575
|
-
}
|
|
4576
|
-
|
|
4577
|
-
// Also check target ancestor's own signal values for parameters not in resolvedParams
|
|
4578
|
-
if (DEBUG$2) {
|
|
4579
|
-
console.debug(
|
|
4580
|
-
`[${pattern}] tryDirectOptimization: Target ancestor has ${targetAncestor.connections.length} connections`,
|
|
4581
|
-
);
|
|
4582
|
-
for (const conn of targetAncestor.connections) {
|
|
4583
|
-
console.debug(
|
|
4584
|
-
`[${pattern}] tryDirectOptimization: Target connection ${conn.paramName}: value=${readSignalForUrlBuild(conn)}, isCustom=${conn.isCustomValue(readSignalForUrlBuild(conn))}`,
|
|
4585
|
-
);
|
|
4586
|
-
}
|
|
4587
|
-
}
|
|
4588
|
-
|
|
4589
|
-
for (const connection of targetAncestor.connections) {
|
|
4590
|
-
const { paramName } = connection;
|
|
4591
|
-
if (paramName in ancestorParams) {
|
|
4592
|
-
if (DEBUG$2) {
|
|
4593
|
-
console.debug(
|
|
4594
|
-
`[${pattern}] tryDirectOptimization: Skipping ${paramName} - already in ancestorParams`,
|
|
4595
|
-
);
|
|
4596
|
-
}
|
|
4597
|
-
continue;
|
|
4598
|
-
}
|
|
4599
|
-
|
|
4600
|
-
// Only include if not already processed and has custom value (not default)
|
|
4601
|
-
const signalValue = readSignalForUrlBuild(connection);
|
|
4602
|
-
if (signalValue !== undefined) {
|
|
4603
|
-
// Don't include path parameters that correspond to literal segments we're optimizing away
|
|
4604
|
-
const targetParam = targetParams.find((p) => p.name === paramName);
|
|
4605
|
-
const isPathParam = targetParam !== undefined; // Any param in segments is a path param
|
|
4606
|
-
if (isPathParam) {
|
|
4607
|
-
// Skip path parameters - we want them to use default values for optimization
|
|
4608
|
-
if (DEBUG$2) {
|
|
4609
|
-
console.debug(
|
|
4610
|
-
`[${pattern}] tryDirectOptimization: Skipping path param ${paramName}=${signalValue} (will use default)`,
|
|
4611
|
-
);
|
|
4612
|
-
}
|
|
4613
|
-
continue;
|
|
4614
|
-
}
|
|
4615
|
-
|
|
4616
|
-
// For query parameters, only include custom values (not defaults)
|
|
4617
|
-
if (connection.isCustomValue(signalValue)) {
|
|
4618
|
-
ancestorParams[paramName] = signalValue;
|
|
4619
|
-
if (DEBUG$2) {
|
|
4620
|
-
console.debug(
|
|
4621
|
-
`[${pattern}] tryDirectOptimization: Added target signal param ${paramName}=${signalValue}`,
|
|
4622
|
-
);
|
|
4623
|
-
}
|
|
4624
|
-
} else if (DEBUG$2) {
|
|
4625
|
-
console.debug(
|
|
4626
|
-
`[${pattern}] tryDirectOptimization: Skipping default value ${paramName}=${signalValue}`,
|
|
4627
|
-
);
|
|
4628
|
-
}
|
|
4629
|
-
} else if (DEBUG$2) {
|
|
4630
|
-
console.debug(
|
|
4631
|
-
`[${pattern}] tryDirectOptimization: Skipping ${paramName}=${signalValue} - undefined value`,
|
|
4632
|
-
);
|
|
4633
|
-
}
|
|
4634
|
-
}
|
|
4635
|
-
|
|
4636
|
-
// Then, get all ancestors starting from the target ancestor's parent (skip the target itself)
|
|
4637
|
-
let currentParent = targetAncestor.parent;
|
|
4638
|
-
|
|
4639
|
-
while (currentParent) {
|
|
4640
|
-
for (const connection of currentParent.connections) {
|
|
4641
|
-
const { paramName } = connection;
|
|
4642
|
-
if (paramName in ancestorParams) {
|
|
4643
|
-
continue;
|
|
4644
|
-
}
|
|
4645
|
-
|
|
4646
|
-
// Only inherit custom values (not defaults) that we don't already have
|
|
4647
|
-
const signalValue = readSignalForUrlBuild(connection);
|
|
3362
|
+
continue;
|
|
3363
|
+
}
|
|
3364
|
+
if (!conn.isCustomValue(entry.value)) {
|
|
3365
|
+
continue;
|
|
3366
|
+
}
|
|
3367
|
+
if (conn.paramType === "path") {
|
|
3368
|
+
const literalSeg =
|
|
3369
|
+
entry.segmentIndex === undefined
|
|
3370
|
+
? undefined
|
|
3371
|
+
: candidateSegments[entry.segmentIndex];
|
|
4648
3372
|
if (
|
|
4649
|
-
|
|
4650
|
-
|
|
3373
|
+
literalSeg &&
|
|
3374
|
+
literalSeg.type === "literal" &&
|
|
3375
|
+
literalSeg.value === String(entry.value)
|
|
4651
3376
|
) {
|
|
4652
|
-
//
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
3377
|
+
continue; // the candidate's path itself encodes this value
|
|
3378
|
+
}
|
|
3379
|
+
if (entry.explicit && entry.owner === "ancestor") {
|
|
3380
|
+
urlParams[name] = entry.value;
|
|
3381
|
+
continue;
|
|
3382
|
+
}
|
|
3383
|
+
if (entry.pageNaming) {
|
|
3384
|
+
// deliberately left out: this url does not follow the signal
|
|
3385
|
+
continue;
|
|
3386
|
+
}
|
|
3387
|
+
if (lenient) {
|
|
3388
|
+
if (entry.explicit) {
|
|
3389
|
+
urlParams[name] = entry.value;
|
|
4660
3390
|
}
|
|
3391
|
+
continue;
|
|
4661
3392
|
}
|
|
3393
|
+
return null;
|
|
4662
3394
|
}
|
|
4663
|
-
|
|
4664
|
-
// Move up the parent chain
|
|
4665
|
-
currentParent = currentParent.parent;
|
|
3395
|
+
urlParams[name] = entry.value;
|
|
4666
3396
|
}
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
*/
|
|
4679
|
-
const tryUseDescendant = (
|
|
4680
|
-
descendantPatternObj,
|
|
4681
|
-
params,
|
|
4682
|
-
parentResolvedParams,
|
|
4683
|
-
) => {
|
|
4684
|
-
// Check basic compatibility
|
|
4685
|
-
const compatibility = checkChildRouteCompatibility(
|
|
4686
|
-
descendantPatternObj,
|
|
4687
|
-
params,
|
|
4688
|
-
);
|
|
4689
|
-
if (!compatibility.isCompatible) {
|
|
4690
|
-
return null;
|
|
3397
|
+
let buildPattern = candidate.pattern;
|
|
3398
|
+
if (dropMissing) {
|
|
3399
|
+
const keptSegments = candidateSegments.filter(
|
|
3400
|
+
(seg) => seg.type !== "param" || seg.name in urlParams,
|
|
3401
|
+
);
|
|
3402
|
+
if (keptSegments.length !== candidateSegments.length) {
|
|
3403
|
+
buildPattern = { ...buildPattern, segments: keptSegments };
|
|
3404
|
+
if (buildPattern.trailingSlash) {
|
|
3405
|
+
buildPattern.trailingSlash = false;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
4691
3408
|
}
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
compatibility,
|
|
4698
|
-
parentResolvedParams,
|
|
3409
|
+
const url = buildUrlFromPattern(
|
|
3410
|
+
buildPattern,
|
|
3411
|
+
urlParams,
|
|
3412
|
+
candidate.originalPattern,
|
|
3413
|
+
candidate,
|
|
4699
3414
|
);
|
|
4700
|
-
if (
|
|
3415
|
+
if (url.includes("/:")) {
|
|
4701
3416
|
return null;
|
|
4702
3417
|
}
|
|
3418
|
+
return url;
|
|
3419
|
+
};
|
|
4703
3420
|
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
)
|
|
3421
|
+
const urlValueEquals = (intendedValue, reproducedValue) => {
|
|
3422
|
+
let a = intendedValue;
|
|
3423
|
+
if (a && typeof a === "object" && a[rawUrlPartSymbol]) {
|
|
3424
|
+
a = a.value;
|
|
3425
|
+
}
|
|
3426
|
+
if (a instanceof Date) {
|
|
3427
|
+
const yyyy = a.getUTCFullYear();
|
|
3428
|
+
const mm = String(a.getUTCMonth() + 1).padStart(2, "0");
|
|
3429
|
+
const dd = String(a.getUTCDate()).padStart(2, "0");
|
|
3430
|
+
a = `${yyyy}-${mm}-${dd}`;
|
|
3431
|
+
}
|
|
3432
|
+
if (compareTwoJsValues(a, reproducedValue)) {
|
|
3433
|
+
return true;
|
|
3434
|
+
}
|
|
3435
|
+
if (a === true && reproducedValue === "") {
|
|
3436
|
+
// `true` is written as a bare "?flag", which an untyped connection
|
|
3437
|
+
// reads back as an empty string
|
|
3438
|
+
return true;
|
|
3439
|
+
}
|
|
3440
|
+
return String(a) === String(reproducedValue);
|
|
4710
3441
|
};
|
|
4711
3442
|
|
|
4712
3443
|
/**
|
|
4713
|
-
*
|
|
3444
|
+
* Does the url reproduce the intended state?
|
|
3445
|
+
* A connection's reproduced value is what the url gives back on a reload:
|
|
3446
|
+
* the value extracted by a matching family pattern that carries the
|
|
3447
|
+
* connection, or the connection's default when no matching pattern does.
|
|
4714
3448
|
*/
|
|
4715
|
-
const
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
3449
|
+
const verifyUrl = (url, intended) => {
|
|
3450
|
+
if (!applyOn(url)) {
|
|
3451
|
+
return false; // whatever this url is, it is not one of OUR urls
|
|
3452
|
+
}
|
|
3453
|
+
const familyPatterns = [];
|
|
3454
|
+
const seen = new Set();
|
|
3455
|
+
const visit = (patternObj) => {
|
|
3456
|
+
if (seen.has(patternObj)) {
|
|
3457
|
+
return;
|
|
3458
|
+
}
|
|
3459
|
+
seen.add(patternObj);
|
|
3460
|
+
familyPatterns.push(patternObj);
|
|
3461
|
+
for (const child of patternObj.children) {
|
|
3462
|
+
visit(child);
|
|
3463
|
+
}
|
|
3464
|
+
};
|
|
3465
|
+
visit(patternObject.familyRoot || patternObject);
|
|
3466
|
+
const matchResultMap = new Map();
|
|
3467
|
+
for (const familyPattern of familyPatterns) {
|
|
3468
|
+
matchResultMap.set(familyPattern, familyPattern.applyOn(url));
|
|
3469
|
+
}
|
|
3470
|
+
for (const [name, entry] of intended) {
|
|
3471
|
+
const conn = entry.connection;
|
|
3472
|
+
if (!conn) {
|
|
3473
|
+
continue; // extra params have no state to lose
|
|
3474
|
+
}
|
|
3475
|
+
if (entry.pageNaming) {
|
|
3476
|
+
continue; // losing it is deliberate (see buildIntendedState)
|
|
3477
|
+
}
|
|
3478
|
+
let reproducedValue;
|
|
3479
|
+
let extracted = false;
|
|
3480
|
+
for (const familyPattern of familyPatterns) {
|
|
3481
|
+
const matchResult = matchResultMap.get(familyPattern);
|
|
3482
|
+
if (!matchResult) {
|
|
4725
3483
|
continue;
|
|
4726
3484
|
}
|
|
4727
|
-
const
|
|
4728
|
-
|
|
4729
|
-
|
|
3485
|
+
const holdsParam =
|
|
3486
|
+
familyPattern.queryConnectionMap.has(name) ||
|
|
3487
|
+
familyPattern.pattern.segments.some(
|
|
3488
|
+
(seg) => seg.type === "param" && seg.name === name,
|
|
3489
|
+
);
|
|
3490
|
+
if (!holdsParam) {
|
|
3491
|
+
continue;
|
|
4730
3492
|
}
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
parentSignalValue !== undefined &&
|
|
4736
|
-
parentConnection.isCustomValue(parentSignalValue)
|
|
4737
|
-
) {
|
|
4738
|
-
// Don't inherit if parameter corresponds to a literal in our path
|
|
4739
|
-
const shouldInherit = !isParameterRedundantWithLiteralSegments(
|
|
4740
|
-
parsedPattern,
|
|
4741
|
-
currentParent.pattern,
|
|
4742
|
-
paramName);
|
|
4743
|
-
|
|
4744
|
-
if (shouldInherit) {
|
|
4745
|
-
finalParams[paramName] = parentSignalValue;
|
|
4746
|
-
}
|
|
3493
|
+
if (name in matchResult && matchResult[name] !== undefined) {
|
|
3494
|
+
reproducedValue = matchResult[name];
|
|
3495
|
+
extracted = true;
|
|
3496
|
+
break;
|
|
4747
3497
|
}
|
|
4748
3498
|
}
|
|
4749
|
-
|
|
4750
|
-
|
|
3499
|
+
if (!extracted) {
|
|
3500
|
+
reproducedValue = conn.getDefaultValue();
|
|
3501
|
+
}
|
|
3502
|
+
const wantedValue =
|
|
3503
|
+
entry.value === undefined ? conn.getDefaultValue() : entry.value;
|
|
3504
|
+
if (!urlValueEquals(wantedValue, reproducedValue)) {
|
|
3505
|
+
return false;
|
|
3506
|
+
}
|
|
4751
3507
|
}
|
|
3508
|
+
return true;
|
|
4752
3509
|
};
|
|
4753
3510
|
|
|
4754
3511
|
/**
|
|
4755
|
-
*
|
|
3512
|
+
* When may this route's url collapse to an ancestor's shorter url?
|
|
3513
|
+
* Policy kept from behavior the round-trip check cannot decide alone
|
|
3514
|
+
* (several faithful urls exist, one is canonical):
|
|
3515
|
+
* - immediate parent, when this pattern's literals sitting at the parent's
|
|
3516
|
+
* param positions all spell the params' DEFAULT values ("/admin/settings"
|
|
3517
|
+
* is "/admin" when settings is the default section) — custom query values
|
|
3518
|
+
* ride along, custom own path values forbid it
|
|
3519
|
+
* - immediate parent otherwise: only when none of this route's own
|
|
3520
|
+
* (non-inherited) connections holds a custom value
|
|
3521
|
+
* - higher ancestors: only for a pure-literal route with no connections
|
|
4756
3522
|
*/
|
|
4757
|
-
const
|
|
4758
|
-
|
|
4759
|
-
|
|
3523
|
+
const isAncestorCollapseAllowed = (ancestorPattern, intended) => {
|
|
3524
|
+
const entryIsMeaningful = (conn) => {
|
|
3525
|
+
const entry = intended.get(conn.paramName);
|
|
3526
|
+
return (
|
|
3527
|
+
Boolean(entry) && entry.value !== undefined && conn.isCustomValue(entry.value)
|
|
3528
|
+
);
|
|
3529
|
+
};
|
|
3530
|
+
if (ancestorPattern !== patternObject.parent) {
|
|
3531
|
+
return (
|
|
3532
|
+
connections.length === 0 &&
|
|
3533
|
+
parsedPattern.segments.every((seg) => seg.type === "literal")
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
let literalsPinDefaults = false;
|
|
3537
|
+
for (const seg of ancestorPattern.pattern.segments) {
|
|
3538
|
+
if (seg.type !== "param") {
|
|
3539
|
+
continue;
|
|
3540
|
+
}
|
|
3541
|
+
const conn = ancestorPattern.pathConnectionMap.get(seg.name);
|
|
3542
|
+
const selfSeg = parsedPattern.segments[seg.index];
|
|
3543
|
+
if (!conn || !selfSeg || selfSeg.type !== "literal") {
|
|
3544
|
+
continue;
|
|
3545
|
+
}
|
|
3546
|
+
if (selfSeg.value !== String(conn.getDefaultValue())) {
|
|
3547
|
+
literalsPinDefaults = false;
|
|
3548
|
+
break;
|
|
3549
|
+
}
|
|
3550
|
+
literalsPinDefaults = true;
|
|
3551
|
+
}
|
|
3552
|
+
if (literalsPinDefaults) {
|
|
3553
|
+
return !connections.some(
|
|
3554
|
+
(conn) =>
|
|
3555
|
+
!conn.inherited &&
|
|
3556
|
+
conn.paramType === "path" &&
|
|
3557
|
+
entryIsMeaningful(conn),
|
|
3558
|
+
);
|
|
4760
3559
|
}
|
|
3560
|
+
return !connections.some(
|
|
3561
|
+
(conn) => !conn.inherited && entryIsMeaningful(conn),
|
|
3562
|
+
);
|
|
3563
|
+
};
|
|
4761
3564
|
|
|
4762
|
-
|
|
4763
|
-
const
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
3565
|
+
const buildMostPreciseUrl = (params = {}) => {
|
|
3566
|
+
const { intended, reachableDescendants } = buildIntendedState(params);
|
|
3567
|
+
// ancestors: start at the immediate parent, climb while urls keep round-
|
|
3568
|
+
// tripping, keep the highest one ("/" is never a target)
|
|
3569
|
+
let ancestorUrl = null;
|
|
3570
|
+
let ancestor = patternObject.parent;
|
|
3571
|
+
while (ancestor && ancestor.originalPattern !== "/") {
|
|
3572
|
+
if (!isAncestorCollapseAllowed(ancestor, intended)) {
|
|
3573
|
+
break;
|
|
3574
|
+
}
|
|
3575
|
+
const url = buildCandidateUrl(ancestor, intended, {
|
|
3576
|
+
dropMissing: true,
|
|
3577
|
+
});
|
|
3578
|
+
if (!url || !verifyUrl(url, intended)) {
|
|
3579
|
+
break;
|
|
3580
|
+
}
|
|
3581
|
+
debug$3(
|
|
3582
|
+
`[${pattern}] ancestor url ${url} (via ${ancestor.originalPattern})`,
|
|
3583
|
+
);
|
|
3584
|
+
ancestorUrl = url;
|
|
3585
|
+
ancestor = ancestor.parent;
|
|
3586
|
+
}
|
|
3587
|
+
if (ancestorUrl) {
|
|
3588
|
+
return ancestorUrl;
|
|
3589
|
+
}
|
|
3590
|
+
const selfUrl = buildCandidateUrl(patternObject, intended, {
|
|
3591
|
+
dropMissing: true,
|
|
3592
|
+
});
|
|
3593
|
+
if (selfUrl && verifyUrl(selfUrl, intended)) {
|
|
3594
|
+
return selfUrl;
|
|
3595
|
+
}
|
|
3596
|
+
// deepest reachable descendant whose url round-trips, walking greedily
|
|
3597
|
+
let current = patternObject;
|
|
3598
|
+
let descendantUrl = null;
|
|
3599
|
+
descend: while (true) {
|
|
3600
|
+
for (const child of current.children) {
|
|
3601
|
+
if (!reachableDescendants.has(child)) {
|
|
3602
|
+
continue;
|
|
4768
3603
|
}
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
3604
|
+
const url = buildCandidateUrl(child, intended);
|
|
3605
|
+
if (url && verifyUrl(url, intended)) {
|
|
3606
|
+
debug$3(
|
|
3607
|
+
`[${pattern}] descendant url ${url} (via ${child.originalPattern})`,
|
|
3608
|
+
);
|
|
3609
|
+
descendantUrl = url;
|
|
3610
|
+
current = child;
|
|
3611
|
+
continue descend;
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
break;
|
|
4779
3615
|
}
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
3616
|
+
if (descendantUrl) {
|
|
3617
|
+
return descendantUrl;
|
|
3618
|
+
}
|
|
3619
|
+
// no url round-trips (state not fully representable): this route's own
|
|
3620
|
+
// url, dropping what it cannot encode
|
|
3621
|
+
return (
|
|
3622
|
+
buildCandidateUrl(patternObject, intended, {
|
|
3623
|
+
dropMissing: true,
|
|
3624
|
+
lenient: true,
|
|
3625
|
+
}) || "/"
|
|
4786
3626
|
);
|
|
4787
3627
|
};
|
|
4788
3628
|
|
|
@@ -4821,10 +3661,8 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
4821
3661
|
// changes must not end the visit this param qualifies.
|
|
4822
3662
|
const carryOverWeakParams = (currentUrl, params) => {
|
|
4823
3663
|
let paramsWithWeak = params;
|
|
4824
|
-
for (const
|
|
4825
|
-
|
|
4826
|
-
...queryConnectionMap,
|
|
4827
|
-
]) {
|
|
3664
|
+
for (const connection of connections) {
|
|
3665
|
+
const { paramName } = connection;
|
|
4828
3666
|
if (!connection.weak || paramName in paramsWithWeak) {
|
|
4829
3667
|
continue;
|
|
4830
3668
|
}
|
|
@@ -4871,20 +3709,18 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
4871
3709
|
|
|
4872
3710
|
// Pattern object with unified data and methods
|
|
4873
3711
|
const patternObject = {
|
|
4874
|
-
// Pattern data properties (formerly patternData)
|
|
4875
3712
|
urlPatternRaw: pattern,
|
|
4876
3713
|
cleanPattern,
|
|
4877
3714
|
connections,
|
|
4878
3715
|
pathConnectionMap, // Separate map for path parameters
|
|
4879
3716
|
queryConnectionMap, // Separate map for query parameters
|
|
4880
3717
|
parsedPattern,
|
|
4881
|
-
signalSet,
|
|
4882
3718
|
children: [],
|
|
4883
3719
|
parent: null,
|
|
3720
|
+
familyRoot: null, // Topmost ancestor, computed during setupPatterns
|
|
4884
3721
|
depth: 0, // Will be calculated after relationships are built
|
|
4885
3722
|
descendantPathSignals: new Map(), // Precomputed during setupPatterns (Map<segmentIndex, conn[]>)
|
|
4886
3723
|
|
|
4887
|
-
// Pattern methods (formerly patternObj methods)
|
|
4888
3724
|
originalPattern: pattern,
|
|
4889
3725
|
pattern: parsedPattern,
|
|
4890
3726
|
applyOn,
|
|
@@ -4985,55 +3821,6 @@ const detectSignals = (routePattern) => {
|
|
|
4985
3821
|
return [updatedPattern, signalConnections];
|
|
4986
3822
|
};
|
|
4987
3823
|
|
|
4988
|
-
/**
|
|
4989
|
-
* Helper: Check if parameter matches any literal in child pattern
|
|
4990
|
-
*/
|
|
4991
|
-
const paramMatchesChildLiteral = (paramValue, childParsedPattern) => {
|
|
4992
|
-
return childParsedPattern.segments.some(
|
|
4993
|
-
(segment) => segment.type === "literal" && segment.value === paramValue,
|
|
4994
|
-
);
|
|
4995
|
-
};
|
|
4996
|
-
|
|
4997
|
-
/**
|
|
4998
|
-
* Helper: Check if a parent parameter can semantically reach a child route
|
|
4999
|
-
* This replaces the fragile position-based matching with semantic verification
|
|
5000
|
-
*/
|
|
5001
|
-
const canParameterReachChildRoute = (
|
|
5002
|
-
paramName,
|
|
5003
|
-
paramValue,
|
|
5004
|
-
parentPattern,
|
|
5005
|
-
childPattern,
|
|
5006
|
-
) => {
|
|
5007
|
-
// Find the parent parameter segment
|
|
5008
|
-
const parentParamSegment = parentPattern.segments.find(
|
|
5009
|
-
(segment) => segment.type === "param" && segment.name === paramName,
|
|
5010
|
-
);
|
|
5011
|
-
|
|
5012
|
-
if (!parentParamSegment) {
|
|
5013
|
-
return true; // Not a path parameter, no conflict
|
|
5014
|
-
}
|
|
5015
|
-
|
|
5016
|
-
// Get parameter's logical path position (not array index)
|
|
5017
|
-
const paramPathPosition = parentParamSegment.index;
|
|
5018
|
-
|
|
5019
|
-
// Find corresponding child segment at the same logical path position
|
|
5020
|
-
const childSegmentAtSamePosition = childPattern.segments.find(
|
|
5021
|
-
(segment) => segment.index === paramPathPosition,
|
|
5022
|
-
);
|
|
5023
|
-
|
|
5024
|
-
if (!childSegmentAtSamePosition) {
|
|
5025
|
-
return true; // Child doesn't extend to this position, no conflict
|
|
5026
|
-
}
|
|
5027
|
-
|
|
5028
|
-
if (childSegmentAtSamePosition.type === "literal") {
|
|
5029
|
-
// Child has a literal at this position - parent parameter must match exactly
|
|
5030
|
-
return childSegmentAtSamePosition.value === paramValue;
|
|
5031
|
-
}
|
|
5032
|
-
|
|
5033
|
-
// Child has parameter at same position - compatible
|
|
5034
|
-
return true;
|
|
5035
|
-
};
|
|
5036
|
-
|
|
5037
3824
|
/**
|
|
5038
3825
|
* Parse a route pattern string into structured segments
|
|
5039
3826
|
*/
|
|
@@ -5546,9 +4333,6 @@ const extractSearchParams = (urlObj, queryConnectionMap) => {
|
|
|
5546
4333
|
return params;
|
|
5547
4334
|
};
|
|
5548
4335
|
|
|
5549
|
-
/**
|
|
5550
|
-
* Build query parameters respecting hierarchical order from ancestor patterns
|
|
5551
|
-
*/
|
|
5552
4336
|
/**
|
|
5553
4337
|
* Build hierarchical query parameters from pattern hierarchy
|
|
5554
4338
|
*
|
|
@@ -5587,17 +4371,13 @@ const buildHierarchicalQueryParams = (
|
|
|
5587
4371
|
}
|
|
5588
4372
|
}
|
|
5589
4373
|
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
`ancestorPatterns:`,
|
|
5598
|
-
ancestorPatterns.map((p) => p.urlPatternRaw),
|
|
5599
|
-
);
|
|
5600
|
-
}
|
|
4374
|
+
debug$3(`Building params for ${originalPattern}`);
|
|
4375
|
+
debug$3(`parsedPattern:`, parsedPattern.original);
|
|
4376
|
+
debug$3(`params:`, params);
|
|
4377
|
+
debug$3(
|
|
4378
|
+
`ancestorPatterns:`,
|
|
4379
|
+
ancestorPatterns.map((p) => p.urlPatternRaw),
|
|
4380
|
+
);
|
|
5601
4381
|
|
|
5602
4382
|
// Step 1: Add query parameters from ancestor patterns (oldest to newest)
|
|
5603
4383
|
// This ensures ancestor parameters come first in their declaration order
|
|
@@ -5610,23 +4390,17 @@ const buildHierarchicalQueryParams = (
|
|
|
5610
4390
|
queryParams[paramName] = params[paramName];
|
|
5611
4391
|
processedParams.add(paramName);
|
|
5612
4392
|
|
|
5613
|
-
|
|
5614
|
-
console.debug(
|
|
5615
|
-
`Added ancestor param: ${paramName}=${params[paramName]}`,
|
|
5616
|
-
);
|
|
5617
|
-
}
|
|
4393
|
+
debug$3(`Added ancestor param: ${paramName}=${params[paramName]}`);
|
|
5618
4394
|
}
|
|
5619
4395
|
}
|
|
5620
4396
|
}
|
|
5621
4397
|
|
|
5622
4398
|
// Step 2: Add query parameters from current pattern
|
|
5623
4399
|
if (parsedPattern.queryParams) {
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
);
|
|
5629
|
-
}
|
|
4400
|
+
debug$3(
|
|
4401
|
+
`Processing current pattern query params:`,
|
|
4402
|
+
parsedPattern.queryParams.map((q) => q.name),
|
|
4403
|
+
);
|
|
5630
4404
|
|
|
5631
4405
|
for (const queryParam of parsedPattern.queryParams) {
|
|
5632
4406
|
const paramName = queryParam.name;
|
|
@@ -5634,11 +4408,7 @@ const buildHierarchicalQueryParams = (
|
|
|
5634
4408
|
queryParams[paramName] = params[paramName];
|
|
5635
4409
|
processedParams.add(paramName);
|
|
5636
4410
|
|
|
5637
|
-
|
|
5638
|
-
console.debug(
|
|
5639
|
-
`Added current param: ${paramName}=${params[paramName]}`,
|
|
5640
|
-
);
|
|
5641
|
-
}
|
|
4411
|
+
debug$3(`Added current param: ${paramName}=${params[paramName]}`);
|
|
5642
4412
|
}
|
|
5643
4413
|
}
|
|
5644
4414
|
}
|
|
@@ -5842,43 +4612,6 @@ const isChildPattern = (childPattern, parentPattern) => {
|
|
|
5842
4612
|
return childSegments.length > parentSegments.length || hasMoreSpecificSegment;
|
|
5843
4613
|
};
|
|
5844
4614
|
|
|
5845
|
-
/**
|
|
5846
|
-
* Check if a parameter is redundant because the child pattern already has it as a literal segment
|
|
5847
|
-
* E.g., parameter "section" is redundant for pattern "/admin/settings/:tab" because "settings" is literal
|
|
5848
|
-
*/
|
|
5849
|
-
const isParameterRedundantWithLiteralSegments = (
|
|
5850
|
-
childPattern,
|
|
5851
|
-
parentPattern,
|
|
5852
|
-
paramName,
|
|
5853
|
-
) => {
|
|
5854
|
-
// Find which segment position corresponds to this parameter in the parent
|
|
5855
|
-
let paramSegmentIndex = -1;
|
|
5856
|
-
for (let i = 0; i < parentPattern.segments.length; i++) {
|
|
5857
|
-
const segment = parentPattern.segments[i];
|
|
5858
|
-
if (segment.type === "param" && segment.name === paramName) {
|
|
5859
|
-
paramSegmentIndex = i;
|
|
5860
|
-
break;
|
|
5861
|
-
}
|
|
5862
|
-
}
|
|
5863
|
-
|
|
5864
|
-
// If parameter not found in parent segments, it's not redundant with path
|
|
5865
|
-
if (paramSegmentIndex === -1) {
|
|
5866
|
-
return false;
|
|
5867
|
-
}
|
|
5868
|
-
|
|
5869
|
-
// Check if child has a literal segment at the same position
|
|
5870
|
-
if (childPattern.segments.length > paramSegmentIndex) {
|
|
5871
|
-
const childSegment = childPattern.segments[paramSegmentIndex];
|
|
5872
|
-
if (childSegment.type === "literal") {
|
|
5873
|
-
// Child has a literal segment where parent has parameter
|
|
5874
|
-
// This means the child is more specific and shouldn't inherit this parameter
|
|
5875
|
-
return true; // Redundant - child already specifies this position with a literal
|
|
5876
|
-
}
|
|
5877
|
-
}
|
|
5878
|
-
|
|
5879
|
-
return false;
|
|
5880
|
-
};
|
|
5881
|
-
|
|
5882
4615
|
/**
|
|
5883
4616
|
* Register all patterns at once and build their relationships
|
|
5884
4617
|
*/
|
|
@@ -5914,6 +4647,17 @@ const setupRoutePatterns = (routePatterns) => {
|
|
|
5914
4647
|
otherRoutePattern.children.push(routePattern);
|
|
5915
4648
|
}
|
|
5916
4649
|
}
|
|
4650
|
+
// Phase 2b: Compute family roots. Two patterns are in the same family when
|
|
4651
|
+
// their parent chains meet — one is ancestor of the other, or they share a
|
|
4652
|
+
// common ancestor. Each pattern has a single parent, so that is exactly:
|
|
4653
|
+
// same topmost ancestor (familyRoot equality).
|
|
4654
|
+
for (const routePattern of routePatternSet) {
|
|
4655
|
+
let root = routePattern;
|
|
4656
|
+
while (root.parent) {
|
|
4657
|
+
root = root.parent;
|
|
4658
|
+
}
|
|
4659
|
+
routePattern.familyRoot = root;
|
|
4660
|
+
}
|
|
5917
4661
|
// Phase 3: Inherit search parameter connections from ancestors
|
|
5918
4662
|
// Search params are global and should be inherited by descendants regardless of path segments
|
|
5919
4663
|
for (const routePattern of routePatternSet) {
|
|
@@ -5971,13 +4715,11 @@ const setupRoutePatterns = (routePatterns) => {
|
|
|
5971
4715
|
routePattern.queryConnectionMap.set(paramName, inheritedConnection);
|
|
5972
4716
|
routePattern.connections.push(inheritedConnection);
|
|
5973
4717
|
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
} else if (DEBUG$2) {
|
|
5980
|
-
console.debug(
|
|
4718
|
+
debug$3(
|
|
4719
|
+
`[${routePattern.originalPattern}] Inherited search param "${paramName}" from ancestor [${ancestorRoutePattern.originalPattern}]`,
|
|
4720
|
+
);
|
|
4721
|
+
} else {
|
|
4722
|
+
debug$3(
|
|
5981
4723
|
`[${routePattern.originalPattern}] Skipped inheriting "${paramName}" - child uses default values, not truly more specific`,
|
|
5982
4724
|
);
|
|
5983
4725
|
}
|
|
@@ -5985,48 +4727,7 @@ const setupRoutePatterns = (routePatterns) => {
|
|
|
5985
4727
|
ancestorRoutePattern = ancestorRoutePattern.parent;
|
|
5986
4728
|
}
|
|
5987
4729
|
}
|
|
5988
|
-
// Phase 4:
|
|
5989
|
-
for (const routePattern of routePatternSet) {
|
|
5990
|
-
const allRelevantSignals = new Set();
|
|
5991
|
-
|
|
5992
|
-
// Add own signals
|
|
5993
|
-
for (const signal of routePattern.signalSet) {
|
|
5994
|
-
allRelevantSignals.add(signal);
|
|
5995
|
-
}
|
|
5996
|
-
|
|
5997
|
-
// Add signals from ancestors (they might be inherited)
|
|
5998
|
-
let parentRoutePattern = routePattern.parent;
|
|
5999
|
-
while (parentRoutePattern) {
|
|
6000
|
-
for (const connection of parentRoutePattern.connections) {
|
|
6001
|
-
allRelevantSignals.add(connection.signal);
|
|
6002
|
-
}
|
|
6003
|
-
// Move up the parent chain
|
|
6004
|
-
parentRoutePattern = parentRoutePattern.parent;
|
|
6005
|
-
}
|
|
6006
|
-
|
|
6007
|
-
// Add signals from descendants (they might be used for optimization)
|
|
6008
|
-
const addDescendantSignals = (patternObj) => {
|
|
6009
|
-
for (const childPattern of patternObj.children) {
|
|
6010
|
-
// Add child's own signals
|
|
6011
|
-
for (const connection of childPattern.connections) {
|
|
6012
|
-
allRelevantSignals.add(connection.signal);
|
|
6013
|
-
}
|
|
6014
|
-
// Recursively add grandchildren signals
|
|
6015
|
-
addDescendantSignals(childPattern);
|
|
6016
|
-
}
|
|
6017
|
-
};
|
|
6018
|
-
addDescendantSignals(routePattern);
|
|
6019
|
-
|
|
6020
|
-
// Update the pattern's signalSet with all relevant signals
|
|
6021
|
-
routePattern.signalSet = allRelevantSignals;
|
|
6022
|
-
|
|
6023
|
-
if (DEBUG$2 && allRelevantSignals.size > 0) {
|
|
6024
|
-
console.debug(
|
|
6025
|
-
`[${routePattern.urlPatternRaw}] Collected ${allRelevantSignals.size} relevant signals`,
|
|
6026
|
-
);
|
|
6027
|
-
}
|
|
6028
|
-
}
|
|
6029
|
-
// Phase 5: Precompute descendant path signals for each pattern (used by canReachLiteralValue)
|
|
4730
|
+
// Phase 4: Precompute descendant path signals for each pattern (used by canReachLiteralValue)
|
|
6030
4731
|
// Stored as a Map<segmentIndex, conn[]> for O(1) lookup by position.
|
|
6031
4732
|
for (const routePattern of routePatternSet) {
|
|
6032
4733
|
const descendantPathSignalsByIndex = new Map();
|
|
@@ -6054,7 +4755,7 @@ const setupRoutePatterns = (routePatterns) => {
|
|
|
6054
4755
|
collectDescendantPathSignals(routePattern);
|
|
6055
4756
|
routePattern.descendantPathSignals = descendantPathSignalsByIndex;
|
|
6056
4757
|
}
|
|
6057
|
-
// Phase
|
|
4758
|
+
// Phase 4b: Flag path params whose values are ALSO declared as literal routes
|
|
6058
4759
|
// ("/games/me/done" next to "/games/me/:section"). That declaration is the
|
|
6059
4760
|
// only reliable statement that the param names pages rather than qualifying
|
|
6060
4761
|
// one — read by shouldUseChildRoute to decide whether an ancestor url may
|
|
@@ -6105,13 +4806,11 @@ const setupRoutePatterns = (routePatterns) => {
|
|
|
6105
4806
|
}
|
|
6106
4807
|
}
|
|
6107
4808
|
}
|
|
6108
|
-
// Phase
|
|
4809
|
+
// Phase 5: Calculate depths for all patterns
|
|
6109
4810
|
for (const routePattern of routePatternSet) {
|
|
6110
4811
|
calculatePatternDepth(routePattern);
|
|
6111
4812
|
}
|
|
6112
|
-
|
|
6113
|
-
console.debug("Pattern registry updated");
|
|
6114
|
-
}
|
|
4813
|
+
debug$3("Pattern registry updated");
|
|
6115
4814
|
};
|
|
6116
4815
|
// Store the most specific parent (closest parent in hierarchy)
|
|
6117
4816
|
const getPathSegmentCount = (pattern) => {
|
|
@@ -6430,14 +5129,11 @@ const route = (pattern, { searchParams } = {}) => {
|
|
|
6430
5129
|
// Only sync non-default values to keep URLs clean (static fallbacks stay invisible)
|
|
6431
5130
|
registerSetup(() => {
|
|
6432
5131
|
const cleanupSignalUrlEffectSet = new Set();
|
|
6433
|
-
|
|
6434
|
-
//
|
|
6435
|
-
const
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
]);
|
|
6439
|
-
for (const [paramName, connection] of connectionMap) {
|
|
6440
|
-
const { signal: paramSignal, debug } = connection;
|
|
5132
|
+
// important: read connections at setup time so it includes query connections
|
|
5133
|
+
// inherited from ancestor patterns
|
|
5134
|
+
const { connections } = routePattern;
|
|
5135
|
+
for (const connection of connections) {
|
|
5136
|
+
const { signal: paramSignal, debug, paramName } = connection;
|
|
6441
5137
|
if (debug) {
|
|
6442
5138
|
console.debug(
|
|
6443
5139
|
`[route] connecting url param "${paramName}" to signal`,
|
|
@@ -6654,14 +5350,11 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
6654
5350
|
newMatching,
|
|
6655
5351
|
} of routeMatchInfoSet) {
|
|
6656
5352
|
const { routePattern } = routePrivateProperties;
|
|
6657
|
-
const { pathConnectionMap, queryConnectionMap } =
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
for (const [paramName, connection] of connectionMap) {
|
|
6664
|
-
const { signal: paramSignal, debug } = connection;
|
|
5353
|
+
const { pathConnectionMap, queryConnectionMap, connections } =
|
|
5354
|
+
routePattern;
|
|
5355
|
+
|
|
5356
|
+
for (const connection of connections) {
|
|
5357
|
+
const { signal: paramSignal, debug, paramName } = connection;
|
|
6665
5358
|
const rawParams = route.rawParamsSignal.value;
|
|
6666
5359
|
const urlParamValue = rawParams[paramName];
|
|
6667
5360
|
|
|
@@ -6675,66 +5368,17 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
6675
5368
|
continue;
|
|
6676
5369
|
}
|
|
6677
5370
|
const otherRawParams = otherRoute.rawParamsSignal.value;
|
|
6678
|
-
const otherRoutePrivateProperties =
|
|
6679
|
-
getRoutePrivateProperties(otherRoute);
|
|
6680
5371
|
|
|
6681
5372
|
// Check if this matching route extracts the parameter
|
|
6682
5373
|
if (paramName in otherRawParams) {
|
|
6683
5374
|
parameterExtractedByMatchingRoute = true;
|
|
6684
5375
|
}
|
|
6685
5376
|
|
|
6686
|
-
//
|
|
6687
|
-
|
|
5377
|
+
// Same family = same topmost ancestor
|
|
5378
|
+
// (familyRoot, computed in setupRoutePatterns)
|
|
6688
5379
|
const otherPatternObj =
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
// Routes are in same family if they share a hierarchical relationship:
|
|
6692
|
-
// 1. One is parent/ancestor of the other
|
|
6693
|
-
// 2. They share a common parent/ancestor
|
|
6694
|
-
let inSameFamily = false;
|
|
6695
|
-
|
|
6696
|
-
// Check if other route is ancestor of this route
|
|
6697
|
-
let currentParent = thisPatternObj.parent;
|
|
6698
|
-
while (currentParent) {
|
|
6699
|
-
if (currentParent === otherPatternObj) {
|
|
6700
|
-
inSameFamily = true;
|
|
6701
|
-
break;
|
|
6702
|
-
}
|
|
6703
|
-
currentParent = currentParent.parent;
|
|
6704
|
-
}
|
|
6705
|
-
|
|
6706
|
-
// Check if this route is ancestor of other route
|
|
6707
|
-
if (!inSameFamily) {
|
|
6708
|
-
currentParent = otherPatternObj.parent;
|
|
6709
|
-
while (currentParent) {
|
|
6710
|
-
if (currentParent === thisPatternObj) {
|
|
6711
|
-
inSameFamily = true;
|
|
6712
|
-
break;
|
|
6713
|
-
}
|
|
6714
|
-
currentParent = currentParent.parent;
|
|
6715
|
-
}
|
|
6716
|
-
}
|
|
6717
|
-
|
|
6718
|
-
// Check if they share a common parent (siblings or cousins)
|
|
6719
|
-
if (!inSameFamily) {
|
|
6720
|
-
const thisAncestors = new Set();
|
|
6721
|
-
currentParent = thisPatternObj.parent;
|
|
6722
|
-
while (currentParent) {
|
|
6723
|
-
thisAncestors.add(currentParent);
|
|
6724
|
-
currentParent = currentParent.parent;
|
|
6725
|
-
}
|
|
6726
|
-
|
|
6727
|
-
currentParent = otherPatternObj.parent;
|
|
6728
|
-
while (currentParent) {
|
|
6729
|
-
if (thisAncestors.has(currentParent)) {
|
|
6730
|
-
inSameFamily = true;
|
|
6731
|
-
break;
|
|
6732
|
-
}
|
|
6733
|
-
currentParent = currentParent.parent;
|
|
6734
|
-
}
|
|
6735
|
-
}
|
|
6736
|
-
|
|
6737
|
-
if (inSameFamily) {
|
|
5380
|
+
getRoutePrivateProperties(otherRoute).routePattern;
|
|
5381
|
+
if (otherPatternObj.familyRoot === routePattern.familyRoot) {
|
|
6738
5382
|
matchingRouteInSameFamily = true;
|
|
6739
5383
|
}
|
|
6740
5384
|
}
|
|
@@ -25013,6 +23657,22 @@ const useUIStateController = (
|
|
|
25013
23657
|
const isProxy = Boolean(props["navi-control-proxy-for"]);
|
|
25014
23658
|
const emptyUIState = resolveEmptyUIState(props, controlType);
|
|
25015
23659
|
|
|
23660
|
+
// Live values controller methods read through the scope (`s.…`) — one list
|
|
23661
|
+
// feeding both init and update: init so the values exist before any
|
|
23662
|
+
// re-render, update so they follow the renders. A value listed in only one
|
|
23663
|
+
// of the two goes stale on mount or across re-renders, silently.
|
|
23664
|
+
const liveValues = () => ({
|
|
23665
|
+
ref: props.ref,
|
|
23666
|
+
id: props.id,
|
|
23667
|
+
name: props.name,
|
|
23668
|
+
props,
|
|
23669
|
+
controlInfo,
|
|
23670
|
+
syncDomState,
|
|
23671
|
+
uiAction: props.uiAction,
|
|
23672
|
+
uiActionInternal,
|
|
23673
|
+
parentUIStateController,
|
|
23674
|
+
});
|
|
23675
|
+
|
|
25016
23676
|
const scope = useRenderScope(
|
|
25017
23677
|
// ── init: runs once on mount ───────────────────────────────────────────
|
|
25018
23678
|
// Creates the controller and all long-lived objects. Captures first-render
|
|
@@ -25099,7 +23759,9 @@ const useUIStateController = (
|
|
|
25099
23759
|
if (controller.facadeChild) {
|
|
25100
23760
|
const child = controller.facadeChild;
|
|
25101
23761
|
const childManaged = child.getManagedControls();
|
|
25102
|
-
if (childManaged.length > 0)
|
|
23762
|
+
if (childManaged.length > 0) {
|
|
23763
|
+
return childManaged;
|
|
23764
|
+
}
|
|
25103
23765
|
return [child];
|
|
25104
23766
|
}
|
|
25105
23767
|
return [];
|
|
@@ -25221,7 +23883,9 @@ const useUIStateController = (
|
|
|
25221
23883
|
// set immediatly (don't wait for preact re-render) so ui is in the right state for:
|
|
25222
23884
|
// - side effect
|
|
25223
23885
|
// - any "input" event that might be dispatched below
|
|
25224
|
-
syncDomState
|
|
23886
|
+
// Read through the scope: syncDomState closes over the render's props
|
|
23887
|
+
// (ref, type, pad), so the mount-time one would write a stale element.
|
|
23888
|
+
s.syncDomState(newUIState, e);
|
|
25225
23889
|
controller.uiState = newUIState;
|
|
25226
23890
|
ownUIStateSignal.value = newUIState;
|
|
25227
23891
|
const controlProxyFor =
|
|
@@ -25242,7 +23906,9 @@ const useUIStateController = (
|
|
|
25242
23906
|
);
|
|
25243
23907
|
chainEvent(siblingUncheckEvent, e);
|
|
25244
23908
|
for (const siblingController of siblings) {
|
|
25245
|
-
if (siblingController === controller)
|
|
23909
|
+
if (siblingController === controller) {
|
|
23910
|
+
continue;
|
|
23911
|
+
}
|
|
25246
23912
|
if (
|
|
25247
23913
|
siblingController.parentUIStateController !==
|
|
25248
23914
|
s.parentUIStateController
|
|
@@ -25454,10 +24120,8 @@ const useUIStateController = (
|
|
|
25454
24120
|
resetUIState: (e) => {
|
|
25455
24121
|
controller.setUIState(controller.state, e);
|
|
25456
24122
|
},
|
|
25457
|
-
onActionEnd:
|
|
24123
|
+
onActionEnd: (e) => {
|
|
25458
24124
|
debugUIState(`"${controlType}" actionEnd called`);
|
|
25459
|
-
// wait for preact to re-render to update readonly as action end side effects are runned
|
|
25460
|
-
// await new Promise((r) => requestAnimationFrame(r));
|
|
25461
24125
|
controller.rules.validation.syncValidity(e);
|
|
25462
24126
|
},
|
|
25463
24127
|
onActionError: (e) => {
|
|
@@ -25493,20 +24157,10 @@ const useUIStateController = (
|
|
|
25493
24157
|
});
|
|
25494
24158
|
controller.rules = rules;
|
|
25495
24159
|
|
|
25496
|
-
// Include all values that controller methods read from the scope so they
|
|
25497
|
-
// are available immediately — even if no re-render happens before the
|
|
25498
|
-
// first user interaction (update only runs on re-renders, not on mount).
|
|
25499
24160
|
return {
|
|
25500
24161
|
controller,
|
|
25501
|
-
ref: props.ref,
|
|
25502
|
-
id: props.id,
|
|
25503
|
-
name: props.name,
|
|
25504
|
-
props,
|
|
25505
|
-
controlInfo,
|
|
25506
|
-
uiAction: props.uiAction,
|
|
25507
|
-
uiActionInternal,
|
|
25508
|
-
parentUIStateController,
|
|
25509
24162
|
parentUiStateSignalHolder,
|
|
24163
|
+
...liveValues(),
|
|
25510
24164
|
};
|
|
25511
24165
|
},
|
|
25512
24166
|
// ── update: runs every render after the first ─────────────────────────
|
|
@@ -25567,16 +24221,7 @@ const useUIStateController = (
|
|
|
25567
24221
|
}
|
|
25568
24222
|
}
|
|
25569
24223
|
}
|
|
25570
|
-
return
|
|
25571
|
-
ref: props.ref,
|
|
25572
|
-
id: props.id,
|
|
25573
|
-
name: props.name,
|
|
25574
|
-
props,
|
|
25575
|
-
controlInfo,
|
|
25576
|
-
uiAction: props.uiAction,
|
|
25577
|
-
uiActionInternal,
|
|
25578
|
-
parentUIStateController,
|
|
25579
|
-
};
|
|
24224
|
+
return liveValues();
|
|
25580
24225
|
},
|
|
25581
24226
|
);
|
|
25582
24227
|
scope.parentUiStateSignalHolder.value =
|
|
@@ -25607,11 +24252,13 @@ const useUIStateController = (
|
|
|
25607
24252
|
return undefined;
|
|
25608
24253
|
}
|
|
25609
24254
|
|
|
25610
|
-
debugUIState(
|
|
24255
|
+
debugUIState(
|
|
24256
|
+
`"${controlType}" registering into "${parentController.controlType}"`,
|
|
24257
|
+
);
|
|
25611
24258
|
parentController.registerChild(controller);
|
|
25612
24259
|
return () => {
|
|
25613
24260
|
debugUIState(
|
|
25614
|
-
`"${controlType}" unregistering from "${
|
|
24261
|
+
`"${controlType}" unregistering from "${parentController.controlType}"`,
|
|
25615
24262
|
);
|
|
25616
24263
|
parentController.unregisterChild(controller);
|
|
25617
24264
|
};
|
|
@@ -25620,42 +24267,8 @@ const useUIStateController = (
|
|
|
25620
24267
|
return controller;
|
|
25621
24268
|
};
|
|
25622
24269
|
|
|
25623
|
-
|
|
25624
|
-
|
|
25625
|
-
*
|
|
25626
|
-
* Children register themselves automatically on mount and unregister on unmount.
|
|
25627
|
-
* Whenever a child fires a UI action, the group re-aggregates all child states
|
|
25628
|
-
* via `aggregateChildStates` and reacts accordingly.
|
|
25629
|
-
*
|
|
25630
|
-
* **Three distinct methods — each with a clear responsibility**:
|
|
25631
|
-
*
|
|
25632
|
-
* - `setUIState(newUIState, e)` — called when a child UI action **changes** the aggregated value.
|
|
25633
|
-
* Updates the group state, then calls `onUIAction(e)` for user-observable reactions
|
|
25634
|
-
* (uiAction, command), then dispatches `navi_ui_state_change` so `control_hooks.jsx`
|
|
25635
|
-
* can trigger the action pipeline (constraints → execute action).
|
|
25636
|
-
*
|
|
25637
|
-
* - `syncInternalState(newUIState)` — called silently during mount/unmount/render-batch.
|
|
25638
|
-
* Updates state and signal with no external reactions whatsoever.
|
|
25639
|
-
*
|
|
25640
|
-
* - `onUIAction(e)` — called when a child's UI action does **not** change the aggregated
|
|
25641
|
-
* value (e.g. re-clicking an already-selected radio). Fires `uiAction` + `command` only;
|
|
25642
|
-
* does not touch state, does not trigger the action pipeline.
|
|
25643
|
-
*
|
|
25644
|
-
* **Child UI action flow**:
|
|
25645
|
-
* 1. Child leaf fires `notifyParentAboutChildUIAction(e, { stateChanged })`.
|
|
25646
|
-
* 2. Group's `onChildUIAction` receives it.
|
|
25647
|
-
* - If `stateChanged=true`: re-aggregates → `setUIState` → full reactions + action pipeline.
|
|
25648
|
-
* - If `stateChanged=false`: calls `onUIAction` → uiAction + command only.
|
|
25649
|
-
*
|
|
25650
|
-
* **Filtering**: `childControlFilter` can exclude certain child types from aggregation
|
|
25651
|
-
* (e.g. ignoring buttons inside a selectable list).
|
|
25652
|
-
*/
|
|
25653
|
-
const CANNOT_DERIVE = Symbol("cannot_derive");
|
|
25654
|
-
|
|
25655
|
-
// Default aggregate/distribute implementations keyed by controlType or stateType.
|
|
25656
|
-
// Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
|
|
25657
|
-
// distributeChildUIState. If neither a default nor an explicit impl is found for a
|
|
25658
|
-
// group, creation throws so the caller knows it must supply them.
|
|
24270
|
+
const CANNOT_DERIVE = Symbol("cannot_derive");
|
|
24271
|
+
|
|
25659
24272
|
// A child that groups other controls and was given no name of its own, holding
|
|
25660
24273
|
// an object — the only shape that can be merged into the object around it.
|
|
25661
24274
|
// A nameless LEAF (an input nobody named) is still a mistake and still warns.
|
|
@@ -25665,19 +24278,25 @@ const isNamelessGrouping = (child, uiState) =>
|
|
|
25665
24278
|
typeof uiState === "object" &&
|
|
25666
24279
|
!Array.isArray(uiState);
|
|
25667
24280
|
|
|
24281
|
+
const firstDefinedChildUIState = (children) => {
|
|
24282
|
+
for (const child of children) {
|
|
24283
|
+
const childUIState = child.uiState;
|
|
24284
|
+
if (childUIState !== undefined) {
|
|
24285
|
+
return childUIState;
|
|
24286
|
+
}
|
|
24287
|
+
}
|
|
24288
|
+
return undefined;
|
|
24289
|
+
};
|
|
24290
|
+
|
|
24291
|
+
// Default aggregate/distribute implementations keyed by controlType or stateType.
|
|
24292
|
+
// Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
|
|
24293
|
+
// distributeChildUIState. If neither a default nor an explicit impl is found for a
|
|
24294
|
+
// group, creation throws so the caller knows it must supply them.
|
|
25668
24295
|
const GROUP_DEFAULTS = {
|
|
25669
24296
|
radio_group: {
|
|
25670
24297
|
childControlFilter: (child) =>
|
|
25671
24298
|
child.controlType === "input" && child.controlHostProps?.type === "radio",
|
|
25672
|
-
aggregateChildStates:
|
|
25673
|
-
for (const child of children) {
|
|
25674
|
-
const childUIState = child.uiState;
|
|
25675
|
-
if (childUIState !== undefined) {
|
|
25676
|
-
return childUIState;
|
|
25677
|
-
}
|
|
25678
|
-
}
|
|
25679
|
-
return undefined;
|
|
25680
|
-
},
|
|
24299
|
+
aggregateChildStates: firstDefinedChildUIState,
|
|
25681
24300
|
distributeChildUIState: (newUIState, childUIStateController) => {
|
|
25682
24301
|
const childSelected = childUIStateController.props.value === newUIState;
|
|
25683
24302
|
if (childSelected) {
|
|
@@ -25734,15 +24353,7 @@ const GROUP_DEFAULTS = {
|
|
|
25734
24353
|
}
|
|
25735
24354
|
return true;
|
|
25736
24355
|
},
|
|
25737
|
-
aggregateChildStates:
|
|
25738
|
-
for (const child of children) {
|
|
25739
|
-
const childUIState = child.uiState;
|
|
25740
|
-
if (childUIState !== undefined) {
|
|
25741
|
-
return childUIState;
|
|
25742
|
-
}
|
|
25743
|
-
}
|
|
25744
|
-
return undefined;
|
|
25745
|
-
},
|
|
24356
|
+
aggregateChildStates: firstDefinedChildUIState,
|
|
25746
24357
|
distributeChildUIState: (newUIState) => newUIState,
|
|
25747
24358
|
},
|
|
25748
24359
|
object: {
|
|
@@ -25811,6 +24422,36 @@ const GROUP_DEFAULTS = {
|
|
|
25811
24422
|
},
|
|
25812
24423
|
};
|
|
25813
24424
|
|
|
24425
|
+
/**
|
|
24426
|
+
* Manages the aggregated UI state of a group of child controls (radio list, checkbox list, etc.).
|
|
24427
|
+
*
|
|
24428
|
+
* Children register themselves automatically on mount and unregister on unmount.
|
|
24429
|
+
* Whenever a child fires a UI action, the group re-aggregates all child states
|
|
24430
|
+
* via `aggregateChildStates` and reacts accordingly.
|
|
24431
|
+
*
|
|
24432
|
+
* **Three distinct methods — each with a clear responsibility**:
|
|
24433
|
+
*
|
|
24434
|
+
* - `setUIState(newUIState, e)` — called when a child UI action **changes** the aggregated value.
|
|
24435
|
+
* Updates the group state, then calls `onUIAction(e)` for user-observable reactions
|
|
24436
|
+
* (uiAction, command), then dispatches `navi_ui_state_change` so `control_hooks.jsx`
|
|
24437
|
+
* can trigger the action pipeline (constraints → execute action).
|
|
24438
|
+
*
|
|
24439
|
+
* - `syncInternalState(newUIState)` — called silently during mount/unmount/render-batch.
|
|
24440
|
+
* Updates state and signal with no external reactions whatsoever.
|
|
24441
|
+
*
|
|
24442
|
+
* - `onUIAction(e)` — called when a child's UI action does **not** change the aggregated
|
|
24443
|
+
* value (e.g. re-clicking an already-selected radio). Fires `uiAction` + `command` only;
|
|
24444
|
+
* does not touch state, does not trigger the action pipeline.
|
|
24445
|
+
*
|
|
24446
|
+
* **Child UI action flow**:
|
|
24447
|
+
* 1. Child leaf fires `notifyParentAboutChildUIAction(e, { stateChanged })`.
|
|
24448
|
+
* 2. Group's `onChildUIAction` receives it.
|
|
24449
|
+
* - If `stateChanged=true`: re-aggregates → `setUIState` → full reactions + action pipeline.
|
|
24450
|
+
* - If `stateChanged=false`: calls `onUIAction` → uiAction + command only.
|
|
24451
|
+
*
|
|
24452
|
+
* **Filtering**: `childControlFilter` can exclude certain child types from aggregation
|
|
24453
|
+
* (e.g. ignoring buttons inside a selectable list).
|
|
24454
|
+
*/
|
|
25814
24455
|
const useUIGroupStateController = (
|
|
25815
24456
|
props,
|
|
25816
24457
|
controlType,
|
|
@@ -25890,7 +24531,9 @@ const useUIGroupStateController = (
|
|
|
25890
24531
|
pendingChangeRef.current = null;
|
|
25891
24532
|
|
|
25892
24533
|
const isMonitoringChild = (childUIStateController) => {
|
|
25893
|
-
if (childUIStateController.isProxy)
|
|
24534
|
+
if (childUIStateController.isProxy) {
|
|
24535
|
+
return false;
|
|
24536
|
+
}
|
|
25894
24537
|
if (
|
|
25895
24538
|
resolvedChildControlFilter &&
|
|
25896
24539
|
!resolvedChildControlFilter(childUIStateController)
|
|
@@ -25900,12 +24543,39 @@ const useUIGroupStateController = (
|
|
|
25900
24543
|
return true;
|
|
25901
24544
|
};
|
|
25902
24545
|
const shouldPropagateStateToChild = (childUIStateController) => {
|
|
25903
|
-
if (!isMonitoringChild(childUIStateController))
|
|
25904
|
-
|
|
25905
|
-
|
|
24546
|
+
if (!isMonitoringChild(childUIStateController)) {
|
|
24547
|
+
return false;
|
|
24548
|
+
}
|
|
24549
|
+
if (childUIStateController.controlType === "button") {
|
|
24550
|
+
return false;
|
|
24551
|
+
}
|
|
24552
|
+
if (childUIStateController.controlType === "link") {
|
|
24553
|
+
return false;
|
|
24554
|
+
}
|
|
25906
24555
|
return true;
|
|
25907
24556
|
};
|
|
25908
24557
|
|
|
24558
|
+
// Live values controller methods read through the scope (`s.…`) — same
|
|
24559
|
+
// contract as the leaf controller's liveValues above: one list feeding both
|
|
24560
|
+
// init and update.
|
|
24561
|
+
const liveValues = () => ({
|
|
24562
|
+
ref,
|
|
24563
|
+
parentUIStateController,
|
|
24564
|
+
uiAction,
|
|
24565
|
+
uiActionInternal,
|
|
24566
|
+
id,
|
|
24567
|
+
name,
|
|
24568
|
+
value,
|
|
24569
|
+
defaultValue,
|
|
24570
|
+
hasValueProp,
|
|
24571
|
+
hasDefaultValueProp,
|
|
24572
|
+
// `props` is what writeBoundSignal reads to find the bound `signal`.
|
|
24573
|
+
// Missing here, a group whose component never re-renders between mount
|
|
24574
|
+
// and the first choice wrote nothing back into its signal — and said
|
|
24575
|
+
// nothing about it: the list showed the choice, the signal stayed empty.
|
|
24576
|
+
props,
|
|
24577
|
+
});
|
|
24578
|
+
|
|
25909
24579
|
const scope = useRenderScope(
|
|
25910
24580
|
// ── init: runs once on mount ───────────────────────────────────────────
|
|
25911
24581
|
(s) => {
|
|
@@ -25915,6 +24585,14 @@ const useUIGroupStateController = (
|
|
|
25915
24585
|
const [publishUIState, subscribeUIState] = createPubSub();
|
|
25916
24586
|
const uiStateSignal = signal(fallbackState);
|
|
25917
24587
|
|
|
24588
|
+
const aggregateGroupUIState = () => {
|
|
24589
|
+
const aggChildState = resolvedAggregateChildStates(
|
|
24590
|
+
childUIStateControllerArray,
|
|
24591
|
+
fallbackState,
|
|
24592
|
+
);
|
|
24593
|
+
return aggChildState === undefined ? fallbackState : aggChildState;
|
|
24594
|
+
};
|
|
24595
|
+
|
|
25918
24596
|
// onChange and applyState live inside init so they close over the stable
|
|
25919
24597
|
// signals/pubsub without needing external refs.
|
|
25920
24598
|
const onChange = (e, { notifyExternal }) => {
|
|
@@ -25933,12 +24611,7 @@ const useUIGroupStateController = (
|
|
|
25933
24611
|
};
|
|
25934
24612
|
return;
|
|
25935
24613
|
}
|
|
25936
|
-
const
|
|
25937
|
-
childUIStateControllerArray,
|
|
25938
|
-
fallbackState,
|
|
25939
|
-
);
|
|
25940
|
-
const groupUIState =
|
|
25941
|
-
aggChildState === undefined ? fallbackState : aggChildState;
|
|
24614
|
+
const groupUIState = aggregateGroupUIState();
|
|
25942
24615
|
debugUIGroup(
|
|
25943
24616
|
e,
|
|
25944
24617
|
`${controlType}.getUIState -> ${JSON.stringify(groupUIState)}`,
|
|
@@ -25947,13 +24620,13 @@ const useUIGroupStateController = (
|
|
|
25947
24620
|
if (notifyExternal === true) {
|
|
25948
24621
|
applyState(groupUIState, e);
|
|
25949
24622
|
} else if (notifyExternal === "silent") {
|
|
25950
|
-
controller.syncInternalState(groupUIState
|
|
24623
|
+
controller.syncInternalState(groupUIState);
|
|
25951
24624
|
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
25952
24625
|
stateChanged: true,
|
|
25953
24626
|
silent: true,
|
|
25954
24627
|
});
|
|
25955
24628
|
} else {
|
|
25956
|
-
controller.syncInternalState(groupUIState
|
|
24629
|
+
controller.syncInternalState(groupUIState);
|
|
25957
24630
|
writeBoundSignal(groupUIState);
|
|
25958
24631
|
}
|
|
25959
24632
|
};
|
|
@@ -26073,12 +24746,7 @@ const useUIGroupStateController = (
|
|
|
26073
24746
|
propagateDownEvent,
|
|
26074
24747
|
);
|
|
26075
24748
|
}
|
|
26076
|
-
const
|
|
26077
|
-
childUIStateControllerArray,
|
|
26078
|
-
fallbackState,
|
|
26079
|
-
);
|
|
26080
|
-
const groupUIState =
|
|
26081
|
-
aggChildState === undefined ? fallbackState : aggChildState;
|
|
24749
|
+
const groupUIState = aggregateGroupUIState();
|
|
26082
24750
|
if (e.type === "initial_state_push") {
|
|
26083
24751
|
controller.syncInternalState(groupUIState);
|
|
26084
24752
|
return;
|
|
@@ -26090,7 +24758,9 @@ const useUIGroupStateController = (
|
|
|
26090
24758
|
},
|
|
26091
24759
|
syncInternalState: (newUIState) => {
|
|
26092
24760
|
const currentUIState = controller.uiState;
|
|
26093
|
-
if (newUIState === currentUIState)
|
|
24761
|
+
if (newUIState === currentUIState) {
|
|
24762
|
+
return;
|
|
24763
|
+
}
|
|
26094
24764
|
controller.uiState = newUIState;
|
|
26095
24765
|
uiStateSignal.value = newUIState;
|
|
26096
24766
|
publishUIState(newUIState);
|
|
@@ -26108,7 +24778,9 @@ const useUIGroupStateController = (
|
|
|
26108
24778
|
s.uiActionInternal?.(currentUIState, e);
|
|
26109
24779
|
if (!skipCommand && controller.props.command) {
|
|
26110
24780
|
const el = controller.ref.current;
|
|
26111
|
-
if (el)
|
|
24781
|
+
if (el) {
|
|
24782
|
+
triggerNaviCommand(el, controller.props.command, e);
|
|
24783
|
+
}
|
|
26112
24784
|
}
|
|
26113
24785
|
},
|
|
26114
24786
|
registerChild: (childUIStateController) => {
|
|
@@ -26159,7 +24831,9 @@ const useUIGroupStateController = (
|
|
|
26159
24831
|
});
|
|
26160
24832
|
return;
|
|
26161
24833
|
}
|
|
26162
|
-
if (!isMonitoringChild(childUIStateController))
|
|
24834
|
+
if (!isMonitoringChild(childUIStateController)) {
|
|
24835
|
+
return;
|
|
24836
|
+
}
|
|
26163
24837
|
const childControlType = childUIStateController.controlType;
|
|
26164
24838
|
debugUIGroup(
|
|
26165
24839
|
`${controlType}.onChildUIAction("${childControlType}") stateChanged=${stateChanged} -> child state: ${JSON.stringify(
|
|
@@ -26181,7 +24855,9 @@ const useUIGroupStateController = (
|
|
|
26181
24855
|
delegatedTo.unregisterChild(childUIStateController);
|
|
26182
24856
|
return;
|
|
26183
24857
|
}
|
|
26184
|
-
if (!isMonitoringChild(childUIStateController))
|
|
24858
|
+
if (!isMonitoringChild(childUIStateController)) {
|
|
24859
|
+
return;
|
|
24860
|
+
}
|
|
26185
24861
|
const childControlType = childUIStateController.controlType;
|
|
26186
24862
|
const index = childUIStateControllerArray.indexOf(
|
|
26187
24863
|
childUIStateController,
|
|
@@ -26194,7 +24870,7 @@ const useUIGroupStateController = (
|
|
|
26194
24870
|
}
|
|
26195
24871
|
childUIStateControllerArray.splice(index, 1);
|
|
26196
24872
|
debugUIGroup(
|
|
26197
|
-
`${controlType}.unregisterChild("${childControlType}") ->
|
|
24873
|
+
`${controlType}.unregisterChild("${childControlType}") -> unregistered (remaining: ${childUIStateControllerArray.length})`,
|
|
26198
24874
|
);
|
|
26199
24875
|
onChange(new CustomEvent(`${childControlType}_unmount`), {
|
|
26200
24876
|
notifyExternal: "silent",
|
|
@@ -26206,7 +24882,9 @@ const useUIGroupStateController = (
|
|
|
26206
24882
|
});
|
|
26207
24883
|
chainEvent(ev, e);
|
|
26208
24884
|
for (const c of childUIStateControllerArray) {
|
|
26209
|
-
if (shouldPropagateStateToChild(c))
|
|
24885
|
+
if (shouldPropagateStateToChild(c)) {
|
|
24886
|
+
c.resetUIState(ev);
|
|
24887
|
+
}
|
|
26210
24888
|
}
|
|
26211
24889
|
onChange(e, { notifyExternal: true });
|
|
26212
24890
|
},
|
|
@@ -26254,13 +24932,17 @@ const useUIGroupStateController = (
|
|
|
26254
24932
|
},
|
|
26255
24933
|
findChildById: (searchId) => {
|
|
26256
24934
|
for (const c of childUIStateControllerArray) {
|
|
26257
|
-
if (c.id === searchId)
|
|
24935
|
+
if (c.id === searchId) {
|
|
24936
|
+
return c;
|
|
24937
|
+
}
|
|
26258
24938
|
}
|
|
26259
24939
|
return null;
|
|
26260
24940
|
},
|
|
26261
24941
|
getChildControllers: () => childUIStateControllerArray,
|
|
26262
24942
|
getManagedControls: () => {
|
|
26263
|
-
if (!cascadeValidationToChildren)
|
|
24943
|
+
if (!cascadeValidationToChildren) {
|
|
24944
|
+
return [];
|
|
24945
|
+
}
|
|
26264
24946
|
return childUIStateControllerArray.slice();
|
|
26265
24947
|
},
|
|
26266
24948
|
// Group children sit next to the group itself: a busy one really does
|
|
@@ -26278,20 +24960,10 @@ const useUIGroupStateController = (
|
|
|
26278
24960
|
});
|
|
26279
24961
|
controller.rules = rules;
|
|
26280
24962
|
|
|
26281
|
-
// Include all values read by controller methods so they are immediately
|
|
26282
|
-
// available, even if the user interacts before the first re-render.
|
|
26283
24963
|
return {
|
|
26284
24964
|
controller,
|
|
26285
24965
|
_onChange: onChange,
|
|
26286
|
-
|
|
26287
|
-
parentUIStateController,
|
|
26288
|
-
uiAction,
|
|
26289
|
-
uiActionInternal,
|
|
26290
|
-
// `props` is what writeBoundSignal reads to find the bound `signal`.
|
|
26291
|
-
// Missing here, a group whose component never re-renders between mount
|
|
26292
|
-
// and the first choice wrote nothing back into its signal — and said
|
|
26293
|
-
// nothing about it: the list showed the choice, the signal stayed empty.
|
|
26294
|
-
props,
|
|
24966
|
+
...liveValues(),
|
|
26295
24967
|
};
|
|
26296
24968
|
},
|
|
26297
24969
|
// ── update: runs every render after the first ─────────────────────────
|
|
@@ -26308,10 +24980,7 @@ const useUIGroupStateController = (
|
|
|
26308
24980
|
controller.defaultValue = defaultValue;
|
|
26309
24981
|
controller.hasValueProp = hasValueProp;
|
|
26310
24982
|
controller.hasDefaultValueProp = hasDefaultValueProp;
|
|
26311
|
-
|
|
26312
|
-
hasValueProp &&
|
|
26313
|
-
(!prevHasValueProp || !compareTwoJsValues(value, prevValue))
|
|
26314
|
-
) {
|
|
24983
|
+
const placeChildrenFrom = (groupUIState) => {
|
|
26315
24984
|
const propagateDownEvent = new CustomEvent(
|
|
26316
24985
|
"propagate_down_set_ui_state",
|
|
26317
24986
|
{ detail: {} },
|
|
@@ -26319,11 +24988,17 @@ const useUIGroupStateController = (
|
|
|
26319
24988
|
for (const childUIStateController of childUIStateControllerArray) {
|
|
26320
24989
|
controller.placeChildUIState(
|
|
26321
24990
|
childUIStateController,
|
|
26322
|
-
|
|
24991
|
+
groupUIState,
|
|
26323
24992
|
propagateDownEvent,
|
|
26324
24993
|
);
|
|
26325
24994
|
}
|
|
26326
|
-
controller.syncInternalState(
|
|
24995
|
+
controller.syncInternalState(groupUIState);
|
|
24996
|
+
};
|
|
24997
|
+
if (
|
|
24998
|
+
hasValueProp &&
|
|
24999
|
+
(!prevHasValueProp || !compareTwoJsValues(value, prevValue))
|
|
25000
|
+
) {
|
|
25001
|
+
placeChildrenFrom(value);
|
|
26327
25002
|
}
|
|
26328
25003
|
if (
|
|
26329
25004
|
boundSignal &&
|
|
@@ -26334,33 +25009,10 @@ const useUIGroupStateController = (
|
|
|
26334
25009
|
// again, exactly as they were when they registered. Without this a
|
|
26335
25010
|
// group would answer a write to its own signal by silently writing its
|
|
26336
25011
|
// former value back over it on the next child interaction.
|
|
26337
|
-
|
|
26338
|
-
"propagate_down_set_ui_state",
|
|
26339
|
-
{ detail: {} },
|
|
26340
|
-
);
|
|
26341
|
-
for (const childUIStateController of childUIStateControllerArray) {
|
|
26342
|
-
controller.placeChildUIState(
|
|
26343
|
-
childUIStateController,
|
|
26344
|
-
defaultValue,
|
|
26345
|
-
propagateDownEvent,
|
|
26346
|
-
);
|
|
26347
|
-
}
|
|
26348
|
-
controller.syncInternalState(defaultValue);
|
|
25012
|
+
placeChildrenFrom(defaultValue);
|
|
26349
25013
|
}
|
|
26350
25014
|
|
|
26351
|
-
return
|
|
26352
|
-
ref,
|
|
26353
|
-
parentUIStateController,
|
|
26354
|
-
uiAction,
|
|
26355
|
-
uiActionInternal,
|
|
26356
|
-
id,
|
|
26357
|
-
name,
|
|
26358
|
-
value,
|
|
26359
|
-
defaultValue,
|
|
26360
|
-
hasValueProp,
|
|
26361
|
-
hasDefaultValueProp,
|
|
26362
|
-
props,
|
|
26363
|
-
};
|
|
25015
|
+
return liveValues();
|
|
26364
25016
|
},
|
|
26365
25017
|
);
|
|
26366
25018
|
|
|
@@ -26371,6 +25023,9 @@ const useUIGroupStateController = (
|
|
|
26371
25023
|
el.__uiStateController__ = controller;
|
|
26372
25024
|
}
|
|
26373
25025
|
return () => {
|
|
25026
|
+
if (el && el.__uiStateController__ === controller) {
|
|
25027
|
+
delete el.__uiStateController__;
|
|
25028
|
+
}
|
|
26374
25029
|
onUIStateControllerDestroyed(controller);
|
|
26375
25030
|
};
|
|
26376
25031
|
}, []);
|
|
@@ -26457,10 +25112,18 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
26457
25112
|
// ── init: runs once on mount ───────────────────────────────────────────
|
|
26458
25113
|
(s) => {
|
|
26459
25114
|
const canRegisterAsFacadeChild = (childController) => {
|
|
26460
|
-
if (childController.controlType === "button")
|
|
26461
|
-
|
|
26462
|
-
|
|
26463
|
-
if (childController.
|
|
25115
|
+
if (childController.controlType === "button") {
|
|
25116
|
+
return false;
|
|
25117
|
+
}
|
|
25118
|
+
if (childController.controlType === "link") {
|
|
25119
|
+
return false;
|
|
25120
|
+
}
|
|
25121
|
+
if (childController.controlType === "facade") {
|
|
25122
|
+
return false;
|
|
25123
|
+
}
|
|
25124
|
+
if (childController.isProxy) {
|
|
25125
|
+
return false;
|
|
25126
|
+
}
|
|
26464
25127
|
if (childController.allowNameless) {
|
|
26465
25128
|
// A control saying it is not a field is not the one the picker talks
|
|
26466
25129
|
// to: the search box above the list, the "select all" switch beside
|
|
@@ -26911,8 +25574,9 @@ const useControlProps = (props, {
|
|
|
26911
25574
|
props.id = props.id || controlId || idDefault;
|
|
26912
25575
|
const controlName = useContext(ControlNameContext);
|
|
26913
25576
|
props.name = props.name || controlName;
|
|
25577
|
+
const isCheckable = isCheckableInput(controlType, props.type);
|
|
26914
25578
|
const toDomProps = newUIState => {
|
|
26915
|
-
if (
|
|
25579
|
+
if (isCheckable) {
|
|
26916
25580
|
const domValue = toDomValue(props.value, {
|
|
26917
25581
|
controlType,
|
|
26918
25582
|
id: props.id,
|
|
@@ -27291,8 +25955,7 @@ const useControlProps = (props, {
|
|
|
27291
25955
|
}
|
|
27292
25956
|
return keyDownDefault(e);
|
|
27293
25957
|
};
|
|
27294
|
-
|
|
27295
|
-
if (isInputCheckable) {
|
|
25958
|
+
if (isCheckable) {
|
|
27296
25959
|
const isRadio = props.type === "radio";
|
|
27297
25960
|
|
|
27298
25961
|
// I've decided that enter on radio/checkbox would not submit form like browser does but
|
|
@@ -27307,7 +25970,6 @@ const useControlProps = (props, {
|
|
|
27307
25970
|
keyDown: e => {
|
|
27308
25971
|
if (e.key === "Enter") {
|
|
27309
25972
|
const inputEl = ref.current;
|
|
27310
|
-
const isRadio = props.type === "radio";
|
|
27311
25973
|
const checked = inputEl.checked;
|
|
27312
25974
|
const always = () => {
|
|
27313
25975
|
if (inputEl.form) {
|
|
@@ -27330,18 +25992,9 @@ const useControlProps = (props, {
|
|
|
27330
25992
|
always
|
|
27331
25993
|
};
|
|
27332
25994
|
}
|
|
27333
|
-
if (checked) {
|
|
27334
|
-
return {
|
|
27335
|
-
name: "enter to uncheck checkbox",
|
|
27336
|
-
allowed: () => dispatchRequestSetUIState(inputEl, undefined, {
|
|
27337
|
-
event: e
|
|
27338
|
-
}),
|
|
27339
|
-
always
|
|
27340
|
-
};
|
|
27341
|
-
}
|
|
27342
25995
|
return {
|
|
27343
|
-
name: "enter to check checkbox",
|
|
27344
|
-
allowed: () => dispatchRequestSetUIState(inputEl, uiStateController.value, {
|
|
25996
|
+
name: checked ? "enter to uncheck checkbox" : "enter to check checkbox",
|
|
25997
|
+
allowed: () => dispatchRequestSetUIState(inputEl, checked ? undefined : uiStateController.value, {
|
|
27345
25998
|
event: e
|
|
27346
25999
|
}),
|
|
27347
26000
|
always
|
|
@@ -27476,7 +26129,6 @@ const useControlProps = (props, {
|
|
|
27476
26129
|
// Same for radio siblings: when a sibling check unchecks this radio
|
|
27477
26130
|
// (radio_sibling_uncheck, internal event, no synthetic input), lastActionValueRef
|
|
27478
26131
|
// keeps the stale value and blocks the user from re-checking this radio.
|
|
27479
|
-
const isCheckable = controlType === "input" && (props.type === "radio" || props.type === "checkbox");
|
|
27480
26132
|
if (!isCheckable) {
|
|
27481
26133
|
const lastActionValue = lastActionValueRef.current;
|
|
27482
26134
|
const valueSameAsLastAction = lastActionValue !== NO_ACTION_YET && compareTwoJsValues(currentValue, lastActionValue);
|
|
@@ -27548,11 +26200,16 @@ const useControlProps = (props, {
|
|
|
27548
26200
|
// a custom concept being combination of "input", "change" and may other events
|
|
27549
26201
|
// this even if trigerred when value changes and can be controlled by actionDebounce and actionAfterChange
|
|
27550
26202
|
const hasNaviChangeEventReaction = Boolean(eventReactionDefinitions?.naviChange || defaultEventReactionDefinitions?.naviChange);
|
|
26203
|
+
// The input effect is installed once per element/options while the reaction
|
|
26204
|
+
// closures (boundAction, custom reactions) are per-render: read through a
|
|
26205
|
+
// ref so the effect always fires the current render's reaction.
|
|
26206
|
+
const applyEventReactionRef = useRef();
|
|
26207
|
+
applyEventReactionRef.current = applyEventReaction;
|
|
27551
26208
|
const refCallback = useCallback(field => {
|
|
27552
26209
|
if (!hasNaviChangeEventReaction || actionEvent === "custom") {
|
|
27553
26210
|
return undefined;
|
|
27554
26211
|
}
|
|
27555
|
-
return addInputEffect(field, e =>
|
|
26212
|
+
return addInputEffect(field, e => applyEventReactionRef.current("naviChange", e), {
|
|
27556
26213
|
waitForChange: actionAfterChange,
|
|
27557
26214
|
debounce: actionDebounce,
|
|
27558
26215
|
debugInteraction
|
|
@@ -27696,36 +26353,13 @@ const createControlInfo = (props, {
|
|
|
27696
26353
|
} else {
|
|
27697
26354
|
statePropName = "value";
|
|
27698
26355
|
defaultStatePropName = "defaultValue";
|
|
27699
|
-
|
|
27700
|
-
|
|
27701
|
-
|
|
27702
|
-
|
|
27703
|
-
|
|
27704
|
-
|
|
27705
|
-
|
|
27706
|
-
// a suggestion of what to start from (and what a reset goes back to),
|
|
27707
|
-
// not an answer — while the signal's value IS the answer, restored
|
|
27708
|
-
// from the url or set by whoever owns it. Taking the default here
|
|
27709
|
-
// would show a suggestion in place of the value on every reload.
|
|
27710
|
-
stateInitial = signal.value !== undefined ? signal.value : props.defaultValue;
|
|
27711
|
-
stateFromSignal = stateInitial;
|
|
27712
|
-
} else {
|
|
27713
|
-
// A plain bound signal with no default (e.g. Wheel): its live value
|
|
27714
|
-
// seeds and controls the state.
|
|
27715
|
-
hasStateProp = true;
|
|
27716
|
-
value = signal.value;
|
|
27717
|
-
stateInitial = value;
|
|
27718
|
-
}
|
|
27719
|
-
} else if (Object.hasOwn(props, "value")) {
|
|
27720
|
-
hasStateProp = true;
|
|
27721
|
-
value = props.value;
|
|
27722
|
-
stateInitial = value;
|
|
27723
|
-
} else if (Object.hasOwn(props, "defaultValue")) {
|
|
27724
|
-
hasStateProp = false;
|
|
27725
|
-
stateInitial = props.defaultValue;
|
|
27726
|
-
} else {
|
|
27727
|
-
hasStateProp = false;
|
|
27728
|
-
stateInitial = undefined;
|
|
26356
|
+
({
|
|
26357
|
+
hasStateProp,
|
|
26358
|
+
stateInitial,
|
|
26359
|
+
stateFromSignal
|
|
26360
|
+
} = resolveValueState(props, controlType, signal));
|
|
26361
|
+
if (hasStateProp) {
|
|
26362
|
+
value = stateInitial;
|
|
27729
26363
|
}
|
|
27730
26364
|
readOnlySupported = INPUT_TYPE_SUPPORTING_READONLY_SET.has(typeProp);
|
|
27731
26365
|
}
|
|
@@ -27742,27 +26376,11 @@ const createControlInfo = (props, {
|
|
|
27742
26376
|
} else if (controlType === "picker" || controlType === "select") {
|
|
27743
26377
|
statePropName = "value";
|
|
27744
26378
|
defaultStatePropName = "defaultValue";
|
|
27745
|
-
|
|
27746
|
-
|
|
27747
|
-
|
|
27748
|
-
|
|
27749
|
-
|
|
27750
|
-
stateInitial = signal.value !== undefined ? signal.value : props.defaultValue;
|
|
27751
|
-
stateFromSignal = stateInitial;
|
|
27752
|
-
} else {
|
|
27753
|
-
hasStateProp = true;
|
|
27754
|
-
stateInitial = signal.value;
|
|
27755
|
-
}
|
|
27756
|
-
} else if (Object.hasOwn(props, "value")) {
|
|
27757
|
-
hasStateProp = true;
|
|
27758
|
-
stateInitial = props.value;
|
|
27759
|
-
} else if (Object.hasOwn(props, "defaultValue")) {
|
|
27760
|
-
hasStateProp = false;
|
|
27761
|
-
stateInitial = props.defaultValue;
|
|
27762
|
-
} else {
|
|
27763
|
-
hasStateProp = false;
|
|
27764
|
-
stateInitial = undefined;
|
|
27765
|
-
}
|
|
26379
|
+
({
|
|
26380
|
+
hasStateProp,
|
|
26381
|
+
stateInitial,
|
|
26382
|
+
stateFromSignal
|
|
26383
|
+
} = resolveValueState(props, controlType, signal));
|
|
27766
26384
|
disabledSupported = true;
|
|
27767
26385
|
// A native <select> has no readonly attribute. What says it is read-only is
|
|
27768
26386
|
// aria-readonly plus a refused interaction — see the select reactions in
|
|
@@ -27797,6 +26415,52 @@ const createControlInfo = (props, {
|
|
|
27797
26415
|
disabledSupported
|
|
27798
26416
|
};
|
|
27799
26417
|
};
|
|
26418
|
+
// Who says what a value-holding control is worth — a bound signal, a `value`,
|
|
26419
|
+
// a `defaultValue` — resolved the same way for every control holding one value
|
|
26420
|
+
// (text input, picker, select). The checkbox/radio branch has its own
|
|
26421
|
+
// resolution: `checked` speaks in booleans and translates to the value.
|
|
26422
|
+
const resolveValueState = (props, controlType, signal) => {
|
|
26423
|
+
if (signal) {
|
|
26424
|
+
if (Object.hasOwn(props, "defaultValue")) {
|
|
26425
|
+
// A bound signal's own default is seeded into `defaultValue` (see
|
|
26426
|
+
// resolveInputProps), so such a control is uncontrolled-with-default;
|
|
26427
|
+
// the signal only receives write-backs (onUIAction).
|
|
26428
|
+
// A signal holding something wins over the default: `defaultValue` is
|
|
26429
|
+
// a suggestion of what to start from (and what a reset goes back to),
|
|
26430
|
+
// not an answer — while the signal's value IS the answer, restored
|
|
26431
|
+
// from the url or set by whoever owns it. Taking the default here
|
|
26432
|
+
// would show a suggestion in place of the value on every reload.
|
|
26433
|
+
const stateInitial = signal.value !== undefined ? signal.value : props.defaultValue;
|
|
26434
|
+
return {
|
|
26435
|
+
hasStateProp: false,
|
|
26436
|
+
stateInitial,
|
|
26437
|
+
stateFromSignal: stateInitial
|
|
26438
|
+
};
|
|
26439
|
+
}
|
|
26440
|
+
// A plain bound signal with no default (e.g. Wheel): its live value
|
|
26441
|
+
// seeds and controls the state.
|
|
26442
|
+
return {
|
|
26443
|
+
hasStateProp: true,
|
|
26444
|
+
stateInitial: signal.value
|
|
26445
|
+
};
|
|
26446
|
+
}
|
|
26447
|
+
if (Object.hasOwn(props, "value")) {
|
|
26448
|
+
return {
|
|
26449
|
+
hasStateProp: true,
|
|
26450
|
+
stateInitial: props.value
|
|
26451
|
+
};
|
|
26452
|
+
}
|
|
26453
|
+
if (Object.hasOwn(props, "defaultValue")) {
|
|
26454
|
+
return {
|
|
26455
|
+
hasStateProp: false,
|
|
26456
|
+
stateInitial: props.defaultValue
|
|
26457
|
+
};
|
|
26458
|
+
}
|
|
26459
|
+
return {
|
|
26460
|
+
hasStateProp: false,
|
|
26461
|
+
stateInitial: undefined
|
|
26462
|
+
};
|
|
26463
|
+
};
|
|
27800
26464
|
// color, radio, image, file etc do not support readonly
|
|
27801
26465
|
const INPUT_TYPE_SUPPORTING_READONLY_SET = new Set(["text", "date", "datetime-local", "email", "month", "number", "password", "search", "tel", "time", "url", "week"]);
|
|
27802
26466
|
// Who, if anyone, is listening to what this control is worth: a handler of its
|
|
@@ -27815,6 +26479,7 @@ const useIsControlListenedTo = props => {
|
|
|
27815
26479
|
};
|
|
27816
26480
|
const useReadOnlyUncontrolled = (props, controlInfo) => {
|
|
27817
26481
|
const listenedTo = useIsControlListenedTo(props);
|
|
26482
|
+
useRef(false);
|
|
27818
26483
|
if (!controlInfo.hasStateProp) {
|
|
27819
26484
|
return false;
|
|
27820
26485
|
}
|
|
@@ -27869,6 +26534,7 @@ const useControlgroupProps = (props, {
|
|
|
27869
26534
|
// can't change. A form or a group around it IS someone listening — that is
|
|
27870
26535
|
// what will send the value and hand a new one back.
|
|
27871
26536
|
const listenedTo = useIsControlListenedTo(props);
|
|
26537
|
+
useRef(false);
|
|
27872
26538
|
const implicitReadOnly = uiGroupStateController.hasValueProp && !listenedTo;
|
|
27873
26539
|
if (implicitReadOnly && !props.readOnly) {
|
|
27874
26540
|
props.readOnly = true;
|
|
@@ -27902,9 +26568,9 @@ const useControlgroupProps = (props, {
|
|
|
27902
26568
|
return [controlRootProps, {
|
|
27903
26569
|
...controlgroupProps,
|
|
27904
26570
|
"name": undefined,
|
|
27905
|
-
// useful to children, not the
|
|
26571
|
+
// useful to children, not the group itself
|
|
27906
26572
|
"required": undefined,
|
|
27907
|
-
// useful to children, not the
|
|
26573
|
+
// useful to children, not the group itself
|
|
27908
26574
|
// How many items the group accepts, read by its controller and by the
|
|
27909
26575
|
// children asking whether there is still room for them. Not an attribute
|
|
27910
26576
|
// any element wears: a <fieldset maxlength> means nothing.
|
|
@@ -27917,29 +26583,6 @@ const useControlgroupProps = (props, {
|
|
|
27917
26583
|
}, controlgroupChildrenWrapperProps];
|
|
27918
26584
|
};
|
|
27919
26585
|
|
|
27920
|
-
/**
|
|
27921
|
-
* Like `useControlProps` but also establishes a 1:1 facade sync between the
|
|
27922
|
-
* picker's hidden input and the first child control inside the picker popup.
|
|
27923
|
-
*
|
|
27924
|
-
* Child → picker input: when the child's UI state changes, the picker input
|
|
27925
|
-
* is updated automatically (no `command="--navi-update"` needed on the child).
|
|
27926
|
-
*
|
|
27927
|
-
* Picker input → child: when the picker input is updated externally (e.g.
|
|
27928
|
-
* via `--navi-update` or `--navi-clear` from outside), the change is
|
|
27929
|
-
* propagated down to the child automatically.
|
|
27930
|
-
*
|
|
27931
|
-
* Returns a 3-tuple `[controlRootProps, controlHostProps, facadeChildrenProps]`.
|
|
27932
|
-
* Use `ControlFacadeChildrenWrapper` with the third element to wrap the popup
|
|
27933
|
-
* children — it resets field contexts and injects the facade controller:
|
|
27934
|
-
*
|
|
27935
|
-
* ```jsx
|
|
27936
|
-
* const [controlRootProps, controlHostProps, facadeChildrenProps] = useControlFacadeProps(props, options);
|
|
27937
|
-
* // …
|
|
27938
|
-
* <ControlFacadeChildrenWrapper {...facadeChildrenProps}>
|
|
27939
|
-
* {children}
|
|
27940
|
-
* </ControlFacadeChildrenWrapper>
|
|
27941
|
-
* ```
|
|
27942
|
-
*/
|
|
27943
26586
|
/**
|
|
27944
26587
|
* What a control holds, read from the control itself.
|
|
27945
26588
|
*
|
|
@@ -27986,6 +26629,30 @@ const useControlUIState = (ref, uiStateInitial) => {
|
|
|
27986
26629
|
}, [ref]);
|
|
27987
26630
|
return uiState;
|
|
27988
26631
|
};
|
|
26632
|
+
|
|
26633
|
+
/**
|
|
26634
|
+
* Like `useControlProps` but also establishes a 1:1 facade sync between the
|
|
26635
|
+
* picker's hidden input and the first child control inside the picker popup.
|
|
26636
|
+
*
|
|
26637
|
+
* Child → picker input: when the child's UI state changes, the picker input
|
|
26638
|
+
* is updated automatically (no `command="--navi-update"` needed on the child).
|
|
26639
|
+
*
|
|
26640
|
+
* Picker input → child: when the picker input is updated externally (e.g.
|
|
26641
|
+
* via `--navi-update` or `--navi-clear` from outside), the change is
|
|
26642
|
+
* propagated down to the child automatically.
|
|
26643
|
+
*
|
|
26644
|
+
* Returns a 3-tuple `[controlRootProps, controlHostProps, facadeChildrenProps]`.
|
|
26645
|
+
* Use `ControlFacadeChildrenWrapper` with the third element to wrap the popup
|
|
26646
|
+
* children — it resets field contexts and injects the facade controller:
|
|
26647
|
+
*
|
|
26648
|
+
* ```jsx
|
|
26649
|
+
* const [controlRootProps, controlHostProps, facadeChildrenProps] = useControlFacadeProps(props, options);
|
|
26650
|
+
* // …
|
|
26651
|
+
* <ControlFacadeChildrenWrapper {...facadeChildrenProps}>
|
|
26652
|
+
* {children}
|
|
26653
|
+
* </ControlFacadeChildrenWrapper>
|
|
26654
|
+
* ```
|
|
26655
|
+
*/
|
|
27989
26656
|
const useControlFacadeProps = (props, options) => {
|
|
27990
26657
|
const [controlRootProps, controlHostProps, {
|
|
27991
26658
|
uiStateController
|
|
@@ -28012,21 +26679,9 @@ const useControlFacadeProps = (props, options) => {
|
|
|
28012
26679
|
const ControlFacadeChildrenWrapper = ({
|
|
28013
26680
|
children,
|
|
28014
26681
|
facadeController
|
|
28015
|
-
}) => jsx(
|
|
28016
|
-
|
|
28017
|
-
children:
|
|
28018
|
-
value: undefined,
|
|
28019
|
-
children: jsx(ControlIdContext.Provider, {
|
|
28020
|
-
value: undefined,
|
|
28021
|
-
children: jsx(RequiredContext.Provider, {
|
|
28022
|
-
value: undefined,
|
|
28023
|
-
children: jsx(ControlNameContext.Provider, {
|
|
28024
|
-
value: undefined,
|
|
28025
|
-
children: children
|
|
28026
|
-
})
|
|
28027
|
-
})
|
|
28028
|
-
})
|
|
28029
|
-
})
|
|
26682
|
+
}) => jsx(ControlChildrenWrapper, {
|
|
26683
|
+
uiStateController: facadeController,
|
|
26684
|
+
children: children
|
|
28030
26685
|
});
|
|
28031
26686
|
const useInteractiveProps = (props, {
|
|
28032
26687
|
uiStateController,
|
|
@@ -28173,7 +26828,7 @@ const useInteractiveProps = (props, {
|
|
|
28173
26828
|
}, []);
|
|
28174
26829
|
}
|
|
28175
26830
|
{
|
|
28176
|
-
const isCheckable = uiStateController.controlType
|
|
26831
|
+
const isCheckable = isCheckableInput(uiStateController.controlType, props.type);
|
|
28177
26832
|
Object.assign(controlHostProps, {
|
|
28178
26833
|
onnavi_clear_ui_state: e => {
|
|
28179
26834
|
uiStateController.clearUIState(e);
|
|
@@ -28406,8 +27061,7 @@ const splitControlProps = props => {
|
|
|
28406
27061
|
ref
|
|
28407
27062
|
};
|
|
28408
27063
|
const controlRootProps = {};
|
|
28409
|
-
const
|
|
28410
|
-
for (const key of propKeySet) {
|
|
27064
|
+
for (const key of Object.keys(props)) {
|
|
28411
27065
|
if (CONTROL_PROP_SET.has(key)) {
|
|
28412
27066
|
if (CONTROL_ATTRIBUTE_SET.has(key)) {
|
|
28413
27067
|
controlHostProps[key] = props[key];
|
|
@@ -28418,6 +27072,7 @@ const splitControlProps = props => {
|
|
|
28418
27072
|
}
|
|
28419
27073
|
return [controlRootProps, controlHostProps];
|
|
28420
27074
|
};
|
|
27075
|
+
const isCheckableInput = (controlType, typeProp) => controlType === "input" && (typeProp === "radio" || typeProp === "checkbox");
|
|
28421
27076
|
|
|
28422
27077
|
// The labels the DOM itself can hand over: a wrapping <label>, or a label[for]
|
|
28423
27078
|
// pointing at a native form element. Everything else — a label[for] on a
|