@esmx/router 3.0.0-rc.103
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/LICENSE +21 -0
- package/README.md +77 -0
- package/README.zh-CN.md +158 -0
- package/dist/error.d.ts +23 -0
- package/dist/error.mjs +64 -0
- package/dist/increment-id.d.ts +7 -0
- package/dist/increment-id.mjs +16 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.mjs +13 -0
- package/dist/location.d.ts +22 -0
- package/dist/location.mjs +64 -0
- package/dist/matcher.d.ts +4 -0
- package/dist/matcher.mjs +46 -0
- package/dist/micro-app.d.ts +18 -0
- package/dist/micro-app.mjs +85 -0
- package/dist/navigation.d.ts +45 -0
- package/dist/navigation.mjs +153 -0
- package/dist/options.d.ts +4 -0
- package/dist/options.mjs +94 -0
- package/dist/route-task.d.ts +40 -0
- package/dist/route-task.mjs +77 -0
- package/dist/route-transition.d.ts +53 -0
- package/dist/route-transition.mjs +356 -0
- package/dist/route.d.ts +77 -0
- package/dist/route.mjs +223 -0
- package/dist/router-link.d.ts +10 -0
- package/dist/router-link.mjs +139 -0
- package/dist/router.d.ts +122 -0
- package/dist/router.mjs +355 -0
- package/dist/scroll.d.ts +33 -0
- package/dist/scroll.mjs +49 -0
- package/dist/types.d.ts +282 -0
- package/dist/types.mjs +18 -0
- package/dist/util.d.ts +27 -0
- package/dist/util.mjs +67 -0
- package/package.json +62 -0
- package/src/error.ts +84 -0
- package/src/increment-id.ts +12 -0
- package/src/index.ts +67 -0
- package/src/location.ts +124 -0
- package/src/matcher.ts +68 -0
- package/src/micro-app.ts +101 -0
- package/src/navigation.ts +202 -0
- package/src/options.ts +135 -0
- package/src/route-task.ts +102 -0
- package/src/route-transition.ts +472 -0
- package/src/route.ts +335 -0
- package/src/router-link.ts +238 -0
- package/src/router.ts +395 -0
- package/src/scroll.ts +106 -0
- package/src/types.ts +381 -0
- package/src/util.ts +133 -0
package/src/route.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { parseLocation, resolveRouteLocationInput } from './location';
|
|
3
|
+
import { parsedOptions } from './options';
|
|
4
|
+
import type { Router } from './router';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type RouteConfirmHook,
|
|
8
|
+
type RouteHandleHook,
|
|
9
|
+
type RouteHandleResult,
|
|
10
|
+
type RouteLayerOptions,
|
|
11
|
+
type RouteLocationInput,
|
|
12
|
+
type RouteMatchResult,
|
|
13
|
+
type RouteMeta,
|
|
14
|
+
type RouteOptions,
|
|
15
|
+
type RouteParsedConfig,
|
|
16
|
+
type RouterParsedOptions,
|
|
17
|
+
type RouteState,
|
|
18
|
+
RouteType
|
|
19
|
+
} from './types';
|
|
20
|
+
import { decodeParams, isNonEmptyPlainObject, isPlainObject } from './util';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configuration for non-enumerable properties in Route class
|
|
24
|
+
* These properties will be hidden during object traversal and serialization
|
|
25
|
+
*/
|
|
26
|
+
export const NON_ENUMERABLE_PROPERTIES = [
|
|
27
|
+
// Private fields - internal implementation details
|
|
28
|
+
'_handled',
|
|
29
|
+
'_handle',
|
|
30
|
+
'_handleResult',
|
|
31
|
+
'_options',
|
|
32
|
+
|
|
33
|
+
// SSR-specific properties - meaningless in client environment
|
|
34
|
+
'req',
|
|
35
|
+
'res',
|
|
36
|
+
|
|
37
|
+
// Internal context - used by framework internally
|
|
38
|
+
'context',
|
|
39
|
+
|
|
40
|
+
// Status code - internal status information
|
|
41
|
+
'statusCode',
|
|
42
|
+
|
|
43
|
+
// Route behavior overrides - framework internal logic
|
|
44
|
+
'confirm',
|
|
45
|
+
|
|
46
|
+
// Layer configuration - used for layer routes
|
|
47
|
+
'layer'
|
|
48
|
+
] satisfies string[];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Append user-provided parameters to URL path
|
|
52
|
+
* @param match Route matching result
|
|
53
|
+
* @param toInput User-provided route location object
|
|
54
|
+
* @param base Base URL
|
|
55
|
+
* @param to Current parsed URL object
|
|
56
|
+
*/
|
|
57
|
+
export function applyRouteParams(
|
|
58
|
+
match: RouteMatchResult,
|
|
59
|
+
toInput: RouteLocationInput,
|
|
60
|
+
base: URL,
|
|
61
|
+
to: URL
|
|
62
|
+
): void {
|
|
63
|
+
if (
|
|
64
|
+
!isPlainObject(toInput) ||
|
|
65
|
+
!isNonEmptyPlainObject(toInput.params) ||
|
|
66
|
+
!match.matches.length
|
|
67
|
+
) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Get the last matched route configuration
|
|
72
|
+
const lastMatch = match.matches[match.matches.length - 1];
|
|
73
|
+
|
|
74
|
+
// Split current path
|
|
75
|
+
const current = to.pathname.split('/');
|
|
76
|
+
|
|
77
|
+
// Compile new path with user parameters and split
|
|
78
|
+
const next = new URL(
|
|
79
|
+
lastMatch.compile(toInput.params).substring(1),
|
|
80
|
+
base
|
|
81
|
+
).pathname.split('/');
|
|
82
|
+
|
|
83
|
+
// Replace current path segments with new path segments
|
|
84
|
+
next.forEach((item, index) => {
|
|
85
|
+
current[index] = item || current[index];
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Update URL path
|
|
89
|
+
to.pathname = current.join('/');
|
|
90
|
+
|
|
91
|
+
// Merge parameters to match result, user parameters take precedence
|
|
92
|
+
Object.assign(match.params, toInput.params);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Route class provides complete route object functionality
|
|
97
|
+
*/
|
|
98
|
+
export class Route {
|
|
99
|
+
// Private fields for handle validation
|
|
100
|
+
private _handled = false;
|
|
101
|
+
private _handle: RouteHandleHook | null = null;
|
|
102
|
+
private _handleResult: RouteHandleResult | null = null;
|
|
103
|
+
private readonly _options: RouterParsedOptions;
|
|
104
|
+
|
|
105
|
+
// Public properties
|
|
106
|
+
public readonly statusCode: number | null = null;
|
|
107
|
+
public readonly state: RouteState;
|
|
108
|
+
public readonly keepScrollPosition: boolean;
|
|
109
|
+
/** Custom confirm handler that overrides default route-transition confirm logic */
|
|
110
|
+
public readonly confirm: RouteConfirmHook | null;
|
|
111
|
+
/** Layer configuration for layer routes */
|
|
112
|
+
public readonly layer: RouteLayerOptions | null;
|
|
113
|
+
|
|
114
|
+
// Read-only properties
|
|
115
|
+
public readonly type: RouteType;
|
|
116
|
+
public readonly req: IncomingMessage | null;
|
|
117
|
+
public readonly res: ServerResponse | null;
|
|
118
|
+
public readonly context: Record<string | symbol, any>;
|
|
119
|
+
public readonly url: URL;
|
|
120
|
+
public readonly path: string;
|
|
121
|
+
public readonly fullPath: string;
|
|
122
|
+
public readonly hash: string;
|
|
123
|
+
public readonly params: Record<string, string> = {};
|
|
124
|
+
public readonly paramsArray: Record<string, string[]> = {};
|
|
125
|
+
public readonly query: Record<string, string | undefined> = {};
|
|
126
|
+
public readonly queryArray: Record<string, string[] | undefined> = {};
|
|
127
|
+
public readonly meta: RouteMeta;
|
|
128
|
+
public readonly matched: readonly RouteParsedConfig[];
|
|
129
|
+
public readonly config: RouteParsedConfig | null;
|
|
130
|
+
|
|
131
|
+
/** @deprecated Use `url.pathname` instead. */
|
|
132
|
+
public get pathname(): string {
|
|
133
|
+
return this.url.pathname;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @deprecated Use `url.href` instead. */
|
|
137
|
+
public get href(): string {
|
|
138
|
+
return this.url.href;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
constructor(routeOptions: Partial<RouteOptions> = {}) {
|
|
142
|
+
const {
|
|
143
|
+
toType = RouteType.push,
|
|
144
|
+
from = null,
|
|
145
|
+
options = parsedOptions()
|
|
146
|
+
} = routeOptions;
|
|
147
|
+
|
|
148
|
+
this._options = options;
|
|
149
|
+
this.type = toType;
|
|
150
|
+
this.req = options.req;
|
|
151
|
+
this.res = options.res;
|
|
152
|
+
this.context = options.context;
|
|
153
|
+
|
|
154
|
+
const base = options.base;
|
|
155
|
+
const toInput = resolveRouteLocationInput(routeOptions.toInput, from);
|
|
156
|
+
const to = options.normalizeURL(parseLocation(toInput, base), from);
|
|
157
|
+
let match: RouteMatchResult | null = null;
|
|
158
|
+
|
|
159
|
+
// Check if URL origin matches base origin (protocol + hostname + port)
|
|
160
|
+
// If origins don't match, treat as external URL and don't attempt route matching
|
|
161
|
+
if (
|
|
162
|
+
to.origin === base.origin &&
|
|
163
|
+
to.pathname.startsWith(base.pathname)
|
|
164
|
+
) {
|
|
165
|
+
const isLayer = toType === RouteType.pushLayer || options.layer;
|
|
166
|
+
match = options.matcher(to, base, (config) => {
|
|
167
|
+
if (isLayer) {
|
|
168
|
+
return config.layer !== false;
|
|
169
|
+
}
|
|
170
|
+
return config.layer !== true;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (match) {
|
|
175
|
+
applyRouteParams(match, toInput, base, to);
|
|
176
|
+
|
|
177
|
+
const decodedParams = decodeParams(match.params);
|
|
178
|
+
|
|
179
|
+
for (const key in decodedParams) {
|
|
180
|
+
const value = decodedParams[key];
|
|
181
|
+
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
this.params[key] = value[0] || '';
|
|
184
|
+
this.paramsArray[key] = value;
|
|
185
|
+
} else {
|
|
186
|
+
this.params[key] = value;
|
|
187
|
+
this.paramsArray[key] = [value];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.url = to;
|
|
193
|
+
this.path = match
|
|
194
|
+
? to.pathname.substring(base.pathname.length - 1)
|
|
195
|
+
: to.pathname;
|
|
196
|
+
this.fullPath = (match ? this.path : to.pathname) + to.search + to.hash;
|
|
197
|
+
this.matched = match ? match.matches : Object.freeze([]);
|
|
198
|
+
this.keepScrollPosition = Boolean(toInput.keepScrollPosition);
|
|
199
|
+
this.confirm = toInput.confirm || null;
|
|
200
|
+
this.layer =
|
|
201
|
+
toType === RouteType.pushLayer && toInput.layer
|
|
202
|
+
? toInput.layer
|
|
203
|
+
: null;
|
|
204
|
+
this.config =
|
|
205
|
+
this.matched.length > 0
|
|
206
|
+
? this.matched[this.matched.length - 1]
|
|
207
|
+
: null;
|
|
208
|
+
this.meta = this.config?.meta || {};
|
|
209
|
+
|
|
210
|
+
const state: RouteState = {};
|
|
211
|
+
if (toInput.state) {
|
|
212
|
+
Object.assign(state, toInput.state);
|
|
213
|
+
}
|
|
214
|
+
this.state = state;
|
|
215
|
+
|
|
216
|
+
for (const key of new Set(to.searchParams.keys())) {
|
|
217
|
+
this.query[key] = to.searchParams.get(key)!;
|
|
218
|
+
this.queryArray[key] = to.searchParams.getAll(key);
|
|
219
|
+
}
|
|
220
|
+
this.hash = to.hash;
|
|
221
|
+
|
|
222
|
+
// Set status code
|
|
223
|
+
// Prioritize user-provided statusCode
|
|
224
|
+
if (typeof toInput.statusCode === 'number') {
|
|
225
|
+
this.statusCode = toInput.statusCode;
|
|
226
|
+
}
|
|
227
|
+
// If statusCode is not provided, keep default null value
|
|
228
|
+
|
|
229
|
+
// Configure property enumerability
|
|
230
|
+
// Set internal implementation details as non-enumerable, keep user-common properties enumerable
|
|
231
|
+
// Set specified properties as non-enumerable according to configuration
|
|
232
|
+
for (const property of NON_ENUMERABLE_PROPERTIES) {
|
|
233
|
+
Object.defineProperty(this, property, { enumerable: false });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
get isPush(): boolean {
|
|
238
|
+
return this.type.startsWith('push');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// handle related getter/setter
|
|
242
|
+
get handle(): RouteHandleHook | null {
|
|
243
|
+
return this._handle;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
set handle(val: RouteHandleHook | null) {
|
|
247
|
+
this.setHandle(val);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get handleResult(): RouteHandleResult | null {
|
|
251
|
+
return this._handleResult;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
set handleResult(val: RouteHandleResult | null) {
|
|
255
|
+
this._handleResult = val;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Set handle function with validation logic wrapper
|
|
260
|
+
*/
|
|
261
|
+
setHandle(val: RouteHandleHook | null): void {
|
|
262
|
+
if (typeof val !== 'function') {
|
|
263
|
+
this._handle = null;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const self = this;
|
|
267
|
+
this._handle = function handle(
|
|
268
|
+
this: Route,
|
|
269
|
+
to: Route,
|
|
270
|
+
from: Route | null,
|
|
271
|
+
router: Router
|
|
272
|
+
) {
|
|
273
|
+
if (self._handled) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
'Route handle hook can only be called once per navigation'
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
self._handled = true;
|
|
279
|
+
return val.call(this, to, from, router);
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Apply navigation-generated state to current route
|
|
285
|
+
* Used by route handlers to add system state like pageId
|
|
286
|
+
* @param navigationState Navigation-generated state to apply
|
|
287
|
+
*/
|
|
288
|
+
applyNavigationState(navigationState: Partial<RouteState>): void {
|
|
289
|
+
Object.assign(this.state, navigationState);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Sync all properties of current route to target route object
|
|
294
|
+
* Used for route object updates in reactive systems
|
|
295
|
+
* @param targetRoute Target route object
|
|
296
|
+
*/
|
|
297
|
+
syncTo(targetRoute: Route): void {
|
|
298
|
+
// Copy enumerable properties
|
|
299
|
+
Object.assign(targetRoute, this);
|
|
300
|
+
|
|
301
|
+
// Copy non-enumerable properties - type-safe property copying
|
|
302
|
+
for (const property of NON_ENUMERABLE_PROPERTIES) {
|
|
303
|
+
if (!(property in this && property in targetRoute)) continue;
|
|
304
|
+
// Use Reflect.set for type-safe property setting
|
|
305
|
+
const value = Reflect.get(this, property);
|
|
306
|
+
Reflect.set(targetRoute, property, value);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clone current route instance
|
|
312
|
+
* Returns a new Route instance with same configuration and state
|
|
313
|
+
*/
|
|
314
|
+
clone(): Route {
|
|
315
|
+
// Reconstruct route object, passing current state and confirm handler
|
|
316
|
+
const toInput: RouteLocationInput = {
|
|
317
|
+
path: this.fullPath,
|
|
318
|
+
state: { ...this.state },
|
|
319
|
+
...(this.confirm && { confirm: this.confirm }),
|
|
320
|
+
...(this.layer && { layer: this.layer }),
|
|
321
|
+
...(this.statusCode !== null && { statusCode: this.statusCode })
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// Get original options from constructor's finalOptions
|
|
325
|
+
const options = this._options;
|
|
326
|
+
|
|
327
|
+
const clonedRoute = new Route({
|
|
328
|
+
options,
|
|
329
|
+
toType: this.type,
|
|
330
|
+
toInput
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return clonedRoute;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { Router } from './router';
|
|
2
|
+
import type {
|
|
3
|
+
RouterLinkAttributes,
|
|
4
|
+
RouterLinkProps,
|
|
5
|
+
RouterLinkResolved,
|
|
6
|
+
RouterLinkType
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
// Constants definition
|
|
10
|
+
const CSS_CLASSES = {
|
|
11
|
+
BASE: 'router-link',
|
|
12
|
+
ACTIVE: 'router-link-active',
|
|
13
|
+
EXACT_ACTIVE: 'router-link-exact-active'
|
|
14
|
+
} satisfies Record<string, string>;
|
|
15
|
+
/**
|
|
16
|
+
* Normalize navigation type with backward compatibility for deprecated replace property
|
|
17
|
+
*/
|
|
18
|
+
function normalizeNavigationType(props: RouterLinkProps): RouterLinkType {
|
|
19
|
+
if (props.replace) {
|
|
20
|
+
console.warn(
|
|
21
|
+
'[RouterLink] The `replace` property is deprecated and will be removed in a future version.\n' +
|
|
22
|
+
'Please use `type="replace"` instead.\n' +
|
|
23
|
+
'Before: <RouterLink replace={true} />\n' +
|
|
24
|
+
'After: <RouterLink type="replace" />'
|
|
25
|
+
);
|
|
26
|
+
return 'replace';
|
|
27
|
+
}
|
|
28
|
+
return props.type || 'push';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get event type list - normalize and validate event types
|
|
33
|
+
*/
|
|
34
|
+
function getEventTypeList(eventType: unknown | unknown[]): string[] {
|
|
35
|
+
const events = Array.isArray(eventType) ? eventType : [eventType];
|
|
36
|
+
const validEvents = events
|
|
37
|
+
.filter((type): type is string => typeof type === 'string')
|
|
38
|
+
.map((type) => type.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
return validEvents.length ? validEvents : ['click'];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Navigation event handler called before navigation - determines if the router should handle the navigation
|
|
45
|
+
*
|
|
46
|
+
* Returns false: Let browser handle default behavior (normal link navigation)
|
|
47
|
+
* Returns true: Router takes over navigation, prevents default browser behavior
|
|
48
|
+
*
|
|
49
|
+
* This function intelligently decides when to let the browser handle clicks
|
|
50
|
+
* (like Ctrl+click for new tabs) vs when to use SPA routing
|
|
51
|
+
*/
|
|
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?.();
|
|
60
|
+
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Execute route navigation
|
|
66
|
+
*/
|
|
67
|
+
async function executeNavigation(
|
|
68
|
+
router: Router,
|
|
69
|
+
props: RouterLinkProps,
|
|
70
|
+
linkType: RouterLinkType
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const { to, layerOptions } = props;
|
|
73
|
+
|
|
74
|
+
switch (linkType) {
|
|
75
|
+
case 'push':
|
|
76
|
+
await router.push(to);
|
|
77
|
+
break;
|
|
78
|
+
case 'replace':
|
|
79
|
+
await router.replace(to);
|
|
80
|
+
break;
|
|
81
|
+
case 'pushWindow':
|
|
82
|
+
await router.pushWindow(to);
|
|
83
|
+
break;
|
|
84
|
+
case 'replaceWindow':
|
|
85
|
+
await router.replaceWindow(to);
|
|
86
|
+
break;
|
|
87
|
+
case 'pushLayer':
|
|
88
|
+
await router.pushLayer(
|
|
89
|
+
layerOptions
|
|
90
|
+
? typeof to === 'string'
|
|
91
|
+
? { path: to, layer: layerOptions }
|
|
92
|
+
: { ...to, layer: layerOptions }
|
|
93
|
+
: to
|
|
94
|
+
);
|
|
95
|
+
break;
|
|
96
|
+
default:
|
|
97
|
+
await router.push(to);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create navigation function
|
|
103
|
+
*/
|
|
104
|
+
function createNavigateFunction(
|
|
105
|
+
router: Router,
|
|
106
|
+
props: RouterLinkProps,
|
|
107
|
+
navigationType: RouterLinkType
|
|
108
|
+
): RouterLinkResolved['navigate'] {
|
|
109
|
+
return async (e: Event): Promise<void> => {
|
|
110
|
+
if (shouldHandleNavigation(e)) {
|
|
111
|
+
await executeNavigation(router, props, navigationType);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Compute HTML attributes
|
|
118
|
+
*/
|
|
119
|
+
function computeAttributes(
|
|
120
|
+
href: string,
|
|
121
|
+
navigationType: RouterLinkType,
|
|
122
|
+
isExternal: boolean,
|
|
123
|
+
isActive: boolean,
|
|
124
|
+
isExactActive: boolean,
|
|
125
|
+
activeClass?: string
|
|
126
|
+
): RouterLinkAttributes {
|
|
127
|
+
// Only pushWindow opens in a new window, replaceWindow replaces current window
|
|
128
|
+
const isNewWindow = navigationType === 'pushWindow';
|
|
129
|
+
|
|
130
|
+
// Build CSS classes
|
|
131
|
+
const classes: string[] = [CSS_CLASSES.BASE];
|
|
132
|
+
if (isActive) {
|
|
133
|
+
classes.push(activeClass || CSS_CLASSES.ACTIVE);
|
|
134
|
+
}
|
|
135
|
+
if (isExactActive) {
|
|
136
|
+
classes.push(CSS_CLASSES.EXACT_ACTIVE);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const attributes: RouterLinkAttributes = {
|
|
140
|
+
href,
|
|
141
|
+
class: classes.join(' ')
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Set target for new window
|
|
145
|
+
if (isNewWindow) {
|
|
146
|
+
attributes.target = '_blank';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Build rel attribute
|
|
150
|
+
const relParts: string[] = [];
|
|
151
|
+
if (isNewWindow) {
|
|
152
|
+
relParts.push('noopener', 'noreferrer');
|
|
153
|
+
}
|
|
154
|
+
if (isExternal) {
|
|
155
|
+
relParts.push('external', 'nofollow');
|
|
156
|
+
}
|
|
157
|
+
if (relParts.length > 0) {
|
|
158
|
+
attributes.rel = relParts.join(' ');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return attributes;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create event handlers generator function
|
|
166
|
+
*/
|
|
167
|
+
function createEventHandlersGenerator(
|
|
168
|
+
router: Router,
|
|
169
|
+
props: RouterLinkProps,
|
|
170
|
+
navigationType: RouterLinkType,
|
|
171
|
+
eventTypes: string[]
|
|
172
|
+
): RouterLinkResolved['createEventHandlers'] {
|
|
173
|
+
return (format?: (eventType: string) => string) => {
|
|
174
|
+
const handlers: Record<string, (e: Event) => Promise<void>> = {};
|
|
175
|
+
const navigate = createNavigateFunction(router, props, navigationType);
|
|
176
|
+
|
|
177
|
+
eventTypes.forEach((eventType) => {
|
|
178
|
+
const eventName = format?.(eventType) ?? eventType.toLowerCase();
|
|
179
|
+
handlers[eventName] = (event) => {
|
|
180
|
+
props.beforeNavigate?.(event, eventType);
|
|
181
|
+
return navigate(event);
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return handlers;
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Framework-agnostic link resolver
|
|
191
|
+
*
|
|
192
|
+
* @param router Router instance
|
|
193
|
+
* @param props Link configuration
|
|
194
|
+
* @returns Resolution result
|
|
195
|
+
*/
|
|
196
|
+
export function createLinkResolver(
|
|
197
|
+
router: Router,
|
|
198
|
+
props: RouterLinkProps
|
|
199
|
+
): RouterLinkResolved {
|
|
200
|
+
const route = router.resolve(props.to);
|
|
201
|
+
const type = normalizeNavigationType(props);
|
|
202
|
+
const href = route.url.href;
|
|
203
|
+
|
|
204
|
+
const isActive = router.isRouteMatched(route, props.exact);
|
|
205
|
+
const isExactActive = router.isRouteMatched(route, 'exact');
|
|
206
|
+
const isExternal = route.url.origin !== router.route.url.origin;
|
|
207
|
+
|
|
208
|
+
const attributes = computeAttributes(
|
|
209
|
+
href,
|
|
210
|
+
type,
|
|
211
|
+
isExternal,
|
|
212
|
+
isActive,
|
|
213
|
+
isExactActive,
|
|
214
|
+
props.activeClass
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const eventTypes = getEventTypeList(props.event || 'click');
|
|
218
|
+
const createEventHandlers = createEventHandlersGenerator(
|
|
219
|
+
router,
|
|
220
|
+
props,
|
|
221
|
+
type,
|
|
222
|
+
eventTypes
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const navigate = createNavigateFunction(router, props, type);
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
route,
|
|
229
|
+
type,
|
|
230
|
+
isActive,
|
|
231
|
+
isExactActive,
|
|
232
|
+
isExternal,
|
|
233
|
+
tag: props.tag || 'a',
|
|
234
|
+
attributes,
|
|
235
|
+
navigate,
|
|
236
|
+
createEventHandlers
|
|
237
|
+
};
|
|
238
|
+
}
|