@symbo.ls/router 3.5.1 → 3.14.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/LICENSE +34 -0
- package/README.md +234 -0
- package/dist/cjs/index.js +281 -0
- package/dist/esm/index.js +261 -0
- package/dist/iife/index.js +754 -0
- package/index.js +186 -260
- package/package.json +32 -7
- package/location.js +0 -3
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { document, window, triggerEventOn } from "@symbo.ls/utils";
|
|
2
|
+
const paramPattern = /^:(.+)/;
|
|
3
|
+
const wildcardPattern = /^\*$/;
|
|
4
|
+
const routeCache = /* @__PURE__ */ new Map();
|
|
5
|
+
const parseRoutePattern = (pattern) => {
|
|
6
|
+
const cached = routeCache.get(pattern);
|
|
7
|
+
if (cached) return cached;
|
|
8
|
+
const segments = pattern.replace(/^\//, "").split("/");
|
|
9
|
+
const params = [];
|
|
10
|
+
let hasWildcard = false;
|
|
11
|
+
for (let i = 0; i < segments.length; i++) {
|
|
12
|
+
const match = segments[i].match(paramPattern);
|
|
13
|
+
if (match) {
|
|
14
|
+
params.push({ index: i, name: match[1] });
|
|
15
|
+
} else if (wildcardPattern.test(segments[i])) {
|
|
16
|
+
hasWildcard = true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const result = { segments, params, hasWildcard, pattern };
|
|
20
|
+
routeCache.set(pattern, result);
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
const matchRoute = (pathname, routes, level = 0) => {
|
|
24
|
+
const pathSegments = pathname.replace(/^\//, "").split("/").filter(Boolean);
|
|
25
|
+
const relevantSegments = pathSegments.slice(level);
|
|
26
|
+
const routePath = "/" + (relevantSegments[0] || "");
|
|
27
|
+
let bestMatch = null;
|
|
28
|
+
let bestScore = -1;
|
|
29
|
+
let matchedParams = {};
|
|
30
|
+
for (const key in routes) {
|
|
31
|
+
if (key === "/*") continue;
|
|
32
|
+
const parsed = parseRoutePattern(key);
|
|
33
|
+
const score = scoreMatch(relevantSegments, parsed);
|
|
34
|
+
if (score > bestScore) {
|
|
35
|
+
bestScore = score;
|
|
36
|
+
bestMatch = key;
|
|
37
|
+
matchedParams = extractParams(relevantSegments, parsed);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!bestMatch && routes["/*"]) {
|
|
41
|
+
bestMatch = "/*";
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
key: bestMatch,
|
|
45
|
+
content: bestMatch ? routes[bestMatch] : null,
|
|
46
|
+
params: matchedParams,
|
|
47
|
+
routePath
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const scoreMatch = (pathSegments, parsed) => {
|
|
51
|
+
const { segments, hasWildcard } = parsed;
|
|
52
|
+
if (!hasWildcard && segments.length !== pathSegments.length && segments.length !== 1) {
|
|
53
|
+
if (segments.length > pathSegments.length) return -1;
|
|
54
|
+
}
|
|
55
|
+
let score = 0;
|
|
56
|
+
const len = Math.min(segments.length, pathSegments.length);
|
|
57
|
+
for (let i = 0; i < len; i++) {
|
|
58
|
+
if (segments[i] === pathSegments[i]) {
|
|
59
|
+
score += 3;
|
|
60
|
+
} else if (paramPattern.test(segments[i])) {
|
|
61
|
+
score += 1;
|
|
62
|
+
} else if (wildcardPattern.test(segments[i])) {
|
|
63
|
+
score += 0.5;
|
|
64
|
+
} else {
|
|
65
|
+
return -1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return score;
|
|
69
|
+
};
|
|
70
|
+
const extractParams = (pathSegments, parsed) => {
|
|
71
|
+
const params = {};
|
|
72
|
+
for (const { index, name } of parsed.params) {
|
|
73
|
+
if (pathSegments[index]) {
|
|
74
|
+
params[name] = decodeURIComponent(pathSegments[index]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return params;
|
|
78
|
+
};
|
|
79
|
+
const parseQuery = (search) => {
|
|
80
|
+
if (!search || search === "?") return {};
|
|
81
|
+
const params = {};
|
|
82
|
+
const searchParams = new URLSearchParams(search);
|
|
83
|
+
searchParams.forEach((value, key) => {
|
|
84
|
+
if (params[key] !== void 0) {
|
|
85
|
+
if (!Array.isArray(params[key])) params[key] = [params[key]];
|
|
86
|
+
params[key].push(value);
|
|
87
|
+
} else {
|
|
88
|
+
params[key] = value;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return params;
|
|
92
|
+
};
|
|
93
|
+
const runGuards = async (guards, context) => {
|
|
94
|
+
if (!guards || !guards.length) return true;
|
|
95
|
+
for (const guard of guards) {
|
|
96
|
+
const result = await guard(context);
|
|
97
|
+
if (result === false) return false;
|
|
98
|
+
if (typeof result === "string") return result;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
102
|
+
const getActiveRoute = (level = 0, route) => {
|
|
103
|
+
if (!route) route = typeof window !== "undefined" ? window.location.pathname : "/";
|
|
104
|
+
const routeArray = route.split("/");
|
|
105
|
+
const activeRoute = routeArray[level + 1];
|
|
106
|
+
if (activeRoute) return `/${activeRoute}`;
|
|
107
|
+
};
|
|
108
|
+
let lastPathname;
|
|
109
|
+
let lastLevel = 0;
|
|
110
|
+
const normalizePath = (p) => !p || p === "srcdoc" || p === "about:srcdoc" ? "/" : p;
|
|
111
|
+
const defaultOptions = {
|
|
112
|
+
level: lastLevel,
|
|
113
|
+
pushState: true,
|
|
114
|
+
initialRender: false,
|
|
115
|
+
scrollToTop: true,
|
|
116
|
+
scrollToNode: false,
|
|
117
|
+
scrollNode: document && document.documentElement,
|
|
118
|
+
scrollBody: false,
|
|
119
|
+
useFragment: false,
|
|
120
|
+
updateState: true,
|
|
121
|
+
scrollToOffset: 0,
|
|
122
|
+
contentElementKey: "content",
|
|
123
|
+
scrollToOptions: { behavior: "smooth" },
|
|
124
|
+
useParamsMatching: true
|
|
125
|
+
};
|
|
126
|
+
const router = async (path, el, state = {}, options = {}) => {
|
|
127
|
+
const element = el || void 0;
|
|
128
|
+
const win = element?.context?.window || window;
|
|
129
|
+
const doc = element?.context?.document || document;
|
|
130
|
+
const opts = {
|
|
131
|
+
...defaultOptions,
|
|
132
|
+
...element.context.routerOptions,
|
|
133
|
+
...options
|
|
134
|
+
};
|
|
135
|
+
lastLevel = opts.lastLevel;
|
|
136
|
+
const ref = element.__ref;
|
|
137
|
+
if (opts.contentElementKey !== "content" && opts.contentElementKey !== ref.contentElementKey || !ref.contentElementKey) {
|
|
138
|
+
ref.contentElementKey = opts.contentElementKey || "content";
|
|
139
|
+
}
|
|
140
|
+
const contentElementKey = ref.contentElementKey || opts.contentElementKey || "content";
|
|
141
|
+
const origin = win.location.origin !== "null" ? win.location.origin : "http://localhost";
|
|
142
|
+
path = normalizePath(path);
|
|
143
|
+
const urlObj = new win.URL(origin + path);
|
|
144
|
+
const { pathname, search, hash } = urlObj;
|
|
145
|
+
const query = parseQuery(search);
|
|
146
|
+
const rootNode = element.node;
|
|
147
|
+
const hashChanged = hash && hash !== win.location.hash.slice(1);
|
|
148
|
+
const pathChanged = pathname !== lastPathname;
|
|
149
|
+
lastPathname = pathname;
|
|
150
|
+
let route, content, params;
|
|
151
|
+
if (opts.useParamsMatching) {
|
|
152
|
+
const match = matchRoute(pathname, element.routes, opts.level);
|
|
153
|
+
route = match.routePath;
|
|
154
|
+
content = match.content;
|
|
155
|
+
params = match.params;
|
|
156
|
+
} else {
|
|
157
|
+
route = getActiveRoute(opts.level, pathname);
|
|
158
|
+
content = element.routes[route || "/"] || element.routes["/*"];
|
|
159
|
+
params = {};
|
|
160
|
+
}
|
|
161
|
+
const scrollNode = opts.scrollToNode ? rootNode : opts.scrollNode;
|
|
162
|
+
if (element.state?.root?.debugging) {
|
|
163
|
+
element.state.root.debugging = false;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!content) {
|
|
167
|
+
if (opts.onNotFound) {
|
|
168
|
+
opts.onNotFound({ pathname, route, element });
|
|
169
|
+
} else if (!opts.silent) {
|
|
170
|
+
console.warn("[smbls/router] no content matched for path", pathname, "\u2014 available routes:", Object.keys(element.routes || {}));
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (opts.guards && opts.guards.length) {
|
|
175
|
+
const guardContext = { pathname, route, params, query, hash, element, state };
|
|
176
|
+
const guardResult = await runGuards(opts.guards, guardContext);
|
|
177
|
+
if (guardResult === false) return;
|
|
178
|
+
if (typeof guardResult === "string") {
|
|
179
|
+
return router(guardResult, el, state, { ...options, guards: [] });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (opts.pushState) {
|
|
183
|
+
try {
|
|
184
|
+
win.history.pushState(state, null, pathname + (search || "") + (hash || ""));
|
|
185
|
+
} catch (e) {
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (pathChanged || !hashChanged) {
|
|
189
|
+
const stateUpdate = { route, hash, debugging: false };
|
|
190
|
+
if (Object.keys(params).length) stateUpdate.params = params;
|
|
191
|
+
if (Object.keys(query).length) stateUpdate.query = query;
|
|
192
|
+
if (opts.updateState) {
|
|
193
|
+
element.state.update(
|
|
194
|
+
stateUpdate,
|
|
195
|
+
{ preventContentUpdate: true }
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (contentElementKey && opts.removeOldElement) {
|
|
199
|
+
element[contentElementKey].remove();
|
|
200
|
+
}
|
|
201
|
+
const originContent = element.__ref?.origin?.content;
|
|
202
|
+
const contentStyles = {};
|
|
203
|
+
if (originContent) {
|
|
204
|
+
for (const k in originContent) {
|
|
205
|
+
const v = originContent[k];
|
|
206
|
+
if (k === "__ref" || k === "props" || k === "node" || k === "parent" || k === "key") continue;
|
|
207
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean" || typeof v === "object" && v !== null && !v.node && !v.__ref) {
|
|
208
|
+
contentStyles[k] = v;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const nextContent = {
|
|
213
|
+
...contentStyles,
|
|
214
|
+
...typeof content === "object" ? content : { extends: content }
|
|
215
|
+
};
|
|
216
|
+
if (opts.useFragment) nextContent.tag = "fragment";
|
|
217
|
+
try {
|
|
218
|
+
element.set(nextContent, { contentElementKey });
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error("[smbls/router] failed to render route content", pathname, err);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (opts.scrollToTop && scrollNode?.scrollTo) {
|
|
224
|
+
scrollNode.scrollTo({
|
|
225
|
+
...opts.scrollToOptions || {},
|
|
226
|
+
top: 0,
|
|
227
|
+
left: 0
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (opts.scrollToNode && content[contentElementKey]?.node?.scrollTo) {
|
|
231
|
+
content[contentElementKey].node.scrollTo({
|
|
232
|
+
...opts.scrollToOptions || {},
|
|
233
|
+
top: 0,
|
|
234
|
+
left: 0
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (hash) {
|
|
238
|
+
const activeNode = doc.getElementById(hash);
|
|
239
|
+
if (activeNode && scrollNode?.scrollTo) {
|
|
240
|
+
const top = activeNode.getBoundingClientRect().top + rootNode.scrollTop - (opts.scrollToOffset || 0);
|
|
241
|
+
scrollNode.scrollTo({
|
|
242
|
+
...opts.scrollToOptions || {},
|
|
243
|
+
top,
|
|
244
|
+
left: 0
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
triggerEventOn("routeChanged", element, opts);
|
|
249
|
+
};
|
|
250
|
+
var index_default = router;
|
|
251
|
+
export {
|
|
252
|
+
index_default as default,
|
|
253
|
+
getActiveRoute,
|
|
254
|
+
lastLevel,
|
|
255
|
+
lastPathname,
|
|
256
|
+
matchRoute,
|
|
257
|
+
parseQuery,
|
|
258
|
+
parseRoutePattern,
|
|
259
|
+
router,
|
|
260
|
+
runGuards
|
|
261
|
+
};
|