@esmx/router 3.0.0-rc.59 → 3.0.0-rc.61

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/options.mjs CHANGED
@@ -30,7 +30,7 @@ function getBaseUrl(options) {
30
30
  return base;
31
31
  }
32
32
  export function parsedOptions(options = {}) {
33
- var _a, _b, _c, _d, _e, _f;
33
+ var _a, _b, _c, _d, _e, _f, _g;
34
34
  const base = getBaseUrl(options);
35
35
  const routes = (_a = options.routes) != null ? _a : [];
36
36
  const compiledRoutes = createRouteMatches(routes);
@@ -50,9 +50,11 @@ export function parsedOptions(options = {}) {
50
50
  matcher: createMatcher(routes, compiledRoutes),
51
51
  normalizeURL: (_c = options.normalizeURL) != null ? _c : (url) => url,
52
52
  fallback: (_d = options.fallback) != null ? _d : fallback,
53
- handleBackBoundary: (_e = options.handleBackBoundary) != null ? _e : () => {
53
+ nextTick: (_e = options.nextTick) != null ? _e : () => {
54
54
  },
55
- handleLayerClose: (_f = options.handleLayerClose) != null ? _f : () => {
55
+ handleBackBoundary: (_f = options.handleBackBoundary) != null ? _f : () => {
56
+ },
57
+ handleLayerClose: (_g = options.handleLayerClose) != null ? _g : () => {
56
58
  }
57
59
  });
58
60
  }
@@ -20,10 +20,7 @@ export class RouteTaskController {
20
20
  this._aborted = true;
21
21
  }
22
22
  shouldCancel(name) {
23
- if (this._aborted) {
24
- return true;
25
- }
26
- return false;
23
+ return this._aborted;
27
24
  }
28
25
  }
29
26
  export async function createRouteTask(opts) {
@@ -6,8 +6,14 @@ import {
6
6
  RouteTaskController,
7
7
  createRouteTask
8
8
  } from "./route-task.mjs";
9
+ import {
10
+ getSavedScrollPosition,
11
+ saveScrollPosition,
12
+ scrollToPosition
13
+ } from "./scroll.mjs";
9
14
  import { RouteType } from "./types.mjs";
10
15
  import {
16
+ isBrowser,
11
17
  isRouteMatched,
12
18
  isUrlEqual,
13
19
  isValidConfirmHookResult,
@@ -106,6 +112,44 @@ export const ROUTE_TRANSITION_HOOKS = {
106
112
  return result;
107
113
  }
108
114
  }
115
+ if (isBrowser && "scrollRestoration" in window.history)
116
+ window.history.scrollRestoration = "manual";
117
+ if (from && isBrowser && !router.isLayer)
118
+ switch (to.type) {
119
+ case RouteType.push:
120
+ case RouteType.replace: {
121
+ if (!to.keepScrollPosition) {
122
+ saveScrollPosition(from.url.href);
123
+ scrollToPosition({ left: 0, top: 0 });
124
+ } else {
125
+ to.applyNavigationState({
126
+ __keepScrollPosition: to.keepScrollPosition
127
+ });
128
+ }
129
+ break;
130
+ }
131
+ case RouteType.go:
132
+ case RouteType.forward:
133
+ case RouteType.back:
134
+ // for popstate
135
+ case RouteType.unknown: {
136
+ saveScrollPosition(from.url.href);
137
+ setTimeout(async () => {
138
+ const state = window.history.state;
139
+ if (state == null ? void 0 : state.__keepScrollPosition) {
140
+ return;
141
+ }
142
+ const savedPosition = getSavedScrollPosition(
143
+ to.url.href,
144
+ { left: 0, top: 0 }
145
+ );
146
+ if (!savedPosition) return;
147
+ await router.parsedOptions.nextTick();
148
+ scrollToPosition(savedPosition);
149
+ });
150
+ break;
151
+ }
152
+ }
109
153
  switch (to.type) {
110
154
  case RouteType.push:
111
155
  return ROUTE_TYPE_HANDLERS.push;
@@ -17,15 +17,14 @@ function getEventTypeList(eventType) {
17
17
  const validEvents = events.filter((type) => typeof type === "string").map((type) => type.trim()).filter(Boolean);
18
18
  return validEvents.length ? validEvents : ["click"];
19
19
  }
20
- function guardEvent(e) {
21
- var _a, _b, _c;
22
- if (!e) return;
23
- if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
24
- if (e.defaultPrevented) return;
25
- if (e.button !== void 0 && e.button !== 0) return;
26
- const target = (_c = (_b = (_a = e.currentTarget) == null ? void 0 : _a.getAttribute) == null ? void 0 : _b.call(_a, "target")) != null ? _c : "";
27
- if (/\b_blank\b/i.test(target)) return;
28
- if (e.preventDefault) e.preventDefault();
20
+ function shouldHandleNavigation(e) {
21
+ var _a;
22
+ if (e.defaultPrevented) return false;
23
+ if (e instanceof MouseEvent) {
24
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return false;
25
+ if (e.button !== void 0 && e.button !== 0) return false;
26
+ }
27
+ (_a = e.preventDefault) == null ? void 0 : _a.call(e);
29
28
  return true;
30
29
  }
31
30
  async function executeNavigation(router, props, linkType) {
@@ -54,10 +53,9 @@ async function executeNavigation(router, props, linkType) {
54
53
  }
55
54
  function createNavigateFunction(router, props, navigationType) {
56
55
  return async (e) => {
57
- var _a;
58
- const eventHandler = (_a = props.eventHandler) != null ? _a : guardEvent;
59
- if (!eventHandler(e)) return;
60
- await executeNavigation(router, props, navigationType);
56
+ if (shouldHandleNavigation(e)) {
57
+ await executeNavigation(router, props, navigationType);
58
+ }
61
59
  };
62
60
  }
63
61
  function computeAttributes(href, navigationType, isExternal, isActive, isExactActive, activeClass) {
@@ -88,13 +86,18 @@ function computeAttributes(href, navigationType, isExternal, isActive, isExactAc
88
86
  }
89
87
  return attributes;
90
88
  }
91
- function createEventHandlersGenerator(navigate, eventTypes) {
92
- return (nameTransform) => {
89
+ function createEventHandlersGenerator(router, props, navigationType, eventTypes) {
90
+ return (format) => {
93
91
  const handlers = {};
92
+ const navigate = createNavigateFunction(router, props, navigationType);
94
93
  eventTypes.forEach((eventType) => {
95
94
  var _a;
96
- const eventName = (_a = nameTransform == null ? void 0 : nameTransform(eventType)) != null ? _a : eventType.toLowerCase();
97
- handlers[eventName] = navigate;
95
+ const eventName = (_a = format == null ? void 0 : format(eventType)) != null ? _a : eventType.toLowerCase();
96
+ handlers[eventName] = (event) => {
97
+ var _a2;
98
+ (_a2 = props.beforeNavigate) == null ? void 0 : _a2.call(props, event, eventType);
99
+ return navigate(event);
100
+ };
98
101
  });
99
102
  return handlers;
100
103
  };
@@ -106,7 +109,6 @@ export function createLinkResolver(router, props) {
106
109
  const isActive = router.isRouteMatched(route, props.exact);
107
110
  const isExactActive = router.isRouteMatched(route, "exact");
108
111
  const isExternal = route.url.origin !== router.route.url.origin;
109
- const navigate = createNavigateFunction(router, props, type);
110
112
  const attributes = computeAttributes(
111
113
  href,
112
114
  type,
@@ -116,7 +118,13 @@ export function createLinkResolver(router, props) {
116
118
  props.activeClass
117
119
  );
118
120
  const eventTypes = getEventTypeList(props.event || "click");
119
- const getEventHandlers = createEventHandlersGenerator(navigate, eventTypes);
121
+ const createEventHandlers = createEventHandlersGenerator(
122
+ router,
123
+ props,
124
+ type,
125
+ eventTypes
126
+ );
127
+ const navigate = createNavigateFunction(router, props, type);
120
128
  return {
121
129
  route,
122
130
  type,
@@ -126,6 +134,6 @@ export function createLinkResolver(router, props) {
126
134
  tag: props.tag || "a",
127
135
  attributes,
128
136
  navigate,
129
- getEventHandlers
137
+ createEventHandlers
130
138
  };
131
139
  }
package/dist/router.d.ts CHANGED
@@ -103,7 +103,7 @@ export declare class Router {
103
103
  * linkData.navigate(); // Programmatic navigation
104
104
  *
105
105
  * // Get event handlers for React
106
- * const handlers = linkData.getEventHandlers(name => `on${name.charAt(0).toUpperCase() + name.slice(1)}`);
106
+ * const handlers = linkData.createEventHandlers(name => `on${name.charAt(0).toUpperCase() + name.slice(1)}`);
107
107
  * // handlers.onClick for React
108
108
  * ```
109
109
  */
package/dist/router.mjs CHANGED
@@ -200,7 +200,7 @@ export class Router {
200
200
  * linkData.navigate(); // Programmatic navigation
201
201
  *
202
202
  * // Get event handlers for React
203
- * const handlers = linkData.getEventHandlers(name => `on${name.charAt(0).toUpperCase() + name.slice(1)}`);
203
+ * const handlers = linkData.createEventHandlers(name => `on${name.charAt(0).toUpperCase() + name.slice(1)}`);
204
204
  * // handlers.onClick for React
205
205
  * ```
206
206
  */
@@ -0,0 +1,33 @@
1
+ /** Internal {@link ScrollToOptions | `ScrollToOptions`}: `left` and `top` properties always have values */
2
+ interface _ScrollPosition extends ScrollToOptions {
3
+ left: number;
4
+ top: number;
5
+ }
6
+ export interface ScrollPositionElement extends ScrollToOptions {
7
+ /**
8
+ * A valid CSS selector. Some special characters need to be escaped (https://mathiasbynens.be/notes/css-escapes).
9
+ * @example
10
+ * Here are some examples:
11
+ *
12
+ * - `.title`
13
+ * - `.content:first-child`
14
+ * - `#marker`
15
+ * - `#marker\~with\~symbols`
16
+ * - `#marker.with.dot`: Selects `class="with dot" id="marker"`, not `id="marker.with.dot"`
17
+ *
18
+ */
19
+ el: string | Element;
20
+ }
21
+ /** Scroll parameters */
22
+ export type ScrollPosition = ScrollToOptions | ScrollPositionElement;
23
+ /** Get current window scroll position */
24
+ export declare const winScrollPos: () => _ScrollPosition;
25
+ /** Scroll to specified position */
26
+ export declare function scrollToPosition(position: ScrollPosition): void;
27
+ /** Stored scroll positions */
28
+ export declare const scrollPositions: Map<string, _ScrollPosition>;
29
+ /** Save scroll position */
30
+ export declare function saveScrollPosition(key: string, scrollPosition?: _ScrollPosition): void;
31
+ /** Get saved scroll position */
32
+ export declare function getSavedScrollPosition(key: string, defaultValue?: _ScrollPosition | null): _ScrollPosition | null;
33
+ export {};
@@ -0,0 +1,49 @@
1
+ export const winScrollPos = () => ({
2
+ left: window.scrollX,
3
+ top: window.scrollY
4
+ });
5
+ function getElementPosition(el, offset) {
6
+ const docRect = document.documentElement.getBoundingClientRect();
7
+ const elRect = el.getBoundingClientRect();
8
+ return {
9
+ behavior: offset.behavior,
10
+ left: elRect.left - docRect.left - (offset.left || 0),
11
+ top: elRect.top - docRect.top - (offset.top || 0)
12
+ };
13
+ }
14
+ export function scrollToPosition(position) {
15
+ if ("el" in position) {
16
+ const positionEl = position.el;
17
+ const el = typeof positionEl === "string" ? document.querySelector(positionEl) : positionEl;
18
+ if (!el) return;
19
+ position = getElementPosition(el, position);
20
+ }
21
+ if ("scrollBehavior" in document.documentElement.style) {
22
+ window.scrollTo(position);
23
+ } else {
24
+ window.scrollTo(
25
+ Number.isFinite(position.left) ? position.left : window.scrollX,
26
+ Number.isFinite(position.top) ? position.top : window.scrollY
27
+ );
28
+ }
29
+ }
30
+ export const scrollPositions = /* @__PURE__ */ new Map();
31
+ const POSITION_KEY = "__scroll_position_key";
32
+ export function saveScrollPosition(key, scrollPosition = winScrollPos()) {
33
+ scrollPosition = { ...scrollPosition };
34
+ scrollPositions.set(key, scrollPosition);
35
+ try {
36
+ if (location.href !== key) return;
37
+ const stateCopy = {
38
+ ...history.state || {},
39
+ [POSITION_KEY]: scrollPosition
40
+ };
41
+ history.replaceState(stateCopy, "");
42
+ } catch (error) {
43
+ }
44
+ }
45
+ export function getSavedScrollPosition(key, defaultValue = null) {
46
+ const scroll = scrollPositions.get(key) || history.state[POSITION_KEY];
47
+ scrollPositions.delete(key);
48
+ return scroll || defaultValue;
49
+ }
package/dist/types.d.ts CHANGED
@@ -43,6 +43,7 @@ export interface RouteLocation {
43
43
  queryArray?: Record<string, string[] | undefined>;
44
44
  hash?: string;
45
45
  state?: RouteState;
46
+ /** When `true`, maintains current scroll position after navigation (default behavior scrolls to top) */
46
47
  keepScrollPosition?: boolean;
47
48
  statusCode?: number | null;
48
49
  layer?: RouteLayerOptions | null;
@@ -194,6 +195,7 @@ export interface RouterOptions {
194
195
  apps?: RouterMicroApp;
195
196
  normalizeURL?: (to: URL, from: URL | null) => URL;
196
197
  fallback?: RouteHandleHook;
198
+ nextTick?: () => Awaitable<void>;
197
199
  rootStyle?: Partial<CSSStyleDeclaration> | false | null;
198
200
  layer?: boolean;
199
201
  zIndex?: number;
@@ -217,7 +219,6 @@ export interface RouterLinkAttributes {
217
219
  target?: '_blank';
218
220
  rel?: string;
219
221
  }
220
- export type RouterLinkEventHandler = <E extends Event = MouseEvent>(event: E) => boolean | void;
221
222
  /**
222
223
  * Framework-agnostic link configuration interface
223
224
  */
@@ -233,7 +234,15 @@ export interface RouterLinkProps {
233
234
  event?: string | string[];
234
235
  tag?: string;
235
236
  layerOptions?: RouteLayerOptions;
236
- eventHandler?: RouterLinkEventHandler;
237
+ /**
238
+ * Hook function called before navigation occurs
239
+ * @param event - The DOM event that triggered the navigation
240
+ * @param eventName - The name of the event that triggered navigation
241
+ *
242
+ * Can prevent default event to block the default navigation handling logic.
243
+ * Call event.preventDefault() to stop the navigation from proceeding.
244
+ */
245
+ beforeNavigate?: (event: Event, eventName: string) => void;
237
246
  }
238
247
  /**
239
248
  * Framework-agnostic link resolution result
@@ -246,6 +255,6 @@ export interface RouterLinkResolved {
246
255
  isExternal: boolean;
247
256
  tag: string;
248
257
  attributes: RouterLinkAttributes;
249
- navigate: (e?: Event) => Promise<void>;
250
- getEventHandlers: (nameTransform?: (eventType: string) => string) => Record<string, (e: Event) => Promise<void> | undefined>;
258
+ navigate: (e: Event) => Promise<void>;
259
+ createEventHandlers: (format?: (eventType: string) => string) => Record<string, (e: Event) => Promise<void>>;
251
260
  }
package/package.json CHANGED
@@ -39,7 +39,7 @@
39
39
  "unbuild": "3.6.0",
40
40
  "vitest": "3.2.4"
41
41
  },
42
- "version": "3.0.0-rc.59",
42
+ "version": "3.0.0-rc.61",
43
43
  "type": "module",
44
44
  "private": false,
45
45
  "exports": {
@@ -58,5 +58,5 @@
58
58
  "template",
59
59
  "public"
60
60
  ],
61
- "gitHead": "d221aba2a43064b5666e714f42904ae80b2b57ad"
61
+ "gitHead": "cce4468980071fe7a0a1e5f4941f1750cacc5215"
62
62
  }
package/src/options.ts CHANGED
@@ -83,6 +83,7 @@ export function parsedOptions(
83
83
  matcher: createMatcher(routes, compiledRoutes),
84
84
  normalizeURL: options.normalizeURL ?? ((url) => url),
85
85
  fallback: options.fallback ?? fallback,
86
+ nextTick: options.nextTick ?? (() => {}),
86
87
  handleBackBoundary: options.handleBackBoundary ?? (() => {}),
87
88
  handleLayerClose: options.handleLayerClose ?? (() => {})
88
89
  });
package/src/route-task.ts CHANGED
@@ -23,10 +23,7 @@ export class RouteTaskController {
23
23
  }
24
24
 
25
25
  shouldCancel(name: string): boolean {
26
- if (this._aborted) {
27
- return true;
28
- }
29
- return false;
26
+ return this._aborted;
30
27
  }
31
28
  }
32
29
 
@@ -5,6 +5,12 @@ import {
5
5
  createRouteTask
6
6
  } from './route-task';
7
7
  import type { Router } from './router';
8
+ import {
9
+ getSavedScrollPosition,
10
+ saveScrollPosition,
11
+ scrollToPosition,
12
+ winScrollPos
13
+ } from './scroll';
8
14
  import { RouteType } from './types';
9
15
 
10
16
  import type {
@@ -15,6 +21,7 @@ import type {
15
21
  RouteNotifyHook
16
22
  } from './types';
17
23
  import {
24
+ isBrowser,
18
25
  isRouteMatched,
19
26
  isUrlEqual,
20
27
  isValidConfirmHookResult,
@@ -174,6 +181,46 @@ export const ROUTE_TRANSITION_HOOKS = {
174
181
  }
175
182
  }
176
183
 
184
+ if (isBrowser && 'scrollRestoration' in window.history)
185
+ window.history.scrollRestoration = 'manual';
186
+ // handle scroll position
187
+ if (from && isBrowser && !router.isLayer)
188
+ switch (to.type) {
189
+ case RouteType.push:
190
+ case RouteType.replace: {
191
+ if (!to.keepScrollPosition) {
192
+ saveScrollPosition(from.url.href);
193
+ scrollToPosition({ left: 0, top: 0 });
194
+ } else {
195
+ to.applyNavigationState({
196
+ __keepScrollPosition: to.keepScrollPosition
197
+ });
198
+ }
199
+ break;
200
+ }
201
+ case RouteType.go:
202
+ case RouteType.forward:
203
+ case RouteType.back:
204
+ // for popstate
205
+ case RouteType.unknown: {
206
+ saveScrollPosition(from.url.href);
207
+ setTimeout(async () => {
208
+ const state = window.history.state;
209
+ if (state?.__keepScrollPosition) {
210
+ return;
211
+ }
212
+ const savedPosition = getSavedScrollPosition(
213
+ to.url.href,
214
+ { left: 0, top: 0 }
215
+ );
216
+ if (!savedPosition) return;
217
+ await router.parsedOptions.nextTick();
218
+ scrollToPosition(savedPosition);
219
+ });
220
+ break;
221
+ }
222
+ }
223
+
177
224
  switch (to.type) {
178
225
  case RouteType.push:
179
226
  return ROUTE_TYPE_HANDLERS.push;
@@ -41,29 +41,22 @@ function getEventTypeList(eventType: unknown | unknown[]): string[] {
41
41
  }
42
42
 
43
43
  /**
44
- * Event guard check - determines if the router should handle the navigation
44
+ * Navigation event handler called before navigation - determines if the router should handle the navigation
45
45
  *
46
- * Returns !0: Let browser handle default behavior (normal link navigation)
47
- * Returns 0: Router takes over navigation, prevents default browser behavior
46
+ * Returns false: Let browser handle default behavior (normal link navigation)
47
+ * Returns true: Router takes over navigation, prevents default browser behavior
48
48
  *
49
49
  * This function intelligently decides when to let the browser handle clicks
50
50
  * (like Ctrl+click for new tabs) vs when to use SPA routing
51
51
  */
52
- function guardEvent(e?: Event & Partial<MouseEvent>): true | undefined {
53
- if (!e) return;
54
- // don't redirect with control keys
55
- if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
56
- // don't redirect when preventDefault called
57
- if (e.defaultPrevented) return;
58
- // don't redirect on right click
59
- if (e.button !== undefined && e.button !== 0) return;
60
- // don't redirect if `target="_blank"`
61
- // @ts-expect-error getAttribute exists
62
- const target = e.currentTarget?.getAttribute?.('target') ?? '';
63
- if (/\b_blank\b/i.test(target)) return;
64
- // Prevent default browser navigation to enable SPA routing
65
- // Note: this may be a Weex event which doesn't have this method
66
- if (e.preventDefault) e.preventDefault();
52
+ function shouldHandleNavigation(e: Event): boolean {
53
+ if (e.defaultPrevented) return false;
54
+ if (e instanceof MouseEvent) {
55
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return false;
56
+ if (e.button !== undefined && e.button !== 0) return false;
57
+ }
58
+
59
+ e.preventDefault?.();
67
60
 
68
61
  return true;
69
62
  }
@@ -112,12 +105,11 @@ function createNavigateFunction(
112
105
  router: Router,
113
106
  props: RouterLinkProps,
114
107
  navigationType: RouterLinkType
115
- ): (e?: Event) => Promise<void> {
116
- return async (e?: Event): Promise<void> => {
117
- const eventHandler = props.eventHandler ?? guardEvent;
118
- if (!eventHandler(e!)) return;
119
-
120
- await executeNavigation(router, props, navigationType);
108
+ ): RouterLinkResolved['navigate'] {
109
+ return async (e: Event): Promise<void> => {
110
+ if (shouldHandleNavigation(e)) {
111
+ await executeNavigation(router, props, navigationType);
112
+ }
121
113
  };
122
114
  }
123
115
 
@@ -173,18 +165,21 @@ function computeAttributes(
173
165
  * Create event handlers generator function
174
166
  */
175
167
  function createEventHandlersGenerator(
176
- navigate: (e: Event) => Promise<void>,
168
+ router: Router,
169
+ props: RouterLinkProps,
170
+ navigationType: RouterLinkType,
177
171
  eventTypes: string[]
178
- ): (
179
- nameTransform?: (eventType: string) => string
180
- ) => Record<string, (e: Event) => Promise<void>> {
181
- return (nameTransform?: (eventType: string) => string) => {
172
+ ): RouterLinkResolved['createEventHandlers'] {
173
+ return (format?: (eventType: string) => string) => {
182
174
  const handlers: Record<string, (e: Event) => Promise<void>> = {};
175
+ const navigate = createNavigateFunction(router, props, navigationType);
183
176
 
184
177
  eventTypes.forEach((eventType) => {
185
- const eventName =
186
- nameTransform?.(eventType) ?? eventType.toLowerCase();
187
- handlers[eventName] = navigate;
178
+ const eventName = format?.(eventType) ?? eventType.toLowerCase();
179
+ handlers[eventName] = (event) => {
180
+ props.beforeNavigate?.(event, eventType);
181
+ return navigate(event);
182
+ };
188
183
  });
189
184
 
190
185
  return handlers;
@@ -210,8 +205,6 @@ export function createLinkResolver(
210
205
  const isExactActive = router.isRouteMatched(route, 'exact');
211
206
  const isExternal = route.url.origin !== router.route.url.origin;
212
207
 
213
- const navigate = createNavigateFunction(router, props, type);
214
-
215
208
  const attributes = computeAttributes(
216
209
  href,
217
210
  type,
@@ -222,7 +215,14 @@ export function createLinkResolver(
222
215
  );
223
216
 
224
217
  const eventTypes = getEventTypeList(props.event || 'click');
225
- const getEventHandlers = createEventHandlersGenerator(navigate, eventTypes);
218
+ const createEventHandlers = createEventHandlersGenerator(
219
+ router,
220
+ props,
221
+ type,
222
+ eventTypes
223
+ );
224
+
225
+ const navigate = createNavigateFunction(router, props, type);
226
226
 
227
227
  return {
228
228
  route,
@@ -233,6 +233,6 @@ export function createLinkResolver(
233
233
  tag: props.tag || 'a',
234
234
  attributes,
235
235
  navigate,
236
- getEventHandlers
236
+ createEventHandlers
237
237
  };
238
238
  }
package/src/router.ts CHANGED
@@ -225,7 +225,7 @@ export class Router {
225
225
  * linkData.navigate(); // Programmatic navigation
226
226
  *
227
227
  * // Get event handlers for React
228
- * const handlers = linkData.getEventHandlers(name => `on${name.charAt(0).toUpperCase() + name.slice(1)}`);
228
+ * const handlers = linkData.createEventHandlers(name => `on${name.charAt(0).toUpperCase() + name.slice(1)}`);
229
229
  * // handlers.onClick for React
230
230
  * ```
231
231
  */
package/src/scroll.ts ADDED
@@ -0,0 +1,108 @@
1
+ import { isBrowser } from './util';
2
+
3
+ /** Internal {@link ScrollToOptions | `ScrollToOptions`}: `left` and `top` properties always have values */
4
+ interface _ScrollPosition extends ScrollToOptions {
5
+ left: number;
6
+ top: number;
7
+ }
8
+
9
+ export interface ScrollPositionElement extends ScrollToOptions {
10
+ /**
11
+ * A valid CSS selector. Some special characters need to be escaped (https://mathiasbynens.be/notes/css-escapes).
12
+ * @example
13
+ * Here are some examples:
14
+ *
15
+ * - `.title`
16
+ * - `.content:first-child`
17
+ * - `#marker`
18
+ * - `#marker\~with\~symbols`
19
+ * - `#marker.with.dot`: Selects `class="with dot" id="marker"`, not `id="marker.with.dot"`
20
+ *
21
+ */
22
+ el: string | Element;
23
+ }
24
+
25
+ /** Scroll parameters */
26
+ export type ScrollPosition = ScrollToOptions | ScrollPositionElement;
27
+
28
+ /** Get current window scroll position */
29
+ export const winScrollPos = (): _ScrollPosition => ({
30
+ left: window.scrollX,
31
+ top: window.scrollY
32
+ });
33
+
34
+ /** Get element position for scrolling in document */
35
+ function getElementPosition(
36
+ el: Element,
37
+ offset: ScrollToOptions
38
+ ): _ScrollPosition {
39
+ const docRect = document.documentElement.getBoundingClientRect();
40
+ const elRect = el.getBoundingClientRect();
41
+
42
+ return {
43
+ behavior: offset.behavior,
44
+ left: elRect.left - docRect.left - (offset.left || 0),
45
+ top: elRect.top - docRect.top - (offset.top || 0)
46
+ };
47
+ }
48
+
49
+ /** Scroll to specified position */
50
+ export function scrollToPosition(position: ScrollPosition): void {
51
+ if ('el' in position) {
52
+ const positionEl = position.el;
53
+
54
+ const el =
55
+ typeof positionEl === 'string'
56
+ ? document.querySelector(positionEl)
57
+ : positionEl;
58
+
59
+ if (!el) return;
60
+
61
+ position = getElementPosition(el, position);
62
+ }
63
+
64
+ if ('scrollBehavior' in document.documentElement.style) {
65
+ window.scrollTo(position);
66
+ } else {
67
+ window.scrollTo(
68
+ Number.isFinite(position.left) ? position.left! : window.scrollX,
69
+ Number.isFinite(position.top) ? position.top! : window.scrollY
70
+ );
71
+ }
72
+ }
73
+
74
+ /** Stored scroll positions */
75
+ export const scrollPositions = new Map<string, _ScrollPosition>();
76
+
77
+ const POSITION_KEY = '__scroll_position_key';
78
+
79
+ /** Save scroll position */
80
+ export function saveScrollPosition(
81
+ key: string,
82
+ scrollPosition = winScrollPos()
83
+ ) {
84
+ scrollPosition = { ...scrollPosition };
85
+ scrollPositions.set(key, scrollPosition);
86
+
87
+ try {
88
+ if (location.href !== key) return;
89
+ // preserve the existing history state as it could be overridden by the user
90
+ const stateCopy = {
91
+ ...(history.state || {}),
92
+ [POSITION_KEY]: scrollPosition
93
+ };
94
+ history.replaceState(stateCopy, '');
95
+ } catch (error) {}
96
+ }
97
+
98
+ /** Get saved scroll position */
99
+ export function getSavedScrollPosition(
100
+ key: string,
101
+ defaultValue: _ScrollPosition | null = null
102
+ ): _ScrollPosition | null {
103
+ const scroll = scrollPositions.get(key) || history.state[POSITION_KEY];
104
+
105
+ // Saved scroll position should not be used multiple times, next time should use newly saved position
106
+ scrollPositions.delete(key);
107
+ return scroll || defaultValue;
108
+ }
package/src/types.ts CHANGED
@@ -91,6 +91,7 @@ export interface RouteLocation {
91
91
  queryArray?: Record<string, string[] | undefined>;
92
92
  hash?: string;
93
93
  state?: RouteState;
94
+ /** When `true`, maintains current scroll position after navigation (default behavior scrolls to top) */
94
95
  keepScrollPosition?: boolean;
95
96
  statusCode?: number | null;
96
97
  layer?: RouteLayerOptions | null;
@@ -266,6 +267,7 @@ export interface RouterOptions {
266
267
  apps?: RouterMicroApp;
267
268
  normalizeURL?: (to: URL, from: URL | null) => URL;
268
269
  fallback?: RouteHandleHook;
270
+ nextTick?: () => Awaitable<void>;
269
271
 
270
272
  rootStyle?: Partial<CSSStyleDeclaration> | false | null;
271
273
  layer?: boolean;
@@ -306,10 +308,6 @@ export interface RouterLinkAttributes {
306
308
  rel?: string;
307
309
  }
308
310
 
309
- export type RouterLinkEventHandler = <E extends Event = MouseEvent>(
310
- event: E
311
- ) => boolean | void;
312
-
313
311
  /**
314
312
  * Framework-agnostic link configuration interface
315
313
  */
@@ -325,7 +323,15 @@ export interface RouterLinkProps {
325
323
  event?: string | string[];
326
324
  tag?: string;
327
325
  layerOptions?: RouteLayerOptions;
328
- eventHandler?: RouterLinkEventHandler;
326
+ /**
327
+ * Hook function called before navigation occurs
328
+ * @param event - The DOM event that triggered the navigation
329
+ * @param eventName - The name of the event that triggered navigation
330
+ *
331
+ * Can prevent default event to block the default navigation handling logic.
332
+ * Call event.preventDefault() to stop the navigation from proceeding.
333
+ */
334
+ beforeNavigate?: (event: Event, eventName: string) => void;
329
335
  }
330
336
 
331
337
  /**
@@ -344,10 +350,10 @@ export interface RouterLinkResolved {
344
350
  attributes: RouterLinkAttributes;
345
351
 
346
352
  // Navigation function
347
- navigate: (e?: Event) => Promise<void>;
353
+ navigate: (e: Event) => Promise<void>;
348
354
 
349
355
  // Event handling
350
- getEventHandlers: (
351
- nameTransform?: (eventType: string) => string
352
- ) => Record<string, (e: Event) => Promise<void> | undefined>;
356
+ createEventHandlers: (
357
+ format?: (eventType: string) => string
358
+ ) => Record<string, (e: Event) => Promise<void>>;
353
359
  }