@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 +5 -3
- package/dist/route-task.mjs +1 -4
- package/dist/route-transition.mjs +44 -0
- package/dist/router-link.mjs +28 -20
- package/dist/router.d.ts +1 -1
- package/dist/router.mjs +1 -1
- package/dist/scroll.d.ts +33 -0
- package/dist/scroll.mjs +49 -0
- package/dist/types.d.ts +13 -4
- package/package.json +2 -2
- package/src/options.ts +1 -0
- package/src/route-task.ts +1 -4
- package/src/route-transition.ts +47 -0
- package/src/router-link.ts +36 -36
- package/src/router.ts +1 -1
- package/src/scroll.ts +108 -0
- package/src/types.ts +15 -9
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
|
-
|
|
53
|
+
nextTick: (_e = options.nextTick) != null ? _e : () => {
|
|
54
54
|
},
|
|
55
|
-
|
|
55
|
+
handleBackBoundary: (_f = options.handleBackBoundary) != null ? _f : () => {
|
|
56
|
+
},
|
|
57
|
+
handleLayerClose: (_g = options.handleLayerClose) != null ? _g : () => {
|
|
56
58
|
}
|
|
57
59
|
});
|
|
58
60
|
}
|
package/dist/route-task.mjs
CHANGED
|
@@ -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;
|
package/dist/router-link.mjs
CHANGED
|
@@ -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
|
|
21
|
-
var _a
|
|
22
|
-
if (
|
|
23
|
-
if (e
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
92
|
-
return (
|
|
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 =
|
|
97
|
-
handlers[eventName] =
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
203
|
+
* const handlers = linkData.createEventHandlers(name => `on${name.charAt(0).toUpperCase() + name.slice(1)}`);
|
|
204
204
|
* // handlers.onClick for React
|
|
205
205
|
* ```
|
|
206
206
|
*/
|
package/dist/scroll.d.ts
ADDED
|
@@ -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 {};
|
package/dist/scroll.mjs
ADDED
|
@@ -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
|
-
|
|
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
|
|
250
|
-
|
|
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.
|
|
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": "
|
|
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
package/src/route-transition.ts
CHANGED
|
@@ -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;
|
package/src/router-link.ts
CHANGED
|
@@ -41,29 +41,22 @@ function getEventTypeList(eventType: unknown | unknown[]): string[] {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Navigation event handler called before navigation - determines if the router should handle the navigation
|
|
45
45
|
*
|
|
46
|
-
* Returns
|
|
47
|
-
* Returns
|
|
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
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
):
|
|
116
|
-
return async (e
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
168
|
+
router: Router,
|
|
169
|
+
props: RouterLinkProps,
|
|
170
|
+
navigationType: RouterLinkType,
|
|
177
171
|
eventTypes: string[]
|
|
178
|
-
):
|
|
179
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
353
|
+
navigate: (e: Event) => Promise<void>;
|
|
348
354
|
|
|
349
355
|
// Event handling
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
) => Record<string, (e: Event) => Promise<void
|
|
356
|
+
createEventHandlers: (
|
|
357
|
+
format?: (eventType: string) => string
|
|
358
|
+
) => Record<string, (e: Event) => Promise<void>>;
|
|
353
359
|
}
|