@jsenv/navi 0.10.2 → 0.11.0
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 +13838 -23291
- package/dist/jsenv_navi.js.map +1281 -0
- package/package.json +5 -7
- package/index.js +0 -122
- package/src/action_private_properties.js +0 -11
- package/src/action_proxy_test.html +0 -353
- package/src/action_run_states.js +0 -5
- package/src/actions.js +0 -1401
- package/src/browser_integration/browser_integration.js +0 -216
- package/src/browser_integration/document_back_and_forward.js +0 -17
- package/src/browser_integration/document_loading_signal.js +0 -100
- package/src/browser_integration/document_state_signal.js +0 -9
- package/src/browser_integration/document_url_signal.js +0 -9
- package/src/browser_integration/use_is_visited.js +0 -19
- package/src/browser_integration/via_history.js +0 -232
- package/src/browser_integration/via_navigation.js +0 -168
- package/src/components/action_execution/form_context.js +0 -5
- package/src/components/action_execution/render_actionable_component.jsx +0 -29
- package/src/components/action_execution/use_action.js +0 -99
- package/src/components/action_execution/use_execute_action.js +0 -193
- package/src/components/action_execution/use_run_on_mount.js +0 -9
- package/src/components/action_renderer.jsx +0 -125
- package/src/components/callout/callout.js +0 -990
- package/src/components/callout/callout_demo.html +0 -201
- package/src/components/callout/test_dynamic_positioning.html +0 -161
- package/src/components/callout/test_html_document_iframe.html +0 -182
- package/src/components/demos/0_button_demo.html +0 -707
- package/src/components/demos/10_column_reordering_debug.html +0 -277
- package/src/components/demos/11_table_selection_debug.html +0 -432
- package/src/components/demos/1_checkbox_demo.html +0 -754
- package/src/components/demos/2_input_textual_demo.html +0 -286
- package/src/components/demos/3_radio_demo.html +0 -874
- package/src/components/demos/4_select_demo.html +0 -100
- package/src/components/demos/5_list_scrollable_demo.html +0 -153
- package/src/components/demos/6_tablist_demo.html +0 -77
- package/src/components/demos/7_table_selection_demo.html +0 -176
- package/src/components/demos/8_table_fixed_headers_demo.html +0 -584
- package/src/components/demos/9_table_column_drag_demo.html +0 -325
- package/src/components/demos/action/0_button_demo.html +0 -204
- package/src/components/demos/action/10_shortcuts_demo.html +0 -189
- package/src/components/demos/action/11_nested_shortcuts_demo.xhtml +0 -401
- package/src/components/demos/action/1_input_text_demo.html +0 -876
- package/src/components/demos/action/2_form_multiple.html +0 -303
- package/src/components/demos/action/3_details_demo.html +0 -203
- package/src/components/demos/action/4_input_checkbox_demo.html +0 -731
- package/src/components/demos/action/5_input_checkbox_state_demo.html +0 -270
- package/src/components/demos/action/6_checkbox_list_demo.html +0 -341
- package/src/components/demos/action/7_radio_list_demo.html +0 -357
- package/src/components/demos/action/8_editable_demo.html +0 -431
- package/src/components/demos/action/9_link_demo.html +0 -194
- package/src/components/demos/demo.md +0 -0
- package/src/components/demos/route/basic/basic.html +0 -14
- package/src/components/demos/route/basic/basic_route_demo.jsx +0 -224
- package/src/components/demos/route/multi/multi.html +0 -14
- package/src/components/demos/route/multi/multi_route_demo.jsx +0 -277
- package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +0 -695
- package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +0 -429
- package/src/components/demos/ui_transition/2_height_transition_test.html +0 -295
- package/src/components/details/details.jsx +0 -245
- package/src/components/details/summary_marker.jsx +0 -141
- package/src/components/edition/editable.jsx +0 -186
- package/src/components/error_boundary_context.js +0 -9
- package/src/components/field/README.md +0 -247
- package/src/components/field/button.jsx +0 -429
- package/src/components/field/checkbox_list.jsx +0 -185
- package/src/components/field/collect_form_element_values.js +0 -82
- package/src/components/field/custom_field.js +0 -106
- package/src/components/field/form.jsx +0 -209
- package/src/components/field/input.jsx +0 -16
- package/src/components/field/input_checkbox.jsx +0 -434
- package/src/components/field/input_radio.jsx +0 -432
- package/src/components/field/input_textual.jsx +0 -389
- package/src/components/field/label.jsx +0 -46
- package/src/components/field/radio_list.jsx +0 -183
- package/src/components/field/select.jsx +0 -256
- package/src/components/field/use_action_events.js +0 -132
- package/src/components/field/use_form_events.js +0 -59
- package/src/components/field/use_ui_state_controller.js +0 -506
- package/src/components/item_tracker/README.md +0 -461
- package/src/components/item_tracker/use_isolated_item_tracker.jsx +0 -209
- package/src/components/item_tracker/use_isolated_item_tracker_demo.html +0 -148
- package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +0 -460
- package/src/components/item_tracker/use_item_tracker.jsx +0 -143
- package/src/components/item_tracker/use_item_tracker_demo.html +0 -207
- package/src/components/item_tracker/use_item_tracker_demo.jsx +0 -216
- package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +0 -87
- package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +0 -61
- package/src/components/keyboard_shortcuts/keyboard_key_meta.js +0 -17
- package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +0 -371
- package/src/components/keyboard_shortcuts/os.js +0 -9
- package/src/components/layout/demos/demo_flex.html +0 -638
- package/src/components/layout/demos/demo_layout_style_buttons.html +0 -351
- package/src/components/layout/demos/demo_layout_style_input.html +0 -226
- package/src/components/layout/demos/demo_layout_style_text.html +0 -514
- package/src/components/layout/flex.jsx +0 -109
- package/src/components/layout/layout_context.jsx +0 -3
- package/src/components/layout/spacing.jsx +0 -20
- package/src/components/layout/use_layout_style.js +0 -249
- package/src/components/link/link.jsx +0 -267
- package/src/components/link/link_with_icon.jsx +0 -52
- package/src/components/loader/loader_background.jsx +0 -372
- package/src/components/loader/loading_spinner.jsx +0 -68
- package/src/components/loader/network_speed.js +0 -83
- package/src/components/loader/rectangle_loading.jsx +0 -244
- package/src/components/props_composition/demos/demo_with_props_style.html +0 -81
- package/src/components/props_composition/with_props_class_name.js +0 -37
- package/src/components/props_composition/with_props_style.js +0 -26
- package/src/components/route.jsx +0 -19
- package/src/components/selection/selection.jsx +0 -1583
- package/src/components/svg/font_sized_svg.jsx +0 -59
- package/src/components/svg/icon_and_text.jsx +0 -21
- package/src/components/svg/svg_mask_overlay.jsx +0 -105
- package/src/components/table/drag/table_drag.jsx +0 -506
- package/src/components/table/resize/table_resize.jsx +0 -650
- package/src/components/table/resize/table_size.js +0 -43
- package/src/components/table/selection/table_selection.js +0 -106
- package/src/components/table/selection/table_selection.jsx +0 -203
- package/src/components/table/sticky/sticky_group.js +0 -354
- package/src/components/table/sticky/table_sticky.js +0 -25
- package/src/components/table/sticky/table_sticky.jsx +0 -501
- package/src/components/table/table.jsx +0 -721
- package/src/components/table/table_css.js +0 -211
- package/src/components/table/table_ui.jsx +0 -49
- package/src/components/table/use_cells_and_columns.js +0 -90
- package/src/components/table/use_object_array_to_cells.js +0 -46
- package/src/components/table/z_indexes.js +0 -23
- package/src/components/tablist/tablist.jsx +0 -99
- package/src/components/text/demos/demo_text_and_icon.html +0 -421
- package/src/components/text/overflow.jsx +0 -15
- package/src/components/text/text.jsx +0 -83
- package/src/components/text/text_and_count.jsx +0 -28
- package/src/components/ui_transition.jsx +0 -128
- package/src/components/use_auto_focus.js +0 -94
- package/src/components/use_batch_during_render.js +0 -33
- package/src/components/use_debounce_true.js +0 -31
- package/src/components/use_dependencies_diff.js +0 -35
- package/src/components/use_focus_group.js +0 -20
- package/src/components/use_initial_value.js +0 -78
- package/src/components/use_is_visited.js +0 -19
- package/src/components/use_ref_array.js +0 -38
- package/src/components/use_signal_sync.js +0 -50
- package/src/components/use_stable_callback.js +0 -68
- package/src/components/use_state_array.js +0 -47
- package/src/docs/actions.md +0 -250
- package/src/docs/demos/resource/action_status.jsx +0 -42
- package/src/docs/demos/resource/demo.md +0 -1
- package/src/docs/demos/resource/resource_demo_0.html +0 -84
- package/src/docs/demos/resource/resource_demo_10_post_gc.html +0 -364
- package/src/docs/demos/resource/resource_demo_11_describe_many.html +0 -362
- package/src/docs/demos/resource/resource_demo_2.html +0 -173
- package/src/docs/demos/resource/resource_demo_3_filtered_users.html +0 -415
- package/src/docs/demos/resource/resource_demo_4_details.html +0 -284
- package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +0 -115
- package/src/docs/demos/resource/resource_demo_6_gc.html +0 -217
- package/src/docs/demos/resource/resource_demo_7_child_gc.html +0 -240
- package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +0 -319
- package/src/docs/demos/resource/resource_demo_9_describe_one.html +0 -472
- package/src/docs/demos/resource/tata.jsx +0 -3
- package/src/docs/demos/resource/toto.jsx +0 -3
- package/src/docs/demos/user_nav/user_nav.html +0 -12
- package/src/docs/demos/user_nav/user_nav.jsx +0 -330
- package/src/docs/resource_dependencies.md +0 -103
- package/src/docs/resource_with_params.md +0 -80
- package/src/navi_css_vars.js +0 -14
- package/src/notes.md +0 -34
- package/src/route/route.js +0 -596
- package/src/route/route.xtest.html +0 -228
- package/src/store/array_signal_store.js +0 -537
- package/src/store/local_storage_signal.js +0 -17
- package/src/store/resource_graph.js +0 -1304
- package/src/store/tests/resource_graph_autoreload_demo.html +0 -12
- package/src/store/tests/resource_graph_autoreload_demo.jsx +0 -964
- package/src/store/tests/resource_graph_dependencies.test_manual.js +0 -95
- package/src/store/value_in_local_storage.js +0 -187
- package/src/symbol_object_signal.js +0 -1
- package/src/use_action_data.js +0 -10
- package/src/use_action_status.js +0 -47
- package/src/utils/add_many_event_listeners.js +0 -15
- package/src/utils/array_add_remove.js +0 -61
- package/src/utils/array_signal.js +0 -15
- package/src/utils/compare_two_js_values.js +0 -172
- package/src/utils/execute_with_cleanup.js +0 -21
- package/src/utils/get_caller_info.js +0 -85
- package/src/utils/is_signal.js +0 -20
- package/src/utils/js_value_weak_map.js +0 -162
- package/src/utils/js_value_weak_map_demo.html +0 -690
- package/src/utils/merge_two_js_values.js +0 -53
- package/src/utils/stringify_for_display.js +0 -131
- package/src/utils/weak_effect.js +0 -48
- package/src/validation/constraints/confirm_constraint.js +0 -14
- package/src/validation/constraints/create_unique_value_constraint.js +0 -27
- package/src/validation/constraints/native_constraints.js +0 -338
- package/src/validation/constraints/readonly_constraint.js +0 -41
- package/src/validation/constraints/same_as_constraint.js +0 -42
- package/src/validation/constraints/single_space_constraint.js +0 -13
- package/src/validation/custom_constraint_validation.js +0 -793
- package/src/validation/custom_message.js +0 -18
- package/src/validation/demos/browser_style.png +0 -0
- package/src/validation/demos/demo_same_as_constraint.html +0 -259
- package/src/validation/demos/form_validation_demo.html +0 -142
- package/src/validation/demos/form_validation_demo_preact.html +0 -87
- package/src/validation/demos/form_validation_native_popover_demo.html +0 -168
- package/src/validation/demos/form_validation_vs_native_demo.html +0 -172
- package/src/validation/hooks/use_constraints.js +0 -23
- package/src/validation/hooks/use_custom_validation_ref.js +0 -73
- package/src/validation/hooks/use_validation_message.js +0 -19
- package/src/validation/input_change_effect.js +0 -106
package/src/route/route.js
DELETED
|
@@ -1,596 +0,0 @@
|
|
|
1
|
-
import { batch, computed, effect, signal } from "@preact/signals";
|
|
2
|
-
import { createAction } from "../actions.js";
|
|
3
|
-
import {
|
|
4
|
-
SYMBOL_IDENTITY,
|
|
5
|
-
compareTwoJsValues,
|
|
6
|
-
} from "../utils/compare_two_js_values.js";
|
|
7
|
-
|
|
8
|
-
let baseUrl = import.meta.dev
|
|
9
|
-
? new URL(window.HTML_ROOT_PATHNAME, window.location).href
|
|
10
|
-
: window.location.origin;
|
|
11
|
-
|
|
12
|
-
export const setBaseUrl = (value) => {
|
|
13
|
-
baseUrl = new URL(value, window.location).href;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const rawUrlPartSymbol = Symbol("raw_url_part");
|
|
17
|
-
export const rawUrlPart = (value) => {
|
|
18
|
-
return {
|
|
19
|
-
[rawUrlPartSymbol]: true,
|
|
20
|
-
value,
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const DEBUG = false;
|
|
25
|
-
const NO_PARAMS = { [SYMBOL_IDENTITY]: Symbol("no_params") };
|
|
26
|
-
// Controls what happens to actions when their route becomes inactive:
|
|
27
|
-
// 'abort' - Cancel the action immediately when route deactivates
|
|
28
|
-
// 'keep-loading' - Allow action to continue running after route deactivation
|
|
29
|
-
//
|
|
30
|
-
// The 'keep-loading' strategy could act like preloading, keeping data ready for potential return.
|
|
31
|
-
// However, since route reactivation triggers action reload anyway, the old data won't be used
|
|
32
|
-
// so it's better to abort the action to avoid unnecessary resource usage.
|
|
33
|
-
const ROUTE_DEACTIVATION_STRATEGY = "abort"; // 'abort', 'keep-loading'
|
|
34
|
-
|
|
35
|
-
const routeSet = new Set();
|
|
36
|
-
// Store previous route states to detect changes
|
|
37
|
-
const routePreviousStateMap = new WeakMap();
|
|
38
|
-
// Store abort controllers per action to control their lifecycle based on route state
|
|
39
|
-
const actionAbortControllerWeakMap = new WeakMap();
|
|
40
|
-
export const updateRoutes = (
|
|
41
|
-
url,
|
|
42
|
-
{
|
|
43
|
-
// state
|
|
44
|
-
replace,
|
|
45
|
-
isVisited,
|
|
46
|
-
},
|
|
47
|
-
) => {
|
|
48
|
-
const routeMatchInfoSet = new Set();
|
|
49
|
-
for (const route of routeSet) {
|
|
50
|
-
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
51
|
-
const { urlPattern } = routePrivateProperties;
|
|
52
|
-
|
|
53
|
-
// Get previous state
|
|
54
|
-
const previousState = routePreviousStateMap.get(route) || {
|
|
55
|
-
active: false,
|
|
56
|
-
params: NO_PARAMS,
|
|
57
|
-
};
|
|
58
|
-
const oldActive = previousState.active;
|
|
59
|
-
const oldParams = previousState.params;
|
|
60
|
-
// Check if the URL matches the route pattern
|
|
61
|
-
const match = urlPattern.exec(url);
|
|
62
|
-
const newActive = Boolean(match);
|
|
63
|
-
let newParams;
|
|
64
|
-
if (match) {
|
|
65
|
-
const { optionalParamKeySet } = routePrivateProperties;
|
|
66
|
-
const extractedParams = extractParams(
|
|
67
|
-
urlPattern,
|
|
68
|
-
url,
|
|
69
|
-
optionalParamKeySet,
|
|
70
|
-
);
|
|
71
|
-
if (compareTwoJsValues(oldParams, extractedParams)) {
|
|
72
|
-
// No change in parameters, keep the old params
|
|
73
|
-
newParams = oldParams;
|
|
74
|
-
} else {
|
|
75
|
-
newParams = extractedParams;
|
|
76
|
-
}
|
|
77
|
-
} else {
|
|
78
|
-
newParams = NO_PARAMS;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const routeMatchInfo = {
|
|
82
|
-
route,
|
|
83
|
-
routePrivateProperties,
|
|
84
|
-
oldActive,
|
|
85
|
-
newActive,
|
|
86
|
-
oldParams,
|
|
87
|
-
newParams,
|
|
88
|
-
};
|
|
89
|
-
routeMatchInfoSet.add(routeMatchInfo);
|
|
90
|
-
// Store current state for next comparison
|
|
91
|
-
routePreviousStateMap.set(route, {
|
|
92
|
-
active: newActive,
|
|
93
|
-
params: newParams,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Apply all signal updates in a batch
|
|
98
|
-
const activeRouteSet = new Set();
|
|
99
|
-
batch(() => {
|
|
100
|
-
for (const {
|
|
101
|
-
route,
|
|
102
|
-
routePrivateProperties,
|
|
103
|
-
newActive,
|
|
104
|
-
newParams,
|
|
105
|
-
} of routeMatchInfoSet) {
|
|
106
|
-
const { activeSignal, paramsSignal, visitedSignal } =
|
|
107
|
-
routePrivateProperties;
|
|
108
|
-
const visited = isVisited(route.url);
|
|
109
|
-
activeSignal.value = newActive;
|
|
110
|
-
paramsSignal.value = newParams;
|
|
111
|
-
visitedSignal.value = visited;
|
|
112
|
-
route.active = newActive;
|
|
113
|
-
route.params = newParams;
|
|
114
|
-
route.visited = visited;
|
|
115
|
-
if (newActive) {
|
|
116
|
-
activeRouteSet.add(route);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// must be after paramsSignal.value update to ensure the proxy target is set
|
|
122
|
-
// (so after the batch call)
|
|
123
|
-
const toLoadSet = new Set();
|
|
124
|
-
const toReloadSet = new Set();
|
|
125
|
-
const abortSignalMap = new Map();
|
|
126
|
-
const routeLoadRequestedMap = new Map();
|
|
127
|
-
|
|
128
|
-
const shouldLoadOrReload = (route, shouldLoad) => {
|
|
129
|
-
const routeAction = route.action;
|
|
130
|
-
const currentAction = routeAction.getCurrentAction();
|
|
131
|
-
if (shouldLoad) {
|
|
132
|
-
if (replace || currentAction.aborted || currentAction.error) {
|
|
133
|
-
shouldLoad = false;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (shouldLoad) {
|
|
137
|
-
toLoadSet.add(currentAction);
|
|
138
|
-
} else {
|
|
139
|
-
toReloadSet.add(currentAction);
|
|
140
|
-
}
|
|
141
|
-
routeLoadRequestedMap.set(route, currentAction);
|
|
142
|
-
// Create a new abort controller for this action
|
|
143
|
-
const actionAbortController = new AbortController();
|
|
144
|
-
actionAbortControllerWeakMap.set(currentAction, actionAbortController);
|
|
145
|
-
abortSignalMap.set(currentAction, actionAbortController.signal);
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const shouldLoad = (route) => {
|
|
149
|
-
shouldLoadOrReload(route, true);
|
|
150
|
-
};
|
|
151
|
-
const shouldReload = (route) => {
|
|
152
|
-
shouldLoadOrReload(route, false);
|
|
153
|
-
};
|
|
154
|
-
const shouldAbort = (route) => {
|
|
155
|
-
const routeAction = route.action;
|
|
156
|
-
const currentAction = routeAction.getCurrentAction();
|
|
157
|
-
const actionAbortController =
|
|
158
|
-
actionAbortControllerWeakMap.get(currentAction);
|
|
159
|
-
if (actionAbortController) {
|
|
160
|
-
actionAbortController.abort(`route no longer matching`);
|
|
161
|
-
actionAbortControllerWeakMap.delete(currentAction);
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
for (const {
|
|
166
|
-
route,
|
|
167
|
-
routePrivateProperties,
|
|
168
|
-
newActive,
|
|
169
|
-
oldActive,
|
|
170
|
-
newParams,
|
|
171
|
-
oldParams,
|
|
172
|
-
} of routeMatchInfoSet) {
|
|
173
|
-
const routeAction = route.action;
|
|
174
|
-
if (!routeAction) {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const becomesActive = newActive && !oldActive;
|
|
179
|
-
const becomesInactive = !newActive && oldActive;
|
|
180
|
-
const paramsChangedWhileActive =
|
|
181
|
-
newActive && oldActive && newParams !== oldParams;
|
|
182
|
-
|
|
183
|
-
// Handle actions for routes that become active
|
|
184
|
-
if (becomesActive) {
|
|
185
|
-
if (DEBUG) {
|
|
186
|
-
console.debug(
|
|
187
|
-
`Route ${routePrivateProperties.urlPattern} became active with params:`,
|
|
188
|
-
newParams,
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
shouldLoad(route);
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Handle actions for routes that become inactive - abort them
|
|
196
|
-
if (becomesInactive && ROUTE_DEACTIVATION_STRATEGY === "abort") {
|
|
197
|
-
shouldAbort(route);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Handle parameter changes while route stays active
|
|
202
|
-
if (paramsChangedWhileActive) {
|
|
203
|
-
if (DEBUG) {
|
|
204
|
-
console.debug(
|
|
205
|
-
`Route ${routePrivateProperties.urlPattern} params changed:`,
|
|
206
|
-
newParams,
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
shouldReload(route);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
loadSet: toLoadSet,
|
|
215
|
-
reloadSet: toReloadSet,
|
|
216
|
-
abortSignalMap,
|
|
217
|
-
routeLoadRequestedMap,
|
|
218
|
-
activeRouteSet,
|
|
219
|
-
};
|
|
220
|
-
};
|
|
221
|
-
const extractParams = (urlPattern, url, ignoreSet = new Set()) => {
|
|
222
|
-
const match = urlPattern.exec(url);
|
|
223
|
-
if (!match) {
|
|
224
|
-
return NO_PARAMS;
|
|
225
|
-
}
|
|
226
|
-
const params = {};
|
|
227
|
-
|
|
228
|
-
// Collect all parameters from URLPattern groups, handling both named and numbered groups
|
|
229
|
-
let wildcardOffset = 0;
|
|
230
|
-
for (const property of URL_PATTERN_PROPERTIES_WITH_GROUP_SET) {
|
|
231
|
-
const urlPartMatch = match[property];
|
|
232
|
-
if (urlPartMatch && urlPartMatch.groups) {
|
|
233
|
-
let localWildcardCount = 0;
|
|
234
|
-
for (const key of Object.keys(urlPartMatch.groups)) {
|
|
235
|
-
const value = urlPartMatch.groups[key];
|
|
236
|
-
const keyAsNumber = parseInt(key, 10);
|
|
237
|
-
if (!isNaN(keyAsNumber)) {
|
|
238
|
-
if (value) {
|
|
239
|
-
// Only include non-empty values and non-ignored wildcard indices
|
|
240
|
-
const wildcardKey = String(wildcardOffset + keyAsNumber);
|
|
241
|
-
if (!ignoreSet.has(wildcardKey)) {
|
|
242
|
-
params[wildcardKey] = decodeURIComponent(value);
|
|
243
|
-
}
|
|
244
|
-
localWildcardCount++;
|
|
245
|
-
}
|
|
246
|
-
} else if (!ignoreSet.has(key)) {
|
|
247
|
-
// Named group (:param or {param}) - only include if not ignored
|
|
248
|
-
params[key] = decodeURIComponent(value);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
// Update wildcard offset for next URL part
|
|
252
|
-
wildcardOffset += localWildcardCount;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return params;
|
|
256
|
-
};
|
|
257
|
-
const URL_PATTERN_PROPERTIES_WITH_GROUP_SET = new Set([
|
|
258
|
-
"protocol",
|
|
259
|
-
"username",
|
|
260
|
-
"password",
|
|
261
|
-
"hostname",
|
|
262
|
-
"pathname",
|
|
263
|
-
"search",
|
|
264
|
-
"hash",
|
|
265
|
-
]);
|
|
266
|
-
|
|
267
|
-
const routePrivatePropertiesMap = new Map();
|
|
268
|
-
const getRoutePrivateProperties = (route) => {
|
|
269
|
-
return routePrivatePropertiesMap.get(route);
|
|
270
|
-
};
|
|
271
|
-
const createRoute = (urlPatternInput) => {
|
|
272
|
-
const cleanupCallbackSet = new Set();
|
|
273
|
-
const cleanup = () => {
|
|
274
|
-
for (const cleanupCallback of cleanupCallbackSet) {
|
|
275
|
-
cleanupCallback();
|
|
276
|
-
}
|
|
277
|
-
cleanupCallbackSet.clear();
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
const route = {
|
|
281
|
-
urlPattern: urlPatternInput,
|
|
282
|
-
isRoute: true,
|
|
283
|
-
active: false,
|
|
284
|
-
params: NO_PARAMS,
|
|
285
|
-
buildUrl: null,
|
|
286
|
-
bindAction: null,
|
|
287
|
-
relativeUrl: null,
|
|
288
|
-
url: null,
|
|
289
|
-
action: null,
|
|
290
|
-
cleanup,
|
|
291
|
-
toString: () => {
|
|
292
|
-
return `route "${urlPatternInput}"`;
|
|
293
|
-
},
|
|
294
|
-
replaceParams: undefined,
|
|
295
|
-
};
|
|
296
|
-
routeSet.add(route);
|
|
297
|
-
|
|
298
|
-
const routePrivateProperties = {
|
|
299
|
-
urlPattern: undefined,
|
|
300
|
-
activeSignal: null,
|
|
301
|
-
paramsSignal: null,
|
|
302
|
-
visitedSignal: null,
|
|
303
|
-
relativeUrlSignal: null,
|
|
304
|
-
urlSignal: null,
|
|
305
|
-
optionalParamKeySet: null,
|
|
306
|
-
};
|
|
307
|
-
routePrivatePropertiesMap.set(route, routePrivateProperties);
|
|
308
|
-
|
|
309
|
-
const buildRelativeUrl = (params = {}) => {
|
|
310
|
-
let relativeUrl = urlPatternInput;
|
|
311
|
-
let hasRawUrlPartWithInvalidChars = false;
|
|
312
|
-
|
|
313
|
-
const encode = (value) => {
|
|
314
|
-
if (value && value[rawUrlPartSymbol]) {
|
|
315
|
-
const rawValue = value.value;
|
|
316
|
-
// Check if raw value contains invalid URL characters
|
|
317
|
-
if (/[\s<>{}|\\^`]/.test(rawValue)) {
|
|
318
|
-
hasRawUrlPartWithInvalidChars = true;
|
|
319
|
-
}
|
|
320
|
-
return rawValue;
|
|
321
|
-
}
|
|
322
|
-
return encodeURIComponent(value);
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
// Replace named parameters (:param and {param})
|
|
326
|
-
for (const key of Object.keys(params)) {
|
|
327
|
-
const value = params[key];
|
|
328
|
-
const encodedValue = encode(value);
|
|
329
|
-
relativeUrl = relativeUrl.replace(`:${key}`, encodedValue);
|
|
330
|
-
relativeUrl = relativeUrl.replace(`{${key}}`, encodedValue);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Handle wildcards: if the pattern ends with /*? (optional wildcard)
|
|
334
|
-
// always remove the wildcard part for URL building since it's optional
|
|
335
|
-
if (relativeUrl.endsWith("/*?")) {
|
|
336
|
-
// Always remove the optional wildcard part for URL building
|
|
337
|
-
relativeUrl = relativeUrl.replace(/\/\*\?$/, "");
|
|
338
|
-
} else {
|
|
339
|
-
// For required wildcards (/*) or other patterns, replace normally
|
|
340
|
-
let wildcardIndex = 0;
|
|
341
|
-
relativeUrl = relativeUrl.replace(/\*/g, () => {
|
|
342
|
-
const paramKey = wildcardIndex.toString();
|
|
343
|
-
const paramValue = params[paramKey];
|
|
344
|
-
const replacement = paramValue ? encode(paramValue) : "*";
|
|
345
|
-
wildcardIndex++;
|
|
346
|
-
return replacement;
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return {
|
|
351
|
-
relativeUrl,
|
|
352
|
-
hasRawUrlPartWithInvalidChars,
|
|
353
|
-
};
|
|
354
|
-
};
|
|
355
|
-
const buildUrl = (params = {}) => {
|
|
356
|
-
const { relativeUrl, hasRawUrlPartWithInvalidChars } =
|
|
357
|
-
buildRelativeUrl(params);
|
|
358
|
-
let processedRelativeUrl = relativeUrl;
|
|
359
|
-
if (processedRelativeUrl[0] === "/") {
|
|
360
|
-
processedRelativeUrl = processedRelativeUrl.slice(1);
|
|
361
|
-
}
|
|
362
|
-
if (hasRawUrlPartWithInvalidChars) {
|
|
363
|
-
return `${baseUrl}/${processedRelativeUrl}`;
|
|
364
|
-
}
|
|
365
|
-
const url = new URL(processedRelativeUrl, baseUrl).href;
|
|
366
|
-
return url;
|
|
367
|
-
};
|
|
368
|
-
route.buildUrl = buildUrl;
|
|
369
|
-
|
|
370
|
-
const activeSignal = signal(false);
|
|
371
|
-
const paramsSignal = signal(NO_PARAMS);
|
|
372
|
-
const visitedSignal = signal(false);
|
|
373
|
-
const relativeUrlSignal = computed(() => {
|
|
374
|
-
const params = paramsSignal.value;
|
|
375
|
-
const { relativeUrl } = buildRelativeUrl(params);
|
|
376
|
-
return relativeUrl;
|
|
377
|
-
});
|
|
378
|
-
const disposeRelativeUrlEffect = effect(() => {
|
|
379
|
-
route.relativeUrl = relativeUrlSignal.value;
|
|
380
|
-
});
|
|
381
|
-
cleanupCallbackSet.add(disposeRelativeUrlEffect);
|
|
382
|
-
|
|
383
|
-
const urlSignal = computed(() => {
|
|
384
|
-
const relativeUrl = relativeUrlSignal.value;
|
|
385
|
-
const url = new URL(relativeUrl, baseUrl).href;
|
|
386
|
-
return url;
|
|
387
|
-
});
|
|
388
|
-
const disposeUrlEffect = effect(() => {
|
|
389
|
-
route.url = urlSignal.value;
|
|
390
|
-
});
|
|
391
|
-
cleanupCallbackSet.add(disposeUrlEffect);
|
|
392
|
-
|
|
393
|
-
const replaceParams = (newParams) => {
|
|
394
|
-
const currentParams = paramsSignal.peek();
|
|
395
|
-
const updatedParams = { ...currentParams, ...newParams };
|
|
396
|
-
const updatedUrl = route.buildUrl(updatedParams);
|
|
397
|
-
if (route.action) {
|
|
398
|
-
route.action.replaceParams(updatedParams);
|
|
399
|
-
}
|
|
400
|
-
browserIntegration.goTo(updatedUrl, { replace: true });
|
|
401
|
-
};
|
|
402
|
-
route.replaceParams = replaceParams;
|
|
403
|
-
|
|
404
|
-
const bindAction = (action) => {
|
|
405
|
-
/*
|
|
406
|
-
*
|
|
407
|
-
* here I need to check the store for that action (if any)
|
|
408
|
-
* and listen store changes to do this:
|
|
409
|
-
*
|
|
410
|
-
* When we detect changes we want to update the route params
|
|
411
|
-
* so we'll need to use goTo(buildUrl(params), { replace: true })
|
|
412
|
-
*
|
|
413
|
-
* reinserted is useful because the item id might have changed
|
|
414
|
-
* but not the mutable key
|
|
415
|
-
*
|
|
416
|
-
*/
|
|
417
|
-
|
|
418
|
-
const { store } = action.meta;
|
|
419
|
-
if (store) {
|
|
420
|
-
const { mutableIdKeys } = store;
|
|
421
|
-
if (mutableIdKeys.length) {
|
|
422
|
-
const mutableIdKey = mutableIdKeys[0];
|
|
423
|
-
const mutableIdValueSignal = computed(() => {
|
|
424
|
-
const params = paramsSignal.value;
|
|
425
|
-
const mutableIdValue = params[mutableIdKey];
|
|
426
|
-
return mutableIdValue;
|
|
427
|
-
});
|
|
428
|
-
const routeItemSignal = store.signalForMutableIdKey(
|
|
429
|
-
mutableIdKey,
|
|
430
|
-
mutableIdValueSignal,
|
|
431
|
-
);
|
|
432
|
-
store.observeProperties(routeItemSignal, (propertyMutations) => {
|
|
433
|
-
const mutableIdPropertyMutation = propertyMutations[mutableIdKey];
|
|
434
|
-
if (!mutableIdPropertyMutation) {
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
route.replaceParams({
|
|
438
|
-
[mutableIdKey]: mutableIdPropertyMutation.newValue,
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/*
|
|
445
|
-
store.registerPropertyLifecycle(activeItemSignal, key, {
|
|
446
|
-
changed: (value) => {
|
|
447
|
-
route.replaceParams({
|
|
448
|
-
[key]: value,
|
|
449
|
-
});
|
|
450
|
-
},
|
|
451
|
-
dropped: () => {
|
|
452
|
-
route.reload();
|
|
453
|
-
},
|
|
454
|
-
reinserted: () => {
|
|
455
|
-
// this will reload all routes which works but
|
|
456
|
-
// - most of the time only "route" is impacted, any other route could stay as is
|
|
457
|
-
// - we already have the data, reloading the route will refetch the backend which is unnecessary
|
|
458
|
-
// we could just remove routing error (which is cause by 404 likely)
|
|
459
|
-
// to actually let the data be displayed
|
|
460
|
-
// because they are available, but in reality the route has no data
|
|
461
|
-
// because the fetch failed
|
|
462
|
-
// so conceptually reloading is fine,
|
|
463
|
-
// the only thing that bothers me a little is that it reloads all routes
|
|
464
|
-
route.reload();
|
|
465
|
-
},
|
|
466
|
-
});
|
|
467
|
-
*/
|
|
468
|
-
|
|
469
|
-
const actionBoundToThisRoute = action.bindParams(paramsSignal);
|
|
470
|
-
route.action = actionBoundToThisRoute;
|
|
471
|
-
return actionBoundToThisRoute;
|
|
472
|
-
};
|
|
473
|
-
route.bindAction = bindAction;
|
|
474
|
-
|
|
475
|
-
private_properties: {
|
|
476
|
-
// Remove leading slash from urlPattern to make it relative to baseUrl
|
|
477
|
-
const normalizedUrlPattern = urlPatternInput.startsWith("/")
|
|
478
|
-
? urlPatternInput.slice(1)
|
|
479
|
-
: urlPatternInput;
|
|
480
|
-
const urlPattern = new URLPattern(normalizedUrlPattern, baseUrl, {
|
|
481
|
-
ignoreCase: true,
|
|
482
|
-
});
|
|
483
|
-
routePrivateProperties.urlPattern = urlPattern;
|
|
484
|
-
routePrivateProperties.activeSignal = activeSignal;
|
|
485
|
-
routePrivateProperties.paramsSignal = paramsSignal;
|
|
486
|
-
routePrivateProperties.visitedSignal = visitedSignal;
|
|
487
|
-
routePrivateProperties.relativeUrlSignal = relativeUrlSignal;
|
|
488
|
-
routePrivateProperties.urlSignal = urlSignal;
|
|
489
|
-
routePrivateProperties.cleanupCallbackSet = cleanupCallbackSet;
|
|
490
|
-
|
|
491
|
-
// Analyze pattern once to detect optional params (named and wildcard indices)
|
|
492
|
-
// Note: Wildcard indices are stored as strings ("0", "1", ...) to match keys from extractParams
|
|
493
|
-
const optionalParamKeySet = new Set();
|
|
494
|
-
normalizedUrlPattern.replace(/:([A-Za-z0-9_]+)\?/g, (_m, name) => {
|
|
495
|
-
optionalParamKeySet.add(name);
|
|
496
|
-
return "";
|
|
497
|
-
});
|
|
498
|
-
let wildcardIndex = 0;
|
|
499
|
-
normalizedUrlPattern.replace(/\*(\?)?/g, (_m, opt) => {
|
|
500
|
-
if (opt === "?") {
|
|
501
|
-
optionalParamKeySet.add(String(wildcardIndex));
|
|
502
|
-
}
|
|
503
|
-
wildcardIndex++;
|
|
504
|
-
return "";
|
|
505
|
-
});
|
|
506
|
-
routePrivateProperties.optionalParamKeySet = optionalParamKeySet;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
return route;
|
|
510
|
-
};
|
|
511
|
-
export const useRouteStatus = (route) => {
|
|
512
|
-
if (import.meta.dev && (!route || !route.isRoute)) {
|
|
513
|
-
throw new TypeError(
|
|
514
|
-
`useRouteStatus() requires a route object, but received ${route}.`,
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
518
|
-
if (!routePrivateProperties) {
|
|
519
|
-
if (import.meta.dev) {
|
|
520
|
-
let errorMessage = `Cannot find route private properties for ${route}.`;
|
|
521
|
-
|
|
522
|
-
errorMessage += `\nThis might be caused by hot reloading - try refreshing the page.`;
|
|
523
|
-
throw new Error(errorMessage);
|
|
524
|
-
}
|
|
525
|
-
throw new Error(`Cannot find route private properties for ${route}`);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const { urlSignal, activeSignal, paramsSignal, visitedSignal } =
|
|
529
|
-
routePrivateProperties;
|
|
530
|
-
|
|
531
|
-
const url = urlSignal.value;
|
|
532
|
-
const active = activeSignal.value;
|
|
533
|
-
const params = paramsSignal.value;
|
|
534
|
-
const visited = visitedSignal.value;
|
|
535
|
-
|
|
536
|
-
return {
|
|
537
|
-
url,
|
|
538
|
-
active,
|
|
539
|
-
params,
|
|
540
|
-
visited,
|
|
541
|
-
};
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
let browserIntegration;
|
|
545
|
-
export const setBrowserIntegration = (integration) => {
|
|
546
|
-
browserIntegration = integration;
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
let onRouteDefined = () => {};
|
|
550
|
-
export const setOnRouteDefined = (v) => {
|
|
551
|
-
onRouteDefined = v;
|
|
552
|
-
};
|
|
553
|
-
/**
|
|
554
|
-
* Define all routes for the application.
|
|
555
|
-
*
|
|
556
|
-
* ⚠️ HOT RELOAD WARNING: When destructuring the returned routes, use 'let' instead of 'const'
|
|
557
|
-
* to allow hot reload to update the route references:
|
|
558
|
-
*
|
|
559
|
-
* ❌ const [ROLE_ROUTE, DATABASE_ROUTE] = defineRoutes({...})
|
|
560
|
-
* ✅ let [ROLE_ROUTE, DATABASE_ROUTE] = defineRoutes({...})
|
|
561
|
-
*
|
|
562
|
-
* @param {Object} routeDefinition - Object mapping URL patterns to actions
|
|
563
|
-
* @returns {Array} Array of route objects in the same order as the keys
|
|
564
|
-
*/
|
|
565
|
-
// All routes MUST be created at once because any url can be accessed
|
|
566
|
-
// at any given time (url can be shared, reloaded, etc..)
|
|
567
|
-
// Later I'll consider adding ability to have dynamic import into the mix
|
|
568
|
-
// (An async function returning an action)
|
|
569
|
-
export const defineRoutes = (routeDefinition) => {
|
|
570
|
-
// Clean up existing routes
|
|
571
|
-
for (const route of routeSet) {
|
|
572
|
-
route.cleanup();
|
|
573
|
-
}
|
|
574
|
-
routeSet.clear();
|
|
575
|
-
|
|
576
|
-
const routeArray = [];
|
|
577
|
-
for (const key of Object.keys(routeDefinition)) {
|
|
578
|
-
const value = routeDefinition[key];
|
|
579
|
-
const route = createRoute(key);
|
|
580
|
-
if (value && value.isAction) {
|
|
581
|
-
route.bindAction(value);
|
|
582
|
-
} else if (typeof value === "function") {
|
|
583
|
-
const actionFromFunction = createAction(value);
|
|
584
|
-
route.bindAction(actionFromFunction);
|
|
585
|
-
} else if (value) {
|
|
586
|
-
route.bindAction(value);
|
|
587
|
-
}
|
|
588
|
-
routeArray.push(route);
|
|
589
|
-
}
|
|
590
|
-
onRouteDefined();
|
|
591
|
-
|
|
592
|
-
return routeArray;
|
|
593
|
-
};
|
|
594
|
-
|
|
595
|
-
// unit test exports
|
|
596
|
-
export { createRoute };
|