@jsenv/navi 0.29.87 → 0.29.89
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 +1066 -2232
- package/dist/jsenv_navi.js.map +25 -11
- package/docs/control_object.md +26 -1
- 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
|
-
|
|
2874
|
+
const resolvedParams = { ...providedParams };
|
|
2881
2875
|
|
|
2882
|
-
//
|
|
2883
|
-
for (const
|
|
2884
|
-
|
|
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
|
-
}
|
|
2894
|
-
|
|
2895
|
-
// Process query connections for parameter resolution
|
|
2896
|
-
for (const [paramName, connection] of queryConnectionMap) {
|
|
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".
|
|
3030
|
+
*
|
|
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").
|
|
3044
3042
|
*
|
|
3045
|
-
*
|
|
3046
|
-
*
|
|
3047
|
-
* Parameter inheritance from parent routes is intentional - only default values
|
|
3048
|
-
* for the current route's own parameters are filtered out.
|
|
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
|
-
}
|
|
3069
|
-
}
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
// Process query parameters
|
|
3073
|
-
for (const [paramName, connection] of queryConnectionMap) {
|
|
3074
|
-
if (paramName in filtered) {
|
|
3075
|
-
// Parameter is explicitly provided - check if we should remove it
|
|
3076
|
-
const paramValue = filtered[paramName];
|
|
3077
|
-
|
|
3078
|
-
if (!connection.isCustomValue(paramValue)) {
|
|
3079
|
-
delete filtered[paramName];
|
|
3080
|
-
}
|
|
3081
|
-
} else {
|
|
3082
|
-
// Parameter not provided but signal has a value
|
|
3083
|
-
const signalValue = readSignalForUrlBuild(connection);
|
|
3084
|
-
if (connection.isCustomValue(signalValue)) {
|
|
3085
|
-
// Only include custom values
|
|
3086
|
-
filtered[paramName] = signalValue;
|
|
3087
|
-
}
|
|
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;
|
|
3088
3060
|
}
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
const userValue = params[conn.paramName];
|
|
3102
|
-
const effectiveValue = userValue !== undefined ? userValue : signalValue;
|
|
3103
|
-
return (
|
|
3104
|
-
effectiveValue === literalValue && conn.isCustomValue(effectiveValue)
|
|
3105
|
-
);
|
|
3106
|
-
});
|
|
3107
|
-
if (parentCanProvide) {
|
|
3108
|
-
return true;
|
|
3109
|
-
}
|
|
3110
|
-
|
|
3111
|
-
// Check user-provided parameters
|
|
3112
|
-
const userCanProvide = Object.entries(params).some(
|
|
3113
|
-
([, value]) => value === literalValue,
|
|
3114
|
-
);
|
|
3115
|
-
if (userCanProvide) {
|
|
3116
|
-
return true;
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
// Check if any descendant path signal provides this literal value AT THE SAME position.
|
|
3120
|
-
// A signal from /map/isochrone/:tab can provide a literal at position 2 (tab position),
|
|
3121
|
-
// but NOT a literal at position 1 (panel position) — even if the signal value matches.
|
|
3122
|
-
// descendantPathSignals is a Map<segmentIndex, conn[]> precomputed during setupPatterns.
|
|
3123
|
-
const connsAtPosition =
|
|
3124
|
-
patternObject.descendantPathSignals.get(literalPosition);
|
|
3125
|
-
if (!connsAtPosition) {
|
|
3126
|
-
return false;
|
|
3127
|
-
}
|
|
3128
|
-
return connsAtPosition.some((conn) => {
|
|
3129
|
-
const signalValue = readSignalForUrlBuild(conn);
|
|
3130
|
-
return signalValue === literalValue && conn.isCustomValue(signalValue);
|
|
3131
|
-
});
|
|
3132
|
-
};
|
|
3133
|
-
const checkChildRouteCompatibility = (childPatternObj, params) => {
|
|
3134
|
-
const childParams = {};
|
|
3135
|
-
let isCompatible = true;
|
|
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,
|
|
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,
|
|
3153
3073
|
);
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
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
|
-
|
|
3196
|
-
continue;
|
|
3197
|
-
}
|
|
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
|
+
};
|
|
3198
3084
|
}
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
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
|
+
};
|
|
3206
3096
|
}
|
|
3207
|
-
|
|
3208
|
-
}
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3211
|
-
// Check both parent signals AND user-provided params for child route matching
|
|
3212
|
-
const paramsToCheck = [
|
|
3213
|
-
...connections,
|
|
3214
|
-
...Object.entries(params).map(([key, value]) => ({
|
|
3215
|
-
paramName: key,
|
|
3216
|
-
userValue: value,
|
|
3217
|
-
isUserProvided: true,
|
|
3218
|
-
})),
|
|
3219
|
-
];
|
|
3220
|
-
|
|
3221
|
-
for (const item of paramsToCheck) {
|
|
3222
|
-
const result = processParameterForChildRoute(
|
|
3223
|
-
item,
|
|
3224
|
-
childPatternObj.pattern,
|
|
3225
|
-
);
|
|
3226
|
-
|
|
3227
|
-
if (DEBUG$2) {
|
|
3228
|
-
console.debug(
|
|
3229
|
-
`[${pattern}] Processing param '${item.paramName}' (userProvided: ${item.isUserProvided}, value: ${item.isUserProvided ? item.userValue : item.signal?.value}) for child ${childPatternObj.originalPattern}: compatible=${result.isCompatible}, shouldInclude=${result.shouldInclude}`,
|
|
3230
|
-
);
|
|
3097
|
+
ancestor = ancestor.parent;
|
|
3231
3098
|
}
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
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);
|
|
3239
3117
|
}
|
|
3240
|
-
break;
|
|
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
3118
|
};
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
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,
|
|
3119
|
+
visit(patternObject);
|
|
3120
|
+
return (
|
|
3121
|
+
found || {
|
|
3122
|
+
connection: undefined,
|
|
3123
|
+
segmentIndex: undefined,
|
|
3124
|
+
owner: "extra",
|
|
3125
|
+
}
|
|
3303
3126
|
);
|
|
3127
|
+
};
|
|
3304
3128
|
|
|
3305
|
-
|
|
3306
|
-
|
|
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
|
-
};
|
|
3129
|
+
for (const [name, value] of Object.entries(explicitParams)) {
|
|
3130
|
+
const { connection, segmentIndex, owner } = findFamilyConnection(name);
|
|
3131
|
+
setEntry(name, value, connection, segmentIndex, owner, true);
|
|
3320
3132
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
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,
|
|
3332
3144
|
);
|
|
3333
|
-
if (hasConflictingLiteral) {
|
|
3334
|
-
return { isCompatible: false };
|
|
3335
|
-
}
|
|
3336
3145
|
}
|
|
3337
3146
|
}
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
shouldInclude: !item.isUserProvided && !matchesChildLiteral,
|
|
3343
|
-
paramName,
|
|
3344
|
-
paramValue,
|
|
3345
|
-
};
|
|
3346
|
-
};
|
|
3347
|
-
|
|
3348
|
-
/**
|
|
3349
|
-
* Helper: Determine if child route should be used based on active parameters
|
|
3350
|
-
*/
|
|
3351
|
-
const shouldUseChildRoute = (
|
|
3352
|
-
childPatternObj,
|
|
3353
|
-
params,
|
|
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) {
|
|
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)) {
|
|
3374
3151
|
continue;
|
|
3375
3152
|
}
|
|
3376
|
-
const
|
|
3377
|
-
if (
|
|
3153
|
+
const conn = ancestor.pathConnectionMap.get(seg.name);
|
|
3154
|
+
if (!conn) {
|
|
3378
3155
|
continue;
|
|
3379
3156
|
}
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
(segment) =>
|
|
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
|
-
}
|
|
3395
|
-
return false;
|
|
3157
|
+
const selfSeg = parsedPattern.segments[seg.index];
|
|
3158
|
+
if (selfSeg && selfSeg.type === "literal") {
|
|
3159
|
+
setEntry(seg.name, selfSeg.value, conn, seg.index, "ancestor", false);
|
|
3396
3160
|
}
|
|
3397
3161
|
}
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
// that aren't available. Only check literal segments that replace parameter positions.
|
|
3402
|
-
// Example: /map/flow/ replaces /:panel/ with "flow", so panel must equal "flow"
|
|
3403
|
-
let hasIncompatibleLiterals = false;
|
|
3404
|
-
let hasMatchingNonDefaultLiterals = false;
|
|
3405
|
-
|
|
3406
|
-
for (let i = 0; i < childPatternObj.pattern.segments.length; i++) {
|
|
3407
|
-
const childSegment = childPatternObj.pattern.segments[i];
|
|
3408
|
-
const parentSegment = parsedPattern.segments[i];
|
|
3409
|
-
|
|
3410
|
-
if (
|
|
3411
|
-
childSegment.type === "literal" &&
|
|
3412
|
-
parentSegment &&
|
|
3413
|
-
parentSegment.type === "param"
|
|
3414
|
-
) {
|
|
3415
|
-
// This literal segment replaces a parameter in the parent
|
|
3416
|
-
const paramName = parentSegment.name;
|
|
3417
|
-
const explicitValue = params[paramName];
|
|
3418
|
-
const connection =
|
|
3419
|
-
pathConnectionMap.get(paramName) || queryConnectionMap.get(paramName);
|
|
3420
|
-
const signalValue = readSignalForUrlBuild(connection);
|
|
3421
|
-
|
|
3422
|
-
// Check if the parameter has the required value
|
|
3423
|
-
if (
|
|
3424
|
-
explicitValue !== childSegment.value &&
|
|
3425
|
-
signalValue !== childSegment.value
|
|
3426
|
-
) {
|
|
3427
|
-
hasIncompatibleLiterals = true;
|
|
3428
|
-
if (DEBUG$2) {
|
|
3429
|
-
console.debug(
|
|
3430
|
-
`[${pattern}] Blocking child route ${childPatternObj.originalPattern} because parameter "${paramName}" must be "${childSegment.value}" but current values are explicit="${explicitValue}" signal="${signalValue}"`,
|
|
3431
|
-
);
|
|
3432
|
-
}
|
|
3433
|
-
break;
|
|
3162
|
+
for (const [name, conn] of ancestor.queryConnectionMap) {
|
|
3163
|
+
if (intended.has(name)) {
|
|
3164
|
+
continue;
|
|
3434
3165
|
}
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
if (explicitValue === childSegment.value && connection) {
|
|
3439
|
-
const defaultValue = connection.getDefaultValue();
|
|
3440
|
-
if (explicitValue !== defaultValue) {
|
|
3441
|
-
hasMatchingNonDefaultLiterals = true;
|
|
3442
|
-
}
|
|
3166
|
+
const value = readSignalForUrlBuild(conn);
|
|
3167
|
+
if (value !== undefined && conn.isCustomValue(value)) {
|
|
3168
|
+
setEntry(name, value, conn, undefined, "ancestor", false);
|
|
3443
3169
|
}
|
|
3444
3170
|
}
|
|
3171
|
+
ancestor = ancestor.parent;
|
|
3445
3172
|
}
|
|
3446
3173
|
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
return
|
|
3450
|
-
}
|
|
3451
|
-
|
|
3452
|
-
// Descending into a child path param means "this URL keeps the value you
|
|
3453
|
-
// are on" — right for a param that QUALIFIES a position (a tab, a mode),
|
|
3454
|
-
// wrong for one that NAMES a page. Two things must both hold for it to be
|
|
3455
|
-
// a name:
|
|
3456
|
-
//
|
|
3457
|
-
// - literal routes are declared for its other values ("/games/me/done").
|
|
3458
|
-
// Declaring them is the developer saying these values are places; a tab
|
|
3459
|
-
// nobody named a route after stays a qualifier;
|
|
3460
|
-
// - it has a default value, which makes THIS url the url of that default:
|
|
3461
|
-
// "/games/me" IS section=a-venir. Descending would leave the default
|
|
3462
|
-
// unaddressable — two states, one url. Without a default ("/map" is not
|
|
3463
|
-
// a panel, it is the absence of one) this url means nothing yet and
|
|
3464
|
-
// stays free to remember where you were.
|
|
3465
|
-
const thisUrlAlreadyMeansAParamValue = (connection) => {
|
|
3466
|
-
if (connection.paramType !== "path") {
|
|
3467
|
-
return false;
|
|
3468
|
-
}
|
|
3469
|
-
if (pathConnectionMap.has(connection.paramName)) {
|
|
3470
|
-
return false; // we carry that param ourselves, we are not its default
|
|
3471
|
-
}
|
|
3472
|
-
if (!connection.namedByLiteralRoutes) {
|
|
3473
|
-
return false;
|
|
3474
|
-
}
|
|
3475
|
-
return connection.getDefaultValue() !== undefined;
|
|
3174
|
+
const intendedValueAt = (name) => {
|
|
3175
|
+
const entry = intended.get(name);
|
|
3176
|
+
return entry ? entry.value : undefined;
|
|
3476
3177
|
};
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
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;
|
|
3178
|
+
const isDescendantReachable = (descendant) => {
|
|
3179
|
+
const selfSegments = parsedPattern.segments;
|
|
3180
|
+
for (const dSeg of descendant.pattern.segments) {
|
|
3181
|
+
if (dSeg.type !== "literal") {
|
|
3182
|
+
continue;
|
|
3498
3183
|
}
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
)
|
|
3508
|
-
|
|
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;
|
|
3509
3195
|
}
|
|
3196
|
+
continue;
|
|
3510
3197
|
}
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
(segment) => {
|
|
3518
|
-
if (segment.type === "literal") return true;
|
|
3519
|
-
if (segment.type === "param") {
|
|
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);
|
|
3520
3204
|
return (
|
|
3521
|
-
|
|
3205
|
+
entry &&
|
|
3206
|
+
entry.value !== undefined &&
|
|
3207
|
+
conn.isCustomValue(entry.value) &&
|
|
3208
|
+
String(entry.value) === dSeg.value
|
|
3522
3209
|
);
|
|
3210
|
+
});
|
|
3211
|
+
if (justifiedBySignal) {
|
|
3212
|
+
continue;
|
|
3523
3213
|
}
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
const nonDefaultParams = Object.entries(params).filter(
|
|
3530
|
-
([paramName, value]) => {
|
|
3531
|
-
if (value === undefined) return false;
|
|
3532
|
-
|
|
3533
|
-
// Check if this parameter has a default value in child's connections
|
|
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;
|
|
3214
|
+
const justifiedByExplicit = [...intended.values()].some(
|
|
3215
|
+
(entry) => entry.explicit && String(entry.value) === dSeg.value,
|
|
3216
|
+
);
|
|
3217
|
+
if (!justifiedByExplicit) {
|
|
3218
|
+
return false;
|
|
3540
3219
|
}
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3220
|
+
}
|
|
3221
|
+
return true;
|
|
3222
|
+
};
|
|
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") {
|
|
3231
|
+
continue;
|
|
3548
3232
|
}
|
|
3549
|
-
|
|
3550
|
-
|
|
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;
|
|
3233
|
+
const selfSeg = selfSegments[dSeg.index];
|
|
3234
|
+
if (selfSeg) {
|
|
3235
|
+
if (selfSeg.type === "literal") {
|
|
3236
|
+
if (selfSeg.value !== dSeg.value) {
|
|
3237
|
+
return false;
|
|
3620
3238
|
}
|
|
3239
|
+
continue;
|
|
3621
3240
|
}
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
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
|
-
}
|
|
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;
|
|
3652
3251
|
}
|
|
3252
|
+
continue;
|
|
3653
3253
|
}
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
if (
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3254
|
+
const connsAtPosition = patternObject.descendantPathSignals.get(
|
|
3255
|
+
dSeg.index,
|
|
3256
|
+
);
|
|
3257
|
+
if (!connsAtPosition || connsAtPosition.length === 0) {
|
|
3258
|
+
return false;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
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;
|
|
3676
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,
|
|
3302
|
+
);
|
|
3303
|
+
continue;
|
|
3304
|
+
}
|
|
3305
|
+
if (!queryReachable || value === undefined) {
|
|
3306
|
+
continue;
|
|
3307
|
+
}
|
|
3308
|
+
if (conn.isCustomValue(value)) {
|
|
3309
|
+
setEntry(paramName, value, conn, undefined, "descendant", false);
|
|
3677
3310
|
}
|
|
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
3311
|
}
|
|
3312
|
+
visitDescendants(child);
|
|
3683
3313
|
}
|
|
3684
|
-
}
|
|
3685
|
-
|
|
3686
|
-
if (DEBUG$2 && shouldUse) {
|
|
3687
|
-
console.debug(
|
|
3688
|
-
`[${pattern}] Will use child route ${childPatternObj.originalPattern}`,
|
|
3689
|
-
);
|
|
3690
|
-
}
|
|
3314
|
+
};
|
|
3315
|
+
visitDescendants(patternObject);
|
|
3691
3316
|
|
|
3692
|
-
return
|
|
3317
|
+
return { intended, reachableDescendants };
|
|
3693
3318
|
};
|
|
3694
3319
|
|
|
3695
3320
|
/**
|
|
3696
|
-
*
|
|
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").
|
|
3697
3327
|
*/
|
|
3698
|
-
const
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3328
|
+
const buildCandidateUrl = (
|
|
3329
|
+
candidate,
|
|
3330
|
+
intended,
|
|
3331
|
+
{ dropMissing, lenient } = {},
|
|
3702
3332
|
) => {
|
|
3703
|
-
|
|
3704
|
-
const
|
|
3705
|
-
for (const
|
|
3706
|
-
|
|
3707
|
-
|
|
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
|
-
}
|
|
3333
|
+
const urlParams = {};
|
|
3334
|
+
const candidateSegments = candidate.pattern.segments;
|
|
3335
|
+
for (const seg of candidateSegments) {
|
|
3336
|
+
if (seg.type !== "param") {
|
|
3337
|
+
continue;
|
|
3775
3338
|
}
|
|
3776
|
-
|
|
3777
|
-
|
|
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) {
|
|
3339
|
+
const entry = intended.get(seg.name);
|
|
3340
|
+
if (!entry || entry.value === undefined) {
|
|
3790
3341
|
continue;
|
|
3791
3342
|
}
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
) {
|
|
3798
|
-
continue; // Child route handles this parameter directly
|
|
3343
|
+
const meaningful = entry.connection
|
|
3344
|
+
? entry.connection.isCustomValue(entry.value)
|
|
3345
|
+
: true;
|
|
3346
|
+
if (meaningful) {
|
|
3347
|
+
urlParams[seg.name] = entry.value;
|
|
3799
3348
|
}
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
segment.type === "literal" && segment.value === parentValue,
|
|
3805
|
-
);
|
|
3806
|
-
if (isConsumedByChildPath) {
|
|
3807
|
-
continue; // Parameter is consumed by child's literal path
|
|
3349
|
+
}
|
|
3350
|
+
for (const [name, entry] of intended) {
|
|
3351
|
+
if (name in urlParams) {
|
|
3352
|
+
continue;
|
|
3808
3353
|
}
|
|
3809
|
-
|
|
3810
|
-
|
|
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
|
|
3354
|
+
if (entry.value === undefined) {
|
|
3355
|
+
continue;
|
|
3818
3356
|
}
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
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;
|
|
3357
|
+
const conn = entry.connection;
|
|
3358
|
+
if (!conn) {
|
|
3359
|
+
if (entry.explicit) {
|
|
3360
|
+
urlParams[name] = entry.value;
|
|
3848
3361
|
}
|
|
3362
|
+
continue;
|
|
3849
3363
|
}
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
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
|
-
|
|
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];
|
|
3900
3372
|
if (
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3373
|
+
literalSeg &&
|
|
3374
|
+
literalSeg.type === "literal" &&
|
|
3375
|
+
literalSeg.value === String(entry.value)
|
|
3904
3376
|
) {
|
|
3905
|
-
//
|
|
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
|
-
);
|
|
3377
|
+
continue; // the candidate's path itself encodes this value
|
|
3934
3378
|
}
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
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
|
-
}
|
|
3379
|
+
if (entry.explicit && entry.owner === "ancestor") {
|
|
3380
|
+
urlParams[name] = entry.value;
|
|
3381
|
+
continue;
|
|
3955
3382
|
}
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
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
|
-
}
|
|
3383
|
+
if (entry.pageNaming) {
|
|
3384
|
+
// deliberately left out: this url does not follow the signal
|
|
3385
|
+
continue;
|
|
3971
3386
|
}
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
// Check if this parameter is not a path parameter that we're optimizing away
|
|
3976
|
-
if (!(paramName in parentPathDefaults)) {
|
|
3977
|
-
nonDefaultQueryParams[paramName] = paramValue;
|
|
3387
|
+
if (lenient) {
|
|
3388
|
+
if (entry.explicit) {
|
|
3389
|
+
urlParams[name] = entry.value;
|
|
3978
3390
|
}
|
|
3391
|
+
continue;
|
|
3979
3392
|
}
|
|
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`);
|
|
3393
|
+
return null;
|
|
4069
3394
|
}
|
|
4070
|
-
|
|
3395
|
+
urlParams[name] = entry.value;
|
|
4071
3396
|
}
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
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,
|
|
3397
|
+
let buildPattern = candidate.pattern;
|
|
3398
|
+
if (dropMissing) {
|
|
3399
|
+
const keptSegments = candidateSegments.filter(
|
|
3400
|
+
(seg) => seg.type !== "param" || seg.name in urlParams,
|
|
4085
3401
|
);
|
|
4086
|
-
if (
|
|
4087
|
-
|
|
4088
|
-
if (
|
|
4089
|
-
|
|
3402
|
+
if (keptSegments.length !== candidateSegments.length) {
|
|
3403
|
+
buildPattern = { ...buildPattern, segments: keptSegments };
|
|
3404
|
+
if (buildPattern.trailingSlash) {
|
|
3405
|
+
buildPattern.trailingSlash = false;
|
|
4090
3406
|
}
|
|
4091
3407
|
}
|
|
4092
3408
|
}
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
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) {
|
|
3409
|
+
const url = buildUrlFromPattern(
|
|
3410
|
+
buildPattern,
|
|
3411
|
+
urlParams,
|
|
3412
|
+
candidate.originalPattern,
|
|
3413
|
+
candidate,
|
|
3414
|
+
);
|
|
3415
|
+
if (url.includes("/:")) {
|
|
4120
3416
|
return null;
|
|
4121
3417
|
}
|
|
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;
|
|
3418
|
+
return url;
|
|
4141
3419
|
};
|
|
4142
3420
|
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
// Check if we can use this child directly
|
|
4148
|
-
const directUrl = tryUseDescendant(startChild, params, resolvedParams);
|
|
4149
|
-
if (!directUrl) {
|
|
4150
|
-
return null;
|
|
3421
|
+
const urlValueEquals = (intendedValue, reproducedValue) => {
|
|
3422
|
+
let a = intendedValue;
|
|
3423
|
+
if (a && typeof a === "object" && a[rawUrlPartSymbol]) {
|
|
3424
|
+
a = a.value;
|
|
4151
3425
|
}
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
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
|
-
}
|
|
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}`;
|
|
4175
3431
|
}
|
|
4176
|
-
|
|
4177
|
-
|
|
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;
|
|
3432
|
+
if (compareTwoJsValues(a, reproducedValue)) {
|
|
3433
|
+
return true;
|
|
4190
3434
|
}
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
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
|
-
}
|
|
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;
|
|
4226
3439
|
}
|
|
4227
|
-
|
|
4228
|
-
return hasMatchingPathOptimization;
|
|
3440
|
+
return String(a) === String(reproducedValue);
|
|
4229
3441
|
};
|
|
4230
3442
|
|
|
4231
3443
|
/**
|
|
4232
|
-
*
|
|
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.
|
|
4233
3448
|
*/
|
|
4234
|
-
const
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
if (DEBUG$2) {
|
|
4244
|
-
console.debug(
|
|
4245
|
-
`[${pattern}] tryUseAncestor: Trying immediate parent ${ancestorPatternObj.originalPattern}`,
|
|
4246
|
-
);
|
|
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;
|
|
4247
3458
|
}
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
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
|
-
}
|
|
3459
|
+
seen.add(patternObj);
|
|
3460
|
+
familyPatterns.push(patternObj);
|
|
3461
|
+
for (const child of patternObj.children) {
|
|
3462
|
+
visit(child);
|
|
4296
3463
|
}
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
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) {
|
|
3483
|
+
continue;
|
|
4304
3484
|
}
|
|
4305
|
-
const
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
if (hasNonDefaultOwnParameters) {
|
|
4310
|
-
if (DEBUG$2) {
|
|
4311
|
-
console.debug(
|
|
4312
|
-
`[${pattern}] tryUseAncestor: Has non-default own parameters, skipping immediate parent optimization`,
|
|
3485
|
+
const holdsParam =
|
|
3486
|
+
familyPattern.queryConnectionMap.has(name) ||
|
|
3487
|
+
familyPattern.pattern.segments.some(
|
|
3488
|
+
(seg) => seg.type === "param" && seg.name === name,
|
|
4313
3489
|
);
|
|
3490
|
+
if (!holdsParam) {
|
|
3491
|
+
continue;
|
|
3492
|
+
}
|
|
3493
|
+
if (name in matchResult && matchResult[name] !== undefined) {
|
|
3494
|
+
reproducedValue = matchResult[name];
|
|
3495
|
+
extracted = true;
|
|
3496
|
+
break;
|
|
4314
3497
|
}
|
|
4315
|
-
return null;
|
|
4316
3498
|
}
|
|
4317
|
-
|
|
4318
|
-
|
|
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
|
-
);
|
|
3499
|
+
if (!extracted) {
|
|
3500
|
+
reproducedValue = conn.getDefaultValue();
|
|
4330
3501
|
}
|
|
4331
|
-
|
|
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) {
|
|
3502
|
+
const wantedValue =
|
|
3503
|
+
entry.value === undefined ? conn.getDefaultValue() : entry.value;
|
|
3504
|
+
if (!urlValueEquals(wantedValue, reproducedValue)) {
|
|
4338
3505
|
return false;
|
|
4339
3506
|
}
|
|
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
3507
|
}
|
|
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;
|
|
3508
|
+
return true;
|
|
4388
3509
|
};
|
|
4389
3510
|
|
|
4390
3511
|
/**
|
|
4391
|
-
*
|
|
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
|
|
4392
3522
|
*/
|
|
4393
|
-
const
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
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,
|
|
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)
|
|
4419
3528
|
);
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
3529
|
+
};
|
|
3530
|
+
if (ancestorPattern !== patternObject.parent) {
|
|
3531
|
+
return (
|
|
3532
|
+
connections.length === 0 &&
|
|
3533
|
+
parsedPattern.segments.every((seg) => seg.type === "literal")
|
|
4423
3534
|
);
|
|
4424
3535
|
}
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
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
|
-
);
|
|
3536
|
+
let literalsPinDefaults = false;
|
|
3537
|
+
for (const seg of ancestorPattern.pattern.segments) {
|
|
3538
|
+
if (seg.type !== "param") {
|
|
3539
|
+
continue;
|
|
4463
3540
|
}
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
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
|
-
);
|
|
3541
|
+
const conn = ancestorPattern.pathConnectionMap.get(seg.name);
|
|
3542
|
+
const selfSeg = parsedPattern.segments[seg.index];
|
|
3543
|
+
if (!conn || !selfSeg || selfSeg.type !== "literal") {
|
|
3544
|
+
continue;
|
|
4474
3545
|
}
|
|
4475
|
-
|
|
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;
|
|
3546
|
+
if (selfSeg.value !== String(conn.getDefaultValue())) {
|
|
3547
|
+
literalsPinDefaults = false;
|
|
3548
|
+
break;
|
|
4491
3549
|
}
|
|
3550
|
+
literalsPinDefaults = true;
|
|
4492
3551
|
}
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
`[${pattern}] tryDirectOptimization: resolvedParams:`,
|
|
4500
|
-
resolvedParams,
|
|
3552
|
+
if (literalsPinDefaults) {
|
|
3553
|
+
return !connections.some(
|
|
3554
|
+
(conn) =>
|
|
3555
|
+
!conn.inherited &&
|
|
3556
|
+
conn.paramType === "path" &&
|
|
3557
|
+
entryIsMeaningful(conn),
|
|
4501
3558
|
);
|
|
4502
3559
|
}
|
|
4503
|
-
|
|
4504
|
-
|
|
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),
|
|
3560
|
+
return !connections.some(
|
|
3561
|
+
(conn) => !conn.inherited && entryIsMeaningful(conn),
|
|
4520
3562
|
);
|
|
3563
|
+
};
|
|
4521
3564
|
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
if (
|
|
4530
|
-
|
|
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
|
-
}
|
|
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;
|
|
4562
3574
|
}
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
ancestorParams[paramName] = value;
|
|
4569
|
-
if (DEBUG$2) {
|
|
4570
|
-
console.debug(
|
|
4571
|
-
`[${pattern}] tryDirectOptimization: Added extra param ${paramName}=${value}`,
|
|
4572
|
-
);
|
|
4573
|
-
}
|
|
3575
|
+
const url = buildCandidateUrl(ancestor, intended, {
|
|
3576
|
+
dropMissing: true,
|
|
3577
|
+
});
|
|
3578
|
+
if (!url || !verifyUrl(url, intended)) {
|
|
3579
|
+
break;
|
|
4574
3580
|
}
|
|
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`,
|
|
3581
|
+
debug$3(
|
|
3582
|
+
`[${pattern}] ancestor url ${url} (via ${ancestor.originalPattern})`,
|
|
4581
3583
|
);
|
|
4582
|
-
|
|
4583
|
-
|
|
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);
|
|
4648
|
-
if (
|
|
4649
|
-
signalValue !== undefined &&
|
|
4650
|
-
connection.isCustomValue(signalValue)
|
|
4651
|
-
) {
|
|
4652
|
-
// Check if this parameter would be redundant with target ancestor's literal segments
|
|
4653
|
-
const isRedundant = isParameterRedundantWithLiteralSegments(
|
|
4654
|
-
targetAncestor.pattern,
|
|
4655
|
-
currentParent.pattern,
|
|
4656
|
-
paramName);
|
|
4657
|
-
|
|
4658
|
-
if (!isRedundant) {
|
|
4659
|
-
ancestorParams[paramName] = signalValue;
|
|
4660
|
-
}
|
|
4661
|
-
}
|
|
4662
|
-
}
|
|
4663
|
-
|
|
4664
|
-
// Move up the parent chain
|
|
4665
|
-
currentParent = currentParent.parent;
|
|
3584
|
+
ancestorUrl = url;
|
|
3585
|
+
ancestor = ancestor.parent;
|
|
4666
3586
|
}
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
targetAncestor.pattern,
|
|
4670
|
-
ancestorParams,
|
|
4671
|
-
targetAncestor.originalPattern,
|
|
4672
|
-
targetAncestor,
|
|
4673
|
-
);
|
|
4674
|
-
};
|
|
4675
|
-
|
|
4676
|
-
/**
|
|
4677
|
-
* Helper: Try to use a descendant route (simple compatibility check)
|
|
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;
|
|
4691
|
-
}
|
|
4692
|
-
|
|
4693
|
-
// Check if we should use this descendant
|
|
4694
|
-
const shouldUse = shouldUseChildRoute(
|
|
4695
|
-
descendantPatternObj,
|
|
4696
|
-
params,
|
|
4697
|
-
compatibility,
|
|
4698
|
-
parentResolvedParams,
|
|
4699
|
-
);
|
|
4700
|
-
if (!shouldUse) {
|
|
4701
|
-
return null;
|
|
3587
|
+
if (ancestorUrl) {
|
|
3588
|
+
return ancestorUrl;
|
|
4702
3589
|
}
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
const inheritParentParameters = (finalParams) => {
|
|
4716
|
-
let currentParent = patternObject.parent;
|
|
4717
|
-
|
|
4718
|
-
// Traverse up the parent chain to inherit parameters
|
|
4719
|
-
while (currentParent) {
|
|
4720
|
-
// Check parent's signal connections for non-default values to inherit
|
|
4721
|
-
// Only inherit query (search) parameters, not path parameters
|
|
4722
|
-
// Path parameters are structural and correspond to specific path segments
|
|
4723
|
-
for (const parentConnection of currentParent.connections) {
|
|
4724
|
-
if (parentConnection.paramType === "path") {
|
|
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)) {
|
|
4725
3602
|
continue;
|
|
4726
3603
|
}
|
|
4727
|
-
const
|
|
4728
|
-
if (
|
|
4729
|
-
|
|
4730
|
-
|
|
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
|
-
}
|
|
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;
|
|
4747
3612
|
}
|
|
4748
3613
|
}
|
|
4749
|
-
|
|
4750
|
-
currentParent = currentParent.parent;
|
|
4751
|
-
}
|
|
4752
|
-
};
|
|
4753
|
-
|
|
4754
|
-
/**
|
|
4755
|
-
* Helper: Build URL for current route with filtered pattern
|
|
4756
|
-
*/
|
|
4757
|
-
const buildCurrentRouteUrl = (finalParams) => {
|
|
4758
|
-
if (!parsedPattern.segments) {
|
|
4759
|
-
return "/";
|
|
3614
|
+
break;
|
|
4760
3615
|
}
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
const filteredPattern = {
|
|
4764
|
-
...parsedPattern,
|
|
4765
|
-
segments: parsedPattern.segments.filter((segment) => {
|
|
4766
|
-
if (segment.type === "param") {
|
|
4767
|
-
return segment.name in finalParams;
|
|
4768
|
-
}
|
|
4769
|
-
return true; // Keep literal segments
|
|
4770
|
-
}),
|
|
4771
|
-
};
|
|
4772
|
-
|
|
4773
|
-
// Remove trailing slash if we filtered out segments
|
|
4774
|
-
if (
|
|
4775
|
-
filteredPattern.segments.length < parsedPattern.segments.length &&
|
|
4776
|
-
parsedPattern.trailingSlash
|
|
4777
|
-
) {
|
|
4778
|
-
filteredPattern.trailingSlash = false;
|
|
3616
|
+
if (descendantUrl) {
|
|
3617
|
+
return descendantUrl;
|
|
4779
3618
|
}
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
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
|
-
* Manages the aggregated UI state of a group of child controls (radio list, checkbox list, etc.).
|
|
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
24270
|
const CANNOT_DERIVE = Symbol("cannot_derive");
|
|
25654
24271
|
|
|
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.
|
|
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: {
|
|
@@ -25770,6 +24381,13 @@ const GROUP_DEFAULTS = {
|
|
|
25770
24381
|
? emptyUIState
|
|
25771
24382
|
: child.uiState;
|
|
25772
24383
|
if (!name) {
|
|
24384
|
+
if (allowNameless) {
|
|
24385
|
+
// A control that says it is not a field is not one, whatever it
|
|
24386
|
+
// holds: a picker used as a door holds the shape its popup draws,
|
|
24387
|
+
// and merging that in would put the popup's keys in the object as
|
|
24388
|
+
// if the door had been a group.
|
|
24389
|
+
continue;
|
|
24390
|
+
}
|
|
25773
24391
|
// A nameless GROUP is a grouping, not a value: it exists to hold its
|
|
25774
24392
|
// children together (a WheelGroup sharing navigation, a fieldset-ish
|
|
25775
24393
|
// cluster) without claiming a key of its own, so what it holds is
|
|
@@ -25779,12 +24397,10 @@ const GROUP_DEFAULTS = {
|
|
|
25779
24397
|
Object.assign(groupValues, uiState);
|
|
25780
24398
|
continue;
|
|
25781
24399
|
}
|
|
25782
|
-
|
|
25783
|
-
|
|
25784
|
-
|
|
25785
|
-
|
|
25786
|
-
);
|
|
25787
|
-
}
|
|
24400
|
+
console.warn(
|
|
24401
|
+
"A group child is missing a name property, its state won't be included in the group state",
|
|
24402
|
+
child,
|
|
24403
|
+
);
|
|
25788
24404
|
continue;
|
|
25789
24405
|
}
|
|
25790
24406
|
groupValues[name] = uiState;
|
|
@@ -25811,6 +24427,36 @@ const GROUP_DEFAULTS = {
|
|
|
25811
24427
|
},
|
|
25812
24428
|
};
|
|
25813
24429
|
|
|
24430
|
+
/**
|
|
24431
|
+
* Manages the aggregated UI state of a group of child controls (radio list, checkbox list, etc.).
|
|
24432
|
+
*
|
|
24433
|
+
* Children register themselves automatically on mount and unregister on unmount.
|
|
24434
|
+
* Whenever a child fires a UI action, the group re-aggregates all child states
|
|
24435
|
+
* via `aggregateChildStates` and reacts accordingly.
|
|
24436
|
+
*
|
|
24437
|
+
* **Three distinct methods — each with a clear responsibility**:
|
|
24438
|
+
*
|
|
24439
|
+
* - `setUIState(newUIState, e)` — called when a child UI action **changes** the aggregated value.
|
|
24440
|
+
* Updates the group state, then calls `onUIAction(e)` for user-observable reactions
|
|
24441
|
+
* (uiAction, command), then dispatches `navi_ui_state_change` so `control_hooks.jsx`
|
|
24442
|
+
* can trigger the action pipeline (constraints → execute action).
|
|
24443
|
+
*
|
|
24444
|
+
* - `syncInternalState(newUIState)` — called silently during mount/unmount/render-batch.
|
|
24445
|
+
* Updates state and signal with no external reactions whatsoever.
|
|
24446
|
+
*
|
|
24447
|
+
* - `onUIAction(e)` — called when a child's UI action does **not** change the aggregated
|
|
24448
|
+
* value (e.g. re-clicking an already-selected radio). Fires `uiAction` + `command` only;
|
|
24449
|
+
* does not touch state, does not trigger the action pipeline.
|
|
24450
|
+
*
|
|
24451
|
+
* **Child UI action flow**:
|
|
24452
|
+
* 1. Child leaf fires `notifyParentAboutChildUIAction(e, { stateChanged })`.
|
|
24453
|
+
* 2. Group's `onChildUIAction` receives it.
|
|
24454
|
+
* - If `stateChanged=true`: re-aggregates → `setUIState` → full reactions + action pipeline.
|
|
24455
|
+
* - If `stateChanged=false`: calls `onUIAction` → uiAction + command only.
|
|
24456
|
+
*
|
|
24457
|
+
* **Filtering**: `childControlFilter` can exclude certain child types from aggregation
|
|
24458
|
+
* (e.g. ignoring buttons inside a selectable list).
|
|
24459
|
+
*/
|
|
25814
24460
|
const useUIGroupStateController = (
|
|
25815
24461
|
props,
|
|
25816
24462
|
controlType,
|
|
@@ -25879,6 +24525,14 @@ const useUIGroupStateController = (
|
|
|
25879
24525
|
: stateType === "object"
|
|
25880
24526
|
? EMPTY_OBJECT
|
|
25881
24527
|
: undefined;
|
|
24528
|
+
// A group told what it holds holds it from the start, before any child has
|
|
24529
|
+
// registered to show it: what it was given is the answer, and the children
|
|
24530
|
+
// are where that answer is shown (see stateGivenFromAbove).
|
|
24531
|
+
const stateInitial = hasValueProp
|
|
24532
|
+
? value
|
|
24533
|
+
: hasDefaultValueProp && defaultValue !== undefined
|
|
24534
|
+
? defaultValue
|
|
24535
|
+
: fallbackState;
|
|
25882
24536
|
const childUIStateControllerArrayRef = useRef([]);
|
|
25883
24537
|
const childUIStateControllerArray = childUIStateControllerArrayRef.current;
|
|
25884
24538
|
// Tracks children rejected by the filter and delegated upward (bubble-up).
|
|
@@ -25890,7 +24544,9 @@ const useUIGroupStateController = (
|
|
|
25890
24544
|
pendingChangeRef.current = null;
|
|
25891
24545
|
|
|
25892
24546
|
const isMonitoringChild = (childUIStateController) => {
|
|
25893
|
-
if (childUIStateController.isProxy)
|
|
24547
|
+
if (childUIStateController.isProxy) {
|
|
24548
|
+
return false;
|
|
24549
|
+
}
|
|
25894
24550
|
if (
|
|
25895
24551
|
resolvedChildControlFilter &&
|
|
25896
24552
|
!resolvedChildControlFilter(childUIStateController)
|
|
@@ -25900,12 +24556,39 @@ const useUIGroupStateController = (
|
|
|
25900
24556
|
return true;
|
|
25901
24557
|
};
|
|
25902
24558
|
const shouldPropagateStateToChild = (childUIStateController) => {
|
|
25903
|
-
if (!isMonitoringChild(childUIStateController))
|
|
25904
|
-
|
|
25905
|
-
|
|
24559
|
+
if (!isMonitoringChild(childUIStateController)) {
|
|
24560
|
+
return false;
|
|
24561
|
+
}
|
|
24562
|
+
if (childUIStateController.controlType === "button") {
|
|
24563
|
+
return false;
|
|
24564
|
+
}
|
|
24565
|
+
if (childUIStateController.controlType === "link") {
|
|
24566
|
+
return false;
|
|
24567
|
+
}
|
|
25906
24568
|
return true;
|
|
25907
24569
|
};
|
|
25908
24570
|
|
|
24571
|
+
// Live values controller methods read through the scope (`s.…`) — same
|
|
24572
|
+
// contract as the leaf controller's liveValues above: one list feeding both
|
|
24573
|
+
// init and update.
|
|
24574
|
+
const liveValues = () => ({
|
|
24575
|
+
ref,
|
|
24576
|
+
parentUIStateController,
|
|
24577
|
+
uiAction,
|
|
24578
|
+
uiActionInternal,
|
|
24579
|
+
id,
|
|
24580
|
+
name,
|
|
24581
|
+
value,
|
|
24582
|
+
defaultValue,
|
|
24583
|
+
hasValueProp,
|
|
24584
|
+
hasDefaultValueProp,
|
|
24585
|
+
// `props` is what writeBoundSignal reads to find the bound `signal`.
|
|
24586
|
+
// Missing here, a group whose component never re-renders between mount
|
|
24587
|
+
// and the first choice wrote nothing back into its signal — and said
|
|
24588
|
+
// nothing about it: the list showed the choice, the signal stayed empty.
|
|
24589
|
+
props,
|
|
24590
|
+
});
|
|
24591
|
+
|
|
25909
24592
|
const scope = useRenderScope(
|
|
25910
24593
|
// ── init: runs once on mount ───────────────────────────────────────────
|
|
25911
24594
|
(s) => {
|
|
@@ -25913,7 +24596,36 @@ const useUIGroupStateController = (
|
|
|
25913
24596
|
`Creating "${controlType}" ui state controller (monitoring some descendants ui state(s))"`,
|
|
25914
24597
|
);
|
|
25915
24598
|
const [publishUIState, subscribeUIState] = createPubSub();
|
|
25916
|
-
const uiStateSignal = signal(
|
|
24599
|
+
const uiStateSignal = signal(stateInitial);
|
|
24600
|
+
|
|
24601
|
+
// What the group is worth right now, and what it keeps when there is
|
|
24602
|
+
// nobody to ask: a list whose items have not arrived yet, a popup built
|
|
24603
|
+
// at open, a group whose children are still mounting. Such a group has
|
|
24604
|
+
// no opinion — its aggregate falls back to the empty of its type, and
|
|
24605
|
+
// taking that for an answer is how a value handed to it evaporates on
|
|
24606
|
+
// the way in, and how that emptiness then travels back up to whoever
|
|
24607
|
+
// handed it (a picker showing its row as unanswered).
|
|
24608
|
+
const aggregateGroupUIState = (whenNobodyCanAnswer) => {
|
|
24609
|
+
const someChildCanAnswer = childUIStateControllerArray.some(
|
|
24610
|
+
shouldPropagateStateToChild,
|
|
24611
|
+
);
|
|
24612
|
+
if (!someChildCanAnswer) {
|
|
24613
|
+
return whenNobodyCanAnswer;
|
|
24614
|
+
}
|
|
24615
|
+
const aggChildState = resolvedAggregateChildStates(
|
|
24616
|
+
childUIStateControllerArray,
|
|
24617
|
+
fallbackState,
|
|
24618
|
+
);
|
|
24619
|
+
if (aggChildState !== undefined) {
|
|
24620
|
+
return aggChildState;
|
|
24621
|
+
}
|
|
24622
|
+
// A group with an aggregate of its own is the one who knows what its
|
|
24623
|
+
// children add up to, `undefined` included — half a time is not a time,
|
|
24624
|
+
// wheels nobody turned have settled nothing. Only the default shapes
|
|
24625
|
+
// fall back to the empty of their type, where "no child says anything"
|
|
24626
|
+
// and "the value is empty" are the same sentence.
|
|
24627
|
+
return stateShapeIsTheDefaultOne ? fallbackState : undefined;
|
|
24628
|
+
};
|
|
25917
24629
|
|
|
25918
24630
|
// onChange and applyState live inside init so they close over the stable
|
|
25919
24631
|
// signals/pubsub without needing external refs.
|
|
@@ -25933,27 +24645,35 @@ const useUIGroupStateController = (
|
|
|
25933
24645
|
};
|
|
25934
24646
|
return;
|
|
25935
24647
|
}
|
|
25936
|
-
const
|
|
25937
|
-
|
|
25938
|
-
|
|
25939
|
-
|
|
24648
|
+
const { controller } = s;
|
|
24649
|
+
// A child mounting or unmounting is not somebody answering: while the
|
|
24650
|
+
// children of a group are still arriving, their aggregate is a partial
|
|
24651
|
+
// reading, and taking it for the truth is how the value the group was
|
|
24652
|
+
// given gets destroyed one row at a time — the first row to register
|
|
24653
|
+
// aggregates alone, the group drops to that, and every row after it is
|
|
24654
|
+
// placed from what is left. A group that derived its own value has
|
|
24655
|
+
// nothing to protect and aggregates as usual.
|
|
25940
24656
|
const groupUIState =
|
|
25941
|
-
|
|
24657
|
+
notifyExternal === "silent" && controller.stateGivenFromAbove
|
|
24658
|
+
? controller.uiState
|
|
24659
|
+
: aggregateGroupUIState(controller.uiState);
|
|
25942
24660
|
debugUIGroup(
|
|
25943
24661
|
e,
|
|
25944
24662
|
`${controlType}.getUIState -> ${JSON.stringify(groupUIState)}`,
|
|
25945
24663
|
);
|
|
25946
|
-
const { controller } = s;
|
|
25947
24664
|
if (notifyExternal === true) {
|
|
24665
|
+
// Somebody answered: what the group is worth is what its children say
|
|
24666
|
+
// between them, from here on.
|
|
24667
|
+
controller.stateGivenFromAbove = false;
|
|
25948
24668
|
applyState(groupUIState, e);
|
|
25949
24669
|
} else if (notifyExternal === "silent") {
|
|
25950
|
-
controller.syncInternalState(groupUIState
|
|
24670
|
+
controller.syncInternalState(groupUIState);
|
|
25951
24671
|
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
25952
24672
|
stateChanged: true,
|
|
25953
24673
|
silent: true,
|
|
25954
24674
|
});
|
|
25955
24675
|
} else {
|
|
25956
|
-
controller.syncInternalState(groupUIState
|
|
24676
|
+
controller.syncInternalState(groupUIState);
|
|
25957
24677
|
writeBoundSignal(groupUIState);
|
|
25958
24678
|
}
|
|
25959
24679
|
};
|
|
@@ -26010,7 +24730,11 @@ const useUIGroupStateController = (
|
|
|
26010
24730
|
hasValueProp,
|
|
26011
24731
|
hasDefaultValueProp,
|
|
26012
24732
|
props,
|
|
26013
|
-
uiState:
|
|
24733
|
+
uiState: stateInitial,
|
|
24734
|
+
// Whether what the group holds was HANDED to it (a parent distributing,
|
|
24735
|
+
// a picker filling its popup, a value prop) rather than worked out from
|
|
24736
|
+
// its children. What it protects is read in onChange.
|
|
24737
|
+
stateGivenFromAbove: hasValueProp || hasDefaultValueProp,
|
|
26014
24738
|
uiStateSignal,
|
|
26015
24739
|
wantRequesterButtonState,
|
|
26016
24740
|
ref,
|
|
@@ -26058,6 +24782,7 @@ const useUIGroupStateController = (
|
|
|
26058
24782
|
);
|
|
26059
24783
|
return;
|
|
26060
24784
|
}
|
|
24785
|
+
controller.stateGivenFromAbove = true;
|
|
26061
24786
|
const propagateEventType =
|
|
26062
24787
|
e.type === "initial_state_push"
|
|
26063
24788
|
? "initial_state_push"
|
|
@@ -26073,14 +24798,10 @@ const useUIGroupStateController = (
|
|
|
26073
24798
|
propagateDownEvent,
|
|
26074
24799
|
);
|
|
26075
24800
|
}
|
|
26076
|
-
const
|
|
26077
|
-
childUIStateControllerArray,
|
|
26078
|
-
fallbackState,
|
|
26079
|
-
);
|
|
26080
|
-
const groupUIState =
|
|
26081
|
-
aggChildState === undefined ? fallbackState : aggChildState;
|
|
24801
|
+
const groupUIState = aggregateGroupUIState(newUIState);
|
|
26082
24802
|
if (e.type === "initial_state_push") {
|
|
26083
24803
|
controller.syncInternalState(groupUIState);
|
|
24804
|
+
writeBoundSignal(groupUIState);
|
|
26084
24805
|
return;
|
|
26085
24806
|
}
|
|
26086
24807
|
applyState(groupUIState, e, { internalBehavior: true });
|
|
@@ -26090,7 +24811,9 @@ const useUIGroupStateController = (
|
|
|
26090
24811
|
},
|
|
26091
24812
|
syncInternalState: (newUIState) => {
|
|
26092
24813
|
const currentUIState = controller.uiState;
|
|
26093
|
-
if (newUIState === currentUIState)
|
|
24814
|
+
if (newUIState === currentUIState) {
|
|
24815
|
+
return;
|
|
24816
|
+
}
|
|
26094
24817
|
controller.uiState = newUIState;
|
|
26095
24818
|
uiStateSignal.value = newUIState;
|
|
26096
24819
|
publishUIState(newUIState);
|
|
@@ -26108,7 +24831,9 @@ const useUIGroupStateController = (
|
|
|
26108
24831
|
s.uiActionInternal?.(currentUIState, e);
|
|
26109
24832
|
if (!skipCommand && controller.props.command) {
|
|
26110
24833
|
const el = controller.ref.current;
|
|
26111
|
-
if (el)
|
|
24834
|
+
if (el) {
|
|
24835
|
+
triggerNaviCommand(el, controller.props.command, e);
|
|
24836
|
+
}
|
|
26112
24837
|
}
|
|
26113
24838
|
},
|
|
26114
24839
|
registerChild: (childUIStateController) => {
|
|
@@ -26128,15 +24853,28 @@ const useUIGroupStateController = (
|
|
|
26128
24853
|
debugUIGroup(
|
|
26129
24854
|
`${controlType}.registerChild("${childControlType}") -> registered (total: ${childUIStateControllerArray.length})`,
|
|
26130
24855
|
);
|
|
26131
|
-
|
|
24856
|
+
const stateToPlaceChildFrom = controller.hasValueProp
|
|
24857
|
+
? controller.value
|
|
24858
|
+
: controller.hasDefaultValueProp
|
|
24859
|
+
? controller.defaultValue
|
|
24860
|
+
: // What the group HOLDS, for a child arriving after the value
|
|
24861
|
+
// did: a list item loaded later, a row scrolled back into a
|
|
24862
|
+
// virtualized list, a popup built at open. Two conditions, and
|
|
24863
|
+
// both are about not overwriting an answer with a silence — the
|
|
24864
|
+
// group must actually hold something, and the child must have
|
|
24865
|
+
// nothing of its own to show (one arriving with its own default
|
|
24866
|
+
// is answering, and the group is what its answers add up to).
|
|
24867
|
+
uiStateHoldsNothing(controller.uiState) ||
|
|
24868
|
+
!uiStateHoldsNothing(childUIStateController.uiState)
|
|
24869
|
+
? undefined
|
|
24870
|
+
: controller.uiState;
|
|
24871
|
+
if (stateToPlaceChildFrom !== undefined) {
|
|
26132
24872
|
const initialEvent = new CustomEvent("initial_state_push", {
|
|
26133
24873
|
detail: {},
|
|
26134
24874
|
});
|
|
26135
24875
|
controller.placeChildUIState(
|
|
26136
24876
|
childUIStateController,
|
|
26137
|
-
|
|
26138
|
-
? controller.value
|
|
26139
|
-
: controller.defaultValue,
|
|
24877
|
+
stateToPlaceChildFrom,
|
|
26140
24878
|
initialEvent,
|
|
26141
24879
|
);
|
|
26142
24880
|
}
|
|
@@ -26159,7 +24897,9 @@ const useUIGroupStateController = (
|
|
|
26159
24897
|
});
|
|
26160
24898
|
return;
|
|
26161
24899
|
}
|
|
26162
|
-
if (!isMonitoringChild(childUIStateController))
|
|
24900
|
+
if (!isMonitoringChild(childUIStateController)) {
|
|
24901
|
+
return;
|
|
24902
|
+
}
|
|
26163
24903
|
const childControlType = childUIStateController.controlType;
|
|
26164
24904
|
debugUIGroup(
|
|
26165
24905
|
`${controlType}.onChildUIAction("${childControlType}") stateChanged=${stateChanged} -> child state: ${JSON.stringify(
|
|
@@ -26181,7 +24921,9 @@ const useUIGroupStateController = (
|
|
|
26181
24921
|
delegatedTo.unregisterChild(childUIStateController);
|
|
26182
24922
|
return;
|
|
26183
24923
|
}
|
|
26184
|
-
if (!isMonitoringChild(childUIStateController))
|
|
24924
|
+
if (!isMonitoringChild(childUIStateController)) {
|
|
24925
|
+
return;
|
|
24926
|
+
}
|
|
26185
24927
|
const childControlType = childUIStateController.controlType;
|
|
26186
24928
|
const index = childUIStateControllerArray.indexOf(
|
|
26187
24929
|
childUIStateController,
|
|
@@ -26194,7 +24936,7 @@ const useUIGroupStateController = (
|
|
|
26194
24936
|
}
|
|
26195
24937
|
childUIStateControllerArray.splice(index, 1);
|
|
26196
24938
|
debugUIGroup(
|
|
26197
|
-
`${controlType}.unregisterChild("${childControlType}") ->
|
|
24939
|
+
`${controlType}.unregisterChild("${childControlType}") -> unregistered (remaining: ${childUIStateControllerArray.length})`,
|
|
26198
24940
|
);
|
|
26199
24941
|
onChange(new CustomEvent(`${childControlType}_unmount`), {
|
|
26200
24942
|
notifyExternal: "silent",
|
|
@@ -26206,7 +24948,9 @@ const useUIGroupStateController = (
|
|
|
26206
24948
|
});
|
|
26207
24949
|
chainEvent(ev, e);
|
|
26208
24950
|
for (const c of childUIStateControllerArray) {
|
|
26209
|
-
if (shouldPropagateStateToChild(c))
|
|
24951
|
+
if (shouldPropagateStateToChild(c)) {
|
|
24952
|
+
c.resetUIState(ev);
|
|
24953
|
+
}
|
|
26210
24954
|
}
|
|
26211
24955
|
onChange(e, { notifyExternal: true });
|
|
26212
24956
|
},
|
|
@@ -26254,13 +24998,17 @@ const useUIGroupStateController = (
|
|
|
26254
24998
|
},
|
|
26255
24999
|
findChildById: (searchId) => {
|
|
26256
25000
|
for (const c of childUIStateControllerArray) {
|
|
26257
|
-
if (c.id === searchId)
|
|
25001
|
+
if (c.id === searchId) {
|
|
25002
|
+
return c;
|
|
25003
|
+
}
|
|
26258
25004
|
}
|
|
26259
25005
|
return null;
|
|
26260
25006
|
},
|
|
26261
25007
|
getChildControllers: () => childUIStateControllerArray,
|
|
26262
25008
|
getManagedControls: () => {
|
|
26263
|
-
if (!cascadeValidationToChildren)
|
|
25009
|
+
if (!cascadeValidationToChildren) {
|
|
25010
|
+
return [];
|
|
25011
|
+
}
|
|
26264
25012
|
return childUIStateControllerArray.slice();
|
|
26265
25013
|
},
|
|
26266
25014
|
// Group children sit next to the group itself: a busy one really does
|
|
@@ -26278,20 +25026,10 @@ const useUIGroupStateController = (
|
|
|
26278
25026
|
});
|
|
26279
25027
|
controller.rules = rules;
|
|
26280
25028
|
|
|
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
25029
|
return {
|
|
26284
25030
|
controller,
|
|
26285
25031
|
_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,
|
|
25032
|
+
...liveValues(),
|
|
26295
25033
|
};
|
|
26296
25034
|
},
|
|
26297
25035
|
// ── update: runs every render after the first ─────────────────────────
|
|
@@ -26308,10 +25046,7 @@ const useUIGroupStateController = (
|
|
|
26308
25046
|
controller.defaultValue = defaultValue;
|
|
26309
25047
|
controller.hasValueProp = hasValueProp;
|
|
26310
25048
|
controller.hasDefaultValueProp = hasDefaultValueProp;
|
|
26311
|
-
|
|
26312
|
-
hasValueProp &&
|
|
26313
|
-
(!prevHasValueProp || !compareTwoJsValues(value, prevValue))
|
|
26314
|
-
) {
|
|
25049
|
+
const placeChildrenFrom = (groupUIState) => {
|
|
26315
25050
|
const propagateDownEvent = new CustomEvent(
|
|
26316
25051
|
"propagate_down_set_ui_state",
|
|
26317
25052
|
{ detail: {} },
|
|
@@ -26319,11 +25054,17 @@ const useUIGroupStateController = (
|
|
|
26319
25054
|
for (const childUIStateController of childUIStateControllerArray) {
|
|
26320
25055
|
controller.placeChildUIState(
|
|
26321
25056
|
childUIStateController,
|
|
26322
|
-
|
|
25057
|
+
groupUIState,
|
|
26323
25058
|
propagateDownEvent,
|
|
26324
25059
|
);
|
|
26325
25060
|
}
|
|
26326
|
-
controller.syncInternalState(
|
|
25061
|
+
controller.syncInternalState(groupUIState);
|
|
25062
|
+
};
|
|
25063
|
+
if (
|
|
25064
|
+
hasValueProp &&
|
|
25065
|
+
(!prevHasValueProp || !compareTwoJsValues(value, prevValue))
|
|
25066
|
+
) {
|
|
25067
|
+
placeChildrenFrom(value);
|
|
26327
25068
|
}
|
|
26328
25069
|
if (
|
|
26329
25070
|
boundSignal &&
|
|
@@ -26334,33 +25075,10 @@ const useUIGroupStateController = (
|
|
|
26334
25075
|
// again, exactly as they were when they registered. Without this a
|
|
26335
25076
|
// group would answer a write to its own signal by silently writing its
|
|
26336
25077
|
// 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);
|
|
25078
|
+
placeChildrenFrom(defaultValue);
|
|
26349
25079
|
}
|
|
26350
25080
|
|
|
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
|
-
};
|
|
25081
|
+
return liveValues();
|
|
26364
25082
|
},
|
|
26365
25083
|
);
|
|
26366
25084
|
|
|
@@ -26371,6 +25089,9 @@ const useUIGroupStateController = (
|
|
|
26371
25089
|
el.__uiStateController__ = controller;
|
|
26372
25090
|
}
|
|
26373
25091
|
return () => {
|
|
25092
|
+
if (el && el.__uiStateController__ === controller) {
|
|
25093
|
+
delete el.__uiStateController__;
|
|
25094
|
+
}
|
|
26374
25095
|
onUIStateControllerDestroyed(controller);
|
|
26375
25096
|
};
|
|
26376
25097
|
}, []);
|
|
@@ -26442,6 +25163,7 @@ const EMPTY_OBJECT = {};
|
|
|
26442
25163
|
*/
|
|
26443
25164
|
const useUIFacadeStateController = (props, realUIStateController) => {
|
|
26444
25165
|
const firstChildControllerRef = useRef(null);
|
|
25166
|
+
const namelessChildSetRef = useRef(new Set());
|
|
26445
25167
|
const updatingRef = useRef(false);
|
|
26446
25168
|
const debugPopup = useDebugPopup();
|
|
26447
25169
|
const debugInteraction = useDebugInteraction();
|
|
@@ -26457,14 +25179,23 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
26457
25179
|
// ── init: runs once on mount ───────────────────────────────────────────
|
|
26458
25180
|
(s) => {
|
|
26459
25181
|
const canRegisterAsFacadeChild = (childController) => {
|
|
26460
|
-
if (childController.controlType === "button")
|
|
26461
|
-
|
|
26462
|
-
|
|
26463
|
-
if (childController.
|
|
25182
|
+
if (childController.controlType === "button") {
|
|
25183
|
+
return false;
|
|
25184
|
+
}
|
|
25185
|
+
if (childController.controlType === "link") {
|
|
25186
|
+
return false;
|
|
25187
|
+
}
|
|
25188
|
+
if (childController.controlType === "facade") {
|
|
25189
|
+
return false;
|
|
25190
|
+
}
|
|
25191
|
+
if (childController.isProxy) {
|
|
25192
|
+
return false;
|
|
25193
|
+
}
|
|
26464
25194
|
if (childController.allowNameless) {
|
|
26465
25195
|
// A control saying it is not a field is not the one the picker talks
|
|
26466
25196
|
// to: the search box above the list, the "select all" switch beside
|
|
26467
25197
|
// it. It is there to help find the answer, not to be it.
|
|
25198
|
+
namelessChildSetRef.current.add(childController);
|
|
26468
25199
|
return false;
|
|
26469
25200
|
}
|
|
26470
25201
|
if (childController.props["navi-list"]) {
|
|
@@ -26487,6 +25218,10 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
26487
25218
|
}
|
|
26488
25219
|
const child = firstChildControllerRef.current;
|
|
26489
25220
|
if (!child) {
|
|
25221
|
+
warnPopupHasNothingButNamelessControls(
|
|
25222
|
+
props,
|
|
25223
|
+
namelessChildSetRef.current,
|
|
25224
|
+
);
|
|
26490
25225
|
return;
|
|
26491
25226
|
}
|
|
26492
25227
|
updatingRef.current = true;
|
|
@@ -26635,6 +25370,11 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
26635
25370
|
|
|
26636
25371
|
const describePicker = (props) =>
|
|
26637
25372
|
`<Picker${props.name ? ` name="${props.name}"` : ""}${props.type ? ` type="${props.type}"` : ""}>`;
|
|
25373
|
+
const warnPopupHasNothingButNamelessControls = (props, namelessChildSet) => {
|
|
25374
|
+
{
|
|
25375
|
+
return;
|
|
25376
|
+
}
|
|
25377
|
+
};
|
|
26638
25378
|
|
|
26639
25379
|
/**
|
|
26640
25380
|
* Returns true when `e` should trigger parent notification (child → parent bubbling).
|
|
@@ -26911,8 +25651,9 @@ const useControlProps = (props, {
|
|
|
26911
25651
|
props.id = props.id || controlId || idDefault;
|
|
26912
25652
|
const controlName = useContext(ControlNameContext);
|
|
26913
25653
|
props.name = props.name || controlName;
|
|
25654
|
+
const isCheckable = isCheckableInput(controlType, props.type);
|
|
26914
25655
|
const toDomProps = newUIState => {
|
|
26915
|
-
if (
|
|
25656
|
+
if (isCheckable) {
|
|
26916
25657
|
const domValue = toDomValue(props.value, {
|
|
26917
25658
|
controlType,
|
|
26918
25659
|
id: props.id,
|
|
@@ -27291,8 +26032,7 @@ const useControlProps = (props, {
|
|
|
27291
26032
|
}
|
|
27292
26033
|
return keyDownDefault(e);
|
|
27293
26034
|
};
|
|
27294
|
-
|
|
27295
|
-
if (isInputCheckable) {
|
|
26035
|
+
if (isCheckable) {
|
|
27296
26036
|
const isRadio = props.type === "radio";
|
|
27297
26037
|
|
|
27298
26038
|
// I've decided that enter on radio/checkbox would not submit form like browser does but
|
|
@@ -27307,7 +26047,6 @@ const useControlProps = (props, {
|
|
|
27307
26047
|
keyDown: e => {
|
|
27308
26048
|
if (e.key === "Enter") {
|
|
27309
26049
|
const inputEl = ref.current;
|
|
27310
|
-
const isRadio = props.type === "radio";
|
|
27311
26050
|
const checked = inputEl.checked;
|
|
27312
26051
|
const always = () => {
|
|
27313
26052
|
if (inputEl.form) {
|
|
@@ -27330,18 +26069,9 @@ const useControlProps = (props, {
|
|
|
27330
26069
|
always
|
|
27331
26070
|
};
|
|
27332
26071
|
}
|
|
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
26072
|
return {
|
|
27343
|
-
name: "enter to check checkbox",
|
|
27344
|
-
allowed: () => dispatchRequestSetUIState(inputEl, uiStateController.value, {
|
|
26073
|
+
name: checked ? "enter to uncheck checkbox" : "enter to check checkbox",
|
|
26074
|
+
allowed: () => dispatchRequestSetUIState(inputEl, checked ? undefined : uiStateController.value, {
|
|
27345
26075
|
event: e
|
|
27346
26076
|
}),
|
|
27347
26077
|
always
|
|
@@ -27476,7 +26206,6 @@ const useControlProps = (props, {
|
|
|
27476
26206
|
// Same for radio siblings: when a sibling check unchecks this radio
|
|
27477
26207
|
// (radio_sibling_uncheck, internal event, no synthetic input), lastActionValueRef
|
|
27478
26208
|
// 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
26209
|
if (!isCheckable) {
|
|
27481
26210
|
const lastActionValue = lastActionValueRef.current;
|
|
27482
26211
|
const valueSameAsLastAction = lastActionValue !== NO_ACTION_YET && compareTwoJsValues(currentValue, lastActionValue);
|
|
@@ -27548,11 +26277,16 @@ const useControlProps = (props, {
|
|
|
27548
26277
|
// a custom concept being combination of "input", "change" and may other events
|
|
27549
26278
|
// this even if trigerred when value changes and can be controlled by actionDebounce and actionAfterChange
|
|
27550
26279
|
const hasNaviChangeEventReaction = Boolean(eventReactionDefinitions?.naviChange || defaultEventReactionDefinitions?.naviChange);
|
|
26280
|
+
// The input effect is installed once per element/options while the reaction
|
|
26281
|
+
// closures (boundAction, custom reactions) are per-render: read through a
|
|
26282
|
+
// ref so the effect always fires the current render's reaction.
|
|
26283
|
+
const applyEventReactionRef = useRef();
|
|
26284
|
+
applyEventReactionRef.current = applyEventReaction;
|
|
27551
26285
|
const refCallback = useCallback(field => {
|
|
27552
26286
|
if (!hasNaviChangeEventReaction || actionEvent === "custom") {
|
|
27553
26287
|
return undefined;
|
|
27554
26288
|
}
|
|
27555
|
-
return addInputEffect(field, e =>
|
|
26289
|
+
return addInputEffect(field, e => applyEventReactionRef.current("naviChange", e), {
|
|
27556
26290
|
waitForChange: actionAfterChange,
|
|
27557
26291
|
debounce: actionDebounce,
|
|
27558
26292
|
debugInteraction
|
|
@@ -27696,36 +26430,13 @@ const createControlInfo = (props, {
|
|
|
27696
26430
|
} else {
|
|
27697
26431
|
statePropName = "value";
|
|
27698
26432
|
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;
|
|
26433
|
+
({
|
|
26434
|
+
hasStateProp,
|
|
26435
|
+
stateInitial,
|
|
26436
|
+
stateFromSignal
|
|
26437
|
+
} = resolveValueState(props, controlType, signal));
|
|
26438
|
+
if (hasStateProp) {
|
|
26439
|
+
value = stateInitial;
|
|
27729
26440
|
}
|
|
27730
26441
|
readOnlySupported = INPUT_TYPE_SUPPORTING_READONLY_SET.has(typeProp);
|
|
27731
26442
|
}
|
|
@@ -27742,27 +26453,11 @@ const createControlInfo = (props, {
|
|
|
27742
26453
|
} else if (controlType === "picker" || controlType === "select") {
|
|
27743
26454
|
statePropName = "value";
|
|
27744
26455
|
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
|
-
}
|
|
26456
|
+
({
|
|
26457
|
+
hasStateProp,
|
|
26458
|
+
stateInitial,
|
|
26459
|
+
stateFromSignal
|
|
26460
|
+
} = resolveValueState(props, controlType, signal));
|
|
27766
26461
|
disabledSupported = true;
|
|
27767
26462
|
// A native <select> has no readonly attribute. What says it is read-only is
|
|
27768
26463
|
// aria-readonly plus a refused interaction — see the select reactions in
|
|
@@ -27797,6 +26492,52 @@ const createControlInfo = (props, {
|
|
|
27797
26492
|
disabledSupported
|
|
27798
26493
|
};
|
|
27799
26494
|
};
|
|
26495
|
+
// Who says what a value-holding control is worth — a bound signal, a `value`,
|
|
26496
|
+
// a `defaultValue` — resolved the same way for every control holding one value
|
|
26497
|
+
// (text input, picker, select). The checkbox/radio branch has its own
|
|
26498
|
+
// resolution: `checked` speaks in booleans and translates to the value.
|
|
26499
|
+
const resolveValueState = (props, controlType, signal) => {
|
|
26500
|
+
if (signal) {
|
|
26501
|
+
if (Object.hasOwn(props, "defaultValue")) {
|
|
26502
|
+
// A bound signal's own default is seeded into `defaultValue` (see
|
|
26503
|
+
// resolveInputProps), so such a control is uncontrolled-with-default;
|
|
26504
|
+
// the signal only receives write-backs (onUIAction).
|
|
26505
|
+
// A signal holding something wins over the default: `defaultValue` is
|
|
26506
|
+
// a suggestion of what to start from (and what a reset goes back to),
|
|
26507
|
+
// not an answer — while the signal's value IS the answer, restored
|
|
26508
|
+
// from the url or set by whoever owns it. Taking the default here
|
|
26509
|
+
// would show a suggestion in place of the value on every reload.
|
|
26510
|
+
const stateInitial = signal.value !== undefined ? signal.value : props.defaultValue;
|
|
26511
|
+
return {
|
|
26512
|
+
hasStateProp: false,
|
|
26513
|
+
stateInitial,
|
|
26514
|
+
stateFromSignal: stateInitial
|
|
26515
|
+
};
|
|
26516
|
+
}
|
|
26517
|
+
// A plain bound signal with no default (e.g. Wheel): its live value
|
|
26518
|
+
// seeds and controls the state.
|
|
26519
|
+
return {
|
|
26520
|
+
hasStateProp: true,
|
|
26521
|
+
stateInitial: signal.value
|
|
26522
|
+
};
|
|
26523
|
+
}
|
|
26524
|
+
if (Object.hasOwn(props, "value")) {
|
|
26525
|
+
return {
|
|
26526
|
+
hasStateProp: true,
|
|
26527
|
+
stateInitial: props.value
|
|
26528
|
+
};
|
|
26529
|
+
}
|
|
26530
|
+
if (Object.hasOwn(props, "defaultValue")) {
|
|
26531
|
+
return {
|
|
26532
|
+
hasStateProp: false,
|
|
26533
|
+
stateInitial: props.defaultValue
|
|
26534
|
+
};
|
|
26535
|
+
}
|
|
26536
|
+
return {
|
|
26537
|
+
hasStateProp: false,
|
|
26538
|
+
stateInitial: undefined
|
|
26539
|
+
};
|
|
26540
|
+
};
|
|
27800
26541
|
// color, radio, image, file etc do not support readonly
|
|
27801
26542
|
const INPUT_TYPE_SUPPORTING_READONLY_SET = new Set(["text", "date", "datetime-local", "email", "month", "number", "password", "search", "tel", "time", "url", "week"]);
|
|
27802
26543
|
// Who, if anyone, is listening to what this control is worth: a handler of its
|
|
@@ -27815,6 +26556,7 @@ const useIsControlListenedTo = props => {
|
|
|
27815
26556
|
};
|
|
27816
26557
|
const useReadOnlyUncontrolled = (props, controlInfo) => {
|
|
27817
26558
|
const listenedTo = useIsControlListenedTo(props);
|
|
26559
|
+
useRef(false);
|
|
27818
26560
|
if (!controlInfo.hasStateProp) {
|
|
27819
26561
|
return false;
|
|
27820
26562
|
}
|
|
@@ -27869,6 +26611,7 @@ const useControlgroupProps = (props, {
|
|
|
27869
26611
|
// can't change. A form or a group around it IS someone listening — that is
|
|
27870
26612
|
// what will send the value and hand a new one back.
|
|
27871
26613
|
const listenedTo = useIsControlListenedTo(props);
|
|
26614
|
+
useRef(false);
|
|
27872
26615
|
const implicitReadOnly = uiGroupStateController.hasValueProp && !listenedTo;
|
|
27873
26616
|
if (implicitReadOnly && !props.readOnly) {
|
|
27874
26617
|
props.readOnly = true;
|
|
@@ -27902,9 +26645,9 @@ const useControlgroupProps = (props, {
|
|
|
27902
26645
|
return [controlRootProps, {
|
|
27903
26646
|
...controlgroupProps,
|
|
27904
26647
|
"name": undefined,
|
|
27905
|
-
// useful to children, not the
|
|
26648
|
+
// useful to children, not the group itself
|
|
27906
26649
|
"required": undefined,
|
|
27907
|
-
// useful to children, not the
|
|
26650
|
+
// useful to children, not the group itself
|
|
27908
26651
|
// How many items the group accepts, read by its controller and by the
|
|
27909
26652
|
// children asking whether there is still room for them. Not an attribute
|
|
27910
26653
|
// any element wears: a <fieldset maxlength> means nothing.
|
|
@@ -27917,29 +26660,6 @@ const useControlgroupProps = (props, {
|
|
|
27917
26660
|
}, controlgroupChildrenWrapperProps];
|
|
27918
26661
|
};
|
|
27919
26662
|
|
|
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
26663
|
/**
|
|
27944
26664
|
* What a control holds, read from the control itself.
|
|
27945
26665
|
*
|
|
@@ -27986,6 +26706,30 @@ const useControlUIState = (ref, uiStateInitial) => {
|
|
|
27986
26706
|
}, [ref]);
|
|
27987
26707
|
return uiState;
|
|
27988
26708
|
};
|
|
26709
|
+
|
|
26710
|
+
/**
|
|
26711
|
+
* Like `useControlProps` but also establishes a 1:1 facade sync between the
|
|
26712
|
+
* picker's hidden input and the first child control inside the picker popup.
|
|
26713
|
+
*
|
|
26714
|
+
* Child → picker input: when the child's UI state changes, the picker input
|
|
26715
|
+
* is updated automatically (no `command="--navi-update"` needed on the child).
|
|
26716
|
+
*
|
|
26717
|
+
* Picker input → child: when the picker input is updated externally (e.g.
|
|
26718
|
+
* via `--navi-update` or `--navi-clear` from outside), the change is
|
|
26719
|
+
* propagated down to the child automatically.
|
|
26720
|
+
*
|
|
26721
|
+
* Returns a 3-tuple `[controlRootProps, controlHostProps, facadeChildrenProps]`.
|
|
26722
|
+
* Use `ControlFacadeChildrenWrapper` with the third element to wrap the popup
|
|
26723
|
+
* children — it resets field contexts and injects the facade controller:
|
|
26724
|
+
*
|
|
26725
|
+
* ```jsx
|
|
26726
|
+
* const [controlRootProps, controlHostProps, facadeChildrenProps] = useControlFacadeProps(props, options);
|
|
26727
|
+
* // …
|
|
26728
|
+
* <ControlFacadeChildrenWrapper {...facadeChildrenProps}>
|
|
26729
|
+
* {children}
|
|
26730
|
+
* </ControlFacadeChildrenWrapper>
|
|
26731
|
+
* ```
|
|
26732
|
+
*/
|
|
27989
26733
|
const useControlFacadeProps = (props, options) => {
|
|
27990
26734
|
const [controlRootProps, controlHostProps, {
|
|
27991
26735
|
uiStateController
|
|
@@ -28012,21 +26756,9 @@ const useControlFacadeProps = (props, options) => {
|
|
|
28012
26756
|
const ControlFacadeChildrenWrapper = ({
|
|
28013
26757
|
children,
|
|
28014
26758
|
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
|
-
})
|
|
26759
|
+
}) => jsx(ControlChildrenWrapper, {
|
|
26760
|
+
uiStateController: facadeController,
|
|
26761
|
+
children: children
|
|
28030
26762
|
});
|
|
28031
26763
|
const useInteractiveProps = (props, {
|
|
28032
26764
|
uiStateController,
|
|
@@ -28173,7 +26905,7 @@ const useInteractiveProps = (props, {
|
|
|
28173
26905
|
}, []);
|
|
28174
26906
|
}
|
|
28175
26907
|
{
|
|
28176
|
-
const isCheckable = uiStateController.controlType
|
|
26908
|
+
const isCheckable = isCheckableInput(uiStateController.controlType, props.type);
|
|
28177
26909
|
Object.assign(controlHostProps, {
|
|
28178
26910
|
onnavi_clear_ui_state: e => {
|
|
28179
26911
|
uiStateController.clearUIState(e);
|
|
@@ -28406,8 +27138,7 @@ const splitControlProps = props => {
|
|
|
28406
27138
|
ref
|
|
28407
27139
|
};
|
|
28408
27140
|
const controlRootProps = {};
|
|
28409
|
-
const
|
|
28410
|
-
for (const key of propKeySet) {
|
|
27141
|
+
for (const key of Object.keys(props)) {
|
|
28411
27142
|
if (CONTROL_PROP_SET.has(key)) {
|
|
28412
27143
|
if (CONTROL_ATTRIBUTE_SET.has(key)) {
|
|
28413
27144
|
controlHostProps[key] = props[key];
|
|
@@ -28418,6 +27149,7 @@ const splitControlProps = props => {
|
|
|
28418
27149
|
}
|
|
28419
27150
|
return [controlRootProps, controlHostProps];
|
|
28420
27151
|
};
|
|
27152
|
+
const isCheckableInput = (controlType, typeProp) => controlType === "input" && (typeProp === "radio" || typeProp === "checkbox");
|
|
28421
27153
|
|
|
28422
27154
|
// The labels the DOM itself can hand over: a wrapping <label>, or a label[for]
|
|
28423
27155
|
// pointing at a native form element. Everything else — a label[for] on a
|
|
@@ -69503,12 +68235,19 @@ const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
|
|
|
69503
68235
|
* past.
|
|
69504
68236
|
* @param {import("ignore:preact").ComponentChildren} [separator] What is written
|
|
69505
68237
|
* between the hours and the minutes. "h" in French, ":" elsewhere.
|
|
68238
|
+
* @param {string} [placeholder] What the wheels show while the time holds
|
|
68239
|
+
* nothing, as "HH:MM". Wheels have no blank row to land on, so their
|
|
68240
|
+
* placeholder is a position rather than a grey word — shown, but not an
|
|
68241
|
+
* answer: the value stays `undefined` until a wheel is turned or a real value
|
|
68242
|
+
* arrives. `defaultValue` is the other half of the pair — a time that IS the
|
|
68243
|
+
* answer, and where a reset goes back to.
|
|
69506
68244
|
* @param {object} [wheelProps] Anything a `Wheel` takes, said once for both of
|
|
69507
68245
|
* them — `visibleCount`, `itemWidth`, `glideSpeed`.
|
|
69508
68246
|
*/
|
|
69509
68247
|
const TimeWheel = ({
|
|
69510
68248
|
minuteStep = 1,
|
|
69511
68249
|
loop = true,
|
|
68250
|
+
placeholder,
|
|
69512
68251
|
separator = naviI18n("time.hour_separator"),
|
|
69513
68252
|
hourLabel = naviI18n("time.hour_label"),
|
|
69514
68253
|
minuteLabel = naviI18n("time.minute_label"),
|
|
@@ -69525,8 +68264,12 @@ const TimeWheel = ({
|
|
|
69525
68264
|
}
|
|
69526
68265
|
return minuteList;
|
|
69527
68266
|
}, [minuteStep]);
|
|
68267
|
+
const {
|
|
68268
|
+
aggregateChildStates
|
|
68269
|
+
} = useAnswered(placeholder, rest, aggregateTime);
|
|
68270
|
+
const placeholderParts = parseTimeParts(placeholder);
|
|
69528
68271
|
return jsxs(WheelGroup, {
|
|
69529
|
-
aggregateChildStates:
|
|
68272
|
+
aggregateChildStates: aggregateChildStates,
|
|
69530
68273
|
distributeChildUIState: distributeTime,
|
|
69531
68274
|
...rest,
|
|
69532
68275
|
children: [jsx(Wheel, {
|
|
@@ -69535,6 +68278,7 @@ const TimeWheel = ({
|
|
|
69535
68278
|
bounded: !loop,
|
|
69536
68279
|
size: size,
|
|
69537
68280
|
"aria-label": hourLabel,
|
|
68281
|
+
defaultValue: placeholderParts ? placeholderParts.hour : undefined,
|
|
69538
68282
|
...wheelProps,
|
|
69539
68283
|
children: HOURS.map(hour => jsx(Wheel.Item, {
|
|
69540
68284
|
value: hour,
|
|
@@ -69550,6 +68294,7 @@ const TimeWheel = ({
|
|
|
69550
68294
|
bounded: !loop,
|
|
69551
68295
|
size: size,
|
|
69552
68296
|
"aria-label": minuteLabel,
|
|
68297
|
+
defaultValue: placeholderParts ? placeholderParts.minute : undefined,
|
|
69553
68298
|
...wheelProps,
|
|
69554
68299
|
children: minutes.map(minute => jsx(Wheel.Item, {
|
|
69555
68300
|
value: minute,
|
|
@@ -69587,6 +68332,12 @@ const TimeWheel = ({
|
|
|
69587
68332
|
* one that goes backwards is not. It is what the bounds keep between them as
|
|
69588
68333
|
* they turn — turn the start into the end and the end moves along, keeping
|
|
69589
68334
|
* that much room.
|
|
68335
|
+
* @param {{ start?: string, end?: string }} [placeholder] What the two wheels
|
|
68336
|
+
* show while the span holds nothing — a position, since wheels have no blank
|
|
68337
|
+
* row to land on, and not an answer: the value stays `undefined` until one of
|
|
68338
|
+
* them is turned. One turn settles both, the untouched bound included, left
|
|
68339
|
+
* where the placeholder put it. For a span that is optional ("any time of
|
|
68340
|
+
* day") and still has to show hours.
|
|
69590
68341
|
* @param {object} [timeProps] Anything a `TimeWheel` takes, said once for both
|
|
69591
68342
|
* of them. `startTimeProps`/`endTimeProps` say it to one of the two, and win
|
|
69592
68343
|
* over this one.
|
|
@@ -69595,6 +68346,7 @@ const TimeRangeWheel = ({
|
|
|
69595
68346
|
minuteStep = 1,
|
|
69596
68347
|
minDuration = 0,
|
|
69597
68348
|
loop = true,
|
|
68349
|
+
placeholder,
|
|
69598
68350
|
size,
|
|
69599
68351
|
startLabel = naviI18n("time_range.from"),
|
|
69600
68352
|
endLabel = naviI18n("time_range.to"),
|
|
@@ -69606,6 +68358,12 @@ const TimeRangeWheel = ({
|
|
|
69606
68358
|
const startId = useId();
|
|
69607
68359
|
const startRef = useRef(null);
|
|
69608
68360
|
const endRef = useRef(null);
|
|
68361
|
+
// One turn settles the whole span: a start somebody chose makes the end an
|
|
68362
|
+
// answer too, left where the placeholder put it.
|
|
68363
|
+
const {
|
|
68364
|
+
answeredRef,
|
|
68365
|
+
aggregateChildStates
|
|
68366
|
+
} = useAnswered(placeholder, rest, aggregateSpan);
|
|
69609
68367
|
|
|
69610
68368
|
// What the pair does while it is being turned: the bound that just moved is
|
|
69611
68369
|
// the one the user is holding, so it stays where it was put and the OTHER one
|
|
@@ -69642,51 +68400,127 @@ const TimeRangeWheel = ({
|
|
|
69642
68400
|
event: e
|
|
69643
68401
|
});
|
|
69644
68402
|
};
|
|
69645
|
-
return
|
|
68403
|
+
return jsx(ControlGroup, {
|
|
69646
68404
|
flex: true,
|
|
69647
68405
|
alignY: "center",
|
|
69648
68406
|
spacing: "s",
|
|
69649
68407
|
size: size,
|
|
68408
|
+
aggregateChildStates: aggregateChildStates,
|
|
69650
68409
|
...rest,
|
|
69651
|
-
children:
|
|
69652
|
-
|
|
69653
|
-
children: startLabel
|
|
69654
|
-
|
|
69655
|
-
|
|
69656
|
-
|
|
69657
|
-
|
|
69658
|
-
|
|
69659
|
-
|
|
69660
|
-
|
|
69661
|
-
|
|
69662
|
-
|
|
69663
|
-
|
|
69664
|
-
|
|
69665
|
-
|
|
69666
|
-
|
|
69667
|
-
|
|
69668
|
-
|
|
69669
|
-
|
|
69670
|
-
|
|
69671
|
-
|
|
69672
|
-
|
|
69673
|
-
|
|
69674
|
-
|
|
69675
|
-
|
|
69676
|
-
|
|
69677
|
-
|
|
69678
|
-
|
|
69679
|
-
|
|
69680
|
-
|
|
69681
|
-
|
|
69682
|
-
|
|
68410
|
+
children: jsxs(AnsweredContext.Provider, {
|
|
68411
|
+
value: answeredRef,
|
|
68412
|
+
children: [startLabel === null ? null : jsx(Text, {
|
|
68413
|
+
size: size,
|
|
68414
|
+
children: startLabel
|
|
68415
|
+
}), jsx(TimeWheel, {
|
|
68416
|
+
id: startId,
|
|
68417
|
+
ref: startRef,
|
|
68418
|
+
name: "start",
|
|
68419
|
+
minuteStep: minuteStep,
|
|
68420
|
+
loop: loop,
|
|
68421
|
+
size: size,
|
|
68422
|
+
placeholder: placeholder ? placeholder.start : undefined,
|
|
68423
|
+
uiAction: (value, e) => keepBoundsApart("start", value, e),
|
|
68424
|
+
...timeProps,
|
|
68425
|
+
...startTimeProps
|
|
68426
|
+
}), endLabel === null ? null : jsx(Text, {
|
|
68427
|
+
size: size,
|
|
68428
|
+
children: endLabel
|
|
68429
|
+
}), jsx(TimeWheel, {
|
|
68430
|
+
ref: endRef,
|
|
68431
|
+
name: "end",
|
|
68432
|
+
minuteStep: minuteStep,
|
|
68433
|
+
loop: loop,
|
|
68434
|
+
size: size,
|
|
68435
|
+
placeholder: placeholder ? placeholder.end : undefined,
|
|
68436
|
+
uiAction: (value, e) => keepBoundsApart("end", value, e)
|
|
68437
|
+
// Which time it comes after, and how much room there must be between
|
|
68438
|
+
// the two: said on the LATER of the two, so the answer is given where
|
|
68439
|
+
// the time one would have to move is (see time_range_constraint.js).
|
|
68440
|
+
,
|
|
68441
|
+
"data-time-after": startId,
|
|
68442
|
+
"data-time-min-duration": minDuration,
|
|
68443
|
+
...timeProps,
|
|
68444
|
+
...endTimeProps
|
|
68445
|
+
})]
|
|
68446
|
+
})
|
|
69683
68447
|
});
|
|
69684
68448
|
};
|
|
68449
|
+
|
|
68450
|
+
/**
|
|
68451
|
+
* Wheels always show something — there is no blank row to land on — so a pair of
|
|
68452
|
+
* them cannot say "nothing set" by looking empty. Their `placeholder` is
|
|
68453
|
+
* therefore a position rather than a grey word: shown like a value, and not one.
|
|
68454
|
+
* The value stays `undefined` until a wheel is turned, which is what tells "any
|
|
68455
|
+
* time of day" from a span somebody chose. `defaultValue` remains what it is
|
|
68456
|
+
* everywhere else — a time that IS the answer.
|
|
68457
|
+
*
|
|
68458
|
+
* What counts as turned is read from the value itself rather than from a
|
|
68459
|
+
* gesture: while nothing has moved off the placeholder, nothing is set; the
|
|
68460
|
+
* moment it differs, it is an answer and stays one, even turned back onto the
|
|
68461
|
+
* placeholder. A wheel's own `uiAction` runs after its group has aggregated, so
|
|
68462
|
+
* a flag set from there would always be one turn late.
|
|
68463
|
+
*
|
|
68464
|
+
* The flag is a ref rather than state because the aggregate a group is created
|
|
68465
|
+
* with is the one it keeps: swapping the function on a later render changes
|
|
68466
|
+
* nothing (see useUIGroupStateController). One stable function reading one ref.
|
|
68467
|
+
*
|
|
68468
|
+
* A pair shares ONE flag through `AnsweredContext`: turning the start settles
|
|
68469
|
+
* the end too, left where the placeholder put it. Each of the two times gating
|
|
68470
|
+
* on its own would answer half a span — and would leave the pair nothing to
|
|
68471
|
+
* compare its own placeholder against.
|
|
68472
|
+
*/
|
|
68473
|
+
const AnsweredContext = createContext(null);
|
|
68474
|
+
const useAnswered = (placeholder, props, aggregateWhenAnswered) => {
|
|
68475
|
+
const answeredFromPair = useContext(AnsweredContext);
|
|
68476
|
+
const ownAnsweredRef = useRef(false);
|
|
68477
|
+
const answeredRef = answeredFromPair || ownAnsweredRef;
|
|
68478
|
+
if (!placeholder || isAnswerGivenByProps(props)) {
|
|
68479
|
+
answeredRef.current = true;
|
|
68480
|
+
}
|
|
68481
|
+
// Inside a pair, only the pair decides: a time that gated on its own would
|
|
68482
|
+
// hand the pair nothing to compare, and half a span cannot be read.
|
|
68483
|
+
const gates = !answeredFromPair;
|
|
68484
|
+
const placeholderRef = useRef(placeholder);
|
|
68485
|
+
placeholderRef.current = placeholder;
|
|
68486
|
+
const aggregateRef = useRef(null);
|
|
68487
|
+
if (!aggregateRef.current) {
|
|
68488
|
+
aggregateRef.current = children => {
|
|
68489
|
+
const aggregated = aggregateWhenAnswered(children);
|
|
68490
|
+
if (answeredRef.current) {
|
|
68491
|
+
return aggregated;
|
|
68492
|
+
}
|
|
68493
|
+
if (compareTwoJsValues(aggregated, placeholderRef.current)) {
|
|
68494
|
+
return undefined;
|
|
68495
|
+
}
|
|
68496
|
+
// It moved: from here on this is an answer, and stays one even when it is
|
|
68497
|
+
// turned back onto the placeholder — somebody chose that time.
|
|
68498
|
+
answeredRef.current = true;
|
|
68499
|
+
return aggregated;
|
|
68500
|
+
};
|
|
68501
|
+
}
|
|
68502
|
+
return {
|
|
68503
|
+
answeredRef,
|
|
68504
|
+
aggregateChildStates: gates ? aggregateRef.current : aggregateWhenAnswered
|
|
68505
|
+
};
|
|
68506
|
+
};
|
|
68507
|
+
const isAnswerGivenByProps = props => props.value !== undefined || props.defaultValue !== undefined || props.signal && props.signal.value !== undefined;
|
|
69685
68508
|
const HOURS = Array.from({
|
|
69686
68509
|
length: HOUR_COUNT
|
|
69687
68510
|
}, (_, hour) => hour);
|
|
69688
68511
|
const padTwo = value => String(value).padStart(2, "0");
|
|
69689
68512
|
|
|
68513
|
+
// The two times as one span, { start, end } — the shape a pair carries.
|
|
68514
|
+
const aggregateSpan = childUIStateControllers => {
|
|
68515
|
+
const span = {};
|
|
68516
|
+
for (const child of childUIStateControllers) {
|
|
68517
|
+
if (child.name === "start" || child.name === "end") {
|
|
68518
|
+
span[child.name] = child.uiState;
|
|
68519
|
+
}
|
|
68520
|
+
}
|
|
68521
|
+
return span;
|
|
68522
|
+
};
|
|
68523
|
+
|
|
69690
68524
|
// The two wheels as one value, "HH:MM".
|
|
69691
68525
|
const aggregateTime = childUIStateControllers => {
|
|
69692
68526
|
let hour = "";
|